diff --git a/src/main/java/net/javahippie/fitpub/controller/CommentController.java b/src/main/java/net/javahippie/fitpub/controller/CommentController.java index b54d349..c2f88bc 100644 --- a/src/main/java/net/javahippie/fitpub/controller/CommentController.java +++ b/src/main/java/net/javahippie/fitpub/controller/CommentController.java @@ -201,13 +201,27 @@ public class CommentController { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - // Soft delete + // Soft delete locally comment.setDeleted(true); commentRepository.save(comment); log.info("User {} deleted comment {}", user.getUsername(), commentId); - // TODO: Send ActivityPub Delete activity to followers if activity is public + // Federate the deletion to remote followers so they tombstone their cached + // copy of the Note. The visibility check mirrors createComment(): the + // original Create was only sent if the parent activity was PUBLIC or + // FOLLOWERS, so the Delete needs to follow the same audience rule. The + // commentUri must match exactly what was used in the original Create + // activity, otherwise remote servers won't be able to match the tombstone + // to the cached note. + Activity activity = activityRepository.findById(activityId).orElse(null); + if (activity != null + && (activity.getVisibility() == Activity.Visibility.PUBLIC + || activity.getVisibility() == Activity.Visibility.FOLLOWERS)) { + String commentUri = baseUrl + "/activities/" + activityId + "/comments/" + commentId; + federationService.sendDeleteActivity(commentUri, user); + log.info("Sent Delete federation for comment {} on activity {}", commentId, activityId); + } return ResponseEntity.noContent().build(); } diff --git a/src/main/java/net/javahippie/fitpub/service/FederationService.java b/src/main/java/net/javahippie/fitpub/service/FederationService.java index ff41cf4..8a1efea 100644 --- a/src/main/java/net/javahippie/fitpub/service/FederationService.java +++ b/src/main/java/net/javahippie/fitpub/service/FederationService.java @@ -311,10 +311,16 @@ public class FederationService { } /** - * Get all follower inbox URLs for a local user. + * Get the inbox URLs of all remote followers of a local user. + * + *
Local followers are deliberately skipped: they live on the same server and
+ * see new activities via the local timeline queries, so there is nothing to
+ * federate to them. Without this filter, every call would attempt to
+ * {@code fetchRemoteActor(null)} for each local follower row, log a stack trace
+ * at ERROR level, and then drop the resulting null from the inbox list.
*
* @param userId the local user's ID
- * @return list of inbox URLs
+ * @return list of remote follower inbox URLs (deduplicated, shared inbox preferred)
*/
@Transactional(readOnly = true)
public List Runs on the {@code taskExecutor} pool. Used for both activity deletes and
+ * comment (Note) deletes — the user-facing HTTP response shouldn't wait on the
+ * federation fanout to remote follower inboxes.
+ *
+ * @param objectUri the URI of the deleted object (e.g., activity URI or comment Note URI)
* @param sender the user who deleted the object
*/
+ @Async("taskExecutor")
public void sendDeleteActivity(String objectUri, User sender) {
try {
String deleteId = baseUrl + "/activities/delete/" + UUID.randomUUID();