This commit is contained in:
DismissedLight
2023-03-27 18:39:02 +08:00
parent 97cbe7cf55
commit f90b828bb4
16 changed files with 109 additions and 185 deletions

View File

@@ -4723,7 +4723,7 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 Cookie 操作 的本地化字符串。
/// 查找类似 米游社 的本地化字符串。
/// </summary>
internal static string ViewUserCookieOperation {
get {

View File

@@ -1639,7 +1639,7 @@
<value>工具</value>
</data>
<data name="ViewUserCookieOperation" xml:space="preserve">
<value>Cookie 操作</value>
<value>米游社</value>
</data>
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
<value>网页登录</value>

View File

@@ -109,9 +109,8 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
GameRecordClientOversea gameRecordClientOs = scope.ServiceProvider.GetRequiredService<GameRecordClientOversea>();
// TODO: Add this option to AppOptions
bool isSilentMode = appDbContext.Settings
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
.GetBoolean();

View File

@@ -35,7 +35,7 @@
HorizontalAlignment="Right"
InputScope="Number"
MaxLength="9"
PlaceholderText="Please fill in your User ID here"/>
PlaceholderText="{shcm:ResourceString Name=ViewPageLoginHoyoverseUserHint}"/>
<Button
Grid.Column="1"
Click="CookieButtonClick"

View File

@@ -61,7 +61,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
if (uid.Length != 9)
{
await ThreadHelper.SwitchToMainThreadAsync();
infoBarService.Information($"请在页面右上方的输入框处填写你的通行证 ID!");
infoBarService.Information(SH.ViewPageLoginHoyoverseUserHint);
return;
}
@@ -80,7 +80,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
}
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
Cookie hoyoLabCookie = Cookie.Parse($"stoken={multiTokenMap["stoken"]}; stuid={uid}");
Cookie hoyoLabCookie = Cookie.Parse($"{Cookie.STUID}={uid};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
// 处理 cookie 并添加用户
(UserOptionResult result, string nickname) = await Ioc.Default
@@ -100,16 +100,16 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
vm.SelectedUser = vm.Users.Single();
}
infoBarService.Success($"用户 [{nickname}] 添加成功");
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
break;
case UserOptionResult.Incomplete:
infoBarService.Information($"此 Cookie 不完整,操作失败");
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无效,操作失败");
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success($"用户 [{nickname}] 更新成功");
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
break;
default:
throw Must.NeverHappen();

View File

@@ -67,9 +67,10 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
Cookie stokenV1 = Cookie.Parse($"stuid={loginTicketCookie["login_uid"]};stoken={multiTokenMap["stoken"]}");
Cookie stokenV1 = Cookie.Parse($"{Cookie.STUID}={loginTicketCookie[Cookie.LOGIN_UID]};{Cookie.STOKEN}={multiTokenMap[Cookie.STOKEN]}");
Response<LoginResult> loginResultResponse = await Ioc.Default
.GetRequiredService<PassportClient2>()
.GetRequiredService<PassportClient>()
.LoginBySTokenAsync(stokenV1, token)
.ConfigureAwait(false);
@@ -87,6 +88,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
// TODO: Move these code somewhere else.
switch (result)
{
case UserOptionResult.Added:
@@ -97,16 +99,16 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
vm.SelectedUser = vm.Users.Single();
}
infoBarService.Success($"用户 [{nickname}] 添加成功");
infoBarService.Success(string.Format(SH.ViewModelUserAdded, nickname));
break;
case UserOptionResult.Incomplete:
infoBarService.Information($"此 Cookie 不完整,操作失败");
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information($"此 Cookie 无效,操作失败");
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success($"用户 [{nickname}] 更新成功");
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, nickname));
break;
default:
throw Must.NeverHappen();

View File

@@ -238,7 +238,7 @@
Icon="{shcm:FontIcon Glyph=&#xEB41;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
<AppBarButton
Command="{Binding AddOsUserCommand}"
Command="{Binding AddOverseaUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel>

View File

@@ -46,7 +46,7 @@ internal sealed class UserViewModel : ObservableObject
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
AddOsUserCommand = new AsyncRelayCommand(AddOsUserAsync);
AddOverseaUserCommand = new AsyncRelayCommand(AddOverseaUserAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
LoginHoyoverseUserCommand = new RelayCommand(LoginHoyoverseUser);
RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync);
@@ -92,7 +92,7 @@ internal sealed class UserViewModel : ObservableObject
/// <summary>
/// 添加国际服用户命令
/// </summary>
public ICommand AddOsUserCommand { get; }
public ICommand AddOverseaUserCommand { get; }
/// <summary>
/// 登录米游社命令
@@ -132,48 +132,17 @@ internal sealed class UserViewModel : ObservableObject
}
}
private async Task AddUserAsync()
private Task AddUserAsync()
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
// Get cookie from user input
ValueResult<bool, string> result = await new UserDialog().GetInputCookieAsync().ConfigureAwait(false);
// User confirms the input
if (result.IsOk)
{
Cookie cookie = Cookie.Parse(result.Value);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie).ConfigureAwait(false);
switch (optionResult)
{
case UserOptionResult.Added:
if (Users!.Count == 1)
{
await ThreadHelper.SwitchToMainThreadAsync();
SelectedUser = Users.Single();
}
infoBarService.Success(string.Format(SH.ViewModelUserAdded, uid));
break;
case UserOptionResult.Incomplete:
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, uid));
break;
default:
throw Must.NeverHappen();
}
}
return AddUserCoreAsync(false);
}
private async Task AddOsUserAsync()
private Task AddOverseaUserAsync()
{
return AddUserCoreAsync(true);
}
private async Task AddUserCoreAsync(bool isOversea)
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
@@ -186,7 +155,7 @@ internal sealed class UserViewModel : ObservableObject
{
Cookie cookie = Cookie.Parse(result.Value);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputOsCookieAsync(cookie).ConfigureAwait(false);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(cookie, isOversea).ConfigureAwait(false);
switch (optionResult)
{

View File

@@ -200,7 +200,7 @@ internal static class ApiOsEndpoints
/// <summary>
/// 计算器结果
/// </summary>
public const string CalculateOsCompute = $"{SgPublicApi}/event/calculateos/compute";
public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
/// <summary>
/// 计算器同步角色详情 size 20
@@ -208,7 +208,7 @@ internal static class ApiOsEndpoints
/// <param name="avatarId">角色Id</param>
/// <param name="uid">uid</param>
/// <returns>角色详情</returns>
public static string CalculateOsSyncAvatarDetail(AvatarId avatarId, PlayerUid uid)
public static string CalculateSyncAvatarDetail(AvatarId avatarId, PlayerUid uid)
{
return $"{SgPublicApi}/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&uid={uid.Value}&region={uid.Region}";
}
@@ -216,7 +216,7 @@ internal static class ApiOsEndpoints
/// <summary>
/// 计算器同步角色列表 size 20
/// </summary>
public const string CalculateOsSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
#endregion

View File

@@ -4,8 +4,10 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
using System.Net.Http.Json;
namespace Snap.Hutao.Web.Hoyolab.Passport;
@@ -13,6 +15,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
/// 通行证客户端
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class PassportClient
{
@@ -34,7 +37,7 @@ internal sealed class PassportClient
}
/// <summary>
/// 异步验证Ltoken
/// 异步验证 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
@@ -50,6 +53,28 @@ internal sealed class PassportClient
return Response.Response.DefaultIfNull(response);
}
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
private class Timestamp
{
[JsonPropertyName("t")]

View File

@@ -39,28 +39,6 @@ internal sealed class PassportClient2 : IPassportClient
/// <inheritdoc/>
public bool IsOversea => false;
/// <summary>
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.PostAsync(ApiEndpoints.AccountGetSTokenByOldToken, null, token)
.ConfigureAwait(false);
Response<LoginResult>? resp = await message.Content
.ReadFromJsonAsync<Response<LoginResult>>(options, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
/// <summary>
/// 异步获取 CookieToken
/// </summary>

View File

@@ -20,7 +20,7 @@ internal sealed class PassportClientOversea : IPassportClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<PassportClient> logger;
private readonly ILogger<PassportClientOversea> logger;
/// <summary>
/// 构造一个新的国际服通行证客户端
@@ -28,7 +28,7 @@ internal sealed class PassportClientOversea : IPassportClient
/// <param name="httpClient">http客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="logger">日志器</param>
public PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClient> logger)
public PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClientOversea> logger)
{
this.httpClient = httpClient;
this.options = options;

View File

@@ -40,7 +40,7 @@ internal sealed class ResourceClient
/// <returns>游戏资源</returns>
public async Task<Response<GameResource>> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
string url = scheme.LauncherId == "10"
string url = scheme.IsOversea
? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
: ApiEndpoints.SdkStaticLauncherResource(scheme);

View File

@@ -55,7 +55,7 @@ internal sealed class BindingClient2
/// <summary>
/// 异步生成祈愿验证密钥
/// 需要stoken
/// 需要 SToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="data">提交数据</param>

View File

@@ -43,24 +43,19 @@ internal sealed class CalculateClient
[ApiInformation(Cookie = CookieType.Cookie)]
public async Task<Response<Consumption>> ComputeAsync(Model.Entity.User user, AvatarPromotionDelta delta, CancellationToken token = default)
{
Response<Consumption>? resp;
string referer = user.IsOversea
? ApiOsEndpoints.ActHoyolabReferer
: ApiEndpoints.WebStaticMihoyoReferer;
if (user.IsOversea)
{
resp = await httpClient
string url = user.IsOversea
? ApiOsEndpoints.CalculateCompute
: ApiEndpoints.CalculateCompute;
Response<Consumption>? resp = await httpClient
.SetUser(user, CookieType.Cookie)
.SetReferer(ApiOsEndpoints.ActHoyolabReferer)
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiOsEndpoints.CalculateOsCompute, delta, options, logger, token)
.SetReferer(referer)
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(url, delta, options, logger, token)
.ConfigureAwait(false);
}
else
{
resp = await httpClient
.SetUser(user, CookieType.Cookie)
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiEndpoints.CalculateCompute, delta, options, logger, token)
.ConfigureAwait(false);
}
return Response.Response.DefaultIfNull(resp);
}
@@ -80,28 +75,22 @@ internal sealed class CalculateClient
Response<ListWrapper<Avatar>>? resp;
// 根据 uid 所属服务器选择 referer 与 api
string referer;
string endpoint;
string referer = userAndUid.User.IsOversea
? ApiOsEndpoints.ActHoyolabReferer
: ApiEndpoints.WebStaticMihoyoReferer;
string url = userAndUid.User.IsOversea
? ApiOsEndpoints.CalculateSyncAvatarList
: ApiEndpoints.CalculateSyncAvatarList;
if (userAndUid.User.IsOversea)
{
referer = ApiOsEndpoints.ActHoyolabReferer;
endpoint = ApiOsEndpoints.CalculateOsSyncAvatarList;
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
}
else
{
referer = ApiEndpoints.WebStaticMihoyoReferer;
endpoint = ApiEndpoints.CalculateSyncAvatarList;
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
}
httpClient
.SetReferer(referer)
.SetUser(userAndUid.User, CookieType.CookieToken);
do
{
filter.Page = currentPage++;
resp = await httpClient
.SetReferer(referer)
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(endpoint, filter, options, logger, token)
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(url, filter, options, logger, token)
.ConfigureAwait(false);
if (resp != null && resp.IsOk())
@@ -130,21 +119,14 @@ internal sealed class CalculateClient
/// <returns>角色详情</returns>
public async Task<Response<AvatarDetail>> GetAvatarDetailAsync(UserAndUid userAndUid, Avatar avatar, CancellationToken token = default)
{
Response<AvatarDetail>? resp;
if (!userAndUid.User.IsOversea)
{
resp = await httpClient
string url = userAndUid.User.IsOversea
? ApiOsEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value)
: ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value);
Response<AvatarDetail>? resp = await httpClient
.SetUser(userAndUid.User, CookieType.CookieToken)
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(url, options, logger, token)
.ConfigureAwait(false);
}
else
{
resp = await httpClient
.SetUser(userAndUid.User, CookieType.CookieToken)
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiOsEndpoints.CalculateOsSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
.ConfigureAwait(false);
}
return Response.Response.DefaultIfNull(resp);
}

View File

@@ -21,9 +21,8 @@ namespace Snap.Hutao.Web.Hutao;
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class HomaSpiralAbyssClient
{
private readonly IServiceProvider serviceProvider;
private readonly HttpClient httpClient;
private readonly GameRecordClient gameRecordClient;
private readonly GameRecordClientOversea gameRecordClientOs;
private readonly JsonSerializerOptions options;
private readonly ILogger<HomaSpiralAbyssClient> logger;
@@ -31,16 +30,14 @@ internal sealed class HomaSpiralAbyssClient
/// 构造一个新的胡桃API客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="gameRecordClient">游戏记录客户端</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, GameRecordClientOversea gameRecordClientOs, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
/// <param name="serviceProvider">服务提供器</param>
public HomaSpiralAbyssClient(HttpClient httpClient, IServiceProvider serviceProvider)
{
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
logger = serviceProvider.GetRequiredService<ILogger<HomaSpiralAbyssClient>>();
this.serviceProvider = serviceProvider;
this.httpClient = httpClient;
this.gameRecordClient = gameRecordClient;
this.gameRecordClientOs = gameRecordClientOs;
this.options = options;
this.logger = logger;
}
/// <summary>
@@ -188,55 +185,27 @@ internal sealed class HomaSpiralAbyssClient
/// <returns>玩家记录</returns>
public async Task<SimpleRecord?> GetPlayerRecordAsync(UserAndUid userAndUid, CancellationToken token = default)
{
if (userAndUid.User.IsOversea)
{
// for oversea server
Response<PlayerInfo> playerInfoResponse = await gameRecordClientOs
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
Response<PlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
if (playerInfoResponse.IsOk())
{
Response<CharacterWrapper> charactersResponse = await gameRecordClientOs
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
if (charactersResponse.IsOk())
{
Response<SpiralAbyss> spiralAbyssResponse = await gameRecordClientOs
.GetSpiralAbyssAsync(userAndUid, SpiralAbyssSchedule.Current, token)
.ConfigureAwait(false);
if (spiralAbyssResponse.IsOk())
{
return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data);
}
}
}
}
else
if (playerInfoResponse.IsOk())
{
// for cn server
Response<PlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
if (playerInfoResponse.IsOk())
if (charactersResponse.IsOk())
{
Response<CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
Response<SpiralAbyss> spiralAbyssResponse = await gameRecordClient
.GetSpiralAbyssAsync(userAndUid, SpiralAbyssSchedule.Current, token)
.ConfigureAwait(false);
if (charactersResponse.IsOk())
if (spiralAbyssResponse.IsOk())
{
Response<SpiralAbyss> spiralAbyssResponse = await gameRecordClient
.GetSpiralAbyssAsync(userAndUid, SpiralAbyssSchedule.Current, token)
.ConfigureAwait(false);
if (spiralAbyssResponse.IsOk())
{
return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data);
}
return new(userAndUid.Uid.Value, charactersResponse.Data.Avatars, spiralAbyssResponse.Data);
}
}
}