This commit is contained in:
DismissedLight
2022-09-29 16:13:47 +08:00
parent 331cc14532
commit 94ef94a621
33 changed files with 672 additions and 543 deletions

View File

@@ -69,6 +69,8 @@ public class AppDbContext : DbContext
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new AvatarInfoConfiguration());
modelBuilder
.ApplyConfiguration(new AvatarInfoConfiguration())
.ApplyConfiguration(new UserConfiguration());
}
}

View File

@@ -24,7 +24,7 @@ internal class I18NExtension : MarkupExtension
static I18NExtension()
{
string currentName = CultureInfo.CurrentUICulture.Name;
Type languageType = EnumerableExtensions.GetValueOrDefault(TranslationMap, currentName, typeof(LanguagezhCN));
Type languageType = TranslationMap.GetValueOrDefault(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType)!;
}

View File

@@ -70,6 +70,83 @@ internal class DbCurrent<TEntity, TMessage>
dbContext.SaveChanges();
}
messenger.Send(message);
}
}
}
/// <summary>
/// 数据库当前项
/// 简化对数据库中选中项的管理
/// </summary>
/// <typeparam name="TObservable">绑定类型</typeparam>
/// <typeparam name="TEntity">实体的类型</typeparam>
/// <typeparam name="TMessage">消息的类型</typeparam>
[SuppressMessage("", "SA1402")]
internal class DbCurrent<TObservable, TEntity, TMessage>
where TObservable : class
where TEntity : class, ISelectable
where TMessage : Message.ValueChangedMessage<TObservable>
{
private readonly DbContext dbContext;
private readonly DbSet<TEntity> dbSet;
private readonly IMessenger messenger;
private readonly Func<TObservable, TEntity> selector;
private TObservable? current;
/// <summary>
/// 构造一个新的数据库当前项
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="dbSet">数据集</param>
/// <param name="selector">选择器</param>
/// <param name="messenger">消息器</param>
public DbCurrent(DbContext dbContext, DbSet<TEntity> dbSet, Func<TObservable, TEntity> selector, IMessenger messenger)
{
this.dbContext = dbContext;
this.dbSet = dbSet;
this.selector = selector;
this.messenger = messenger;
}
/// <summary>
/// 当前选中的项
/// </summary>
public TObservable? Current
{
get => current;
set
{
// prevent useless sets
if (current == value)
{
return;
}
// only update when not processing a deletion
if (value != null)
{
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = false;
dbSet.Update(entity);
dbContext.SaveChanges();
}
}
TMessage message = (TMessage)Activator.CreateInstance(typeof(TMessage), current, value)!;
current = value;
if (current != null)
{
TEntity entity = selector(current);
entity.IsSelected = true;
dbSet.Update(entity);
dbContext.SaveChanges();
}
messenger.Send(message);
}
}

View File

@@ -25,4 +25,16 @@ internal static class Clipboard
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);
}
/// <summary>
/// 设置文本
/// </summary>
/// <param name="text">文本</param>
public static void SetText(string text)
{
DataPackage content = new();
content.SetText(text);
Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content);
Windows.ApplicationModel.DataTransfer.Clipboard.Flush();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Extension;
@@ -99,23 +100,35 @@ public static partial class EnumerableExtensions
}
/// <summary>
/// 获取值或默认值
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <typeparam name="TValue">值类型</typeparam>
/// <param name="dictionary">字典</param>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>结果值</returns>
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
if (dictionary.TryGetValue(key, out TValue? value))
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
{
return value;
++value;
return true;
}
return defaultValue;
return false;
}
/// <summary>

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Extension;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using EntityUser = Snap.Hutao.Model.Entity.User;
@@ -56,7 +55,7 @@ public class User : Observable
}
/// <inheritdoc cref="EntityUser.Cookie"/>
public string? Cookie
public Cookie Cookie
{
get => inner.Cookie;
set => inner.Cookie = value;
@@ -72,27 +71,6 @@ public class User : Observable
/// </summary>
public bool IsInitialized { get => isInitialized; }
/// <summary>
/// 将cookie的字符串形式转换为字典
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>包含cookie信息的字典</returns>
public static IDictionary<string, string> MapCookie(string cookie)
{
SortedDictionary<string, string> cookieDictionary = new();
string[] values = cookie.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
string cookieName = parts[0].Trim();
string cookieValue = parts.Length == 1 ? string.Empty : parts[1].Trim();
cookieDictionary.Add(cookieName, cookieValue);
}
return cookieDictionary;
}
/// <summary>
/// 从数据库恢复用户
/// </summary>
@@ -108,114 +86,30 @@ public class User : Observable
CancellationToken token = default)
{
User user = new(inner);
bool successful = await user.ResumeInternalAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
return successful ? user : null;
}
/// <summary>
/// 初始化用户
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="authClient">授权客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="false"/> </returns>
/// <returns>用户是否初始化完成若Cookie失效会返回 <see langword="null"/> </returns>
internal static async Task<User?> CreateAsync(
IDictionary<string, string> cookie,
Cookie cookie,
UserClient userClient,
BindingClient userGameRoleClient,
AuthClient authClient,
CancellationToken token = default)
{
string simplifiedCookie = ToCookieString(cookie);
EntityUser inner = EntityUser.Create(simplifiedCookie);
User user = new(inner);
bool successful = await user.CreateInternalAsync(cookie, userClient, userGameRoleClient, authClient, token).ConfigureAwait(false);
User user = new(EntityUser.Create(cookie));
bool successful = await user.InitializeCoreAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
return successful ? user : null;
}
/// <summary>
/// 尝试升级到Stoken
/// </summary>
/// <param name="addition">额外的token</param>
/// <param name="authClient">验证客户端</param>
/// <param name="token">取消令牌</param>
/// <returns>是否升级成功</returns>
internal async Task<bool> TryUpgradeByLoginTicketAsync(IDictionary<string, string> addition, AuthClient authClient, CancellationToken token)
{
IDictionary<string, string> cookie = MapCookie(Cookie!);
if (addition.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
{
cookie[CookieKeys.LOGIN_TICKET] = loginTicket;
}
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? loginUid))
{
cookie[CookieKeys.LOGIN_UID] = loginUid;
}
bool result = await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false);
if (result)
{
Cookie = ToCookieString(cookie);
}
return result;
}
/// <summary>
/// 添加 Stoken
/// </summary>
/// <param name="addition">额外的cookie</param>
internal void AddStoken(IDictionary<string, string> addition)
{
IDictionary<string, string> cookie = MapCookie(Cookie!);
if (addition.TryGetValue(CookieKeys.STOKEN, out string? stoken))
{
cookie[CookieKeys.STOKEN] = stoken;
}
if (addition.TryGetValue(CookieKeys.STUID, out string? stuid))
{
cookie[CookieKeys.STUID] = stuid;
}
}
private static string ToCookieString(IDictionary<string, string> cookie)
{
return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
private static async Task<bool> TryRequestStokenAndAddToCookieAsync(IDictionary<string, string> cookie, AuthClient authClient, CancellationToken token)
{
if (cookie.TryGetValue(CookieKeys.LOGIN_TICKET, out string? loginTicket))
{
string? loginUid = cookie.GetValueOrDefault(CookieKeys.LOGIN_UID) ?? cookie.GetValueOrDefault(CookieKeys.LTUID);
if (loginUid != null)
{
Dictionary<string, string> stokens = await authClient
.GetMultiTokenByLoginTicketAsync(loginTicket, loginUid, token)
.ConfigureAwait(false);
if (stokens.TryGetValue(CookieKeys.STOKEN, out string? stoken) && stokens.TryGetValue(CookieKeys.LTOKEN, out string? ltoken))
{
cookie[CookieKeys.STOKEN] = stoken;
cookie[CookieKeys.LTOKEN] = ltoken;
cookie[CookieKeys.STUID] = cookie[CookieKeys.LTUID];
return true;
}
}
}
return false;
}
private async Task<bool> ResumeInternalAsync(
private async Task<bool> InitializeCoreAsync(
UserClient userClient,
BindingClient userGameRoleClient,
CancellationToken token = default)
@@ -225,38 +119,14 @@ public class User : Observable
return true;
}
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
await InitializeUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
isInitialized = true;
return UserInfo != null && UserGameRoles.Any();
}
private async Task<bool> CreateInternalAsync(
IDictionary<string, string> cookie,
UserClient userClient,
BindingClient userGameRoleClient,
AuthClient authClient,
CancellationToken token = default)
{
if (isInitialized)
{
return true;
}
if (await TryRequestStokenAndAddToCookieAsync(cookie, authClient, token).ConfigureAwait(false))
{
Cookie = ToCookieString(cookie);
}
await PrepareUserInfoAndUserGameRolesAsync(userClient, userGameRoleClient, token).ConfigureAwait(false);
isInitialized = true;
return UserInfo != null && UserGameRoles.Any();
}
private async Task PrepareUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
private async Task InitializeUserInfoAndUserGameRolesAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token)
{
UserInfo = await userClient
.GetUserFullInfoAsync(this, token)

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Model.Entity.Configuration;
/// <summary>
/// 用户配置
/// </summary>
internal class UserConfiguration : IEntityTypeConfiguration<User>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property(e => e.Cookie)
.HasColumnType("TEXT")
.HasConversion(
e => e == null ? string.Empty : e.ToString(),
e => Cookie.Parse(e));
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Web.Hoyolab;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@@ -28,14 +29,14 @@ public class User : ISelectable
/// <summary>
/// 用户的Cookie
/// </summary>
public string? Cookie { get; set; }
public Cookie Cookie { get; set; } = default!;
/// <summary>
/// 创建一个新的用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <returns>新创建的用户</returns>
public static User Create(string cookie)
public static User Create(Cookie cookie)
{
return new() { Cookie = cookie };
}

View File

@@ -21,6 +21,7 @@ public enum WeaponType
WEAPON_SWORD_ONE_HAND = 1,
#region Not Used
/// <summary>
/// ?
/// </summary>

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Model.Metadata.Abstraction;
/// <summary>
/// 指示该类为简述统计物品的源
/// </summary>
public interface ISummaryItemSource
{
/// <summary>
/// 星级
/// </summary>
ItemQuality Quality { get; }
/// <summary>
/// 转换到简述统计物品
/// </summary>
/// <param name="lastPull">距上个五星</param>
/// <param name="time">时间</param>
/// <param name="isUp">是否为Up物品</param>
/// <returns>简述统计物品</returns>
SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp);
}

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
/// 角色
/// </summary>
public class Avatar : IStatisticsItemSource, INameQuality
public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
{
/// <summary>
/// Id

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 武器
/// </summary>
public class Weapon : IStatisticsItemSource, INameQuality
public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
{
/// <summary>
/// Id

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.4.0" />
Version="1.1.5.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -5,8 +5,9 @@ using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Windows.UI;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -34,35 +35,28 @@ public static class GachaStatisticsExtensions
}
/// <summary>
/// 增加计数
/// 完成添加
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
public static void Increase<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
/// <param name="summaryItems">简述物品列表</param>
public static void CompleteAdding(this List<SummaryItem> summaryItems)
{
++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
}
// we can't trust first item's prev state.
bool isPreviousUp = true;
/// <summary>
/// 增加计数
/// </summary>
/// <typeparam name="TKey">键类型</typeparam>
/// <param name="dict">字典</param>
/// <param name="key">键</param>
/// <returns>是否存在键值</returns>
public static bool TryIncrease<TKey>(this Dictionary<TKey, int> dict, TKey key)
where TKey : notnull
{
ref int value = ref CollectionsMarshal.GetValueRefOrNullRef(dict, key);
if (!Unsafe.IsNullRef(ref value))
// mark the IsGuarentee
foreach (SummaryItem item in summaryItems)
{
++value;
return true;
if (item.IsUp && (!isPreviousUp))
{
item.IsGuarentee = true;
}
isPreviousUp = item.IsUp;
item.Color = GetColorByName(item.Name);
}
return false;
// reverse items
summaryItems.Reverse();
}
/// <summary>
@@ -103,4 +97,14 @@ public static class GachaStatisticsExtensions
.OrderByDescending(item => item.Count)
.ToList();
}
private static Color GetColorByName(string name)
{
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Span<byte> first = new(codes, 0, 5);
Span<byte> second = new(codes, 5, 5);
Span<byte> third = new(codes, 10, 5);
Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average());
return color;
}
}

View File

@@ -89,9 +89,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackAvatar(item, avatar, isUp);
avatarWishBuilder.TrackAvatar(item, avatar, isUp);
weaponWishBuilder.TrackAvatar(item, avatar, isUp);
permanentWishBuilder.Track(item, avatar, isUp);
avatarWishBuilder.Track(item, avatar, isUp);
weaponWishBuilder.Track(item, avatar, isUp);
}
// It's a weapon
@@ -116,9 +116,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
break;
}
permanentWishBuilder.TrackWeapon(item, weapon, isUp);
avatarWishBuilder.TrackWeapon(item, weapon, isUp);
weaponWishBuilder.TrackWeapon(item, weapon, isUp);
permanentWishBuilder.Track(item, weapon, isUp);
avatarWishBuilder.Track(item, weapon, isUp);
weaponWishBuilder.Track(item, weapon, isUp);
}
else
{

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Abstraction;

View File

@@ -5,12 +5,8 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Security.Cryptography;
using System.Text;
using Windows.UI;
namespace Snap.Hutao.Service.GachaLog.Factory;
@@ -75,9 +71,9 @@ internal class TypedWishSummaryBuilder
/// 追踪物品
/// </summary>
/// <param name="item">祈愿物品</param>
/// <param name="avatar">对应角色</param>
/// <param name="source">对应武器</param>
/// <param name="isUp">是否为Up物品</param>
public void TrackAvatar(GachaItem item, Avatar avatar, bool isUp)
public void Track(GachaItem item, ISummaryItemSource source, bool isUp)
{
if (typeEvaluator(item.GachaType))
{
@@ -89,7 +85,7 @@ internal class TypedWishSummaryBuilder
++totalCountTracker;
TrackFromToTime(item.Time);
switch (avatar.Quality)
switch (source.Quality)
{
case ItemQuality.QUALITY_ORANGE:
{
@@ -102,55 +98,7 @@ internal class TypedWishSummaryBuilder
lastUpOrangePullTracker = 0;
}
summaryItemCache.Add(avatar.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
lastOrangePullTracker = 0;
++totalOrangePullTracker;
break;
}
case ItemQuality.QUALITY_PURPLE:
{
lastPurplePullTracker = 0;
++totalPurplePullTracker;
break;
}
}
}
}
/// <summary>
/// 追踪物品
/// </summary>
/// <param name="item">祈愿物品</param>
/// <param name="weapon">对应武器</param>
/// <param name="isUp">是否为Up物品</param>
public void TrackWeapon(GachaItem item, Weapon weapon, bool isUp)
{
if (typeEvaluator(item.GachaType))
{
++lastOrangePullTracker;
++lastPurplePullTracker;
++lastUpOrangePullTracker;
// track total pulls
++totalCountTracker;
TrackFromToTime(item.Time);
switch (weapon.RankLevel)
{
case ItemQuality.QUALITY_ORANGE:
{
TrackMinMaxOrangePull(lastOrangePullTracker);
averageOrangePullTracker.Add(lastOrangePullTracker);
if (isUp)
{
averageUpOrangePullTracker.Add(lastUpOrangePullTracker);
lastUpOrangePullTracker = 0;
}
summaryItemCache.Add(weapon.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
summaryItemCache.Add(source.ToSummaryItem(lastOrangePullTracker, item.Time, isUp));
lastOrangePullTracker = 0;
++totalOrangePullTracker;
@@ -179,7 +127,7 @@ internal class TypedWishSummaryBuilder
/// <returns>类型化祈愿统计信息</returns>
public TypedWishSummary ToTypedWishSummary()
{
CompleteSummaryItems(summaryItemCache);
summaryItemCache.CompleteAdding();
double totalCountDouble = totalCountTracker;
return new()
@@ -209,37 +157,6 @@ internal class TypedWishSummaryBuilder
};
}
private static void CompleteSummaryItems(List<SummaryItem> summaryItems)
{
// we can't trust first item's prev state.
bool isPreviousUp = true;
// mark the IsGuarentee
foreach (SummaryItem item in summaryItems)
{
if (item.IsUp && (!isPreviousUp))
{
item.IsGuarentee = true;
}
isPreviousUp = item.IsUp;
item.Color = GetColorByName(item.Name);
}
// reverse items
summaryItems.Reverse();
}
private static Color GetColorByName(string name)
{
byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name));
Span<byte> first = new(codes, 0, 5);
Span<byte> second = new(codes, 5, 5);
Span<byte> third = new(codes, 10, 5);
Color color = Color.FromArgb(255, first.Average()/*.HalfRange()*/, second.Average()/*.HalfRange()*/, third.Average()/*.HalfRange()*/);
return color;
}
private void TrackMinMaxOrangePull(int lastOrangePull)
{
if (lastOrangePull < minOrangePullTracker || minOrangePullTracker == 0)

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
@@ -35,10 +35,10 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
Model.Binding.User? user = userService.CurrentUser;
Model.Binding.User? user = userService.Current;
if (user != null)
{
if (user.Cookie!.Contains(CookieKeys.STOKEN) && user.SelectedUserGameRole != null)
if (user.Cookie!.ContainsSToken() && user.SelectedUserGameRole != null)
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
@@ -51,6 +51,6 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
}
}
return new(false, null!);
return new(false, "当前用户的Cookie不包含 Stoken");
}
}

View File

@@ -1,12 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Automation.Provider;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户服务
@@ -16,52 +16,28 @@ public interface IUserService
/// <summary>
/// 获取或设置当前用户
/// </summary>
User? CurrentUser { get; set; }
BindingUser? Current { get; set; }
/// <summary>
/// 初始化用户服务及所有用户
/// 异步获取同步的用户信息集合
/// 对集合的操作应通过服务抽象完成
/// 此操作不能取消
/// </summary>
/// <returns>准备完成的用户信息枚举</returns>
Task<ObservableCollection<User>> GetUserCollectionAsync();
/// <returns>准备完成的用户信息集合</returns>
Task<ObservableCollection<BindingUser>> GetUserCollectionAsync();
/// <summary>
/// 异步添加用户
/// 通常用户是未初始化的
/// 尝试异步处理输入的Cookie
/// </summary>
/// <param name="user">待添加的用户</param>
/// <param name="uid">用户的米游社UID,用于检查是否包含重复的用户</param>
/// <returns>用户初始化是否成功</returns>
Task<UserAddResult> TryAddUserAsync(User user, string uid);
/// <summary>
/// 尝试使用 login_ticket 升级用户
/// </summary>
/// <param name="addiition">额外的Cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>是否升级成功</returns>
Task<ValueResult<bool, string>> TryUpgradeUserByLoginTicketAsync(IDictionary<string, string> addiition, CancellationToken token = default);
/// <summary>
/// 尝试使用 Stoken 升级用户
/// </summary>
/// <param name="stoken">stoken</param>
/// <returns>是否升级成功</returns>
Task<ValueResult<bool, string>> TryUpgradeUserByStokenAsync(IDictionary<string, string> stoken);
/// <param name="cookie">Cookie</param>
/// <returns>处理的结果</returns>
Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie);
/// <summary>
/// 异步移除用户
/// </summary>
/// <param name="user">待移除的用户</param>
/// <returns>任务</returns>
Task RemoveUserAsync(User user);
/// <summary>
/// 创建一个新的绑定用户
/// 若存在 login_ticket 与 login_uid 则 自动获取 stoken
/// </summary>
/// <param name="cookie">cookie的字符串形式</param>
/// <returns>新的绑定用户</returns>
Task<User?> CreateUserAsync(IDictionary<string, string> cookie);
}
Task RemoveUserAsync(BindingUser user);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户帮助类
/// </summary>
internal static class UserHelper
{
/// <summary>
/// 尝试获取用户
/// </summary>
/// <param name="users">待查找的用户集合</param>
/// <param name="uid">uid</param>
/// <param name="user">用户</param>
/// <returns>是否存在用户</returns>
public static bool TryGetUserByUid(ObservableCollection<BindingUser> users, string uid, [NotNullWhen(true)] out BindingUser? user)
{
user = users.SingleOrDefault(u => u.UserInfo!.Uid == uid);
return user != null;
}
}

View File

@@ -1,25 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户添加操作结果
/// </summary>
public enum UserAddResult
public enum UserOptionResult
{
/// <summary>
/// 添加成功
/// </summary>
Added,
/// <summary>
/// Cookie不完整
/// </summary>
Incomplete,
/// <summary>
/// Cookie信息已经失效
/// </summary>
Invalid,
/// <summary>
/// 用户的Cookie成功更新
/// </summary>
Updated,
/// <summary>
/// 已经存在该用户
/// 升级到Stoken
/// </summary>
AlreadyExists,
Upgraded,
}

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
@@ -12,7 +11,7 @@ using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.Model.Binding.User;
namespace Snap.Hutao.Service;
namespace Snap.Hutao.Service.User;
/// <summary>
/// 用户服务
@@ -23,7 +22,7 @@ internal class UserService : IUserService
{
private readonly AppDbContext appDbContext;
private readonly UserClient userClient;
private readonly BindingClient userGameRoleClient;
private readonly BindingClient bindingClient;
private readonly AuthClient authClient;
private readonly IMessenger messenger;
@@ -35,25 +34,25 @@ internal class UserService : IUserService
/// </summary>
/// <param name="appDbContext">应用程序数据库上下文</param>
/// <param name="userClient">用户客户端</param>
/// <param name="userGameRoleClient">角色客户端</param>
/// <param name="bindingClient">角色客户端</param>
/// <param name="authClient">验证客户端</param>
/// <param name="messenger">消息器</param>
public UserService(
AppDbContext appDbContext,
UserClient userClient,
BindingClient userGameRoleClient,
BindingClient bindingClient,
AuthClient authClient,
IMessenger messenger)
{
this.appDbContext = appDbContext;
this.userClient = userClient;
this.userGameRoleClient = userGameRoleClient;
this.bindingClient = bindingClient;
this.authClient = authClient;
this.messenger = messenger;
}
/// <inheritdoc/>
public BindingUser? CurrentUser
public BindingUser? Current
{
get => currentUser;
set
@@ -90,45 +89,6 @@ internal class UserService : IUserService
}
}
/// <inheritdoc/>
public async Task<UserAddResult> TryAddUserAsync(BindingUser newUser, string uid)
{
Must.NotNull(userCollection!);
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
{
// Prevent users from adding a completely same cookie.
if (userWithSameUid.Cookie == newUser.Cookie)
{
return UserAddResult.AlreadyExists;
}
else
{
// Update user cookie here.
userWithSameUid.Cookie = newUser.Cookie;
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return UserAddResult.Updated;
}
}
else
{
Verify.Operation(newUser.IsInitialized, "该用户尚未初始化");
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
userCollection.Add(newUser);
// Sync database
appDbContext.Users.Add(newUser.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return UserAddResult.Added;
}
}
/// <inheritdoc/>
public Task RemoveUserAsync(BindingUser user)
{
@@ -152,7 +112,7 @@ internal class UserService : IUserService
foreach (Model.Entity.User entity in appDbContext.Users)
{
BindingUser? initialized = await BindingUser
.ResumeAsync(entity, userClient, userGameRoleClient)
.ResumeAsync(entity, userClient, bindingClient)
.ConfigureAwait(false);
if (initialized != null)
@@ -168,58 +128,94 @@ internal class UserService : IUserService
}
userCollection = new(users);
CurrentUser = users.SingleOrDefault(user => user.IsSelected);
Current = users.SingleOrDefault(user => user.IsSelected);
}
return userCollection;
}
/// <inheritdoc/>
public Task<BindingUser?> CreateUserAsync(IDictionary<string, string> cookie)
{
return BindingUser.CreateAsync(cookie, userClient, userGameRoleClient, authClient);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> TryUpgradeUserByLoginTicketAsync(IDictionary<string, string> addition, CancellationToken token = default)
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
{
Must.NotNull(userCollection!);
if (addition.TryGetValue(CookieKeys.LOGIN_UID, out string? uid))
// 检查 uid 是否存在
if (cookie.TryGetUid(out string? uid))
{
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
// 检查 login ticket 是否存在
// 若存在则尝试升级至 stoken
await TryAddMultiTokenAsync(cookie, uid).ConfigureAwait(false);
// 检查 uid 对应用户是否存在
if (UserHelper.TryGetUserByUid(userCollection, uid, out BindingUser? userWithSameUid))
{
// Update user cookie here.
if (await userWithSameUid.TryUpgradeByLoginTicketAsync(addition, authClient, token))
// 检查 stoken 是否存在
if (cookie.ContainsSToken())
{
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(true, userWithSameUid.UserInfo?.Nickname ?? string.Empty);
// insert stoken directly
userWithSameUid.Cookie!.InsertSToken(uid, cookie);
return new(UserOptionResult.Upgraded, uid);
}
if (cookie.ContainsLTokenAndCookieToken())
{
UpdateUserCookie(cookie, userWithSameUid);
return new(UserOptionResult.Updated, uid);
}
}
}
return new(false, string.Empty);
}
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> TryUpgradeUserByStokenAsync(IDictionary<string, string> stoken)
{
Must.NotNull(userCollection!);
if (stoken.TryGetValue(CookieKeys.STUID, out string? uid))
{
// 查找是否有相同的uid
if (userCollection.SingleOrDefault(u => u.UserInfo!.Uid == uid) is BindingUser userWithSameUid)
else if (cookie.ContainsLTokenAndCookieToken())
{
// Update user cookie here.
userWithSameUid.AddStoken(stoken);
appDbContext.Users.Update(userWithSameUid.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(true, userWithSameUid.UserInfo?.Nickname ?? string.Empty);
return await TryCreateUserAndAddAsync(userCollection, cookie).ConfigureAwait(false);
}
}
return new(false, string.Empty);
return new(UserOptionResult.Incomplete, null!);
}
private async Task TryAddMultiTokenAsync(Cookie cookie, string uid)
{
if (cookie.TryGetLoginTicket(out string? loginTicket))
{
// get multitoken
Dictionary<string, string> multiToken = await authClient
.GetMultiTokenByLoginTicketAsync(loginTicket, uid, default)
.ConfigureAwait(false);
if (multiToken.Count >= 2)
{
cookie.InsertMultiToken(uid, multiToken);
cookie.RemoveLoginTicket();
}
}
}
private void UpdateUserCookie(Cookie cookie, BindingUser user)
{
user.Cookie = cookie;
appDbContext.Users.Update(user.Entity);
appDbContext.SaveChanges();
}
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
{
cookie.Trim();
BindingUser? newUser = await BindingUser.CreateAsync(cookie, userClient, bindingClient).ConfigureAwait(false);
if (newUser != null)
{
// Sync cache
await ThreadHelper.SwitchToMainThreadAsync();
users.Add(newUser);
// Sync database
appDbContext.Users.Add(newUser.Entity);
await appDbContext.SaveChangesAsync().ConfigureAwait(false);
return new(UserOptionResult.Added, newUser.UserInfo!.Uid);
}
else
{
return new(UserOptionResult.Invalid, null!);
}
}
}

View File

@@ -44,8 +44,6 @@ public sealed partial class AchievementImportDialog : ContentDialog
/// <returns>导入选项</returns>
public async Task<ValueResult<bool, ImportStrategy>> GetImportStrategyAsync()
{
//await ThreadHelper.SwitchToMainThreadAsync();
ContentDialogResult result = await ShowAsync();
ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex;

View File

@@ -42,7 +42,6 @@ public sealed partial class GachaLogImportDialog : ContentDialog
/// <returns>是否导入</returns>
public async Task<bool> GetShouldImportAsync()
{
//await ThreadHelper.SwitchToMainThreadAsync();
return await ShowAsync() == ContentDialogResult.Primary;
}
}

View File

@@ -5,6 +5,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.View.Dialog;
@@ -13,7 +14,7 @@ namespace Snap.Hutao.View.Dialog;
/// </summary>
public sealed partial class UserAutoCookieDialog : ContentDialog
{
private IDictionary<string, string>? cookie;
private Cookie? cookie;
/// <summary>
/// 构造一个新的用户自动Cookie对话框
@@ -29,7 +30,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
/// 获取输入的Cookie
/// </summary>
/// <returns>输入的结果</returns>
public async Task<ValueResult<bool, IDictionary<string, string>>> GetInputCookieAsync()
public async Task<ValueResult<bool, Cookie>> GetInputCookieAsync()
{
ContentDialogResult result = await ShowAsync();
return new(result == ContentDialogResult.Primary && cookie != null, cookie!);
@@ -58,16 +59,10 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
{
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
{
try
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
cookie = cookies.ToDictionary(c => c.Name, c => c.Value);
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
}
catch (Exception)
{
}
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
cookie = Cookie.FromCoreWebView2Cookies(cookies);
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
}
}
}

View File

@@ -19,6 +19,15 @@ public sealed partial class TitleView : UserControl
InitializeComponent();
}
/// <summary>
/// 标题
/// </summary>
[SuppressMessage("", "CA1822")]
public string Title
{
get => $"胡桃 {Core.CoreEnvironment.Version}";
}
/// <summary>
/// 获取可拖动区域
/// </summary>
@@ -26,9 +35,4 @@ public sealed partial class TitleView : UserControl
{
get => DragableGrid;
}
public string Title
{
get => $"胡桃 {Core.CoreEnvironment.Version}";
}
}

View File

@@ -3,14 +3,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Model.Binding;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab;
using System.Collections.ObjectModel;
using Windows.ApplicationModel.DataTransfer;
namespace Snap.Hutao.ViewModel;
@@ -54,7 +55,7 @@ internal class UserViewModel : ObservableObject
{
if (SetProperty(ref selectedUser, value))
{
userService.CurrentUser = value;
userService.Current = value;
}
}
}
@@ -75,7 +76,7 @@ internal class UserViewModel : ObservableObject
public ICommand AddUserCommand { get; }
/// <summary>
/// 升级到Stoken命令
/// 登录米哈游通行证升级到Stoken命令
/// </summary>
public ICommand UpgradeToStokenCommand { get; }
@@ -89,51 +90,10 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ICommand CopyCookieCommand { get; }
private static (bool Valid, bool Upgrade) TryValidateCookie(IDictionary<string, string> map, out IDictionary<string, string> cookie)
{
int validFlag = 4;
int stokenFlag = 2;
cookie = new SortedDictionary<string, string>();
foreach ((string key, string value) in map)
{
switch (key)
{
case CookieKeys.COOKIE_TOKEN:
case CookieKeys.ACCOUNT_ID:
case CookieKeys.LTOKEN:
case CookieKeys.LTUID:
{
validFlag--;
cookie.Add(key, value);
break;
}
case CookieKeys.STOKEN:
case CookieKeys.STUID:
{
stokenFlag--;
cookie.Add(key, value);
break;
}
case CookieKeys.LOGIN_TICKET:
case CookieKeys.LOGIN_UID:
{
cookie.Add(key, value);
break;
}
}
}
return (validFlag == 0, stokenFlag == 0);
}
private async Task OpenUIAsync()
{
Users = await userService.GetUserCollectionAsync().ConfigureAwait(true);
SelectedUser = userService.CurrentUser;
SelectedUser = userService.Current;
}
private async Task AddUserAsync()
@@ -145,50 +105,29 @@ internal class UserViewModel : ObservableObject
// User confirms the input
if (result.IsOk)
{
(bool valid, bool upgradable) = TryValidateCookie(User.MapCookie(result.Value), out IDictionary<string, string> cookie);
if (valid)
{
if (await userService.CreateUserAsync(cookie).ConfigureAwait(false) is User user)
{
switch (await userService.TryAddUserAsync(user, cookie[CookieKeys.ACCOUNT_ID]).ConfigureAwait(false))
{
case UserAddResult.Added:
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 添加成功");
break;
case UserAddResult.Updated:
infoBarService.Success($"用户 [{user.UserInfo!.Nickname}] 更新成功");
break;
case UserAddResult.AlreadyExists:
infoBarService.Information($"用户 [{user.UserInfo!.Nickname}] 已经存在");
break;
default:
throw Must.NeverHappen();
}
}
else
{
infoBarService.Warning("此 Cookie 无法获取用户信息,请重新输入");
}
}
else
{
if (upgradable)
{
(bool success, string nickname) = await userService.TryUpgradeUserByStokenAsync(cookie).ConfigureAwait(false);
Cookie cookie = Cookie.Parse(result.Value);
if (success)
{
infoBarService.Information($"用户 [{nickname}] 的 Stoken 更新成功");
}
else
{
infoBarService.Warning($"未找到匹配的可升级用户");
}
}
else
{
infoBarService.Warning("提供的文本不是正确的 Cookie ,请重新输入");
}
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
switch (optionResult)
{
case UserOptionResult.Added:
infoBarService.Success($"用户 [{uid}] 添加成功");
break;
case UserOptionResult.Incomplete:
infoBarService.Information($"此 Cookie 不完整,操作失败");
break;
case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无法,操作失败");
break;
case UserOptionResult.Updated:
infoBarService.Success($"用户 [{uid}] 更新成功");
break;
case UserOptionResult.Upgraded:
infoBarService.Information($"用户 [{uid}] 升级成功");
break;
default:
throw Must.NeverHappen();
}
}
}
@@ -197,15 +136,16 @@ internal class UserViewModel : ObservableObject
{
// Get cookie from user input
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
(bool isOk, IDictionary<string, string> addition) = await new UserAutoCookieDialog(mainWindow).GetInputCookieAsync().ConfigureAwait(false);
(bool isOk, Cookie addition) = await new UserAutoCookieDialog(mainWindow).GetInputCookieAsync().ConfigureAwait(false);
// User confirms the input
if (isOk)
{
(bool isUpgraded, string nickname) = await userService.TryUpgradeUserByLoginTicketAsync(addition).ConfigureAwait(false);
if (isUpgraded)
(UserOptionResult result, string nickname) = await userService.ProcessInputCookieAsync(addition).ConfigureAwait(false);
if (result == UserOptionResult.Upgraded)
{
infoBarService.Information($"用户 [{nickname}] 的 Cookie 已成功添加 Stoken");
infoBarService.Information($"用户 [{nickname}] 的 Cookie 升级成功");
}
else
{
@@ -228,10 +168,7 @@ internal class UserViewModel : ObservableObject
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
try
{
DataPackage content = new();
content.SetText(Must.NotNull(user.Cookie!));
Clipboard.SetContent(content);
Clipboard.Flush();
Clipboard.SetText(user.Cookie?.ToString() ?? string.Empty);
infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功");
}
catch (Exception e)

View File

@@ -0,0 +1,216 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Extension;
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// 封装了米哈游的Cookie
/// </summary>
public partial class Cookie
{
private readonly SortedDictionary<string, string> inner;
/// <summary>
/// 构造一个空白的Cookie
/// </summary>
public Cookie()
: this(new())
{
}
/// <summary>
/// 构造一个新的Cookie
/// </summary>
/// <param name="dict">源</param>
private Cookie(SortedDictionary<string, string> dict)
{
inner = dict;
}
/// <summary>
/// 解析Cookie字符串
/// </summary>
/// <param name="cookieString">cookie字符串</param>
/// <returns>新的Cookie对象</returns>
public static Cookie Parse(string cookieString)
{
SortedDictionary<string, string> cookieMap = new();
string[] values = cookieString.TrimEnd(';').Split(';');
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
string name = parts[0].Trim();
string value = parts.Length == 1 ? string.Empty : parts[1].Trim();
cookieMap.Add(name, value);
}
return new(cookieMap);
}
public static Cookie FromCoreWebView2Cookies(IReadOnlyList<CoreWebView2Cookie> webView2Cookies)
{
SortedDictionary<string, string> cookieMap = new();
foreach (CoreWebView2Cookie cookie in webView2Cookies)
{
cookieMap.Add(cookie.Name, cookie.Value);
}
return new(cookieMap);
}
/// <summary>
/// 存在 LoginTicket
/// </summary>
/// <returns>是否存在</returns>
public bool ContainsLoginTicket()
{
return inner.ContainsKey(LOGIN_TICKET);
}
/// <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="uid">uid</param>
/// <param name="multiToken">tokens</param>
public void InsertMultiToken(string uid, Dictionary<string, string> multiToken)
{
inner[STUID] = uid;
inner[STOKEN] = multiToken[STOKEN];
inner[LTUID] = uid;
inner[LTOKEN] = multiToken[LTOKEN];
}
/// <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>
/// 移除 LoginTicket
/// </summary>
public void RemoveLoginTicket()
{
inner.Remove(LOGIN_TICKET);
inner.Remove(LOGIN_UID);
}
/// <summary>
/// 移除无效的键
/// </summary>
public void Trim()
{
foreach (string key in inner.Keys.ToList())
{
if (key == ACCOUNT_ID
|| key == COOKIE_TOKEN
|| key == LOGIN_UID
|| key == LOGIN_TICKET
|| key == LTUID
|| key == LTOKEN
|| key == STUID
|| key == STOKEN)
{
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);
}
}
if (uidCounter.Count > 0)
{
uid = uidCounter.MaxBy(kvp => kvp.Value).Key;
return true;
}
else
{
uid = null;
return false;
}
}
/// <summary>
/// 转换为Cookie的字符串表示
/// </summary>
/// <returns>Cookie的字符串表示</returns>
public override string ToString()
{
return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
}
/// <summary>
/// 键部分
/// </summary>
[SuppressMessage("", "SA1310")]
[SuppressMessage("", "SA1600")]
public partial class Cookie
{
public const string COOKIE_TOKEN = "cookie_token";
public const string ACCOUNT_ID = "account_id";
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 STOKEN = "stoken";
public const string STUID = "stuid";
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// Cookie的键
/// </summary>
[SuppressMessage("", "SA1310")]
[SuppressMessage("", "SA1600")]
internal static class CookieKeys
{
public const string ACCOUNT_ID = "account_id";
public const string COOKIE_TOKEN = "cookie_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 STOKEN = "stoken";
public const string STUID = "stuid";
}

View File

@@ -53,7 +53,7 @@ internal static class HttpClientExtensions
/// <returns>客户端</returns>
internal static HttpClient SetUser(this HttpClient httpClient, User user)
{
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie);
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie?.ToString() ?? string.Empty);
return httpClient;
}

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.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -46,7 +47,10 @@ internal class AuthClient
if (resp?.Data != null)
{
return resp.Data.List.ToDictionary(n => n.Name, n => n.Token);
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

@@ -26,6 +26,7 @@ internal class GameRecordClient
/// </summary>
/// <param name="httpClient">请求器</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public GameRecordClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
{
this.httpClient = httpClient;

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab;
/// <summary>
/// Uid Token 对
/// </summary>
public struct UidToken
{
/// <summary>
/// Uid
/// </summary>
public string Uid;
/// <summary>
/// Token
/// </summary>
public string Token;
/// <summary>
/// 构造一个新的 Uid Token 对
/// </summary>
/// <param name="uid">uid</param>
/// <param name="token">token</param>
public UidToken(string uid, string token)
{
Uid = uid;
Token = token;
}
}