[backend/federation] Resolve user profile mentions (ISH-33)
This commit is contained in:
parent
2f3ca1e477
commit
665154b1eb
7 changed files with 139 additions and 16 deletions
|
@ -22,16 +22,19 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
|||
acct += $"@{user.Host}";
|
||||
|
||||
var profileEmoji = emoji?.Where(p => user.Emojis.Contains(p.Id)).ToList() ?? await GetEmoji([user]);
|
||||
var fields = profile?.Fields
|
||||
.Select(p => new Field
|
||||
{
|
||||
Name = p.Name,
|
||||
Value = p.Value,
|
||||
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
||||
? DateTime.Now.ToStringIso8601Like()
|
||||
: null
|
||||
})
|
||||
.ToList();
|
||||
var mentions = profile?.Mentions ?? [];
|
||||
var fields = profile != null
|
||||
? await profile.Fields
|
||||
.Select(async p => new Field
|
||||
{
|
||||
Name = p.Name,
|
||||
Value = await mfmConverter.ToHtmlAsync(p.Value, mentions, user.Host),
|
||||
VerifiedAt = p.IsVerified.HasValue && p.IsVerified.Value
|
||||
? DateTime.Now.ToStringIso8601Like()
|
||||
: null
|
||||
})
|
||||
.AwaitAllAsync()
|
||||
: null;
|
||||
|
||||
var res = new AccountEntity
|
||||
{
|
||||
|
@ -46,7 +49,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
|||
FollowersCount = user.FollowersCount,
|
||||
FollowingCount = user.FollowingCount,
|
||||
StatusesCount = user.NotesCount,
|
||||
Note = await mfmConverter.ToHtmlAsync(profile?.Description ?? "", [], user.Host),
|
||||
Note = await mfmConverter.ToHtmlAsync(profile?.Description ?? "", mentions, user.Host),
|
||||
Url = profile?.Url ?? user.Uri ?? user.GetPublicUrl(config.Value),
|
||||
AvatarStaticUrl = user.AvatarUrl ?? user.GetIdenticonUrl(config.Value), //TODO
|
||||
HeaderUrl = user.BannerUrl ?? _transparent,
|
||||
|
@ -54,7 +57,7 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
|||
MovedToAccount = null, //TODO
|
||||
IsBot = user.IsBot,
|
||||
IsDiscoverable = user.IsExplorable,
|
||||
Fields = fields ?? [],
|
||||
Fields = fields?.ToList() ?? [],
|
||||
Emoji = profileEmoji
|
||||
};
|
||||
|
||||
|
|
|
@ -297,6 +297,6 @@ public class Note : IEntity
|
|||
[J("uri")] public required string Uri { get; set; }
|
||||
[J("url")] public string? Url { get; set; }
|
||||
[J("username")] public required string Username { get; set; }
|
||||
[J("host")] public required string Host { get; set; }
|
||||
[J("host")] public required string? Host { get; set; }
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ public static class ServiceExtensions
|
|||
.AddScoped<DriveService>()
|
||||
.AddScoped<NotificationService>()
|
||||
.AddScoped<DatabaseMaintenanceService>()
|
||||
.AddScoped<UserProfileMentionsResolver>()
|
||||
.AddScoped<AuthorizedFetchMiddleware>()
|
||||
.AddScoped<AuthenticationMiddleware>()
|
||||
.AddScoped<ErrorHandlerMiddleware>()
|
||||
|
|
|
@ -6,10 +6,10 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
|||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool EqualsInvariant(this string s1, string s2) =>
|
||||
public static bool EqualsInvariant(this string? s1, string? s2) =>
|
||||
string.Equals(s1, s2, StringComparison.InvariantCulture);
|
||||
|
||||
public static bool EqualsIgnoreCase(this string s1, string s2) =>
|
||||
public static bool EqualsIgnoreCase(this string? s1, string s2) =>
|
||||
string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static string Truncate(this string target, int maxLength)
|
||||
|
|
|
@ -133,6 +133,33 @@ public class UserResolver(
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<User?> ResolveAsyncLimited(string query, Func<bool> limitReached)
|
||||
{
|
||||
query = NormalizeQuery(query);
|
||||
|
||||
// First, let's see if we already know the user
|
||||
var user = await userSvc.GetUserFromQueryAsync(query);
|
||||
if (user != null)
|
||||
return await GetUpdatedUser(user);
|
||||
|
||||
// We don't, so we need to run WebFinger
|
||||
var (acct, uri) = await WebFingerAsync(query);
|
||||
|
||||
// Check the database again with the new data
|
||||
if (uri != query) user = await userSvc.GetUserFromQueryAsync(uri);
|
||||
if (user == null && acct != query) await userSvc.GetUserFromQueryAsync(acct);
|
||||
if (user != null)
|
||||
return await GetUpdatedUser(user);
|
||||
|
||||
if (limitReached()) return null;
|
||||
|
||||
using (await KeyedLocker.LockAsync(uri))
|
||||
{
|
||||
// Pass the job on to userSvc, which will create the user
|
||||
return await userSvc.CreateUserAsync(uri, acct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<User> GetUpdatedUser(User user)
|
||||
{
|
||||
if (!user.NeedsUpdate) return user;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Types;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class UserProfileMentionsResolver(ActivityPub.UserResolver userResolver, IOptions<Config.InstanceSection> config)
|
||||
{
|
||||
private int _recursionLimit = 10;
|
||||
|
||||
public async Task<List<Note.MentionedUser>> ResolveMentions(UserProfile.Field[]? fields, string? bio)
|
||||
{
|
||||
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));
|
||||
var mentionNodes = EnumerateMentions(nodes);
|
||||
var users = await mentionNodes.DistinctBy(p => p.Acct)
|
||||
.Select(async p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await userResolver.ResolveAsyncLimited(p.Acct,
|
||||
() => --_recursionLimit >= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.AwaitAllNoConcurrencyAsync();
|
||||
|
||||
return users.Where(p => p != null)
|
||||
.Cast<User>()
|
||||
.Select(p => new Note.MentionedUser
|
||||
{
|
||||
Host = p.Host,
|
||||
Uri = p.Uri ?? p.GetPublicUri(config.Value),
|
||||
Url = p.UserProfile?.Url,
|
||||
Username = p.Username
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ReturnTypeCanBeEnumerable.Local",
|
||||
Justification = "Roslyn inspection says this hurts performance")]
|
||||
private static List<MfmMentionNode> EnumerateMentions(IEnumerable<MfmNode> 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;
|
||||
}
|
||||
}
|
|
@ -107,6 +107,8 @@ public class UserService(
|
|||
.Select(p => new UserProfile.Field { Name = p.Name!, Value = p.Value! })
|
||||
.ToArray();
|
||||
|
||||
var bio = actor.MkSummary ?? await mfmConverter.FromHtmlAsync(actor.Summary);
|
||||
|
||||
user = new User
|
||||
{
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
|
@ -136,7 +138,7 @@ public class UserService(
|
|||
var profile = new UserProfile
|
||||
{
|
||||
User = user,
|
||||
Description = actor.MkSummary ?? await mfmConverter.FromHtmlAsync(actor.Summary),
|
||||
Description = bio,
|
||||
//Birthday = TODO,
|
||||
//Location = TODO,
|
||||
Fields = fields ?? [],
|
||||
|
@ -157,6 +159,7 @@ public class UserService(
|
|||
var processPendingDeletes = await ResolveAvatarAndBanner(user, actor);
|
||||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
UpdateProfileMentionsInBackground(user);
|
||||
return user;
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
|
@ -256,6 +259,7 @@ public class UserService(
|
|||
db.Update(user);
|
||||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
UpdateProfileMentionsInBackground(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -586,4 +590,22 @@ public class UserService(
|
|||
// Clean up user list memberships
|
||||
await db.UserListMembers.Where(p => p.UserList.User == user && p.User == followee).ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "Projectables")]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "Method only makes sense for users")]
|
||||
private void UpdateProfileMentionsInBackground(User user)
|
||||
{
|
||||
_ = followupTaskSvc.ExecuteTask("UpdateProfileMentionsInBackground", async provider =>
|
||||
{
|
||||
var bgDbContext = provider.GetRequiredService<DatabaseContext>();
|
||||
var bgMentionsResolver = provider.GetRequiredService<UserProfileMentionsResolver>();
|
||||
var bgUser = await bgDbContext.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == user.Id);
|
||||
if (bgUser?.UserProfile == null) return;
|
||||
bgUser.UserProfile.Mentions =
|
||||
await bgMentionsResolver.ResolveMentions(bgUser.UserProfile.Fields, bgUser.UserProfile.Description);
|
||||
bgDbContext.Update(bgUser.UserProfile);
|
||||
await bgDbContext.SaveChangesAsync();
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue