diff --git a/.gitignore b/.gitignore
index 9e870ea9..8a1261c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
\ No newline at end of file
+src/Snap.Hutao/Snap.Hutao.Test/bin/
+src/Snap.Hutao/Snap.Hutao.Test/obj/
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs
similarity index 71%
rename from src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs
rename to src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs
index 4c104834..279a58f6 100644
--- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs
+++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/HttpClientGenerator.cs
@@ -11,7 +11,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text;
-namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
+namespace Snap.Hutao.SourceGeneration.DependencyInjection;
///
/// 注入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 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 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 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()");
}
- lineBuilder.Append(";");
+ lineBuilder.Append(';');
lines.Add(lineBuilder.ToString());
}
diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs
similarity index 59%
rename from src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs
rename to src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs
index d55b18de..9da45793 100644
--- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/InjectionGenerator.cs
+++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DependencyInjection/InjectionGenerator.cs
@@ -11,7 +11,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text;
-namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
+namespace Snap.Hutao.SourceGeneration.DependencyInjection;
///
/// 注入代码生成器
@@ -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 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 arguments = injectionInfo.ConstructorArguments;
+ TypedConstant injectAs = arguments[0];
+
+ string injectAsName = injectAs.ToCSharpString();
+ switch (injectAsName)
{
- lineBuilder
- .Clear()
- .Append("\r\n");
-
- ImmutableArray 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
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs
new file mode 100644
index 00000000..f799efdd
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs
@@ -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()
+ .AddSingleton()
+ .BuildServiceProvider();
+
+ Assert.IsNotNull(services.GetService());
+ Assert.IsNotNull(services.GetService());
+ }
+
+ private interface IService
+ {
+ }
+
+ private sealed class ServiceA : IService
+ {
+ }
+
+ private sealed class ServiceB : IService
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs b/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs
new file mode 100644
index 00000000..ab67c7ea
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj
new file mode 100644
index 00000000..f1b8a08c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net7.0
+ disable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao.sln b/src/Snap.Hutao/Snap.Hutao.sln
index 4edf3482..12a43506 100644
--- a/src/Snap.Hutao/Snap.Hutao.sln
+++ b/src/Snap.Hutao/Snap.Hutao.sln
@@ -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
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
index 23a8a390..81181c6a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Caching/ImageCache.cs
@@ -18,7 +18,7 @@ namespace Snap.Hutao.Core.Caching;
///
[HighQuality]
[Injection(InjectAs.Singleton, typeof(IImageCache))]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
index bfc9ad0a..01cdabf7 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
@@ -24,16 +24,31 @@ internal static class CoreEnvironment
///
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
+ ///
+ /// Hoyolab请求UA
+ ///
+ public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
+
///
/// 米游社移动端请求UA
///
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
+ ///
+ /// Hoyolab 移动端请求UA
+ ///
+ public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
+
///
/// 米游社 Rpc 版本
///
public const string HoyolabXrpcVersion = "2.44.1";
+ ///
+ /// Hoyolab Rpc 版本
+ ///
+ public const string HoyolabOsXrpcVersion = "2.28.0";
+
///
/// 盐
///
@@ -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();
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs
index f3f77c84..87fa6a09 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientAttribute.cs
@@ -15,7 +15,16 @@ internal sealed class HttpClientAttribute : Attribute
/// 构造一个新的特性
///
/// 配置
- public HttpClientAttribute(HttpClientConfigration configration)
+ public HttpClientAttribute(HttpClientConfiguration configration)
+ {
+ }
+
+ ///
+ /// 构造一个新的特性
+ ///
+ /// 配置
+ /// 实现的接口类型
+ public HttpClientAttribute(HttpClientConfiguration configration, Type interfaceType)
{
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfiguration.cs
similarity index 78%
rename from src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs
rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfiguration.cs
index 4d12e220..76609c09 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfiguration.cs
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
/// Http客户端配置
///
[HighQuality]
-internal enum HttpClientConfigration
+internal enum HttpClientConfiguration
{
///
/// 默认配置
@@ -23,4 +23,9 @@ internal enum HttpClientConfigration
/// 米游社登录请求配置
///
XRpc2,
+
+ ///
+ /// 国际服Hoyolab请求配置
+ ///
+ XRpc3,
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs
new file mode 100644
index 00000000..cff17d77
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/EnumerableServiceExtension.cs
@@ -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;
+
+///
+/// 服务集合扩展
+///
+internal static class EnumerableServiceExtension
+{
+ ///
+ /// 选择对应的服务
+ ///
+ /// 服务类型
+ /// 服务集合
+ /// 名称
+ /// 对应的服务
+ public static TService Pick(this IEnumerable services, string name)
+ where TService : INamedService
+ {
+ return services.Single(s => s.Name == name);
+ }
+
+ ///
+ /// 选择对应的服务
+ ///
+ /// 服务类型
+ /// 服务集合
+ /// 是否为海外服/Hoyolab
+ /// 对应的服务
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static TService Pick(this IEnumerable services, bool isOversea)
+ where TService : IOverseaSupport
+ {
+ return services.Single(s => s.IsOversea == isOversea);
+ }
+
+ ///
+ /// 选择对应的服务
+ ///
+ /// 服务类型
+ /// 服务提供器
+ /// 是否为海外服/Hoyolab
+ /// 对应的服务
+ public static TService PickRequiredService(this IServiceProvider serviceProvider, bool isOversea)
+ where TService : IOverseaSupport
+ {
+ return serviceProvider.GetRequiredService>().Pick(isOversea);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamedService.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs
similarity index 86%
rename from src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamedService.cs
rename to src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs
index 191b02c6..e22ea9b8 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Abstraction/INamedService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/INamedService.cs
@@ -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;
///
/// 有名称的对象
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs
new file mode 100644
index 00000000..44a57100
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IOverseaSupport.cs
@@ -0,0 +1,15 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Core.DependencyInjection;
+
+///
+/// 海外服/Hoyolab 可区分
+///
+internal interface IOverseaSupport
+{
+ ///
+ /// 是否为 海外服/Hoyolab
+ ///
+ public bool IsOversea { get; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs
index 1a532ad2..451a1c66 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs
@@ -12,6 +12,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
[HighQuality]
internal static partial class IocHttpClientConfiguration
{
+ private const string ApplicationJson = "application/json";
+
///
/// 添加
///
@@ -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");
}
+
+ ///
+ /// 对于需要添加动态密钥的客户端使用此配置
+ /// 国际服 API 测试
+ ///
+ /// 配置后的客户端
+ 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");
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs
deleted file mode 100644
index 7282591d..00000000
--- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/NamedServiceExtension.cs
+++ /dev/null
@@ -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;
-
-///
-/// 命名服务扩展
-///
-internal static class NamedServiceExtension
-{
- ///
- /// 选择对应的服务
- ///
- /// 服务类型
- /// 服务集合
- /// 名称
- /// 对应的服务
- public static TService Pick(this IEnumerable services, string name)
- where TService : INamedService
- {
- return services.Single(s => s.Name == name);
- }
-}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
index e901a89f..5ba3272f 100644
--- a/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
+++ b/src/Snap.Hutao/Snap.Hutao/GlobalUsing.cs
@@ -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
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
index f4c9f196..23f1e61a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs
@@ -103,11 +103,10 @@ internal sealed class User : ObservableObject
internal static async Task ResumeAsync(EntityUser inner, CancellationToken token = default)
{
User user = new(inner);
- bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
- 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
/// 创建并初始化用户
///
/// cookie
+ /// 是否为国际服
/// 取消令牌
/// 用户
- internal static async Task CreateAsync(Cookie cookie, CancellationToken token = default)
+ internal static async Task 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 lTokenResponse = await provider
- .GetRequiredService()
+ Response lTokenResponse = await provider
+ .PickRequiredService(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 cookieTokenResponse = await provider
- .GetRequiredService()
+ .PickRequiredService(Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(Entity, token)
.ConfigureAwait(false);
@@ -230,38 +233,32 @@ internal sealed class User : ObservableObject
private async Task TrySetUserInfoAsync(IServiceProvider provider, CancellationToken token)
{
Response response = await provider
- .GetRequiredService()
+ .PickRequiredService(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 TrySetUserGameRolesAsync(IServiceProvider provider, CancellationToken token)
{
- Response actionTicketResponse = await provider
- .GetRequiredService()
- .GetActionTicketByStokenAsync("game_role", Entity)
- .ConfigureAwait(false);
+ Response> userGameRolesResponse = await provider
+ .GetRequiredService()
+ .GetUserGameRolesOverseaAwareAsync(Entity, token)
+ .ConfigureAwait(false);
- if (actionTicketResponse.IsOk())
+ if (userGameRolesResponse.IsOk())
{
- string actionTicket = actionTicketResponse.Data.Ticket;
-
- Response> userGameRolesResponse = await provider
- .GetRequiredService()
- .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
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs
index 4b492b93..986b489a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs
@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Web.Hoyolab;
+using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -66,8 +67,22 @@ internal sealed class User : ISelectable
/// 新创建的用户
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 };
+ }
+
+ ///
+ /// 创建一个国际服用户
+ ///
+ /// cookie
+ /// 新创建的用户
+ 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 };
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Immutable/IntrinsicImmutables.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Immutable/IntrinsicImmutables.cs
index 5a55a5bb..47d9f869 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Immutable/IntrinsicImmutables.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/Immutable/IntrinsicImmutables.cs
@@ -11,12 +11,35 @@ namespace Snap.Hutao.Model.Intrinsic.Immutable;
[HighQuality]
internal static class IntrinsicImmutables
{
- private static readonly ImmutableHashSet associationTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
- private static readonly ImmutableHashSet weaponTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
- private static readonly ImmutableHashSet itemQualities = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
- private static readonly ImmutableHashSet bodyTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
- private static readonly ImmutableHashSet fightProperties = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
- private static readonly ImmutableHashSet elementNames = new HashSet(7)
+ ///
+ /// 所属地区
+ ///
+ public static readonly ImmutableHashSet AssociationTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
+
+ ///
+ /// 武器类型
+ ///
+ public static readonly ImmutableHashSet WeaponTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
+
+ ///
+ /// 物品类型
+ ///
+ public static readonly ImmutableHashSet ItemQualities = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
+
+ ///
+ /// 身材类型
+ ///
+ public static readonly ImmutableHashSet BodyTypes = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
+
+ ///
+ /// 战斗属性
+ ///
+ public static readonly ImmutableHashSet FightProperties = Enum.GetValues().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType().ToImmutableHashSet();
+
+ ///
+ /// 元素名称
+ ///
+ public static readonly ImmutableHashSet ElementNames = new HashSet(7)
{
SH.ModelIntrinsicElementNameFire,
SH.ModelIntrinsicElementNameWater,
@@ -26,34 +49,4 @@ internal static class IntrinsicImmutables
SH.ModelIntrinsicElementNameIce,
SH.ModelIntrinsicElementNameRock,
}.ToImmutableHashSet();
-
- ///
- /// 所属地区
- ///
- public static ImmutableHashSet AssociationTypes { get => associationTypes; }
-
- ///
- /// 武器类型
- ///
- public static ImmutableHashSet WeaponTypes { get => weaponTypes; }
-
- ///
- /// 物品类型
- ///
- public static ImmutableHashSet ItemQualities { get => itemQualities; }
-
- ///
- /// 身材类型
- ///
- public static ImmutableHashSet BodyTypes { get => bodyTypes; }
-
- ///
- /// 战斗属性
- ///
- public static ImmutableHashSet FightProperties { get => fightProperties; }
-
- ///
- /// 元素名称
- ///
- public static ImmutableHashSet ElementNames { get => elementNames; }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
index 3fba8d01..fbee516d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs
@@ -411,6 +411,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
+ ///
+ /// 查找类似 网络异常 的本地化字符串。
+ ///
+ internal static string ModelBindingUserInitializationFailed {
+ get {
+ return ResourceManager.GetString("ModelBindingUserInitializationFailed", resourceCulture);
+ }
+ }
+
///
/// 查找类似 第 {0} 期 的本地化字符串。
///
@@ -1257,6 +1266,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
+ ///
+ /// 查找类似 Hoyolab 账号不支持使用 SToken 刷新祈愿记录 的本地化字符串。
+ ///
+ internal static string ServiceGachaLogUrlProviderStokenUnsupported {
+ get {
+ return ResourceManager.GetString("ServiceGachaLogUrlProviderStokenUnsupported", resourceCulture);
+ }
+ }
+
///
/// 查找类似 不支持的 Item Id:{0} 的本地化字符串。
///
@@ -1456,11 +1474,11 @@ namespace Snap.Hutao.Resource.Localization {
}
///
- /// 查找类似 输入的 Cookie 必须包含 Stoken 的本地化字符串。
+ /// 查找类似 输入的 Cookie 必须包含 SToken 的本地化字符串。
///
- 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 {
}
///
- /// 查找类似 在此处输入包含 Stoken 的 Cookie 的本地化字符串。
+ /// 查找类似 在此处输入包含 SToken 的 Cookie 的本地化字符串。
///
internal static string ViewDialogUserInputPlaceholder {
get {
@@ -2229,6 +2247,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
+ ///
+ /// 查找类似 Hoyolab 账号不支持验证 的本地化字符串。
+ ///
+ internal static string ViewModelDailyNoteHoyolabVerificationUnsupported {
+ get {
+ return ResourceManager.GetString("ViewModelDailyNoteHoyolabVerificationUnsupported", resourceCulture);
+ }
+ }
+
///
/// 查找类似 30 分钟 | 3.75 树脂 的本地化字符串。
///
@@ -3292,20 +3319,20 @@ namespace Snap.Hutao.Resource.Localization {
}
///
- /// 查找类似 Stoken 刷新 的本地化字符串。
+ /// 查找类似 SToken 刷新 的本地化字符串。
///
- internal static string ViewPageGachaLogRefreshByStoken {
+ internal static string ViewPageGachaLogRefreshBySToken {
get {
- return ResourceManager.GetString("ViewPageGachaLogRefreshByStoken", resourceCulture);
+ return ResourceManager.GetString("ViewPageGachaLogRefreshBySToken", resourceCulture);
}
}
///
/// 查找类似 使用当前用户的 Cookie 信息刷新祈愿记录 的本地化字符串。
///
- 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 {
}
}
+ ///
+ /// 查找类似 请输入你的 Hoyolab Uid 的本地化字符串。
+ ///
+ internal static string ViewPageLoginHoyoverseUserHint {
+ get {
+ return ResourceManager.GetString("ViewPageLoginHoyoverseUserHint", resourceCulture);
+ }
+ }
+
///
/// 查找类似 我已登录 的本地化字符串。
///
@@ -4687,7 +4723,7 @@ namespace Snap.Hutao.Resource.Localization {
}
///
- /// 查找类似 Cookie 操作 的本地化字符串。
+ /// 查找类似 米游社 的本地化字符串。
///
internal static string ViewUserCookieOperation {
get {
@@ -4695,6 +4731,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
+ ///
+ /// 查找类似 Hoyolab 的本地化字符串。
+ ///
+ internal static string ViewUserCookieOperation2 {
+ get {
+ return ResourceManager.GetString("ViewUserCookieOperation2", resourceCulture);
+ }
+ }
+
///
/// 查找类似 网页登录 的本地化字符串。
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index 82831ddc..992f76b0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -582,8 +582,8 @@
输入的 Cookie 必须包含 Mid
-
- 输入的 Cookie 必须包含 Stoken
+
+ 输入的 Cookie 必须包含 SToken
输入的 Cookie 无法获取用户信息
@@ -757,7 +757,7 @@
操作文档
- 在此处输入包含 Stoken 的 Cookie
+ 在此处输入包含 SToken 的 Cookie
祈愿记录
@@ -1185,10 +1185,10 @@
使用由你提供的 Url 刷新祈愿记录
-
- Stoken 刷新
+
+ SToken 刷新
-
+
使用当前用户的 Cookie 信息刷新祈愿记录
@@ -1639,7 +1639,7 @@
工具
- Cookie 操作
+ 米游社
网页登录
@@ -1806,4 +1806,19 @@
立即登录或注册
+
+ 网络异常
+
+
+ Hoyolab 账号不支持使用 SToken 刷新祈愿记录
+
+
+ Hoyolab 账号不支持验证
+
+
+ 请输入你的 Hoyolab Uid
+
+
+ Hoyolab
+
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs
index 68b036a9..866f3d30 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs
@@ -24,14 +24,17 @@ namespace Snap.Hutao.Service.AvatarInfo;
internal sealed class AvatarInfoDbOperation
{
private readonly AppDbContext appDbContext;
+ private readonly IServiceProvider serviceProvider;
///
/// 构造一个新的角色信息数据库操作
///
/// 数据库上下文
- public AvatarInfoDbOperation(AppDbContext appDbContext)
+ /// 服务提供器
+ public AvatarInfoDbOperation(AppDbContext appDbContext, IServiceProvider serviceProvider)
{
this.appDbContext = appDbContext;
+ this.serviceProvider = serviceProvider;
}
///
@@ -90,24 +93,24 @@ internal sealed class AvatarInfoDbOperation
.ToList();
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
- GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService();
+ IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService(userAndUid.User.IsOversea);
Response playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
- token.ThrowIfCancellationRequested();
-
if (playerInfoResponse.IsOk())
{
Response charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
+ token.ThrowIfCancellationRequested();
+
if (charactersResponse.IsOk())
{
List characters = charactersResponse.Data.Avatars;
- GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService();
+ GameRecordCharacterAvatarInfoComposer composer = serviceProvider.GetRequiredService();
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);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
index 04235ba2..5306daeb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs
@@ -31,20 +31,16 @@ internal sealed class AvatarInfoService : IAvatarInfoService
/// 构造一个新的角色信息服务
///
/// 数据库上下文
- /// 元数据服务
- /// 简述工厂
- /// 日志器
+ /// 服务提供器
public AvatarInfoService(
AppDbContext appDbContext,
- IMetadataService metadataService,
- ISummaryFactory summaryFactory,
- ILogger logger)
+ IServiceProvider serviceProvider)
{
- this.metadataService = metadataService;
- this.summaryFactory = summaryFactory;
- this.logger = logger;
+ metadataService = serviceProvider.GetRequiredService();
+ summaryFactory = serviceProvider.GetRequiredService();
+ logger = serviceProvider.GetRequiredService>();
- avatarInfoDbOperation = new(appDbContext);
+ avatarInfoDbOperation = new(appDbContext, serviceProvider);
}
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs
index 8bf52e47..9f91aedd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs
@@ -60,23 +60,17 @@ internal sealed class DailyNoteNotifier
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService();
AuthClient authClient = scope.ServiceProvider.GetRequiredService();
- Response actionTicketResponse = await authClient
- .GetActionTicketByStokenAsync("game_role", entry.User)
+ string? attribution = SH.ServiceDailyNoteNotifierAttribution;
+
+ Response> rolesResponse = await scope.ServiceProvider
+ .GetRequiredService()
+ .GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
- string? attribution = SH.ServiceDailyNoteNotifierAttribution;
- if (actionTicketResponse.IsOk())
+ if (rolesResponse.IsOk())
{
- Response> rolesResponse = await scope.ServiceProvider
- .GetRequiredService()
- .GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User)
- .ConfigureAwait(false);
-
- if (rolesResponse.IsOk())
- {
- List roles = rolesResponse.Data.List;
- attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn";
- }
+ List roles = rolesResponse.Data.List;
+ attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unknown";
}
ToastContentBuilder builder = new ToastContentBuilder()
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
index 3add8387..8b98e0df 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs
@@ -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();
- GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService();
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
{
DailyNoteEntry newEntry = DailyNoteEntry.Create(role);
- Web.Response.Response dailyNoteResponse = await gameRecordClient
+ Web.Response.Response dailyNoteResponse = await scope.ServiceProvider
+ .PickRequiredService(PlayerUid.IsOversea(roleUid))
.GetDailyNoteAsync(role)
.ConfigureAwait(false);
@@ -109,12 +109,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient();
- GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService();
+ // TODO: Add this option to AppOptions
bool isSilentMode = appDbContext.Settings
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
.GetBoolean();
bool isGameRunning = scope.ServiceProvider.GetRequiredService().IsGameRunning();
+
if (isSilentMode && isGameRunning)
{
// Prevent notify when we are in game && silent mode.
@@ -123,7 +124,8 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient n.User))
{
- Web.Response.Response dailyNoteResponse = await gameRecordClient
+ Web.Response.Response dailyNoteResponse = await scope.ServiceProvider
+ .PickRequiredService(PlayerUid.IsOversea(entry.Uid))
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
index c7785c81..094799c5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/GachaLogService.cs
@@ -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,
};
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs
index 2db0d111..066a361c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs
@@ -10,11 +10,11 @@ using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
///
-/// 使用Stokn提供祈愿Url
+/// 使用 SToken 提供祈愿 Url
///
[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
///
/// 用户服务
/// 绑定客户端
- public GachaLogQueryStokenProvider(IUserService userService, BindingClient2 bindingClient2)
+ public GachaLogQuerySTokenProvider(IUserService userService, BindingClient2 bindingClient2)
{
this.userService = userService;
this.bindingClient2 = bindingClient2;
}
///
- public string Name { get => nameof(GachaLogQueryStokenProvider); }
+ public string Name { get => nameof(GachaLogQuerySTokenProvider); }
///
public async Task> 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 authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs
index 04b66c0f..c71a64e6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/IGachaLogQueryProvider.cs
@@ -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;
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs
index e59d8bce..b9427cd5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/RefreshOption.cs
@@ -24,7 +24,7 @@ internal enum RefreshOption
///
/// 通过Stoken刷新
///
- Stoken,
+ SToken,
///
/// 手动输入Url刷新
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs
index e365b570..4e9d6dde 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/IGameLocator.cs
@@ -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;
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs
index 749aef85..aeea4eaa 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 游戏文件包转换器
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class PackageConverter
{
private readonly JsonSerializerOptions options;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
index 2e613867..f97e23d4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs
@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Metadata;
///
[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";
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs
index 45365467..517459bc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs
@@ -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? spiralAbysses;
@@ -28,12 +28,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
///
/// 构造一个新的深渊记录服务
///
- /// 数据库上下文
- /// 游戏记录客户端
- public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient)
+ /// 服务提供器
+ public SpiralAbyssRecordService(IServiceProvider serviceProvider)
{
- this.appDbContext = appDbContext;
- this.gameRecordClient = gameRecordClient;
+ appDbContext = serviceProvider.GetRequiredService();
+ this.serviceProvider = serviceProvider;
}
///
@@ -70,7 +69,8 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
- Response response = await gameRecordClient
+ Response response = await serviceProvider
+ .PickRequiredService(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs
index 8546d4a8..3e34f08e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs
@@ -45,8 +45,9 @@ internal interface IUserService
/// 尝试异步处理输入的Cookie
///
/// Cookie
+ /// 是否为国际服
/// 处理的结果
- Task> ProcessInputCookieAsync(Cookie cookie);
+ Task> ProcessInputCookieAsync(Cookie cookie, bool isOversea);
///
/// 异步刷新 Cookie 的 CookieToken
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
index e111713c..69fcb40c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs
@@ -191,7 +191,7 @@ internal class UserService : IUserService
}
///
- public async Task> ProcessInputCookieAsync(Cookie cookie)
+ public async Task> 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();
- 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 cookieTokenResponse = await scope.ServiceProvider
- .GetRequiredService()
- .GetCookieAccountInfoBySTokenAsync(user.Entity)
- .ConfigureAwait(false);
+ .PickRequiredService(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> TryCreateUserAndAddAsync(Cookie cookie)
+ private async Task> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
{
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService();
+ BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
- BindingUser? newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs
index e712f613..f880b515 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs
@@ -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)
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
index 3d2416fe..fa6c400c 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml
@@ -48,9 +48,9 @@
+ Text="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySToken}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs
new file mode 100644
index 00000000..a174b0fe
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs
@@ -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;
+
+///
+/// 登录米哈游通行证页面
+///
+internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
+{
+ ///
+ /// 构造一个新的登录米哈游通行证页面
+ ///
+ public LoginHoyoverseUserPage()
+ {
+ InitializeComponent();
+ }
+
+ [SuppressMessage("", "VSTHRD100")]
+ private async void OnRootLoaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ await WebView.EnsureCoreWebView2Async();
+
+ CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
+ IReadOnlyList cookies = await manager.GetCookiesAsync("https://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().Error(ex);
+ }
+ }
+
+ private async Task HandleCurrentCookieAsync(CancellationToken token = default)
+ {
+ CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
+ IReadOnlyList cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
+
+ IInfoBarService infoBarService = Ioc.Default.GetRequiredService();
+
+ // 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> multiTokenResponse = await Ioc.Default
+ .GetRequiredService()
+ .GetMultiTokenByLoginTicketAsync(loginTicketCookie, true, token)
+ .ConfigureAwait(false);
+
+ if (!multiTokenResponse.IsOk())
+ {
+ return;
+ }
+
+ Dictionary 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()
+ .ProcessInputCookieAsync(hoyoLabCookie, true)
+ .ConfigureAwait(false);
+
+ Ioc.Default.GetRequiredService().GoBack();
+
+ switch (result)
+ {
+ case UserOptionResult.Added:
+ ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService();
+ if (vm.Users!.Count == 1)
+ {
+ await ThreadHelper.SwitchToMainThreadAsync();
+ vm.SelectedUser = vm.Users.Single();
+ }
+
+ infoBarService.Success(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();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginMihoyoUserPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginMihoyoUserPage.xaml.cs
index ad2314e7..06fdd656 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginMihoyoUserPage.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginMihoyoUserPage.xaml.cs
@@ -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 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> multiTokenResponse = await Ioc.Default
.GetRequiredService()
- .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 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 loginResultResponse = await Ioc.Default
- .GetRequiredService()
- .LoginByStokenAsync(stokenV1, token)
+ .GetRequiredService()
+ .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()
- .ProcessInputCookieAsync(stokenV2)
+ .ProcessInputCookieAsync(stokenV2, false)
.ConfigureAwait(false);
Ioc.Default.GetRequiredService().GoBack();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService();
+ // 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();
}
}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
index 8bd5f811..8ed284fd 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
@@ -116,10 +116,7 @@
SelectionMode="Single">
-
+
@@ -204,27 +201,65 @@
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewUserDefaultDescription}"
Visibility="{Binding Users.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}"/>
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
index dace233f..519f9bbd 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/DailyNoteViewModel.cs
@@ -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().Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported);
+ }
+ else
+ {
+ // ContentDialog must be created by main thread.
+ await ThreadHelper.SwitchToMainThreadAsync();
+ await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
+ }
}
else
{
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs
index c29176fa..83e88cce 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/GachaLog/GachaLogViewModel.cs
@@ -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; }
///
- /// Stoken 刷新命令
+ /// SToken 刷新命令
///
- public ICommand RefreshByStokenCommand { get; }
+ public ICommand RefreshBySTokenCommand { get; }
///
/// 手动输入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()
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
index d59490b1..4a59406b 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
@@ -46,7 +46,9 @@ internal sealed class UserViewModel : ObservableObject
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
+ AddOverseaUserCommand = new AsyncRelayCommand(AddOverseaUserAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
+ LoginHoyoverseUserCommand = new RelayCommand(LoginHoyoverseUser);
RemoveUserCommand = new AsyncRelayCommand(RemoveUserAsync);
CopyCookieCommand = new RelayCommand(CopyCookie);
RefreshCookieTokenCommand = new AsyncRelayCommand(RefreshCookieTokenAsync);
@@ -87,11 +89,21 @@ internal sealed class UserViewModel : ObservableObject
///
public ICommand AddUserCommand { get; }
+ ///
+ /// 添加国际服用户命令
+ ///
+ public ICommand AddOverseaUserCommand { get; }
+
///
/// 登录米游社命令
///
public ICommand LoginMihoyoUserCommand { get; }
+ ///
+ /// 登录米游社命令
+ ///
+ public ICommand LoginHoyoverseUserCommand { get; }
+
///
/// 移除用户命令
///
@@ -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
}
}
+ ///
+ /// 打开浏览器登录 hoyolab 以获取 cookie
+ ///
+ private void LoginHoyoverseUser()
+ {
+ if (Core.WebView2Helper.IsSupported)
+ {
+ serviceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default);
+ }
+ else
+ {
+ infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
+ }
+ }
+
private async Task RemoveUserAsync(User? user)
{
if (user != null)
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
index 21dd5f16..885cd68a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
@@ -46,7 +46,7 @@ internal static class ApiEndpoints
/// 获取 stoken 与 ltoken
///
/// 操作类型 game_role
- /// Stoken
+ /// SToken
/// uid
/// Url
public static string AuthActionTicket(string actionType, string stoken, string uid)
@@ -86,7 +86,7 @@ internal static class ApiEndpoints
///
/// 用户游戏角色
///
- public const string UserGameRolesByStoken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
+ public const string UserGameRolesBySToken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
///
/// AuthKey
@@ -289,12 +289,12 @@ internal static class ApiEndpoints
public const string AccountGetCookieTokenBySToken = $"{PassportApiAuthApi}/getCookieAccountInfoBySToken";
///
- /// 获取Ltoken
+ /// 获取LToken
///
- public const string AccountGetLtokenByStoken = $"{PassportApiAuthApi}/getLTokenBySToken";
+ public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken";
///
- /// 获取V2Stoken
+ /// 获取V2SToken
///
public const string AccountGetSTokenByOldToken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
index 6743bd2d..88fd90c6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Game;
+using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Web;
@@ -10,9 +12,161 @@ namespace Snap.Hutao.Web;
///
[HighQuality]
[SuppressMessage("", "SA1201")]
+[SuppressMessage("", "SA1202")]
[SuppressMessage("", "SA1124")]
internal static class ApiOsEndpoints
{
+ #region ApiAccountOsApi
+
+ ///
+ /// Hoyolab App Login api
+ /// Can fetch stoken
+ ///
+ public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
+
+ ///
+ /// 获取 Ltoken
+ ///
+ public const string AccountGetLTokenBySToken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
+
+ ///
+ /// fetch CookieToken
+ ///
+ public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
+ #endregion
+
+ #region ApiGeetest
+
+ ///
+ /// 获取GT码
+ ///
+ /// gt
+ /// GT码Url
+ public static string GeetestGetType(string gt)
+ {
+ return $"{ApiNaGeetest}/gettype.php?gt={gt}";
+ }
+
+ ///
+ /// 验证接口
+ ///
+ /// gt
+ /// challenge流水号
+ /// 验证接口Url
+ 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
+
+ ///
+ /// 获取 stoken 与 ltoken
+ ///
+ /// 登录票证
+ /// uid
+ /// Url
+ public static string AuthMultiToken(string loginTicket, string loginUid)
+ {
+ return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
+ }
+
+ ///
+ /// 获取 stoken 与 ltoken
+ ///
+ /// 操作类型 game_role
+ /// SToken
+ /// uid
+ /// Url
+ 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
+
+ ///
+ /// 用户游戏角色
+ ///
+ /// 用户游戏角色字符串
+ public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
+
+ ///
+ /// 用户游戏角色
+ ///
+ /// 地区代号
+ /// 用户游戏角色字符串
+ public static string UserGameRolesByLtoken(string region)
+ {
+ return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region}";
+ }
+
+ #endregion
+
+ #region BbsApiOsApi
+
+ ///
+ /// 查询其他用户详细信息
+ ///
+ /// bbs Uid
+ /// 查询其他用户详细信息字符串
+ public static string UserFullInfoQuery(string bbsUid)
+ {
+ return $"{BbsApiOs}/community/painter/wapi/user/full";
+ }
+
+ ///
+ /// 国际服角色基本信息
+ ///
+ /// uid
+ /// 角色基本信息字符串
+ public static string GameRecordRoleBasicInfo(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
+ }
+
+ ///
+ /// 国际服角色信息
+ ///
+ public const string GameRecordCharacter = $"{BbsApiOsGameRecordApi}/character";
+
+ ///
+ /// 国际服游戏记录实时便笺
+ ///
+ /// uid
+ /// 游戏记录实时便笺字符串
+ public static string GameRecordDailyNote(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
+ }
+
+ ///
+ /// 国际服游戏记录主页
+ ///
+ /// uid
+ /// 游戏记录主页字符串
+ public static string GameRecordIndex(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
+ }
+
+ ///
+ /// 国际服深渊信息
+ ///
+ /// 深渊类型
+ /// Uid
+ /// 深渊信息字符串
+ public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
+ }
+
+ #endregion
+
#region Hk4eApiOsGachaInfoApi
///
@@ -26,6 +180,46 @@ internal static class ApiOsEndpoints
}
#endregion
+ #region SgPublicApi
+
+ ///
+ /// 计算器家具计算
+ ///
+ public const string CalculateFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
+
+ ///
+ /// 计算器角色列表 size 20
+ ///
+ public const string CalculateAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
+
+ ///
+ /// 计算器武器列表 size 20
+ ///
+ public const string CalculateWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
+
+ ///
+ /// 计算器结果
+ ///
+ public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
+
+ ///
+ /// 计算器同步角色详情 size 20
+ ///
+ /// 角色Id
+ /// uid
+ /// 角色详情
+ 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}";
+ }
+
+ ///
+ /// 计算器同步角色列表 size 20
+ ///
+ public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
+
+ #endregion
+
#region SdkStaticLauncherApi
///
@@ -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";
+
+ ///
+ /// Web static referer
+ ///
+ public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
+
+ ///
+ /// Act hoyolab referer
+ ///
+ public const string ActHoyolabReferer = "https://act.hoyolab.com/";
+
#endregion
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs
index 3decfd09..be4030ee 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/CoreWebView2Extension.cs
@@ -23,15 +23,26 @@ internal static class CoreWebView2Extension
return webView;
}
+ ///
+ /// 设置 移动端OsUA
+ ///
+ /// webview2
+ /// 链式调用的WebView2
+ public static CoreWebView2 SetMobileOverseaUserAgent(this CoreWebView2 webView)
+ {
+ webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabOsMobileUA;
+ return webView;
+ }
+
///
/// 设置WebView2的Cookie
///
/// webview2
/// CookieToken
- /// Ltoken
- /// Stoken
+ /// LToken
+ /// SToken
/// 链式调用的WebView2
- 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;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
index 298dbcc5..dfdf5529 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSInterface.cs
@@ -70,7 +70,7 @@ internal class MiHoYoJSInterface
User user = serviceProvider.GetRequiredService().Current!;
return await serviceProvider
.GetRequiredService()
- .GetActionTicketByStokenAsync(jsParam.Payload!.ActionType, user.Entity)
+ .GetActionTicketBySTokenAsync(jsParam.Payload!.ActionType, user.Entity)
.ConfigureAwait(false);
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
index 93ff907a..a3a57db7 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Enka/EnkaClient.cs
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Enka;
/// Enka API 客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class EnkaClient
{
private const string EnkaAPI = "https://enka.network/api/uid/{0}";
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs
index 345b1ab4..f17ee448 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Geetest/GeetestClient.cs
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Geetest;
/// 极验客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class GeetestClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs
index 1ea0a8ae..84ee96be 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/App/Account/AccountClient.cs
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.App.Account;
///
[HighQuality]
[UseDynamicSecret]
-[HttpClient(HttpClientConfigration.XRpc)]
+[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class AccountClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs
new file mode 100644
index 00000000..7c19f4cf
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/IUserClient.cs
@@ -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;
+
+///
+/// 用户信息客户端
+///
+internal interface IUserClient : IOverseaSupport
+{
+ ///
+ /// 获取当前用户详细信息
+ ///
+ /// 用户
+ /// 取消令牌
+ /// 详细信息
+ Task> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default);
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs
index 240c18bb..00839239 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs
@@ -14,8 +14,8 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
///
[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;
}
+ ///
+ public bool IsOversea => false;
+
///
/// 获取当前用户详细信息
///
@@ -52,4 +55,22 @@ internal sealed class UserClient
return Response.Response.DefaultIfNull(resp);
}
+
+ ///
+ /// 获取当前用户详细信息,使用 LToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// 详细信息
+ [ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
+ public async Task> GetOsUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(user, CookieType.LToken)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs
new file mode 100644
index 00000000..76d0f237
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClientOversea.cs
@@ -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;
+
+///
+/// 用户信息客户端 Hoyolab版
+///
+[UseDynamicSecret]
+[HttpClient(HttpClientConfiguration.XRpc, typeof(IUserClient))]
+internal sealed class UserClientOversea : IUserClient
+{
+ private readonly HttpClient httpClient;
+ private readonly JsonSerializerOptions options;
+ private readonly ILogger logger;
+
+ ///
+ /// 构造一个新的用户信息客户端
+ ///
+ /// http客户端
+ /// Json序列化选项
+ /// 日志器
+ public UserClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ {
+ this.httpClient = httpClient;
+ this.options = options;
+ this.logger = logger;
+ }
+
+ ///
+ public bool IsOversea => true;
+
+ ///
+ /// 获取当前用户详细信息,使用 LToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// 详细信息
+ [ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
+ public async Task> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(user, CookieType.LToken)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
index 846117ea..8c520dc6 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
@@ -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)
+ ///
+ /// 提取其中的 stoken 信息
+ /// Used for hoyolab account.
+ ///
+ /// A cookie contains stoken and stuid, without mid.
+ /// 是否获取成功
+ 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);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
index e9c18fcd..ab1f0a8e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
@@ -38,4 +38,9 @@ internal enum SaltType
/// LK2
///
LK2,
+
+ ///
+ /// Hoyolab K2
+ ///
+ OSK2,
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs
index 4a9224b0..97965579 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Common/Announcement/AnnouncementClient.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
///
/// 公告客户端
///
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class AnnouncementClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs
index 44ad80e3..1454db93 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Event/GachaInfo/GachaInfoClient.cs
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
/// 祈愿记录客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class GachaInfoClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs
new file mode 100644
index 00000000..6012590f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/IPassportClient.cs
@@ -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;
+
+///
+/// 通行证客户端
+///
+internal interface IPassportClient : IOverseaSupport
+{
+ ///
+ /// 异步获取 CookieToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// cookie token
+ Task> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default);
+
+ ///
+ /// 异步获取 LToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// uid 与 cookie token
+ Task> GetLTokenBySTokenAsync(User user, CancellationToken token = default);
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LoginResult.cs
index d5609a42..3253bc77 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LoginResult.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LoginResult.cs
@@ -10,7 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
internal class LoginResult
{
///
- /// Stoken 包装
+ /// SToken 包装
///
[JsonPropertyName("token")]
public TokenWrapper Token { get; set; } = default!;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LtokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LtokenWrapper.cs
index a3a5d694..cd6e66b3 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LtokenWrapper.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/LtokenWrapper.cs
@@ -4,14 +4,14 @@
namespace Snap.Hutao.Web.Hoyolab.Passport;
///
-/// Ltoken 包装器
+/// LToken 包装器
///
[HighQuality]
-internal sealed class LtokenWrapper
+internal sealed class LTokenWrapper
{
///
- /// Ltoken
+ /// LToken
///
[JsonPropertyName("ltoken")]
- public string Ltoken { get; set; } = default!;
+ public string LToken { get; set; } = default!;
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs
index bbed4295..c131fc6f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient.cs
@@ -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;
/// 通行证客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[UseDynamicSecret]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class PassportClient
{
private readonly HttpClient httpClient;
@@ -34,7 +37,7 @@ internal sealed class PassportClient
}
///
- /// 异步验证Ltoken
+ /// 异步验证 LToken
///
/// 用户
/// 取消令牌
@@ -50,6 +53,28 @@ internal sealed class PassportClient
return Response.Response.DefaultIfNull(response);
}
+ ///
+ /// V1 SToken 登录
+ ///
+ /// v1 SToken
+ /// 取消令牌
+ /// 登录数据
+ [ApiInformation(Salt = SaltType.PROD)]
+ public async Task> 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? resp = await message.Content
+ .ReadFromJsonAsync>(options, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
private class Timestamp
{
[JsonPropertyName("t")]
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs
index b9542300..bf5019bc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs
@@ -16,8 +16,8 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
///
[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
/// 构造一个新的通行证客户端
///
/// http客户端
- /// json序列化选项
+ /// Json序列化选项
/// 日志器
public PassportClient2(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
{
@@ -36,27 +36,8 @@ internal sealed class PassportClient2
this.logger = logger;
}
- ///
- /// V1Stoken 登录
- ///
- /// v1 Stoken
- /// 取消令牌
- /// 登录数据
- [ApiInformation(Salt = SaltType.PROD)]
- public async Task> 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? resp = await message.Content
- .ReadFromJsonAsync>(options, token)
- .ConfigureAwait(false);
-
- return Response.Response.DefaultIfNull(resp);
- }
+ ///
+ public bool IsOversea => false;
///
/// 异步获取 CookieToken
@@ -77,18 +58,18 @@ internal sealed class PassportClient2
}
///
- /// 异步获取 Ltoken
+ /// 异步获取 LToken
///
/// 用户
/// 取消令牌
/// uid 与 cookie token
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
- public async Task> GetLTokenBySTokenAsync(User user, CancellationToken token)
+ public async Task> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
- Response? resp = await httpClient
+ Response? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
- .TryCatchGetFromJsonAsync>(ApiEndpoints.AccountGetLtokenByStoken, options, logger, token)
+ .TryCatchGetFromJsonAsync>(ApiEndpoints.AccountGetLTokenBySToken, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs
new file mode 100644
index 00000000..ecc4eb02
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClientOversea.cs
@@ -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;
+
+///
+/// 通行证客户端 XRPC 版
+///
+[HttpClient(HttpClientConfiguration.XRpc3, typeof(IPassportClient))]
+internal sealed class PassportClientOversea : IPassportClient
+{
+ private readonly HttpClient httpClient;
+ private readonly JsonSerializerOptions options;
+ private readonly ILogger logger;
+
+ ///
+ /// 构造一个新的国际服通行证客户端
+ ///
+ /// http客户端
+ /// Json序列化选项
+ /// 日志器
+ public PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ {
+ this.httpClient = httpClient;
+ this.options = options;
+ this.logger = logger;
+ }
+
+ ///
+ public bool IsOversea => true;
+
+ ///
+ /// 异步获取 CookieToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// cookie token
+ [ApiInformation(Cookie = CookieType.SToken)]
+ public async Task> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
+ {
+ STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
+
+ Response? resp = await httpClient
+ .SetUser(user, CookieType.SToken)
+ .TryCatchPostAsJsonAsync>(ApiOsEndpoints.AccountGetCookieTokenBySToken, data, options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 异步获取 LToken
+ ///
+ /// 用户
+ /// 取消令牌
+ /// uid 与 cookie token
+ [ApiInformation(Cookie = CookieType.SToken)]
+ public async Task> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
+ {
+ STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
+
+ Response? resp = await httpClient
+ .SetUser(user, CookieType.SToken)
+ .TryCatchPostAsJsonAsync>(ApiOsEndpoints.AccountGetLTokenBySToken, data, options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/STokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/STokenWrapper.cs
new file mode 100644
index 00000000..04d0d8fc
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/STokenWrapper.cs
@@ -0,0 +1,33 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Web.Hoyolab.Passport;
+
+///
+/// SToken 包装器
+///
+internal sealed class STokenWrapper
+{
+ ///
+ /// 构造一个新的SToken 包装器
+ ///
+ /// stoken
+ /// uid
+ public STokenWrapper(string stoken, string uid)
+ {
+ SToken = stoken;
+ Uid = uid;
+ }
+
+ ///
+ /// SToken
+ ///
+ [JsonPropertyName("stoken")]
+ public string SToken { get; set; }
+
+ ///
+ /// Uid
+ ///
+ [JsonPropertyName("uid")]
+ public string Uid { get; set; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
index 25d4fe49..7add9407 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/PlayerUid.cs
@@ -36,6 +36,22 @@ internal readonly struct PlayerUid
return new(source);
}
+ ///
+ /// 判断是否为国际服
+ /// We make this a static method rather than property,
+ /// to avoid unnecessary memory allocation.
+ ///
+ /// uid
+ /// 是否为国际服
+ public static bool IsOversea(string uid)
+ {
+ return uid[0] switch
+ {
+ >= '1' and <= '4' => false,
+ _ => true,
+ };
+ }
+
///
public override string ToString()
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
index 06a76df3..907e9fef 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// 游戏资源客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class ResourceClient
{
private readonly HttpClient httpClient;
@@ -40,7 +40,7 @@ internal sealed class ResourceClient
/// 游戏资源
public async Task> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
- string url = scheme.LauncherId == "10"
+ string url = scheme.IsOversea
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
: ApiEndpoints.SdkStaticLauncherResource(scheme);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs
index d67b4359..cb31990b 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
///
[HighQuality]
[UseDynamicSecret]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class AuthClient
{
private readonly HttpClient httpClient;
@@ -43,13 +43,15 @@ internal sealed class AuthClient
/// 用户
/// 操作凭证
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
- public async Task> GetActionTicketByStokenAsync(string action, User user)
+ public async Task> GetActionTicketBySTokenAsync(string action, User user)
{
+ string url = ApiEndpoints.AuthActionTicket(action, user.SToken?[Cookie.STOKEN] ?? string.Empty, user.Aid!);
+
Response? resp = await httpClient
- .SetUser(user, CookieType.SToken)
- .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
- .TryCatchGetFromJsonAsync>(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>(url, options, logger)
+ .ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
@@ -58,15 +60,20 @@ internal sealed class AuthClient
/// 获取 MultiToken
///
/// login cookie
+ /// 是否为国际服
/// 取消令牌
/// 包含token的字典
- public async Task>> GetMultiTokenByLoginTicketAsync(Cookie cookie, CancellationToken token)
+ public async Task>> 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>? resp = await httpClient
- .TryCatchGetFromJsonAsync>>(ApiEndpoints.AuthMultiToken(loginTicket, loginUid), options, logger, token)
+ .TryCatchGetFromJsonAsync>>(url, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs
index a8c598fe..d18c186a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs
@@ -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;
/// 绑定客户端
///
[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 logger;
@@ -23,18 +26,53 @@ internal sealed class BindingClient
///
/// 构造一个新的用户游戏角色提供器
///
+ /// 服务提供器
/// 请求器
/// Json序列化选项
/// 日志器
- public BindingClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ public BindingClient(IServiceProvider serviceProvider, HttpClient httpClient)
{
+ options = serviceProvider.GetRequiredService();
+ logger = serviceProvider.GetRequiredService>();
+
+ this.serviceProvider = serviceProvider;
this.httpClient = httpClient;
- this.options = options;
- this.logger = logger;
}
///
- /// 获取用户角色信息
+ /// 异步获取用户角色信息
+ /// 自动判断是否为国际服
+ ///
+ /// 用户
+ /// 取消令牌
+ /// 用户角色信息
+ public async Task>> GetUserGameRolesOverseaAwareAsync(User user, CancellationToken token = default)
+ {
+ if (user.IsOversea)
+ {
+ return await GetOverseaUserGameRolesByCookieAsync(user, token).ConfigureAwait(false);
+ }
+ else
+ {
+ Response actionTicketResponse = await serviceProvider
+ .GetRequiredService()
+ .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, ActionTicketWrapper>(actionTicketResponse);
+ }
+ }
+ }
+
+ ///
+ /// 异步获取用户角色信息
///
/// 操作凭证
/// 用户
@@ -52,4 +90,21 @@ internal sealed class BindingClient
return Response.Response.DefaultIfNull(resp);
}
+
+ ///
+ /// 异步获取国际服用户角色信息
+ ///
+ /// 用户
+ /// 取消令牌
+ /// 用户角色信息
+ [ApiInformation(Cookie = CookieType.LToken)]
+ public async Task>> GetOverseaUserGameRolesByCookieAsync(User user, CancellationToken token = default)
+ {
+ Response>? resp = await httpClient
+ .SetUser(user, CookieType.LToken)
+ .TryCatchGetFromJsonAsync>>(ApiOsEndpoints.UserGameRolesByCookie, options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs
index a1c6ce14..3f1e63af 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient2.cs
@@ -11,11 +11,11 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
///
-/// Stoken绑定客户端
+/// SToken绑定客户端
///
[HighQuality]
[UseDynamicSecret]
-[HttpClient(HttpClientConfigration.XRpc)]
+[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class BindingClient2
{
private readonly HttpClient httpClient;
@@ -42,12 +42,12 @@ internal sealed class BindingClient2
/// 取消令牌
/// 用户角色信息
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.LK2)]
- public async Task> GetUserGameRolesByStokenAsync(User user, CancellationToken token = default)
+ public async Task> GetUserGameRolesBySTokenAsync(User user, CancellationToken token = default)
{
Response>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.LK2, true)
- .TryCatchGetFromJsonAsync>>(ApiEndpoints.UserGameRolesByStoken, options, logger, token)
+ .TryCatchGetFromJsonAsync>>(ApiEndpoints.UserGameRolesBySToken, options, logger, token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data?.List);
@@ -55,7 +55,7 @@ internal sealed class BindingClient2
///
/// 异步生成祈愿验证密钥
- /// 需要stoken
+ /// 需要 SToken
///
/// 用户
/// 提交数据
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs
index 609796c5..f481fdcc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/CalculateClient.cs
@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
/// 养成计算器客户端
///
[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> 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? resp = await httpClient
.SetUser(user, CookieType.Cookie)
- .SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
- .TryCatchPostAsJsonAsync>(ApiEndpoints.CalculateCompute, delta, options, logger, token)
+ .SetReferer(referer)
+ .TryCatchPostAsJsonAsync>(url, delta, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -65,14 +73,24 @@ internal sealed class CalculateClient
List avatars = new();
Response>? 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>>(ApiEndpoints.CalculateSyncAvatarList, filter, options, logger, token)
+ .TryCatchPostAsJsonAsync>>(url, filter, options, logger, token)
.ConfigureAwait(false);
if (resp != null && resp.IsOk())
@@ -101,9 +119,13 @@ internal sealed class CalculateClient
/// 角色详情
public async Task> 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? resp = await httpClient
.SetUser(userAndUid.User, CookieType.CookieToken)
- .TryCatchGetFromJsonAsync>(ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
+ .TryCatchGetFromJsonAsync>(url, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/CharacterData.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/CharacterData.cs
new file mode 100644
index 00000000..0eb01900
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/CharacterData.cs
@@ -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;
+
+///
+/// 角色 POST 数据
+///
+internal sealed class CharacterData
+{
+ ///
+ /// 构造一个新的角色 POST 数据
+ ///
+ /// uid
+ /// 角色id
+ public CharacterData(PlayerUid uid, IEnumerable characterIds)
+ {
+ CharacterIds = characterIds;
+ Uid = uid.Value;
+ Server = uid.Region;
+ }
+
+ ///
+ /// 角色id
+ ///
+ [JsonPropertyName("character_ids")]
+ public IEnumerable CharacterIds { get; }
+
+ ///
+ /// uid
+ ///
+ [JsonPropertyName("role_id")]
+ public string Uid { get; } = default!;
+
+ ///
+ /// 服务器
+ ///
+ [JsonPropertyName("server")]
+ public string Server { get; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs
index 601e8663..b863f3ce 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/CardClient.cs
@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
///
[HighQuality]
[UseDynamicSecret]
-[HttpClient(HttpClientConfigration.XRpc)]
+[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class CardClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
index 86f2cbf7..8482fe22 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs
@@ -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;
///
[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 logger;
@@ -30,15 +29,19 @@ internal sealed class GameRecordClient
/// 构造一个新的游戏记录提供器
///
/// 请求器
- /// json序列化选项
- /// 日志器
- public GameRecordClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ /// 访问提供器
+ public GameRecordClient(HttpClient httpClient, IServiceProvider serviceProvider)
{
+ options = serviceProvider.GetRequiredService();
+ logger = serviceProvider.GetRequiredService>();
+
this.httpClient = httpClient;
- this.options = options;
- this.logger = logger;
+ this.serviceProvider = serviceProvider;
}
+ ///
+ public bool IsOversea => false;
+
///
/// 异步获取实时便笺
///
@@ -54,16 +57,14 @@ internal sealed class GameRecordClient
.TryCatchGetFromJsonAsync>(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 = serviceProvider.GetRequiredService();
if (await cardVerifier.TryGetXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is string challenge)
{
- Ioc.Default.GetRequiredService().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 characterIds)
- {
- CharacterIds = characterIds;
- Uid = uid.Value;
- Server = uid.Region;
- }
-
- [JsonPropertyName("character_ids")]
- public IEnumerable CharacterIds { get; }
-
- [JsonPropertyName("role_id")]
- public string Uid { get; }
-
- [JsonPropertyName("server")]
- public string Server { get; }
- }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs
new file mode 100644
index 00000000..215e3b4a
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOversea.cs
@@ -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;
+
+///
+/// Hoyoverse game record provider
+///
+[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 logger;
+
+ ///
+ /// 构造一个新的游戏记录提供器
+ ///
+ /// 请求器
+ /// json序列化选项
+ /// 日志器
+ public GameRecordClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ {
+ this.httpClient = httpClient;
+ this.options = options;
+ this.logger = logger;
+ }
+
+ ///
+ public bool IsOversea => true;
+
+ ///
+ /// 异步获取实时便笺
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 实时便笺
+ [ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
+ public async Task> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家基础信息
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 玩家的基础信息
+ [ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
+ public async Task> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家深渊信息
+ ///
+ /// 用户
+ /// 1:当期,2:上期
+ /// 取消令牌
+ /// 深渊信息
+ [ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
+ public async Task> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家角色详细信息
+ ///
+ /// 用户与角色
+ /// 玩家的基础信息
+ /// 取消令牌
+ /// 角色列表
+ [ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
+ public async Task> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
+ {
+ CharacterData data = new(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id));
+
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
+ .TryCatchPostAsJsonAsync>(ApiOsEndpoints.GameRecordCharacter, data, options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs
new file mode 100644
index 00000000..9ee9bb5f
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/IGameRecordClient.cs
@@ -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;
+
+///
+/// 游戏记录提供器
+///
+internal interface IGameRecordClient : IOverseaSupport
+{
+ ///
+ /// 获取玩家角色详细信息
+ ///
+ /// 用户与角色
+ /// 玩家的基础信息
+ /// 取消令牌
+ /// 角色列表
+ Task> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default);
+
+ ///
+ /// 异步获取实时便笺
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 实时便笺
+ Task> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default);
+
+ ///
+ /// 获取玩家基础信息
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 玩家的基础信息
+ Task> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default);
+
+ ///
+ /// 获取玩家深渊信息
+ ///
+ /// 用户
+ /// 1:当期,2:上期
+ /// 取消令牌
+ /// 深渊信息
+ Task> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default);
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs
index d7d91cec..84ef3834 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaLogUploadClient.cs
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃日志客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaLogUploadClient
{
private readonly HttpClient httpClient;
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs
index 9b6e3c0a..af0e6243 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaPassportClient.cs
@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃通行证客户端
///
[HighQuality]
-[HttpClient(HttpClientConfigration.Default)]
+[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaPassportClient
{
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs
index 440e2596..aa718be5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaSpiralAbyssClient.cs
@@ -18,11 +18,11 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃API客户端
///
[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 logger;
@@ -30,15 +30,14 @@ internal sealed class HomaSpiralAbyssClient
/// 构造一个新的胡桃API客户端
///
/// http客户端
- /// 游戏记录客户端
- /// json序列化选项
- /// 日志器
- public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger logger)
+ /// 服务提供器
+ public HomaSpiralAbyssClient(HttpClient httpClient, IServiceProvider serviceProvider)
{
+ options = serviceProvider.GetRequiredService();
+ logger = serviceProvider.GetRequiredService>();
+
+ this.serviceProvider = serviceProvider;
this.httpClient = httpClient;
- this.gameRecordClient = gameRecordClient;
- this.options = options;
- this.logger = logger;
}
///
@@ -186,6 +185,8 @@ internal sealed class HomaSpiralAbyssClient
/// 玩家记录
public async Task GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
{
+ IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService(userAndUid.User.IsOversea);
+
Response playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
index 9a89be1e..a5f14145 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/Response.cs
@@ -40,6 +40,12 @@ internal class Response
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
+ ///
+ /// 返回本体或带有消息提示的默认值
+ ///
+ /// 本体
+ /// 调用方法名称
+ /// 本体或默认值,当本体为 null 时 返回默认值
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);
}
+ ///
+ /// 返回本体或带有消息提示的默认值
+ ///
+ /// 类型
+ /// 其他类型
+ /// 本体
+ /// 调用方法名称
+ /// 本体或默认值,当本体为 null 时 返回默认值
+ public static Response DefaultIfNull(Response? 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);
+ }
+ }
+
///
public override string ToString()
{