Iceshrimp.NET/Iceshrimp.Backend/Core/Services/UserProfileMentionsResolver.cs

142 lines
5.3 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 = await fields.SelectMany<ASField, string?>(p => [p.Name, p.Value])
.Select(async p => await MfmConverter.ExtractMentionsFromHtmlAsync(p))
.AwaitAllAsync();
var parsedBio = actor.MkSummary == null ? await MfmConverter.ExtractMentionsFromHtmlAsync(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;
}
}