diff --git a/Iceshrimp.Backend/Core/Database/Migrations/20240617151122_AddFollowRelationshipIdColumns.cs b/Iceshrimp.Backend/Core/Database/Migrations/20240617151122_AddFollowRelationshipIdColumns.cs new file mode 100644 index 00000000..27a2eeb8 --- /dev/null +++ b/Iceshrimp.Backend/Core/Database/Migrations/20240617151122_AddFollowRelationshipIdColumns.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Iceshrimp.Backend.Core.Database.Migrations +{ + /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240617151122_AddFollowRelationshipIdColumns")] + public partial class AddFollowRelationshipIdColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "relationshipId", + table: "following", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "relationshipId", + table: "follow_request", + type: "uuid", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "relationshipId", + table: "following"); + + migrationBuilder.DropColumn( + name: "relationshipId", + table: "follow_request"); + } + } +} diff --git a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs index f2b38800..944f8b9d 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1158,6 +1158,10 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("followerSharedInbox") .HasComment("[Denormalized]"); + b.Property("RelationshipId") + .HasColumnType("uuid") + .HasColumnName("relationshipId"); + b.Property("RequestId") .HasMaxLength(128) .HasColumnType("character varying(128)") @@ -1238,6 +1242,10 @@ namespace Iceshrimp.Backend.Core.Database.Migrations .HasColumnName("followerSharedInbox") .HasComment("[Denormalized]"); + b.Property("RelationshipId") + .HasColumnType("uuid") + .HasColumnName("relationshipId"); + b.HasKey("Id"); b.HasIndex("CreatedAt"); diff --git a/Iceshrimp.Backend/Core/Database/Tables/FollowRequest.cs b/Iceshrimp.Backend/Core/Database/Tables/FollowRequest.cs index 44cf970d..dd1df03e 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/FollowRequest.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/FollowRequest.cs @@ -36,6 +36,9 @@ public class FollowRequest : IEntity [Column("requestId")] [StringLength(128)] public string? RequestId { get; set; } + + [Column("relationshipId")] + public Guid? RelationshipId { get; set; } /// /// [Denormalized] diff --git a/Iceshrimp.Backend/Core/Database/Tables/Following.cs b/Iceshrimp.Backend/Core/Database/Tables/Following.cs index 9683c46c..964491e9 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/Following.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/Following.cs @@ -79,6 +79,9 @@ public class Following [Column("followeeSharedInbox")] [StringLength(512)] public string? FolloweeSharedInbox { get; set; } + + [Column("relationshipId")] + public Guid? RelationshipId { get; set; } [ForeignKey(nameof(FolloweeId))] [InverseProperty(nameof(User.IncomingFollowRelationships))] diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs index 53641abe..fd86bc67 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityRenderer.cs @@ -109,7 +109,7 @@ public class ActivityRenderer( return res; } - public ASFollow RenderFollow(User follower, User followee) + public ASFollow RenderFollow(User follower, User followee, Guid? relationshipId) { if (follower.Host == null && followee.Host == null) throw GracefulException.BadRequest("Refusing to render follow activity between two remote users"); @@ -118,10 +118,10 @@ public class ActivityRenderer( return RenderFollow(userRenderer.RenderLite(follower), userRenderer.RenderLite(followee), - RenderFollowId(follower, followee)); + RenderFollowId(follower, followee, relationshipId)); } - public ASActivity RenderUnfollow(User follower, User followee) + public ASActivity RenderUnfollow(User follower, User followee, Guid? relationshipId) { if (follower.Host == null && followee.Host == null) throw GracefulException.BadRequest("Refusing to render unfollow activity between two remote users"); @@ -132,13 +132,13 @@ public class ActivityRenderer( { var actor = userRenderer.RenderLite(follower); var obj = userRenderer.RenderLite(followee); - return RenderUndo(actor, RenderFollow(actor, obj, RenderFollowId(follower, followee))); + return RenderUndo(actor, RenderFollow(actor, obj, RenderFollowId(follower, followee, relationshipId))); } else { var actor = userRenderer.RenderLite(followee); var obj = userRenderer.RenderLite(follower); - return RenderReject(actor, RenderFollow(actor, obj, RenderFollowId(follower, followee))); + return RenderReject(actor, RenderFollow(actor, obj, RenderFollowId(follower, followee, relationshipId))); } } @@ -178,8 +178,8 @@ public class ActivityRenderer( }; [SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "This only makes sense for users")] - private string RenderFollowId(User follower, User followee) => - $"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}/{Guid.NewGuid().ToStringLower()}"; + private string RenderFollowId(User follower, User followee, Guid? relationshipId) => + $"https://{config.Value.WebDomain}/follows/{follower.Id}/{followee.Id}/{(relationshipId ?? Guid.NewGuid()).ToStringLower()}"; public static ASAnnounce RenderAnnounce( ASNote note, ASActor actor, List to, List cc, string uri diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 9dd872f9..0eb4d364 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -561,7 +561,8 @@ public class UserService( FollowerInbox = request.FollowerInbox, FolloweeInbox = request.FolloweeInbox, FollowerSharedInbox = request.FollowerSharedInbox, - FolloweeSharedInbox = request.FolloweeSharedInbox + FolloweeSharedInbox = request.FolloweeSharedInbox, + RelationshipId = request.RelationshipId }; await db.Users.Where(p => p.Id == request.Follower.Id) @@ -645,13 +646,16 @@ public class UserService( if (follower.Host != null && followee.Host != null) throw GracefulException.UnprocessableEntity("Cannot process follow between two remote users"); - if (followee.Host != null) + Guid? relationshipId = null; + + if (followee.IsRemoteUser) { - var activity = activityRenderer.RenderFollow(follower, followee); + relationshipId = Guid.NewGuid(); + var activity = activityRenderer.RenderFollow(follower, followee, relationshipId); await deliverSvc.DeliverToAsync(activity, follower, followee); } - if (follower.Host != null) + if (follower.IsRemoteUser) { if (requestId == null) throw GracefulException.UnprocessableEntity("Cannot process remote follow without requestId"); @@ -688,7 +692,8 @@ public class UserService( FolloweeInbox = followee.Inbox, FollowerInbox = follower.Inbox, FolloweeSharedInbox = followee.SharedInbox, - FollowerSharedInbox = follower.SharedInbox + FollowerSharedInbox = follower.SharedInbox, + RelationshipId = relationshipId }; await db.AddAsync(request); @@ -764,13 +769,18 @@ public class UserService( /// public async Task UnfollowUserAsync(User user, User followee) { - if ((followee.PrecomputedIsFollowedBy ?? false) || (followee.PrecomputedIsRequestedBy ?? false)) + if (((followee.PrecomputedIsFollowedBy ?? false) || (followee.PrecomputedIsRequestedBy ?? false)) && + followee.IsRemoteUser) { - if (followee.Host != null) - { - var activity = activityRenderer.RenderUnfollow(user, followee); - await deliverSvc.DeliverToAsync(activity, user, followee); - } + var relationshipId = await db.Followings.Where(p => p.Follower == user && p.Followee == followee) + .Select(p => p.RelationshipId) + .FirstOrDefaultAsync() ?? + await db.FollowRequests.Where(p => p.Follower == user && p.Followee == followee) + .Select(p => p.RelationshipId) + .FirstOrDefaultAsync(); + + var activity = activityRenderer.RenderUnfollow(user, followee, relationshipId); + await deliverSvc.DeliverToAsync(activity, user, followee); } if (followee.PrecomputedIsFollowedBy ?? false) @@ -787,7 +797,7 @@ public class UserService( db.RemoveRange(followings); await db.SaveChangesAsync(); - if (followee.Host != null) + if (followee.IsRemoteUser) { _ = followupTaskSvc.ExecuteTask("DecrementInstanceOutgoingFollowsCounter", async provider => {