[backend/services] Move profile field verification into background-task queue
This commit is contained in:
parent
9d0cb44743
commit
4fe3fe8d2c
2 changed files with 70 additions and 35 deletions
|
@ -47,6 +47,9 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
case UserPurgeJobData userPurgeJob:
|
case UserPurgeJobData userPurgeJob:
|
||||||
await ProcessUserPurgeAsync(userPurgeJob, scope, token);
|
await ProcessUserPurgeAsync(userPurgeJob, scope, token);
|
||||||
break;
|
break;
|
||||||
|
case ProfileFieldUpdateJobData profileFieldUpdateJob:
|
||||||
|
await ProcessProfileFieldUpdateAsync(profileFieldUpdateJob, scope, token);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +302,46 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
|
|
||||||
logger.LogDebug("User {id} purged successfully", jobData.UserId);
|
logger.LogDebug("User {id} purged successfully", jobData.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "Projectables")]
|
||||||
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
||||||
|
private static async Task ProcessProfileFieldUpdateAsync(
|
||||||
|
ProfileFieldUpdateJobData jobData,
|
||||||
|
IServiceProvider scope,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var db = scope.GetRequiredService<DatabaseContext>();
|
||||||
|
var userSvc = scope.GetRequiredService<UserService>();
|
||||||
|
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
|
||||||
|
|
||||||
|
logger.LogDebug("Processing profile field update for user {id}", jobData.UserId);
|
||||||
|
|
||||||
|
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == jobData.UserId, token);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Failed to update profile fields for user {id}: user not found in database", jobData.UserId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.UserProfile == null)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Failed to update profile fields for user {id}: user profile not found in database", jobData.UserId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileUrl = user.Host != null ? user.UserProfile.Url : null;
|
||||||
|
if (user.Host != null && profileUrl == null)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Failed to update profile fields for user {id}: profile URL not found in database", jobData.UserId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await userSvc.VerifyProfileFieldsAsync(user, profileUrl);
|
||||||
|
db.Update(user.UserProfile!);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
|
||||||
|
logger.LogDebug("Profile fields for user {id} updated successfully", jobData.UserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonDerivedType(typeof(DriveFileDeleteJobData), "driveFileDelete")]
|
[JsonDerivedType(typeof(DriveFileDeleteJobData), "driveFileDelete")]
|
||||||
|
@ -307,6 +350,7 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
[JsonDerivedType(typeof(FilterExpiryJobData), "filterExpiry")]
|
[JsonDerivedType(typeof(FilterExpiryJobData), "filterExpiry")]
|
||||||
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
|
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
|
||||||
[JsonDerivedType(typeof(UserPurgeJobData), "userPurge")]
|
[JsonDerivedType(typeof(UserPurgeJobData), "userPurge")]
|
||||||
|
[JsonDerivedType(typeof(ProfileFieldUpdateJobData), "profileFieldUpdate")]
|
||||||
public abstract class BackgroundTaskJobData;
|
public abstract class BackgroundTaskJobData;
|
||||||
|
|
||||||
public class DriveFileDeleteJobData : BackgroundTaskJobData
|
public class DriveFileDeleteJobData : BackgroundTaskJobData
|
||||||
|
@ -339,3 +383,8 @@ public class UserPurgeJobData : BackgroundTaskJobData
|
||||||
{
|
{
|
||||||
[JR] [J("userId")] public required string UserId { get; set; }
|
[JR] [J("userId")] public required string UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ProfileFieldUpdateJobData : BackgroundTaskJobData
|
||||||
|
{
|
||||||
|
[JR] [J("userId")] public required string UserId { get; set; }
|
||||||
|
}
|
|
@ -226,7 +226,7 @@ public class UserService(
|
||||||
var processPendingDeletes = await ResolveAvatarAndBannerAsync(user, actor);
|
var processPendingDeletes = await ResolveAvatarAndBannerAsync(user, actor);
|
||||||
await processPendingDeletes();
|
await processPendingDeletes();
|
||||||
user = await UpdateProfileMentionsAsync(user, actor);
|
user = await UpdateProfileMentionsAsync(user, actor);
|
||||||
UpdateProfileFieldsInBackground(user);
|
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new ProfileFieldUpdateJobData { UserId = user.Id});
|
||||||
UpdateUserPinnedNotesInBackground(actor, user);
|
UpdateUserPinnedNotesInBackground(actor, user);
|
||||||
_ = followupTaskSvc.ExecuteTaskAsync("UpdateInstanceUserCounter", async provider =>
|
_ = followupTaskSvc.ExecuteTaskAsync("UpdateInstanceUserCounter", async provider =>
|
||||||
{
|
{
|
||||||
|
@ -361,7 +361,7 @@ public class UserService(
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await processPendingDeletes();
|
await processPendingDeletes();
|
||||||
user = await UpdateProfileMentionsAsync(user, actor, true);
|
user = await UpdateProfileMentionsAsync(user, actor, true);
|
||||||
UpdateProfileFieldsInBackground(user);
|
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new ProfileFieldUpdateJobData { UserId = user.Id});
|
||||||
UpdateUserPinnedNotesInBackground(actor, user, true);
|
UpdateUserPinnedNotesInBackground(actor, user, true);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
@ -400,7 +400,7 @@ public class UserService(
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
user = await UpdateProfileMentionsAsync(user, null, wait: true);
|
user = await UpdateProfileMentionsAsync(user, null, wait: true);
|
||||||
UpdateProfileFieldsInBackground(user);
|
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new ProfileFieldUpdateJobData { UserId = user.Id});
|
||||||
|
|
||||||
var avatar = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == user.AvatarId);
|
var avatar = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == user.AvatarId);
|
||||||
var banner = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == user.BannerId);
|
var banner = await db.DriveFiles.FirstOrDefaultAsync(p => p.Id == user.BannerId);
|
||||||
|
@ -1170,47 +1170,33 @@ public class UserService(
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "Projectables")]
|
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
||||||
private void UpdateProfileFieldsInBackground(User user)
|
public async Task<User> VerifyProfileFieldsAsync(User user, string? profileUrl)
|
||||||
{
|
{
|
||||||
if (KeyedLocker.IsInUse($"profileFields:{user.Id}")) return;
|
profileUrl ??= user.GetPublicUrl(instance.Value);
|
||||||
|
|
||||||
_ = followupTaskSvc.ExecuteTaskAsync("UpdateProfileFieldsInBackground", async provider =>
|
foreach (var userProfileField in user.UserProfile!.Fields)
|
||||||
{
|
{
|
||||||
using (await KeyedLocker.LockAsync($"profileFields:{user.Id}"))
|
if (!userProfileField.Value.StartsWith("https://")
|
||||||
{
|
|| !Uri.TryCreate(userProfileField.Value, UriKind.Absolute, out var uri))
|
||||||
var bgDbContext = provider.GetRequiredService<DatabaseContext>();
|
continue;
|
||||||
var userId = user.Id;
|
|
||||||
var bgUser = await bgDbContext.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == userId);
|
|
||||||
if (bgUser?.UserProfile == null) return;
|
|
||||||
var profileUrl = user.Host != null ? user.UserProfile?.Url : bgUser.GetPublicUrl(instance.Value);
|
|
||||||
if (profileUrl == null) return;
|
|
||||||
|
|
||||||
foreach (var userProfileField in bgUser.UserProfile.Fields)
|
var res = await httpClient.GetAsync(uri);
|
||||||
{
|
|
||||||
if (!userProfileField.Value.StartsWith("https://") || !Uri.TryCreate(userProfileField.Value, UriKind.Absolute, out var uri))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var res = await httpClient.GetAsync(uri);
|
if (!res.IsSuccessStatusCode || res.Content.Headers.ContentType?.MediaType != "text/html")
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!res.IsSuccessStatusCode || res.Content.Headers.ContentType?.MediaType != "text/html")
|
var html = await res.Content.ReadAsStringAsync();
|
||||||
continue;
|
var document = await new HtmlParser().ParseDocumentAsync(html);
|
||||||
|
|
||||||
var html = await res.Content.ReadAsStringAsync();
|
userProfileField.IsVerified =
|
||||||
var document = await new HtmlParser().ParseDocumentAsync(html);
|
document.Links.Any(a => (a.GetAttribute("rel")?.Contains("me")
|
||||||
|
?? false)
|
||||||
|
&& a.GetAttribute("href") == profileUrl
|
||||||
|
|| a.GetAttribute("href") == user.GetUriOrPublicUri(instance.Value));
|
||||||
|
}
|
||||||
|
|
||||||
userProfileField.IsVerified =
|
return user;
|
||||||
document.Links.Any(a => (a.GetAttribute("rel")?.Contains("me")
|
|
||||||
?? false)
|
|
||||||
&& a.GetAttribute("href") == profileUrl
|
|
||||||
|| a.GetAttribute("href") == user.GetUriOrPublicUri(instance.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
bgDbContext.Update(bgUser.UserProfile);
|
|
||||||
await bgDbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> ResolveHashtags(IMfmNode[]? parsedText, ASActor? actor = null)
|
private List<string> ResolveHashtags(IMfmNode[]? parsedText, ASActor? actor = null)
|
||||||
|
|
Loading…
Add table
Reference in a new issue