support sign in bbs

This commit is contained in:
DismissedLight
2022-11-27 16:00:15 +08:00
parent 88037049f3
commit 8f273e69b5
35 changed files with 767 additions and 617 deletions

View File

@@ -32,4 +32,4 @@ internal class InvokeCommandOnUnloadedBehavior : BehaviorBase<UIElement>
base.OnDetaching();
}
}
}

View File

@@ -20,6 +20,11 @@ internal static class CoreEnvironment
/// </summary>
public const string HoyolabUA = $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社移动端请求UA
/// </summary>
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36 miHoYoBBS/{HoyolabXrpcVersion}";
/// <summary>
/// 米游社 Rpc 版本
/// </summary>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
using System.Runtime.InteropServices;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
namespace Snap.Hutao.Core;
@@ -39,5 +40,9 @@ internal static class TaskSchedulerHelper
{
return false;
}
catch (COMException)
{
return false;
}
}
}

View File

@@ -38,19 +38,6 @@ public static class Must
}
}
/// <summary>
/// 任务异常
/// </summary>
/// <typeparam name="T">任务结果类型</typeparam>
/// <param name="message">异常消息</param>
/// <returns>异常的任务</returns>
[SuppressMessage("", "VSTHRD200")]
public static Task<T> Fault<T>(string message)
{
InvalidOperationException exception = new(message);
return Task.FromException<T>(exception);
}
/// <summary>
/// Unconditionally throws an <see cref="NotSupportedException"/>.
/// </summary>

View File

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

View File

@@ -109,7 +109,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
// cache
await ThreadHelper.SwitchToMainThreadAsync();
entries?.Single(e => e.UserId == entry.UserId && e.Uid == entry.Uid).UpdateDailyNote(dailyNote);
entries?.SingleOrDefault(e => e.UserId == entry.UserId && e.Uid == entry.Uid)?.UpdateDailyNote(dailyNote);
if (notify)
{

View File

@@ -22,5 +22,6 @@ internal interface IGameLocator : INamed
/// 路径应当包含启动器文件名称
/// </summary>
/// <returns>游戏启动器位置</returns>
[Obsolete("不应定位启动器位置")]
Task<ValueResult<bool, string>> LocateLauncherPathAsync();
}

View File

@@ -3,6 +3,8 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Xaml.Interactivity;
using Snap.Hutao.Control.Behavior;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service;

View File

@@ -1,13 +1,17 @@
<ContentDialog
<ContentDialog
x:Class="Snap.Hutao.View.Dialog.SignInWebViewDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Style="{StaticResource DefaultContentDialogStyle}">
Closed="OnContentDialogClosed"
Style="{StaticResource DefaultContentDialogStyle}"
Title="米游社每日签到"
PrimaryButtonText="完成"
DefaultButton="Primary">
<Grid Loaded="OnGridLoaded" Height="600" Width="380">
<WebView2 Name="WebView"/>
<Grid Loaded="OnGridLoaded">
<WebView2 Name="WebView" Height="456" Width="380"/>
</Grid>
</ContentDialog>

View File

@@ -1,16 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Bridge;
using Snap.Hutao.Web.Bridge.Model;
using Snap.Hutao.Web.Bridge.Model.Event;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Windows.UI.Popups;
namespace Snap.Hutao.View.Dialog;
@@ -19,6 +15,10 @@ namespace Snap.Hutao.View.Dialog;
/// </summary>
public sealed partial class SignInWebViewDialog : ContentDialog
{
private readonly IServiceScope scope;
[SuppressMessage("", "IDE0052")]
private SignInJsInterface? signInJsInterface;
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µ<EFBFBD>ǩ<EFBFBD><C7A9><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>ͼ<EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>
/// </summary>
@@ -27,6 +27,7 @@ public sealed partial class SignInWebViewDialog : ContentDialog
{
InitializeComponent();
XamlRoot = window.Content.XamlRoot;
scope = Ioc.Default.CreateScope();
}
private void OnGridLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
@@ -38,31 +39,27 @@ public sealed partial class SignInWebViewDialog : ContentDialog
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2 coreWebView2 = WebView.CoreWebView2;
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
ILogger<MiHoYoJsBridge> logger = Ioc.Default.GetRequiredService<ILogger<MiHoYoJsBridge>>();
IUserService userService = scope.ServiceProvider.GetRequiredService<IUserService>();
User? user = userService.Current;
coreWebView2.SetCookie(user?.CookieToken, user?.Ltoken);
coreWebView2.SetMobileUserAgent();
coreWebView2.InitializeBridge(logger, false)
.Register<JsEventClosePage>(e => Hide())
.Register<JsEventRealPersonValidation>(e => infoBarService.Information("<22>޷<EFBFBD>ʹ<EFBFBD>ô˹<C3B4><CBB9><EFBFBD>", "<22><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"))
.Register<JsEventGetStatusBarHeight>(s => s.Callback(result => result.Data["statusBarHeight"] = 0))
.Register<JsEventGetDynamicSecretV1>(s => s.Callback(result =>
{
result.Data["DS"] = DynamicSecretHandler.GetDynamicSecret(nameof(SaltType.K2), nameof(DynamicSecretVersion.Gen1), includeChars: true);
}))
.Register<JsEventGetUserInfo>(s => s.Callback(result =>
{
result.Data["id"] = "111";
result.Data["gender"] = 0;
result.Data["nickname"] = "222";
result.Data["introduce"] = "333";
result.Data["avatar_url"] = "https://img-static.mihoyo.com/communityweb/upload/52de23f1b1a060e4ccaa8b24c1305dd9.png";
}));
if (user == null)
{
return;
}
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
signInJsInterface = new(coreWebView2, scope.ServiceProvider);
#if DEBUG
coreWebView2.OpenDevToolsWindow();
#endif
coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501");
}
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
signInJsInterface = null;
scope.Dispose();
}
}

View File

@@ -61,6 +61,16 @@
Icon="&#xECAA;"
Header="胡桃"
Description="{Binding AppVersion}"/>
<sc:Setting
Icon="&#xE975;"
Header="设备ID">
<sc:Setting.Description>
<TextBlock
IsTextSelectionEnabled="True"
Text="{Binding DeviceId}"
Style="{StaticResource CaptionTextBlockStyle}"/>
</sc:Setting.Description>
</sc:Setting>
<sc:Setting
Icon="&#xED15;"
Header="反馈"
@@ -159,14 +169,23 @@
</sc:Setting>
<sc:Setting
Icon="&#xE756;"
Header="删除所有用户"
Description="直接删除用户表的所有记录,用于修复特定的账号冲突问题"
Background="#80800000">
Icon="&#xE9D5;"
Header="米游社每日签到">
<sc:Setting.Description>
<StackPanel>
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Text="对当前选中的账号进行签到"/>
<TextBlock
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="Yellow"
Text="可能需要重启应用以对其他账号签到"/>
</StackPanel>
</sc:Setting.Description>
<sc:Setting.ActionContent>
<Button
Content="执行"
Command="{Binding Experimental.DeleteUsersCommand}"/>
Content="打开签到对话框"
Command="{Binding ShowSignInWebViewDialogCommand}"/>
</sc:Setting.ActionContent>
</sc:Setting>
@@ -179,6 +198,20 @@
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="危险功能" Foreground="#FF800000">
<sc:Setting
Icon="&#xE756;"
Header="删除所有用户"
Description="直接删除用户表的所有记录,用于修复特定的账号冲突问题"
Background="#80800000">
<sc:Setting.ActionContent>
<Button
Content="执行"
Command="{Binding Experimental.DeleteUsersCommand}"/>
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -194,26 +194,12 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="Webview"/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton Label="米游社签到" Icon="{shcm:FontIcon Glyph=&#xEB41;}" Command="{Binding ShowSignInWebViewDialogCommand}"/>
</CommandBar>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="Cookie 操作"/>
<CommandBar DefaultLabelPosition="Right">
<AppBarButton Label="登录米哈游通行证" Icon="{shcm:FontIcon Glyph=&#xEB41;}" Command="{Binding LoginMihoyoUserCommand}">
<!--<AppBarButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="网页米游社" Command="{Binding LoginMihoyoBBSCommand}"/>
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=&#xF4A5;}" Text="米游社" Command="{Binding LoginMihoyoBBS2Command}"/>
</MenuFlyout>
</AppBarButton.Flyout>-->
</AppBarButton>
<AppBarButton Label="登录米哈游通行证" Icon="{shcm:FontIcon Glyph=&#xEB41;}" Command="{Binding LoginMihoyoUserCommand}"/>
<AppBarButton
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="手动输入"

View File

@@ -81,6 +81,7 @@ internal class SettingViewModel : ObservableObject
SetGamePathCommand = asyncRelayCommandFactory.Create(SetGamePathAsync);
DebugExceptionCommand = asyncRelayCommandFactory.Create(DebugThrowExceptionAsync);
DeleteGameWebCacheCommand = new RelayCommand(DeleteGameWebCache);
ShowSignInWebViewDialogCommand = asyncRelayCommandFactory.Create(ShowSignInWebViewDialogAsync);
}
/// <summary>
@@ -92,6 +93,15 @@ internal class SettingViewModel : ObservableObject
get => Core.CoreEnvironment.Version.ToString();
}
/// <summary>
/// 设备Id
/// </summary>
[SuppressMessage("", "CA1822")]
public string DeviceId
{
get => Core.CoreEnvironment.HutaoDeviceId;
}
/// <summary>
/// 空的历史卡池是否可见
/// </summary>
@@ -161,6 +171,11 @@ internal class SettingViewModel : ObservableObject
/// </summary>
public ICommand DeleteGameWebCacheCommand { get; }
/// <summary>
/// 签到对话框命令
/// </summary>
public ICommand ShowSignInWebViewDialogCommand { get; }
private async Task SetGamePathAsync()
{
IGameLocator locator = Ioc.Default.GetRequiredService<IEnumerable<IGameLocator>>()
@@ -188,6 +203,12 @@ internal class SettingViewModel : ObservableObject
}
}
private async Task ShowSignInWebViewDialogAsync()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
await new SignInWebViewDialog(mainWindow).ShowAsync();
}
private async Task DebugThrowExceptionAsync()
{
#if DEBUG

View File

@@ -46,7 +46,6 @@ internal class UserViewModel : ObservableObject
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
RemoveUserCommand = asyncRelayCommandFactory.Create<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
ShowSignInWebViewDialogCommand = asyncRelayCommandFactory.Create(ShowSignInWebViewDialogAsync);
}
/// <summary>
@@ -84,8 +83,6 @@ internal class UserViewModel : ObservableObject
/// </summary>
public ICommand LoginMihoyoUserCommand { get; }
public ICommand ShowSignInWebViewDialogCommand { get; }
/// <summary>
/// 移除用户命令
/// </summary>
@@ -168,10 +165,4 @@ internal class UserViewModel : ObservableObject
infoBarService.Error(e);
}
}
private async Task ShowSignInWebViewDialogAsync()
{
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
await new SignInWebViewDialog(mainWindow).ShowAsync();
}
}

View File

@@ -1,107 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Hoyolab;
using WinRT;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// Bridge 拓展
/// </summary>
public static class BridgeExtension
{
private const string InitializeJsInterfaceScript = """
let c = {};
c.postMessage = str => chrome.webview.hostObjects.MiHoYoJsBridge.OnMessage(str);
c.closePage = () => c.postMessage('{"method":"closePage"}');
window.MiHoYoJSInterface = c;
""";
private const string HideScrollBarScript = """
let st = document.createElement('style');
st.innerHTML = '::-webkit-scrollbar{display:none}';
document.querySelector('body').appendChild(st);
""";
/// <summary>
/// 设置 移动端UA
/// </summary>
/// <param name="webView">webview2</param>
public static void SetMobileUserAgent(this CoreWebView2 webView)
{
webView.Settings.UserAgent = "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/106.0.5249.126 Mobile Safari/537.36 miHoYoBBS/2.41.0";
}
/// <summary>
/// 初始化调用桥
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="logger">日志器</param>
/// <param name="checkHost">检查主机</param>
/// <returns>初始化后的调用桥</returns>
public static MiHoYoJsBridge InitializeBridge(this CoreWebView2 webView, ILogger<MiHoYoJsBridge> logger, bool checkHost = true)
{
MiHoYoJsBridge bridge = new(webView, logger);
var result = webView.As<ICoreWebView2Interop>().AddHostObjectToScript("MiHoYoJsBridge", bridge);
webView.DOMContentLoaded += OnDOMContentLoaded;
webView.NavigationStarting += (coreWebView2, args) => OnWebViewNavigationStarting(coreWebView2, args, checkHost);
return bridge;
}
/// <summary>
/// 设置WebView2的Cookie
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="cookieToken">CookieToken</param>
/// <param name="ltoken">Ltoken</param>
/// <param name="stoken">Stoken</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetCookie(this CoreWebView2 webView, Cookie? cookieToken = null, Cookie? ltoken = null, Cookie? stoken = null)
{
CoreWebView2CookieManager cookieManager = webView.CookieManager;
if (cookieToken != null)
{
cookieManager.AddMihoyoCookie("account_id", cookieToken).AddMihoyoCookie("cookie_token", cookieToken);
}
if (ltoken != null)
{
cookieManager.AddMihoyoCookie("ltuid", ltoken).AddMihoyoCookie("ltoken", ltoken);
}
if (stoken != null)
{
cookieManager.AddMihoyoCookie("stuid", stoken).AddMihoyoCookie("stoken", stoken);
}
return webView;
}
private static CoreWebView2CookieManager AddMihoyoCookie(this CoreWebView2CookieManager manager, string name, Cookie cookie)
{
manager.AddOrUpdateCookie(manager.CreateCookie(name, cookie[name], ".mihoyo.com", "/"));
return manager;
}
[SuppressMessage("", "VSTHRD100")]
private static async void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
string result = await coreWebView2.ExecuteScriptAsync(HideScrollBarScript);
_ = result;
}
[SuppressMessage("", "VSTHRD100")]
private static async void OnWebViewNavigationStarting(CoreWebView2 coreWebView2, CoreWebView2NavigationStartingEventArgs args, bool checkHost)
{
if (!checkHost || new Uri(args.Uri).Host.EndsWith("mihoyo.com"))
{
string result = await coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript);
_ = result;
}
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Hoyolab;
using WinRT;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// Bridge 拓展
/// </summary>
public static class CoreWebView2Extension
{
/// <summary>
/// 设置 移动端UA
/// </summary>
/// <param name="webView">webview2</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetMobileUserAgent(this CoreWebView2 webView)
{
webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabMobileUA;
return webView;
}
/// <summary>
/// 设置WebView2的Cookie
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="cookieToken">CookieToken</param>
/// <param name="ltoken">Ltoken</param>
/// <param name="stoken">Stoken</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetCookie(this CoreWebView2 webView, Cookie? cookieToken = null, Cookie? ltoken = null, Cookie? stoken = null)
{
CoreWebView2CookieManager cookieManager = webView.CookieManager;
if (cookieToken != null)
{
cookieManager.AddMihoyoCookie("account_id", cookieToken).AddMihoyoCookie("cookie_token", cookieToken);
}
if (ltoken != null)
{
cookieManager.AddMihoyoCookie("ltuid", ltoken).AddMihoyoCookie("ltoken", ltoken);
}
if (stoken != null)
{
cookieManager.AddMihoyoCookie("stuid", stoken).AddMihoyoCookie("stoken", stoken);
}
return webView;
}
private static CoreWebView2CookieManager AddMihoyoCookie(this CoreWebView2CookieManager manager, string name, Cookie cookie)
{
manager.AddOrUpdateCookie(manager.CreateCookie(name, cookie[name], ".mihoyo.com", "/"));
return manager;
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// ICoreWebView2Interop
/// </summary>
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("912b34a7-d10b-49c4-af18-7cb7e604e01a")]
public interface ICoreWebView2Interop
{
/// <summary>
/// Add the provided host object to script running in the WebView with the specified name.
/// </summary>
/// <param name="name">名称</param>
/// <param name="obj">对象</param>
/// <returns>结果</returns>
HRESULT AddHostObjectToScript([In] string name, [In] ref object obj);
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.InteropServices;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 调用桥暴露的COM接口
/// </summary>
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IMiHoYoJsBridge
{
/// <summary>
/// 消息发生时调用
/// </summary>
/// <param name="str">消息</param>
void OnMessage(string str);
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// Web 调用
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class JsMethodAttribute : Attribute
{
/// <summary>
/// 构造一个新的Web 调用特性
/// </summary>
/// <param name="name">函数名称</param>
public JsMethodAttribute(string name)
{
Name = name;
}
/// <summary>
/// 调用函数名称
/// </summary>
public string Name { get; init; }
}

View File

@@ -0,0 +1,335 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Convert;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Bridge.Model;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using System.Text;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 调用桥
/// </summary>
public class MiHoYoJSInterface
{
private const string InitializeJsInterfaceScript2 = """
window.MiHoYoJSInterface = {
postMessage: function(arg) { chrome.webview.postMessage(arg) },
closePage: function() { this.postMessage('{"method":"closePage"}') },
};
""";
private const string HideScrollBarScript = """
let st = document.createElement('style');
st.innerHTML = '::-webkit-scrollbar{display:none}';
document.querySelector('body').appendChild(st);
""";
private readonly CoreWebView2 webView;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<MiHoYoJSInterface> logger;
private readonly JsonSerializerOptions options;
/// <summary>
/// 构造一个新的调用桥
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="serviceProvider">服务提供器</param>
protected MiHoYoJSInterface(CoreWebView2 webView, IServiceProvider serviceProvider)
{
this.webView = webView;
this.serviceProvider = serviceProvider;
logger = serviceProvider.GetRequiredService<ILogger<MiHoYoJSInterface>>();
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
webView.WebMessageReceived += OnWebMessageReceived;
webView.DOMContentLoaded += OnDOMContentLoaded;
webView.NavigationStarting += OnNavigationStarting;
}
/// <summary>
/// 获取ActionTicket
/// </summary>
/// <param name="jsParam">参数</param>
/// <returns>响应</returns>
[JsMethod("getActionTicket")]
public virtual async Task<IJsResult?> GetActionTicketAsync(JsParam<ActionTypePayload> jsParam)
{
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
return await serviceProvider
.GetRequiredService<AuthClient>()
.GetActionTicketWrapperByStokenAsync(jsParam.Payload!.ActionType, user.Entity)
.ConfigureAwait(false);
}
/// <summary>
/// 获取Http请求头
/// </summary>
/// <param name="param">参数</param>
/// <returns>Http请求头</returns>
[JsMethod("getHTTPRequestHeaders")]
public virtual JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
throw new NotImplementedException();
}
/// <summary>
/// 异步获取账户信息
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("getCookieInfo")]
public virtual JsResult<Dictionary<string, string>> GetCookieInfo(JsParam param)
{
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
return new()
{
Data = new()
{
[Cookie.LTUID] = user.Ltoken![Cookie.LTUID],
[Cookie.LTOKEN] = user.Ltoken[Cookie.LTOKEN],
[Cookie.LOGIN_TICKET] = string.Empty,
},
};
}
/// <summary>
/// 获取1代动态密钥
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("getDS")]
public virtual JsResult<Dictionary<string, string>> GetDynamicSecrectV1(JsParam param)
{
string salt = DynamicSecretHandler.DynamicSecrets[nameof(SaltType.LK2)];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string r = GetRandomString();
string check = Md5Convert.ToHexString($"salt={salt}&t={t}&r={r}").ToLowerInvariant();
return new() { Data = new() { ["DS"] = $"{t},{r},{check}", }, };
static string GetRandomString()
{
const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
StringBuilder sb = new(6);
for (int i = 0; i < 6; i++)
{
int pos = Random.Shared.Next(0, RandomRange.Length);
sb.Append(RandomRange[pos]);
}
return sb.ToString();
}
}
/// <summary>
/// 获取用户基本信息
/// </summary>
/// <param name="param">参数</param>
/// <returns>响应</returns>
[JsMethod("getUserInfo")]
public virtual JsResult<Dictionary<string, object>> GetUserInfo(JsParam param)
{
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
UserInfo info = user.UserInfo!;
return new()
{
Data = new()
{
["id"] = info.Uid,
["gender"] = info.Gender,
["nickname"] = info.Nickname,
["introduce"] = info.Introduce,
["avatar_url"] = info.AvatarUrl,
},
};
}
[JsMethod("getCookieToken")]
public virtual async Task<JsResult<Dictionary<string, string>>> GetCookieTokenAsync(JsParam<CookieTokenPayload> param)
{
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
string? cookieToken;
if (param.Payload!.ForceRefresh)
{
cookieToken = await Ioc.Default
.GetRequiredService<PassportClient2>()
.GetCookieAccountInfoBySTokenAsync(user.Entity, default)
.ConfigureAwait(false);
user.CookieToken![Cookie.COOKIE_TOKEN] = cookieToken!;
Ioc.Default.GetRequiredService<AppDbContext>().Users.UpdateAndSave(user.Entity);
}
else
{
cookieToken = user.CookieToken![Cookie.COOKIE_TOKEN];
}
return new() { Data = new() { [Cookie.COOKIE_TOKEN] = cookieToken! } };
}
[JsMethod("configure_share")]
public virtual Task<IJsResult?> ConfigureShare(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("showAlertDialog")]
public virtual Task<IJsResult?> ShowAlertDialogAsync(JsParam param)
{
return Task.FromException<IJsResult?>(new NotImplementedException());
}
[JsMethod("closePage")]
public virtual IJsResult? ClosePage(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("startRealPersonValidation")]
public virtual IJsResult? StartRealPersonValidation(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("startRealnameAuth")]
public virtual IJsResult? StartRealnameAuth(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("getDS2")]
public virtual IJsResult? GetDynamicSecrectV2(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("genAuthKey")]
public virtual IJsResult? GenAuthKey(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("genAppAuthKey")]
public virtual IJsResult? GenAppAuthKey(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("getStatusBarHeight")]
public virtual IJsResult? GetStatusBarHeight(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("pushPage")]
public virtual IJsResult? PushPage(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("openSystemBrowser")]
public virtual IJsResult? OpenSystemBrowser(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("saveLoginTicket")]
public virtual IJsResult? SaveLoginTicket(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("getNotificationSettings")]
public virtual Task<IJsResult?> GetNotificationSettingsAsync(JsParam param)
{
throw new NotImplementedException();
}
[JsMethod("showToast")]
public virtual Task<IJsResult?> ShowToast(JsParam param)
{
throw new NotImplementedException();
}
private async Task<string> ExecuteCallbackScriptAsync(string callback, string? payload = null)
{
if (string.IsNullOrEmpty(callback))
{
// prevent executing this callback
return string.Empty;
}
string js = new StringBuilder()
.Append("javascript:mhyWebBridge(")
.Append('"')
.Append(callback)
.Append('"')
.AppendIf(payload != null, ',')
.Append(payload)
.Append(')')
.ToString();
logger?.LogInformation("[ExecuteScript] {js}", js);
await ThreadHelper.SwitchToMainThreadAsync();
return await webView.ExecuteScriptAsync(js);
}
[SuppressMessage("", "VSTHRD100")]
private async void OnWebMessageReceived(CoreWebView2 webView2, CoreWebView2WebMessageReceivedEventArgs args)
{
string message = args.TryGetWebMessageAsString();
logger?.LogInformation("[OnMessage] {message}", message);
JsParam param = JsonSerializer.Deserialize<JsParam>(message)!;
IJsResult? result = param.Method switch
{
"getActionTicket" => await GetActionTicketAsync(param).ConfigureAwait(false),
"getHTTPRequestHeaders" => GetHttpRequestHeader(param),
"getCookieInfo" => GetCookieInfo(param),
"getDS" => GetDynamicSecrectV1(param),
"getUserInfo" => GetUserInfo(param),
"getCookieToken" => await GetCookieTokenAsync(param).ConfigureAwait(false),
"configure_share" => null,
"login" => null,
_ => null,
};
if (result != null)
{
await ExecuteCallbackScriptAsync(param.Callback, result.ToString(options)).ConfigureAwait(false);
}
}
private void OnDOMContentLoaded(CoreWebView2 coreWebView2, CoreWebView2DOMContentLoadedEventArgs args)
{
coreWebView2.ExecuteScriptAsync(HideScrollBarScript).AsTask().SafeForget(logger);
}
private void OnNavigationStarting(CoreWebView2 coreWebView2, CoreWebView2NavigationStartingEventArgs args)
{
if (new Uri(args.Uri).Host.EndsWith("mihoyo.com"))
{
coreWebView2.ExecuteScriptAsync(InitializeJsInterfaceScript2).AsTask().SafeForget(logger);
}
}
}

View File

@@ -1,103 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Bridge.Model;
using Snap.Hutao.Web.Bridge.Model.Event;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 调用桥
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public sealed class MiHoYoJsBridge : IMiHoYoJsBridge
{
private readonly CoreWebView2 webView;
private readonly ILogger<MiHoYoJsBridge>? logger;
private readonly Dictionary<string, string> jsWebInvokeTypeCache = new();
private readonly Dictionary<string, Action<JsParam>> callbackHandlers = new();
/// <summary>
/// 构造一个新的调用桥
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="logger">日志器</param>
internal MiHoYoJsBridge(CoreWebView2 webView, ILogger<MiHoYoJsBridge>? logger = null)
{
this.webView = webView;
this.logger = logger;
}
/// <summary>
/// 消息发生时调用
/// </summary>
/// <param name="message">消息</param>
public void OnMessage(string message)
{
logger?.LogInformation("[OnMessage] {message}", message);
JsParam p = JsonSerializer.Deserialize<JsParam>(message)!;
p.Bridge = this;
callbackHandlers.GetValueOrDefault(p.Method)?.Invoke(p);
}
/// <summary>
/// 调用JS回调
/// </summary>
/// <param name="callbackName">回调名称</param>
/// <param name="payload">传输的数据</param>
/// <returns>执行结果</returns>
public Task<string> InvokeJsCallbackAsync(string callbackName, string? payload = null)
{
if (string.IsNullOrEmpty(callbackName))
{
return Task.FromResult(string.Empty);
}
string dataStr = payload == null ? string.Empty : $", {payload}";
string js = $"javascript:mhyWebBridge(\"{callbackName}\"{dataStr})";
logger?.LogInformation("[InvokeJsCallback] {js}", js);
return webView.ExecuteScriptAsync(js).AsTask();
}
/// <summary>
/// 注册回调
/// </summary>
/// <typeparam name="T">回调类型</typeparam>
/// <param name="callback">回调</param>
/// <returns>桥</returns>
public MiHoYoJsBridge Register<T>(Action<JsParam> callback)
where T : notnull
{
callbackHandlers[GetCallbackName<T>()] = callback;
return this;
}
/// <summary>
/// 注册回调
/// </summary>
/// <typeparam name="T">回调类型</typeparam>
/// <param name="callback">回调</param>
/// <returns>桥</returns>
public MiHoYoJsBridge Register<T>(Action<JsParam, T> callback)
where T : notnull
{
callbackHandlers[GetCallbackName<T>()] = p => callback(p, p.Data.As<T>());
return this;
}
private string GetCallbackName<T>()
{
Type type = typeof(T);
string invokeName = type.GetCustomAttribute<WebInvokeAttribute>()?.Name
?? throw new ArgumentException("Type Callback not registered.");
return jsWebInvokeTypeCache[type.Name] = invokeName;
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Hoyolab.Passport;
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 账户信息
/// </summary>
public class AccountInformation
{
/// <summary>
/// 构造一个新的账户信息
/// </summary>
/// <param name="userInformation">用户信息</param>
/// <param name="ltoken">ltoken</param>
public AccountInformation(UserInformation userInformation, string ltoken)
{
Aid = userInformation.Aid;
Mid = userInformation.Mid;
AccountName = userInformation.AccountName;
AreaCode = userInformation.AreaCode;
Mobile = userInformation.Mobile;
Email = userInformation.Email;
Token = ltoken;
TokenType = 2;
}
/// <summary>
/// 账户Id
/// </summary>
[JsonPropertyName("aid")]
public string Aid { get; set; } = default!;
/// <summary>
/// 米哈游Id
/// </summary>
[JsonPropertyName("mid")]
public string Mid { get; set; } = default!;
/// <summary>
/// 空
/// </summary>
[JsonPropertyName("accountName")]
public string AccountName { get; set; } = default!;
/// <summary>
/// 区域码 +86
/// </summary>
[JsonPropertyName("areaCode")]
public string AreaCode { get; set; } = default!;
/// <summary>
/// 手机号 111****1111
/// </summary>
[JsonPropertyName("mobile")]
public string Mobile { get; set; } = default!;
/// <summary>
/// 空
/// </summary>
[JsonPropertyName("email")]
public string Email { get; set; } = default!;
/// <summary>
/// Token
/// </summary>
[JsonPropertyName("tokenStr")]
public string Token { get; set; } = default!;
/// <summary>
/// Token 类型
/// </summary>
[JsonPropertyName("tokenType")]
public int TokenType { get; set; } = default!;
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 操作类型包装
/// </summary>
public class ActionTypePayload
{
/// <summary>
/// 操作类型
/// </summary>
[JsonPropertyName("action_type")]
public string ActionType { get; set; } = default!;
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 获取CookieToken的请求
/// </summary>
public class CookieTokenPayload
{
/// <summary>
/// 强制刷新
/// </summary>
[JsonPropertyName("forceRefresh")]
public bool ForceRefresh { get; set; }
}

View File

@@ -1,196 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model.Event;
/// <summary>
/// Web 调用
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class WebInvokeAttribute : Attribute
{
/// <summary>
/// 构造一个新的Web 调用特性
/// </summary>
/// <param name="name">函数名称</param>
public WebInvokeAttribute(string name)
{
Name = name;
}
/// <summary>
/// 调用函数名称
/// </summary>
public string Name { get; init; }
}
public class ButtonParam
{
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("style")]
public string Style { get; set; } = default!;
}
public abstract class GenAuthKeyBase
{
[JsonPropertyName("game_biz")]
public string Biz { get; set; } = default!;
[JsonPropertyName("auth_appid")]
public string AppId { get; set; } = default!;
[JsonPropertyName("game_uid")]
public uint Uid { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; } = default!;
}
[WebInvoke("closePage")]
public struct JsEventClosePage
{
}
[WebInvoke("configure_share")]
public class JsEventConfigureShare
{
[JsonPropertyName("enable")]
public bool Enable { get; set; }
}
[WebInvoke("genAppAuthKey")]
public class JsEventGenAppAuthKey
: GenAuthKeyBase
{
}
[WebInvoke("genAuthKey")]
public class JsEventGenAuthKey
: GenAuthKeyBase
{
}
[WebInvoke("getActionTicket")]
public class JsEventGetActionTicket
{
[JsonPropertyName("action_type")]
public string ActionType { get; set; } = default!;
}
[WebInvoke("getCookieToken")]
public class JsEventGetCookieToken
{
[JsonPropertyName("forceRefresh")]
public bool ForceRefresh { get; set; }
}
[WebInvoke("getDS")]
public struct JsEventGetDynamicSecretV1
{
}
[WebInvoke("getDS2")]
public class JsEventGetDynamicSecretV2
{
[JsonPropertyName("query")]
public Dictionary<string, string> Query { get; set; } = new();
[JsonPropertyName("body")]
public string Body { get; set; } = default!;
}
[WebInvoke("getNotificationSettings")]
public struct JsEventGetNotificationSettings
{
}
[WebInvoke("startRealnameAuth")]
public struct JsEventGetRealNameStatus
{
// guess
}
[WebInvoke("getHTTPRequestHeaders")]
public struct JsEventGetRequestHeader
{
}
[WebInvoke("getStatusBarHeight")]
public struct JsEventGetStatusBarHeight
{
// just zero
}
[WebInvoke("getUserInfo")]
public struct JsEventGetUserInfo
{
}
[WebInvoke("getCookieInfo")]
public struct JsEventGetWebLoginInfo
{
}
[WebInvoke("openSystemBrowser")]
public class JsEventOpenSystemBrowser
{
[JsonPropertyName("open_url")]
public string PageUrl { get; set; } = default!;
}
[WebInvoke("pushPage")]
public class JsEventPushPage
{
private string pageUrl = default!;
[JsonPropertyName("page")]
public string PageUrl
{
get => pageUrl;
set => SetPageUrl(value);
}
private void SetPageUrl(string value)
{
pageUrl = value.StartsWith("mihoyobbs")
? value.Replace("mihoyobbs://", "https://bbs.mihoyo.com/dby/").Replace("topic", "topicDetail")
: value;
}
}
[WebInvoke("startRealPersonValidation")]
public struct JsEventRealPersonValidation
{
}
[WebInvoke("saveLoginTicket")]
public class JsEventSaveLoginTicket
{
[JsonPropertyName("login_ticket")]
public string LoginTicket { get; set; } = default!;
}
[WebInvoke("showAlertDialog")]
public class JsEventShowAlertDialog
{
[JsonPropertyName("title")]
public string Title { get; set; } = default!;
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
[JsonPropertyName("buttons")]
public List<ButtonParam> Buttons { get; set; } = new();
}
[WebInvoke("showToast")]
public class JsEventShowToast
{
[JsonPropertyName("toast")]
public string Text { get; set; } = default!;
[JsonPropertyName("type")]
public string Type { get; set; } = default!;
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 指示此为Js结果
/// </summary>
public interface IJsResult
{
/// <summary>
/// 转换到Json字符串表示
/// </summary>
/// <param name="options">序列化参数</param>
/// <returns>JSON字符串</returns>
string ToString(JsonSerializerOptions options);
}

View File

@@ -4,6 +4,7 @@
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 由WebView向客户端传递的参数
/// Js 参数
/// </summary>
public class JsParam
@@ -12,52 +13,54 @@ public class JsParam
/// 方法名称
/// </summary>
[JsonPropertyName("method")]
public string Method { get; set; } = string.Empty;
public string Method { get; set; } = default!;
/// <summary>
/// 数据
/// 数据 可以为空
/// </summary>
[JsonPropertyName("payload")]
public JsonElement Data { get; set; }
public JsonElement? Payload { get; set; }
/// <summary>
/// 回调名称
/// 回调名称,调用 JavaScript:mhyWebBridge 时作为首个参数传入
/// </summary>
[JsonPropertyName("callback")]
public string CallbackName { get; set; } = string.Empty;
public string Callback { get; set; } = default!;
}
/// <summary>
/// 由WebView向客户端传递的参数
/// Js 参数
/// </summary>
/// <typeparam name="TPayload">Payload 类型</typeparam>
[SuppressMessage("", "SA1402")]
public class JsParam<TPayload>
{
/// <summary>
/// 方法名称
/// </summary>
[JsonPropertyName("method")]
public string Method { get; set; } = default!;
/// <summary>
/// 对应的调用桥
/// 数据 可以为空
/// </summary>
internal MiHoYoJsBridge Bridge { get; set; } = null!;
[JsonPropertyName("payload")]
public TPayload Payload { get; set; } = default!;
/// <summary>
/// 执行回调
/// 回调的名称,调用 JavaScript:mhyWebBridge 时作为首个参数传入
/// </summary>
/// <param name="resultFactory">结果工厂</param>
public void Callback(Func<JsResult>? resultFactory = null)
[JsonPropertyName("callback")]
public string Callback { get; set; } = string.Empty;
public static implicit operator JsParam<TPayload>(JsParam jsParam)
{
JsResult? result = resultFactory?.Invoke() ?? new();
Callback(result?.ToString());
}
/// <summary>
/// 执行回调
/// </summary>
/// <param name="resultModifier">结果工厂</param>
public void Callback(Action<JsResult> resultModifier)
{
JsResult result = new();
resultModifier(result);
Callback(result?.ToString());
}
/// <summary>
/// 执行回调
/// </summary>
/// <param name="result">结果</param>
public void Callback(string? result = null)
{
Bridge.InvokeJsCallbackAsync(CallbackName, result).GetAwaiter().GetResult();
return new JsParam<TPayload>()
{
Method = jsParam.Method,
Payload = jsParam.Payload.HasValue ? jsParam.Payload.Value.Deserialize<TPayload>() : default,
Callback = jsParam.Callback,
};
}
}

View File

@@ -4,9 +4,11 @@
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// 用于传回网页的参数
/// Js结果
/// </summary>
public class JsResult
/// <typeparam name="TData">内部数据类型</typeparam>
public class JsResult<TData> : IJsResult
{
/// <summary>
/// 代码
@@ -24,10 +26,10 @@ public class JsResult
/// 数据
/// </summary>
[JsonPropertyName("data")]
public Dictionary<string, object> Data { get; set; } = new();
public TData Data { get; set; } = default!;
/// <inheritdoc/>
public override string ToString()
string IJsResult.ToString(JsonSerializerOptions options)
{
return JsonSerializer.Serialize(this);
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Bridge.Model;
/// <summary>
/// JsonElement 拓展
/// </summary>
public static class JsonElementExtension
{
/// <summary>
/// 序列化到对应类型
/// </summary>
/// <typeparam name="T">对应类型</typeparam>
/// <param name="jsonElement">元素</param>
/// <returns>对应类型的实例</returns>
public static T As<T>(this JsonElement jsonElement)
where T : notnull
{
return jsonElement.Deserialize<T>()!;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Web.Bridge.Model;
namespace Snap.Hutao.Web.Bridge;
/// <summary>
/// 签到页面JS桥
/// </summary>
public class SignInJsInterface : MiHoYoJSInterface
{
/// <inheritdoc cref="MiHoYoJSInterface(CoreWebView2, IServiceProvider)"/>
public SignInJsInterface(CoreWebView2 webView, IServiceProvider serviceProvider)
: base(webView, serviceProvider)
{
}
/// <inheritdoc/>
public override JsResult<Dictionary<string, string>> GetHttpRequestHeader(JsParam param)
{
return new()
{
Data = new Dictionary<string, string>()
{
{ "x-rpc-client_type", "2" },
{ "x-rpc-device_id", Core.CoreEnvironment.HoyolabDeviceId },
{ "x-rpc-app_version", Core.CoreEnvironment.HoyolabXrpcVersion },
},
};
}
}

View File

@@ -10,6 +10,8 @@ namespace Snap.Hutao.Web.Hoyolab;
[SuppressMessage("", "SA1600")]
public partial class Cookie
{
public const string LOGIN_TICKET = "login_ticket";
public const string ACCOUNT_ID = "account_id";
public const string COOKIE_TOKEN = "cookie_token";

View File

@@ -15,10 +15,11 @@ namespace Snap.Hutao.Web.Hoyolab.DynamicSecret;
[Injection(InjectAs.Transient)]
public class DynamicSecretHandler : DelegatingHandler
{
private const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
/// <summary>
/// 盐
/// </summary>
// https://github.com/UIGF-org/Hoyolab.Salt
private static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
public static readonly ImmutableDictionary<string, string> DynamicSecrets = new Dictionary<string, string>()
{
[nameof(SaltType.K2)] = "TsmyHpZg8gFAVKTtlPaL6YwMldzxZJxQ",
[nameof(SaltType.LK2)] = "osgT0DljLarYxgebPPHJFjdaxPfoiHGt",
@@ -27,6 +28,8 @@ public class DynamicSecretHandler : DelegatingHandler
[nameof(SaltType.PROD)] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
}.ToImmutableDictionary();
private const string RandomRange = "abcdefghijklmnopqrstuvwxyz1234567890";
/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
{
@@ -66,36 +69,6 @@ public class DynamicSecretHandler : DelegatingHandler
return await base.SendAsync(request, token).ConfigureAwait(false);
}
/// <summary>
/// 获取DS
/// </summary>
/// <param name="saltType">nameof <see cref="SaltType"/></param>
/// <param name="version">nameof <see cref="DynamicSecretVersion"/></param>
/// <param name="body">body</param>
/// <param name="query">query</param>
/// <param name="includeChars">是否需要字母</param>
/// <returns>DS</returns>
public static string GetDynamicSecret(string saltType, string version, string? body = null, string? query = null, bool includeChars = true)
{
string salt = DynamicSecrets[saltType];
long t = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string r = includeChars ? GetRandomStringWithChars() : GetRandomStringNoChars();
string dsContent = $"salt={salt}&t={t}&r={r}";
if (version == nameof(DynamicSecretVersion.Gen2))
{
string[] queries = Uri.UnescapeDataString(query!).Split('?', 2);
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
dsContent = $"{dsContent}&b={body}&q={q}";
}
return Md5Convert.ToHexString(dsContent).ToLowerInvariant();
}
private static string GetRandomStringWithChars()
{
StringBuilder sb = new(6);

View File

@@ -58,6 +58,24 @@ internal class AuthClient
return null;
}
/// <summary>
/// 异步获取操作凭证
/// </summary>
/// <param name="action">操作</param>
/// <param name="user">用户</param>
/// <returns>操作凭证</returns>
[ApiInformation(Cookie = CookieType.Stoken, Salt = SaltType.K2)]
public async Task<Response<ActionTicketWrapper>?> GetActionTicketWrapperByStokenAsync(string action, User user)
{
Response<ActionTicketWrapper>? resp = await httpClient
.SetUser(user, CookieType.Stoken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<ActionTicketWrapper>>(ApiEndpoints.AuthActionTicket(action, user.Stoken![Cookie.STOKEN], user.Aid!), options, logger)
.ConfigureAwait(false);
return resp;
}
/// <summary>
/// 获取 MultiToken
/// </summary>

View File

@@ -43,6 +43,11 @@ public enum KnownReturnCode : int
/// </summary>
RET_NEED_AIGIS = -3101,
/// <summary>
/// 请在米游社App内打开~
/// </summary>
PleaseOpenInBbsApp = -1104,
/// <summary>
/// 登录信息已失效,请重新登录
/// </summary>

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Bridge.Model;
namespace Snap.Hutao.Web.Response;
@@ -60,7 +61,7 @@ public class Response : ISupportValidation
/// </summary>
/// <typeparam name="TData">数据类型</typeparam>
[SuppressMessage("", "SA1402")]
public class Response<TData> : Response
public class Response<TData> : Response, IJsResult
{
/// <summary>
/// 构造一个新的 Mihoyo 标准API响应
@@ -91,4 +92,10 @@ public class Response<TData> : Response
{
return ReturnCode == 0;
}
/// <inheritdoc/>
string IJsResult.ToString(JsonSerializerOptions options)
{
return JsonSerializer.Serialize(this);
}
}