Full featured auto complete

This commit is contained in:
2025-10-12 16:55:32 +08:00
parent e624c2bb3e
commit 37ea882ef7
8 changed files with 347 additions and 275 deletions

View File

@@ -160,6 +160,26 @@ public class AccountServiceGrpc(
return response; return response;
} }
public override async Task<GetAccountBatchResponse> SearchAccount(SearchAccountRequest request, ServerCallContext context)
{
var accounts = await _db.Accounts
.AsNoTracking()
.Where(a => EF.Functions.ILike(a.Name, $"%{request.Query}%"))
.Include(a => a.Profile)
.ToListAsync();
var perks = await subscriptions.GetPerkSubscriptionsAsync(
accounts.Select(x => x.Id).ToList()
);
foreach (var account in accounts)
if (perks.TryGetValue(account.Id, out var perk))
account.PerkSubscription = perk?.ToReference();
var response = new GetAccountBatchResponse();
response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue()));
return response;
}
public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request, public override async Task<ListAccountsResponse> ListAccounts(ListAccountsRequest request,
ServerCallContext context) ServerCallContext context)
{ {

View File

@@ -254,6 +254,7 @@ service AccountService {
rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc GetAccountBatch(GetAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc GetBotAccountBatch(GetBotAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {} rpc LookupAccountBatch(LookupAccountBatchRequest) returns (GetAccountBatchResponse) {}
rpc SearchAccount(SearchAccountRequest) returns (GetAccountBatchResponse) {}
rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {} rpc ListAccounts(ListAccountsRequest) returns (ListAccountsResponse) {}
rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {} rpc GetAccountStatus(GetAccountRequest) returns (AccountStatus) {}
@@ -343,6 +344,10 @@ message LookupAccountBatchRequest {
repeated string names = 1; repeated string names = 1;
} }
message SearchAccountRequest {
string query = 1;
}
message GetAccountBatchResponse { message GetAccountBatchResponse {
repeated Account accounts = 1; // List of accounts repeated Account accounts = 1; // List of accounts
} }

View File

@@ -27,6 +27,13 @@ public class AccountClientHelper(AccountService.AccountServiceClient accounts)
return response.Accounts.ToList(); return response.Accounts.ToList();
} }
public async Task<List<Account>> SearchAccounts(string query)
{
var request = new SearchAccountRequest { Query = query };
var response = await accounts.SearchAccountAsync(request);
return response.Accounts.ToList();
}
public async Task<List<Account>> GetBotAccountBatch(List<Guid> automatedIds) public async Task<List<Account>> GetBotAccountBatch(List<Guid> automatedIds)
{ {
var request = new GetBotAccountBatchRequest(); var request = new GetBotAccountBatchRequest();

View File

@@ -1,10 +1,12 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Autocompletion; namespace DysonNetwork.Sphere.Autocompletion;
public class AutocompletionService(AppDatabase db) public class AutocompletionService(AppDatabase db, AccountClientHelper accountsHelper)
{ {
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, int limit = 10) public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, Guid? chatId = null, Guid? realmId = null, int limit = 10)
{ {
if (string.IsNullOrWhiteSpace(content)) if (string.IsNullOrWhiteSpace(content))
return []; return [];
@@ -14,7 +16,8 @@ public class AutocompletionService(AppDatabase db)
var afterAt = content[1..]; var afterAt = content[1..];
string type; string type;
string query; string query;
if (afterAt.Contains('/')) bool hadSlash = afterAt.Contains('/');
if (hadSlash)
{ {
var parts = afterAt.Split('/', 2); var parts = afterAt.Split('/', 2);
type = parts[0]; type = parts[0];
@@ -25,7 +28,8 @@ public class AutocompletionService(AppDatabase db)
type = "u"; type = "u";
query = afterAt; query = afterAt;
} }
return await AutocompleteAt(type, query, limit);
return await AutocompleteAt(type, query, chatId, realmId, hadSlash, limit);
} }
if (!content.StartsWith(':')) return []; if (!content.StartsWith(':')) return [];
@@ -33,15 +37,49 @@ public class AutocompletionService(AppDatabase db)
var query = content[1..]; var query = content[1..];
return await AutocompleteSticker(query, limit); return await AutocompleteSticker(query, limit);
} }
} }
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteAt(string type, string query, int limit) private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteAt(string type, string query, Guid? chatId, Guid? realmId, bool hadSlash,
int limit)
{ {
var results = new List<DysonNetwork.Shared.Models.Autocompletion>(); var results = new List<DysonNetwork.Shared.Models.Autocompletion>();
switch (type) switch (type)
{ {
case "u":
var allAccounts = await accountsHelper.SearchAccounts(query);
var filteredAccounts = allAccounts;
if (chatId.HasValue)
{
var chatMemberIds = await db.ChatMembers
.Where(m => m.ChatRoomId == chatId.Value && m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.AccountId)
.ToListAsync();
var chatMemberIdStrings = chatMemberIds.Select(id => id.ToString()).ToHashSet();
filteredAccounts = allAccounts.Where(a => chatMemberIdStrings.Contains(a.Id)).ToList();
}
else if (realmId.HasValue)
{
var realmMemberIds = await db.RealmMembers
.Where(m => m.RealmId == realmId.Value && m.LeaveAt == null)
.Select(m => m.AccountId)
.ToListAsync();
var realmMemberIdStrings = realmMemberIds.Select(id => id.ToString()).ToHashSet();
filteredAccounts = allAccounts.Where(a => realmMemberIdStrings.Contains(a.Id)).ToList();
}
var users = filteredAccounts
.Take(limit)
.Select(a => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "user",
Keyword = "@" + (hadSlash ? "u/" : "") + a.Name,
Data = SnAccount.FromProtoValue(a)
})
.ToList();
results.AddRange(users);
break;
case "p": case "p":
var publishers = await db.Publishers var publishers = await db.Publishers
.Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%")) .Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%"))
@@ -49,7 +87,7 @@ public class AutocompletionService(AppDatabase db)
.Select(p => new DysonNetwork.Shared.Models.Autocompletion .Select(p => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "publisher", Type = "publisher",
Keyword = p.Name, Keyword = "@p/" + p.Name,
Data = p Data = p
}) })
.ToListAsync(); .ToListAsync();
@@ -63,7 +101,7 @@ public class AutocompletionService(AppDatabase db)
.Select(r => new DysonNetwork.Shared.Models.Autocompletion .Select(r => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "realm", Type = "realm",
Keyword = r.Slug, Keyword = "@r/" + r.Slug,
Data = r Data = r
}) })
.ToListAsync(); .ToListAsync();
@@ -77,7 +115,7 @@ public class AutocompletionService(AppDatabase db)
.Select(c => new DysonNetwork.Shared.Models.Autocompletion .Select(c => new DysonNetwork.Shared.Models.Autocompletion
{ {
Type = "chat", Type = "chat",
Keyword = c.Name!, Keyword = "@c/" + c.Name,
Data = c Data = c
}) })
.ToListAsync(); .ToListAsync();

View File

@@ -332,7 +332,7 @@ public partial class ChatController(
return Ok(response); return Ok(response);
} }
[HttpGet("{roomId:guid}/autocomplete")] [HttpPost("{roomId:guid}/autocomplete")]
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId) public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) if (HttpContext.Items["CurrentUser"] is not Account currentUser)
@@ -344,7 +344,7 @@ public partial class ChatController(
if (!isMember) if (!isMember)
return StatusCode(403, "You are not a member of this chat room."); return StatusCode(403, "You are not a member of this chat room.");
var result = await aus.GetAutocompletion(request.Content, limit: 10); var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
return Ok(result); return Ok(result);
} }
} }

View File

@@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration);
// Add application services // Add application services
builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppServices();
builder.Services.AddAppRateLimiting(); builder.Services.AddAppRateLimiting();
builder.Services.AddAppAuthentication(); builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();

View File

@@ -282,9 +282,9 @@ public class PublisherService(
public int SubscribersCount { get; set; } public int SubscribersCount { get; set; }
} }
private const string PublisherStatsCacheKey = "PublisherStats_{0}"; private const string PublisherStatsCacheKey = "publisher:{0}:stats";
private const string PublisherHeatmapCacheKey = "PublisherHeatmap_{0}"; private const string PublisherHeatmapCacheKey = "publisher:{0}:heatmap";
private const string PublisherFeatureCacheKey = "PublisherFeature_{0}_{1}"; private const string PublisherFeatureCacheKey = "publisher:{0}:feature:{1}";
public async Task<PublisherStats?> GetPublisherStats(string name) public async Task<PublisherStats?> GetPublisherStats(string name)
{ {
@@ -329,7 +329,7 @@ public class PublisherService(
public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name) public async Task<ActivityHeatmap?> GetPublisherHeatmap(string name)
{ {
var cacheKey = string.Format(PublisherHeatmapCacheKey, name); var cacheKey = string.Format(PublisherHeatmapCacheKey, name);
var heatmap = await cache.GetAsync<ActivityHeatmap>(cacheKey); var heatmap = await cache.GetAsync<ActivityHeatmap?>(cacheKey);
if (heatmap is not null) if (heatmap is not null)
return heatmap; return heatmap;

View File

@@ -15,6 +15,7 @@ using System.Text.Json.Serialization;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.GeoIp; using DysonNetwork.Shared.GeoIp;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere.Autocompletion; using DysonNetwork.Sphere.Autocompletion;
using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Discovery;
@@ -25,7 +26,7 @@ namespace DysonNetwork.Sphere.Startup;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddAppServices(this IServiceCollection services)
{ {
services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddLocalization(options => options.ResourcesPath = "Resources");
@@ -119,6 +120,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<WebFeedService>(); services.AddScoped<WebFeedService>();
services.AddScoped<DiscoveryService>(); services.AddScoped<DiscoveryService>();
services.AddScoped<PollService>(); services.AddScoped<PollService>();
services.AddScoped<AccountClientHelper>();
services.AddScoped<AutocompletionService>(); services.AddScoped<AutocompletionService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower(); var translationProvider = configuration["Translation:Provider"]?.ToLower();