feat: add backfill_outbox to ActivityPubService and ActivityPubPort

This commit is contained in:
2026-05-13 01:32:14 +02:00
parent ca9a504632
commit e92c6789d9
2 changed files with 92 additions and 0 deletions

View File

@@ -975,6 +975,91 @@ impl ActivityPubService {
Ok(()) Ok(())
} }
pub async fn backfill_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let data = self.federation_config.to_request_data();
let actor = url::Url::parse(actor_url)?;
let root: serde_json::Value = client
.get(outbox_url)
.header("Accept", "application/activity+json")
.send()
.await?
.json()
.await?;
let first = match root.get("first").and_then(|v| v.as_str()) {
Some(url) => url.to_string(),
None => {
tracing::debug!(outbox = %outbox_url, "outbox has no first page, nothing to backfill");
return Ok(());
}
};
let mut current_url = first;
let mut visited = std::collections::HashSet::new();
loop {
if !visited.insert(current_url.clone()) {
tracing::warn!(url = %current_url, "backfill: loop detected, stopping");
break;
}
let page: serde_json::Value = match client
.get(&current_url)
.header("Accept", "application/activity+json")
.send()
.await
{
Ok(resp) => match resp.json().await {
Ok(v) => v,
Err(e) => {
tracing::error!(error = %e, url = %current_url, "backfill: failed to parse page JSON");
break;
}
},
Err(e) => {
tracing::error!(error = %e, url = %current_url, "backfill: HTTP request failed");
break;
}
};
if let Some(items) = page.get("orderedItems").and_then(|v| v.as_array()) {
for item in items {
let activity_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
if activity_type != "Create" && activity_type != "Add" {
continue;
}
let object = match item.get("object") {
Some(o) if o.is_object() => o.clone(),
_ => continue,
};
let ap_id = match object
.get("id")
.and_then(|v| v.as_str())
.and_then(|s| url::Url::parse(s).ok())
{
Some(u) => u,
None => continue,
};
if let Err(e) = data.object_handler.on_create(&ap_id, &actor, object).await {
tracing::warn!(ap_id = %ap_id, error = %e, "backfill: failed to process item, skipping");
}
}
}
match page.get("next").and_then(|v| v.as_str()) {
Some(next) => current_url = next.to_string(),
None => break,
}
}
tracing::info!(outbox = %outbox_url, pages = visited.len(), "backfill complete");
Ok(())
}
fn spawn_backfill(&self, owner_user_id: uuid::Uuid, follower_inbox_url: String) { fn spawn_backfill(&self, owner_user_id: uuid::Uuid, follower_inbox_url: String) {
let config = self.federation_config.clone(); let config = self.federation_config.clone();
let base_url = self.base_url.clone(); let base_url = self.base_url.clone();

View File

@@ -31,6 +31,7 @@ pub trait ActivityPubPort: Send + Sync {
async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()>; async fn add_blocked_domain(&self, domain: &str, reason: Option<&str>) -> anyhow::Result<()>;
async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()>; async fn remove_blocked_domain(&self, domain: &str) -> anyhow::Result<()>;
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>>; async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>>;
async fn backfill_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()>;
} }
#[async_trait] #[async_trait]
@@ -97,6 +98,9 @@ impl ActivityPubPort for ActivityPubService {
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> { async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
self.get_blocked_domains().await self.get_blocked_domains().await
} }
async fn backfill_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()> {
self.backfill_outbox(outbox_url, actor_url).await
}
} }
pub struct NoopActivityPubService; pub struct NoopActivityPubService;
@@ -154,4 +158,7 @@ impl ActivityPubPort for NoopActivityPubService {
async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> { async fn get_blocked_domains(&self) -> anyhow::Result<Vec<BlockedDomain>> {
Ok(vec![]) Ok(vec![])
} }
async fn backfill_outbox(&self, _: &str, _: &str) -> anyhow::Result<()> {
Ok(())
}
} }