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}"/> + + + + + + + + + + + + + + /// 操作类型 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() {