Put Federation on async executor

This commit is contained in:
Tim Zöller 2026-04-07 19:40:04 +02:00
parent fde80672f2
commit 0e32aab244
2 changed files with 31 additions and 5 deletions

View file

@ -201,13 +201,27 @@ public class CommentController {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} }
// Soft delete // Soft delete locally
comment.setDeleted(true); comment.setDeleted(true);
commentRepository.save(comment); commentRepository.save(comment);
log.info("User {} deleted comment {}", user.getUsername(), commentId); 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(); return ResponseEntity.noContent().build();
} }

View file

@ -311,10 +311,16 @@ public class FederationService {
} }
/** /**
* Get all follower inbox URLs for a local user. * Get the inbox URLs of all <em>remote</em> followers of a local user.
*
* <p>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 * @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) @Transactional(readOnly = true)
public List<String> getFollowerInboxes(UUID userId) { public List<String> getFollowerInboxes(UUID userId) {
@ -325,6 +331,7 @@ public class FederationService {
List<Follow> followers = followRepository.findAcceptedFollowersByActorUri(actorUri); List<Follow> followers = followRepository.findAcceptedFollowersByActorUri(actorUri);
return followers.stream() return followers.stream()
.filter(follow -> follow.getRemoteActorUri() != null) // skip local followers (no federation needed)
.map(follow -> { .map(follow -> {
try { try {
RemoteActor actor = remoteActorRepository.findByActorUri(follow.getRemoteActorUri()) RemoteActor actor = remoteActorRepository.findByActorUri(follow.getRemoteActorUri())
@ -529,9 +536,14 @@ public class FederationService {
/** /**
* Send a Delete activity to notify followers that an object has been deleted. * Send a Delete activity to notify followers that an object has been deleted.
* *
* @param objectUri the URI of the deleted object (e.g., activity URI) * <p>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 * @param sender the user who deleted the object
*/ */
@Async("taskExecutor")
public void sendDeleteActivity(String objectUri, User sender) { public void sendDeleteActivity(String objectUri, User sender) {
try { try {
String deleteId = baseUrl + "/activities/delete/" + UUID.randomUUID(); String deleteId = baseUrl + "/activities/delete/" + UUID.randomUUID();