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 =>
{