Iceshrimp.NET/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.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

141 lines
5.2 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.MfmSharp;
using Microsoft.Extensions.Options;
using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver;
namespace Iceshrimp.Backend.Core.Services;
using MentionTuple = (List<Note.MentionedUser> mentions,
Dictionary<(string usernameLower, string webDomain), string> splitDomainMapping);
public class UserProfileMentionsResolver(
ActivityPub.UserResolver userResolver,
IOptions<Config.InstanceSection> config
) : IScopedService
{
public async Task<MentionTuple> ResolveMentionsAsync(ASActor actor, string? host)
{
var fields = actor.Attachments?.OfType<ASField>()
.Where(p => p is { Name: not null, Value: not null })
.ToList()
?? [];
if (fields is not { Count: > 0 } && (actor.MkSummary ?? actor.Summary) == null) return ([], []);
var parsedFields = fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
.Select(MfmConverter.ExtractMentionsFromHtml);
var parsedBio = actor.MkSummary == null ? MfmConverter.ExtractMentionsFromHtml(actor.Summary) : [];
var userUris = parsedFields.Prepend(parsedBio).SelectMany(p => p).ToList();
var mentionNodes = new List<MfmMentionNode>();
if (actor.MkSummary != null)
{
var nodes = MfmParser.Parse(actor.MkSummary.ReplaceLineEndings("\n"));
mentionNodes = EnumerateMentions(nodes);
}
var users = await mentionNodes
.DistinctBy(p => p.Acct)
.Select(p => userResolver.ResolveOrNullAsync(GetQuery(p.User, p.Host ?? host),
ResolveFlags.Acct))
.AwaitAllNoConcurrencyAsync();
users.AddRange(await userUris
.Distinct()
.Select(p => userResolver.ResolveOrNullAsync(p, EnforceUriFlags))
.AwaitAllNoConcurrencyAsync());
var mentions = users.NotNull()
.DistinctBy(p => p.Id)
.Select(p => new Note.MentionedUser
{
Host = p.Host,
Uri = p.Uri ?? p.GetPublicUri(config.Value),
Url = p.IsRemoteUser ? p.UserProfile?.Url : p.GetPublicUrl(config.Value),
Username = p.Username
})
.ToList();
var splitDomainMapping = users.Where(p => p is { IsRemoteUser: true, Uri: not null })
.Cast<User>()
.Where(p => new Uri(p.Uri!).Host != p.Host)
.DistinctBy(p => p.Host)
.ToDictionary(p => (p.UsernameLower, new Uri(p.Uri!).Host), p => p.Host!);
return (mentions, splitDomainMapping);
}
public async Task<List<Note.MentionedUser>> ResolveMentionsAsync(
UserProfile.Field[]? fields, string? bio, string? host
)
{
if (fields is not { Length: > 0 } && bio == null) return [];
var input = (fields ?? [])
.SelectMany<UserProfile.Field, string>(p => [p.Name, p.Value])
.Prepend(bio)
.Where(p => p != null)
.Cast<string>()
.ToList();
var nodes = input.SelectMany(p => MfmParser.Parse(p.ReplaceLineEndings("\n"))).ToArray();
var mentionNodes = EnumerateMentions(nodes);
var users = await mentionNodes
.DistinctBy(p => p.Acct)
.Select(p => userResolver.ResolveOrNullAsync(GetQuery(p.User, p.Host ?? host),
ResolveFlags.Acct))
.AwaitAllNoConcurrencyAsync();
return users.NotNull()
.DistinctBy(p => p.Id)
.Select(p => new Note.MentionedUser
{
Host = p.Host,
Uri = p.Uri ?? p.GetPublicUri(config.Value),
Url = p.IsRemoteUser ? p.UserProfile?.Url : p.GetPublicUrl(config.Value),
Username = p.Username
})
.ToList();
}
[SuppressMessage("ReSharper", "ReturnTypeCanBeEnumerable.Local",
Justification = "Roslyn inspection says this hurts performance")]
private static List<MfmMentionNode> EnumerateMentions(Span<IMfmNode> nodes)
{
var list = new List<MfmMentionNode>();
foreach (var node in nodes)
{
if (node is MfmMentionNode mention)
list.Add(mention);
else
list.AddRange(EnumerateMentions(node.Children));
}
return list;
}
[SuppressMessage("ReSharper", "ReturnTypeCanBeEnumerable.Local",
Justification = "Roslyn inspection says this hurts performance")]
[OverloadResolutionPriority(1)]
private static List<MfmMentionNode> EnumerateMentions(Span<IMfmInlineNode> nodes)
{
var list = new List<MfmMentionNode>();
foreach (var node in nodes)
{
if (node is MfmMentionNode mention)
list.Add(mention);
else
list.AddRange(EnumerateMentions(node.Children));
}
return list;
}
}