using Iceshrimp.Backend.Components.PublicPreview.Schemas; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Components.PublicPreview.Renderers; public class NoteRenderer( DatabaseContext db, UserRenderer userRenderer, MfmRenderer mfm, MediaProxyService mediaProxy, IOptions instance, IOptionsSnapshot security ) : IScopedService { public async Task RenderOne(Note? note) { if (note == null) return null; var allNotes = ((Note?[]) [note, note.Reply, note.Renote]).NotNull().ToList(); var mentions = await GetMentionsAsync(allNotes); var emoji = await GetEmojiAsync(allNotes); var users = await GetUsersAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes); var polls = await GetPollsAsync(allNotes); return await RenderAsync(note, users, mentions, emoji, attachments, polls); } private async Task RenderAsync( Note note, List users, Dictionary> mentions, Dictionary> emoji, Dictionary?> attachments, Dictionary polls ) { var renderedText = await mfm.RenderAsync(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]); var inlineMediaUrls = renderedText?.InlineMedia.Select(m => m.Src).ToArray() ?? []; var res = new PreviewNote { User = users.First(p => p.Id == note.User.Id), Text = renderedText?.Html, Cw = note.Cw, RawText = note.Text, QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value), QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false, Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(), Poll = polls.GetValueOrDefault(note.Id), CreatedAt = note.CreatedAt.ToDisplayStringTz(), UpdatedAt = note.UpdatedAt?.ToDisplayStringTz() }; return res; } private async Task>> GetMentionsAsync(List notes) { var mentions = notes.SelectMany(n => n.Mentions).Distinct().ToList(); if (mentions.Count == 0) return notes.ToDictionary>(p => p.Id, _ => []); var users = await db.Users.Where(p => mentions.Contains(p.Id)) .ToDictionaryAsync(p => p.Id, p => new Note.MentionedUser { Host = p.Host, Uri = p.Uri ?? p.GetPublicUri(instance.Value), Url = p.UserProfile?.Url, Username = p.Username }); return notes.ToDictionary(p => p.Id, p => users.Where(u => p.Mentions.Contains(u.Key)).Select(u => u.Value).ToList()); } private async Task>> GetEmojiAsync(List notes) { var ids = notes.SelectMany(n => n.Emojis).Distinct().ToList(); if (ids.Count == 0) return notes.ToDictionary>(p => p.Id, _ => []); var emoji = await db.Emojis.Where(p => ids.Contains(p.Id)).ToListAsync(); return notes.ToDictionary(p => p.Id, p => emoji.Where(e => p.Emojis.Contains(e.Id)).ToList()); } private async Task> GetUsersAsync(List notes) { if (notes is []) return []; return await userRenderer.RenderManyAsync(notes.Select(p => p.User).Distinct().ToList()); } private async Task?>> GetAttachmentsAsync(List notes) { if (security.Value.PublicPreview is Enums.PublicPreview.RestrictedNoMedia) return notes.ToDictionary?>(p => p.Id, p => p.FileIds is [] ? null : []); var ids = notes.SelectMany(p => p.FileIds).ToList(); var files = await db.DriveFiles.Where(p => ids.Contains(p.Id)).ToListAsync(); return notes .ToDictionary?>(p => p.Id, p => files .Where(f => p.FileIds.Contains(f.Id)) .Select(f => new PreviewAttachment { MimeType = f.Type, Url = mediaProxy.GetProxyUrl(f), Name = f.Name, Alt = f.Comment, Sensitive = f.IsSensitive }) .ToList()); } private async Task> GetPollsAsync(List notes) { if (notes is []) return new Dictionary(); var ids = notes.Select(p => p.Id).ToList(); var polls = await db.Polls .Where(p => ids.Contains(p.NoteId)) .ToListAsync(); return polls.ToDictionary(p => p.NoteId, p => new PreviewPoll { ExpiresAt = p.ExpiresAt, Multiple = p.Multiple, Choices = p.Choices.Zip(p.Votes).Select(c => (c.First, c.Second)).ToList(), VotersCount = p.VotersCount }); } public async Task> RenderManyAsync(List notes) { if (notes is []) return []; var allNotes = notes.SelectMany(p => [p, p.Renote, p.Reply]).NotNull().Distinct().ToList(); var users = await GetUsersAsync(allNotes); var mentions = await GetMentionsAsync(allNotes); var emoji = await GetEmojiAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes); var polls = await GetPollsAsync(allNotes); return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments, polls)) .AwaitAllAsync() .ToListAsync(); } }