Merge pull request #608 from Xhichn/main

Add basic support for hoyoverse account
This commit is contained in:
DismissedLight
2023-03-27 19:03:53 +08:00
committed by GitHub
81 changed files with 1792 additions and 465 deletions

4
.gitignore vendored
View File

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

View File

@@ -11,7 +11,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
/// <summary>
/// 注入HttpClient代码生成器
@@ -21,9 +21,10 @@ namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
[Generator]
public class HttpClientGenerator : ISourceGenerator
{
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2";
private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.Default";
private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc";
private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc2";
private const string XRpc3Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.XRpc3";
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute";
@@ -44,38 +45,41 @@ public class HttpClientGenerator : ISourceGenerator
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
sourceCodeBuilder.Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class IocHttpClientConfiguration
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}","1.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
{
""");
// This class is generated by Snap.Hutao.SourceGeneration
FillWithHttpClients(receiver, sourceCodeBuilder);
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Net.Http;
sourceCodeBuilder.Append("""
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class IocHttpClientConfiguration
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
{{");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append(@"
return services;
}
}");
return services;
}
}
""");
context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
private static void FillWithHttpClients(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
@@ -84,16 +88,23 @@ internal static partial class IocHttpClientConfiguration
{
lineBuilder.Clear().Append("\r\n");
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
AttributeData httpClientInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
}
string injectAsName = injectAs.ToCSharpString();
TypedConstant configuration = arguments[0];
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
string injectAsName = configuration.ToCSharpString();
switch (injectAsName)
{
case DefaultName:
@@ -105,8 +116,11 @@ internal static partial class IocHttpClientConfiguration
case XRpc2Name:
lineBuilder.Append("XRpc2Configuration)");
break;
case XRpc3Name:
lineBuilder.Append("XRpc3Configuration)");
break;
default:
throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]");
throw new InvalidOperationException($"非法的 HttpClientConfiguration 值: [{injectAsName}]");
}
AttributeData? handlerInfo = classSymbol
@@ -120,11 +134,11 @@ internal static partial class IocHttpClientConfiguration
foreach (KeyValuePair<string, TypedConstant> property in properties)
{
lineBuilder.Append(" ");
lineBuilder.Append(' ');
lineBuilder.Append(property.Key);
lineBuilder.Append(" = ");
lineBuilder.Append(property.Value.ToCSharpString());
lineBuilder.Append(",");
lineBuilder.Append(',');
}
lineBuilder.Append(" })");
@@ -135,7 +149,7 @@ internal static partial class IocHttpClientConfiguration
lineBuilder.Append(".AddHttpMessageHandler<DynamicSecretHandler>()");
}
lineBuilder.Append(";");
lineBuilder.Append(';');
lines.Add(lineBuilder.ToString());
}

View File

@@ -11,7 +11,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Snap.Hutao.SourceGeneration.DedendencyInjection;
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
/// <summary>
/// 注入代码生成器
@@ -41,30 +41,32 @@ public class InjectionGenerator : ISourceGenerator
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($@"// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtension
{{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{toolName}"",""1.0.0.0"")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{{");
sourceCodeBuilder.Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// This class is generated by Snap.Hutao.SourceGeneration
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
internal static partial class ServiceCollectionExtension
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}","1.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{
""");
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append(@"
return services;
}
}");
sourceCodeBuilder.Append("""
return services;
}
}
""");
context.AddSource("ServiceCollectionExtension.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
@@ -76,46 +78,42 @@ internal static partial class ServiceCollectionExtension
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
IEnumerable<AttributeData> datas = classSymbol
AttributeData injectionInfo = classSymbol
.GetAttributes()
.Where(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
foreach (AttributeData injectionInfo in datas)
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped(");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton<");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient<");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped<");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
}
lineBuilder.Append($"{classSymbol.ToDisplayString()}>();");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))
@@ -153,4 +151,4 @@ internal static partial class ServiceCollectionExtension
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ namespace Snap.Hutao.Core.Caching;
/// </summary>
[HighQuality]
[Injection(InjectAs.Singleton, typeof(IImageCache))]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
[PrimaryHttpMessageHandler(MaxConnectionsPerServer = 8)]
internal sealed class ImageCache : IImageCache, IImageCacheFilePathOperation
{

View File

@@ -24,16 +24,31 @@ internal static class CoreEnvironment
/// </summary>
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// Hoyolab请求UA
/// </summary>
public const string HoyolabOsUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
/// <summary>
/// 米游社移动端请求UA
/// </summary>
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// Hoyolab 移动端请求UA
/// </summary>
public const string HoyolabOsMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBSOversea/{HoyolabOsXrpcVersion}";
/// <summary>
/// 米游社 Rpc 版本
/// </summary>
public const string HoyolabXrpcVersion = "2.44.1";
/// <summary>
/// Hoyolab Rpc 版本
/// </summary>
public const string HoyolabOsXrpcVersion = "2.28.0";
/// <summary>
/// 盐
/// </summary>
@@ -45,6 +60,9 @@ internal static class CoreEnvironment
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
// This SALT is not reliable
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
}.ToImmutableDictionary();
/// <summary>

View File

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

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
/// Http客户端配置
/// </summary>
[HighQuality]
internal enum HttpClientConfigration
internal enum HttpClientConfiguration
{
/// <summary>
/// 默认配置
@@ -23,4 +23,9 @@ internal enum HttpClientConfigration
/// 米游社登录请求配置
/// </summary>
XRpc2,
/// <summary>
/// 国际服Hoyolab请求配置
/// </summary>
XRpc3,
}

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
[HighQuality]
internal static partial class IocHttpClientConfiguration
{
private const string ApplicationJson = "application/json";
/// <summary>
/// 添加 <see cref="HttpClient"/>
/// </summary>
@@ -37,7 +39,7 @@ internal static partial class IocHttpClientConfiguration
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
@@ -51,7 +53,7 @@ internal static partial class IocHttpClientConfiguration
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
client.DefaultRequestHeaders.Add("x-rpc-aigis", string.Empty);
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
@@ -60,4 +62,18 @@ internal static partial class IocHttpClientConfiguration
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2");
}
/// <summary>
/// 对于需要添加动态密钥的客户端使用此配置
/// 国际服 API 测试
/// </summary>
/// <param name="client">配置后的客户端</param>
private static void XRpc3Configuration(HttpClient client)
{
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabOsUA);
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0");
client.DefaultRequestHeaders.Add("x-rpc-client_type", "4");
}
}

View File

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

View File

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

View File

@@ -103,11 +103,10 @@ internal sealed class User : ObservableObject
internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
{
User user = new(inner);
bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
if (!isOk)
if (!await user.InitializeCoreAsync(token).ConfigureAwait(false))
{
user.UserInfo = new UserInfo() { Nickname = "网络异常" };
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
user.UserGameRoles = new();
}
@@ -118,15 +117,17 @@ internal sealed class User : ObservableObject
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async Task<User?> CreateAsync(Cookie cookie, CancellationToken token = default)
internal static async Task<User?> CreateAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
EntityUser entity = EntityUser.Create(cookie);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
entity.Mid = cookie.GetValueOrDefault(Cookie.MID);
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
entity.IsOversea = isOversea;
if (entity.Aid != null && entity.Mid != null)
{
@@ -156,22 +157,24 @@ internal sealed class User : ObservableObject
using (IServiceScope scope = Ioc.Default.CreateScope())
{
if (!await TrySetUserInfoAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
bool isOversea = Entity.IsOversea;
if (!await TrySetLTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserGameRolesAsync(scope.ServiceProvider, token).ConfigureAwait(false))
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
if (!await TrySetUserInfoAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserGameRolesAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
@@ -188,14 +191,14 @@ internal sealed class User : ObservableObject
return true;
}
Response<LtokenWrapper> lTokenResponse = await provider
.GetRequiredService<PassportClient2>()
Response<LTokenWrapper> lTokenResponse = await provider
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetLTokenBySTokenAsync(Entity, token)
.ConfigureAwait(false);
if (lTokenResponse.IsOk())
{
LToken = Cookie.Parse($"{Cookie.LTUID}={Entity.Aid};{Cookie.LTOKEN}={lTokenResponse.Data.Ltoken}");
LToken = Cookie.Parse($"{Cookie.LTUID}={Entity.Aid};{Cookie.LTOKEN}={lTokenResponse.Data.LToken}");
return true;
}
else
@@ -212,7 +215,7 @@ internal sealed class User : ObservableObject
}
Response<UidCookieToken> cookieTokenResponse = await provider
.GetRequiredService<PassportClient2>()
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(Entity, token)
.ConfigureAwait(false);
@@ -230,38 +233,32 @@ internal sealed class User : ObservableObject
private async Task<bool> TrySetUserInfoAsync(IServiceProvider provider, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await provider
.GetRequiredService<UserClient>()
.PickRequiredService<IUserClient>(Entity.IsOversea)
.GetUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);
UserInfo = response.Data?.UserInfo;
return UserInfo != null;
if (response.IsOk())
{
UserInfo = response.Data.UserInfo;
return true;
}
else
{
return false;
}
}
private async Task<bool> TrySetUserGameRolesAsync(IServiceProvider provider, CancellationToken token)
{
Response<ActionTicketWrapper> actionTicketResponse = await provider
.GetRequiredService<AuthClient>()
.GetActionTicketByStokenAsync("game_role", Entity)
.ConfigureAwait(false);
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await provider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(Entity, token)
.ConfigureAwait(false);
if (actionTicketResponse.IsOk())
if (userGameRolesResponse.IsOk())
{
string actionTicket = actionTicketResponse.Data.Ticket;
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await provider
.GetRequiredService<BindingClient>()
.GetUserGameRolesByActionTicketAsync(actionTicket, Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{
UserGameRoles = userGameRolesResponse.Data.List;
return UserGameRoles.Any();
}
else
{
return false;
}
UserGameRoles = userGameRolesResponse.Data.List;
return UserGameRoles.Any();
}
else
{

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Database;
using Snap.Hutao.Web.Hoyolab;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -66,8 +67,22 @@ internal sealed class User : ISelectable
/// <returns>新创建的用户</returns>
public static User Create(Cookie cookie)
{
_ = cookie.TryGetAsStoken(out Cookie? stoken);
_ = cookie.TryGetAsLtoken(out Cookie? ltoken);
_ = cookie.TryGetAsSToken(out Cookie? stoken);
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };
}
/// <summary>
/// 创建一个国际服用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <returns>新创建的用户</returns>
public static User CreateOs(Cookie cookie)
{
_ = cookie.TryGetAsLegacySToken(out Cookie? stoken);
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };

View File

@@ -11,12 +11,35 @@ namespace Snap.Hutao.Model.Intrinsic.Immutable;
[HighQuality]
internal static class IntrinsicImmutables
{
private static readonly ImmutableHashSet<string> associationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
private static readonly ImmutableHashSet<string> weaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
private static readonly ImmutableHashSet<string> itemQualities = Enum.GetValues<ItemQuality>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
private static readonly ImmutableHashSet<string> bodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
private static readonly ImmutableHashSet<string> fightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
private static readonly ImmutableHashSet<string> elementNames = new HashSet<string>(7)
/// <summary>
/// 所属地区
/// </summary>
public static readonly ImmutableHashSet<string> AssociationTypes = Enum.GetValues<AssociationType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 武器类型
/// </summary>
public static readonly ImmutableHashSet<string> WeaponTypes = Enum.GetValues<WeaponType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 物品类型
/// </summary>
public static readonly ImmutableHashSet<string> ItemQualities = Enum.GetValues<ItemQuality>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 身材类型
/// </summary>
public static readonly ImmutableHashSet<string> BodyTypes = Enum.GetValues<BodyType>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 战斗属性
/// </summary>
public static readonly ImmutableHashSet<string> FightProperties = Enum.GetValues<FightProperty>().Select(e => e.GetLocalizedDescriptionOrDefault()).OfType<string>().ToImmutableHashSet();
/// <summary>
/// 元素名称
/// </summary>
public static readonly ImmutableHashSet<string> ElementNames = new HashSet<string>(7)
{
SH.ModelIntrinsicElementNameFire,
SH.ModelIntrinsicElementNameWater,
@@ -26,34 +49,4 @@ internal static class IntrinsicImmutables
SH.ModelIntrinsicElementNameIce,
SH.ModelIntrinsicElementNameRock,
}.ToImmutableHashSet();
/// <summary>
/// 所属地区
/// </summary>
public static ImmutableHashSet<string> AssociationTypes { get => associationTypes; }
/// <summary>
/// 武器类型
/// </summary>
public static ImmutableHashSet<string> WeaponTypes { get => weaponTypes; }
/// <summary>
/// 物品类型
/// </summary>
public static ImmutableHashSet<string> ItemQualities { get => itemQualities; }
/// <summary>
/// 身材类型
/// </summary>
public static ImmutableHashSet<string> BodyTypes { get => bodyTypes; }
/// <summary>
/// 战斗属性
/// </summary>
public static ImmutableHashSet<string> FightProperties { get => fightProperties; }
/// <summary>
/// 元素名称
/// </summary>
public static ImmutableHashSet<string> ElementNames { get => elementNames; }
}

View File

@@ -411,6 +411,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 网络异常 的本地化字符串。
/// </summary>
internal static string ModelBindingUserInitializationFailed {
get {
return ResourceManager.GetString("ModelBindingUserInitializationFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 第 {0} 期 的本地化字符串。
/// </summary>
@@ -1257,6 +1266,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 Hoyolab 账号不支持使用 SToken 刷新祈愿记录 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderStokenUnsupported {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderStokenUnsupported", resourceCulture);
}
}
/// <summary>
/// 查找类似 不支持的 Item Id{0} 的本地化字符串。
/// </summary>
@@ -1456,11 +1474,11 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 输入的 Cookie 必须包含 Stoken 的本地化字符串。
/// 查找类似 输入的 Cookie 必须包含 SToken 的本地化字符串。
/// </summary>
internal static string ServiceUserProcessCookieNoStoken {
internal static string ServiceUserProcessCookieNoSToken {
get {
return ResourceManager.GetString("ServiceUserProcessCookieNoStoken", resourceCulture);
return ResourceManager.GetString("ServiceUserProcessCookieNoSToken", resourceCulture);
}
}
@@ -1996,7 +2014,7 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 在此处输入包含 Stoken 的 Cookie 的本地化字符串。
/// 查找类似 在此处输入包含 SToken 的 Cookie 的本地化字符串。
/// </summary>
internal static string ViewDialogUserInputPlaceholder {
get {
@@ -2229,6 +2247,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 Hoyolab 账号不支持验证 的本地化字符串。
/// </summary>
internal static string ViewModelDailyNoteHoyolabVerificationUnsupported {
get {
return ResourceManager.GetString("ViewModelDailyNoteHoyolabVerificationUnsupported", resourceCulture);
}
}
/// <summary>
/// 查找类似 30 分钟 | 3.75 树脂 的本地化字符串。
/// </summary>
@@ -3292,20 +3319,20 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 Stoken 刷新 的本地化字符串。
/// 查找类似 SToken 刷新 的本地化字符串。
/// </summary>
internal static string ViewPageGachaLogRefreshByStoken {
internal static string ViewPageGachaLogRefreshBySToken {
get {
return ResourceManager.GetString("ViewPageGachaLogRefreshByStoken", resourceCulture);
return ResourceManager.GetString("ViewPageGachaLogRefreshBySToken", resourceCulture);
}
}
/// <summary>
/// 查找类似 使用当前用户的 Cookie 信息刷新祈愿记录 的本地化字符串。
/// </summary>
internal static string ViewPageGachaLogRefreshByStokenDescription {
internal static string ViewPageGachaLogRefreshBySTokenDescription {
get {
return ResourceManager.GetString("ViewPageGachaLogRefreshByStokenDescription", resourceCulture);
return ResourceManager.GetString("ViewPageGachaLogRefreshBySTokenDescription", resourceCulture);
}
}
@@ -3966,6 +3993,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 请输入你的 Hoyolab Uid 的本地化字符串。
/// </summary>
internal static string ViewPageLoginHoyoverseUserHint {
get {
return ResourceManager.GetString("ViewPageLoginHoyoverseUserHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 我已登录 的本地化字符串。
/// </summary>
@@ -4687,7 +4723,7 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 Cookie 操作 的本地化字符串。
/// 查找类似 米游社 的本地化字符串。
/// </summary>
internal static string ViewUserCookieOperation {
get {
@@ -4695,6 +4731,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 Hoyolab 的本地化字符串。
/// </summary>
internal static string ViewUserCookieOperation2 {
get {
return ResourceManager.GetString("ViewUserCookieOperation2", resourceCulture);
}
}
/// <summary>
/// 查找类似 网页登录 的本地化字符串。
/// </summary>

View File

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

View File

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

View File

@@ -31,20 +31,16 @@ internal sealed class AvatarInfoService : IAvatarInfoService
/// 构造一个新的角色信息服务
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="metadataService">元数据服务</param>
/// <param name="summaryFactory">简述工厂</param>
/// <param name="logger">日志器</param>
/// <param name="serviceProvider">服务提供器</param>
public AvatarInfoService(
AppDbContext appDbContext,
IMetadataService metadataService,
ISummaryFactory summaryFactory,
ILogger<AvatarInfoService> logger)
IServiceProvider serviceProvider)
{
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;
metadataService = serviceProvider.GetRequiredService<IMetadataService>();
summaryFactory = serviceProvider.GetRequiredService<ISummaryFactory>();
logger = serviceProvider.GetRequiredService<ILogger<AvatarInfoService>>();
avatarInfoDbOperation = new(appDbContext);
avatarInfoDbOperation = new(appDbContext, serviceProvider);
}
/// <inheritdoc/>

View File

@@ -60,23 +60,17 @@ internal sealed class DailyNoteNotifier
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
AuthClient authClient = scope.ServiceProvider.GetRequiredService<AuthClient>();
Response<ActionTicketWrapper> actionTicketResponse = await authClient
.GetActionTicketByStokenAsync("game_role", entry.User)
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
if (actionTicketResponse.IsOk())
if (rolesResponse.IsOk())
{
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn";
}
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unknown";
}
ToastContentBuilder builder = new ToastContentBuilder()

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Binding.User;
@@ -12,6 +11,7 @@ using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using System.Collections.ObjectModel;
using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote;
@@ -60,13 +60,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
{
DailyNoteEntry newEntry = DailyNoteEntry.Create(role);
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(roleUid))
.GetDailyNoteAsync(role)
.ConfigureAwait(false);
@@ -109,12 +109,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
// TODO: Add this option to AppOptions
bool isSilentMode = appDbContext.Settings
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
.GetBoolean();
bool isGameRunning = scope.ServiceProvider.GetRequiredService<IGameService>().IsGameRunning();
if (isSilentMode && isGameRunning)
{
// Prevent notify when we are in game && silent mode.
@@ -123,7 +124,8 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
{
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(entry.Uid))
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 游戏文件包转换器
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class PackageConverter
{
private readonly JsonSerializerOptions options;

View File

@@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Metadata;
/// </summary>
[HighQuality]
[Injection(InjectAs.Singleton, typeof(IMetadataService))]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed partial class MetadataService : IMetadataService, IMetadataServiceInitialization
{
private const string MetaFileName = "Meta.json";

View File

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

View File

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

View File

@@ -191,7 +191,7 @@ internal class UserService : IUserService
}
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea)
{
await ThreadHelper.SwitchToBackgroundAsync();
string? mid = cookie.GetValueOrDefault(Cookie.MID);
@@ -208,10 +208,10 @@ internal class UserService : IUserService
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (cookie.TryGetAsStoken(out Cookie? stoken))
if (cookie.TryGetAsSToken(out Cookie? stoken))
{
user.SToken = stoken;
user.LToken = cookie.TryGetAsLtoken(out Cookie? ltoken) ? ltoken : user.LToken;
user.LToken = cookie.TryGetAsLToken(out Cookie? ltoken) ? ltoken : user.LToken;
user.CookieToken = cookie.TryGetAsCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
await appDbContext.Users.UpdateAndSaveAsync(user.Entity).ConfigureAwait(false);
@@ -219,13 +219,13 @@ internal class UserService : IUserService
}
else
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken);
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
}
}
}
else
{
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false);
return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false);
}
}
@@ -235,9 +235,9 @@ internal class UserService : IUserService
using (IServiceScope scope = scopeFactory.CreateScope())
{
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
.GetRequiredService<PassportClient2>()
.GetCookieAccountInfoBySTokenAsync(user.Entity)
.ConfigureAwait(false);
.PickRequiredService<IPassportClient>(user.Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
@@ -265,14 +265,14 @@ internal class UserService : IUserService
return user != null;
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie)
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
{
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
BindingUser? newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache

View File

@@ -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)

View File

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

View File

@@ -0,0 +1,50 @@
<Page
x:Class="Snap.Hutao.View.Page.LoginHoyoverseUserPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid Loaded="OnRootLoaded">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="{shcm:ResourceString Name=ViewPageLoginMihoyoUserTitle}"/>
<Grid
Grid.Row="0"
Margin="16"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox
x:Name="UidInputText"
Grid.Column="0"
Width="240"
Margin="0,0,16,0"
HorizontalAlignment="Right"
InputScope="Number"
MaxLength="9"
PlaceholderText="{shcm:ResourceString Name=ViewPageLoginHoyoverseUserHint}"/>
<Button
Grid.Column="1"
Click="CookieButtonClick"
Content="{shcm:ResourceString Name=ViewPageLoginMihoyoUserLoggedInAction}"/>
</Grid>
<WebView2
x:Name="WebView"
Grid.Row="2"
Margin="0,0,0,0"/>
</Grid>
</Page>

View File

@@ -0,0 +1,123 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 登录米哈游通行证页面
/// </summary>
internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
/// 构造一个新的登录米哈游通行证页面
/// </summary>
public LoginHoyoverseUserPage()
{
InitializeComponent();
}
[SuppressMessage("", "VSTHRD100")]
private async void OnRootLoaded(object sender, RoutedEventArgs e)
{
try
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
foreach (CoreWebView2Cookie item in cookies)
{
manager.DeleteCookie(item);
}
WebView.CoreWebView2.Navigate("https://account.hoyolab.com/#/login");
}
catch (Exception ex)
{
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ex);
}
}
private async Task HandleCurrentCookieAsync(CancellationToken token = default)
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
// Get user id from text input, login_uid is missed in cookie
string uid = UidInputText.Text;
if (uid.Length != 9)
{
await ThreadHelper.SwitchToMainThreadAsync();
infoBarService.Information(SH.ViewPageLoginHoyoverseUserHint);
return;
}
Cookie loginTicketCookie = Cookie.FromCoreWebView2Cookies(cookies);
loginTicketCookie["login_uid"] = uid;
// 使用 loginTicket 获取 stoken
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, true, token)
.ConfigureAwait(false);
if (!multiTokenResponse.IsOk())
{
return;
}
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
Cookie hoyoLabCookie = Cookie.Parse($"{Cookie.STUID}={uid};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
// 处理 cookie 并添加用户
(UserOptionResult result, string nickname) = await Ioc.Default
.GetRequiredService<IUserService>()
.ProcessInputCookieAsync(hoyoLabCookie, true)
.ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
switch (result)
{
case UserOptionResult.Added:
ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService<ViewModel.UserViewModel>();
if (vm.Users!.Count == 1)
{
await ThreadHelper.SwitchToMainThreadAsync();
vm.SelectedUser = vm.Users.Single();
}
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
break;
case UserOptionResult.Incomplete:
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
break;
default:
throw Must.NeverHappen();
}
}
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -49,7 +49,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
}
}
private async Task HandleCurrentCookieAsync(CancellationToken token)
private async Task HandleCurrentCookieAsync(CancellationToken token = default)
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
@@ -57,7 +57,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie loginTicketCookie = Cookie.FromCoreWebView2Cookies(cookies);
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, token)
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, false, token)
.ConfigureAwait(false);
if (!multiTokenResponse.IsOk())
@@ -67,10 +67,11 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
Cookie stokenV1 = Cookie.Parse($"stuid={loginTicketCookie["login_uid"]};stoken={multiTokenMap["stoken"]}");
Cookie stokenV1 = Cookie.Parse($"{Cookie.STUID}={loginTicketCookie[Cookie.LOGIN_UID]};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
Response<LoginResult> loginResultResponse = await Ioc.Default
.GetRequiredService<PassportClient2>()
.LoginByStokenAsync(stokenV1, token)
.GetRequiredService<PassportClient>()
.LoginBySTokenAsync(stokenV1, token)
.ConfigureAwait(false);
if (!loginResultResponse.IsOk())
@@ -81,12 +82,13 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie stokenV2 = Cookie.FromLoginResult(loginResultResponse.Data);
(UserOptionResult result, string nickname) = await Ioc.Default
.GetRequiredService<IUserService>()
.ProcessInputCookieAsync(stokenV2)
.ProcessInputCookieAsync(stokenV2, false)
.ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
// TODO: Move these code somewhere else.
switch (result)
{
case UserOptionResult.Added:
@@ -97,16 +99,16 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
vm.SelectedUser = vm.Users.Single();
}
infoBarService.Success($"用户 [{nickname}] 添加成功");
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
break;
case UserOptionResult.Incomplete:
infoBarService.Information($"此 Cookie 不完整,操作失败");
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无效,操作失败");
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success($"用户 [{nickname}] 更新成功");
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
break;
default:
throw Must.NeverHappen();
@@ -115,6 +117,6 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -116,10 +116,7 @@
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid
Width="200"
Padding="0,12"
Background="Transparent">
<Grid Padding="0,12" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
@@ -204,27 +201,65 @@
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewUserDefaultDescription}"
Visibility="{Binding Users.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}"/>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewUserCookieOperation}"/>
<StackPanel
Margin="0,0,6,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<AppBarButton
Grid.Row="1"
Grid.Column="0"
Command="{Binding RefreshCookieTokenCommand}"
Icon="{shcm:FontIcon Glyph=&#xE72C;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationRefreshCookieAction}"/>
<AppBarButton
Command="{Binding LoginMihoyoUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xEB41;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
<AppBarButton
Command="{Binding AddUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel>
<AppBarSeparator Grid.RowSpan="2" Grid.Column="1"/>
<TextBlock
Grid.Row="0"
Grid.Column="2"
Margin="4,6,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewUserCookieOperation}"/>
<StackPanel
Grid.Row="1"
Grid.Column="2"
Orientation="Horizontal">
<AppBarButton
Command="{Binding LoginMihoyoUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xEB41;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
<AppBarButton
Command="{Binding AddUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel>
<AppBarSeparator Grid.RowSpan="2" Grid.Column="3"/>
<TextBlock
Grid.Row="0"
Grid.Column="4"
Margin="4,6,0,0"
Style="{StaticResource BaseTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewUserCookieOperation2}"/>
<StackPanel
Grid.Row="1"
Grid.Column="4"
Orientation="Horizontal">
<AppBarButton
Command="{Binding LoginHoyoverseUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xEB41;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
<AppBarButton
Command="{Binding AddOverseaUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel>
</Grid>
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -247,9 +247,17 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel
{
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
// TODO: Add verify support for oversea user
if (userAndUid.User.IsOversea)
{
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported);
}
else
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
}
}
else
{

View File

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

View File

@@ -46,7 +46,9 @@ internal sealed class UserViewModel : ObservableObject
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
AddOverseaUserCommand = new AsyncRelayCommand(AddOverseaUserAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
LoginHoyoverseUserCommand = new RelayCommand(LoginHoyoverseUser);
RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
RefreshCookieTokenCommand = new AsyncRelayCommand(RefreshCookieTokenAsync);
@@ -87,11 +89,21 @@ internal sealed class UserViewModel : ObservableObject
/// </summary>
public ICommand AddUserCommand { get; }
/// <summary>
/// 添加国际服用户命令
/// </summary>
public ICommand AddOverseaUserCommand { get; }
/// <summary>
/// 登录米游社命令
/// </summary>
public ICommand LoginMihoyoUserCommand { get; }
/// <summary>
/// 登录米游社命令
/// </summary>
public ICommand LoginHoyoverseUserCommand { get; }
/// <summary>
/// 移除用户命令
/// </summary>
@@ -120,7 +132,17 @@ internal sealed class UserViewModel : ObservableObject
}
}
private async Task AddUserAsync()
private Task AddUserAsync()
{
return AddUserCoreAsync(false);
}
private Task AddOverseaUserAsync()
{
return AddUserCoreAsync(true);
}
private async Task AddUserCoreAsync(bool isOversea)
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
@@ -133,7 +155,7 @@ internal sealed class UserViewModel : ObservableObject
{
Cookie cookie = Cookie.Parse(result.Value);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
switch (optionResult)
{
@@ -173,6 +195,21 @@ internal sealed class UserViewModel : ObservableObject
}
}
/// <summary>
/// 打开浏览器登录 hoyolab 以获取 cookie
/// </summary>
private void LoginHoyoverseUser()
{
if (Core.WebView2Helper.IsSupported)
{
serviceProvider.GetRequiredService<INavigationService>().Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
}
else
{
infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
}
}
private async Task RemoveUserAsync(User? user)
{
if (user != null)

View File

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

View File

@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Web;
@@ -10,9 +12,161 @@ namespace Snap.Hutao.Web;
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1201")]
[SuppressMessage("", "SA1202")]
[SuppressMessage("", "SA1124")]
internal static class ApiOsEndpoints
{
#region ApiAccountOsApi
/// <summary>
/// Hoyolab App Login api
/// Can fetch stoken
/// </summary>
public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
/// <summary>
/// 获取 Ltoken
/// </summary>
public const string AccountGetLTokenBySToken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
/// <summary>
/// fetch CookieToken
/// </summary>
public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
#endregion
#region ApiGeetest
/// <summary>
/// 获取GT码
/// </summary>
/// <param name="gt">gt</param>
/// <returns>GT码Url</returns>
public static string GeetestGetType(string gt)
{
return $"{ApiNaGeetest}/gettype.php?gt={gt}";
}
/// <summary>
/// 验证接口
/// </summary>
/// <param name="gt">gt</param>
/// <param name="challenge">challenge流水号</param>
/// <returns>验证接口Url</returns>
public static string GeetestAjax(string gt, string challenge)
{
return $"{ApiNaGeetest}/ajax.php?gt={gt}&challenge={challenge}&lang=zh-cn&pt=0&client_type=web";
}
#endregion
#region ApiOsTakumiAuthApi
/// <summary>
/// 获取 stoken 与 ltoken
/// </summary>
/// <param name="loginTicket">登录票证</param>
/// <param name="loginUid">uid</param>
/// <returns>Url</returns>
public static string AuthMultiToken(string loginTicket, string loginUid)
{
return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
}
/// <summary>
/// 获取 stoken 与 ltoken
/// </summary>
/// <param name="actionType">操作类型 game_role</param>
/// <param name="stoken">SToken</param>
/// <param name="uid">uid</param>
/// <returns>Url</returns>
public static string AuthActionTicket(string actionType, string stoken, string uid)
{
return $"{ApiAccountOsAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
}
#endregion
#region ApiOsTaKumiApi
/// <summary>
/// 用户游戏角色
/// </summary>
/// <returns>用户游戏角色字符串</returns>
public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
/// <summary>
/// 用户游戏角色
/// </summary>
/// <param name="region">地区代号</param>
/// <returns>用户游戏角色字符串</returns>
public static string UserGameRolesByLtoken(string region)
{
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global&region={region}";
}
#endregion
#region BbsApiOsApi
/// <summary>
/// 查询其他用户详细信息
/// </summary>
/// <param name="bbsUid">bbs Uid</param>
/// <returns>查询其他用户详细信息字符串</returns>
public static string UserFullInfoQuery(string bbsUid)
{
return $"{BbsApiOs}/community/painter/wapi/user/full";
}
/// <summary>
/// 国际服角色基本信息
/// </summary>
/// <param name="uid">uid</param>
/// <returns>角色基本信息字符串</returns>
public static string GameRecordRoleBasicInfo(PlayerUid uid)
{
return $"{BbsApiOsGameRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
}
/// <summary>
/// 国际服角色信息
/// </summary>
public const string GameRecordCharacter = $"{BbsApiOsGameRecordApi}/character";
/// <summary>
/// 国际服游戏记录实时便笺
/// </summary>
/// <param name="uid">uid</param>
/// <returns>游戏记录实时便笺字符串</returns>
public static string GameRecordDailyNote(PlayerUid uid)
{
return $"{BbsApiOsGameRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
}
/// <summary>
/// 国际服游戏记录主页
/// </summary>
/// <param name="uid">uid</param>
/// <returns>游戏记录主页字符串</returns>
public static string GameRecordIndex(PlayerUid uid)
{
return $"{BbsApiOsGameRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
}
/// <summary>
/// 国际服深渊信息
/// </summary>
/// <param name="scheduleType">深渊类型</param>
/// <param name="uid">Uid</param>
/// <returns>深渊信息字符串</returns>
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid)
{
return $"{BbsApiOsGameRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
}
#endregion
#region Hk4eApiOsGachaInfoApi
/// <summary>
@@ -26,6 +180,46 @@ internal static class ApiOsEndpoints
}
#endregion
#region SgPublicApi
/// <summary>
/// 计算器家具计算
/// </summary>
public const string CalculateFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
/// <summary>
/// 计算器角色列表 size 20
/// </summary>
public const string CalculateAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
/// <summary>
/// 计算器武器列表 size 20
/// </summary>
public const string CalculateWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
/// <summary>
/// 计算器结果
/// </summary>
public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
/// <summary>
/// 计算器同步角色详情 size 20
/// </summary>
/// <param name="avatarId">角色Id</param>
/// <param name="uid">uid</param>
/// <returns>角色详情</returns>
public static string CalculateSyncAvatarDetail(AvatarId avatarId, PlayerUid uid)
{
return $"{SgPublicApi}/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&uid={uid.Value}&region={uid.Region}";
}
/// <summary>
/// 计算器同步角色列表 size 20
/// </summary>
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
#endregion
#region SdkStaticLauncherApi
/// <summary>
@@ -40,10 +234,35 @@ internal static class ApiOsEndpoints
#endregion
#region Hosts | Queries
private const string ApiNaGeetest = "https://api-na.geetest.com";
private const string ApiOsTakumi = "https://api-os-takumi.hoyoverse.com";
private const string ApiOsTakumiBindingApi = $"{ApiOsTakumi}/binding/api";
private const string ApiAccountOs = "https://api-account-os.hoyolab.com";
private const string ApiAccountOsBindingApi = $"{ApiAccountOs}/binding/api";
private const string ApiAccountOsAuthApi = $"{ApiAccountOs}/account/auth/api";
private const string BbsApiOs = "https://bbs-api-os.hoyolab.com";
private const string BbsApiOsGameRecordApi = $"{BbsApiOs}/game_record/genshin/api";
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
private const string SgPublicApi = "https://sg-public-api.hoyolab.com";
/// <summary>
/// Web static referer
/// </summary>
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
/// <summary>
/// Act hoyolab referer
/// </summary>
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
#endregion
}

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Enka;
/// Enka API 客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class EnkaClient
{
private const string EnkaAPI = "https://enka.network/api/uid/{0}";

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Geetest;
/// 极验客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class GeetestClient
{
private readonly HttpClient httpClient;

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.App.Account;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfigration.XRpc)]
[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class AccountClient
{
private readonly HttpClient httpClient;

View File

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

View File

@@ -14,8 +14,8 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfigration.XRpc)]
internal sealed class UserClient
[HttpClient(HttpClientConfiguration.XRpc, typeof(IUserClient))]
internal sealed class UserClient : IUserClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
@@ -34,6 +34,9 @@ internal sealed class UserClient
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => false;
/// <summary>
/// 获取当前用户详细信息
/// </summary>
@@ -52,4 +55,22 @@ internal sealed class UserClient
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取当前用户详细信息,使用 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
public async Task<Response<UserFullInfoWrapper>> GetOsUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.LToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

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

View File

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

View File

@@ -38,4 +38,9 @@ internal enum SaltType
/// LK2
/// </summary>
LK2,
/// <summary>
/// Hoyolab K2
/// </summary>
OSK2,
}

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
/// <summary>
/// 公告客户端
/// </summary>
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class AnnouncementClient
{
private readonly HttpClient httpClient;

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
/// 祈愿记录客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class GachaInfoClient
{
private readonly HttpClient httpClient;

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,10 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Net.Http.Json;
namespace Snap.Hutao.Web.Hoyolab.Passport;
@@ -13,7 +15,8 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
/// 通行证客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class PassportClient
{
private readonly HttpClient httpClient;
@@ -34,7 +37,7 @@ internal sealed class PassportClient
}
/// <summary>
/// 异步验证Ltoken
/// 异步验证 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
@@ -50,6 +53,28 @@ internal sealed class PassportClient
return Response.Response.DefaultIfNull(response);
}
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
private class Timestamp
{
[JsonPropertyName("t")]

View File

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

View File

@@ -0,0 +1,78 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Net.Http.Json;
using Windows.ApplicationModel.Contacts;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端 XRPC 版
/// </summary>
[HttpClient(HttpClientConfiguration.XRpc3, typeof(IPassportClient))]
internal sealed class PassportClientOversea : IPassportClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<PassportClientOversea> logger;
/// <summary>
/// 构造一个新的国际服通行证客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="logger">日志器</param>
public PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClientOversea> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => true;
/// <summary>
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken)]
public async Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
{
STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
Response<UidCookieToken>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.TryCatchPostAsJsonAsync<STokenWrapper, Response<UidCookieToken>>(ApiOsEndpoints.AccountGetCookieTokenBySToken, data, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken)]
public async Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
Response<LTokenWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.TryCatchPostAsJsonAsync<STokenWrapper, Response<LTokenWrapper>>(ApiOsEndpoints.AccountGetLTokenBySToken, data, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// SToken 包装器
/// </summary>
internal sealed class STokenWrapper
{
/// <summary>
/// 构造一个新的SToken 包装器
/// </summary>
/// <param name="stoken">stoken</param>
/// <param name="uid">uid</param>
public STokenWrapper(string stoken, string uid)
{
SToken = stoken;
Uid = uid;
}
/// <summary>
/// SToken
/// </summary>
[JsonPropertyName("stoken")]
public string SToken { get; set; }
/// <summary>
/// Uid
/// </summary>
[JsonPropertyName("uid")]
public string Uid { get; set; }
}

View File

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

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// 游戏资源客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class ResourceClient
{
private readonly HttpClient httpClient;
@@ -40,7 +40,7 @@ internal sealed class ResourceClient
/// <returns>游戏资源</returns>
public async Task<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.LauncherId == "10"
string url = scheme.IsOversea
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
: ApiEndpoints.SdkStaticLauncherResource(scheme);

View File

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

View File

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

View File

@@ -11,11 +11,11 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// Stoken绑定客户端
/// SToken绑定客户端
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfigration.XRpc)]
[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class BindingClient2
{
private readonly HttpClient httpClient;
@@ -42,12 +42,12 @@ internal sealed class BindingClient2
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.LK2)]
public async Task<List<UserGameRole>> GetUserGameRolesByStokenAsync(User user, CancellationToken token = default)
public async Task<List<UserGameRole>> GetUserGameRolesBySTokenAsync(User user, CancellationToken token = default)
{
Response<ListWrapper<UserGameRole>>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.LK2, true)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRolesByStoken, options, logger, token)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRolesBySToken, options, logger, token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data?.List);
@@ -55,7 +55,7 @@ internal sealed class BindingClient2
/// <summary>
/// 异步生成祈愿验证密钥
/// 需要stoken
/// 需要 SToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="data">提交数据</param>

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
/// 养成计算器客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class CalculateClient
{
private readonly HttpClient httpClient;
@@ -43,10 +43,18 @@ internal sealed class CalculateClient
[ApiInformation(Cookie = CookieType.Cookie)]
public async Task<Response<Consumption>> ComputeAsync(Model.Entity.User user, AvatarPromotionDelta delta, CancellationToken token = default)
{
string referer = user.IsOversea
? ApiOsEndpoints.ActHoyolabReferer
: ApiEndpoints.WebStaticMihoyoReferer;
string url = user.IsOversea
? ApiOsEndpoints.CalculateCompute
: ApiEndpoints.CalculateCompute;
Response<Consumption>? resp = await httpClient
.SetUser(user, CookieType.Cookie)
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiEndpoints.CalculateCompute, delta, options, logger, token)
.SetReferer(referer)
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(url, delta, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -65,14 +73,24 @@ internal sealed class CalculateClient
List<Avatar> avatars = new();
Response<ListWrapper<Avatar>>? resp;
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
// 根据 uid 所属服务器选择 referer 与 api
string referer = userAndUid.User.IsOversea
? ApiOsEndpoints.ActHoyolabReferer
: ApiEndpoints.WebStaticMihoyoReferer;
string url = userAndUid.User.IsOversea
? ApiOsEndpoints.CalculateSyncAvatarList
: ApiEndpoints.CalculateSyncAvatarList;
httpClient
.SetReferer(referer)
.SetUser(userAndUid.User, CookieType.CookieToken);
do
{
filter.Page = currentPage++;
resp = await httpClient
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(ApiEndpoints.CalculateSyncAvatarList, filter, options, logger, token)
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(url, filter, options, logger, token)
.ConfigureAwait(false);
if (resp != null && resp.IsOk())
@@ -101,9 +119,13 @@ internal sealed class CalculateClient
/// <returns>角色详情</returns>
public async Task<Response<AvatarDetail>> GetAvatarDetailAsync(UserAndUid userAndUid, Avatar avatar, CancellationToken token = default)
{
string url = userAndUid.User.IsOversea
? ApiOsEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value)
: ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value);
Response<AvatarDetail>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.CookieToken)
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(url, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

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

View File

@@ -16,7 +16,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfigration.XRpc)]
[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class CardClient
{
private readonly HttpClient httpClient;

View File

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

View File

@@ -0,0 +1,117 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// Hoyoverse game record provider
/// </summary>
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc3, typeof(IGameRecordClient))]
[PrimaryHttpMessageHandler(UseCookies = false)]
internal sealed class GameRecordClientOversea : IGameRecordClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<GameRecordClient> logger;
/// <summary>
/// 构造一个新的游戏记录提供器
/// </summary>
/// <param name="httpClient">请求器</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public GameRecordClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => true;
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
public async Task<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
{
Response<DailyNote.DailyNote>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.Cookie)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<DailyNote.DailyNote>>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
public async Task<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
{
Response<PlayerInfo>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.Cookie)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<PlayerInfo>>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="userAndUid">用户</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
public async Task<Response<SpiralAbyss.SpiralAbyss>> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
Response<SpiralAbyss.SpiralAbyss>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.Cookie)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
public async Task<Response<CharacterWrapper>> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
{
CharacterData data = new(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id));
Response<CharacterWrapper>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.Cookie)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchPostAsJsonAsync<CharacterData, Response<CharacterWrapper>>(ApiOsEndpoints.GameRecordCharacter, data, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

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

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃日志客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaLogUploadClient
{
private readonly HttpClient httpClient;

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃通行证客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaPassportClient
{
/// <summary>

View File

@@ -18,11 +18,11 @@ namespace Snap.Hutao.Web.Hutao;
/// 胡桃API客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaSpiralAbyssClient
{
private readonly IServiceProvider serviceProvider;
private readonly HttpClient httpClient;
private readonly GameRecordClient gameRecordClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<HomaSpiralAbyssClient> logger;
@@ -30,15 +30,14 @@ internal sealed class HomaSpiralAbyssClient
/// 构造一个新的胡桃API客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="gameRecordClient">游戏记录客户端</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
/// <param name="serviceProvider">服务提供器</param>
public HomaSpiralAbyssClient(HttpClient httpClient, IServiceProvider serviceProvider)
{
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
logger = serviceProvider.GetRequiredService<ILogger<HomaSpiralAbyssClient>>();
this.serviceProvider = serviceProvider;
this.httpClient = httpClient;
this.gameRecordClient = gameRecordClient;
this.options = options;
this.logger = logger;
}
/// <summary>
@@ -186,6 +185,8 @@ internal sealed class HomaSpiralAbyssClient
/// <returns>玩家记录</returns>
public async Task<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
{
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
Response<PlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);

View File

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