Skip to content

Commit

Permalink
feat(BrowserFinger): add IBrowserFingerService (#2860)
Browse files Browse the repository at this point in the history
* feat: 增加指纹服务

* chore: 添加指纹服务

* feat: 增加指纹组件

* doc: 移除注释

* doc: AI 聊天增加浏览器指纹组件

* refactor: 实现指纹算法

* doc: 增加 Chats 限制提示信息

* test: 增加单元测试
  • Loading branch information
ArgoZhang authored Jan 27, 2024
1 parent 7218fb3 commit 4252d26
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 14 deletions.
13 changes: 9 additions & 4 deletions src/BootstrapBlazor.Server/Components/Pages/Chats.razor
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@
}
</div>
<div class="chat-footer">
<Textarea Id="@Id" class="chat-footer-tx" rows="3" @bind-Value="@Context" PlaceHolder="Type user query here. (Shift + Enter for new line)"></Textarea>
<div class="chat-buttons">
<Button Icon="fa-regular fa-paper-plane" Color="Color.Primary" OnClick="GetCompletionsAsync" IsAsync="true" class="btn-send"></Button>
<Button Icon="fa-solid fa-xmark" Color="Color.Danger" OnClick="CreateNewTopic" class="btn-clear"></Button>
<label class="chat-info">@((MarkupString)Localizer["ChatInfo", _currentCount, _totalCount].Value)</label>
<div class="d-flex">
<Textarea Id="@Id" class="chat-footer-tx" rows="3" @bind-Value="@Context" PlaceHolder="Type user query here. (Shift + Enter for new line)"></Textarea>
<div class="chat-buttons">
<Button Icon="fa-regular fa-paper-plane" Color="Color.Primary" OnClick="GetCompletionsAsync" IsDisabled="_isDisabled" IsAsync="true" class="btn-send"></Button>
<Button Icon="fa-solid fa-xmark" Color="Color.Danger" OnClick="CreateNewTopic" IsDisabled="_isDisabled" class="btn-clear"></Button>
</div>
</div>
</div>
</DemoBlock>

<BrowserFinger></BrowserFinger>
56 changes: 55 additions & 1 deletion src/BootstrapBlazor.Server/Components/Pages/Chats.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Azure.AI.OpenAI;
using Microsoft.AspNetCore.Components.Authorization;
using System.Collections.Concurrent;

namespace BootstrapBlazor.Server.Components.Pages;

Expand All @@ -20,18 +20,50 @@ public partial class Chats
[NotNull]
private IStringLocalizer<Chats>? Localizer { get; set; }

[Inject]
[NotNull]
private IBrowserFingerService? BrowserFingerService { get; set; }

[Inject]
[NotNull]
private IVersionService? VersionService { get; set; }

private string? Context { get; set; }

private List<AzureOpenAIChatMessage> Messages { get; } = [];

private static string? GetStackClass(ChatRole role) => CssBuilder.Default("msg-stack").AddClass("msg-stack-assistant", role == ChatRole.Assistant).Build();

private static readonly ConcurrentDictionary<string, int> _cache = new();

private string? _code;

private readonly int _totalCount = 50;

private int _currentCount;

private bool _isDisabled;

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

_code = await GetFingerCodeAsync();
_currentCount = _cache.GetOrAdd(_code, key => _totalCount);
_isDisabled = _currentCount < 1;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

if (!firstRender)
{
await InvokeVoidAsync("scroll", Id);
Expand Down Expand Up @@ -76,9 +108,31 @@ private async Task GetCompletionsAsync()
StateHasChanged();
}
}

_ = Task.Run(async () =>
{
await Task.Delay(100);
if (!string.IsNullOrEmpty(_code))
{
_currentCount = _cache.AddOrUpdate(_code, key => _totalCount, (key, number) => number - 1);
_isDisabled = _currentCount < 1;
}
else
{
_isDisabled = true;
}
await InvokeAsync(StateHasChanged);
});
}
}

private async Task<string> GetFingerCodeAsync()
{
var code = await BrowserFingerService.GetFingerCodeAsync();
code ??= $"BootstrapBlazor{VersionService.GetVersion()}";
return code;
}

private void CreateNewTopic()
{
Context = null;
Expand Down
7 changes: 7 additions & 0 deletions src/BootstrapBlazor.Server/Components/Pages/Chats.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,15 @@
border-radius: .5rem;
box-shadow: rgb(210, 208, 206) 0 2px 4px, rgb(237, 235, 233) 0 0 2px;
display: flex;
flex-direction: column;
}

.chat-footer .chat-info {
margin-bottom: .25rem;
font-size: 80%;
color: rgba(var(--bs-body-color-rgb), 0.6);
}

::deep .chat-footer-tx {
resize: none;
flex: 1;
Expand Down
5 changes: 1 addition & 4 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -5778,10 +5778,7 @@
"ChatNormalIntro": "Chat dialog by calling <code>IAzureOpenAIService</code> service method <code>GetChatCompletionsAsync</code>",
"ChatUserTitle": "Chat session",
"ChatUserMessageTitle": "Hello {0}",
"ChatDemoDesc": "Since <code>GPT</code> has no free interface, this component requires <code>Gitee</code> or <code>Github</code> to authorize and log in to limit the number of times the account can be used",
"ChatExit": "Exit",
"ChatGitee": "Gitee OAuth",
"ChatGithub": "Github OAuth"
"ChatInfo": "Due to the lack of free interfaces in GPT, the number of experiences has been <b>{1}</b> times, and the current number is <b>{0}</b>"
},
"BootstrapBlazor.Server.Components.Samples.HtmlRenderers": {
"Title": "Html Renderer",
Expand Down
5 changes: 1 addition & 4 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -5778,10 +5778,7 @@
"ChatNormalIntro": "通过调用 <code>IAzureOpenAIService</code> 服务方法 <code>GetChatCompletionsAsync</code> 进行聊天对话",
"ChatUserTitle": "聊天场景",
"ChatUserMessageTitle": "你好 {0}",
"ChatDemoDesc": "由于 <code>GPT</code> 无免费接口,本组件需要 <code>Gitee</code> 或者 <code>Github</code> 授权登录后对账户进行使用次数限制",
"ChatExit": "退出",
"ChatGitee": "Gitee 授权",
"ChatGithub": "Github 授权"
"ChatInfo": "由于 <code>GPT</code> 无免费接口,体验次数为 <b>{1}</b> 次,当前次数为 <b>{0}</b>"
},
"BootstrapBlazor.Server.Components.Samples.HtmlRenderers": {
"Title": "Html 转化器",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapModuleComponentBase
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// 浏览器指纹组件
/// </summary>
[BootstrapModuleAutoLoader("BrowserFinger/BrowserFinger.razor.js", AutoInvokeInit = false, AutoInvokeDispose = false)]
public partial class BrowserFinger : IDisposable
{
[Inject]
[NotNull]
private IBrowserFingerService? BrowserFingerService { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

BrowserFingerService.Subscribe(this, Callback);
}

private async Task<string?> Callback() => await InvokeAsync<string?>("getFingerCode");

/// <summary>
/// Dispose 方法
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
BrowserFingerService.Unsubscribe(this);
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function getFingerCode() {
return getCanvasFingerprint();
}

const getCanvasFingerprint = () => {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(128, 0, 0)';
ctx.fillRect(10, 10, 100, 100);

ctx.fillStyle = 'rgb(0, 128, 0)';
ctx.fillRect(50, 50, 100, 100);
ctx.strokeStyle = 'rgb(0, 0, 128)'
ctx.lineWidth = 5;
ctx.strokeRect(30, 30, 80, 80);

ctx.font = '20px Arial';
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillText('BootstrapBlazor', 60, 116);

const dataURL = canvas.toDataURL();
const hash = hashCode(dataURL);
return hash.toString();
}

const hashCode = str => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(1);
hash = (hash << 5) - hash + char;
hash |= 0;
}
return hash;
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ protected virtual void Dispose(bool disposing)
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <exception cref="NotImplementedException"></exception>
public void Dispose()
{
Dispose(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
services.TryAddScoped<IReconnectorProvider, ReconnectorProvider>();
services.TryAddScoped<IGeoLocationService, DefaultGeoLocationService>();
services.TryAddScoped<IComponentHtmlRenderer, ComponentHtmlRenderer>();
services.TryAddScoped<IBrowserFingerService, DefaultBrowserFingerService>();

services.AddScoped<TabItemTextOptions>();
services.AddScoped<DialogService>();
Expand Down
43 changes: 43 additions & 0 deletions src/BootstrapBlazor/Services/DefaultBrowserFingerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using System.Collections.Concurrent;

namespace BootstrapBlazor.Components;

/// <summary>
/// 浏览器指纹服务
/// </summary>
class DefaultBrowserFingerService : IBrowserFingerService
{
private ConcurrentDictionary<object, Func<Task<string?>>> Cache { get; } = new();

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="target"></param>
/// <param name="callback"></param>
public void Subscribe(object target, Func<Task<string?>> callback) => Cache.GetOrAdd(target, k => callback);

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="target"></param>
public void Unsubscribe(object target) => Cache.TryRemove(target, out _);

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public async Task<string?> GetFingerCodeAsync()
{
string? code = null;
var cb = Cache.LastOrDefault();
if (cb.Value != null)
{
code = await cb.Value();
}
return code;
}
}
30 changes: 30 additions & 0 deletions src/BootstrapBlazor/Services/IBrowserFingerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// 浏览器指纹接口
/// </summary>
public interface IBrowserFingerService
{
/// <summary>
/// 订阅指纹方法回调
/// </summary>
/// <param name="target"></param>
/// <param name="callback"></param>
void Subscribe(object target, Func<Task<string?>> callback);

/// <summary>
/// 取消指纹方法回调
/// </summary>
/// <param name="target"></param>
void Unsubscribe(object target);

/// <summary>
/// 获得当前浏览器指纹方法
/// </summary>
/// <returns></returns>
Task<string?> GetFingerCodeAsync();
}
20 changes: 20 additions & 0 deletions test/UnitTest/Services/BrowserFingerServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using Microsoft.Extensions.DependencyInjection;

namespace UnitTest.Services;

public class BrowserFingerServiceTest : BootstrapBlazorTestBase
{
[Fact]
public async Task GetFingerCodeAsync_Ok()
{
var service = Context.Services.GetRequiredService<IBrowserFingerService>();
var cut = Context.RenderComponent<BrowserFinger>();
var code = await service.GetFingerCodeAsync();
cut.Instance.Dispose();
Assert.Null(code);
}
}

0 comments on commit 4252d26

Please sign in to comment.