[backend/federation] Refactor NoteServer to take a User param instead of an ASActor to save DB roundtrips
This commit is contained in:
parent
19ffbe7814
commit
c35d503c12
3 changed files with 65 additions and 76 deletions
|
@ -65,7 +65,7 @@ public class ActivityHandlerService(
|
|||
//TODO: should we handle other types of creates?
|
||||
if (activity.Object is not ASNote note)
|
||||
throw GracefulException.UnprocessableEntity("Create activity object is invalid");
|
||||
await noteSvc.ProcessNoteAsync(note, activity.Actor);
|
||||
await noteSvc.ProcessNoteAsync(note, resolvedActor);
|
||||
return;
|
||||
}
|
||||
case ASDelete:
|
||||
|
@ -86,7 +86,7 @@ public class ActivityHandlerService(
|
|||
throw GracefulException.UnprocessableEntity("Delete activity object is invalid");
|
||||
if (await db.Notes.AnyAsync(p => p.Uri == tombstone.Id))
|
||||
{
|
||||
await noteSvc.DeleteNoteAsync(tombstone, activity.Actor);
|
||||
await noteSvc.DeleteNoteAsync(tombstone, resolvedActor);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -105,28 +105,28 @@ public class ActivityHandlerService(
|
|||
{
|
||||
if (activity.Object is not ASActor obj)
|
||||
throw GracefulException.UnprocessableEntity("Follow activity object is invalid");
|
||||
await FollowAsync(obj, activity.Actor, activity.Id);
|
||||
await FollowAsync(obj, activity.Actor, resolvedActor, activity.Id);
|
||||
return;
|
||||
}
|
||||
case ASUnfollow:
|
||||
{
|
||||
if (activity.Object is not ASActor obj)
|
||||
throw GracefulException.UnprocessableEntity("Unfollow activity object is invalid");
|
||||
await UnfollowAsync(obj, activity.Actor);
|
||||
await UnfollowAsync(obj, resolvedActor);
|
||||
return;
|
||||
}
|
||||
case ASAccept:
|
||||
{
|
||||
if (activity.Object is not ASFollow obj)
|
||||
throw GracefulException.UnprocessableEntity("Accept activity object is invalid");
|
||||
await AcceptAsync(obj, activity.Actor);
|
||||
await AcceptAsync(obj, resolvedActor);
|
||||
return;
|
||||
}
|
||||
case ASReject:
|
||||
{
|
||||
if (activity.Object is not ASFollow obj)
|
||||
throw GracefulException.UnprocessableEntity("Reject activity object is invalid");
|
||||
await RejectAsync(obj, activity.Actor);
|
||||
await RejectAsync(obj, resolvedActor);
|
||||
return;
|
||||
}
|
||||
case ASUndo:
|
||||
|
@ -134,10 +134,10 @@ public class ActivityHandlerService(
|
|||
switch (activity.Object)
|
||||
{
|
||||
case ASFollow { Object: ASActor followee }:
|
||||
await UnfollowAsync(followee, activity.Actor);
|
||||
await UnfollowAsync(followee, resolvedActor);
|
||||
return;
|
||||
case ASLike { Object: ASNote likedNote }:
|
||||
await noteSvc.UnlikeNoteAsync(likedNote, activity.Actor);
|
||||
await noteSvc.UnlikeNoteAsync(likedNote, resolvedActor);
|
||||
return;
|
||||
default:
|
||||
throw GracefulException.UnprocessableEntity("Undo activity object is invalid");
|
||||
|
@ -147,7 +147,7 @@ public class ActivityHandlerService(
|
|||
{
|
||||
if (activity.Object is not ASNote note)
|
||||
throw GracefulException.UnprocessableEntity("Like activity object is invalid");
|
||||
await noteSvc.LikeNoteAsync(note, activity.Actor);
|
||||
await noteSvc.LikeNoteAsync(note, resolvedActor);
|
||||
return;
|
||||
}
|
||||
case ASUpdate:
|
||||
|
@ -160,7 +160,7 @@ public class ActivityHandlerService(
|
|||
await userSvc.UpdateUserAsync(resolvedActor, actor);
|
||||
return;
|
||||
case ASNote note:
|
||||
await noteSvc.ProcessNoteUpdateAsync(note, activity.Actor, resolvedActor);
|
||||
await noteSvc.ProcessNoteUpdateAsync(note, resolvedActor);
|
||||
return;
|
||||
default:
|
||||
throw GracefulException.UnprocessableEntity("Update activity object is invalid");
|
||||
|
@ -233,7 +233,8 @@ public class ActivityHandlerService(
|
|||
throw GracefulException.UnprocessableEntity("Invalid or unsupported announce object");
|
||||
|
||||
var dbNote = await noteSvc.ResolveNoteAsync(note.Id, note.VerifiedFetch ? note : null);
|
||||
var renote = await noteSvc.CreateNoteAsync(resolvedActor, announce.GetVisibility(activity.Actor), renote: dbNote);
|
||||
var renote = await noteSvc.CreateNoteAsync(resolvedActor, announce.GetVisibility(activity.Actor),
|
||||
renote: dbNote);
|
||||
await notificationSvc.GenerateRenoteNotification(renote);
|
||||
return;
|
||||
}
|
||||
|
@ -244,9 +245,8 @@ public class ActivityHandlerService(
|
|||
|
||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall",
|
||||
Justification = "Projectable functions can very much be translated to SQL")]
|
||||
private async Task FollowAsync(ASActor followeeActor, ASActor followerActor, string requestId)
|
||||
private async Task FollowAsync(ASActor followeeActor, ASActor followerActor, User follower, string requestId)
|
||||
{
|
||||
var follower = await userResolver.ResolveAsync(followerActor.Id);
|
||||
var followee = await userResolver.ResolveAsync(followeeActor.Id);
|
||||
|
||||
if (followee.Host != null) throw new Exception("Cannot process follow for remote followee");
|
||||
|
@ -324,10 +324,9 @@ public class ActivityHandlerService(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task UnfollowAsync(ASActor followeeActor, ASActor followerActor)
|
||||
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
||||
{
|
||||
//TODO: send reject? or do we not want to copy that part of the old ap core
|
||||
var follower = await userResolver.ResolveAsync(followerActor.Id);
|
||||
var followee = await userResolver.ResolveAsync(followeeActor.Id);
|
||||
|
||||
await db.FollowRequests.Where(p => p.Follower == follower && p.Followee == followee).ExecuteDeleteAsync();
|
||||
|
@ -350,33 +349,32 @@ public class ActivityHandlerService(
|
|||
}
|
||||
}
|
||||
|
||||
private async Task AcceptAsync(ASFollow obj, ASActor actor)
|
||||
private async Task AcceptAsync(ASFollow obj, User actor)
|
||||
{
|
||||
var prefix = $"https://{config.Value.WebDomain}/follows/";
|
||||
if (!obj.Id.StartsWith(prefix))
|
||||
throw GracefulException.UnprocessableEntity($"Object id '{obj.Id}' not a valid follow request id");
|
||||
|
||||
var resolvedActor = await userResolver.ResolveAsync(actor.Id);
|
||||
var ids = obj.Id[prefix.Length..].TrimEnd('/').Split("/");
|
||||
if (ids.Length != 2 || ids[1] != resolvedActor.Id)
|
||||
var ids = obj.Id[prefix.Length..].TrimEnd('/').Split("/");
|
||||
if (ids.Length != 2 || ids[1] != actor.Id)
|
||||
throw GracefulException
|
||||
.UnprocessableEntity($"Actor id '{resolvedActor.Id}' doesn't match followee id '{ids[1]}'");
|
||||
.UnprocessableEntity($"Actor id '{actor.Id}' doesn't match followee id '{ids[1]}'");
|
||||
|
||||
var request = await db.FollowRequests
|
||||
.Include(p => p.Follower.UserProfile)
|
||||
.Include(p => p.Followee.UserProfile)
|
||||
.FirstOrDefaultAsync(p => p.Followee == resolvedActor && p.FollowerId == ids[0]);
|
||||
.FirstOrDefaultAsync(p => p.Followee == actor && p.FollowerId == ids[0]);
|
||||
|
||||
if (request == null)
|
||||
throw GracefulException
|
||||
.UnprocessableEntity($"No follow request matching follower '{ids[0]}' and followee '{resolvedActor.Id}' found");
|
||||
.UnprocessableEntity($"No follow request matching follower '{ids[0]}' and followee '{actor.Id}' found");
|
||||
|
||||
var following = new Following
|
||||
{
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Follower = request.Follower,
|
||||
Followee = resolvedActor,
|
||||
Followee = actor,
|
||||
FollowerHost = request.FollowerHost,
|
||||
FolloweeHost = request.FolloweeHost,
|
||||
FollowerInbox = request.FollowerInbox,
|
||||
|
@ -385,7 +383,7 @@ public class ActivityHandlerService(
|
|||
FolloweeSharedInbox = request.FolloweeSharedInbox
|
||||
};
|
||||
|
||||
resolvedActor.FollowersCount++;
|
||||
actor.FollowersCount++;
|
||||
request.Follower.FollowingCount++;
|
||||
|
||||
db.Remove(request);
|
||||
|
@ -394,23 +392,22 @@ public class ActivityHandlerService(
|
|||
await notificationSvc.GenerateFollowRequestAcceptedNotification(request);
|
||||
}
|
||||
|
||||
private async Task RejectAsync(ASFollow follow, ASActor actor)
|
||||
private async Task RejectAsync(ASFollow follow, User actor)
|
||||
{
|
||||
if (follow is not { Actor: not null })
|
||||
throw GracefulException.UnprocessableEntity("Refusing to reject object with invalid follow object");
|
||||
|
||||
var resolvedActor = await userResolver.ResolveAsync(actor.Id);
|
||||
var resolvedFollower = await userResolver.ResolveAsync(follow.Actor.Id);
|
||||
if (resolvedFollower is not { Host: null })
|
||||
throw GracefulException.UnprocessableEntity("Refusing to reject remote follow");
|
||||
|
||||
await db.FollowRequests.Where(p => p.Followee == resolvedActor && p.Follower == resolvedFollower)
|
||||
await db.FollowRequests.Where(p => p.Followee == actor && p.Follower == resolvedFollower)
|
||||
.ExecuteDeleteAsync();
|
||||
var count = await db.Followings.Where(p => p.Followee == resolvedActor && p.Follower == resolvedFollower)
|
||||
var count = await db.Followings.Where(p => p.Followee == actor && p.Follower == resolvedFollower)
|
||||
.ExecuteDeleteAsync();
|
||||
if (count > 0)
|
||||
{
|
||||
resolvedActor.FollowersCount -= count;
|
||||
actor.FollowersCount -= count;
|
||||
resolvedFollower.FollowingCount -= count;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
@ -418,7 +415,7 @@ public class ActivityHandlerService(
|
|||
await db.Notifications
|
||||
.Where(p => p.Type == Notification.NotificationType.FollowRequestAccepted)
|
||||
.Where(p => p.Notifiee == resolvedFollower &&
|
||||
p.Notifier == resolvedActor)
|
||||
p.Notifier == actor)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,25 +61,28 @@ public class ASNote : ASObject
|
|||
|
||||
public bool VerifiedFetch = false;
|
||||
|
||||
public Note.NoteVisibility GetVisibility(ASActor actor)
|
||||
public Note.NoteVisibility GetVisibility(User actor)
|
||||
{
|
||||
if (actor.Host == null) throw new Exception("Can't get recipients for local actor");
|
||||
|
||||
if (To.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))
|
||||
return Note.NoteVisibility.Public;
|
||||
if (Cc.Any(p => p.Id == $"{Constants.ActivityStreamsNs}#Public"))
|
||||
return Note.NoteVisibility.Home;
|
||||
if (To.Any(p => p.Id is not null && p.Id == (actor.Followers?.Id ?? actor.Id + "/followers")))
|
||||
if (To.Any(p => p.Id is not null && p.Id == (actor.FollowersUri ?? actor.Uri + "/followers")))
|
||||
return Note.NoteVisibility.Followers;
|
||||
|
||||
return Note.NoteVisibility.Specified;
|
||||
}
|
||||
|
||||
public List<string> GetRecipients(ASActor actor)
|
||||
public List<string> GetRecipients(User actor)
|
||||
{
|
||||
if (actor.Host == null) throw new Exception("Can't get recipients for local actor");
|
||||
return To.Concat(Cc)
|
||||
.Select(p => p.Id)
|
||||
.Distinct()
|
||||
.Where(p => p != $"{Constants.ActivityStreamsNs}#Public" &&
|
||||
p != (actor.Followers?.Id ?? actor.Id + "/followers"))
|
||||
p != (actor.FollowersUri ?? actor.Uri + "/followers"))
|
||||
.Where(p => p != null)
|
||||
.Select(p => p!)
|
||||
.ToList();
|
||||
|
|
|
@ -228,7 +228,7 @@ public class NoteService(
|
|||
await deliverSvc.DeliverToFollowersAsync(activity, note.User, recipients);
|
||||
}
|
||||
|
||||
public async Task DeleteNoteAsync(ASTombstone note, ASActor actor)
|
||||
public async Task DeleteNoteAsync(ASTombstone note, User actor)
|
||||
{
|
||||
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery (it doesn't know about IncludeCommonProperties())
|
||||
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||
|
@ -238,24 +238,22 @@ public class NoteService(
|
|||
return;
|
||||
}
|
||||
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
|
||||
// ReSharper disable once EntityFramework.NPlusOne.IncompleteDataUsage (same reason as above)
|
||||
if (dbNote.User != user)
|
||||
if (dbNote.User != actor)
|
||||
{
|
||||
logger.LogDebug("Note '{id}' isn't owned by actor requesting its deletion, skipping", note.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, user.Id);
|
||||
logger.LogDebug("Deleting note '{id}' owned by {userId}", note.Id, actor.Id);
|
||||
|
||||
user.NotesCount--;
|
||||
actor.NotesCount--;
|
||||
db.Remove(dbNote);
|
||||
eventSvc.RaiseNoteDeleted(this, dbNote);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<Note> ProcessNoteAsync(ASNote note, ASActor actor)
|
||||
public async Task<Note> ProcessNoteAsync(ASNote note, User actor)
|
||||
{
|
||||
var dbHit = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||
|
||||
|
@ -269,15 +267,12 @@ public class NoteService(
|
|||
_resolverHistory.Add(note.Id);
|
||||
logger.LogDebug("Creating note: {id}", note.Id);
|
||||
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
logger.LogDebug("Resolved user to {userId}", user.Id);
|
||||
|
||||
// Validate note
|
||||
if (note.AttributedTo is not { Count: 1 } || note.AttributedTo[0].Id != user.Uri)
|
||||
if (note.AttributedTo is not { Count: 1 } || note.AttributedTo[0].Id != actor.Uri)
|
||||
throw GracefulException.UnprocessableEntity("User.Uri doesn't match Note.AttributedTo");
|
||||
if (user.Uri == null)
|
||||
if (actor.Uri == null)
|
||||
throw GracefulException.UnprocessableEntity("User.Uri is null");
|
||||
if (new Uri(note.Id).IdnHost != new Uri(user.Uri).IdnHost)
|
||||
if (new Uri(note.Id).IdnHost != new Uri(actor.Uri).IdnHost)
|
||||
throw GracefulException.UnprocessableEntity("User.Uri host doesn't match Note.Id host");
|
||||
if (!note.Id.StartsWith("https://"))
|
||||
throw GracefulException.UnprocessableEntity("Note.Id schema is invalid");
|
||||
|
@ -285,7 +280,7 @@ public class NoteService(
|
|||
throw GracefulException.UnprocessableEntity("Note.Url schema is invalid");
|
||||
if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3))
|
||||
throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical");
|
||||
if (user.IsSuspended)
|
||||
if (actor.IsSuspended)
|
||||
throw GracefulException.Forbidden("User is suspended");
|
||||
|
||||
//TODO: resolve anything related to the note as well (attachments, emoji, etc)
|
||||
|
@ -303,9 +298,9 @@ public class NoteService(
|
|||
Url = note.Url?.Id, //FIXME: this doesn't seem to work yet
|
||||
Text = note.MkContent ?? await mfmConverter.FromHtmlAsync(note.Content, mentions),
|
||||
Cw = note.Summary,
|
||||
UserId = user.Id,
|
||||
UserId = actor.Id,
|
||||
CreatedAt = createdAt,
|
||||
UserHost = user.Host,
|
||||
UserHost = actor.Host,
|
||||
Visibility = note.GetVisibility(actor),
|
||||
Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id) : null
|
||||
};
|
||||
|
@ -343,14 +338,14 @@ public class NoteService(
|
|||
}
|
||||
|
||||
var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null;
|
||||
var files = await ProcessAttachmentsAsync(note.Attachments, user, sensitive);
|
||||
var files = await ProcessAttachmentsAsync(note.Attachments, actor, sensitive);
|
||||
if (files.Count != 0)
|
||||
{
|
||||
dbNote.FileIds = files.Select(p => p.Id).ToList();
|
||||
dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList();
|
||||
}
|
||||
|
||||
user.NotesCount++;
|
||||
actor.NotesCount++;
|
||||
if (dbNote.Reply != null) dbNote.Reply.RepliesCount++;
|
||||
await db.Notes.AddAsync(dbNote);
|
||||
await db.SaveChangesAsync();
|
||||
|
@ -364,13 +359,12 @@ public class NoteService(
|
|||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage",
|
||||
Justification = "Inspection doesn't understand IncludeCommonProperties()")]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")]
|
||||
public async Task<Note> ProcessNoteUpdateAsync(ASNote note, ASActor actor, User? resolvedActor = null)
|
||||
public async Task<Note> ProcessNoteUpdateAsync(ASNote note, User actor)
|
||||
{
|
||||
var dbNote = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||
if (dbNote == null) return await ProcessNoteAsync(note, actor);
|
||||
|
||||
resolvedActor ??= await userResolver.ResolveAsync(actor.Id);
|
||||
if (dbNote.User != resolvedActor)
|
||||
if (dbNote.User != actor)
|
||||
throw GracefulException.UnprocessableEntity("Refusing to update note of user other than actor");
|
||||
if (dbNote.User.IsSuspended)
|
||||
throw GracefulException.Forbidden("User is suspended");
|
||||
|
@ -425,7 +419,7 @@ public class NoteService(
|
|||
|
||||
//TODO: handle updated alt text et al
|
||||
var sensitive = (note.Sensitive ?? false) || dbNote.Cw != null;
|
||||
var files = await ProcessAttachmentsAsync(note.Attachments, resolvedActor, sensitive);
|
||||
var files = await ProcessAttachmentsAsync(note.Attachments, actor, sensitive);
|
||||
dbNote.FileIds = files.Select(p => p.Id).ToList();
|
||||
dbNote.AttachedFileTypes = files.Select(p => p.Type).ToList();
|
||||
|
||||
|
@ -574,8 +568,7 @@ public class NoteService(
|
|||
if (res != null) return res;
|
||||
}
|
||||
|
||||
//TODO: we don't need to fetch the actor every time, we can use userResolver here
|
||||
var actor = await fetchSvc.FetchActorAsync(attrTo.Id);
|
||||
var actor = await userResolver.ResolveAsync(attrTo.Id);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -615,38 +608,34 @@ public class NoteService(
|
|||
}
|
||||
}
|
||||
|
||||
public async Task UnlikeNoteAsync(Note note, User user)
|
||||
public async Task UnlikeNoteAsync(Note note, User actor)
|
||||
{
|
||||
var count = await db.NoteLikes.Where(p => p.Note == note && p.User == user).ExecuteDeleteAsync();
|
||||
var count = await db.NoteLikes.Where(p => p.Note == note && p.User == actor).ExecuteDeleteAsync();
|
||||
if (count == 0) return;
|
||||
if (user.Host == null && note.UserHost != null)
|
||||
if (actor.Host == null && note.UserHost != null)
|
||||
{
|
||||
var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(user),
|
||||
activityRenderer.RenderLike(note, user));
|
||||
await deliverSvc.DeliverToFollowersAsync(activity, user, [note.User]);
|
||||
var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(actor),
|
||||
activityRenderer.RenderLike(note, actor));
|
||||
await deliverSvc.DeliverToFollowersAsync(activity, actor, [note.User]);
|
||||
}
|
||||
|
||||
eventSvc.RaiseNoteUnliked(this, note, user);
|
||||
eventSvc.RaiseNoteUnliked(this, note, actor);
|
||||
await db.Notifications
|
||||
.Where(p => p.Type == Notification.NotificationType.Like &&
|
||||
p.Notifiee == note.User &&
|
||||
p.Notifier == user)
|
||||
p.Notifier == actor)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
public async Task LikeNoteAsync(ASNote note, ASActor actor)
|
||||
public async Task LikeNoteAsync(ASNote note, User actor)
|
||||
{
|
||||
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot register like for unknown note");
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
|
||||
await LikeNoteAsync(dbNote, user);
|
||||
await LikeNoteAsync(dbNote, actor);
|
||||
}
|
||||
|
||||
public async Task UnlikeNoteAsync(ASNote note, ASActor actor)
|
||||
public async Task UnlikeNoteAsync(ASNote note, User actor)
|
||||
{
|
||||
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot unregister like for unknown note");
|
||||
var user = await userResolver.ResolveAsync(actor.Id);
|
||||
|
||||
await UnlikeNoteAsync(dbNote, user);
|
||||
await UnlikeNoteAsync(dbNote, actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue