From 3ad938b38ca8e8012a43b204ff2ab39adf00f863 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 25 Apr 2026 11:33:19 +0300 Subject: [PATCH] [lib] expose backlinks properly, add dids field support --- docs/xrpc/backlinks.md | 1 + src/backlinks/api.rs | 4 ++++ src/backlinks/mod.rs | 43 ++++++++++++++++++++++++++++++++++++++++-- src/control/mod.rs | 5 +++++ src/lib.rs | 4 ++-- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/xrpc/backlinks.md b/docs/xrpc/backlinks.md index fd146a7..65b1fdb 100644 --- a/docs/xrpc/backlinks.md +++ b/docs/xrpc/backlinks.md @@ -14,6 +14,7 @@ return records that link to a given subject. | :--- | :--- | :--- | | `subject` | yes | AT URI or DID to look up backlinks for. | | `source` | no | filter by source collection, e.g. `app.bsky.feed.like`. also accepts `collection:path` form to further filter by field path, e.g. `app.bsky.feed.like:subject.uri`. the path is matched against the dotted field path within the record (`.` is prepended automatically). | +| `did` | no | filter links to those from specific users. | | `limit` | no | max results to return (default 50, max 100). | | `cursor` | no | opaque pagination cursor from a previous response. | | `reverse` | no | if `true`, return results in reverse order (default `false`). | diff --git a/src/backlinks/api.rs b/src/backlinks/api.rs index db0b0e5..860b421 100644 --- a/src/backlinks/api.rs +++ b/src/backlinks/api.rs @@ -21,6 +21,8 @@ pub struct GetBacklinksParams { pub subject: String, /// filter by source collection, optionally with a path suffix `collection:path` pub source: Option, + #[serde(default)] + pub did: Vec, pub limit: Option, pub cursor: Option, pub reverse: Option, @@ -43,6 +45,8 @@ pub struct GetBacklinksOutput { pub struct GetBacklinksCountParams { pub subject: String, pub source: Option, + #[serde(default)] + pub did: Vec, } #[derive(Serialize)] diff --git a/src/backlinks/mod.rs b/src/backlinks/mod.rs index 4f96646..f9cf069 100644 --- a/src/backlinks/mod.rs +++ b/src/backlinks/mod.rs @@ -1,7 +1,7 @@ pub mod links; pub mod store; -pub(crate) mod api; +pub mod api; use std::ops::Bound; use std::sync::Arc; @@ -35,6 +35,7 @@ impl BacklinksControl { subject: subject.into(), collection: None, path: None, + dids: Vec::new(), limit: 50, reverse: false, cursor: None, @@ -48,6 +49,7 @@ impl BacklinksControl { subject: subject.into(), collection: None, path: None, + dids: Vec::new(), } } } @@ -60,6 +62,7 @@ pub struct BacklinksFetch { subject: String, collection: Option, path: Option, + dids: Vec, limit: usize, reverse: bool, cursor: Option>, @@ -72,6 +75,12 @@ impl BacklinksFetch { self } + /// filter results to specific source author DIDs. + pub fn dids(mut self, dids: Vec) -> Self { + self.dids = dids; + self + } + /// filter results to a specific dotted field path within the record (e.g. `.subject.uri`). /// has no effect unless a collection is also set. pub fn path(mut self, path: impl Into) -> Self { @@ -161,6 +170,13 @@ impl BacklinksFetch { warn!("backlinks: could not extract source from reverse key"); continue; }; + + if !self.dids.is_empty() { + if !self.dids.iter().any(|d| d == did) { + continue; + } + } + let Some(cid) = store::lookup_cid_from_ks(&db.records, did, col, rkey) else { // record deleted after backlink was written continue; @@ -190,6 +206,7 @@ pub struct BacklinksCount { subject: String, collection: Option, path: Option, + dids: Vec, } impl BacklinksCount { @@ -199,6 +216,12 @@ impl BacklinksCount { self } + /// filter results to specific source author DIDs. + pub fn dids(mut self, dids: Vec) -> Self { + self.dids = dids; + self + } + /// filter to a specific dotted field path within the record (e.g. `.subject.uri`). /// has no effect unless a collection is also set. pub fn path(mut self, path: impl Into) -> Self { @@ -224,7 +247,23 @@ impl BacklinksCount { self.collection.as_deref(), self.path.as_deref(), ); - Ok(db.backlinks.prefix(&scan_prefix).count() as u64) + + if !self.dids.is_empty() { + let mut count = 0; + for item in db.backlinks.prefix(&scan_prefix) { + let key = item.key().into_diagnostic()?; + let Some((_col, _path, did, _rkey)) = store::source_from_reverse_key(&key) + else { + continue; + }; + if self.dids.iter().any(|d| d == did) { + count += 1; + } + } + Ok(count) + } else { + Ok(db.backlinks.prefix(&scan_prefix).count() as u64) + } }) .await .into_diagnostic()? diff --git a/src/control/mod.rs b/src/control/mod.rs index c26d9e6..a9a6220 100644 --- a/src/control/mod.rs +++ b/src/control/mod.rs @@ -122,6 +122,11 @@ pub struct Hydrant { } impl Hydrant { + /// Returns a reference to the internal DID resolver. + pub fn resolver(&self) -> &crate::resolver::Resolver { + &self.state.resolver + } + /// open the database and configure hydrant from `config`. /// /// this sets up the database, applies any filter configuration from `config`, and diff --git a/src/lib.rs b/src/lib.rs index 7bd8461..dd792f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ pub(crate) mod api; #[cfg(feature = "indexer")] pub(crate) mod backfill; #[cfg(feature = "backlinks")] -pub(crate) mod backlinks; +pub mod backlinks; #[cfg(feature = "indexer")] pub(crate) mod crawler; pub(crate) mod db; @@ -39,7 +39,7 @@ pub(crate) mod ingest; #[cfg(feature = "indexer")] pub(crate) mod ops; pub(crate) mod patch; -pub(crate) mod resolver; +pub mod resolver; pub(crate) mod state; pub(crate) mod util; -- 2.51.2