Iceshrimp.NET/Iceshrimp.Backend/Components/PublicPreview/Renderers/NoteRenderer.cs
Laura Hausmann 9ff79c92e0
[backend/libmfm] Improve performance of AngleSharp calls for MFM-HTML conversion, improve UrlNode HTML representation
This makes sure the AngleSharp owner document is only created once per application lifecycle, and replaces all async calls with their synchronous counterparts (since the input is already loaded in memory, using async for this just creates overhead)
2025-03-24 18:05:21 +01:00

148 lines
6.7 KiB
C#

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<Config.InstanceSection> instance,
IOptionsSnapshot<Config.SecuritySection> security
) : IScopedService
{
public async Task<PreviewNote?> 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 Render(note, users, mentions, emoji, attachments, polls);
}
private PreviewNote Render(
Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions,
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
Dictionary<string, PreviewPoll> polls
)
{
var renderedText = mfm.Render(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,
Uri = note.Uri ?? note.GetPublicUri(instance.Value),
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<Dictionary<string, List<Note.MentionedUser>>> GetMentionsAsync(List<Note> notes)
{
var mentions = notes.SelectMany(n => n.Mentions).Distinct().ToList();
if (mentions.Count == 0) return notes.ToDictionary<Note, string, List<Note.MentionedUser>>(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<Dictionary<string, List<Emoji>>> GetEmojiAsync(List<Note> notes)
{
var ids = notes.SelectMany(n => n.Emojis).Distinct().ToList();
if (ids.Count == 0) return notes.ToDictionary<Note, string, List<Emoji>>(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<List<PreviewUser>> GetUsersAsync(List<Note> notes)
{
if (notes is []) return [];
return await userRenderer.RenderManyAsync(notes.Select(p => p.User).Distinct().ToList());
}
private async Task<Dictionary<string, List<PreviewAttachment>?>> GetAttachmentsAsync(List<Note> notes)
{
if (security.Value.PublicPreview is Enums.PublicPreview.RestrictedNoMedia)
return notes.ToDictionary<Note, string, List<PreviewAttachment>?>(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<Note, string, List<PreviewAttachment>?>(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<Dictionary<string, PreviewPoll>> GetPollsAsync(List<Note> notes)
{
if (notes is []) return new Dictionary<string, PreviewPoll>();
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<List<PreviewNote>> RenderManyAsync(List<Note> notes)
{
if (notes is []) return [];
var allNotes = notes.SelectMany<Note, Note?>(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 notes.Select(p => Render(p, users, mentions, emoji, attachments, polls)).ToList();
}
}