[backend/federation] Rework UserProfileMentionsResolver to no longer recursively resolve user profile mentions
This commit is contained in:
parent
435632857b
commit
9a01fbecdd
10 changed files with 6329 additions and 134 deletions
|
@ -1080,6 +1080,7 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options)
|
|||
.HasDefaultValueSql("'{}'::public.notification_type_enum[]");
|
||||
entity.Property(e => e.FFVisibility)
|
||||
.HasDefaultValue(UserProfile.UserProfileFFVisibility.Public);
|
||||
entity.Property(e => e.MentionsResolved).HasDefaultValue(false);
|
||||
|
||||
entity.HasOne(d => d.PinnedPage)
|
||||
.WithOne(p => p.UserProfile)
|
||||
|
|
6149
Iceshrimp.Backend/Core/Database/Migrations/20240226185219_AddUserProfileMentionsResolvedColumn.Designer.cs
generated
Normal file
6149
Iceshrimp.Backend/Core/Database/Migrations/20240226185219_AddUserProfileMentionsResolvedColumn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserProfileMentionsResolvedColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "mentionsResolved",
|
||||
table: "user_profile",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "mentionsResolved",
|
||||
table: "user_profile");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4577,6 +4577,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
|||
.HasColumnName("mentions")
|
||||
.HasDefaultValueSql("'[]'::jsonb");
|
||||
|
||||
b.Property<bool>("MentionsResolved")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("mentionsResolved");
|
||||
|
||||
b.Property<string>("ModerationNote")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
|
@ -176,6 +176,9 @@ public class UserProfile
|
|||
[InverseProperty(nameof(Tables.User.UserProfile))]
|
||||
public virtual User User { get; set; } = null!;
|
||||
|
||||
[Column("mentionsResolved")]
|
||||
public bool MentionsResolved;
|
||||
|
||||
public class Field
|
||||
{
|
||||
[J("name")] public required string Name { get; set; }
|
||||
|
|
16
Iceshrimp.Backend/Core/Extensions/TaskExtensions.cs
Normal file
16
Iceshrimp.Backend/Core/Extensions/TaskExtensions.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace Iceshrimp.Backend.Core.Extensions;
|
||||
|
||||
public static class TaskExtensions
|
||||
{
|
||||
public static async Task SafeWaitAsync(this Task task, TimeSpan timeSpan)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task.WaitAsync(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
|
@ -136,7 +136,9 @@ public class UserResolver(
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<User?> ResolveAsyncLimited(string username, string? host, Func<bool> limitReached)
|
||||
public async Task<User?> ResolveAsyncOrNull(string username, string? host)
|
||||
{
|
||||
try
|
||||
{
|
||||
var query = $"acct:{username}@{host}";
|
||||
|
||||
|
@ -156,16 +158,21 @@ public class UserResolver(
|
|||
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);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<User?> ResolveAsyncLimited(string uri, Func<bool> limitReached)
|
||||
public async Task<User?> ResolveAsyncOrNull(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
// First, let's see if we already know the user
|
||||
var user = await userSvc.GetUserFromQueryAsync(uri);
|
||||
|
@ -183,14 +190,17 @@ public class UserResolver(
|
|||
if (user != null)
|
||||
return await GetUpdatedUser(user);
|
||||
|
||||
if (limitReached()) return null;
|
||||
|
||||
using (await KeyedLocker.LockAsync(resolvedUri))
|
||||
{
|
||||
// Pass the job on to userSvc, which will create the user
|
||||
return await userSvc.CreateUserAsync(resolvedUri, acct);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<User> GetUpdatedUser(User user)
|
||||
{
|
||||
|
|
|
@ -4,8 +4,6 @@ public class FollowupTaskService(IServiceScopeFactory serviceScopeFactory)
|
|||
{
|
||||
public bool IsBackgroundWorker { get; private set; }
|
||||
|
||||
public IServiceProvider? ServiceProvider { get; set; }
|
||||
|
||||
public Task ExecuteTask(string taskName, Func<IServiceProvider, Task> work)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
@ -16,7 +14,6 @@ public class FollowupTaskService(IServiceScopeFactory serviceScopeFactory)
|
|||
var provider = scope.ServiceProvider;
|
||||
var instance = provider.GetRequiredService<FollowupTaskService>();
|
||||
instance.IsBackgroundWorker = true;
|
||||
instance.ServiceProvider = ServiceProvider ?? provider;
|
||||
await work(provider);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
@ -10,16 +10,9 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class UserProfileMentionsResolver(
|
||||
ActivityPub.UserResolver userResolver,
|
||||
IOptions<Config.InstanceSection> config
|
||||
)
|
||||
public class UserProfileMentionsResolver(ActivityPub.UserResolver userResolver, IOptions<Config.InstanceSection> config)
|
||||
{
|
||||
private int _recursionLimit = 10;
|
||||
|
||||
public async Task<List<Note.MentionedUser>> ResolveMentions(
|
||||
ASActor actor, string? host
|
||||
)
|
||||
public async Task<List<Note.MentionedUser>> ResolveMentions(ASActor actor, string? host)
|
||||
{
|
||||
var fields = actor.Attachments?.OfType<ASField>()
|
||||
.Where(p => p is { Name: not null, Value: not null })
|
||||
|
@ -43,33 +36,12 @@ public class UserProfileMentionsResolver(
|
|||
|
||||
var users = await mentionNodes
|
||||
.DistinctBy(p => p.Acct)
|
||||
.Select(async p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await userResolver.ResolveAsyncLimited(p.Username, p.Host ?? host,
|
||||
() => _recursionLimit-- <= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Select(async p => await userResolver.ResolveAsyncOrNull(p.Username, p.Host ?? host))
|
||||
.AwaitAllNoConcurrencyAsync();
|
||||
|
||||
users.AddRange(await userUris
|
||||
.Distinct()
|
||||
.Select(async p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await userResolver.ResolveAsyncLimited(p, () => _recursionLimit-- <= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Select(async p => await userResolver.ResolveAsyncOrNull(p))
|
||||
.AwaitAllNoConcurrencyAsync());
|
||||
|
||||
return users.Where(p => p != null)
|
||||
|
@ -85,9 +57,7 @@ public class UserProfileMentionsResolver(
|
|||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<Note.MentionedUser>> ResolveMentions(
|
||||
UserProfile.Field[]? fields, string? bio, string? host
|
||||
)
|
||||
public async Task<List<Note.MentionedUser>> ResolveMentions(UserProfile.Field[]? fields, string? bio, string? host)
|
||||
{
|
||||
if (fields is not { Length: > 0 } && bio == null) return [];
|
||||
var input = (fields ?? [])
|
||||
|
@ -101,18 +71,7 @@ public class UserProfileMentionsResolver(
|
|||
var mentionNodes = EnumerateMentions(nodes);
|
||||
var users = await mentionNodes
|
||||
.DistinctBy(p => p.Acct)
|
||||
.Select(async p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await userResolver.ResolveAsyncLimited(p.Username, p.Host ?? host,
|
||||
() => _recursionLimit-- <= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Select(async p => await userResolver.ResolveAsyncOrNull(p.Username, p.Host ?? host))
|
||||
.AwaitAllNoConcurrencyAsync();
|
||||
|
||||
return users.Where(p => p != null)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using AsyncKeyedLock;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
|
@ -30,6 +31,12 @@ public class UserService(
|
|||
EmojiService emojiSvc
|
||||
)
|
||||
{
|
||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||
{
|
||||
o.PoolSize = 100;
|
||||
o.PoolInitialFill = 5;
|
||||
});
|
||||
|
||||
private (string Username, string? Host) AcctToTuple(string acct)
|
||||
{
|
||||
if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");
|
||||
|
@ -50,6 +57,16 @@ public class UserService(
|
|||
return await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == query) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
else if (query.StartsWith($"https://{instance.Value.WebDomain}/@"))
|
||||
{
|
||||
query = query[$"https://{instance.Value.WebDomain}/@".Length..];
|
||||
if (query.Split('@').Length != 1)
|
||||
return await GetUserFromQueryAsync($"acct:{query}");
|
||||
|
||||
return await db.Users.IncludeCommonProperties()
|
||||
.FirstOrDefaultAsync(p => p.Username == query.ToLower()) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
return await db.Users
|
||||
|
@ -165,7 +182,7 @@ public class UserService(
|
|||
var processPendingDeletes = await ResolveAvatarAndBanner(user, actor);
|
||||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
await UpdateProfileMentionsInBackground(user, actor);
|
||||
await UpdateProfileMentions(user, actor);
|
||||
return user;
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
|
@ -267,10 +284,12 @@ public class UserService(
|
|||
user.UserProfile.UserHost = user.Host;
|
||||
user.UserProfile.Url = actor.Url?.Link;
|
||||
|
||||
user.UserProfile.MentionsResolved = false;
|
||||
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
await UpdateProfileMentionsInBackground(user, actor);
|
||||
await UpdateProfileMentions(user, actor);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -605,14 +624,19 @@ public class UserService(
|
|||
[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 async Task UpdateProfileMentionsInBackground(User user, ASActor? actor)
|
||||
private async Task UpdateProfileMentions(User user, ASActor? actor)
|
||||
{
|
||||
if (followupTaskSvc.IsBackgroundWorker) return;
|
||||
if (KeyedLocker.IsInUse($"profileMentions:{user.Id}")) return;
|
||||
|
||||
var task = followupTaskSvc.ExecuteTask("UpdateProfileMentionsInBackground", async provider =>
|
||||
{
|
||||
using (await KeyedLocker.LockAsync($"profileMentions:{user.Id}"))
|
||||
{
|
||||
var bgDbContext = provider.GetRequiredService<DatabaseContext>();
|
||||
var bgMentionsResolver = (followupTaskSvc.ServiceProvider ?? provider)
|
||||
.GetRequiredService<UserProfileMentionsResolver>();
|
||||
var bgUser = await bgDbContext.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == user.Id);
|
||||
var bgMentionsResolver = provider.GetRequiredService<UserProfileMentionsResolver>();
|
||||
var userId = user.Id;
|
||||
var bgUser = await bgDbContext.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == userId);
|
||||
if (bgUser?.UserProfile == null) return;
|
||||
|
||||
if (actor != null)
|
||||
|
@ -639,15 +663,16 @@ public class UserService(
|
|||
else
|
||||
{
|
||||
bgUser.UserProfile.Mentions = await bgMentionsResolver.ResolveMentions(bgUser.UserProfile.Fields,
|
||||
bgUser.UserProfile.Description,
|
||||
bgUser.Host);
|
||||
bgUser.UserProfile.Description, bgUser.Host);
|
||||
}
|
||||
|
||||
bgUser.UserProfile.MentionsResolved = true;
|
||||
bgDbContext.Update(bgUser.UserProfile);
|
||||
await bgDbContext.SaveChangesAsync();
|
||||
user = bgUser;
|
||||
}
|
||||
});
|
||||
|
||||
if (followupTaskSvc.IsBackgroundWorker)
|
||||
await task;
|
||||
await task.SafeWaitAsync(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue