Auto completion in chat

This commit is contained in:
2025-10-12 16:00:32 +08:00
parent f4a659fce5
commit 9631cd3edd
4 changed files with 159 additions and 3 deletions

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace DysonNetwork.Shared.Models;
public class AutocompletionRequest
{
[Required] public string Content { get; set; } = null!;
}
public class Autocompletion
{
public string Type { get; set; } = null!;
public string Keyword { get; set; } = null!;
public object Data { get; set; } = null!;
}

View File

@@ -0,0 +1,121 @@
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Autocompletion;
public class AutocompletionService(AppDatabase db)
{
public async Task<List<DysonNetwork.Shared.Models.Autocompletion>> GetAutocompletion(string content, int limit = 10)
{
if (string.IsNullOrWhiteSpace(content))
return [];
if (content.StartsWith('@'))
{
var afterAt = content[1..];
string type;
string query;
if (afterAt.Contains('/'))
{
var parts = afterAt.Split('/', 2);
type = parts[0];
query = parts.Length > 1 ? parts[1] : "";
}
else
{
type = "u";
query = afterAt;
}
return await AutocompleteAt(type, query, limit);
}
if (!content.StartsWith(':')) return [];
{
var query = content[1..];
return await AutocompleteSticker(query, limit);
}
}
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteAt(string type, string query, int limit)
{
var results = new List<DysonNetwork.Shared.Models.Autocompletion>();
switch (type)
{
case "p":
var publishers = await db.Publishers
.Where(p => EF.Functions.Like(p.Name, $"{query}%") || EF.Functions.Like(p.Nick, $"{query}%"))
.Take(limit)
.Select(p => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "publisher",
Keyword = p.Name,
Data = p
})
.ToListAsync();
results.AddRange(publishers);
break;
case "r":
var realms = await db.Realms
.Where(r => EF.Functions.Like(r.Slug, $"{query}%") || EF.Functions.Like(r.Name, $"{query}%"))
.Take(limit)
.Select(r => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "realm",
Keyword = r.Slug,
Data = r
})
.ToListAsync();
results.AddRange(realms);
break;
case "c":
var chats = await db.ChatRooms
.Where(c => c.Name != null && EF.Functions.Like(c.Name, $"{query}%"))
.Take(limit)
.Select(c => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "chat",
Keyword = c.Name!,
Data = c
})
.ToListAsync();
results.AddRange(chats);
break;
}
return results;
}
private async Task<List<DysonNetwork.Shared.Models.Autocompletion>> AutocompleteSticker(string query, int limit)
{
var stickers = await db.Stickers
.Include(s => s.Pack)
.Where(s => EF.Functions.Like(s.Slug, $"{query}%"))
.Take(limit)
.Select(s => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "sticker",
Keyword = s.Slug,
Data = s
})
.ToListAsync();
// Also possibly search by pack prefix? But user said slug after :
// Perhaps combine or search packs
var packs = await db.StickerPacks
.Where(p => EF.Functions.Like(p.Prefix, $"{query}%"))
.Take(limit)
.Select(p => new DysonNetwork.Shared.Models.Autocompletion
{
Type = "sticker_pack",
Keyword = p.Prefix,
Data = p
})
.ToListAsync();
var results = stickers.Concat(packs).Take(limit).ToList();
return results;
}
}

View File

@@ -4,6 +4,7 @@ using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Data;
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Proto;
using DysonNetwork.Sphere.Autocompletion;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -17,7 +18,8 @@ public partial class ChatController(
ChatService cs, ChatService cs,
ChatRoomService crs, ChatRoomService crs,
FileService.FileServiceClient files, FileService.FileServiceClient files,
AccountService.AccountServiceClient accounts AccountService.AccountServiceClient accounts,
AutocompletionService aus
) : ControllerBase ) : ControllerBase
{ {
public class MarkMessageReadRequest public class MarkMessageReadRequest
@@ -329,4 +331,20 @@ public partial class ChatController(
var response = await cs.GetSyncDataAsync(roomId, request.LastSyncTimestamp); var response = await cs.GetSyncDataAsync(roomId, request.LastSyncTimestamp);
return Ok(response); return Ok(response);
} }
}
[HttpGet("{roomId:guid}/autocomplete")]
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
return Unauthorized();
var accountId = Guid.Parse(currentUser.Id);
var isMember = await db.ChatMembers
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
if (!isMember)
return StatusCode(403, "You are not a member of this chat room.");
var result = await aus.GetAutocompletion(request.Content, limit: 10);
return Ok(result);
}
}

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.Sphere.Autocompletion;
using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.WebReader;
using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Discovery;
using DysonNetwork.Sphere.Poll; using DysonNetwork.Sphere.Poll;
@@ -118,6 +119,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<AutocompletionService>();
var translationProvider = configuration["Translation:Provider"]?.ToLower(); var translationProvider = configuration["Translation:Provider"]?.ToLower();
switch (translationProvider) switch (translationProvider)
@@ -129,4 +131,4 @@ public static class ServiceCollectionExtensions
return services; return services;
} }
} }