fix(federation): fix 27 AP bugs, gaps, and inconsistencies
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:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user