support ltoken requests #207

This commit is contained in:
DismissedLight
2022-11-18 21:39:45 +08:00
parent a8bc0cf9b2
commit efcf0620d7
35 changed files with 864 additions and 475 deletions

View File

@@ -102,8 +102,8 @@ dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_diagnostic.SA1629.severity = silent
dotnet_diagnostic.SA1642.severity = silent
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0060.severity = none

View File

@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221109.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221116.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,11 +5,7 @@ using Microsoft.Win32;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Json;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using System.Collections.Immutable;
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
using System.Text.Unicode;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core;

View File

@@ -0,0 +1,286 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20221118095755_SplitStoken")]
partial class SplitStoken
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Stoken")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class SplitStoken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Stoken",
table: "users",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Stoken",
table: "users");
}
}
}

View File

@@ -0,0 +1,292 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Context.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20221118124745_AddAidMid")]
partial class AddAidMid
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("Current")
.HasColumnType("INTEGER");
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("ShowInHomeWidget")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("Stoken")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class AddAidMid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Aid",
table: "users",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Mid",
table: "users",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Aid",
table: "users");
migrationBuilder.DropColumn(
name: "Mid",
table: "users");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.HasAnnotation("ProductVersion", "7.0.0");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -231,12 +231,21 @@ namespace Snap.Hutao.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("Cookie")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("Stoken")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("users");

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
@@ -57,21 +58,28 @@ public class User : ObservableObject
/// <inheritdoc cref="EntityUser.Cookie"/>
public Cookie? Cookie
{
get => inner.Cookie;
set => inner.Cookie = value;
}
/// <inheritdoc cref="EntityUser.Stoken"/>
public Cookie? Stoken
{
get => inner.Cookie;
set
{
inner.Cookie = value;
OnPropertyChanged(nameof(HasSToken));
inner.Stoken = value;
OnPropertyChanged(nameof(HasStoken));
}
}
/// <summary>
/// 是否拥有 SToken
/// </summary>
public bool HasSToken
public bool HasStoken
{
get => inner.Cookie!.ContainsSToken();
get => inner.Stoken != null;
}
/// <summary>
@@ -88,14 +96,12 @@ public class User : ObservableObject
/// 从数据库恢复用户
/// </summary>
/// <param name="inner">数据库实体</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="false"/> </returns>
internal static async Task<User?> ResumeAsync(EntityUser inner, UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
internal static async Task<User?> ResumeAsync(EntityUser inner, CancellationToken token = default)
{
User user = new(inner);
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
bool successful = await user.InitializeCoreAsync(token).ConfigureAwait(false);
return successful ? user : null;
}
@@ -103,40 +109,54 @@ public class User : ObservableObject
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="null"/> </returns>
internal static async Task<User?> CreateAsync(Cookie cookie, UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
internal static async Task<User?> CreateAsync(Cookie cookie, CancellationToken token = default)
{
User user = new(EntityUser.Create(cookie));
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
return successful ? user : null;
EntityUser entity = EntityUser.Create(cookie);
UserInformation? userInfo = await Ioc.Default
.GetRequiredService<PassportClient>()
.VerifyLtokenAsync(cookie, CancellationToken.None)
.ConfigureAwait(false);
entity.Aid = userInfo?.Aid;
entity.Mid = userInfo?.Mid;
if (entity.Aid == null && entity.Mid == null)
{
return null;
}
else
{
User user = new(entity);
bool initialized = await user.InitializeCoreAsync(token).ConfigureAwait(false);
return initialized ? user : null;
}
}
/// <summary>
/// 更新SToken
/// </summary>
/// <param name="uid">uid</param>
/// <param name="cookie">cookie</param>
internal void UpdateSToken(string uid, Cookie cookie)
/// <param name="stoken">cookie</param>
internal void UpdateSToken(Cookie stoken)
{
Cookie!.InsertSToken(uid, cookie);
OnPropertyChanged(nameof(HasSToken));
Stoken = stoken;
OnPropertyChanged(nameof(HasStoken));
}
private async Task<bool> InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
private async Task<bool> InitializeCoreAsync(CancellationToken token = default)
{
if (isInitialized)
{
return true;
}
UserInfo = await userClient
UserInfo = await Ioc.Default.GetRequiredService<UserClient>()
.GetUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);
UserGameRoles = await userGameRoleClient
UserGameRoles = await Ioc.Default.GetRequiredService<BindingClient>()
.GetUserGameRolesByCookieAsync(Entity, token)
.ConfigureAwait(false);

View File

@@ -18,7 +18,13 @@ internal class UserConfiguration : IEntityTypeConfiguration<User>
builder.Property(e => e.Cookie)
.HasColumnType("TEXT")
.HasConversion(
e => e == null ? string.Empty : e.ToString(),
e => e!.ToString(),
e => Cookie.Parse(e));
builder.Property(e => e.Stoken)
.HasColumnType("TEXT")
.HasConversion(
e => e!.ToString(),
e => Cookie.Parse(e));
}
}

View File

@@ -21,16 +21,31 @@ public class User : ISelectable
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 米游社账号 Id
/// </summary>
public string? Aid { get; set; }
/// <summary>
/// 米哈游 Id
/// </summary>
public string? Mid { get; set; }
/// <summary>
/// 是否被选中
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// 用户的Cookie
/// 用户的 Cookie
/// </summary>
public Cookie? Cookie { get; set; }
/// <summary>
/// 用户的 Stoken V2
/// </summary>
public Cookie? Stoken { get; set; }
/// <summary>
/// 创建一个新的用户
/// </summary>

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive.Converter;
using Windows.ApplicationModel.Chat;
namespace Snap.Hutao.Model.Primitive;

View File

@@ -37,7 +37,7 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
Model.Binding.User.User? user = userService.Current;
if (user != null && user.SelectedUserGameRole != null)
{
if (user.Cookie!.ContainsSToken())
if (user.HasStoken)
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
@@ -54,7 +54,7 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
}
else
{
return new(false, "当前用户的Cookie不包含 Stoken");
return new(false, "当前用户的 Cookie 不包含 Stoken");
}
}
else

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User.User;
@@ -15,13 +16,12 @@ internal static class UserHelper
/// 尝试获取用户
/// </summary>
/// <param name="users">待查找的用户集合</param>
/// <param name="uid">uid</param>
/// <param name="mid">米哈游Id</param>
/// <param name="user">用户</param>
/// <returns>是否存在用户</returns>
public static bool TryGetUserByUid(ObservableCollection<BindingUser> users, string uid, [NotNullWhen(true)] out BindingUser? user)
public static bool TryGetUser(ObservableCollection<BindingUser> users, string mid, [NotNullWhen(true)] out BindingUser? user)
{
user = users.SingleOrDefault(u => u.UserInfo!.Uid == uid);
user = users.SingleOrDefault(u => u.Entity.Mid == mid);
return user != null;
}
}

View File

@@ -114,13 +114,11 @@ internal class UserService : IUserService
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
foreach (Model.Entity.User entity in appDbContext.Users)
{
BindingUser? initialized = await BindingUser
.ResumeAsync(entity, userClient, bindingClient)
.ResumeAsync(entity)
.ConfigureAwait(false);
if (initialized != null)
@@ -177,51 +175,44 @@ internal class UserService : IUserService
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
{
cookie.Trim();
Must.NotNull(userCollection!);
// 检查 uid 是否存在
if (cookie.TryGetUid(out string? uid))
string? mid = await cookie.GetMidAsync().ConfigureAwait(false);
if (mid == null)
{
// 检查 login ticket 是否存在
// 若存在则尝试升级至 stoken
await cookie.TryAddMultiTokenAsync(uid).ConfigureAwait(false);
// 检查 uid 对应用户是否存在
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 检查 stoken 是否存在
if (cookie.ContainsSToken())
{
// insert stoken
await ThreadHelper.SwitchToMainThreadAsync();
userWithSameUid.UpdateSToken(uid, cookie);
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
return new(UserOptionResult.Upgraded, uid);
}
if (cookie.ContainsLTokenAndCookieToken())
{
await ThreadHelper.SwitchToMainThreadAsync();
userWithSameUid.Cookie = cookie;
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
return new(UserOptionResult.Updated, uid);
}
}
}
else if (cookie.ContainsLTokenAndCookieToken())
{
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false);
}
return new(UserOptionResult.Invalid, "输入的Cookie无法获取用户信息");
}
return new(UserOptionResult.Incomplete, null!);
// 检查 mid 对应用户是否存在
if (UserHelper.TryGetUser(userCollection, mid, out BindingUser? user))
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 检查 stoken 是否存在
if (user.HasStoken)
{
// update stoken
await ThreadHelper.SwitchToMainThreadAsync();
user.UpdateSToken(cookie);
appDbContext.Users.UpdateAndSave(user.Entity);
return new(UserOptionResult.Upgraded, mid);
}
await ThreadHelper.SwitchToMainThreadAsync();
user.Cookie = cookie;
appDbContext.Users.UpdateAndSave(user.Entity);
return new(UserOptionResult.Updated, mid);
}
}
else
{
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false);
}
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie)
@@ -229,10 +220,8 @@ internal class UserService : IUserService
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
UserClient userClient = scope.ServiceProvider.GetRequiredService<UserClient>();
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
BindingUser? newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache
@@ -256,7 +245,7 @@ internal class UserService : IUserService
}
else
{
return new(UserOptionResult.Invalid, null!);
return new(UserOptionResult.Invalid, "输入的Cookie无法获取用户信息");
}
}
}

View File

@@ -127,7 +127,7 @@
<TextBlock Text="{Binding UserInfo.Nickname}"/>
<TextBlock
Text="Stoken"
Visibility="{Binding HasSToken,Converter={StaticResource BoolToVisibilityConverter}}"
Visibility="{Binding HasStoken,Converter={StaticResource BoolToVisibilityConverter}}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="Green"
Opacity="0.6"/>
@@ -210,7 +210,7 @@
<AppBarButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="网页米游社" Command="{Binding LoginMihoyoBBSCommand}"/>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="米游社" Command="{Binding LoginMihoyoBBS2Command}"/>
<!--<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="米游社" Command="{Binding LoginMihoyoBBS2Command}"/>-->
</MenuFlyout>
</AppBarButton.Flyout>
</AppBarButton>

View File

@@ -142,7 +142,7 @@ internal class UserViewModel : ObservableObject
private async Task LoginMihoyoBBS2Async()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
await new LoginMihoyoBBSDialog(mainWindow).GetInputAccountPasswordAsync();
await new LoginMihoyoBBSDialog(mainWindow).GetInputAccountPasswordAsync().ConfigureAwait(false);
}
private void LoginMihoyoBBS()

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -43,7 +42,7 @@ internal class UserClient
{
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.Cookie)
//.SetReferer(ApiEndpoints.BbsReferer)
.SetReferer(ApiEndpoints.BbsReferer)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiEndpoints.UserFullInfo, options, logger, token)
.ConfigureAwait(false);

View File

@@ -10,19 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab;
[SuppressMessage("", "SA1600")]
public partial class Cookie
{
public const string ACCOUNT_ID = "account_id";
public const string COOKIE_TOKEN = "cookie_token";
public const string E_HK4E_TOKEN = "e_hk4e_token";
public const string LOGIN_TICKET = "login_ticket";
public const string LOGIN_UID = "login_uid";
public const string LTOKEN = "ltoken";
public const string LTUID = "ltuid";
public const string MID = "mid";
public const string STOKEN = "stoken";
public const string STUID = "stuid";
}

View File

@@ -3,6 +3,7 @@
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
namespace Snap.Hutao.Web.Hoyolab;
@@ -65,152 +66,52 @@ public partial class Cookie
}
/// <summary>
/// 存在 LoginTicket
/// 此 Cookie 是 SToken
/// </summary>
/// <returns>是否存在</returns>
public bool ContainsLoginTicket()
public bool IsStoken()
{
return inner.ContainsKey(LOGIN_TICKET);
}
int stokenFlag = 0;
/// <summary>
/// 存在 LToken 与 CookieToken
/// </summary>
/// <returns>是否存在</returns>
public bool ContainsLTokenAndCookieToken()
{
return inner.ContainsKey(LTOKEN) && inner.ContainsKey(COOKIE_TOKEN);
}
/// <summary>
/// 存在 SToken
/// </summary>
/// <returns>是否存在</returns>
public bool ContainsSToken()
{
return inner.ContainsKey(STOKEN);
}
/// <summary>
/// 插入 Stoken
/// </summary>
/// <param name="stuid">stuid</param>
/// <param name="cookie">cookie</param>
public void InsertSToken(string stuid, Cookie cookie)
{
inner[STUID] = stuid;
inner[STOKEN] = cookie.inner[STOKEN];
}
/// <summary>
/// 移除无效的键
/// </summary>
public void Trim()
{
foreach (string key in inner.Keys.ToList())
foreach (string key in inner.Keys)
{
if (key == ACCOUNT_ID
|| key == COOKIE_TOKEN
|| key == LOGIN_UID
|| key == LOGIN_TICKET
|| key == LTUID
|| key == LTOKEN
|| key == STUID
|| key == STOKEN)
if (key is MID or STOKEN or STUID)
{
continue;
}
else
{
inner.Remove(key);
}
}
}
/// <inheritdoc cref="Dictionary2{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
return inner.TryGetValue(key, out value);
}
public bool TryGetLoginTicket([NotNullWhen(true)] out string? loginTicket)
{
return inner.TryGetValue(LOGIN_TICKET, out loginTicket);
}
public bool TryGetUid([NotNullWhen(true)] out string? uid)
{
Dictionary<string, int> uidCounter = new();
foreach ((string key, string value) in inner)
{
if (key is ACCOUNT_ID or LOGIN_UID or LTUID or STUID)
{
uidCounter.Increase(key);
stokenFlag++;
}
}
if (uidCounter.Count > 0)
return stokenFlag == 3;
}
/// <summary>
/// 异步获取 Mid
/// </summary>
/// <returns>mid</returns>
public async Task<string?> GetMidAsync()
{
string? mid;
if (IsStoken())
{
// fix #88 自带页面登录米游社,提示登录失败
string key = uidCounter.MaxBy(kvp => kvp.Value).Key;
uid = inner[key];
return true;
mid = inner[MID];
}
else
{
uid = null;
return false;
}
}
/// <summary>
/// 异步尝试添加MultiToken
/// </summary>
/// <param name="uid">uid</param>
/// <returns>任务</returns>
public async Task TryAddMultiTokenAsync(string uid)
{
if (TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
UserInformation? userInfo = await Ioc.Default
.GetRequiredService<PassportClient>()
.VerifyLtokenAsync(this, CancellationToken.None)
.ConfigureAwait(false);
if (multiToken.Count >= 2)
{
inner[STUID] = uid;
inner[STOKEN] = multiToken[STOKEN];
inner[LTUID] = uid;
inner[LTOKEN] = multiToken[LTOKEN];
inner.Remove(LOGIN_TICKET);
inner.Remove(LOGIN_UID);
}
mid = userInfo?.Mid;
}
return mid;
}
/// <summary>
/// 根据类型输出对应的Cookie
/// </summary>
/// <param name="type">类型</param>
/// <returns>Cookie对应的字符串表示</returns>
public string ToString(CookieType type)
/// <inheritdoc cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
IEnumerable<KeyValuePair<string, string>> results;
results = type switch
{
CookieType.None => Enumerable.Empty<KeyValuePair<string, string>>(),
CookieType.Cookie => inner.Where(kvp => kvp.Key is E_HK4E_TOKEN or LTUID or LTOKEN or ACCOUNT_ID or COOKIE_TOKEN),
CookieType.Stoken => inner.Where(kvp => kvp.Key is STUID or STOKEN or MID),
CookieType.All => inner,
_ => throw Must.NeverHappen(type.ToString()),
};
return string.Join(';', results.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return inner.TryGetValue(key, out value);
}
/// <summary>
@@ -219,6 +120,6 @@ public partial class Cookie
/// <returns>Cookie的字符串表示</returns>
public override string ToString()
{
return ToString(CookieType.All);
return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
}

View File

@@ -1,48 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Convert;
using System.Text;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
/// <summary>
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供动态密钥
/// </summary>
internal abstract class DynamicSecretProvider : Md5Convert
{
private const string RandomRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
/// <summary>
/// 创建动态密钥
/// </summary>
/// <param name="saltType">SALT 类型</param>
/// <returns>密钥</returns>
public static string Create(SaltType saltType)
{
Verify.Operation(saltType is SaltType.K2 or SaltType.LK2, "SALT 值无效");
// unix timestamp
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string r = GetRandomString();
string salt = Core.CoreEnvironment.DynamicSecrets[saltType];
string check = ToHexString($"salt={salt}&t={t}&r={r}").ToLowerInvariant();
return $"{t},{r},{check}";
}
private static string GetRandomString()
{
StringBuilder sb = new(6);
for (int i = 0; i < 6; i++)
{
int pos = Random.Shared.Next(0, RandomRange.Length);
sb.Append(RandomRange[pos]);
}
return sb.ToString();
}
}

View File

@@ -1,68 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Convert;
namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
/// <summary>
/// 为MiHoYo接口请求器 <see cref="Requester"/> 提供2代动态密钥
/// </summary>
internal abstract class DynamicSecretProvider2 : Md5Convert
{
/// <summary>
/// 创建动态密钥
/// </summary>
/// <param name="saltType">SALT 类型</param>
/// <param name="options">json格式化器</param>
/// <param name="queryUrl">查询url</param>
/// <param name="postBody">请求体</param>
/// <returns>密钥</returns>
public static string Create(SaltType saltType, JsonSerializerOptions options, string queryUrl, object? postBody = null)
{
Verify.Operation(saltType is SaltType.X6 or SaltType.X4 or SaltType.PROD, "SALT 值无效");
// unix timestamp
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// random
int r = GetRandom();
// body
string b = postBody is null ? GetDefaultBody(saltType) : JsonSerializer.Serialize(postBody, options);
// query
string[] queries = queryUrl.Split('?', 2);
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
// check
string salt = Core.CoreEnvironment.DynamicSecrets[saltType];
string check = ToHexString($"salt={salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
return $"{t},{r},{check}";
}
private static string GetDefaultBody(SaltType saltType)
{
return saltType switch
{
SaltType.X4 or SaltType.X6 => string.Empty,
SaltType.PROD => "{}",
_ => throw Must.NeverHappen(((int)saltType).ToString()),
};
}
private static int GetRandom()
{
// 原汁原味
// v16 = time(0LL);
// srand(v16);
// v17 = (int)((double)rand() / 2147483650.0 * 100000.0 + 100000.0) % 1000000;
// if (v17 >= 100001)
// v18 = v17;
// else
// v18 = v17 + 542367;
int rand = Random.Shared.Next(100000, 200000);
return rand == 100000 ? 642367 : rand;
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.DynamicSecret.Http;
using Snap.Hutao.Web.Request;
using System.Net.Http;
@@ -25,47 +24,4 @@ internal static class HttpClientDynamicSecretExtensions
client.DefaultRequestHeaders.Set("DS-Option", $"{version}|{salt}|{includeChars}");
return client;
}
/// <summary>
/// 使用一代动态密钥执行 GET/POST 操作
/// </summary>
/// <param name="httpClient">请求器</param>
/// <param name="type">SALT 类型</param>
/// <returns>响应</returns>
[Obsolete]
public static HttpClient UsingDynamicSecret1(this HttpClient httpClient, SaltType type)
{
httpClient.DefaultRequestHeaders.Set("DS", DynamicSecretProvider.Create(type));
return httpClient;
}
/// <summary>
/// 使用二代动态密钥执行 GET/POST 操作
/// </summary>
/// <param name="httpClient">请求器</param>
/// <param name="type">SALT 类型</param>
/// <param name="options">选项</param>
/// <param name="url">地址</param>
/// <returns>响应</returns>
[Obsolete]
public static IDynamicSecretHttpClient UsingDynamicSecret2(this HttpClient httpClient, SaltType type, JsonSerializerOptions options, string url)
{
return new DynamicSecretHttpClient(httpClient, type, options, url);
}
/// <summary>
/// 使用二代动态密钥执行 GET/POST 操作
/// </summary>
/// <typeparam name="TValue">请求数据的类型</typeparam>
/// <param name="httpClient">请求器</param>
/// <param name="type">SALT 类型</param>
/// <param name="options">选项</param>
/// <param name="url">地址</param>
/// <param name="data">post数据</param>
/// <returns>响应</returns>
public static IDynamicSecretHttpClient<TValue> UsingDynamicSecret2<TValue>(this HttpClient httpClient, SaltType type, JsonSerializerOptions options, string url, TValue data)
where TValue : class
{
return new DynamicSecretHttpClient<TValue>(httpClient, type, options, url, data);
}
}
}

View File

@@ -2,8 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Request;
using System.Net.Http;
using System.Net.Http.Json;
@@ -62,7 +60,7 @@ internal static class HttpClientExtensions
}
/// <summary>
/// 设置用户的Cookie
/// 设置用户的 Cookie
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="user">实体用户</param>
@@ -70,7 +68,25 @@ internal static class HttpClientExtensions
/// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, Model.Entity.User user, CookieType cookie)
{
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie!.ToString(cookie));
if (cookie == CookieType.Stoken)
{
return httpClient.SetRawCookie(user.Stoken!);
}
else
{
return httpClient.SetRawCookie(user.Cookie!);
}
}
/// <summary>
/// 设置 Cookie
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="cookie">Cookie</param>
/// <returns>客户端</returns>
internal static HttpClient SetRawCookie(this HttpClient httpClient, Cookie cookie)
{
httpClient.DefaultRequestHeaders.Set("Cookie", cookie.ToString());
return httpClient;
}

View File

@@ -1,10 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -36,18 +35,18 @@ internal class PassportClient
/// <summary>
/// 异步验证Ltoken
/// </summary>
/// <param name="user">用户</param>
/// <param name="cookie">待校验的 Cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>验证信息</returns>
[ApiInformation(Cookie = CookieType.Cookie)]
public async Task<UserInformation?> VerifyLtokenAsync(User user, CancellationToken token)
public async Task<UserInformation?> VerifyLtokenAsync(Cookie cookie, CancellationToken token)
{
Response<UserInformation>? response = await httpClient
.SetUser(user, CookieType.All)
.TryCatchPostAsJsonAsync<Timestamp, Response<UserInformation>>(ApiEndpoints.AccountVerifyLtoken, new(), options, logger, token)
Response<UserInfoWrapper>? response = await httpClient
.SetRawCookie(cookie)
.TryCatchPostAsJsonAsync<Timestamp, Response<UserInfoWrapper>>(ApiEndpoints.AccountVerifyLtoken, new(), options, logger, token)
.ConfigureAwait(false);
return response?.Data;
return response?.Data?.UserInfo;
}
private class Timestamp

View File

@@ -50,8 +50,8 @@ internal class PassportClient2
};
Response<LoginResult>? resp = await httpClient
.UsingDynamicSecret2(SaltType.PROD, options, ApiEndpoints.AccountLoginByPassword, data)
.TryCatchPostAsJsonAsync<Response<LoginResult>>(logger, token)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchPostAsJsonAsync<Dictionary<string, string>, Response<LoginResult>>(ApiEndpoints.AccountLoginByPassword, data, options, logger, token)
.ConfigureAwait(false);
return resp?.Data;
@@ -68,8 +68,8 @@ internal class PassportClient2
{
Response<UidCookieToken>? resp = await httpClient
.SetUser(user, CookieType.Stoken)
.UsingDynamicSecret2(SaltType.PROD, options, ApiEndpoints.AccountCookieAccountInfoBySToken)
.TryCatchGetFromJsonAsync<Response<UidCookieToken>>(logger, token)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchGetFromJsonAsync<Response<UidCookieToken>>(ApiEndpoints.AccountCookieAccountInfoBySToken, options, logger, token)
.ConfigureAwait(false);
return resp?.Data;

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 用户信息包装器
/// </summary>
public class UserInfoWrapper
{
/// <summary>
/// 用户信息
/// </summary>
[JsonPropertyName("user_info")]
public UserInformation UserInfo { get; set; } = default!;
}

View File

@@ -1,10 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
@@ -101,4 +97,4 @@ public class UserInformation
/// </summary>
[JsonPropertyName("links")]
public List<Link> Links { get; set; } = default!;
}
}

View File

@@ -1,11 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
/// <summary>

View File

@@ -1,11 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
/// <summary>

View File

@@ -41,47 +41,20 @@ internal class AuthClient
/// <param name="action">操作</param>
/// <param name="user">用户</param>
/// <returns>操作凭证</returns>
[ApiInformation(Cookie = CookieType.Stoken, Salt = DynamicSecret.SaltType.K2)]
[ApiInformation(Cookie = CookieType.Stoken, Salt = SaltType.K2)]
public async Task<string?> GetActionTicketByStokenAsync(string action, User user)
{
if (user.Cookie!.TryGetValue(Cookie.STOKEN, out string? stoken))
if (user.Stoken != null)
{
if (user.Cookie.TryGetUid(out string? uid))
{
Response<ActionTicketWrapper>? resp = await httpClient
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<ActionTicketWrapper>>(ApiEndpoints.AuthActionTicket(action, stoken, uid), options, logger)
.ConfigureAwait(false);
user.Stoken.TryGetValue(Cookie.STOKEN, out string? token);
Response<ActionTicketWrapper>? resp = await httpClient
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<ActionTicketWrapper>>(ApiEndpoints.AuthActionTicket(action, token!, user.Aid!), options, logger)
.ConfigureAwait(false);
return resp?.Data?.Ticket;
}
return resp?.Data?.Ticket;
}
return null;
}
/// <summary>
/// 获取 MultiToken
/// </summary>
/// <param name="loginTicket">登录票证</param>
/// <param name="loginUid">uid</param>
/// <param name="token">取消令牌</param>
/// <returns>包含token的字典</returns>
[Obsolete]
public async Task<Dictionary<string, string>> GetMultiTokenByLoginTicketAsync(string loginTicket, string loginUid, CancellationToken token)
{
Response<ListWrapper<NameToken>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<ListWrapper<NameToken>>>(ApiEndpoints.AuthMultiToken(loginTicket, loginUid), options, logger, token)
.ConfigureAwait(false);
if (resp?.Data != null)
{
Dictionary<string, string> dict = resp.Data.List.ToDictionary(n => n.Name, n => n.Token);
Must.Argument(dict.ContainsKey(Cookie.LTOKEN), "MultiToken 应该包含 ltoken");
Must.Argument(dict.ContainsKey(Cookie.STOKEN), "MultiToken 应该包含 stoken");
return dict;
}
return new();
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
@@ -64,11 +65,11 @@ internal class BindingClient2
public async Task<GameAuthKey?> GenerateAuthenticationKeyAsync(User user, GenAuthKeyData data, CancellationToken token = default)
{
Response<GameAuthKey>? resp = await httpClient
.SetUser(user, CookieType.Stoken)
.SetReferer("https://app.mihoyo.com")
.UsingDynamicSecret1(SaltType.K2)
.TryCatchPostAsJsonAsync<GenAuthKeyData, Response<GameAuthKey>>(ApiEndpoints.BindingGenAuthKey, data, options, logger, token)
.ConfigureAwait(false);
.SetUser(user, CookieType.Stoken)
.SetReferer("https://app.mihoyo.com")
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchPostAsJsonAsync<GenAuthKeyData, Response<GameAuthKey>>(ApiEndpoints.BindingGenAuthKey, data, options, logger, token)
.ConfigureAwait(false);
return resp?.Data;
}

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.DynamicSecret.Http;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Widget;
using Snap.Hutao.Web.Response;
@@ -129,8 +128,8 @@ internal class GameRecordClient
Response<CharacterWrapper>? resp = await httpClient
.SetUser(user, CookieType.Cookie)
.UsingDynamicSecret2(SaltType.X4, options, ApiEndpoints.GameRecordCharacter, data)
.TryCatchPostAsJsonAsync<Response<CharacterWrapper>>(logger, token)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.X4, false)
.TryCatchPostAsJsonAsync<CharacterData, Response<CharacterWrapper>>(ApiEndpoints.GameRecordCharacter, data, options, logger, token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data?.Avatars);

View File

@@ -177,13 +177,8 @@ internal class HomaClient
{
Must.NotNull(user.SelectedUserGameRole!);
PlayerInfo? playerInfo = await gameRecordClient
.GetPlayerInfoAsync(user.Entity, user.SelectedUserGameRole, token)
.ConfigureAwait(false);
Must.NotNull(playerInfo!);
List<Character> characters = await gameRecordClient
.GetCharactersAsync(user.Entity, user.SelectedUserGameRole, playerInfo, token)
.GetCharactersAsync(user.Entity, user.SelectedUserGameRole, token)
.ConfigureAwait(false);
SpiralAbyss? spiralAbyssInfo = await gameRecordClient