Merge branch 'DGP-Studio:main' into main

This commit is contained in:
Xhichn
2023-03-24 20:24:37 +08:00
committed by GitHub
25 changed files with 396 additions and 79 deletions

View File

@@ -32,7 +32,7 @@ internal sealed class ExceptionRecorder
{
#if RELEASE
#pragma warning disable VSTHRD002
Ioc.Default.GetRequiredService<Web.Hutao.HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
Ioc.Default.GetRequiredService<Web.Hutao.HomaLogUploadClient>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
#pragma warning restore VSTHRD002
#endif
StringBuilder dataDetailBuilder = new();

View File

@@ -29,6 +29,16 @@ internal static class SettingKeys
/// </summary>
public const string DataFolderPath = "DataFolderPath";
/// <summary>
/// 通行证用户名(邮箱)
/// </summary>
public const string PassportUserName = "PassportUserName";
/// <summary>
/// 通行证密码
/// </summary>
public const string PassportPassword = "PassportPassword";
/// <summary>
/// 静态资源合约
/// 新增合约时 请注意

View File

@@ -214,7 +214,7 @@ internal sealed class User : ObservableObject
return false;
}
if (await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}

View File

@@ -4281,6 +4281,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 胡桃账号 的本地化字符串。
/// </summary>
internal static string ViewPageSettingHutaoPassportHeader {
get {
return ResourceManager.GetString("ViewPageSettingHutaoPassportHeader", resourceCulture);
}
}
/// <summary>
/// 查找类似 在完整阅读原神和胡桃工具箱用户协议后,我选择启用「启动游戏-高级功能」 的本地化字符串。
/// </summary>
@@ -4488,6 +4497,24 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 登录失败,请重新登录 的本地化字符串。
/// </summary>
internal static string ViewServiceHutaoUserLoginFailHint {
get {
return ResourceManager.GetString("ViewServiceHutaoUserLoginFailHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 立即登录或注册 的本地化字符串。
/// </summary>
internal static string ViewServiceHutaoUserLoginOrRegisterHint {
get {
return ResourceManager.GetString("ViewServiceHutaoUserLoginOrRegisterHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 战斗数据 的本地化字符串。
/// </summary>

View File

@@ -1797,4 +1797,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewPageSettingHutaoPassportHeader" xml:space="preserve">
<value>胡桃账号</value>
</data>
<data name="ViewServiceHutaoUserLoginFailHint" xml:space="preserve">
<value>登录失败,请重新登录</value>
</data>
<data name="ViewServiceHutaoUserLoginOrRegisterHint" xml:space="preserve">
<value>立即登录或注册</value>
</data>
</root>

View File

@@ -81,10 +81,11 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
try
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
foreach (MODULEENTRY32 entry in StructMarshal.EnumerateModuleEntry32(snapshot))
{
if (entry.th32ProcessID == processId && entry.szModule.AsNullTerminatedReadOnlySpan().SequenceEqual(moduleName))
__CHAR_256* pszModule = &entry.szModule;
ReadOnlySpan<byte> szModuleLocal = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)pszModule);
if (entry.th32ProcessID == processId && szModuleLocal.SequenceEqual(moduleName))
{
return entry;
}

View File

@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.Hutao;
[Injection(InjectAs.Scoped, typeof(IHutaoService))]
internal sealed class HutaoService : IHutaoService
{
private readonly HomaClient homaClient;
private readonly HomaSpiralAbyssClient homaClient;
private readonly IMemoryCache memoryCache;
private readonly AppDbContext appDbContext;
private readonly JsonSerializerOptions options;
@@ -30,7 +30,7 @@ internal sealed class HutaoService : IHutaoService
/// <param name="memoryCache">内存缓存</param>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="options">Json序列化选项</param>
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache, AppDbContext appDbContext, JsonSerializerOptions options)
public HutaoService(HomaSpiralAbyssClient homaClient, IMemoryCache memoryCache, AppDbContext appDbContext, JsonSerializerOptions options)
{
this.homaClient = homaClient;
this.memoryCache = memoryCache;
@@ -103,18 +103,21 @@ internal sealed class HutaoService : IHutaoService
}
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
T web = webResponse.IsOk() ? webResponse.Data : new();
T? data = webResponse.Data;
try
{
appDbContext.ObjectCache.AddAndSave(new()
if (data != null)
{
Key = key,
appDbContext.ObjectCache.AddAndSave(new()
{
Key = key,
// we hold the cache for 4 hours, then just expire it.
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(web, options),
});
// we hold the cache for 4 hours, then just expire it.
ExpireTime = DateTimeOffset.Now.AddHours(4),
Value = JsonSerializer.Serialize(data, options),
});
}
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
{
@@ -122,6 +125,6 @@ internal sealed class HutaoService : IHutaoService
// TODO: Not ignore it.
}
return memoryCache.Set(key, web, TimeSpan.FromMinutes(30));
return memoryCache.Set(key, data ?? new(), TimeSpan.FromHours(4));
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃用户服务
/// </summary>
[Injection(InjectAs.Singleton, typeof(IHutaoUserService))]
internal sealed class HutaoUserService : IHutaoUserService, IHutaoUserServiceInitialization
{
private readonly HomaPassportClient passportClient;
private readonly TaskCompletionSource initializeCompletionSource = new();
private bool isInitialized;
/// <summary>
/// 构造一个新的胡桃用户服务
/// </summary>
/// <param name="passportClient">通行证客户端</param>
public HutaoUserService(HomaPassportClient passportClient)
{
this.passportClient = passportClient;
}
/// <summary>
/// 用户名
/// </summary>
public string? UserName { get; private set; }
/// <summary>
/// 访问令牌
/// </summary>
public string? Token { get; private set; }
/// <summary>
/// 异步初始化
/// </summary>
/// <returns>任务</returns>
public async ValueTask<bool> InitializeAsync()
{
await initializeCompletionSource.Task.ConfigureAwait(false);
return isInitialized;
}
/// <inheritdoc/>
public async Task InitializeInternalAsync(CancellationToken token = default)
{
string userName = LocalSetting.Get(SettingKeys.PassportUserName, string.Empty);
string passport = LocalSetting.Get(SettingKeys.PassportPassword, string.Empty);
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(passport))
{
Web.Response.Response<string> response = await passportClient.LoginAsync(userName, passport, token).ConfigureAwait(false);
if (response.IsOk())
{
Token = response.Data;
UserName = userName;
isInitialized = true;
}
else
{
UserName = SH.ViewServiceHutaoUserLoginFailHint;
}
}
else
{
UserName = SH.ViewServiceHutaoUserLoginOrRegisterHint;
}
initializeCompletionSource.TrySetResult();
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 胡桃用户服务
/// </summary>
internal interface IHutaoUserService : ICastableService
{
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Hutao;
/// <summary>
/// 指示该类为用户服务初始化器
/// </summary>
internal interface IHutaoUserServiceInitialization
{
/// <summary>
/// 异步初始化
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>任务</returns>
Task InitializeInternalAsync(CancellationToken token = default);
}

View File

@@ -244,7 +244,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.4-alpha" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.206-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -72,6 +72,10 @@
Description="{Binding AppVersion}"
Header="{shcm:ResourceString Name=AppName}"
HeaderIcon="{shcm:FontIcon Glyph=&#xECAA;}"/>
<clw:SettingsCard
Description="{Binding AppVersion}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE716;}"/>
<clw:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE8C8;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCopyDeviceIdAction}"
@@ -199,15 +203,9 @@
IsOpen="{Binding Options.IsAdvancedLaunchOptionsEnabled}"
Message="{shcm:ResourceString Name=ViewPageSettingFeaturesDangerousHint}"
Severity="Error"/>
<InfoBar
IsClosable="False"
IsOpen="{Binding IsElevated, Converter={StaticResource BoolNegationConverter}}"
Message="{shcm:ResourceString Name=ViewPageLaunchGameElevationHint}"
Severity="Warning"/>
<clw:SettingsCard
Background="{StaticResource SystemFillColorCriticalBackgroundBrush}"
BorderBrush="{StaticResource SystemFillColorCriticalBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE730;}">
@@ -217,6 +215,7 @@
IsOn="{Binding Options.IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
</clw:SettingsCard>
<InfoBar
Margin="0,4,0,0"
IsClosable="False"
IsOpen="True"
Message="{shcm:ResourceString Name=ViewPageSettingDangerousHint}"

View File

@@ -50,7 +50,7 @@ internal sealed class AchievementImporter
{
if (await GetUIAFFromClipboardAsync().ConfigureAwait(false) is UIAF uiaf)
{
return await ImportAsync(achievementService.CurrentArchive!, uiaf).ConfigureAwait(false);
return await TryImportAsync(achievementService.CurrentArchive!, uiaf).ConfigureAwait(false);
}
else
{
@@ -85,7 +85,7 @@ internal sealed class AchievementImporter
if (isOk)
{
return await ImportAsync(achievementService.CurrentArchive, uiaf!).ConfigureAwait(false);
return await TryImportAsync(achievementService.CurrentArchive, uiaf!).ConfigureAwait(false);
}
else
{
@@ -114,7 +114,7 @@ internal sealed class AchievementImporter
}
}
private async Task<bool> ImportAsync(EntityAchievementArchive archive, UIAF uiaf)
private async Task<bool> TryImportAsync(EntityAchievementArchive archive, UIAF uiaf)
{
if (uiaf.IsCurrentVersionSupported())
{

View File

@@ -176,7 +176,7 @@ internal sealed class SpiralAbyssRecordViewModel : Abstraction.ViewModel, IRecip
private async Task UploadSpiralAbyssRecordAsync()
{
HomaClient homaClient = serviceProvider.GetRequiredService<HomaClient>();
HomaSpiralAbyssClient homaClient = serviceProvider.GetRequiredService<HomaSpiralAbyssClient>();
IInfoBarService infoBarService = serviceProvider.GetRequiredService<IInfoBarService>();
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Web.Hutao;
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
internal sealed class HomaClient2
internal sealed class HomaLogUploadClient
{
private readonly HttpClient httpClient;
@@ -21,7 +21,7 @@ internal sealed class HomaClient2
/// 构造一个新的胡桃日志客户端
/// </summary>
/// <param name="httpClient">Http客户端</param>
public HomaClient2(HttpClient httpClient)
public HomaLogUploadClient(HttpClient httpClient)
{
this.httpClient = httpClient;
}
@@ -50,4 +50,4 @@ internal sealed class HomaClient2
Info = exception.ToString(),
};
}
}
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Web.Hutao;
/// <summary>
/// 胡桃通行证客户端
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
internal sealed class HomaPassportClient
{
/// <summary>
/// 通行证请求公钥
/// </summary>
public const string PublicKey = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5W2SEyZSlP2zBI1Sn8Gd
TwbZoXlUGNKyoVrY8SVYu9GMefdGZCrUQNkCG/Np8pWPmSSEFGd5oeug/oIMtCZQ
NOn0drlR+pul/XZ1KQhKmj/arWjN1XNok2qXF7uxhqD0JyNT/Fxy6QvzqIpBsM9S
7ajm8/BOGlPG1SInDPaqTdTRTT30AuN+IhWEEFwT3Ctv1SmDupHs2Oan5qM7Y3uw
b6K1rbnk5YokiV2FzHajGUymmSKXqtG1USZzwPqImpYb4Z0M/StPFWdsKqexBqMM
mkXckI5O98GdlszEmQ0Ejv5Fx9fR2rXRwM76S4iZTfabYpiMbb4bM42mHMauupj6
9QIDAQAB
-----END PUBLIC KEY-----
""";
private readonly HttpClient httpClient;
/// <summary>
/// 构造一个新的胡桃通行证客户端
/// </summary>
/// <param name="httpClient">Http客户端</param>
public HomaPassportClient(HttpClient httpClient)
{
this.httpClient = httpClient;
}
/// <summary>
/// 异步获取验证码
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="isResetPassword">是否重置账号密码</param>
/// <param name="token">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response.Response> VerifyAsync(string email, bool isResetPassword, CancellationToken token = default)
{
Dictionary<string, object> data = new()
{
["UserName"] = Encrypt(email),
["IsResetPassword"] = isResetPassword,
};
Response.Response? resp = await httpClient
.TryCatchPostAsJsonAsync<Dictionary<string, object>, Response.Response>(HutaoEndpoints.PassportVerify, data, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步注册
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="password">密码</param>
/// <param name="verifyCode">验证码</param>
/// <param name="token">取消令牌</param>
/// <returns>响应,包含登录令牌</returns>
public async Task<Response<string>> RegisterAsync(string email, string password, string verifyCode, CancellationToken token = default)
{
Dictionary<string, string> data = new()
{
["UserName"] = Encrypt(email),
["Password"] = Encrypt(password),
["VerifyCode"] = Encrypt(verifyCode),
};
Response<string>? resp = await httpClient
.TryCatchPostAsJsonAsync<Dictionary<string, string>, Response<string>>(HutaoEndpoints.PassportRegister, data, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步重置密码
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="password">密码</param>
/// <param name="verifyCode">验证码</param>
/// <param name="token">取消令牌</param>
/// <returns>响应,包含登录令牌</returns>
public async Task<Response<string>> ResetPasswordAsync(string email, string password, string verifyCode, CancellationToken token = default)
{
Dictionary<string, string> data = new()
{
["UserName"] = Encrypt(email),
["Password"] = Encrypt(password),
["VerifyCode"] = Encrypt(verifyCode),
};
Response<string>? resp = await httpClient
.TryCatchPostAsJsonAsync<Dictionary<string, string>, Response<string>>(HutaoEndpoints.PassportResetPassword, data, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步登录
/// </summary>
/// <param name="email">邮箱</param>
/// <param name="password">密码</param>
/// <param name="token">取消令牌</param>
/// <returns>响应,包含登录令牌</returns>
public async Task<Response<string>> LoginAsync(string email, string password, CancellationToken token = default)
{
Dictionary<string, string> data = new()
{
["UserName"] = Encrypt(email),
["Password"] = Encrypt(password),
};
Response<string>? resp = await httpClient
.TryCatchPostAsJsonAsync<Dictionary<string, string>, Response<string>>(HutaoEndpoints.PassportLogin, data, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
private static string Encrypt(string text)
{
byte[] plaintextBytes = Encoding.UTF8.GetBytes(text);
using (RSACryptoServiceProvider rsa = new(2048))
{
rsa.ImportFromPem(PublicKey);
byte[] encryptedBytes = rsa.Encrypt(plaintextBytes, true);
return Convert.ToBase64String(encryptedBytes);
}
}
}

View File

@@ -19,12 +19,12 @@ namespace Snap.Hutao.Web.Hutao;
/// </summary>
[HighQuality]
[HttpClient(HttpClientConfigration.Default)]
internal sealed class HomaClient
internal sealed class HomaSpiralAbyssClient
{
private readonly HttpClient httpClient;
private readonly GameRecordClient gameRecordClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<HomaClient> logger;
private readonly ILogger<HomaSpiralAbyssClient> logger;
/// <summary>
/// 构造一个新的胡桃API客户端
@@ -33,7 +33,7 @@ internal sealed class HomaClient
/// <param name="gameRecordClient">游戏记录客户端</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public HomaClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaClient> logger)
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
{
this.httpClient = httpClient;
this.gameRecordClient = gameRecordClient;

View File

@@ -16,6 +16,29 @@ internal static class HutaoEndpoints
/// </summary>
public const string StaticHutao = "static.hut.ao";
#region Passport
/// <summary>
/// 获取注册验证码
/// </summary>
public const string PassportVerify = $"{HomaSnapGenshinApi}/Passport/Verify";
/// <summary>
/// 注册账号
/// </summary>
public const string PassportRegister = $"{HomaSnapGenshinApi}/Passport/Register";
/// <summary>
/// 重设密码
/// </summary>
public const string PassportResetPassword = $"{HomaSnapGenshinApi}/Passport/ResetPassword";
/// <summary>
/// 登录
/// </summary>
public const string PassportLogin = $"{HomaSnapGenshinApi}/Passport/Login";
#endregion
#region HutaoAPI
/// <summary>

View File

@@ -40,6 +40,12 @@ internal class Response
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
public static Response DefaultIfNull(Response? response, [CallerMemberName] string callerName = default!)
{
// 0x26F19335 is a magic number that hashed from "Snap.Hutao"
return response ?? new(0x26F19335, $"[{callerName}] 中的请求异常");
}
/// <summary>
/// 返回本体或带有消息提示的默认值
/// </summary>

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
namespace Snap.Hutao.Win32;
/// <summary>
/// 内存拓展 for <see cref="Memory{T}"/> and <see cref="Span{T}"/>
/// </summary>
[HighQuality]
internal static class MemoryExtension
{
/// <summary>
/// 将 __CHAR_256 转换到 字符串
/// </summary>
/// <param name="char256">目标字符数组</param>
/// <returns>结果字符串</returns>
public static unsafe ReadOnlySpan<byte> AsNullTerminatedReadOnlySpan(this in __CHAR_256 char256)
{
fixed (CHAR* pszChar = &char256._0)
{
return MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)pszChar);
}
}
}