mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #608 from Xhichn/main
Add basic support for hoyoverse account
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/
|
||||
@@ -11,7 +11,7 @@ using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 注入HttpClient代码生成器
|
||||
@@ -21,9 +21,10 @@ namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
|
||||
[Generator]
|
||||
public class HttpClientGenerator : ISourceGenerator
|
||||
{
|
||||
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default";
|
||||
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc";
|
||||
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2";
|
||||
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.Default";
|
||||
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc";
|
||||
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc2";
|
||||
private const string XRpc3Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc3";
|
||||
|
||||
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
|
||||
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
|
||||
@@ -44,38 +45,41 @@ public class HttpClientGenerator : ISourceGenerator
|
||||
return;
|
||||
}
|
||||
|
||||
string toolName = this.GetGeneratorType().FullName;
|
||||
|
||||
StringBuilder sourceCodeBuilder = new();
|
||||
|
||||
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
sourceCodeBuilder.Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// This class is generated by Snap.Hutao.SourceGeneration
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
// This class is generated by Snap.Hutao.SourceGeneration
|
||||
FillWithHttpClients(receiver, sourceCodeBuilder);
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using System.Net.Http;
|
||||
sourceCodeBuilder.Append("""
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{{");
|
||||
|
||||
FillWithInjectionServices(receiver, sourceCodeBuilder);
|
||||
sourceCodeBuilder.Append(@"
|
||||
return services;
|
||||
}
|
||||
}");
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
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();
|
||||
@@ -84,16 +88,23 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
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:
|
||||
@@ -105,8 +116,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
case XRpc2Name:
|
||||
lineBuilder.Append("XRpc2Configuration)");
|
||||
break;
|
||||
case XRpc3Name:
|
||||
lineBuilder.Append("XRpc3Configuration)");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]");
|
||||
throw new InvalidOperationException($"非法的 HttpClientConfiguration 值: [{injectAsName}]");
|
||||
}
|
||||
|
||||
AttributeData? handlerInfo = classSymbol
|
||||
@@ -120,11 +134,11 @@ internal static partial class IocHttpClientConfiguration
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> property in properties)
|
||||
{
|
||||
lineBuilder.Append(" ");
|
||||
lineBuilder.Append(' ');
|
||||
lineBuilder.Append(property.Key);
|
||||
lineBuilder.Append(" = ");
|
||||
lineBuilder.Append(property.Value.ToCSharpString());
|
||||
lineBuilder.Append(",");
|
||||
lineBuilder.Append(',');
|
||||
}
|
||||
|
||||
lineBuilder.Append(" })");
|
||||
@@ -135,7 +149,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
|
||||
}
|
||||
|
||||
lineBuilder.Append(";");
|
||||
lineBuilder.Append(';');
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 注入代码生成器
|
||||
@@ -41,30 +41,32 @@ public class InjectionGenerator : ISourceGenerator
|
||||
return;
|
||||
}
|
||||
|
||||
string toolName = this.GetGeneratorType().FullName;
|
||||
|
||||
StringBuilder sourceCodeBuilder = new();
|
||||
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// This class is generated by Snap.Hutao.SourceGeneration
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{{");
|
||||
sourceCodeBuilder.Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
// This class is generated by Snap.Hutao.SourceGeneration
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}","1.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillWithInjectionServices(receiver, sourceCodeBuilder);
|
||||
sourceCodeBuilder.Append(@"
|
||||
return services;
|
||||
}
|
||||
}");
|
||||
sourceCodeBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
|
||||
}
|
||||
@@ -76,46 +78,42 @@ internal static partial class ServiceCollectionExtension
|
||||
|
||||
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))
|
||||
@@ -153,4 +151,4 @@ internal static partial class ServiceCollectionExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Snap.Hutao.Core.Caching;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Injection(InjectAs.Singleton, typeof(IImageCache))]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
|
||||
internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
|
||||
{
|
||||
|
||||
@@ -24,16 +24,31 @@ internal static class CoreEnvironment
|
||||
/// </summary>
|
||||
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 移动端请求UA
|
||||
/// </summary>
|
||||
public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.44.1";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabOsXrpcVersion = "2.28.0";
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
/// </summary>
|
||||
@@ -45,6 +60,9 @@ internal static class CoreEnvironment
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
|
||||
// This SALT is not reliable
|
||||
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,7 +15,16 @@ internal sealed class HttpClientAttribute : Attribute
|
||||
/// 构造一个新的特性
|
||||
/// </summary>
|
||||
/// <param name="configration">配置</param>
|
||||
public HttpClientAttribute(HttpClientConfigration configration)
|
||||
public HttpClientAttribute(HttpClientConfiguration configration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的特性
|
||||
/// </summary>
|
||||
/// <param name="configration">配置</param>
|
||||
/// <param name="interfaceType">实现的接口类型</param>
|
||||
public HttpClientAttribute(HttpClientConfiguration configration, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
/// Http客户端配置
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal enum HttpClientConfigration
|
||||
internal enum HttpClientConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
@@ -23,4 +23,9 @@ internal enum HttpClientConfigration
|
||||
/// 米游社登录请求配置
|
||||
/// </summary>
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服Hoyolab请求配置
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -12,6 +12,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
|
||||
[HighQuality]
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
private const string ApplicationJson = "application/json";
|
||||
|
||||
/// <summary>
|
||||
/// 添加 <see cref="HttpClient"/>
|
||||
/// </summary>
|
||||
@@ -37,7 +39,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
@@ -51,7 +53,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
@@ -60,4 +62,18 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 国际服 API 测试
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
private static void XRpc3Configuration(HttpClient client)
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabOsUA);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "4");
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@ internal sealed class User : ObservableObject
|
||||
internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
|
||||
{
|
||||
User user = new(inner);
|
||||
bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
if (!await user.InitializeCoreAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
user.UserInfo = new UserInfo() { Nickname = "网络异常" };
|
||||
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
|
||||
user.UserGameRoles = new();
|
||||
}
|
||||
|
||||
@@ -118,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)
|
||||
{
|
||||
@@ -156,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;
|
||||
}
|
||||
@@ -188,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
|
||||
@@ -212,7 +215,7 @@ internal sealed class User : ObservableObject
|
||||
}
|
||||
|
||||
Response<UidCookieToken> cookieTokenResponse = await provider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.PickRequiredService<IPassportClient>(Entity.IsOversea)
|
||||
.GetCookieAccountInfoBySTokenAsync(Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -230,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
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -66,8 +67,22 @@ 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 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个国际服用户
|
||||
/// </summary>
|
||||
/// <param name="cookie">cookie</param>
|
||||
/// <returns>新创建的用户</returns>
|
||||
public static User CreateOs(Cookie cookie)
|
||||
{
|
||||
_ = cookie.TryGetAsLegacySToken(out Cookie? stoken);
|
||||
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
|
||||
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
|
||||
|
||||
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };
|
||||
|
||||
@@ -11,12 +11,35 @@ namespace Snap.Hutao.Model.Intrinsic.Immutable;
|
||||
[HighQuality]
|
||||
internal static class IntrinsicImmutables
|
||||
{
|
||||
private static readonly ImmutableHashSet<string> associationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<string> weaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<string> itemQualities = Enum.GetValues<ItemQuality>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<string> bodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<string> fightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<string> elementNames = new HashSet<string>(7)
|
||||
/// <summary>
|
||||
/// 所属地区
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> AssociationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 武器类型
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> WeaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 物品类型
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> ItemQualities = Enum.GetValues<ItemQuality>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 身材类型
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> BodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 战斗属性
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> FightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 元素名称
|
||||
/// </summary>
|
||||
public static readonly ImmutableHashSet<string> ElementNames = new HashSet<string>(7)
|
||||
{
|
||||
SH.ModelIntrinsicElementNameFire,
|
||||
SH.ModelIntrinsicElementNameWater,
|
||||
@@ -26,34 +49,4 @@ internal static class IntrinsicImmutables
|
||||
SH.ModelIntrinsicElementNameIce,
|
||||
SH.ModelIntrinsicElementNameRock,
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
/// <summary>
|
||||
/// 所属地区
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> AssociationTypes { get => associationTypes; }
|
||||
|
||||
/// <summary>
|
||||
/// 武器类型
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> WeaponTypes { get => weaponTypes; }
|
||||
|
||||
/// <summary>
|
||||
/// 物品类型
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> ItemQualities { get => itemQualities; }
|
||||
|
||||
/// <summary>
|
||||
/// 身材类型
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> BodyTypes { get => bodyTypes; }
|
||||
|
||||
/// <summary>
|
||||
/// 战斗属性
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> FightProperties { get => fightProperties; }
|
||||
|
||||
/// <summary>
|
||||
/// 元素名称
|
||||
/// </summary>
|
||||
public static ImmutableHashSet<string> ElementNames { get => elementNames; }
|
||||
}
|
||||
@@ -411,6 +411,15 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 网络异常 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ModelBindingUserInitializationFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ModelBindingUserInitializationFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 第 {0} 期 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -1257,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>
|
||||
@@ -1456,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1996,7 +2014,7 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 在此处输入包含 Stoken 的 Cookie 的本地化字符串。
|
||||
/// 查找类似 在此处输入包含 SToken 的 Cookie 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewDialogUserInputPlaceholder {
|
||||
get {
|
||||
@@ -2229,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>
|
||||
@@ -3292,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3966,6 +3993,15 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 请输入你的 Hoyolab Uid 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewPageLoginHoyoverseUserHint {
|
||||
get {
|
||||
return ResourceManager.GetString("ViewPageLoginHoyoverseUserHint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 我已登录 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -4687,7 +4723,7 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Cookie 操作 的本地化字符串。
|
||||
/// 查找类似 米游社 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewUserCookieOperation {
|
||||
get {
|
||||
@@ -4695,6 +4731,15 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Hoyolab 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewUserCookieOperation2 {
|
||||
get {
|
||||
return ResourceManager.GetString("ViewUserCookieOperation2", 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">
|
||||
@@ -1639,7 +1639,7 @@
|
||||
<value>工具</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperation" xml:space="preserve">
|
||||
<value>Cookie 操作</value>
|
||||
<value>米游社</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>网页登录</value>
|
||||
@@ -1806,4 +1806,19 @@
|
||||
<data name="ViewServiceHutaoUserLoginOrRegisterHint" xml:space="preserve">
|
||||
<value>立即登录或注册</value>
|
||||
</data>
|
||||
<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>
|
||||
<data name="ViewUserCookieOperation2" xml:space="preserve">
|
||||
<value>Hoyolab</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,24 +93,24 @@ internal sealed class AvatarInfoDbOperation
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
|
||||
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (playerInfoResponse.IsOk())
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
|
||||
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (charactersResponse.IsOk())
|
||||
{
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
GameRecordCharacterAvatarInfoComposer composer = serviceProvider.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
@@ -215,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);
|
||||
|
||||
@@ -31,20 +31,16 @@ internal sealed class AvatarInfoService : IAvatarInfoService
|
||||
/// 构造一个新的角色信息服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="summaryFactory">简述工厂</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public AvatarInfoService(
|
||||
AppDbContext appDbContext,
|
||||
IMetadataService metadataService,
|
||||
ISummaryFactory summaryFactory,
|
||||
ILogger<AvatarInfoService> logger)
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
this.metadataService = metadataService;
|
||||
this.summaryFactory = summaryFactory;
|
||||
this.logger = logger;
|
||||
metadataService = serviceProvider.GetRequiredService<IMetadataService>();
|
||||
summaryFactory = serviceProvider.GetRequiredService<ISummaryFactory>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<AvatarInfoService>>();
|
||||
|
||||
avatarInfoDbOperation = new(appDbContext);
|
||||
avatarInfoDbOperation = new(appDbContext, serviceProvider);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -60,23 +60,17 @@ internal sealed class DailyNoteNotifier
|
||||
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
|
||||
AuthClient authClient = scope.ServiceProvider.GetRequiredService<AuthClient>();
|
||||
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await authClient
|
||||
.GetActionTicketByStokenAsync("game_role", entry.User)
|
||||
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
|
||||
|
||||
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesOverseaAwareAsync(entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
|
||||
if (actionTicketResponse.IsOk())
|
||||
if (rolesResponse.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;
|
||||
@@ -12,6 +11,7 @@ using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using System.Collections.ObjectModel;
|
||||
using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote;
|
||||
@@ -60,13 +60,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
|
||||
|
||||
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
|
||||
{
|
||||
DailyNoteEntry newEntry = DailyNoteEntry.Create(role);
|
||||
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
|
||||
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(roleUid))
|
||||
.GetDailyNoteAsync(role)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -109,12 +109,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
|
||||
|
||||
// TODO: Add this option to AppOptions
|
||||
bool isSilentMode = appDbContext.Settings
|
||||
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
|
||||
.GetBoolean();
|
||||
bool isGameRunning = scope.ServiceProvider.GetRequiredService<IGameService>().IsGameRunning();
|
||||
|
||||
if (isSilentMode && isGameRunning)
|
||||
{
|
||||
// Prevent notify when we are in game && silent mode.
|
||||
@@ -123,7 +124,8 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
|
||||
|
||||
foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
|
||||
{
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
|
||||
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(entry.Uid))
|
||||
.GetDailyNoteAsync(new(entry.User, entry.Uid))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -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,20 +24,25 @@ 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()
|
||||
{
|
||||
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
|
||||
{
|
||||
if (userAndUid.User.IsOversea)
|
||||
{
|
||||
return new(false, SH.ServiceGachaLogUrlProviderStokenUnsupported);
|
||||
}
|
||||
|
||||
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid);
|
||||
Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
/// 游戏文件包转换器
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class PackageConverter
|
||||
{
|
||||
private readonly JsonSerializerOptions options;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Metadata;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Injection(InjectAs.Singleton, typeof(IMetadataService))]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed partial class MetadataService : IMetadataService, IMetadataServiceInitialization
|
||||
{
|
||||
private const string MetaFileName = "Meta.json";
|
||||
|
||||
@@ -19,8 +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 string? uid;
|
||||
private ObservableCollection<SpiralAbyssEntry>? spiralAbysses;
|
||||
@@ -28,12 +28,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
|
||||
/// <summary>
|
||||
/// 构造一个新的深渊记录服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="gameRecordClient">游戏记录客户端</param>
|
||||
public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public SpiralAbyssRecordService(IServiceProvider serviceProvider)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.gameRecordClient = gameRecordClient;
|
||||
appDbContext = serviceProvider.GetRequiredService<AppDbContext>();
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -70,7 +69,8 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
|
||||
|
||||
private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await gameRecordClient
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await serviceProvider
|
||||
.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea)
|
||||
.GetSpiralAbyssAsync(userAndUid, schedule)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -45,8 +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);
|
||||
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,13 +219,13 @@ internal class UserService : IUserService
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken);
|
||||
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false);
|
||||
return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,9 +235,9 @@ internal class UserService : IUserService
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.GetCookieAccountInfoBySTokenAsync(user.Entity)
|
||||
.ConfigureAwait(false);
|
||||
.PickRequiredService<IPassportClient>(user.Entity.IsOversea)
|
||||
.GetCookieAccountInfoBySTokenAsync(user.Entity)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (cookieTokenResponse.IsOk())
|
||||
{
|
||||
@@ -265,14 +265,14 @@ internal class UserService : IUserService
|
||||
return user != null;
|
||||
}
|
||||
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie)
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
|
||||
{
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
|
||||
BindingUser? newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
|
||||
if (newUser != null)
|
||||
{
|
||||
// Sync cache
|
||||
|
||||
@@ -47,9 +47,18 @@ internal sealed partial class SignInWebViewDialog : ContentDialog
|
||||
return;
|
||||
}
|
||||
|
||||
coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileUserAgent();
|
||||
signInJsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501");
|
||||
if (user.Entity.IsOversea)
|
||||
{
|
||||
coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileOverseaUserAgent();
|
||||
signInJsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
coreWebView2.Navigate("https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481");
|
||||
}
|
||||
else
|
||||
{
|
||||
coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileUserAgent();
|
||||
signInJsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<Page
|
||||
x:Class="Snap.Hutao.View.Page.LoginHoyoverseUserPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Loaded="OnRootLoaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{shcm:ResourceString Name=ViewPageLoginMihoyoUserTitle}"/>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
x:Name="UidInputText"
|
||||
Grid.Column="0"
|
||||
Width="240"
|
||||
Margin="0,0,16,0"
|
||||
HorizontalAlignment="Right"
|
||||
InputScope="Number"
|
||||
MaxLength="9"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewPageLoginHoyoverseUserHint}"/>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Click="CookieButtonClick"
|
||||
Content="{shcm:ResourceString Name=ViewPageLoginMihoyoUserLoggedInAction}"/>
|
||||
</Grid>
|
||||
|
||||
<WebView2
|
||||
x:Name="WebView"
|
||||
Grid.Row="2"
|
||||
Margin="0,0,0,0"/>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Response;
|
||||
|
||||
namespace Snap.Hutao.View.Page;
|
||||
|
||||
/// <summary>
|
||||
/// 登录米哈游通行证页面
|
||||
/// </summary>
|
||||
internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的登录米哈游通行证页面
|
||||
/// </summary>
|
||||
public LoginHoyoverseUserPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
|
||||
foreach (CoreWebView2Cookie item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
|
||||
WebView.CoreWebView2.Navigate("https://account.hoyolab.com/#/login");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCurrentCookieAsync(CancellationToken token = default)
|
||||
{
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
|
||||
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
// Get user id from text input, login_uid is missed in cookie
|
||||
string uid = UidInputText.Text;
|
||||
|
||||
if (uid.Length != 9)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
infoBarService.Information(SH.ViewPageLoginHoyoverseUserHint);
|
||||
return;
|
||||
}
|
||||
|
||||
Cookie loginTicketCookie = Cookie.FromCoreWebView2Cookies(cookies);
|
||||
loginTicketCookie["login_uid"] = uid;
|
||||
|
||||
// 使用 loginTicket 获取 stoken
|
||||
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
|
||||
.GetRequiredService<AuthClient>()
|
||||
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, true, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!multiTokenResponse.IsOk())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
|
||||
Cookie hoyoLabCookie = Cookie.Parse($"{Cookie.STUID}={uid};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
|
||||
|
||||
// 处理 cookie 并添加用户
|
||||
(UserOptionResult result, string nickname) = await Ioc.Default
|
||||
.GetRequiredService<IUserService>()
|
||||
.ProcessInputCookieAsync(hoyoLabCookie, true)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case UserOptionResult.Added:
|
||||
ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService<ViewModel.UserViewModel>();
|
||||
if (vm.Users!.Count == 1)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
vm.SelectedUser = vm.Users.Single();
|
||||
}
|
||||
|
||||
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
|
||||
break;
|
||||
case UserOptionResult.Incomplete:
|
||||
infoBarService.Information(SH.ViewModelUserIncomplete);
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
infoBarService.Information(SH.ViewModelUserInvalid);
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
|
||||
private void CookieButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
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())
|
||||
@@ -67,10 +67,11 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
|
||||
|
||||
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
|
||||
|
||||
Cookie stokenV1 = Cookie.Parse($"stuid={loginTicketCookie["login_uid"]};stoken={multiTokenMap["stoken"]}");
|
||||
Cookie stokenV1 = Cookie.Parse($"{Cookie.STUID}={loginTicketCookie[Cookie.LOGIN_UID]};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
|
||||
|
||||
Response<LoginResult> loginResultResponse = await Ioc.Default
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.LoginByStokenAsync(stokenV1, token)
|
||||
.GetRequiredService<PassportClient>()
|
||||
.LoginBySTokenAsync(stokenV1, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!loginResultResponse.IsOk())
|
||||
@@ -81,12 +82,13 @@ 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();
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
// TODO: Move these code somewhere else.
|
||||
switch (result)
|
||||
{
|
||||
case UserOptionResult.Added:
|
||||
@@ -97,16 +99,16 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
|
||||
vm.SelectedUser = vm.Users.Single();
|
||||
}
|
||||
|
||||
infoBarService.Success($"用户 [{nickname}] 添加成功");
|
||||
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
|
||||
break;
|
||||
case UserOptionResult.Incomplete:
|
||||
infoBarService.Information($"此 Cookie 不完整,操作失败");
|
||||
infoBarService.Information(SH.ViewModelUserIncomplete);
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
infoBarService.Information($"此 Cookie 无效,操作失败");
|
||||
infoBarService.Information(SH.ViewModelUserInvalid);
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
infoBarService.Success($"用户 [{nickname}] 更新成功");
|
||||
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
@@ -115,6 +117,6 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
|
||||
|
||||
private void CookieButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
|
||||
HandleCurrentCookieAsync().SafeForget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,10 +116,7 @@
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid
|
||||
Width="200"
|
||||
Padding="0,12"
|
||||
Background="Transparent">
|
||||
<Grid Padding="0,12" Background="Transparent">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
@@ -204,27 +201,65 @@
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewUserDefaultDescription}"
|
||||
Visibility="{Binding Users.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}"/>
|
||||
<TextBlock
|
||||
Margin="10,6,0,6"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewUserCookieOperation}"/>
|
||||
<StackPanel
|
||||
Margin="0,0,6,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<AppBarButton
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Command="{Binding RefreshCookieTokenCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationRefreshCookieAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding LoginMihoyoUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding AddUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</StackPanel>
|
||||
<AppBarSeparator Grid.RowSpan="2" Grid.Column="1"/>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Margin="4,6,0,0"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewUserCookieOperation}"/>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal">
|
||||
<AppBarButton
|
||||
Command="{Binding LoginMihoyoUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding AddUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</StackPanel>
|
||||
<AppBarSeparator Grid.RowSpan="2" Grid.Column="3"/>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
Margin="4,6,0,0"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{shcm:ResourceString Name=ViewUserCookieOperation2}"/>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="4"
|
||||
Orientation="Horizontal">
|
||||
<AppBarButton
|
||||
Command="{Binding LoginHoyoverseUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding AddOverseaUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
|
||||
@@ -247,9 +247,17 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel
|
||||
{
|
||||
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
|
||||
// TODO: Add verify support for oversea user
|
||||
if (userAndUid.User.IsOversea)
|
||||
{
|
||||
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
|
||||
}
|
||||
}
|
||||
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,9 @@ internal sealed class UserViewModel : ObservableObject
|
||||
|
||||
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
|
||||
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
|
||||
AddOverseaUserCommand = new AsyncRelayCommand(AddOverseaUserAsync);
|
||||
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
|
||||
LoginHoyoverseUserCommand = new RelayCommand(LoginHoyoverseUser);
|
||||
RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync);
|
||||
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
|
||||
RefreshCookieTokenCommand = new AsyncRelayCommand(RefreshCookieTokenAsync);
|
||||
@@ -87,11 +89,21 @@ internal sealed class UserViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand AddUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加国际服用户命令
|
||||
/// </summary>
|
||||
public ICommand AddOverseaUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录米游社命令
|
||||
/// </summary>
|
||||
public ICommand LoginMihoyoUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录米游社命令
|
||||
/// </summary>
|
||||
public ICommand LoginHoyoverseUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 移除用户命令
|
||||
/// </summary>
|
||||
@@ -120,7 +132,17 @@ internal sealed class UserViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddUserAsync()
|
||||
private Task AddUserAsync()
|
||||
{
|
||||
return AddUserCoreAsync(false);
|
||||
}
|
||||
|
||||
private Task AddOverseaUserAsync()
|
||||
{
|
||||
return AddUserCoreAsync(true);
|
||||
}
|
||||
|
||||
private async Task AddUserCoreAsync(bool isOversea)
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
@@ -133,7 +155,7 @@ internal sealed class UserViewModel : ObservableObject
|
||||
{
|
||||
Cookie cookie = Cookie.Parse(result.Value);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
|
||||
|
||||
switch (optionResult)
|
||||
{
|
||||
@@ -173,6 +195,21 @@ internal sealed class UserViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开浏览器登录 hoyolab 以获取 cookie
|
||||
/// </summary>
|
||||
private void LoginHoyoverseUser()
|
||||
{
|
||||
if (Core.WebView2Helper.IsSupported)
|
||||
{
|
||||
serviceProvider.GetRequiredService<INavigationService>().Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
if (user != null)
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
@@ -10,9 +12,161 @@ 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>
|
||||
/// 获取GT码
|
||||
/// </summary>
|
||||
/// <param name="gt">gt</param>
|
||||
/// <returns>GT码Url</returns>
|
||||
public static string GeetestGetType(string gt)
|
||||
{
|
||||
return $"{ApiNaGeetest}/gettype.php?gt={gt}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证接口
|
||||
/// </summary>
|
||||
/// <param name="gt">gt</param>
|
||||
/// <param name="challenge">challenge流水号</param>
|
||||
/// <returns>验证接口Url</returns>
|
||||
public static string GeetestAjax(string gt, string challenge)
|
||||
{
|
||||
return $"{ApiNaGeetest}/ajax.php?gt={gt}&challenge={challenge}&lang=zh-cn&pt=0&client_type=web";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiOsTakumiAuthApi
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stoken 与 ltoken
|
||||
/// </summary>
|
||||
/// <param name="loginTicket">登录票证</param>
|
||||
/// <param name="loginUid">uid</param>
|
||||
/// <returns>Url</returns>
|
||||
public static string AuthMultiToken(string loginTicket, string loginUid)
|
||||
{
|
||||
return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stoken 与 ltoken
|
||||
/// </summary>
|
||||
/// <param name="actionType">操作类型 game_role</param>
|
||||
/// <param name="stoken">SToken</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>Url</returns>
|
||||
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
||||
{
|
||||
return $"{ApiAccountOsAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiOsTaKumiApi
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
/// <returns>用户游戏角色字符串</returns>
|
||||
public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
/// <param name="region">地区代号</param>
|
||||
/// <returns>用户游戏角色字符串</returns>
|
||||
public static string UserGameRolesByLtoken(string region)
|
||||
{
|
||||
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region BbsApiOsApi
|
||||
|
||||
/// <summary>
|
||||
/// 查询其他用户详细信息
|
||||
/// </summary>
|
||||
/// <param name="bbsUid">bbs Uid</param>
|
||||
/// <returns>查询其他用户详细信息字符串</returns>
|
||||
public static string UserFullInfoQuery(string bbsUid)
|
||||
{
|
||||
return $"{BbsApiOs}/community/painter/wapi/user/full";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服角色基本信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>角色基本信息字符串</returns>
|
||||
public static string GameRecordRoleBasicInfo(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服角色信息
|
||||
/// </summary>
|
||||
public const string GameRecordCharacter = $"{BbsApiOsGameRecordApi}/character";
|
||||
|
||||
/// <summary>
|
||||
/// 国际服游戏记录实时便笺
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>游戏记录实时便笺字符串</returns>
|
||||
public static string GameRecordDailyNote(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服游戏记录主页
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>游戏记录主页字符串</returns>
|
||||
public static string GameRecordIndex(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服深渊信息
|
||||
/// </summary>
|
||||
/// <param name="scheduleType">深渊类型</param>
|
||||
/// <param name="uid">Uid</param>
|
||||
/// <returns>深渊信息字符串</returns>
|
||||
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hk4eApiOsGachaInfoApi
|
||||
|
||||
/// <summary>
|
||||
@@ -26,6 +180,46 @@ internal static class ApiOsEndpoints
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SgPublicApi
|
||||
|
||||
/// <summary>
|
||||
/// 计算器家具计算
|
||||
/// </summary>
|
||||
public const string CalculateFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器角色列表 size 20
|
||||
/// </summary>
|
||||
public const string CalculateAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器武器列表 size 20
|
||||
/// </summary>
|
||||
public const string CalculateWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器结果
|
||||
/// </summary>
|
||||
public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器同步角色详情 size 20
|
||||
/// </summary>
|
||||
/// <param name="avatarId">角色Id</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>角色详情</returns>
|
||||
public static string CalculateSyncAvatarDetail(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 CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
|
||||
|
||||
#endregion
|
||||
|
||||
#region SdkStaticLauncherApi
|
||||
|
||||
/// <summary>
|
||||
@@ -40,10 +234,35 @@ internal static class ApiOsEndpoints
|
||||
#endregion
|
||||
|
||||
#region Hosts | Queries
|
||||
private const string ApiNaGeetest = "https://api-na.geetest.com";
|
||||
|
||||
private const string ApiOsTakumi = "https://api-os-takumi.hoyoverse.com";
|
||||
private const string ApiOsTakumiBindingApi = $"{ApiOsTakumi}/binding/api";
|
||||
|
||||
private const string ApiAccountOs = "https://api-account-os.hoyolab.com";
|
||||
private const string ApiAccountOsBindingApi = $"{ApiAccountOs}/binding/api";
|
||||
private const string ApiAccountOsAuthApi = $"{ApiAccountOs}/account/auth/api";
|
||||
|
||||
private const string BbsApiOs = "https://bbs-api-os.hoyolab.com";
|
||||
private const string BbsApiOsGameRecordApi = $"{BbsApiOs}/game_record/genshin/api";
|
||||
|
||||
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
|
||||
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
|
||||
|
||||
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
|
||||
private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
|
||||
|
||||
private const string SgPublicApi = "https://sg-public-api.hoyolab.com";
|
||||
|
||||
/// <summary>
|
||||
/// Web static referer
|
||||
/// </summary>
|
||||
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
|
||||
|
||||
/// <summary>
|
||||
/// Act hoyolab referer
|
||||
/// </summary>
|
||||
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -23,15 +23,26 @@ internal static class CoreWebView2Extension
|
||||
return webView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 移动端OsUA
|
||||
/// </summary>
|
||||
/// <param name="webView">webview2</param>
|
||||
/// <returns>链式调用的WebView2</returns>
|
||||
public static CoreWebView2 SetMobileOverseaUserAgent(this CoreWebView2 webView)
|
||||
{
|
||||
webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabOsMobileUA;
|
||||
return webView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置WebView2的Cookie
|
||||
/// </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;
|
||||
|
||||
@@ -40,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Enka;
|
||||
/// Enka API 客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class EnkaClient
|
||||
{
|
||||
private const string EnkaAPI = "https://enka.network/api/uid/{0}";
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Geetest;
|
||||
/// 极验客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class GeetestClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.App.Account;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
[HttpClient(HttpClientConfiguration.XRpc)]
|
||||
internal sealed class AccountClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -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(HttpClientConfigration.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>
|
||||
@@ -52,4 +55,22 @@ internal sealed class UserClient
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户详细信息,使用 LToken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>详细信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.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.OSK2, false)
|
||||
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
@@ -116,7 +116,33 @@ internal sealed partial class Cookie
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetAsLtoken([NotNullWhen(true)] out Cookie? cookie)
|
||||
/// <summary>
|
||||
/// 提取其中的 stoken 信息
|
||||
/// Used for hoyolab account.
|
||||
/// </summary>
|
||||
/// <param name="cookie">A cookie contains stoken and stuid, without mid.</param>
|
||||
/// <returns>是否获取成功</returns>
|
||||
public bool TryGetAsLegacySToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
bool hasSToken = TryGetValue(STOKEN, out string? stoken);
|
||||
bool hasSTuid = TryGetValue(STUID, out string? stuid);
|
||||
|
||||
if (hasSToken && hasSTuid)
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[STOKEN] = stoken!,
|
||||
[STUID] = stuid!,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetAsLToken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
bool hasLtoken = TryGetValue(LTOKEN, out string? ltoken);
|
||||
bool hasStuid = TryGetValue(LTUID, out string? ltuid);
|
||||
|
||||
@@ -38,4 +38,9 @@ internal enum SaltType
|
||||
/// LK2
|
||||
/// </summary>
|
||||
LK2,
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab K2
|
||||
/// </summary>
|
||||
OSK2,
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
|
||||
/// <summary>
|
||||
/// 公告客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class AnnouncementClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
/// 祈愿记录客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class GachaInfoClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
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;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
|
||||
@@ -13,7 +15,8 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
/// 通行证客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class PassportClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -34,7 +37,7 @@ internal sealed class PassportClient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步验证Ltoken
|
||||
/// 异步验证 LToken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
@@ -50,6 +53,28 @@ internal sealed class PassportClient
|
||||
return Response.Response.DefaultIfNull(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V1 SToken 登录
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
HttpResponseMessage message = await httpClient
|
||||
.SetHeader("Cookie", stokenV1.ToString())
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
|
||||
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Response<LoginResult>? resp = await message.Content
|
||||
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
private class Timestamp
|
||||
{
|
||||
[JsonPropertyName("t")]
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.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,27 +36,8 @@ internal sealed class PassportClient2
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// V1Stoken 登录
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
HttpResponseMessage message = await httpClient
|
||||
.SetHeader("Cookie", stokenV1.ToString())
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
|
||||
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Response<LoginResult>? resp = await message.Content
|
||||
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public bool IsOversea => false;
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 CookieToken
|
||||
@@ -77,18 +58,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);
|
||||
|
||||
@@ -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<PassportClientOversea> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的国际服通行证客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClientOversea> 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,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
|
||||
/// <summary>
|
||||
/// SToken 包装器
|
||||
/// </summary>
|
||||
internal sealed class STokenWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的SToken 包装器
|
||||
/// </summary>
|
||||
/// <param name="stoken">stoken</param>
|
||||
/// <param name="uid">uid</param>
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
/// 游戏资源客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class ResourceClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -40,7 +40,7 @@ internal sealed class ResourceClient
|
||||
/// <returns>游戏资源</returns>
|
||||
public async Task<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
|
||||
{
|
||||
string url = scheme.LauncherId == "10"
|
||||
string url = scheme.IsOversea
|
||||
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
|
||||
: ApiEndpoints.SdkStaticLauncherResource(scheme);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class AuthClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -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,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;
|
||||
|
||||
@@ -13,9 +15,10 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
/// 绑定客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[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>
|
||||
@@ -52,4 +90,21 @@ internal sealed class BindingClient
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取国际服用户角色信息
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>用户角色信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken)]
|
||||
public async Task<Response<ListWrapper<UserGameRole>>> GetOverseaUserGameRolesByCookieAsync(User user, CancellationToken token = default)
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>>? resp = await httpClient
|
||||
.SetUser(user, CookieType.LToken)
|
||||
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiOsEndpoints.UserGameRolesByCookie, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ using System.Net.Http;
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Stoken绑定客户端
|
||||
/// SToken绑定客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
[HttpClient(HttpClientConfiguration.XRpc)]
|
||||
internal sealed class BindingClient2
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -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);
|
||||
@@ -55,7 +55,7 @@ internal sealed class BindingClient2
|
||||
|
||||
/// <summary>
|
||||
/// 异步生成祈愿验证密钥
|
||||
/// 需要stoken
|
||||
/// 需要 SToken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="data">提交数据</param>
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
/// 养成计算器客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class CalculateClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
@@ -43,10 +43,18 @@ internal sealed class CalculateClient
|
||||
[ApiInformation(Cookie = CookieType.Cookie)]
|
||||
public async Task<Response<Consumption>> ComputeAsync(Model.Entity.User user, AvatarPromotionDelta delta, CancellationToken token = default)
|
||||
{
|
||||
string referer = user.IsOversea
|
||||
? ApiOsEndpoints.ActHoyolabReferer
|
||||
: ApiEndpoints.WebStaticMihoyoReferer;
|
||||
|
||||
string url = user.IsOversea
|
||||
? ApiOsEndpoints.CalculateCompute
|
||||
: ApiEndpoints.CalculateCompute;
|
||||
|
||||
Response<Consumption>? resp = await httpClient
|
||||
.SetUser(user, CookieType.Cookie)
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiEndpoints.CalculateCompute, delta, options, logger, token)
|
||||
.SetReferer(referer)
|
||||
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(url, delta, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
@@ -65,14 +73,24 @@ internal sealed class CalculateClient
|
||||
|
||||
List<Avatar> avatars = new();
|
||||
Response<ListWrapper<Avatar>>? resp;
|
||||
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
|
||||
|
||||
// 根据 uid 所属服务器选择 referer 与 api
|
||||
string referer = userAndUid.User.IsOversea
|
||||
? ApiOsEndpoints.ActHoyolabReferer
|
||||
: ApiEndpoints.WebStaticMihoyoReferer;
|
||||
string url = userAndUid.User.IsOversea
|
||||
? ApiOsEndpoints.CalculateSyncAvatarList
|
||||
: ApiEndpoints.CalculateSyncAvatarList;
|
||||
|
||||
httpClient
|
||||
.SetReferer(referer)
|
||||
.SetUser(userAndUid.User, CookieType.CookieToken);
|
||||
|
||||
do
|
||||
{
|
||||
filter.Page = currentPage++;
|
||||
resp = await httpClient
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(ApiEndpoints.CalculateSyncAvatarList, filter, options, logger, token)
|
||||
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(url, filter, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (resp != null && resp.IsOk())
|
||||
@@ -101,9 +119,13 @@ internal sealed class CalculateClient
|
||||
/// <returns>角色详情</returns>
|
||||
public async Task<Response<AvatarDetail>> GetAvatarDetailAsync(UserAndUid userAndUid, Avatar avatar, CancellationToken token = default)
|
||||
{
|
||||
string url = userAndUid.User.IsOversea
|
||||
? ApiOsEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value)
|
||||
: ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value);
|
||||
|
||||
Response<AvatarDetail>? resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.CookieToken)
|
||||
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
|
||||
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(url, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.XRpc)]
|
||||
[HttpClient(HttpClientConfiguration.XRpc)]
|
||||
internal sealed class CardClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -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(HttpClientConfigration.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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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.Web.Hoyolab.Annotation;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
|
||||
/// <summary>
|
||||
/// Hoyoverse game record provider
|
||||
/// </summary>
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfiguration.XRpc3, typeof(IGameRecordClient))]
|
||||
[PrimaryHttpMessageHandler(UseCookies = false)]
|
||||
internal sealed class GameRecordClientOversea : IGameRecordClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<GameRecordClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏记录提供器
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public 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.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.OSK2, false)
|
||||
.TryCatchGetFromJsonAsync<Response<DailyNote.DailyNote>>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家基础信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家的基础信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.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.OSK2, false)
|
||||
.TryCatchGetFromJsonAsync<Response<PlayerInfo>>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家深渊信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户</param>
|
||||
/// <param name="schedule">1:当期,2:上期</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>深渊信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.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.OSK2, false)
|
||||
.TryCatchGetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家角色详细信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="playerInfo">玩家的基础信息</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.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.OSK2, false)
|
||||
.TryCatchPostAsJsonAsync<CharacterData, Response<CharacterWrapper>>(ApiOsEndpoints.GameRecordCharacter, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hutao;
|
||||
/// 胡桃日志客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class HomaLogUploadClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hutao;
|
||||
/// 胡桃通行证客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class HomaPassportClient
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -18,11 +18,11 @@ namespace Snap.Hutao.Web.Hutao;
|
||||
/// 胡桃API客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
internal sealed class HomaSpiralAbyssClient
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly GameRecordClient gameRecordClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<HomaSpiralAbyssClient> logger;
|
||||
|
||||
@@ -30,15 +30,14 @@ internal sealed class HomaSpiralAbyssClient
|
||||
/// 构造一个新的胡桃API客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="gameRecordClient">游戏记录客户端</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public HomaSpiralAbyssClient(HttpClient httpClient, IServiceProvider serviceProvider)
|
||||
{
|
||||
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
|
||||
logger = serviceProvider.GetRequiredService<ILogger<HomaSpiralAbyssClient>>();
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.httpClient = httpClient;
|
||||
this.gameRecordClient = gameRecordClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,6 +185,8 @@ internal sealed class HomaSpiralAbyssClient
|
||||
/// <returns>玩家记录</returns>
|
||||
public async Task<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
|
||||
{
|
||||
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
|
||||
|
||||
Response<PlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -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