diff --git a/DysonNetwork.Shared/Models/Autocompletion.cs b/DysonNetwork.Shared/Models/Autocompletion.cs new file mode 100644 index 0000000..6c14fd1 --- /dev/null +++ b/DysonNetwork.Shared/Models/Autocompletion.cs @@ -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!; +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Autocompletion/AutocompletionService.cs b/DysonNetwork.Sphere/Autocompletion/AutocompletionService.cs new file mode 100644 index 0000000..10221de --- /dev/null +++ b/DysonNetwork.Sphere/Autocompletion/AutocompletionService.cs @@ -0,0 +1,121 @@ +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Sphere.Autocompletion; + +public class AutocompletionService(AppDatabase db) +{ + public async Task> 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> AutocompleteAt(string type, string query, int limit) + { + var results = new List(); + + 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> 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; + } +} diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index a6f30df..9c7d4f0 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -4,6 +4,7 @@ using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; +using DysonNetwork.Sphere.Autocompletion; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -17,7 +18,8 @@ public partial class ChatController( ChatService cs, ChatRoomService crs, FileService.FileServiceClient files, - AccountService.AccountServiceClient accounts + AccountService.AccountServiceClient accounts, + AutocompletionService aus ) : ControllerBase { public class MarkMessageReadRequest @@ -329,4 +331,20 @@ public partial class ChatController( var response = await cs.GetSyncDataAsync(roomId, request.LastSyncTimestamp); return Ok(response); } -} \ No newline at end of file + + [HttpGet("{roomId:guid}/autocomplete")] + public async Task>> 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); + } +} diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 66cd27b..24dcf82 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ using System.Text.Json.Serialization; using System.Threading.RateLimiting; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.GeoIp; +using DysonNetwork.Sphere.Autocompletion; using DysonNetwork.Sphere.WebReader; using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Poll; @@ -118,6 +119,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); var translationProvider = configuration["Translation:Provider"]?.ToLower(); switch (translationProvider) @@ -129,4 +131,4 @@ public static class ServiceCollectionExtensions return services; } -} \ No newline at end of file +}