From 94ef94a6213ceb8e61aafb0eb8a5b1cabd83e2f5 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Thu, 29 Sep 2022 16:13:47 +0800 Subject: [PATCH] impl --- .../Context/Database/AppDbContext.cs | 4 +- .../Control/Markup/I18NExtension.cs | 2 +- .../Snap.Hutao/Core/Database/DbCurrent.cs | 77 +++++++ .../Core/IO/DataTransfer/Clipboard.cs | 12 + .../Extension/EnumerableExtensions.cs | 31 ++- .../Snap.Hutao/Model/Binding/User.cs | 150 +----------- .../Entity/Configuration/UserConfiguration.cs | 24 ++ .../Snap.Hutao/Model/Entity/User.cs | 5 +- .../Snap.Hutao/Model/Intrinsic/WeaponType.cs | 1 + .../Abstraction/ISummaryItemSource.cs | 27 +++ .../Model/Metadata/Avatar/Avatar.cs | 2 +- .../Model/Metadata/Weapon/Weapon.cs | 2 +- .../Snap.Hutao/Package.appxmanifest | 2 +- .../Factory/GachaStatisticsExtensions.cs | 54 +++-- .../Factory/GachaStatisticsFactory.cs | 12 +- .../GachaLog/Factory/HistoryWishBuilder.cs | 1 + .../Factory/TypedWishSummaryBuilder.cs | 95 +------- .../UrlProvider/GachaLogUrlStokenProvider.cs | 8 +- .../Snap.Hutao/Service/User/IUserService.cs | 50 ++-- .../Snap.Hutao/Service/User/UserHelper.cs | 34 +++ .../{UserAddResult.cs => UserOptionResult.cs} | 18 +- .../Snap.Hutao/Service/User/UserService.cs | 162 +++++++------ .../Dialog/AchievementImportDialog.xaml.cs | 2 - .../View/Dialog/GachaLogImportDialog.xaml.cs | 1 - .../View/Dialog/UserAutoCookieDialog.xaml.cs | 19 +- .../Snap.Hutao/View/TitleView.xaml.cs | 14 +- .../Snap.Hutao/ViewModel/UserViewModel.cs | 129 +++-------- .../Snap.Hutao/Web/Hoyolab/Cookie.cs | 216 ++++++++++++++++++ .../Snap.Hutao/Web/Hoyolab/CookieKeys.cs | 21 -- .../Web/Hoyolab/HttpClientExtensions.cs | 2 +- .../Web/Hoyolab/Takumi/Auth/AuthClient.cs | 6 +- .../Takumi/GameRecord/GameRecordClient.cs | 1 + .../Snap.Hutao/Web/Hoyolab/UidToken.cs | 31 +++ 33 files changed, 672 insertions(+), 543 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/UserConfiguration.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemSource.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/User/UserHelper.cs rename src/Snap.Hutao/Snap.Hutao/Service/User/{UserAddResult.cs => UserOptionResult.cs} (56%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs b/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs index bde6842b..c0ffb9fd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Context/Database/AppDbContext.cs @@ -69,6 +69,8 @@ public class AppDbContext : DbContext /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfiguration(new AvatarInfoConfiguration()); + modelBuilder + .ApplyConfiguration(new AvatarInfoConfiguration()) + .ApplyConfiguration(new UserConfiguration()); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs index 653c4d2e..0a8d0a09 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs @@ -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)!; } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs index 3d7fc8c1..4fde20be 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Database/DbCurrent.cs @@ -70,6 +70,83 @@ internal class DbCurrent dbContext.SaveChanges(); } + messenger.Send(message); + } + } +} + +/// +/// 数据库当前项 +/// 简化对数据库中选中项的管理 +/// +/// 绑定类型 +/// 实体的类型 +/// 消息的类型 +[SuppressMessage("", "SA1402")] +internal class DbCurrent + where TObservable : class + where TEntity : class, ISelectable + where TMessage : Message.ValueChangedMessage +{ + private readonly DbContext dbContext; + private readonly DbSet dbSet; + private readonly IMessenger messenger; + private readonly Func selector; + + private TObservable? current; + + /// + /// 构造一个新的数据库当前项 + /// + /// 数据库上下文 + /// 数据集 + /// 选择器 + /// 消息器 + public DbCurrent(DbContext dbContext, DbSet dbSet, Func selector, IMessenger messenger) + { + this.dbContext = dbContext; + this.dbSet = dbSet; + this.selector = selector; + this.messenger = messenger; + } + + /// + /// 当前选中的项 + /// + 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); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs index fa9ba271..bd7bf0a7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/DataTransfer/Clipboard.cs @@ -25,4 +25,16 @@ internal static class Clipboard string json = await view.GetTextAsync(); return JsonSerializer.Deserialize(json, options); } + + /// + /// 设置文本 + /// + /// 文本 + public static void SetText(string text) + { + DataPackage content = new(); + content.SetText(text); + Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(content); + Windows.ApplicationModel.DataTransfer.Clipboard.Flush(); + } } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs index 93ea94fc..e4795d9b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs @@ -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 } /// - /// 获取值或默认值 + /// 增加计数 /// /// 键类型 - /// 值类型 - /// 字典 + /// 字典 /// 键 - /// 默认值 - /// 结果值 - public static TValue? GetValueOrDefault(this IDictionary dictionary, TKey key, TValue? defaultValue = default) + public static void Increase(this Dictionary dict, TKey key) where TKey : notnull { - if (dictionary.TryGetValue(key, out TValue? value)) + ++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _); + } + + /// + /// 增加计数 + /// + /// 键类型 + /// 字典 + /// 键 + /// 是否存在键值 + public static bool TryIncrease(this Dictionary 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; } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs index 2a51b012..5fcd09d8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User.cs @@ -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 } /// - public string? Cookie + public Cookie Cookie { get => inner.Cookie; set => inner.Cookie = value; @@ -72,27 +71,6 @@ public class User : Observable /// public bool IsInitialized { get => isInitialized; } - /// - /// 将cookie的字符串形式转换为字典 - /// - /// cookie的字符串形式 - /// 包含cookie信息的字典 - public static IDictionary MapCookie(string cookie) - { - SortedDictionary 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; - } - /// /// 从数据库恢复用户 /// @@ -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; } /// - /// 初始化用户 + /// 创建并初始化用户 /// /// cookie /// 用户客户端 /// 角色客户端 - /// 授权客户端 /// 取消令牌 - /// 用户是否初始化完成,若Cookie失效会返回 + /// 用户是否初始化完成,若Cookie失效会返回 internal static async Task CreateAsync( - IDictionary 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; } - /// - /// 尝试升级到Stoken - /// - /// 额外的token - /// 验证客户端 - /// 取消令牌 - /// 是否升级成功 - internal async Task TryUpgradeByLoginTicketAsync(IDictionary addition, AuthClient authClient, CancellationToken token) - { - IDictionary 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; - } - - /// - /// 添加 Stoken - /// - /// 额外的cookie - internal void AddStoken(IDictionary addition) - { - IDictionary 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 cookie) - { - return string.Join(';', cookie.Select(kvp => $"{kvp.Key}={kvp.Value}")); - } - - private static async Task TryRequestStokenAndAddToCookieAsync(IDictionary 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 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 ResumeInternalAsync( + private async Task 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 CreateInternalAsync( - IDictionary 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) diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/UserConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/UserConfiguration.cs new file mode 100644 index 00000000..69964c1e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Configuration/UserConfiguration.cs @@ -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; + +/// +/// 用户配置 +/// +internal class UserConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.Property(e => e.Cookie) + .HasColumnType("TEXT") + .HasConversion( + e => e == null ? string.Empty : e.ToString(), + e => Cookie.Parse(e)); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 8542f0dc..ca000fda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -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 /// /// 用户的Cookie /// - public string? Cookie { get; set; } + public Cookie Cookie { get; set; } = default!; /// /// 创建一个新的用户 /// /// cookie /// 新创建的用户 - public static User Create(string cookie) + public static User Create(Cookie cookie) { return new() { Cookie = cookie }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs index ebe6fd8c..0f9b2a3b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/WeaponType.cs @@ -21,6 +21,7 @@ public enum WeaponType WEAPON_SWORD_ONE_HAND = 1, #region Not Used + /// /// ? /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemSource.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemSource.cs new file mode 100644 index 00000000..b843309a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Abstraction/ISummaryItemSource.cs @@ -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; + +/// +/// 指示该类为简述统计物品的源 +/// +public interface ISummaryItemSource +{ + /// + /// 星级 + /// + ItemQuality Quality { get; } + + /// + /// 转换到简述统计物品 + /// + /// 距上个五星 + /// 时间 + /// 是否为Up物品 + /// 简述统计物品 + SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs index fc23883c..843c571a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs @@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Avatar; /// /// 角色 /// -public class Avatar : IStatisticsItemSource, INameQuality +public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality { /// /// Id diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs index 87f070bb..46f613d0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Weapon/Weapon.cs @@ -12,7 +12,7 @@ namespace Snap.Hutao.Model.Metadata.Weapon; /// /// 武器 /// -public class Weapon : IStatisticsItemSource, INameQuality +public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality { /// /// Id diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index a4cf5ef8..2c0b5fda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.1.5.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs index f9de6fcf..44d5c75c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs @@ -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 } /// - /// 增加计数 + /// 完成添加 /// - /// 键类型 - /// 字典 - /// 键 - public static void Increase(this Dictionary dict, TKey key) - where TKey : notnull + /// 简述物品列表 + public static void CompleteAdding(this List summaryItems) { - ++CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _); - } + // we can't trust first item's prev state. + bool isPreviousUp = true; - /// - /// 增加计数 - /// - /// 键类型 - /// 字典 - /// 键 - /// 是否存在键值 - public static bool TryIncrease(this Dictionary 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(); } /// @@ -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 first = new(codes, 0, 5); + Span second = new(codes, 5, 5); + Span third = new(codes, 10, 5); + Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average()); + return color; + } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs index f1852372..90c5f8d7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -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 { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs index 3337865e..c8e53cfe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/HistoryWishBuilder.cs @@ -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; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs index cc3c41f2..bbccd898 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/TypedWishSummaryBuilder.cs @@ -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 /// 追踪物品 /// /// 祈愿物品 - /// 对应角色 + /// 对应武器 /// 是否为Up物品 - 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; - } - } - } - } - - /// - /// 追踪物品 - /// - /// 祈愿物品 - /// 对应武器 - /// 是否为Up物品 - 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 /// 类型化祈愿统计信息 public TypedWishSummary ToTypedWishSummary() { - CompleteSummaryItems(summaryItemCache); + summaryItemCache.CompleteAdding(); double totalCountDouble = totalCountTracker; return new() @@ -209,37 +157,6 @@ internal class TypedWishSummaryBuilder }; } - private static void CompleteSummaryItems(List 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 first = new(codes, 0, 5); - Span second = new(codes, 5, 5); - Span 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) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs index 88674a7d..9994545f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/UrlProvider/GachaLogUrlStokenProvider.cs @@ -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 /// public async Task> 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"); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index 9ada459a..bd189307 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs @@ -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; /// /// 用户服务 @@ -16,52 +16,28 @@ public interface IUserService /// /// 获取或设置当前用户 /// - User? CurrentUser { get; set; } + BindingUser? Current { get; set; } /// + /// 初始化用户服务及所有用户 /// 异步获取同步的用户信息集合 /// 对集合的操作应通过服务抽象完成 /// 此操作不能取消 /// - /// 准备完成的用户信息枚举 - Task> GetUserCollectionAsync(); + /// 准备完成的用户信息集合 + Task> GetUserCollectionAsync(); /// - /// 异步添加用户 - /// 通常用户是未初始化的 + /// 尝试异步处理输入的Cookie /// - /// 待添加的用户 - /// 用户的米游社UID,用于检查是否包含重复的用户 - /// 用户初始化是否成功 - Task TryAddUserAsync(User user, string uid); - - /// - /// 尝试使用 login_ticket 升级用户 - /// - /// 额外的Cookie - /// 取消令牌 - /// 是否升级成功 - Task> TryUpgradeUserByLoginTicketAsync(IDictionary addiition, CancellationToken token = default); - - /// - /// 尝试使用 Stoken 升级用户 - /// - /// stoken - /// 是否升级成功 - Task> TryUpgradeUserByStokenAsync(IDictionary stoken); + /// Cookie + /// 处理的结果 + Task> ProcessInputCookieAsync(Cookie cookie); /// /// 异步移除用户 /// /// 待移除的用户 /// 任务 - Task RemoveUserAsync(User user); - - /// - /// 创建一个新的绑定用户 - /// 若存在 login_ticket 与 login_uid 则 自动获取 stoken - /// - /// cookie的字符串形式 - /// 新的绑定用户 - Task CreateUserAsync(IDictionary cookie); -} + Task RemoveUserAsync(BindingUser user); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserHelper.cs new file mode 100644 index 00000000..f416ea16 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserHelper.cs @@ -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; + +/// +/// 用户帮助类 +/// +internal static class UserHelper +{ + /// + /// 尝试获取用户 + /// + /// 待查找的用户集合 + /// uid + /// 用户 + /// 是否存在用户 + public static bool TryGetUserByUid(ObservableCollection users, string uid, [NotNullWhen(true)] out BindingUser? user) + { + user = users.SingleOrDefault(u => u.UserInfo!.Uid == uid); + + return user != null; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserAddResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs similarity index 56% rename from src/Snap.Hutao/Snap.Hutao/Service/User/UserAddResult.cs rename to src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs index 715d3108..c96f7ca5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserAddResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserOptionResult.cs @@ -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; /// /// 用户添加操作结果 /// -public enum UserAddResult +public enum UserOptionResult { /// /// 添加成功 /// Added, + /// + /// Cookie不完整 + /// + Incomplete, + + /// + /// Cookie信息已经失效 + /// + Invalid, + /// /// 用户的Cookie成功更新 /// Updated, /// - /// 已经存在该用户 + /// 升级到Stoken /// - AlreadyExists, + Upgraded, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index b423a7b1..b0b09917 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -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; /// /// 用户服务 @@ -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 /// /// 应用程序数据库上下文 /// 用户客户端 - /// 角色客户端 + /// 角色客户端 /// 验证客户端 /// 消息器 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; } /// - public BindingUser? CurrentUser + public BindingUser? Current { get => currentUser; set @@ -90,45 +89,6 @@ internal class UserService : IUserService } } - /// - public async Task 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; - } - } - /// 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; } /// - public Task CreateUserAsync(IDictionary cookie) - { - return BindingUser.CreateAsync(cookie, userClient, userGameRoleClient, authClient); - } - - /// - public async Task> TryUpgradeUserByLoginTicketAsync(IDictionary addition, CancellationToken token = default) + public async Task> 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); - } - - /// - public async Task> TryUpgradeUserByStokenAsync(IDictionary 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 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> TryCreateUserAndAddAsync(ObservableCollection 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!); + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs index 421ff476..1398547d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/AchievementImportDialog.xaml.cs @@ -44,8 +44,6 @@ public sealed partial class AchievementImportDialog : ContentDialog /// 导入选项 public async Task> GetImportStrategyAsync() { - //await ThreadHelper.SwitchToMainThreadAsync(); - ContentDialogResult result = await ShowAsync(); ImportStrategy strategy = (ImportStrategy)ImportModeSelector.SelectedIndex; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs index 841bf276..fa209f5a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/GachaLogImportDialog.xaml.cs @@ -42,7 +42,6 @@ public sealed partial class GachaLogImportDialog : ContentDialog /// 是否导入 public async Task GetShouldImportAsync() { - //await ThreadHelper.SwitchToMainThreadAsync(); return await ShowAsync() == ContentDialogResult.Primary; } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs index 947da7e4..4b12b40d 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserAutoCookieDialog.xaml.cs @@ -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; /// public sealed partial class UserAutoCookieDialog : ContentDialog { - private IDictionary? cookie; + private Cookie? cookie; /// /// 构造一个新的用户自动Cookie对话框 @@ -29,7 +30,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog /// 获取输入的Cookie /// /// 输入的结果 - public async Task>> GetInputCookieAsync() + public async Task> 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 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 cookies = await manager.GetCookiesAsync("https://user.mihoyo.com"); + cookie = Cookie.FromCoreWebView2Cookies(cookies); + WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged; } } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs index d81f8243..2a35e584 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs @@ -19,6 +19,15 @@ public sealed partial class TitleView : UserControl InitializeComponent(); } + /// + /// 标题 + /// + [SuppressMessage("", "CA1822")] + public string Title + { + get => $"胡桃 {Core.CoreEnvironment.Version}"; + } + /// /// 获取可拖动区域 /// @@ -26,9 +35,4 @@ public sealed partial class TitleView : UserControl { get => DragableGrid; } - - public string Title - { - get => $"胡桃 {Core.CoreEnvironment.Version}"; - } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs index df2e2851..7145c62b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs @@ -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; } /// - /// 升级到Stoken命令 + /// 登录米哈游通行证升级到Stoken命令 /// public ICommand UpgradeToStokenCommand { get; } @@ -89,51 +90,10 @@ internal class UserViewModel : ObservableObject /// public ICommand CopyCookieCommand { get; } - private static (bool Valid, bool Upgrade) TryValidateCookie(IDictionary map, out IDictionary cookie) - { - int validFlag = 4; - int stokenFlag = 2; - - cookie = new SortedDictionary(); - - 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 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(); - (bool isOk, IDictionary 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(); 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) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs new file mode 100644 index 00000000..d80bc00b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs @@ -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; + +/// +/// 封装了米哈游的Cookie +/// +public partial class Cookie +{ + private readonly SortedDictionary inner; + + /// + /// 构造一个空白的Cookie + /// + public Cookie() + : this(new()) + { + } + + /// + /// 构造一个新的Cookie + /// + /// 源 + private Cookie(SortedDictionary dict) + { + inner = dict; + } + + /// + /// 解析Cookie字符串 + /// + /// cookie字符串 + /// 新的Cookie对象 + public static Cookie Parse(string cookieString) + { + SortedDictionary 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 webView2Cookies) + { + SortedDictionary cookieMap = new(); + + foreach (CoreWebView2Cookie cookie in webView2Cookies) + { + cookieMap.Add(cookie.Name, cookie.Value); + } + + return new(cookieMap); + } + + /// + /// 存在 LoginTicket + /// + /// 是否存在 + public bool ContainsLoginTicket() + { + return inner.ContainsKey(LOGIN_TICKET); + } + + /// + /// 存在 LToken 与 CookieToken + /// + /// 是否存在 + public bool ContainsLTokenAndCookieToken() + { + return inner.ContainsKey(LTOKEN) && inner.ContainsKey(COOKIE_TOKEN); + } + + /// + /// 存在 SToken + /// + /// 是否存在 + public bool ContainsSToken() + { + return inner.ContainsKey(STOKEN); + } + + /// + /// 插入Stoken + /// + /// uid + /// tokens + public void InsertMultiToken(string uid, Dictionary multiToken) + { + inner[STUID] = uid; + inner[STOKEN] = multiToken[STOKEN]; + + inner[LTUID] = uid; + inner[LTOKEN] = multiToken[LTOKEN]; + } + + /// + /// 插入 Stoken + /// + /// stuid + /// cookie + public void InsertSToken(string stuid, Cookie cookie) + { + inner[STUID] = stuid; + inner[STOKEN] = cookie.inner[STOKEN]; + } + + /// + /// 移除 LoginTicket + /// + public void RemoveLoginTicket() + { + inner.Remove(LOGIN_TICKET); + inner.Remove(LOGIN_UID); + } + + /// + /// 移除无效的键 + /// + 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); + } + } + } + + /// + 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 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; + } + } + + /// + /// 转换为Cookie的字符串表示 + /// + /// Cookie的字符串表示 + public override string ToString() + { + return string.Join(';', inner.Select(kvp => $"{kvp.Key}={kvp.Value}")); + } +} + +/// +/// 键部分 +/// +[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"; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs deleted file mode 100644 index 563c4b84..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/CookieKeys.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab; - -/// -/// Cookie的键 -/// -[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"; -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs index 7832256d..9f5ac6b3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HttpClientExtensions.cs @@ -53,7 +53,7 @@ internal static class HttpClientExtensions /// 客户端 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; } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs index 32e085dd..5dc01ba2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Auth/AuthClient.cs @@ -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 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(); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs index 1542935f..36c902b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClient.cs @@ -26,6 +26,7 @@ internal class GameRecordClient /// /// 请求器 /// json序列化选项 + /// 日志器 public GameRecordClient(HttpClient httpClient, JsonSerializerOptions options, ILogger logger) { this.httpClient = httpClient; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs new file mode 100644 index 00000000..c4bd216f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/UidToken.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab; + +/// +/// Uid Token 对 +/// +public struct UidToken +{ + /// + /// Uid + /// + public string Uid; + + /// + /// Token + /// + public string Token; + + /// + /// 构造一个新的 Uid Token 对 + /// + /// uid + /// token + public UidToken(string uid, string token) + { + Uid = uid; + Token = token; + } +} \ No newline at end of file