add hutao api client impl

This commit is contained in:
DismissedLight
2022-06-04 18:53:57 +08:00
parent 83e045697a
commit 912895549e
96 changed files with 3189 additions and 336 deletions

View File

@@ -91,6 +91,9 @@ dotnet_diagnostic.SA1623.severity = none
# SA1636: File header copyright text should match
dotnet_diagnostic.SA1636.severity = none
# SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = none
[*.vb]
#### 命名样式 ####

View File

@@ -42,6 +42,7 @@ public partial class App : Application
{
IServiceProvider services = new ServiceCollection()
.AddLogging(builder => builder.AddDebug())
.AddDatebase()
.AddHttpClients()
.AddDefaultJsonSerializerOptions()
.AddInjections(typeof(App))

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Context.Database;
@@ -18,4 +19,9 @@ internal class AppDbContext : DbContext
: base(options)
{
}
/// <summary>
/// 设置项
/// </summary>
public DbSet<SettingEntry> Settings { get; set; } = default!;
}

View File

@@ -0,0 +1,33 @@
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Media.Animation;
using System.Numerics;
namespace Snap.Hutao.Control.Animation;
/// <summary>
/// 图片放大动画
/// </summary>
internal class ImageZoomInAnimation : ImplicitAnimation<string, Vector3>
{
/// <summary>
/// 构造一个新的图片放大动画
/// </summary>
public ImageZoomInAnimation()
{
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
To = "1.1";
Duration = TimeSpan.FromSeconds(0.5);
}
/// <inheritdoc/>
protected override string ExplicitTarget => nameof(Visual.Scale);
/// <inheritdoc/>
protected override (Vector3?, Vector3?) GetParsedValues()
{
return (To?.ToVector3(), From?.ToVector3());
}
}

View File

@@ -0,0 +1,33 @@
using CommunityToolkit.WinUI.UI;
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Media.Animation;
using System.Numerics;
namespace Snap.Hutao.Control.Animation;
/// <summary>
/// 图片缩小动画
/// </summary>
internal class ImageZoomOutAnimation : ImplicitAnimation<string, Vector3>
{
/// <summary>
/// 构造一个新的图片缩小动画
/// </summary>
public ImageZoomOutAnimation()
{
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut;
EasingType = CommunityToolkit.WinUI.UI.Animations.EasingType.Circle;
To = "1";
Duration = TimeSpan.FromSeconds(0.5);
}
/// <inheritdoc/>
protected override string ExplicitTarget => nameof(Visual.Scale);
/// <inheritdoc/>
protected override (Vector3?, Vector3?) GetParsedValues()
{
return (To?.ToVector3(), From?.ToVector3());
}
}

View File

@@ -40,16 +40,22 @@ internal class AutoHeightBehavior : BehaviorBase<FrameworkElement>
protected override void OnAssociatedObjectLoaded()
{
AssociatedObject.SizeChanged += OnSizeChanged;
UpdateElementHeight();
}
/// <inheritdoc/>
protected override void OnAssociatedObjectUnloaded()
protected override void OnDetaching()
{
AssociatedObject.SizeChanged -= OnSizeChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
AssociatedObject.Height = (double)((FrameworkElement)sender).ActualWidth * (TargetHeight / TargetWidth);
UpdateElementHeight();
}
private void UpdateElementHeight()
{
AssociatedObject.Height = (double)AssociatedObject.ActualWidth * (TargetHeight / TargetWidth);
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可异步初始化
/// </summary>
internal interface IAsyncInitializable
{
/// <summary>
/// 是否已经初始化完成
/// </summary>
public bool IsInitialized { get; }
/// <summary>
/// 异步初始化
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>初始化任务</returns>
Task<bool> InitializeAsync(CancellationToken cancellationToken = default);
}

View File

@@ -41,4 +41,4 @@ public static class Browser
failAction?.Invoke(ex);
}
}
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Converting;
/// <summary>
/// 支持Md5转换
/// </summary>
internal abstract class Md5Convert
{
/// <summary>
/// 获取字符串的MD5计算结果
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>计算的结果</returns>
public static string ToHexString(string source)
{
byte[] bytes = Encoding.UTF8.GetBytes(source);
byte[] hash = MD5.HashData(bytes);
return Convert.ToHexString(hash);
}
}

View File

@@ -2,9 +2,8 @@
// Licensed under the MIT license.
using Microsoft.Win32;
using Snap.Hutao.Core.Converting;
using Snap.Hutao.Extension;
using System.Security.Cryptography;
using System.Text;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;
@@ -41,8 +40,6 @@ internal static class CoreEnvironment
{
string userName = Environment.UserName;
object? machineGuid = Registry.GetValue(CryptographyKey, MachineGuidValue, userName);
byte[] bytes = Encoding.UTF8.GetBytes($"{userName}{machineGuid}");
byte[] hash = MD5.HashData(bytes);
return Convert.ToHexString(hash);
return Md5Convert.ToHexString($"{userName}{machineGuid}");
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Net.Http;
namespace Snap.Hutao.Core;
/// <summary>
/// Http Json 处理
/// </summary>
public class HttpJson
{
private readonly Json json;
private readonly HttpClient httpClient;
/// <summary>
/// 初始化一个新的 Http Json 处理 实例
/// </summary>
/// <param name="json">Json 处理器</param>
/// <param name="httpClient">http 客户端</param>
public HttpJson(Json json, HttpClient httpClient)
{
this.json = json;
this.httpClient = httpClient;
}
/// <summary>
/// 从网站上下载json并转换为对象
/// </summary>
/// <typeparam name="T">对象的类型</typeparam>
/// <param name="url">链接</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
public async Task<T?> FromWebsiteAsync<T>(string url, CancellationToken cancellationToken = default)
{
string response = await httpClient.GetStringAsync(url, cancellationToken);
return json.ToObject<T>(response);
}
}

View File

@@ -1,133 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Newtonsoft.Json;
using System.IO;
namespace Snap.Hutao.Core;
/// <summary>
/// Json操作
/// </summary>
[Injection(InjectAs.Transient)]
public class Json
{
private readonly ILogger logger;
private readonly JsonSerializerSettings jsonSerializerSettings = new()
{
DateFormatString = "yyyy'-'MM'-'dd' 'HH':'mm':'ss.FFFFFFFK",
Formatting = Formatting.Indented,
};
/// <summary>
/// 初始化一个新的 Json操作 实例
/// </summary>
/// <param name="logger">日志器</param>
public Json(ILogger<Json> logger)
{
this.logger = logger;
}
/// <summary>
/// 将JSON反序列化为指定的.NET类型
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="value">要反序列化的JSON</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
public T? ToObject<T>(string value)
{
try
{
return JsonConvert.DeserializeObject<T>(value);
}
catch (Exception ex)
{
logger.LogError("反序列化Json时遇到问题:{ex}", ex);
}
return default;
}
/// <summary>
/// 将JSON反序列化为指定的.NET类型
/// 若为null则返回一个新建的实例
/// </summary>
/// <typeparam name="T">指定的类型</typeparam>
/// <param name="value">字符串</param>
/// <returns>Json字符串中的反序列化对象, 如果反序列化失败会抛出异常</returns>
public T ToObjectOrNew<T>(string value)
where T : new()
{
return ToObject<T>(value) ?? new T();
}
/// <summary>
/// 将指定的对象序列化为JSON字符串
/// </summary>
/// <param name="value">要序列化的对象</param>
/// <returns>对象的JSON字符串表示形式</returns>
public string Stringify(object? value)
{
return JsonConvert.SerializeObject(value, jsonSerializerSettings);
}
/// <summary>
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="fileName">存放JSON数据的文件路径</param>
/// <returns>JSON字符串中的反序列化对象, 如果反序列化失败则抛出异常,若文件不存在则返回 <see langword="null"/></returns>
public T? FromFile<T>(string fileName)
{
if (File.Exists(fileName))
{
// FileShare.Read is important to read some file
using (StreamReader sr = new(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)))
{
return ToObject<T>(sr.ReadToEnd());
}
}
else
{
return default;
}
}
/// <summary>
/// 使用 <see cref="FileMode.Open"/>, <see cref="FileAccess.Read"/> 和 <see cref="FileShare.Read"/> 从文件中读取后转化为实体类
/// 若为null则返回一个新建的实例
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="fileName">存放JSON数据的文件路径</param>
/// <returns>JSON字符串中的反序列化对象</returns>
public T FromFileOrNew<T>(string fileName)
where T : new()
{
return FromFile<T>(fileName) ?? new T();
}
/// <summary>
/// 从文件中读取后转化为实体类
/// </summary>
/// <typeparam name="T">要反序列化的对象的类型</typeparam>
/// <param name="file">存放JSON数据的文件</param>
/// <returns>JSON字符串中的反序列化对象</returns>
public T? FromFile<T>(FileInfo file)
{
using (StreamReader sr = file.OpenText())
{
return ToObject<T>(sr.ReadToEnd());
}
}
/// <summary>
/// 将对象保存到文件
/// </summary>
/// <param name="fileName">文件名称</param>
/// <param name="value">对象</param>
public void ToFile(string fileName, object? value)
{
File.WriteAllText(fileName, Stringify(value));
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Core.Json.Converter;
/// <summary>
/// 逗号分隔列表转换器
/// </summary>
internal class SeparatorCommaInt32EnumerableConverter : JsonConverter<IEnumerable<int>>
{
/// <inheritdoc/>
public override IEnumerable<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? team = reader.GetString();
IEnumerable<int>? ids = team?.Split(',').Select(x => int.Parse(x));
return ids ?? Enumerable.Empty<int>();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, IEnumerable<int> value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(',', value));
}
}

View File

@@ -1,43 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core;
/// <summary>
/// 简单的实现了 <see cref="INotifyPropertyChanged"/> 接口
/// </summary>
public class Observable : INotifyPropertyChanged
{
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// 设置字段的值
/// </summary>
/// <typeparam name="T">字段类型</typeparam>
/// <param name="storage">现有值</param>
/// <param name="value">新的值</param>
/// <param name="propertyName">属性名称</param>
protected void Set<T>([NotNullIfNotNull("value")] ref T storage, T value, [CallerMemberName] string propertyName = default!)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
/// <summary>
/// 触发 <see cref="PropertyChanged"/>
/// </summary>
/// <param name="propertyName">属性名称</param>
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -68,7 +68,7 @@ internal static class LocalSetting
}
else
{
Set(key, defaultValue);
SetValueType(key, defaultValue);
return defaultValue;
}
}

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Core.Validation;
public static class Must
{
/// <summary>
/// Throws an exception if the specified parameter's value is null.
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is null.
/// </summary>
/// <typeparam name="T">The type of the parameter.</typeparam>
/// <param name="value">The value of the argument.</param>

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core;
/// 检测 WebView2运行时 是否存在
/// 不再使用注册表检查方式
/// </summary>
internal class WebView2Helper
internal static class WebView2Helper
{
private static bool hasEverDetected = false;
private static bool isSupported = false;

View File

@@ -0,0 +1,69 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Extension;
/// <summary>
/// <see cref="IEnumerable{T}"/> 扩展
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// 将二维可枚举对象一维化
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <returns>扁平的对象</returns>
public static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source)
{
return source.SelectMany(x => x);
}
/// <summary>
/// 计数
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <typeparam name="TKey">计数的键类型</typeparam>
/// <param name="source">源</param>
/// <param name="keySelector">键选择器</param>
/// <returns>计数表</returns>
public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)
where TKey : notnull, IEquatable<TKey>
{
CounterInt32<TKey> counter = new();
foreach (TSource item in source)
{
counter.Increase(keySelector(item));
}
return counter;
}
/// <summary>
/// 表示一个对 <see cref="TItem"/> 类型的计数器
/// </summary>
/// <typeparam name="TItem">待计数的类型</typeparam>
private class CounterInt32<TItem> : Dictionary<TItem, int>
where TItem : notnull, IEquatable<TItem>
{
/// <summary>
/// 增加计数器
/// </summary>
/// <param name="item">物品</param>
public void Increase(TItem? item)
{
if (item != null)
{
if (!ContainsKey(item))
{
this[item] = 0;
}
this[item] += 1;
}
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
global using CommunityToolkit.Mvvm.DependencyInjection;
global using Microsoft;
global using Microsoft.Extensions.Logging;
global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;

View File

@@ -76,11 +76,7 @@ internal static class IocConfiguration
bool shouldMigrate = false;
if (!myDocument.FileExists(dbFile))
{
shouldMigrate = true;
}
else
if (myDocument.FileExists(dbFile))
{
string? versionString = LocalSetting.Get<string>(SettingKeys.LastAppVersion);
if (Version.TryParse(versionString, out Version? lastVersion))
@@ -91,6 +87,10 @@ internal static class IocConfiguration
}
}
}
else
{
shouldMigrate = true;
}
if (shouldMigrate)
{
@@ -104,6 +104,6 @@ internal static class IocConfiguration
LocalSetting.Set(SettingKeys.LastAppVersion, CoreEnvironment.Version.ToString());
return services
.AddPooledDbContextFactory<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
.AddDbContextPool<AppDbContext>(builder => builder.UseSqlite(sqlConnectionString));
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 设置入口
/// </summary>
[Table("settings")]
public class SettingEntry
{
/// <summary>
/// 构造一个新的设置入口
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
public SettingEntry(string key, string? value)
{
Key = key;
Value = value;
}
/// <summary>
/// 键
/// </summary>
[Key]
public string Key { get; set; }
/// <summary>
/// 值
/// </summary>
public string? Value { get; set; }
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
/// <summary>
/// 用户
/// </summary>
[Table("users")]
public class User
{
/// <summary>
/// 用户的Cookie
/// </summary>
public string? Cookie { get; set; }
}

View File

@@ -62,4 +62,4 @@ public interface INavigationService
/// <param name="pageType">同步的页面类型</param>
/// <returns>是否同步成功</returns>
bool SyncSelectedNavigationViewItemWith(Type pageType);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
namespace Snap.Hutao.Service.Abstraction;
/// <summary>
/// 用户服务
/// </summary>
public interface IUserService
{
/// <summary>
/// 获取当前用户信息
/// </summary>
User CurrentUser { get; }
}

View File

@@ -28,7 +28,7 @@ internal class AnnouncementService : IAnnouncementService
/// <inheritdoc/>
public async Task<AnnouncementWrapper> GetAnnouncementsAsync(ICommand openAnnouncementUICommand, CancellationToken cancellationToken = default)
{
AnnouncementWrapper? wrapper = await announcementProvider.GetAnnouncementWrapperAsync(cancellationToken);
AnnouncementWrapper? wrapper = await announcementProvider.GetAnnouncementsAsync(cancellationToken);
List<AnnouncementContent> contents = await announcementProvider.GetAnnouncementContentsAsync(cancellationToken);
Dictionary<int, string?> contentMap = contents
@@ -42,12 +42,6 @@ internal class AnnouncementService : IAnnouncementService
// 将公告内容联入公告列表
JoinAnnouncements(openAnnouncementUICommand, contentMap, announcementListWrappers);
// we only cares about activities
if (announcementListWrappers[0].List is List<Announcement> activities)
{
AdjustActivitiesTime(ref activities);
}
return wrapper;
}
@@ -64,7 +58,6 @@ internal class AnnouncementService : IAnnouncementService
{
listWrapper.List?.ForEach(item =>
{
// fix key issue
if (contentMap.TryGetValue(item.AnnId, out string? rawContent))
{
// remove <t/> tag
@@ -76,26 +69,4 @@ internal class AnnouncementService : IAnnouncementService
});
});
}
private void AdjustActivitiesTime(ref List<Announcement> activities)
{
// Match yyyy/MM/dd HH:mm:ss time format
Regex dateTimeRegex = new(@"(\d+\/\d+\/\d+\s\d+:\d+:\d+)", RegexOptions.IgnoreCase | RegexOptions.Multiline);
activities.ForEach(item =>
{
Match matched = dateTimeRegex.Match(item.Content ?? string.Empty);
if (matched.Success && DateTime.TryParse(matched.Value, out DateTime time))
{
if (time > item.StartTime && time < item.EndTime)
{
item.StartTime = time;
}
}
});
activities = activities
.OrderBy(i => i.StartTime)
.ThenBy(i => i.EndTime)
.ToList();
}
}

View File

@@ -13,8 +13,7 @@
<Nullable>enable</Nullable>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<NeutralLanguage>zh-CN</NeutralLanguage>
<DefaultLanguage>zh-cn</DefaultLanguage>
<DefaultLanguage>zh-CN</DefaultLanguage>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
@@ -53,15 +52,11 @@
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="Microsoft.AppCenter.Analytics" Version="4.5.1" />
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.5.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.1.46" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.2.32" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -26,7 +26,6 @@ public sealed partial class MainView : UserControl
navigationService = Ioc.Default.GetRequiredService<INavigationService>();
navigationService.Initialize(NavView, ContentFrame);
navigationService.Navigate<Page.AnnouncementPage>();
navigationService.Navigate<Page.AnnouncementPage>(true);
}
}

View File

@@ -13,7 +13,7 @@
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcc="using:Snap.Hutao.Control.Cancellable"
xmlns:shvc="using:Snap.Hutao.View.Converter"
xmlns:shvc="using:Snap.Hutao.View.Converter" xmlns:shca="using:Snap.Hutao.Control.Animation"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
@@ -82,18 +82,10 @@
</Border.Background>
<cwua:Explicit.Animations>
<cwua:AnimationSet x:Name="ImageZoomInAnimation">
<cwua:ScaleAnimation
EasingMode="EaseOut"
EasingType="Circle"
To="1.1"
Duration="0:0:0.5"/>
<shca:ImageZoomInAnimation/>
</cwua:AnimationSet>
<cwua:AnimationSet x:Name="ImageZoomOutAnimation">
<cwua:ScaleAnimation
EasingMode="EaseOut"
EasingType="Circle"
To="1"
Duration="0:0:0.5"/>
<shca:ImageZoomOutAnimation/>
</cwua:AnimationSet>
</cwua:Explicit.Animations>
</Border>
@@ -105,12 +97,6 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="{Binding ShouldShowTimeDescription,Converter={StaticResource BoolToVisibilityConverter}}">
<!--<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#00000000"/>
<GradientStop Offset="1" Color="#A0000000"/>
</LinearGradientBrush>
</Border.Background>-->
<ProgressBar
MinHeight="2"
Value="{Binding TimePercent,Mode=OneWay}"

View File

@@ -11,13 +11,34 @@ internal static class ApiEndpoints
/// <summary>
/// 公告列表
/// </summary>
public static readonly string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}";
public const string AnnList = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnList?{AnnouncementQuery}";
/// <summary>
/// 公告内容
/// </summary>
public static readonly string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}";
public const string AnnContent = $"{Hk4eApi}/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery}";
/// <summary>
/// 游戏记录主页
/// </summary>
public const string GameRecordIndex = $"{ApiTakumiRecordApi}/index?role_id={{0}}&server={{1}}";
/// <summary>
/// 深渊信息
/// </summary>
public const string SpiralAbyss = $"{ApiTakumiRecordApi}/spiralAbyss?schedule_type={{0}}&role_id={{1}}&server={{2}}";
/// <summary>
/// 角色信息
/// </summary>
public const string Character = $"{ApiTakumiRecordApi}/character";
public const string UserGameRoles = $"{ApiTakumi}/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn";
private const string ApiTakumi = "https://api-takumi.mihoyo.com";
private const string ApiTakumiRecord = "https://api-takumi-record.mihoyo.com";
private const string ApiTakumiRecordApi = $"{ApiTakumiRecord}/game_record/app/genshin/api";
private const string Hk4eApi = "https://hk4e-api.mihoyo.com";
private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc&region=cn_gf01&level=55&uid=100000000";
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Converting;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Web.Response;
using System.Linq;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
/// <summary>
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供2代动态密钥
/// </summary>
internal abstract class DynamicSecretProvider2 : Md5Convert
{
/// <summary>
/// 似乎已经与版本号无关自2.11.1以来未曾改变salt
/// </summary>
public const string AppVersion = "2.16.1";
/// <summary>
/// 米游社的盐
/// 计算过程https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
/// </summary>
private static readonly string APISalt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
private static readonly Random Random = new();
/// <summary>
/// 创建动态密钥
/// </summary>
/// <param name="json">json格式化器</param>
/// <param name="queryUrl">查询url</param>
/// <param name="postBody">请求体</param>
/// <returns>密钥</returns>
public static string Create(Json json, string queryUrl, object? postBody = null)
{
// unix timestamp
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// random
string r = GetRandomString();
// body
string b = postBody is null ? string.Empty : json.Stringify(postBody);
// query
string q = string.Empty;
string? query = new UriBuilder(queryUrl).Query;
if (!string.IsNullOrEmpty(query))
{
q = string.Join("&", query.Split('&').OrderBy(x => x));
}
// check
string check = ToHexString($"salt={APISalt}&t={t}&r={r}&b={b}&q={q}");
return $"{t},{r},{check}";
}
private static string GetRandomString()
{
// 原汁原味
// https://github.com/Azure99/GenshinPlayerQuery/issues/20#issuecomment-913641512
// int rand = (int)((Random.Next() / (int.MaxValue + 3D) * 100000D) + 100000D) % 1000000;
// if (rand < 100001)
// {
// rand += 542367;
// }
int rand = Random.Next(100000, 200000);
if (rand == 100000)
{
rand = 642367;
}
return rand.ToString();
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
/// <summary>
/// 动态密钥扩展
/// </summary>
internal static class RequesterExtensions
{
/// <summary>
/// 使用二代动态密钥执行 GET 操作
/// </summary>
/// <typeparam name="TResult">返回的类类型</typeparam>
/// <param name="requester">请求器</param>
/// <param name="url">地址</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应</returns>
public static async Task<Response<TResult>?> GetUsingDS2Async<TResult>(
this Requester requester,
string url,
CancellationToken cancellationToken = default)
{
requester.Headers["DS"] = DynamicSecretProvider2.Create(requester.Json, url);
return await requester
.GetAsync<TResult>(url, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// 使用二代动态密钥执行 POST 操作
/// </summary>
/// <typeparam name="TResult">返回的类类型</typeparam>
/// <param name="requester">请求器</param>
/// <param name="url">地址</param>
/// <param name="data">post数据</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应</returns>
public static async Task<Response<TResult>?> PostUsingDS2Async<TResult>(
this Requester requester,
string url,
object data,
CancellationToken cancellationToken = default)
{
requester.Headers["DS"] = DynamicSecretProvider2.Create(requester.Json, url, data);
return await requester
.PostAsync<TResult>(url, data, cancellationToken)
.ConfigureAwait(false);
}
}

View File

@@ -100,19 +100,19 @@ public class Announcement : AnnouncementContent
/// 类型标签
/// </summary>
[JsonPropertyName("type_label")]
public string? TypeLabel { get; set; }
public string TypeLabel { get; set; } = default!;
/// <summary>
/// 标签文本
/// </summary>
[JsonPropertyName("tag_label")]
public string? TagLabel { get; set; }
public string TagLabel { get; set; } = default!;
/// <summary>
/// 标签图标
/// </summary>
[JsonPropertyName("tag_icon")]
public string? TagIcon { get; set; }
public string TagIcon { get; set; } = default!;
/// <summary>
/// 登录提醒
@@ -156,13 +156,13 @@ public class Announcement : AnnouncementContent
/// 标签开始时间
/// </summary>
[JsonPropertyName("tag_start_time")]
public string? TagStartTime { get; set; }
public string TagStartTime { get; set; } = default!;
/// <summary>
/// 标签结束时间
/// </summary>
[JsonPropertyName("tag_end_time")]
public string? TagEndTime { get; set; }
public string TagEndTime { get; set; } = default!;
/// <summary>
/// 提醒版本

View File

@@ -20,30 +20,30 @@ public class AnnouncementContent
/// 公告标题
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
public string Title { get; set; } = default!;
/// <summary>
/// 副标题
/// </summary>
[JsonPropertyName("subtitle")]
public string? Subtitle { get; set; }
public string Subtitle { get; set; } = default!;
/// <summary>
/// 横幅Url
/// </summary>
[JsonPropertyName("banner")]
public string? Banner { get; set; }
public string Banner { get; set; } = default!;
/// <summary>
/// 内容字符串
/// 可能包含了一些html格式
/// </summary>
[JsonPropertyName("content")]
public string? Content { get; set; }
public string Content { get; set; } = default!;
/// <summary>
/// 语言
/// </summary>
[JsonPropertyName("lang")]
public string? Lang { get; set; }
public string Lang { get; set; } = default!;
}

View File

@@ -21,5 +21,5 @@ public class AnnouncementListWrapper : ListWrapper<Announcement>
/// 类型标签
/// </summary>
[JsonPropertyName("type_label")]
public string? TypeLabel { get; set; }
public string TypeLabel { get; set; } = default!;
}

View File

@@ -20,7 +20,6 @@ internal class AnnouncementProvider
/// 构造一个新的公告提供器
/// </summary>
/// <param name="requester">请求器</param>
/// <param name="gZipRequester">GZip 请求器</param>
public AnnouncementProvider(Requester requester)
{
this.requester = requester;
@@ -31,12 +30,13 @@ internal class AnnouncementProvider
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>公告列表</returns>
public async Task<AnnouncementWrapper?> GetAnnouncementWrapperAsync(CancellationToken cancellationToken = default)
public async Task<AnnouncementWrapper?> GetAnnouncementsAsync(CancellationToken cancellationToken = default)
{
Response<AnnouncementWrapper>? resp = await requester
.Reset()
.GetAsync<AnnouncementWrapper>(ApiEndpoints.AnnList, cancellationToken)
.ConfigureAwait(false);
return resp?.Data;
}
@@ -51,9 +51,10 @@ internal class AnnouncementProvider
Response<ListWrapper<AnnouncementContent>>? resp = await requester
.Reset()
.AddHeader("Accept", RequestOptions.Json)
.AddHeader(RequestHeaders.Accept, RequestOptions.Json)
.GetAsync<ListWrapper<AnnouncementContent>>(ApiEndpoints.AnnContent, cancellationToken)
.ConfigureAwait(false);
return resp?.Data?.List ?? new();
}
}

View File

@@ -20,11 +20,11 @@ public class AnnouncementType
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
public string Name { get; set; } = default!;
/// <summary>
/// 国际化名称
/// </summary>
[JsonPropertyName("mi18n_name")]
public string? MI18NName { get; set; }
public string MI18NName { get; set; } = default!;
}

View File

@@ -22,7 +22,7 @@ public class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
/// 类型列表
/// </summary>
[JsonPropertyName("type_list")]
public List<AnnouncementType>? TypeList { get; set; }
public List<AnnouncementType> TypeList { get; set; } = default!;
/// <summary>
/// 提醒
@@ -46,5 +46,5 @@ public class AnnouncementWrapper : ListWrapper<AnnouncementListWrapper>
/// 时间戳
/// </summary>
[JsonPropertyName("t")]
public string? TimeStamp { get; set; }
public string TimeStamp { get; set; } = default!;
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// 用户游戏角色
/// </summary>
public record UserGameRole
{
/// <summary>
/// hk4e_cn for Genshin Impact
/// </summary>
[JsonPropertyName("game_biz")]
public string GameBiz { get; set; } = default!;
/// <summary>
/// 服务器
/// </summary>
[JsonPropertyName("region")]
public string Region { get; set; } = default!;
/// <summary>
/// 游戏Uid
/// </summary>
[JsonPropertyName("game_uid")]
public string GameUid { get; set; } = default!;
/// <summary>
/// 昵称
/// </summary>
[JsonPropertyName("nickname")]
public string Nickname { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 是否选中
/// </summary>
[JsonPropertyName("is_chosen")]
public bool IsChosen { get; set; }
/// <summary>
/// 地区名称
/// </summary>
[JsonPropertyName("region_name")]
public string RegionName { get; set; } = default!;
/// <summary>
/// 是否为官服
/// </summary>
[JsonPropertyName("is_official")]
public string IsOfficial { get; set; } = default!;
/// <summary>
/// 转化为 <see cref="PlayerUid"/>
/// </summary>
/// <returns>一个等价的 <see cref="PlayerUid"/> 实例</returns>
public PlayerUid AsPlayerUid()
{
return new PlayerUid(GameUid, Region);
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Nickname} | {RegionName} | Lv.{Level}";
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// 用户游戏角色提供器
/// </summary>
[Injection(InjectAs.Transient)]
internal class UserGameRoleProvider
{
private readonly IUserService userService;
private readonly Requester requester;
/// <summary>
/// 构造一个新的用户游戏角色提供器
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="requester">请求器</param>
public UserGameRoleProvider(IUserService userService, Requester requester)
{
this.userService = userService;
this.requester = requester;
}
/// <summary>
/// 获取用户角色信息
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户角色信息</returns>
public async Task<List<UserGameRole>> GetUserGameRolesAsync(CancellationToken cancellationToken = default)
{
Response<ListWrapper<UserGameRole>>? resp = await requester
.Reset()
.SetAcceptJson()
.SetCommonUA()
.SetRequestWithHyperion()
.SetUser(userService.CurrentUser)
.GetAsync<ListWrapper<UserGameRole>>(ApiEndpoints.UserGameRoles, cancellationToken)
.ConfigureAwait(false);
return resp?.Data?.List ?? new();
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 角色的基础信息
/// </summary>
public class Avatar
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 图片Url
/// </summary>
[JsonPropertyName("image")]
public string Image { get; set; } = default!;
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 元素英文名称
/// </summary>
[JsonPropertyName("element")]
public string Element { get; set; } = default!;
/// <summary>
/// 好感度
/// </summary>
[JsonPropertyName("fetter")]
public int Fetter { get; set; }
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")]
public Rarity Rarity { get; set; }
/// <summary>
/// 激活的命座数
/// </summary>
[JsonPropertyName("actived_constellation_num")]
public int ActivedConstellationNum { get; set; }
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 角色详细详细
/// </summary>
public class Character : Avatar
{
/// <summary>
/// 角色图片
/// </summary>
[Obsolete("we don't want to use this ugly pic here")]
[JsonPropertyName("image")]
public new string Image { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 武器
/// </summary>
[JsonPropertyName("weapon")]
public Weapon Weapon { get; set; } = default!;
/// <summary>
/// 圣遗物
/// </summary>
[JsonPropertyName("reliquaries")]
public List<Reliquary> Reliquaries { get; set; } = default!;
/// <summary>
/// 命座
/// </summary>
[JsonPropertyName("constellations")]
public List<Constellation> Constellations { get; set; } = default!;
/// <summary>
/// 时装
/// </summary>
[JsonPropertyName("costumes")]
public List<Costume> Costumes { get; set; } = default!;
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 包装详细角色信息列表
/// </summary>
public class CharacterWrapper
{
/// <summary>
/// 角色列表
/// </summary>
[JsonPropertyName("avatars")]
public List<Character> Avatars { get; set; } = default!;
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 命座
/// </summary>
public class Constellation
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 效果描述
/// </summary>
[JsonPropertyName("effect")]
public string Effect { get; set; } = default!;
/// <summary>
/// 是否激活
/// </summary>
[JsonPropertyName("is_actived")]
public bool IsActived { get; set; }
/// <summary>
/// 位置
/// </summary>
[JsonPropertyName("pos")]
public int Position { get; set; }
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 角色装扮
/// </summary>
public class Costume
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[Obsolete("不应使用此处的图标")]
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 稀有度
/// </summary>
public enum Rarity
{
/// <summary>
/// 一星
/// </summary>
Gray = 1,
/// <summary>
/// 二星
/// </summary>
Green = 2,
/// <summary>
/// 三星
/// </summary>
Blue = 3,
/// <summary>
/// 四星
/// </summary>
Purple = 4,
/// <summary>
/// 五星
/// </summary>
Orange = 5,
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 圣遗物
/// </summary>
public class Reliquary
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 部位
/// </summary>
[JsonPropertyName("pos")]
public ReliquaryPosition Position { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")]
public Rarity Rarity { get; set; }
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 圣遗物套装
/// </summary>
[JsonPropertyName("set")]
public ReliquarySet ReliquarySet { get; set; } = default!;
/// <summary>
/// 部位名称
/// </summary>
[JsonPropertyName("pos_name")]
public string PositionName { get; set; } = default!;
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 圣遗物套装效果
/// </summary>
public class ReliquaryAffix
{
/// <summary>
/// 激活个数
/// </summary>
[JsonPropertyName("activation_number")]
public int ActivationNumber { get; set; }
/// <summary>
/// 效果描述
/// </summary>
[JsonPropertyName("effect")]
public string? Effect { get; set; }
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 圣遗物部位
/// </summary>
public enum ReliquaryPosition
{
/// <summary>
/// 生之花
/// </summary>
FlowerOfLife = 1,
/// <summary>
/// 死之羽
/// </summary>
PlumeOfDeath = 2,
/// <summary>
/// 时之沙
/// </summary>
SandOfEon = 3,
/// <summary>
/// 空之杯
/// </summary>
GobletOfEonothem = 4,
/// <summary>
/// 理之冠
/// </summary>
CircletOfLogos = 5,
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 圣遗物套装信息
/// </summary>
public class ReliquarySet
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 套装名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 套装效果
/// </summary>
[JsonPropertyName("affixes")]
public List<ReliquaryAffix> Affixes { get; set; } = default!;
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 武器信息
/// </summary>
public class Weapon
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")] public int Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")] public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")] public string Icon { get; set; } = default!;
/// <summary>
/// 类型
/// </summary>
[JsonPropertyName("type")] public WeaponType Type { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")] public Rarity Rarity { get; set; }
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")] public int Level { get; set; }
/// <summary>
/// 突破等级
/// </summary>
[JsonPropertyName("promote_level")] public int PromoteLevel { get; set; }
/// <summary>
/// 类型名称
/// </summary>
[JsonPropertyName("type_name")] public string TypeName { get; set; } = default!;
/// <summary>
/// 武器介绍
/// </summary>
[JsonPropertyName("desc")] public string Description { get; set; } = default!;
/// <summary>
/// 精炼等级
/// </summary>
[JsonPropertyName("affix_level")] public int AffixLevel { get; set; }
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 武器类型
/// </summary>
public enum WeaponType
{
/// <summary>
/// 单手剑
/// </summary>
Sword = 1,
/// <summary>
/// 法器
/// </summary>
Catalyst = 10,
/// <summary>
/// 双手剑
/// </summary>
Claymore = 11,
/// <summary>
/// 弓
/// </summary>
Bow = 12,
/// <summary>
/// 长柄武器
/// </summary>
Polearm = 13,
}

View File

@@ -0,0 +1,118 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 游戏记录提供器
/// </summary>
[Injection(InjectAs.Transient)]
internal class GameRecordProvider
{
private readonly IUserService userService;
private readonly Requester requester;
/// <summary>
/// 构造一个新的游戏记录提供器
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="requester">请求器</param>
public GameRecordProvider(IUserService userService, Requester requester)
{
this.userService = userService;
this.requester = requester;
}
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
public async Task<PlayerInfo?> GetPlayerInfoAsync(PlayerUid uid, CancellationToken token)
{
string url = string.Format(ApiEndpoints.GameRecordIndex, uid.Value, uid.Region);
Response<PlayerInfo>? resp = await PrepareRequester()
.GetUsingDS2Async<PlayerInfo>(url, token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
public async Task<SpiralAbyss.SpiralAbyss?> GetSpiralAbyssAsync(PlayerUid uid, SpiralAbyssSchedule schedule, CancellationToken token = default)
{
string url = string.Format(ApiEndpoints.SpiralAbyss, (int)schedule, uid.Value, uid.Region);
Response<SpiralAbyss.SpiralAbyss>? resp = await PrepareRequester()
.GetUsingDS2Async<SpiralAbyss.SpiralAbyss>(url, token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="uid">uid</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>角色列表</returns>
public async Task<List<Character>> GetCharactersAsync(PlayerUid uid, PlayerInfo playerInfo, CancellationToken cancellationToken = default)
{
CharacterData data = new(uid, playerInfo.Avatars.Select(x => x.Id));
Response<CharacterWrapper>? resp = await PrepareRequester()
.PostUsingDS2Async<CharacterWrapper>(ApiEndpoints.Character, data, cancellationToken)
.ConfigureAwait(false);
return resp?.Data?.Avatars ?? new();
}
private Requester PrepareRequester()
{
return requester
.Reset()
.SetAcceptJson()
.AddHeader(RequestHeaders.AppVersion, DynamicSecretProvider2.AppVersion)
.SetCommonUA()
.AddHeader(RequestHeaders.ClientType, RequestOptions.DefaultClientType)
.SetRequestWithHyperion()
.SetUser(userService.CurrentUser);
}
private class CharacterData
{
public CharacterData(PlayerUid uid, IEnumerable<int> characterIds)
{
CharacterIds = characterIds;
Uid = uid.Value;
Server = uid.Region;
}
[JsonPropertyName("character_ids")]
public IEnumerable<int> CharacterIds { get; }
[JsonPropertyName("role_id")]
public string Uid { get; }
[JsonPropertyName("server")]
public string Server { get; }
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 家园信息
/// </summary>
public class Home
{
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 历史访客数
/// </summary>
[JsonPropertyName("visit_num")]
public int VisitNum { get; set; }
/// <summary>
/// 最高洞天仙力
/// </summary>
[JsonPropertyName("comfort_num")]
public int ComfortNum { get; set; }
/// <summary>
/// 获得摆设数
/// </summary>
[JsonPropertyName("item_num")]
public int ItemNum { get; set; }
/// <summary>
/// 洞天名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 洞天等级名称
/// </summary>
[JsonPropertyName("comfort_level_name")]
public string ComfortLevelName { get; set; } = default!;
/// <summary>
/// 洞天等级图标
/// </summary>
[JsonPropertyName("comfort_level_icon")]
public string ComfortLevelIcon { get; set; } = default!;
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 供奉信息
/// </summary>
public class Offering
{
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public string Level { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Request;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 玩家信息
/// </summary>
public class PlayerInfo
{
/// <summary>
/// 持有的角色的信息
/// </summary>
[JsonPropertyName("avatars")]
public List<Avatar.Avatar> Avatars { get; set; } = default!;
/// <summary>
/// 玩家的基本信息
/// </summary>
[JsonPropertyName("stats")]
public PlayerStats PlayerStat { get; set; } = default!;
/// <summary>
/// 世界探索
/// </summary>
[JsonPropertyName("world_explorations")]
public List<WorldExploration> WorldExplorations { get; set; } = default!;
/// <summary>
/// 洞天
/// </summary>
[JsonPropertyName("homes")]
public List<Home> Homes { get; set; } = default!;
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 玩家统计数据
/// </summary>
public class PlayerStats
{
/// <summary>
/// 活跃天数
/// </summary>
[JsonPropertyName("active_day_number")]
public int ActiveDayNumber { get; set; }
/// <summary>
/// 成就完成数
/// </summary>
[JsonPropertyName("achievement_number")]
public int AchievementNumber { get; set; }
/// <summary>
/// 风神瞳个数
/// </summary>
[JsonPropertyName("anemoculus_number")]
public int AnemoculusNumber { get; set; }
/// <summary>
/// 岩神瞳个数
/// </summary>
[JsonPropertyName("geoculus_number")]
public int GeoculusNumber { get; set; }
/// <summary>
/// 雷神瞳个数
/// </summary>
[JsonPropertyName("electroculus_number")]
public int ElectroculusNumber { get; set; }
/// <summary>
/// 角色个数
/// </summary>
[JsonPropertyName("avatar_number")]
public int AvatarNumber { get; set; }
/// <summary>
/// 传送锚点个数
/// </summary>
[JsonPropertyName("way_point_number")]
public int WayPointNumber { get; set; }
/// <summary>
/// 秘境个数
/// </summary>
[JsonPropertyName("domain_number")]
public int DomainNumber { get; set; }
/// <summary>
/// 深渊层数
/// </summary>
[JsonPropertyName("spiral_abyss")]
public string SpiralAbyss { get; set; } = default!;
/// <summary>
/// 华丽宝箱个数
/// </summary>
[JsonPropertyName("luxurious_chest_number")]
public int LuxuriousChestNumber { get; set; }
/// <summary>
/// 珍贵宝箱个数
/// </summary>
[JsonPropertyName("precious_chest_number")]
public int PreciousChestNumber { get; set; }
/// <summary>
/// 精致宝箱个数
/// </summary>
[JsonPropertyName("exquisite_chest_number")]
public int ExquisiteChestNumber { get; set; }
/// <summary>
/// 普通宝箱个数
/// </summary>
[JsonPropertyName("common_chest_number")]
public int CommonChestNumber { get; set; }
/// <summary>
/// 奇馈宝箱
/// </summary>
[JsonPropertyName("magic_chest_number")]
public int MagicChestNumber { get; set; }
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 仅包含头像的角色信息
/// </summary>
public class Avatar
{
/// <summary>
/// Id
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")]
public int Rarity { get; set; }
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 表示一次战斗
/// </summary>
public class Battle
{
/// <summary>
/// 索引
/// </summary>
[JsonPropertyName("index")]
public int Index { get; set; }
/// <summary>
/// 时间戳
/// </summary>
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = default!;
/// <summary>
/// 参战角色
/// </summary>
[JsonPropertyName("avatars")]
public List<Avatar> Avatars { get; set; } = default!;
/// <summary>
/// 时间
/// </summary>
public DateTime Time
{
get
{
return DateTimeOffset.FromUnixTimeSeconds(int.Parse(Timestamp)).LocalDateTime;
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 层
/// </summary>
public class Floor
{
/// <summary>
/// 层号
/// </summary>
[JsonPropertyName("index")]
public int Index { get; set; }
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 是否解锁
/// </summary>
[JsonPropertyName("is_unlock")]
public string IsUnlock { get; set; } = default!;
/// <summary>
/// 结束时间
/// </summary>
[JsonPropertyName("settle_time")]
public string SettleTime { get; set; } = default!;
/// <summary>
/// 星数
/// </summary>
[JsonPropertyName("star")]
public int Star { get; set; }
/// <summary>
/// 最大星数
/// </summary>
[JsonPropertyName("max_star")]
public int MaxStar { get; set; }
/// <summary>
/// 层信息
/// </summary>
[JsonPropertyName("levels")]
public List<Level> Levels { get; set; } = default!;
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 间
/// </summary>
public class Level
{
/// <summary>
/// 索引
/// </summary>
[JsonPropertyName("index")]
public int Index { get; set; }
/// <summary>
/// 星数
/// </summary>
[JsonPropertyName("star")]
public int Star { get; set; }
/// <summary>
/// 最大星数
/// </summary>
[JsonPropertyName("max_star")]
public int MaxStar { get; set; }
/// <summary>
/// 上下半
/// </summary>
[JsonPropertyName("battles")]
public List<Battle> Battles { get; set; } = default!;
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 角色数值排行信息
/// </summary>
public class Rank
{
/// <summary>
/// 角色Id
/// </summary>
[JsonPropertyName("avatar_id")]
public int AvatarId { get; set; }
/// <summary>
/// 角色图标
/// </summary>
[JsonPropertyName("avatar_icon")]
public string AvatarIcon { get; set; } = default!;
/// <summary>
/// 值
/// </summary>
[JsonPropertyName("value")]
public int Value { get; set; }
/// <summary>
/// 稀有度
/// </summary>
[JsonPropertyName("rarity")]
public int Rarity { get; set; }
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
/// <summary>
/// 深境螺旋信息
/// </summary>
public class SpiralAbyss
{
/// <summary>
/// 计划Id
/// </summary>
[JsonPropertyName("schedule_id")]
public int ScheduleId { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[JsonPropertyName("start_time")]
public string StartTime { get; set; } = default!;
/// <summary>
/// 结束时间
/// </summary>
[JsonPropertyName("end_time")]
public string EndTime { get; set; } = default!;
/// <summary>
/// 战斗次数
/// </summary>
[JsonPropertyName("total_battle_times")]
public int TotalBattleTimes { get; set; }
/// <summary>
/// 胜利次数
/// </summary>
[JsonPropertyName("total_win_times")]
public int TotalWinTimes { get; set; }
/// <summary>
/// 最深抵达
/// </summary>
[JsonPropertyName("max_floor")]
public string MaxFloor { get; set; } = default!;
/// <summary>
/// 出战次数
/// </summary>
[JsonPropertyName("reveal_rank")]
public List<Rank> RevealRank { get; set; } = default!;
/// <summary>
/// 击破次数
/// </summary>
[JsonPropertyName("defeat_rank")]
public List<Rank> DefeatRank { get; set; } = default!;
/// <summary>
/// 最强一击
/// </summary>
[JsonPropertyName("damage_rank")]
public List<Rank> DamageRank { get; set; } = default!;
/// <summary>
/// 承受伤害
/// </summary>
[JsonPropertyName("take_damage_rank")]
public List<Rank> TakeDamageRank { get; set; } = default!;
/// <summary>
/// 元素战技
/// </summary>
[JsonPropertyName("normal_skill_rank")]
public List<Rank> NormalSkillRank { get; set; } = default!;
/// <summary>
/// 元素爆发
/// </summary>
[JsonPropertyName("energy_skill_rank")]
public List<Rank> EnergySkillRank { get; set; } = default!;
/// <summary>
/// 层信息
/// </summary>
[JsonPropertyName("floors")]
public List<Floor> Floors { get; set; } = default!;
/// <summary>
/// 共获得渊星
/// </summary>
[JsonPropertyName("total_star")]
public int TotalStar { get; set; }
/// <summary>
/// 是否解锁
/// </summary>
[JsonPropertyName("is_unlock")]
public bool IsUnlock { get; set; }
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 深渊期数类型
/// </summary>
public enum SpiralAbyssSchedule
{
/// <summary>
/// 当期
/// </summary>
Current = 1,
/// <summary>
/// 上期
/// </summary>
Last = 2,
}

View File

@@ -0,0 +1,118 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 世界探索
/// </summary>
public class WorldExploration
{
/// <summary>
/// 等级
/// </summary>
[JsonPropertyName("level")]
public int Level { get; set; }
/// <summary>
/// 探索度
/// Maxmium is 1000
/// </summary>
[JsonPropertyName("exploration_percentage")]
public int ExplorationPercentage { get; set; }
/// <summary>
/// 图标
/// </summary>
[JsonPropertyName("icon")]
public string Icon { get; set; } = default!;
/// <summary>
/// 名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 类型
/// Offering
/// Reputation
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = default!;
/// <summary>
/// 供奉进度
/// </summary>
[JsonPropertyName("offerings")]
public List<Offering> Offerings { get; set; } = default!;
/// <summary>
/// ID
/// </summary>
[JsonPropertyName("id")]
public int Id { get; set; }
/// <summary>
/// 父ID 当无链接的父对象时为0
/// </summary>
[JsonPropertyName("parent_id")]
public int ParentId { get; set; }
/// <summary>
/// 地图链接
/// </summary>
[JsonPropertyName("map_url")]
public string MapUrl { get; set; } = default!;
/// <summary>
/// 攻略链接 无攻略时为 <see cref="string.Empty"/>
/// </summary>
[JsonPropertyName("strategy_url")]
public string StrategyUrl { get; set; } = default!;
/// <summary>
/// 背景图片
/// </summary>
[JsonPropertyName("background_image")]
public string BackgroundImage { get; set; } = default!;
/// <summary>
/// 反色图标
/// </summary>
[JsonPropertyName("inner_icon")]
public string InnerIcon { get; set; } = default!;
/// <summary>
/// 背景图片
/// </summary>
[JsonPropertyName("cover")]
public string Cover { get; set; } = default!;
/// <summary>
/// 百分比*100进度
/// </summary>
public double ExplorationPercentageBy10
{
get => ExplorationPercentage / 10.0;
}
/// <summary>
/// 类型名称转换器
/// </summary>
public string TypeFormatted
{
get => IsReputation ? "声望等级" : "供奉等级";
}
/// <summary>
/// 指示当前是否为声望
/// </summary>
public bool IsReputation
{
get => Type == "Reputation";
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi;
/// <summary>
/// 玩家 Uid
/// </summary>
public struct PlayerUid
{
private string? region = null;
/// <summary>
/// 构造一个新的玩家 Uid 结构
/// </summary>
/// <param name="value">uid</param>
/// <param name="region">服务器,当提供该参数时会无条件信任</param>
public PlayerUid(string value, string? region = default)
{
Requires.Argument(value.Length == 9, nameof(value), "uid应为9位数字");
Value = value;
if (region != null)
{
this.region = region;
}
}
/// <summary>
/// UID 的实际值
/// </summary>
public string Value { get; }
/// <summary>
/// 地区代码
/// </summary>
public string Region
{
get
{
region ??= EvaluateRegion(Value[0]);
return region;
}
}
private static string EvaluateRegion(char first)
{
return first switch
{
>= '1' and <= '4' => "cn_gf01", // 国服
'5' => "cn_qd01", // 渠道
'6' => "os_usa", // 美服
'7' => "os_euro", // 欧服
'8' => "os_asia", // 亚服
'9' => "os_cht", // 台服
_ => Must.NeverHappen<string>(),
};
}
}

View File

@@ -0,0 +1,341 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
using Snap.Hutao.Web.Hutao.Model;
using Snap.Hutao.Web.Hutao.Model.Post;
using Snap.Hutao.Web.Request;
using Snap.Hutao.Web.Response;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Web.Hutao;
/// <summary>
/// 胡桃API提供器
/// </summary>
[Injection(InjectAs.Transient)]
internal class HutaoAPIProvider : IAsyncInitializable
{
private const string AuthAPIHost = "https://auth.snapgenshin.com";
private const string HutaoAPI = "https://hutao-api.snapgenshin.com";
private const string PostContentType = "text/json";
private readonly Requester requester;
private readonly AuthRequester authRequester;
private readonly GameRecordProvider gameRecordProvider;
private readonly UserGameRoleProvider userGameRoleProvider;
private bool isInitialized = false;
/// <summary>
/// 构造一个新的胡桃API提供器
/// </summary>
/// <param name="requester">请求器</param>
/// <param name="authRequester">支持验证的请求器</param>
/// <param name="gameRecordProvider">游戏记录提供器</param>
/// <param name="userGameRoleProvider">用户游戏角色提供器</param>
public HutaoAPIProvider(
Requester requester,
AuthRequester authRequester,
GameRecordProvider gameRecordProvider,
UserGameRoleProvider userGameRoleProvider)
{
this.requester = requester;
this.authRequester = authRequester;
this.gameRecordProvider = gameRecordProvider;
this.userGameRoleProvider = userGameRoleProvider;
}
/// <inheritdoc/>
public bool IsInitialized { get => isInitialized; }
/// <inheritdoc/>
public async Task<bool> InitializeAsync(CancellationToken token = default)
{
Auth auth = new(
"08d9e212-0cb3-4d71-8ed7-003606da7b20",
"7ueWgZGn53dDhrm8L5ZRw+YWfOeSWtgQmJWquRgaygw=");
Response<Token>? resp = await requester
.Reset()
.PostAsync<Token>($"{AuthAPIHost}/Auth/Login", auth, PostContentType, token)
.ConfigureAwait(false);
authRequester.AuthToken = Must.NotNull(resp?.Data?.AccessToken!);
isInitialized = true;
return isInitialized;
}
/// <summary>
/// 检查对应的uid当前是否上传了数据
/// </summary>
/// <param name="uid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>当前是否上传了数据</returns>
public async Task<bool> CheckPeriodRecordUploadedAsync(string uid, CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<UploadStatus>? resp = await authRequester
.GetAsync<UploadStatus>($"{HutaoAPI}/Record/CheckRecord/{uid}", token)
.ConfigureAwait(false);
return resp?.Data is not null && resp.Data.PeriodUploaded;
}
/// <summary>
/// 异步获取总览数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>总览信息</returns>
public async Task<Overview?> GetOverviewAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<Overview>? resp = await authRequester
.GetAsync<Overview>($"{HutaoAPI}/Statistics/Overview", token)
.ConfigureAwait(false);
return resp?.Data;
}
/// <summary>
/// 异步获取角色出场率
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色出场率</returns>
public async Task<IEnumerable<AvatarParticipation>> GetAvatarParticipationsAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<AvatarParticipation>>? resp = await authRequester
.GetAsync<IEnumerable<AvatarParticipation>>($"{HutaoAPI}/Statistics/AvatarParticipation", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<AvatarParticipation>();
}
/// <summary>
/// 异步获取角色圣遗物搭配
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色圣遗物搭配</returns>
public async Task<IEnumerable<AvatarReliquaryUsage>> GetAvatarReliquaryUsagesAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<AvatarReliquaryUsage>>? resp = await authRequester
.GetAsync<IEnumerable<AvatarReliquaryUsage>>($"{HutaoAPI}/Statistics/AvatarReliquaryUsage", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<AvatarReliquaryUsage>();
}
/// <summary>
/// 异步获取角色搭配数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色搭配数据</returns>
public async Task<IEnumerable<TeamCollocation>> GetTeamCollocationsAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<TeamCollocation>>? resp = await authRequester
.GetAsync<IEnumerable<TeamCollocation>>($"{HutaoAPI}/Statistics/TeamCollocation", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<TeamCollocation>();
}
/// <summary>
/// 异步获取角色武器搭配数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色武器搭配数据</returns>
public async Task<IEnumerable<AvatarWeaponUsage>> GetAvatarWeaponUsagesAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<AvatarWeaponUsage>>? resp = await authRequester
.GetAsync<IEnumerable<AvatarWeaponUsage>>($"{HutaoAPI}/Statistics/AvatarWeaponUsage", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<AvatarWeaponUsage>();
}
/// <summary>
/// 异步获取角色命座信息
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
public async Task<IEnumerable<AvatarConstellationNum>> GetAvatarConstellationsAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<AvatarConstellationNum>>? resp = await authRequester
.GetAsync<IEnumerable<AvatarConstellationNum>>($"{HutaoAPI}/Statistics/Constellation", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<AvatarConstellationNum>();
}
/// <summary>
/// 异步获取队伍出场次数
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>队伍出场列表</returns>
public async Task<IEnumerable<TeamCombination>> GetTeamCombinationsAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<TeamCombination>>? resp = await authRequester
.GetAsync<IEnumerable<TeamCombination>>($"{HutaoAPI}/Statistics/TeamCombination", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<TeamCombination>();
}
/// <summary>
/// 异步获取角色图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>角色图片列表</returns>
public async Task<IEnumerable<Item>> GetAvatarMapAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<Item>>? resp = await authRequester
.GetAsync<IEnumerable<Item>>($"{HutaoAPI}/GenshinItem/Avatars", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<Item>();
}
/// <summary>
/// 异步获取武器图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>武器图片列表</returns>
public async Task<IEnumerable<Item>> GetWeaponMapAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<Item>>? resp = await authRequester
.GetAsync<IEnumerable<Item>>($"{HutaoAPI}/GenshinItem/Weapons", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<Item>();
}
/// <summary>
/// 异步获取圣遗物图片列表
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>圣遗物图片列表</returns>
public async Task<IEnumerable<Item>> GetReliquaryMapAsync(CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
Response<IEnumerable<Item>>? resp = await authRequester
.GetAsync<IEnumerable<Item>>($"{HutaoAPI}/GenshinItem/Reliquaries", token)
.ConfigureAwait(false);
return resp?.Data ?? Enumerable.Empty<Item>();
}
/// <summary>
/// 异步获取所有记录并上传到数据库
/// </summary>
/// <param name="confirmAsyncFunc">异步确认委托</param>
/// <param name="resultAsyncFunc">结果确认委托</param>
/// <param name="token">取消令牌</param>
/// <returns>任务</returns>
public async Task GetAllRecordsAndUploadAsync(Func<PlayerRecord, Task<bool>> confirmAsyncFunc, Func<Response.Response, Task> resultAsyncFunc, CancellationToken token = default)
{
List<UserGameRole> userGameRoles = await userGameRoleProvider
.GetUserGameRolesAsync(token);
foreach (UserGameRole role in userGameRoles)
{
PlayerInfo? playerInfo = await gameRecordProvider
.GetPlayerInfoAsync(role.AsPlayerUid(), token);
Must.NotNull(playerInfo!);
List<Character> characters = await gameRecordProvider
.GetCharactersAsync(role.AsPlayerUid(), playerInfo, token);
SpiralAbyss? spiralAbyssInfo = await gameRecordProvider
.GetSpiralAbyssAsync(role.AsPlayerUid(), SpiralAbyssSchedule.Current, token);
Must.NotNull(spiralAbyssInfo!);
PlayerRecord playerRecord = PlayerRecord.Create(role.GameUid, characters, spiralAbyssInfo);
if (await confirmAsyncFunc(playerRecord))
{
Response<string>? resp = null;
if (Response.Response.IsOk(await UploadItemsAsync(characters, token)))
{
resp = await authRequester
.PostAsync<string>($"{HutaoAPI}/Record/Upload", playerRecord, PostContentType, token);
}
await resultAsyncFunc(resp ?? Response.Response.CreateForException($"{role.GameUid}-记录提交失败。"));
}
}
}
/// <summary>
/// 异步上传物品所有物品
/// </summary>
/// <param name="characters">角色详细信息</param>
/// <param name="token">取消令牌</param>
/// <returns>响应</returns>
private async Task<Response<string>?> UploadItemsAsync(List<Character> characters, CancellationToken token = default)
{
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
IEnumerable<Item> avatars = characters
.Select(avatar => new Item(avatar.Id, avatar.Name, avatar.Icon))
.DistinctBy(item => item.Id);
IEnumerable<Item> weapons = characters
.Select(avatar => avatar.Weapon)
.Select(weapon => new Item(weapon.Id, weapon.Name, weapon.Icon))
.DistinctBy(item => item.Id);
IEnumerable<Item> reliquaries = characters
.Select(avatars => avatars.Reliquaries)
.Flatten()
.Where(relic => relic.Position == ReliquaryPosition.FlowerOfLife)
.DistinctBy(relic => relic.Id)
.Select(relic => new Item(relic.ReliquarySet.Id, relic.ReliquarySet.Name, relic.Icon));
GenshinItemWrapper? data = new(avatars, weapons, reliquaries);
return await authRequester
.PostAsync<string>($"{HutaoAPI}/GenshinItem/Upload", data, PostContentType, token)
.ConfigureAwait(false);
}
private class Auth
{
public Auth(string appid, string secret)
{
Appid = appid;
Secret = secret;
}
public string Appid { get; }
public string Secret { get; }
}
private class Token
{
public string AccessToken { get; } = default!;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 命座比例
/// </summary>
public class AvatarConstellationNum
{
/// <summary>
/// 角色ID
/// </summary>
public int Avatar { get; set; }
/// <summary>
/// 持有率
/// </summary>
public decimal HoldingRate { get; set; }
/// <summary>
/// 各命座比率
/// </summary>
public IEnumerable<Rate<int>> Rate { get; set; } = default!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 出场数据
/// </summary>
public class AvatarParticipation
{
/// <summary>
/// 层
/// </summary>
public int Floor { get; set; }
/// <summary>
/// 角色比率
/// </summary>
public IEnumerable<Rate<int>> AvatarUsage { get; set; } = default!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 圣遗物配置数据
/// </summary>
public class AvatarReliquaryUsage
{
/// <summary>
/// 角色Id
/// </summary>
public int Avatar { get; set; }
/// <summary>
/// 圣遗物比率
/// </summary>
public IEnumerable<Rate<ReliquarySets>> ReliquaryUsage { get; set; } = default!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 武器使用数据
/// </summary>
public class AvatarWeaponUsage
{
/// <summary>
/// 角色Id
/// </summary>
public int Avatar { get; set; }
/// <summary>
/// 武器比率
/// </summary>
public IEnumerable<Rate<int>> Weapons { get; set; } = default!;
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Snap.Hutao.Web.Hutao.Model.Converter;
/// <summary>
/// 圣遗物套装转换器
/// </summary>
internal class ReliquarySetsConverter : JsonConverter<ReliquarySets>
{
/// <inheritdoc/>
public override ReliquarySets? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? source = reader.GetString();
if (source != null)
{
string[] sets = source.Split(';');
return new(sets.Select(set => new ReliquarySet(set)));
}
else
{
return null;
}
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, ReliquarySets value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(';', value));
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 胡桃数据库物品
/// </summary>
public class Item
{
/// <summary>
/// 构造一个新的胡桃数据库物品
/// </summary>
/// <param name="id">物品Id</param>
/// <param name="name">名称</param>
/// <param name="url">链接</param>
[JsonConstructor]
public Item(int id, string name, string url)
{
Id = id;
Name = name;
Url = url;
}
/// <summary>
/// 物品Id
/// </summary>
public int Id { get; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// 链接
/// </summary>
public string Url { get; }
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 层
/// </summary>
public class LevelInfo
{
/// <summary>
/// 间
/// </summary>
public int Floor { get; set; }
/// <summary>
/// 上下半 0,1
/// </summary>
public int Index { get; set; }
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 统计数据
/// </summary>
public class Overview
{
/// <summary>
/// 所有用户数量
/// </summary>
public int TotalPlayerCount { get; set; }
/// <summary>
/// 当期提交深渊数据用户数量
/// </summary>
public int CollectedPlayerCount { get; set; }
/// <summary>
/// 当期满星用户数量
/// </summary>
public int FullStarPlayerCount { get; set; }
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 角色圣遗物套装
/// </summary>
public class AvatarReliquarySet
{
/// <summary>
/// 构造一个新的角色圣遗物套装
/// </summary>
/// <param name="id">套装Id</param>
/// <param name="count">个数</param>
public AvatarReliquarySet(int id, int count)
{
Id = id;
Count = count;
}
/// <summary>
/// 构造一个新的角色圣遗物套装
/// </summary>
/// <param name="kvp">键值对</param>
public AvatarReliquarySet(KeyValuePair<int, int> kvp)
: this(kvp.Key, kvp.Value)
{
}
/// <summary>
/// 套装Id
/// </summary>
public int Id { get; }
/// <summary>
/// 个数
/// </summary>
public int Count { get; }
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 角色武器
/// </summary>
public class AvatarWeapon
{
/// <summary>
/// 构造一个新的角色武器
/// </summary>
/// <param name="id">武器Id</param>
/// <param name="level">武器等级</param>
/// <param name="affixLevel">精炼等级</param>
public AvatarWeapon(int id, int level, int affixLevel)
{
Id = id;
Level = level;
AffixLevel = affixLevel;
}
/// <summary>
/// 武器等级
/// </summary>
public int Id { get; }
/// <summary>
/// 武器等级
/// </summary>
public int Level { get; }
/// <summary>
/// 精炼
/// </summary>
public int AffixLevel { get; }
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 原神物品包装器
/// </summary>
public class GenshinItemWrapper
{
/// <summary>
/// 构造一个新的原神物品包装器
/// </summary>
/// <param name="avatars">角色</param>
/// <param name="weapons">武器</param>
/// <param name="reliquaries">圣遗物</param>
public GenshinItemWrapper(IEnumerable<Item> avatars, IEnumerable<Item> weapons, IEnumerable<Item> reliquaries)
{
Avatars = avatars;
Weapons = weapons;
Reliquaries = reliquaries;
}
/// <summary>
/// 角色列表
/// </summary>
public IEnumerable<Item> Avatars { get; }
/// <summary>
/// 武器列表
/// </summary>
public IEnumerable<Item> Weapons { get; }
/// <summary>
/// 圣遗物列表
/// </summary>
public IEnumerable<Item> Reliquaries { get; }
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 带有层信息的间
/// </summary>
internal class IndexedLevel
{
/// <summary>
/// 构造一个新的带有层信息的间
/// </summary>
/// <param name="floorIndex">层号</param>
/// <param name="level">间信息</param>
public IndexedLevel(int floorIndex, Level level)
{
FloorIndex = floorIndex;
Level = level;
}
/// <summary>
/// 层号
/// </summary>
public int FloorIndex { get; }
/// <summary>
/// 层信息
/// </summary>
public Level Level { get; }
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 玩家角色
/// </summary>
public class PlayerAvatar
{
/// <summary>
/// 构造一个新的玩家角色
/// </summary>
/// <param name="avatar">角色</param>
internal PlayerAvatar(Character avatar)
{
Id = avatar.Id;
Level = avatar.Level;
ActivedConstellationNum = avatar.ActivedConstellationNum;
Weapon = new(avatar.Weapon.Id, avatar.Weapon.Level, avatar.Weapon.AffixLevel);
ReliquarySets = avatar.Reliquaries
.CountBy(relic => relic.ReliquarySet.Id)
.Select(kvp => new AvatarReliquarySet(kvp))
.ToList();
}
/// <summary>
/// 角色Id
/// </summary>
public int Id { get; }
/// <summary>
/// 角色等级
/// </summary>
public int Level { get; }
/// <summary>
/// 命座
/// </summary>
public int ActivedConstellationNum { get; }
/// <summary>
/// 武器
/// </summary>
public AvatarWeapon Weapon { get; }
/// <summary>
/// 圣遗物套装
/// </summary>
public List<AvatarReliquarySet> ReliquarySets { get; }
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 玩家记录
/// 使用 <see cref="Create(string, List{Character}, SpiralAbyss)"/> 来构建一个实例
/// </summary>
public class PlayerRecord
{
/// <summary>
/// 构造一个新的玩家记录
/// </summary>
/// <param name="uid">uid</param>
/// <param name="playerAvatars">玩家角色</param>
/// <param name="playerSpiralAbyssesLevels">玩家深渊信息</param>
private PlayerRecord(string uid, IEnumerable<PlayerAvatar> playerAvatars, IEnumerable<PlayerSpiralAbyssLevel> playerSpiralAbyssesLevels)
{
Uid = uid;
PlayerAvatars = playerAvatars;
PlayerSpiralAbyssesLevels = playerSpiralAbyssesLevels;
}
/// <summary>
/// uid
/// </summary>
public string Uid { get; }
/// <summary>
/// 玩家角色
/// </summary>
public IEnumerable<PlayerAvatar> PlayerAvatars { get; }
/// <summary>
/// 玩家深渊信息
/// </summary>
public IEnumerable<PlayerSpiralAbyssLevel> PlayerSpiralAbyssesLevels { get; }
/// <summary>
/// 建造玩家记录
/// </summary>
/// <param name="uid">玩家的uid</param>
/// <param name="detailAvatars">角色详情信息</param>
/// <param name="spiralAbyss">深渊信息</param>
/// <returns>玩家记录</returns>
internal static PlayerRecord Create(string uid, List<Character> detailAvatars, SpiralAbyss spiralAbyss)
{
IEnumerable<PlayerAvatar> playerAvatars = detailAvatars
.Select(avatar => new PlayerAvatar(avatar));
IEnumerable<PlayerSpiralAbyssLevel> playerSpiralAbyssLevels = spiralAbyss.Floors
.SelectMany(f => f.Levels, (f, level) => new IndexedLevel(f.Index, level))
.Select(indexedLevel => new PlayerSpiralAbyssLevel(indexedLevel));
PlayerRecord playerRecord = new(uid, playerAvatars, playerSpiralAbyssLevels);
return playerRecord;
}
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 玩家深渊某间的战斗信息
/// </summary>
public class PlayerSpiralAbyssBattle
{
/// <summary>
/// 构造一个新的战斗信息
/// </summary>
/// <param name="battle">战斗</param>
internal PlayerSpiralAbyssBattle(Battle battle)
{
BattleIndex = battle.Index;
AvatarIds = battle.Avatars.Select(a => a.Id);
}
/// <summary>
/// 战斗上下半间 0,1
/// </summary>
public int BattleIndex { get; }
/// <summary>
/// 角色Id列表
/// </summary>
public IEnumerable<int> AvatarIds { get; }
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using System.Linq;
namespace Snap.Hutao.Web.Hutao.Model.Post;
/// <summary>
/// 玩家深渊战斗间信息
/// </summary>
public class PlayerSpiralAbyssLevel
{
/// <summary>
/// 构造一个新的玩家深渊战斗间信息
/// </summary>
/// <param name="indexedLevel">楼层</param>
internal PlayerSpiralAbyssLevel(IndexedLevel indexedLevel)
{
FloorIndex = indexedLevel.FloorIndex;
LevelIndex = indexedLevel.Level.Index;
Star = indexedLevel.Level.Star;
Battles = indexedLevel.Level.Battles
.Select(battle => new PlayerSpiralAbyssBattle(battle));
}
/// <summary>
/// 层号
/// </summary>
public int FloorIndex { get; }
/// <summary>
/// 间号
/// </summary>
public int LevelIndex { get; }
/// <summary>
/// 星数
/// </summary>
public int Star { get; }
/// <summary>
/// 战斗列表 分上下半间
/// </summary>
public IEnumerable<PlayerSpiralAbyssBattle> Battles { get; }
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 比率
/// </summary>
/// <typeparam name="T"><see cref="Id"/> 的类型</typeparam>
public class Rate<T>
{
/// <summary>
/// 表示唯一标识符的实例
/// </summary>
public T Id { get; set; } = default!;
/// <summary>
/// 比率
/// </summary>
public decimal Value { get; set; }
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 圣遗物套装
/// </summary>
public class ReliquarySet
{
/// <summary>
/// 构造一个新的圣遗物套装
/// </summary>
/// <param name="set">简单套装字符串</param>
public ReliquarySet(string set)
{
string[]? deconstructed = set.Split('-');
Id = int.Parse(deconstructed[0]);
Count = int.Parse(deconstructed[1]);
}
/// <summary>
/// Id
/// </summary>
public int Id { get; }
/// <summary>
/// 个数
/// </summary>
public int Count { get; }
/// <inheritdoc/>
public override string ToString()
{
return $"{Id}-{Count}";
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hutao.Model.Converter;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 包装圣遗物套装
/// </summary>
[JsonConverter(typeof(ReliquarySetsConverter))]
public class ReliquarySets : List<ReliquarySet>
{
/// <summary>
/// 构造一个新的圣遗物包装器
/// </summary>
/// <param name="sets">圣遗物套装</param>
public ReliquarySets(IEnumerable<ReliquarySet> sets)
: base(sets)
{
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Json.Converter;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 队伍
/// </summary>
public class Team
{
/// <summary>
/// 上半
/// </summary>
[JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))]
public IEnumerable<int> UpHalf { get; set; } = null!;
/// <summary>
/// 下半
/// </summary>
[JsonConverter(typeof(SeparatorCommaInt32EnumerableConverter))]
public IEnumerable<int> DownHalf { get; set; } = null!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 组队数据
/// </summary>
public class TeamCollocation
{
/// <summary>
/// 角色Id
/// </summary>
public int Avatar { get; set; }
/// <summary>
/// 角色搭配比率
/// </summary>
public IEnumerable<Rate<int>> Collocations { get; set; } = default!;
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 队伍上场率
/// </summary>
public record TeamCombination
{
/// <summary>
/// 层
/// </summary>
public LevelInfo Level { get; set; } = null!;
/// <summary>
/// 队伍
/// </summary>
public IEnumerable<Rate<Team>> Teams { get; set; } = null!;
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Model;
/// <summary>
/// 上传信息
/// </summary>
public class UploadStatus
{
/// <summary>
/// 本期是否已经上传
/// </summary>
public bool PeriodUploaded { get; set; }
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Request
{
/// <summary>
/// 请求头的键
/// </summary>
public class RequestHeaders
{
/// <summary>
/// Accept
/// </summary>
public const string Accept = "Accept";
/// <summary>
/// Cookie
/// </summary>
public const string Cookie = "Cookie";
/// <summary>
/// x-rpc-app_version
/// </summary>
public const string AppVersion = "x-rpc-app_version";
/// <summary>
/// User-Agent
/// </summary>
public const string UserAgent = "User-Agent";
/// <summary>
/// x-rpc-client_type
/// </summary>
public const string ClientType = "x-rpc-client_type";
/// <summary>
/// X-Requested-With
/// </summary>
public const string RequestWith = "X-Requested-With";
}
}

View File

@@ -8,22 +8,10 @@ namespace Snap.Hutao.Web.Request
/// </summary>
public class RequestOptions : Dictionary<string, string>
{
/// <summary>
/// 不再使用
/// </summary>
[Obsolete("不再使用")]
public const string CommonUA2101 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.10.1";
/// <summary>
/// 不再使用
/// </summary>
[Obsolete("不再使用")]
public const string CommonUA2111 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.11.1";
/// <summary>
/// 支持更新的DS2算法
/// </summary>
public const string CommonUA2161 = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.16.1";
public const string CommonUA = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/2.16.1";
/// <summary>
/// 应用程序/Json
@@ -35,6 +23,11 @@ namespace Snap.Hutao.Web.Request
/// </summary>
public const string Hyperion = "com.mihoyo.hyperion";
/// <summary>
/// 默认的客户端类型
/// </summary>
public const string DefaultClientType = "5";
/// <summary>
/// 设备Id
/// </summary>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Json;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -20,6 +21,8 @@ public class Requester
private readonly IInfoBarService infoBarService;
private readonly ILogger<Requester> logger;
private bool isRequesting = false;
/// <summary>
/// 构造一个新的 <see cref="Requester"/> 对象
/// </summary>
@@ -40,6 +43,11 @@ public class Requester
/// </summary>
public RequestOptions Headers { get; set; } = new RequestOptions();
/// <summary>
/// 此请求器使用的Json解析器
/// </summary>
public Json Json { get => json; }
/// <summary>
/// 内部使用的 <see cref="System.Net.Http.HttpClient"/>
/// </summary>
@@ -52,7 +60,7 @@ public class Requester
/// <param name="url">地址</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response<TResult>?> GetAsync<TResult>(string? url, CancellationToken cancellationToken = default)
public async Task<Response<TResult>?> GetAsync<TResult>(string url, CancellationToken cancellationToken = default)
{
if (url is null)
{
@@ -73,14 +81,14 @@ public class Requester
/// <param name="data">要发送的.NET匿名对象</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, CancellationToken cancellationToken = default)
public async Task<Response<TResult>?> PostAsync<TResult>(string url, object data, CancellationToken cancellationToken = default)
{
if (url is null)
{
return Response<TResult>.CreateForEmptyUrl();
}
string dataString = json.Stringify(data);
string dataString = Json.Stringify(data);
HttpContent content = new StringContent(dataString);
Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
@@ -98,14 +106,14 @@ public class Requester
/// <param name="contentType">内容类型</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>响应</returns>
public async Task<Response<TResult>?> PostAsync<TResult>(string? url, object data, string contentType, CancellationToken cancellationToken = default)
public async Task<Response<TResult>?> PostAsync<TResult>(string url, object data, string contentType, CancellationToken cancellationToken = default)
{
if (url is null)
{
return Response<TResult>.CreateForEmptyUrl();
}
string dataString = json.Stringify(data);
string dataString = Json.Stringify(data);
HttpContent content = new StringContent(dataString, Encoding.UTF8, contentType);
Task<HttpResponseMessage> PostMethod(HttpClient client, CancellationToken token) => client.PostAsync(url, content, token);
@@ -121,6 +129,7 @@ public class Requester
/// <returns>链式调用需要的实例</returns>
public Requester Reset()
{
Verify.Operation(!isRequesting, "无法在请求发生时重置请求器");
Headers.Clear();
return this;
}
@@ -133,10 +142,48 @@ public class Requester
/// <returns>链式调用需要的实例</returns>
public Requester AddHeader(string key, string value)
{
Verify.Operation(!isRequesting, "无法在请求发生时修改请求头");
Headers.Add(key, value);
return this;
}
/// <summary>
/// 接受Json
/// </summary>
/// <returns>链式调用需要的实例</returns>
public Requester SetAcceptJson()
{
return AddHeader(RequestHeaders.Accept, RequestOptions.Json);
}
/// <summary>
/// 设置常规UA
/// </summary>
/// <returns>链式调用需要的实例</returns>
public Requester SetCommonUA()
{
return AddHeader(RequestHeaders.UserAgent, RequestOptions.CommonUA);
}
/// <summary>
/// 设置为由米游社请求
/// </summary>
/// <returns>链式调用需要的实例</returns>
public Requester SetRequestWithHyperion()
{
return AddHeader(RequestHeaders.RequestWith, RequestOptions.Hyperion);
}
/// <summary>
/// 添加Cookie请求头
/// </summary>
/// <param name="user">用户</param>
/// <returns>链式调用需要的实例</returns>
public Requester SetUser(User user)
{
return AddHeader(RequestHeaders.Cookie, user.Cookie ?? string.Empty);
}
/// <summary>
/// 在请求前准备 <see cref="System.Net.Http.HttpClient"/>
/// </summary>
@@ -151,14 +198,15 @@ public class Requester
}
private async Task<Response<TResult>?> RequestAsync<TResult>(
Func<HttpClient, CancellationToken, Task<HttpResponseMessage>> requestFunc,
Func<HttpClient, CancellationToken, Task<HttpResponseMessage>> requestAsyncFunc,
CancellationToken cancellationToken = default)
{
isRequesting = true;
PrepareHttpClient();
try
{
HttpResponseMessage response = await requestFunc
HttpResponseMessage response = await requestAsyncFunc
.Invoke(HttpClient, cancellationToken)
.ConfigureAwait(false);
@@ -166,12 +214,15 @@ public class Requester
.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false);
Response<TResult>? resp = json.ToObject<Response<TResult>>(contentString);
if (resp?.ToString() is string representable)
Response<TResult>? resp = Json.ToObject<Response<TResult>>(contentString);
if (resp is null)
{
infoBarService.Information(representable);
return Response<TResult>.CreateForJsonException(contentString);
}
ValidateResponse(resp);
return resp;
}
catch (Exception ex)
@@ -181,7 +232,17 @@ public class Requester
}
finally
{
isRequesting = false;
logger.LogInformation("Request Completed");
}
}
private void ValidateResponse<TResult>(Response<TResult> resp)
{
// 未知的返回代码
if (!Enum.IsDefined(typeof(KnownReturnCode), resp.ReturnCode))
{
infoBarService.Information(resp.ToString());
}
}
}

View File

@@ -8,6 +8,11 @@ namespace Snap.Hutao.Web.Response;
/// </summary>
public enum KnownReturnCode
{
/// <summary>
/// Url为 空
/// </summary>
JsonParseIssue = -2000000002,
/// <summary>
/// Url为 空
/// </summary>

View File

@@ -16,5 +16,5 @@ public class ListWrapper<T>
/// 列表
/// </summary>
[JsonPropertyName("list")]
public List<T>? List { get; set; }
public List<T> List { get; set; } = default!;
}

View File

@@ -20,7 +20,7 @@ public class Response
/// 消息
/// </summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
public string Message { get; set; } = default!;
/// <summary>
/// 响应是否正常

View File

@@ -31,6 +31,20 @@ public class Response<TData> : Response
};
}
/// <summary>
/// 构造一个失败的响应
/// </summary>
/// <param name="message">消息</param>
/// <returns>响应</returns>
public static Response<TData> CreateForJsonException(string message)
{
return new Response<TData>()
{
ReturnCode = (int)KnownReturnCode.InternalFailure,
Message = message,
};
}
/// <summary>
/// 构造一个空Url的响应
/// </summary>