diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs index f26ddfea..b2843df4 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityHandlerService.cs @@ -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(); } -} \ No newline at end of file +} diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs index c373b82c..e0a4cb48 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs @@ -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 GetRecipients(ASActor actor) + public List 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(); diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index d156badd..3c2f010a 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -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 ProcessNoteAsync(ASNote note, ASActor actor) + public async Task 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 ProcessNoteUpdateAsync(ASNote note, ASActor actor, User? resolvedActor = null) + public async Task 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); } -} \ No newline at end of file +}