Iceshrimp.NET/Iceshrimp.Backend/Components/PublicPreview/Renderers/NoteRenderer.cs

149 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 await RenderAsync(note, users, mentions, emoji, attachments, polls);
}
private async Task<PreviewNote> RenderAsync(
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 = 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<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 await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments, polls))
.AwaitAllAsync()
.ToListAsync();
}
}