feat(activitypub-base): impl fetch_actor_urls_from_collection + resolve_actor_profiles (concurrent, 5s timeout)

This commit is contained in:
2026-05-15 00:33:14 +02:00
parent d62dde67bb
commit 58126f195c
3 changed files with 76 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
tokio = { workspace = true } tokio = { workspace = true }
futures = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }

View File

@@ -1602,16 +1602,81 @@ impl domain::ports::FederationActionPort for ActivityPubService {
async fn fetch_actor_urls_from_collection( async fn fetch_actor_urls_from_collection(
&self, &self,
_collection_url: &str, collection_url: &str,
) -> Result<Vec<String>, domain::errors::DomainError> { ) -> Result<Vec<String>, domain::errors::DomainError> {
Ok(vec![]) let resp: serde_json::Value = reqwest::Client::new()
.get(collection_url)
.header("Accept", "application/activity+json, application/ld+json")
.send()
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?
.json()
.await
.map_err(|e| domain::errors::DomainError::ExternalService(e.to_string()))?;
let empty = vec![];
let items = resp["orderedItems"].as_array().unwrap_or(&empty);
Ok(items
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect())
} }
async fn resolve_actor_profiles( async fn resolve_actor_profiles(
&self, &self,
_urls: Vec<String>, urls: Vec<String>,
) -> Vec<domain::models::actor_connection_summary::ActorConnectionSummary> { ) -> Vec<domain::models::actor_connection_summary::ActorConnectionSummary> {
vec![] use futures::future;
async fn fetch_one(
url: String,
) -> Option<domain::models::actor_connection_summary::ActorConnectionSummary> {
let resp: serde_json::Value = tokio::time::timeout(
std::time::Duration::from_secs(5),
reqwest::Client::new()
.get(&url)
.header("Accept", "application/activity+json")
.send(),
)
.await
.ok()?
.ok()?
.json()
.await
.ok()?;
let ap_url = resp["id"].as_str()?.to_string();
let preferred_username = resp["preferredUsername"].as_str().unwrap_or("").to_string();
let domain_str = url::Url::parse(&ap_url)
.ok()
.and_then(|u| u.host_str().map(|s| s.to_string()))
.unwrap_or_default();
let handle = format!("{}@{}", preferred_username, domain_str);
let display_name = resp["name"].as_str().map(|s| s.to_string());
let avatar_url = resp["icon"]["url"].as_str().map(|s| s.to_string());
Some(
domain::models::actor_connection_summary::ActorConnectionSummary {
url: ap_url,
handle,
display_name,
avatar_url,
},
)
}
let futs: Vec<_> = urls.into_iter().map(fetch_one).collect();
let results = future::join_all(futs).await;
results
.into_iter()
.filter_map(|r| {
if r.is_none() {
tracing::warn!("failed to resolve actor profile (timeout or parse error)");
}
r
})
.collect()
} }
} }

View File

@@ -4,6 +4,12 @@ where
{ {
} }
fn _assert_impl_federation_action_port_connections()
where
crate::service::ActivityPubService: domain::ports::FederationActionPort,
{
}
use super::*; use super::*;
use crate::repository::{Follower, FollowerStatus, RemoteActor}; use crate::repository::{Follower, FollowerStatus, RemoteActor};