refactor(domain): remove FetchRemoteActorPosts/FetchActorConnections from DomainEvent; add FederationSchedulerPort

This commit is contained in:
2026-05-15 13:28:19 +02:00
parent e935c8973e
commit a902154777
13 changed files with 1310 additions and 233 deletions

View File

@@ -12,9 +12,7 @@ pub struct FederationEventService {
pub users: Arc<dyn UserRepository>,
pub ap: Arc<dyn OutboundFederationPort>,
pub base_url: String,
pub federation_action: Arc<dyn domain::ports::FederationActionPort>,
pub ap_repo: Arc<dyn ActivityPubRepository>,
pub remote_actor_connections: Arc<dyn domain::ports::RemoteActorConnectionRepository>,
}
impl FederationEventService {
@@ -148,112 +146,6 @@ impl FederationEventService {
.await
}
DomainEvent::FetchRemoteActorPosts {
actor_ap_url,
outbox_url,
} => {
let notes = match self
.federation_action
.fetch_outbox_page(outbox_url, 1)
.await
{
Ok(n) => n,
Err(e) => {
tracing::warn!(outbox_url, error = %e, "failed to fetch remote outbox");
return Ok(());
}
};
let actor_url = url::Url::parse(actor_ap_url)
.map_err(|e| DomainError::ExternalService(e.to_string()))?;
let author_id = self.ap_repo.intern_remote_actor(&actor_url).await?;
// Resolve and cache display info so thought cards show proper names.
let profiles = self
.federation_action
.resolve_actor_profiles(vec![actor_ap_url.clone()])
.await;
if let Some(profile) = profiles.into_iter().next() {
let _ = self
.ap_repo
.update_remote_actor_display(
&author_id,
profile.display_name.as_deref(),
profile.avatar_url.as_deref(),
)
.await;
}
for note in notes {
let ap_id = match url::Url::parse(&note.ap_id) {
Ok(u) => u,
Err(_) => continue,
};
let _ = self
.ap_repo
.accept_note(
&ap_id,
&author_id,
&note.content,
note.published,
note.sensitive,
note.content_warning,
"public",
None,
)
.await;
}
Ok(())
}
DomainEvent::FetchActorConnections {
actor_ap_url,
collection_url,
connection_type,
page,
} => {
let urls = match self
.federation_action
.fetch_actor_urls_from_collection(collection_url)
.await
{
Ok(u) => u,
Err(e) => {
tracing::warn!(
collection_url,
error = %e,
"failed to fetch actor connections collection"
);
return Ok(());
}
};
if urls.is_empty() {
return Ok(());
}
let summaries = self.federation_action.resolve_actor_profiles(urls).await;
if summaries.is_empty() {
return Ok(());
}
tracing::info!(
count = summaries.len(),
connection_type,
actor = actor_ap_url,
"caching actor connections"
);
self.remote_actor_connections
.upsert_connections(actor_ap_url, connection_type, *page, &summaries)
.await?;
Ok(())
}
DomainEvent::LikeAdded {
like_id: _,
user_id,
@@ -438,9 +330,7 @@ mod tests {
users: Arc::new(store.clone()),
ap: spy,
base_url: "https://example.com".to_string(),
federation_action: Arc::new(store.clone()),
ap_repo: Arc::new(store.clone()),
remote_actor_connections: Arc::new(store.clone()),
}
}
@@ -772,35 +662,6 @@ mod tests {
assert!(spy.updated.lock().unwrap().is_empty());
}
#[tokio::test]
async fn fetch_remote_actor_posts_is_noop_when_outbox_empty() {
let store = TestStore::default();
let spy = Arc::new(SpyPort::default());
svc(&store, spy.clone())
.process(&DomainEvent::FetchRemoteActorPosts {
actor_ap_url: "https://mastodon.social/users/alice".into(),
outbox_url: "https://mastodon.social/users/alice/outbox".into(),
})
.await
.unwrap();
// TestStore.fetch_outbox_page returns Ok(vec![]) — no notes, no error
}
#[tokio::test]
async fn fetch_actor_connections_is_noop_when_collection_empty() {
let store = TestStore::default();
let spy = Arc::new(SpyPort::default());
svc(&store, spy.clone())
.process(&DomainEvent::FetchActorConnections {
actor_ap_url: "https://mastodon.social/users/alice".into(),
collection_url: "https://mastodon.social/users/alice/followers".into(),
connection_type: "followers".into(),
page: 1,
})
.await
.unwrap();
}
#[tokio::test]
async fn like_added_local_user_remote_thought_broadcasts_like() {
let store = TestStore::default();