mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix enka api
This commit is contained in:
@@ -36,13 +36,13 @@ internal static class CoreEnvironment
|
||||
/// 盐
|
||||
/// </summary>
|
||||
// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
|
||||
public static readonly ImmutableDictionary<SaltType, string> DynamicSecrets = new Dictionary<SaltType, string>()
|
||||
{
|
||||
[nameof(SaltType.K2)] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
|
||||
[nameof(SaltType.LK2)] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
|
||||
[nameof(SaltType.X4)] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[nameof(SaltType.X6)] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
[SaltType.K2] = "dZAwGk4e9aC0MXXItkwnHamjA1x30IYw",
|
||||
[SaltType.LK2] = "IEIZiKYaput2OCKQprNuGsog1NZc1FkS",
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2203,7 +2203,7 @@ namespace Snap.Hutao.Resource.Localization {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 移除用户 的本地化字符串。
|
||||
/// 查找类似 移除角色 的本地化字符串。
|
||||
/// </summary>
|
||||
internal static string ViewPageDailyNoteRemoveToolTip {
|
||||
get {
|
||||
|
||||
@@ -832,7 +832,7 @@
|
||||
<value>提醒通知</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteRemoveToolTip" xml:space="preserve">
|
||||
<value>移除用户</value>
|
||||
<value>移除角色</value>
|
||||
</data>
|
||||
<data name="ViewPageDailyNoteResinDiscountUsed" xml:space="preserve">
|
||||
<value>本周已消耗减半次数</value>
|
||||
|
||||
@@ -250,7 +250,7 @@ internal class GachaLogService : IGachaLogService
|
||||
{
|
||||
state.ConfigType = configType;
|
||||
long? dbEndId = null;
|
||||
GachaLogConfigration configration = new(query, configType);
|
||||
GachaLogQueryOptions configration = new(query, configType);
|
||||
List<GachaItem> itemsToAdd = new();
|
||||
|
||||
do
|
||||
@@ -285,7 +285,7 @@ internal class GachaLogService : IGachaLogService
|
||||
|
||||
progress.Report(state);
|
||||
|
||||
if (currentTypeAddingCompleted || items.Count < GachaLogConfigration.Size)
|
||||
if (currentTypeAddingCompleted || items.Count < GachaLogQueryOptions.Size)
|
||||
{
|
||||
// exit current type fetch loop
|
||||
break;
|
||||
|
||||
@@ -42,7 +42,7 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
|
||||
|
||||
if (authkeyResponse.IsOk())
|
||||
{
|
||||
return new(true, GachaLogConfigration.AsQuery(data, authkeyResponse.Data));
|
||||
return new(true, GachaLogQueryOptions.AsQuery(data, authkeyResponse.Data));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -82,12 +82,11 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
ReadOnlySpan<byte> match = isOversea
|
||||
? "https://webstatic-sea.hoyoverse.com/genshin/event/e20190909gacha-v2/index.html"u8
|
||||
: "https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html"u8;
|
||||
ReadOnlySpan<byte> zero = "\0"u8;
|
||||
|
||||
int index = span.LastIndexOf(match);
|
||||
if (index >= 0)
|
||||
{
|
||||
int length = span[index..].IndexOf(zero);
|
||||
int length = span[index..].IndexOf("\0"u8);
|
||||
return Encoding.UTF8.GetString(span.Slice(index, length));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed partial class AdoptCalculatorDialog : ContentDialog
|
||||
{
|
||||
private readonly IServiceScope scope;
|
||||
[SuppressMessage("", "IDE0052")]
|
||||
private CalculatorJsInterface? dailyNoteJsInterface;
|
||||
private MiHoYoJSInterface? jsInterface;
|
||||
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD><C2B5><EFBFBD><EFBFBD>ɼ<EFBFBD><C9BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
|
||||
@@ -50,7 +50,7 @@ public sealed partial class AdoptCalculatorDialog : ContentDialog
|
||||
}
|
||||
|
||||
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
|
||||
dailyNoteJsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
jsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
|
||||
#if DEBUG
|
||||
coreWebView2.OpenDevToolsWindow();
|
||||
@@ -60,7 +60,7 @@ public sealed partial class AdoptCalculatorDialog : ContentDialog
|
||||
|
||||
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
|
||||
{
|
||||
dailyNoteJsInterface = null;
|
||||
jsInterface = null;
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed partial class DailyNoteVerificationDialog : ContentDialog
|
||||
private readonly IServiceScope scope;
|
||||
private readonly UserAndUid userAndUid;
|
||||
|
||||
private DailyNoteJsInterface? dailyNoteJsInterface;
|
||||
private MiHoYoJSInterface? jsInterface;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的实时便笺验证对话框
|
||||
@@ -45,8 +45,8 @@ public sealed partial class DailyNoteVerificationDialog : ContentDialog
|
||||
|
||||
Model.Entity.User user = userAndUid.User;
|
||||
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
|
||||
dailyNoteJsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
dailyNoteJsInterface.ClosePageRequested += OnClosePageRequested;
|
||||
jsInterface = new(coreWebView2, scope.ServiceProvider);
|
||||
jsInterface.ClosePageRequested += OnClosePageRequested;
|
||||
|
||||
string query = $"?role_id={userAndUid.Uid.Value}&server={userAndUid.Uid.Region}";
|
||||
coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/{query}");
|
||||
@@ -59,10 +59,10 @@ public sealed partial class DailyNoteVerificationDialog : ContentDialog
|
||||
|
||||
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
|
||||
{
|
||||
if (dailyNoteJsInterface != null)
|
||||
if (jsInterface != null)
|
||||
{
|
||||
dailyNoteJsInterface!.ClosePageRequested -= OnClosePageRequested;
|
||||
dailyNoteJsInterface = null;
|
||||
jsInterface!.ClosePageRequested -= OnClosePageRequested;
|
||||
jsInterface = null;
|
||||
}
|
||||
|
||||
scope.Dispose();
|
||||
|
||||
@@ -165,66 +165,65 @@
|
||||
</ItemsControl.ItemContainerTransitions>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid
|
||||
MinHeight="48"
|
||||
Margin="0,8,0,0"
|
||||
Padding="8"
|
||||
<Border
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource CardBackgroundBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="{ThemeResource CardBorderThickness}"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Margin="8,0"
|
||||
Padding="0,0,0,0"
|
||||
Command="{Binding Path=DataContext.SaveAchievementCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
|
||||
<Grid Grid.Column="1" Margin="8,0,0,0">
|
||||
Style="{StaticResource BorderCardStyle}">
|
||||
<Grid
|
||||
MinHeight="48"
|
||||
Margin="0,8,0,0"
|
||||
Padding="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- text -->
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
<!-- time -->
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<!-- pic -->
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<!-- count -->
|
||||
<ColumnDefinition Width="32"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Inner.Title}"/>
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Margin="8,0"
|
||||
Padding="0,0,0,0"
|
||||
Command="{Binding Path=DataContext.SaveAchievementCommand, Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
|
||||
<Grid Grid.Column="1" Margin="8,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- text -->
|
||||
<ColumnDefinition/>
|
||||
<!-- time -->
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<!-- pic -->
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<!-- count -->
|
||||
<ColumnDefinition Width="32"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Inner.Title}"/>
|
||||
<TextBlock
|
||||
Margin="0,2,0,0"
|
||||
Style="{StaticResource SecondaryTextStyle}"
|
||||
Text="{Binding Inner.Description}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,2,0,0"
|
||||
Style="{StaticResource SecondaryTextStyle}"
|
||||
Text="{Binding Inner.Description}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="12,0,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Time}"
|
||||
Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<Image
|
||||
Grid.Column="2"
|
||||
Height="32"
|
||||
Source="ms-appx:///Resource/Icon/UI_ItemIcon_201.png"/>
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Inner.FinishReward.Count}"/>
|
||||
Grid.Column="1"
|
||||
Margin="12,0,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Time}"
|
||||
Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<Image
|
||||
Grid.Column="2"
|
||||
Height="32"
|
||||
Source="ms-appx:///Resource/Icon/UI_ItemIcon_201.png"/>
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Inner.FinishReward.Count}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -20,6 +20,7 @@ using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage.Pickers;
|
||||
using BindingAchievement = Snap.Hutao.Model.Binding.Achievement.Achievement;
|
||||
using BindingAchievementGoal = Snap.Hutao.Model.Binding.Achievement.AchievementGoal;
|
||||
using EntityAchievementArchive = Snap.Hutao.Model.Entity.AchievementArchive;
|
||||
using MetadataAchievementGoal = Snap.Hutao.Model.Metadata.Achievement.AchievementGoal;
|
||||
@@ -33,8 +34,8 @@ namespace Snap.Hutao.ViewModel;
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipient
|
||||
{
|
||||
private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(Model.Binding.Achievement.Achievement.IsChecked), SortDirection.Ascending);
|
||||
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(Model.Binding.Achievement.Achievement.Time), SortDirection.Descending);
|
||||
private static readonly SortDescription IncompletedItemsFirstSortDescription = new(nameof(BindingAchievement.IsChecked), SortDirection.Ascending);
|
||||
private static readonly SortDescription CompletionTimeSortDescription = new(nameof(BindingAchievement.Time), SortDirection.Descending);
|
||||
|
||||
private readonly IAchievementService achievementService;
|
||||
private readonly IMetadataService metadataService;
|
||||
@@ -84,7 +85,7 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
RemoveArchiveCommand = new AsyncRelayCommand(RemoveArchiveAsync);
|
||||
SearchAchievementCommand = new RelayCommand<string>(SearchAchievement);
|
||||
SortIncompletedSwitchCommand = new RelayCommand(UpdateAchievementsSort);
|
||||
SaveAchievementCommand = new RelayCommand<Model.Binding.Achievement.Achievement>(SaveAchievement);
|
||||
SaveAchievementCommand = new RelayCommand<BindingAchievement>(SaveAchievement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -352,13 +353,13 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
{
|
||||
if (search.Length == 5 && int.TryParse(search, out int achiId))
|
||||
{
|
||||
Achievements.Filter = (object o) => ((Model.Binding.Achievement.Achievement)o).Inner.Id == achiId;
|
||||
Achievements.Filter = (object o) => ((BindingAchievement)o).Inner.Id == achiId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Achievements.Filter = (object o) =>
|
||||
{
|
||||
Model.Binding.Achievement.Achievement achi = (Model.Binding.Achievement.Achievement)o;
|
||||
BindingAchievement achi = (BindingAchievement)o;
|
||||
return achi.Inner.Title.Contains(search) || achi.Inner.Description.Contains(search);
|
||||
};
|
||||
}
|
||||
@@ -494,7 +495,7 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
private async Task UpdateAchievementsAsync(EntityAchievementArchive archive)
|
||||
{
|
||||
List<Model.Metadata.Achievement.Achievement> rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false);
|
||||
List<Model.Binding.Achievement.Achievement> combined;
|
||||
List<BindingAchievement> combined;
|
||||
try
|
||||
{
|
||||
combined = achievementService.GetAchievements(archive, rawAchievements);
|
||||
@@ -535,7 +536,7 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
if (Achievements != null)
|
||||
{
|
||||
Achievements.Filter = goal != null
|
||||
? ((object o) => o is Model.Binding.Achievement.Achievement achi && achi.Inner.Goal == goal.Id)
|
||||
? ((object o) => o is BindingAchievement achi && achi.Inner.Goal == goal.Id)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -547,7 +548,7 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
if (Achievements != null && AchievementGoals != null)
|
||||
{
|
||||
Dictionary<int, GoalAggregation> counter = AchievementGoals.ToDictionary(x => x.Id, x => new GoalAggregation(x));
|
||||
foreach (Model.Binding.Achievement.Achievement achievement in Achievements.SourceCollection.OfType<Model.Binding.Achievement.Achievement>())
|
||||
foreach (BindingAchievement achievement in Achievements.SourceCollection.OfType<BindingAchievement>())
|
||||
{
|
||||
ref GoalAggregation aggregation = ref CollectionsMarshal.GetValueRefOrNullRef(counter, achievement.Inner.Goal);
|
||||
aggregation.Count += 1;
|
||||
@@ -568,7 +569,7 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveAchievement(Model.Binding.Achievement.Achievement? achievement)
|
||||
private void SaveAchievement(BindingAchievement? achievement)
|
||||
{
|
||||
if (achievement != null)
|
||||
{
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace Snap.Hutao.Web.Bridge;
|
||||
|
||||
/// <summary>
|
||||
/// 养成计算器调用桥
|
||||
/// </summary>
|
||||
public class CalculatorJsInterface : MiHoYoJSInterface
|
||||
{
|
||||
/// <inheritdoc cref="MiHoYoJSInterface(CoreWebView2, IServiceProvider)"/>
|
||||
public CalculatorJsInterface(CoreWebView2 webView, IServiceProvider serviceProvider)
|
||||
: base(webView, serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace Snap.Hutao.Web.Bridge;
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺页面调用桥
|
||||
/// </summary>
|
||||
public class DailyNoteJsInterface : MiHoYoJSInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的实时便笺页面调用桥
|
||||
/// </summary>
|
||||
/// <param name="webView">webview</param>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public DailyNoteJsInterface(CoreWebView2 webView, IServiceProvider serviceProvider)
|
||||
: base(webView, serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class MiHoYoJSInterface
|
||||
/// <returns>响应</returns>
|
||||
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV1(JsParam param)
|
||||
{
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[nameof(SaltType.LK2)];
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[SaltType.LK2];
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
string r = GetRandomString();
|
||||
string check = Core.Convert.ToMd5HexString($"salt={salt}&t={t}&r={r}").ToLowerInvariant();
|
||||
@@ -152,7 +152,7 @@ public class MiHoYoJSInterface
|
||||
/// <returns>响应</returns>
|
||||
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV2(JsParam<DynamicSecrect2Playload> param)
|
||||
{
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[nameof(SaltType.X4)];
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[SaltType.X4];
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
int r = GetRandom();
|
||||
string b = param.Payload.Body;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.Web.Enka;
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class EnkaClient
|
||||
{
|
||||
private const string EnkaAPI = "https://enka.network/u/{0}/__data.json";
|
||||
private const string EnkaAPI = "https://enka.network/api/uid/{0}";
|
||||
private const string EnkaAPIHutaoForward = "https://enka-api.hut.ao/{0}";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
@@ -47,8 +47,7 @@ internal class AccountClient
|
||||
{
|
||||
Response<GameAuthKey>? resp = await httpClient
|
||||
.SetUser(user, CookieType.Stoken)
|
||||
|
||||
// .SetReferer("https://app.mihoyo.com")
|
||||
.SetReferer("https://app.mihoyo.com")
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, false)
|
||||
.TryCatchPostAsJsonAsync<GenAuthKeyData, Response<GameAuthKey>>(ApiEndpoints.AppAuthGenAuthKey, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
|
||||
/// <summary>
|
||||
/// 动态密钥创建选项
|
||||
/// </summary>
|
||||
internal class DynamicSecretCreationOptions
|
||||
{
|
||||
private const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
|
||||
|
||||
private readonly bool includeChars;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的动态密钥创建选项
|
||||
/// </summary>
|
||||
/// <param name="option">选项字符串</param>
|
||||
public DynamicSecretCreationOptions(string option)
|
||||
{
|
||||
string[] definations = option.Split('|');
|
||||
string version = definations[0];
|
||||
string saltType = definations[1];
|
||||
string includeChars = definations[2];
|
||||
|
||||
Version = Enum.Parse<DynamicSecretVersion>(version);
|
||||
SaltType = Enum.Parse<SaltType>(saltType);
|
||||
this.includeChars = bool.Parse(includeChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DS 版本
|
||||
/// </summary>
|
||||
public DynamicSecretVersion Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// SALT 类型
|
||||
/// </summary>
|
||||
public SaltType SaltType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认 Body
|
||||
/// </summary>
|
||||
public string DefaultBody
|
||||
{
|
||||
get => SaltType == SaltType.PROD ? "{}" : string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 随机字符串
|
||||
/// </summary>
|
||||
public string RandomString
|
||||
{
|
||||
get => includeChars ? GetRandomStringWithChars() : GetRandomStringNoChars();
|
||||
}
|
||||
|
||||
private static string GetRandomStringWithChars()
|
||||
{
|
||||
StringBuilder sb = new(6);
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
int pos = Random.Shared.Next(0, RandomRange.Length);
|
||||
sb.Append(RandomRange[pos]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetRandomStringNoChars()
|
||||
{
|
||||
int rand = Random.Shared.Next(100000, 200000);
|
||||
return $"{(rand == 100000 ? 642367 : rand)}";
|
||||
}
|
||||
}
|
||||
@@ -13,63 +13,43 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
[Injection(InjectAs.Transient)]
|
||||
public class DynamicSecretHandler : DelegatingHandler
|
||||
{
|
||||
private const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
|
||||
{
|
||||
if (request.Headers.TryGetValues("DS-Option", out IEnumerable<string>? values))
|
||||
{
|
||||
string[] definations = values.Single().Split('|');
|
||||
string version = definations[0];
|
||||
string saltType = definations[1];
|
||||
bool includeChars = definations[2] == "true";
|
||||
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[saltType];
|
||||
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
string r = includeChars ? GetRandomStringWithChars() : GetRandomStringNoChars();
|
||||
|
||||
string dsContent = $"salt={salt}&t={t}&r={r}";
|
||||
|
||||
if (version == nameof(DynamicSecretVersion.Gen2))
|
||||
{
|
||||
string b = request.Content != null
|
||||
? await request.Content.ReadAsStringAsync(token).ConfigureAwait(false)
|
||||
: (saltType == nameof(SaltType.PROD) ? "{}" : string.Empty); // PROD's default value is {}
|
||||
|
||||
string[] queries = Uri.UnescapeDataString(request.RequestUri!.Query).Split('?', 2);
|
||||
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
|
||||
|
||||
dsContent = $"{dsContent}&b={b}&q={q}";
|
||||
}
|
||||
|
||||
string check = Core.Convert.ToMd5HexString(dsContent).ToLowerInvariant();
|
||||
|
||||
DynamicSecretCreationOptions options = new(values.Single());
|
||||
await ProcessRequestWithOptionsAsync(request, options, token).ConfigureAwait(false);
|
||||
request.Headers.Remove("DS-Option");
|
||||
request.Headers.Set("DS", $"{t},{r},{check}");
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GetRandomStringWithChars()
|
||||
private static async Task ProcessRequestWithOptionsAsync(HttpRequestMessage request, DynamicSecretCreationOptions options, CancellationToken token)
|
||||
{
|
||||
StringBuilder sb = new(6);
|
||||
string salt = Core.CoreEnvironment.DynamicSecrets[options.SaltType];
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
string r = options.RandomString;
|
||||
|
||||
string dsContent = $"salt={salt}&t={t}&r={r}";
|
||||
|
||||
// ds2 b & q process
|
||||
if (options.Version == DynamicSecretVersion.Gen2)
|
||||
{
|
||||
int pos = Random.Shared.Next(0, RandomRange.Length);
|
||||
sb.Append(RandomRange[pos]);
|
||||
string b = request.Content != null
|
||||
? await request.Content.ReadAsStringAsync(token).ConfigureAwait(false)
|
||||
: options.DefaultBody; // PROD's default value is {}
|
||||
|
||||
string[] queries = Uri.UnescapeDataString(request.RequestUri!.Query).Split('?', 2);
|
||||
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
|
||||
|
||||
dsContent = $"{dsContent}&b={b}&q={q}";
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
string check = Core.Convert.ToMd5HexString(dsContent).ToLowerInvariant();
|
||||
request.Headers.Set("DS", $"{t},{r},{check}");
|
||||
}
|
||||
|
||||
private static string GetRandomStringNoChars()
|
||||
{
|
||||
int rand = Random.Shared.Next(100000, 200000);
|
||||
return $"{(rand == 100000 ? 642367 : rand)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ internal class GachaInfoClient
|
||||
/// <param name="config">查询</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>单个祈愿记录页面</returns>
|
||||
public async Task<Response<GachaLogPage>> GetGachaLogPageAsync(GachaLogConfigration config, CancellationToken token = default)
|
||||
public async Task<Response<GachaLogPage>> GetGachaLogPageAsync(GachaLogQueryOptions config, CancellationToken token = default)
|
||||
{
|
||||
string query = config.AsQuery();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
|
||||
/// <summary>
|
||||
/// 祈愿记录请求配置
|
||||
/// </summary>
|
||||
public struct GachaLogConfigration
|
||||
public struct GachaLogQueryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 尺寸
|
||||
@@ -17,12 +17,12 @@ public struct GachaLogConfigration
|
||||
public const int Size = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Below keys are required:
|
||||
/// Keys required:
|
||||
/// authkey_ver
|
||||
/// auth_appid
|
||||
/// authkey
|
||||
/// sign_type
|
||||
/// Below keys used as control:
|
||||
/// Keys used as control:
|
||||
/// lang
|
||||
/// gacha_type
|
||||
/// size
|
||||
@@ -36,7 +36,7 @@ public struct GachaLogConfigration
|
||||
/// <param name="query">原始查询字符串</param>
|
||||
/// <param name="type">祈愿类型</param>
|
||||
/// <param name="endId">终止Id</param>
|
||||
public GachaLogConfigration(string query, GachaConfigType type, long endId = 0L)
|
||||
public GachaLogQueryOptions(string query, GachaConfigType type, long endId = 0L)
|
||||
{
|
||||
innerQuery = QueryString.Parse(query);
|
||||
|
||||
Reference in New Issue
Block a user