diff --git a/Iceshrimp.Backend/Controllers/Web/UserController.cs b/Iceshrimp.Backend/Controllers/Web/UserController.cs index f07bf2f4..cd55ec2b 100644 --- a/Iceshrimp.Backend/Controllers/Web/UserController.cs +++ b/Iceshrimp.Backend/Controllers/Web/UserController.cs @@ -141,6 +141,25 @@ public class UserController( await userSvc.FollowUserAsync(user, followee); } + [HttpPost("{id}/remove_from_followers")] + [ProducesResults(HttpStatusCode.OK)] + [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)] + public async Task RemoveFromFollowers(string id) + { + var user = HttpContext.GetUserOrFail(); + if (user.Id == id) + throw GracefulException.BadRequest("You cannot unfollow yourself"); + + var follower = await db.Users + .Where(p => p.Id == id) + .IncludeCommonProperties() + .PrecomputeRelationshipData(user) + .FirstOrDefaultAsync() ?? + throw GracefulException.RecordNotFound(); + + await userSvc.RemoveFromFollowersAsync(user, follower); + } + [HttpPost("{id}/unfollow")] [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)] public async Task UnfollowUser(string id) diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index ffe404bf..81085245 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -874,6 +874,61 @@ public class UserService( } } + /// + /// Make sure to call .PrecomputeRelationshipData(user) on the database query for the followee + /// + public async Task RemoveFromFollowersAsync(User user, User follower) + { + if ((follower.PrecomputedIsFollowing ?? false) && follower.IsRemoteUser) + { + var followingId = await db.Followings + .Where(p => p.Followee == user && p.Follower == follower) + .Select(p => p.Id) + .FirstAsync(); + + var accept = activityRenderer.RenderAccept(user, follower, followingId); + var activity = activityRenderer.RenderUndo(userRenderer.RenderLite(user), accept); + await deliverSvc.DeliverToAsync(activity, user, follower); + } + + if (follower.PrecomputedIsFollowing ?? false) + { + var followers = await db.Followings + .Where(p => p.Followee == user && p.Follower == follower) + .ToListAsync(); + + await db.Users + .Where(p => p.Id == user.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowersCount, + i => i.FollowersCount - followers.Count)); + + await db.Users + .Where(p => p.Id == follower.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.FollowingCount, + i => i.FollowingCount - followers.Count)); + + db.RemoveRange(followers); + await db.SaveChangesAsync(); + + if (follower.IsRemoteUser) + { + _ = followupTaskSvc.ExecuteTask("DecrementInstanceIncomingFollowsCounter", async provider => + { + var bgDb = provider.GetRequiredService(); + var bgInstanceSvc = provider.GetRequiredService(); + var dbInstance = + await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(follower); + await bgDb.Instances.Where(p => p.Id == dbInstance.Id) + .ExecuteUpdateAsync(p => p.SetProperty(i => i.IncomingFollows, + i => i.IncomingFollows - 1)); + }); + } + + follower.PrecomputedIsFollowedBy = false; + eventSvc.RaiseUserUnfollowed(this, follower, user); + } + } + /// /// Make sure to call .PrecomputeRelationshipData(user) on the database query for the followee ///