using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Services; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Controllers.Renderers; public class NoteRenderer(UserRenderer userRenderer, DatabaseContext db, EmojiService emojiSvc) { public async Task RenderOne(Note note, User? localUser, NoteRendererDto? data = null) { var res = await RenderBaseInternal(note, localUser, data); var renote = note.Renote is { IsPureRenote: true } ? await RenderRenote(note.Renote, localUser, data) : null; var quote = note.Renote is { IsPureRenote: false } ? await RenderBase(note.Renote, localUser, data) : null; var reply = note.Reply != null ? await RenderBase(note.Reply, localUser, data) : null; res.Renote = renote; res.RenoteId = note.RenoteId; res.Quote = quote; res.QuoteId = note.RenoteId; res.Reply = reply; res.ReplyId = note.ReplyId; return res; } private async Task RenderRenote(Note note, User? localUser, NoteRendererDto? data = null) { var res = await RenderBaseInternal(note, localUser, data); var quote = note.Renote is { IsPureRenote: false } ? await RenderBase(note.Renote, localUser, data) : null; res.Quote = quote; res.QuoteId = note.RenoteId; return res; } private async Task RenderBase(Note note, User? localUser, NoteRendererDto? data = null) => await RenderBaseInternal(note, localUser, data); private async Task RenderBaseInternal(Note note, User? localUser, NoteRendererDto? data = null) { var user = (data?.Users ?? await GetUsers([note])).First(p => p.Id == note.User.Id); var attachments = (data?.Attachments ?? await GetAttachments([note])).Where(p => note.FileIds.Contains(p.Id)); var reactions = (data?.Reactions ?? await GetReactions([note], localUser)).Where(p => p.NoteId == note.Id); return new NoteResponse { Id = note.Id, CreatedAt = note.CreatedAt.ToStringIso8601Like(), Text = note.Text, Cw = note.Cw, Visibility = RenderVisibility(note.Visibility), User = user, Attachments = attachments.ToList(), Reactions = reactions.ToList() }; } private static string RenderVisibility(Note.NoteVisibility visibility) => visibility switch { Note.NoteVisibility.Public => "public", Note.NoteVisibility.Home => "home", Note.NoteVisibility.Followers => "followers", Note.NoteVisibility.Specified => "specified", _ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, null) }; private async Task> GetUsers(IEnumerable notesList) { var users = notesList.Select(p => p.User).DistinctBy(p => p.Id); return await userRenderer.RenderMany(users).ToListAsync(); } private async Task> GetAttachments(IEnumerable notesList) { var ids = notesList.SelectMany(p => p.FileIds).Distinct(); var files = await db.DriveFiles.Where(p => ids.Contains(p.Id)).ToListAsync(); return files.Select(p => new NoteAttachment { Id = p.Id, Url = p.PublicUrl, ThumbnailUrl = p.PublicThumbnailUrl, Blurhash = p.Blurhash, AltText = p.Comment }) .ToList(); } private async Task> GetReactions(List notes, User? user) { if (user == null) return []; var counts = notes.ToDictionary(p => p.Id, p => p.Reactions); var res = await db.NoteReactions .Where(p => notes.Contains(p.Note)) .GroupBy(p => p.Reaction) .Select(p => new NoteReactionSchema { NoteId = p.First().NoteId, Count = (int)counts[p.First().NoteId].GetValueOrDefault(p.First().Reaction, 1), Reacted = db.NoteReactions.Any(i => i.NoteId == p.First().NoteId && i.Reaction == p.First().Reaction && i.User == user), Name = p.First().Reaction, Url = null }) .ToListAsync(); foreach (var item in res.Where(item => item.Name.StartsWith(':'))) { var hit = await emojiSvc.ResolveEmoji(item.Name); if (hit == null) continue; item.Url = hit.PublicUrl; } return res; } private static List GetAllNotes(IEnumerable notes) { return notes.SelectMany(p => [p, p.Reply, p.Renote, p.Renote?.Renote]) .OfType() .Distinct() .ToList(); } public async Task> RenderMany(IEnumerable notes, User? user) { var notesList = notes.ToList(); var allNotes = GetAllNotes(notesList); var data = new NoteRendererDto { Users = await GetUsers(allNotes), Attachments = await GetAttachments(allNotes), Reactions = await GetReactions(allNotes, user) }; return await notesList.Select(p => RenderOne(p, user, data)).AwaitAllAsync(); } public class NoteRendererDto { public List? Attachments; public List? Reactions; public List? Users; } }