fix(federation): fix 27 AP bugs, gaps, and inconsistencies
Some checks failed
lint / lint (push) Failing after 9m26s
test / unit (push) Successful in 16m3s

Round 1 — 18 bug fixes:
- remote likes/boosts now persist in engagement tables
- intern_remote_actor uses name@domain, expanded username to VARCHAR(255)
- PgRemoteActorRepository upsert/find now handles all fields
- update_following_status no longer a no-op, count_followers counts all
- accept/reject follow publishes event before DB mark (atomicity)
- fetch_outbox_page follows pagination via next links
- actor URL canonicalized to /users/{uuid}
- content_to_html escapes single quotes
- WebFinger accepts application/ld+json type
- try_from_ap accepts Article and Page object types
- feed SQL uses parameterized viewer UUID instead of format!
- content cap raised from 500 to 5000 chars
- also_known_as changed from Option<String> to Vec<String>
- connections fetch always triggers from page 1

Round 2 — 9 gap fixes:
- on_announce_removed handler deletes boost row on Undo(Announce)
- on_update handles Person/Service/Group actor profile updates
- sync_remote_actor_to_user syncs remote_actors → users on create/update
- FederationBlockPort: block_by_username sends Block activity to remote
- domain RemoteActor gains inbox_url, shared_inbox_url fields
- remote_actors attachment column (JSONB) with read/write
- .well-known/host-meta endpoint
- 256KB body limit on AP inbox routes
- outbox cleanup job (7-day retention, hourly sweep)
This commit is contained in:
2026-05-29 11:28:40 +02:00
parent f9de21dcfa
commit 84edf58de6
32 changed files with 565 additions and 142 deletions

View File

@@ -136,4 +136,7 @@ impl ActivityPubRepository for TestApRepo {
) -> Result<Option<ActorApUrls>, DomainError> {
Ok(self.actor_ap_urls.lock().unwrap().get(user_id).cloned())
}
async fn sync_remote_actor_to_user(&self, _actor_ap_url: &str) -> Result<(), DomainError> {
Ok(())
}
}

View File

@@ -44,16 +44,14 @@ pub async fn accept_follow_request(
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
federation
.mark_follower_accepted(user_id, actor_url)
.await?;
events
.publish(&DomainEvent::RemoteFollowAccepted {
local_user_id: user_id.clone(),
remote_actor_url: actor_url.to_string(),
})
.await
.map_err(|e| DomainError::Internal(e.to_string()))
.map_err(|e| DomainError::Internal(e.to_string()))?;
federation.mark_follower_accepted(user_id, actor_url).await
}
pub async fn reject_follow_request(
@@ -62,16 +60,14 @@ pub async fn reject_follow_request(
user_id: &UserId,
actor_url: &str,
) -> Result<(), DomainError> {
federation
.mark_follower_rejected(user_id, actor_url)
.await?;
events
.publish(&DomainEvent::RemoteFollowRejected {
local_user_id: user_id.clone(),
remote_actor_url: actor_url.to_string(),
})
.await
.map_err(|e| DomainError::Internal(e.to_string()))
.map_err(|e| DomainError::Internal(e.to_string()))?;
federation.mark_follower_rejected(user_id, actor_url).await
}
pub async fn list_remote_followers(
@@ -179,8 +175,9 @@ pub async fn get_actor_connections_page(
}
};
if stale {
// Always fetch from page 1 — the full collection is fetched and chunked.
let _ = scheduler
.schedule_connections_fetch(&actor.url, &collection_url, connection_type, page)
.schedule_connections_fetch(&actor.url, &collection_url, connection_type, 1)
.await;
}
let has_more = items.len() >= PAGE_SIZE;

View File

@@ -11,10 +11,12 @@ fn remote_actor(url: &str, handle: &str) -> RemoteActor {
avatar_url: None,
bio: None,
banner_url: None,
also_known_as: None,
also_known_as: vec![],
outbox_url: None,
followers_url: None,
following_url: None,
inbox_url: None,
shared_inbox_url: None,
attachment: vec![],
last_fetched_at: Utc::now(),
}

View File

@@ -8,8 +8,8 @@ use domain::{
user::User,
},
ports::{
BlockRepository, BoostRepository, EventPublisher, FederationFollowPort, FollowRepository,
LikeRepository, UserReader,
BlockRepository, BoostRepository, EventPublisher, FederationBlockPort,
FederationFollowPort, FollowRepository, LikeRepository, UserReader,
},
value_objects::{BoostId, LikeId, ThoughtId, UserId, Username},
};
@@ -217,10 +217,14 @@ pub async fn reject_follow(
pub async fn block_by_username(
blocks: &dyn BlockRepository,
users: &dyn UserReader,
federation: &dyn FederationBlockPort,
events: &dyn EventPublisher,
blocker_id: &UserId,
username: &str,
) -> Result<(), DomainError> {
if username.contains('@') {
return federation.block_remote(blocker_id, username).await;
}
let uname = Username::new(username).map_err(|_| DomainError::NotFound)?;
let target = users
.find_by_username(&uname)
@@ -232,10 +236,14 @@ pub async fn block_by_username(
pub async fn unblock_by_username(
blocks: &dyn BlockRepository,
users: &dyn UserReader,
federation: &dyn FederationBlockPort,
events: &dyn EventPublisher,
blocker_id: &UserId,
username: &str,
) -> Result<(), DomainError> {
if username.contains('@') {
return federation.unblock_remote(blocker_id, username).await;
}
let uname = Username::new(username).map_err(|_| DomainError::NotFound)?;
let target = users
.find_by_username(&uname)