very fast at protocol indexer with flexible filtering, xrpc queries, cursor-backed event stream, and more, built on fjall
rust fjall at-protocol atproto indexer
59
fork

Configure Feed

Select the types of activity you want to include in your feed.

[lib] expose backlinks properly, add dids field support

dawn 3ad938b3 c93a1abc

+53 -4
+1
docs/xrpc/backlinks.md
··· 14 14 | :--- | :--- | :--- | 15 15 | `subject` | yes | AT URI or DID to look up backlinks for. | 16 16 | `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). | 17 + | `did` | no | filter links to those from specific users. | 17 18 | `limit` | no | max results to return (default 50, max 100). | 18 19 | `cursor` | no | opaque pagination cursor from a previous response. | 19 20 | `reverse` | no | if `true`, return results in reverse order (default `false`). |
+4
src/backlinks/api.rs
··· 21 21 pub subject: String, 22 22 /// filter by source collection, optionally with a path suffix `collection:path` 23 23 pub source: Option<String>, 24 + #[serde(default)] 25 + pub did: Vec<String>, 24 26 pub limit: Option<u64>, 25 27 pub cursor: Option<String>, 26 28 pub reverse: Option<bool>, ··· 43 45 pub struct GetBacklinksCountParams { 44 46 pub subject: String, 45 47 pub source: Option<String>, 48 + #[serde(default)] 49 + pub did: Vec<String>, 46 50 } 47 51 48 52 #[derive(Serialize)]
+41 -2
src/backlinks/mod.rs
··· 1 1 pub mod links; 2 2 pub mod store; 3 3 4 - pub(crate) mod api; 4 + pub mod api; 5 5 6 6 use std::ops::Bound; 7 7 use std::sync::Arc; ··· 35 35 subject: subject.into(), 36 36 collection: None, 37 37 path: None, 38 + dids: Vec::new(), 38 39 limit: 50, 39 40 reverse: false, 40 41 cursor: None, ··· 48 49 subject: subject.into(), 49 50 collection: None, 50 51 path: None, 52 + dids: Vec::new(), 51 53 } 52 54 } 53 55 } ··· 60 62 subject: String, 61 63 collection: Option<String>, 62 64 path: Option<String>, 65 + dids: Vec<String>, 63 66 limit: usize, 64 67 reverse: bool, 65 68 cursor: Option<Vec<u8>>, ··· 69 72 /// filter results to the given source collection NSID. 70 73 pub fn collection(mut self, collection: impl Into<String>) -> Self { 71 74 self.collection = Some(collection.into()); 75 + self 76 + } 77 + 78 + /// filter results to specific source author DIDs. 79 + pub fn dids(mut self, dids: Vec<String>) -> Self { 80 + self.dids = dids; 72 81 self 73 82 } 74 83 ··· 161 170 warn!("backlinks: could not extract source from reverse key"); 162 171 continue; 163 172 }; 173 + 174 + if !self.dids.is_empty() { 175 + if !self.dids.iter().any(|d| d == did) { 176 + continue; 177 + } 178 + } 179 + 164 180 let Some(cid) = store::lookup_cid_from_ks(&db.records, did, col, rkey) else { 165 181 // record deleted after backlink was written 166 182 continue; ··· 190 206 subject: String, 191 207 collection: Option<String>, 192 208 path: Option<String>, 209 + dids: Vec<String>, 193 210 } 194 211 195 212 impl BacklinksCount { ··· 199 216 self 200 217 } 201 218 219 + /// filter results to specific source author DIDs. 220 + pub fn dids(mut self, dids: Vec<String>) -> Self { 221 + self.dids = dids; 222 + self 223 + } 224 + 202 225 /// filter to a specific dotted field path within the record (e.g. `.subject.uri`). 203 226 /// has no effect unless a collection is also set. 204 227 pub fn path(mut self, path: impl Into<String>) -> Self { ··· 224 247 self.collection.as_deref(), 225 248 self.path.as_deref(), 226 249 ); 227 - Ok(db.backlinks.prefix(&scan_prefix).count() as u64) 250 + 251 + if !self.dids.is_empty() { 252 + let mut count = 0; 253 + for item in db.backlinks.prefix(&scan_prefix) { 254 + let key = item.key().into_diagnostic()?; 255 + let Some((_col, _path, did, _rkey)) = store::source_from_reverse_key(&key) 256 + else { 257 + continue; 258 + }; 259 + if self.dids.iter().any(|d| d == did) { 260 + count += 1; 261 + } 262 + } 263 + Ok(count) 264 + } else { 265 + Ok(db.backlinks.prefix(&scan_prefix).count() as u64) 266 + } 228 267 }) 229 268 .await 230 269 .into_diagnostic()?
+5
src/control/mod.rs
··· 122 122 } 123 123 124 124 impl Hydrant { 125 + /// Returns a reference to the internal DID resolver. 126 + pub fn resolver(&self) -> &crate::resolver::Resolver { 127 + &self.state.resolver 128 + } 129 + 125 130 /// open the database and configure hydrant from `config`. 126 131 /// 127 132 /// this sets up the database, applies any filter configuration from `config`, and
+2 -2
src/lib.rs
··· 31 31 #[cfg(feature = "indexer")] 32 32 pub(crate) mod backfill; 33 33 #[cfg(feature = "backlinks")] 34 - pub(crate) mod backlinks; 34 + pub mod backlinks; 35 35 #[cfg(feature = "indexer")] 36 36 pub(crate) mod crawler; 37 37 pub(crate) mod db; ··· 39 39 #[cfg(feature = "indexer")] 40 40 pub(crate) mod ops; 41 41 pub(crate) mod patch; 42 - pub(crate) mod resolver; 42 + pub mod resolver; 43 43 pub(crate) mod state; 44 44 pub(crate) mod util; 45 45