From 7c9deda3a8b7b60defe78e9b16da1cd70e3b64db Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 15 Jul 2026 17:55:56 +0100 Subject: [PATCH] bobbin: add listNetworkVouches Signed-off-by: oppiliappan --- bobbin/crates/resolver/src/legacy_upgrade.rs | 1 + bobbin/crates/xrpc/src/lib.rs | 123 ++++++++++++++----- lexicons/graph/listNetworkVouches.json | 57 +++++++++ web/src/routes/timeline/+page.svelte | 4 +- 4 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 lexicons/graph/listNetworkVouches.json diff --git a/bobbin/crates/resolver/src/legacy_upgrade.rs b/bobbin/crates/resolver/src/legacy_upgrade.rs index d08a459c..e7b3e8b7 100644 --- a/bobbin/crates/resolver/src/legacy_upgrade.rs +++ b/bobbin/crates/resolver/src/legacy_upgrade.rs @@ -377,6 +377,7 @@ async fn upgrade_pull( l.rounds }; Some(Pull { + blobs: None, created_at: l.created_at, blobs: None, body: l.body, diff --git a/bobbin/crates/xrpc/src/lib.rs b/bobbin/crates/xrpc/src/lib.rs index 7c21597e..1aaab551 100644 --- a/bobbin/crates/xrpc/src/lib.rs +++ b/bobbin/crates/xrpc/src/lib.rs @@ -49,6 +49,7 @@ use bobbin_types::sh_tangled::feed::reaction::{Reaction, ReactionRecord}; use bobbin_types::sh_tangled::feed::star::{Star, StarRecord}; use bobbin_types::sh_tangled::git::ref_update::{RefUpdate, RefUpdateRecord}; use bobbin_types::sh_tangled::graph::follow::{Follow, FollowRecord}; +use bobbin_types::sh_tangled::graph::list_network_vouches::ListNetworkVouches; use bobbin_types::sh_tangled::graph::vouch::{Vouch, VouchRecord}; use bobbin_types::sh_tangled::knot::member::{ Member as KnotMember, MemberRecord as KnotMemberRecord, @@ -300,9 +301,11 @@ pub fn router(state: AppState) -> Router { "/xrpc/sh.tangled.publicKey.countKeys", get(count_public_keys), ) - .route("/xrpc/sh.tangled.graph.listVouches", get(list_vouches)) - .route("/xrpc/sh.tangled.graph.countVouches", get(count_vouches)) .route("/xrpc/sh.tangled.feed.getTimeline", get(feed::get_timeline)) + .route( + "/xrpc/sh.tangled.graph.listNetworkVouches", + get(list_network_vouches), + ) .route("/xrpc/sh.tangled.feed.listStarsBy", get(list_stars_by)) .route("/xrpc/sh.tangled.feed.countStarsBy", get(count_stars_by)) .route( @@ -318,11 +321,6 @@ pub fn router(state: AppState) -> Router { "/xrpc/sh.tangled.graph.countFollowsBy", get(count_follows_by), ) - .route("/xrpc/sh.tangled.graph.listVouchesBy", get(list_vouches_by)) - .route( - "/xrpc/sh.tangled.graph.countVouchesBy", - get(count_vouches_by), - ) .route( "/xrpc/sh.tangled.git.listRefUpdatesBy", get(list_ref_updates_by), @@ -2237,18 +2235,98 @@ async fn count_public_keys( count_for::(&state, q).map(Json) } -async fn list_vouches( - State(state): State, - XrpcQuery(q): XrpcQuery>, -) -> Result { - list_records::, _>(&state, q).await +fn vouch_network(state: &AppState, actor: &SubjectRef) -> std::collections::HashSet { + let vouch_by = nsid_static("sh.tangled.graph.vouch.by"); + state + .edges + .sources_for(&EdgeKey::new(vouch_by, actor.clone())) + .iter() + .filter_map(|uri| Did::::new_owned(uri.rkey()?.as_ref()).ok()) + .map(|did| did.as_ref().to_string()) + .collect() +} + +fn vouches_received( + state: &AppState, + actor: &SubjectRef, + network: std::collections::HashSet, +) -> Vec { + let vouch = nsid_static("sh.tangled.graph.vouch"); + state + .edges + .list_filtered( + &EdgeKey::new(vouch, actor.clone()), + PageCursor::Start, + PageLimit::new(PageLimit::MAX).unwrap(), + SortDir::Desc, + move |uri| source_authority_did(uri).is_some_and(|did| network.contains(did.as_ref())), + ) + .items } -async fn count_vouches( +fn paginate_vouches( + candidates: impl Iterator, + cursor: Option<&str>, + limit: PageLimit, +) -> (Vec, Option) { + let mut seen = std::collections::HashSet::new(); + let mut combined: Vec = candidates + .filter(|item| seen.insert(item.uri.as_ref().to_string())) + .collect(); + combined.sort_by(|a, b| b.sort_micros.cmp(&a.sort_micros)); + + let start = match cursor.and_then(|s| s.parse::().ok()) { + None => 0, + Some(micros) => combined + .iter() + .position(|item| item.sort_micros < micros) + .unwrap_or(combined.len()), + }; + let limit = limit.get() as usize; + let has_more = combined.len() > start + limit; + let items: Vec = combined.into_iter().skip(start).take(limit).collect(); + let next = has_more + .then(|| items.last().map(|item| item.sort_micros.to_string())) + .flatten(); + (items, next) +} + +async fn list_network_vouches( State(state): State, - XrpcQuery(q): XrpcQuery, -) -> Result, XrpcError> { - count_for::(&state, q).map(Json) + XrpcQuery(q): XrpcQuery, +) -> Result { + let actor = SubjectRef::Did(q.actor.clone()); + let limit = parse_limit(q.limit.and_then(|n| u32::try_from(n).ok()))?; + let vouch_nsid = nsid_static("sh.tangled.graph.vouch"); + + let network = vouch_network(&state, &actor); + let received = vouches_received(&state, &actor, network); + let sent = state + .edges + .list( + &EdgeKey::new(nsid_static("sh.tangled.graph.vouch.by"), actor), + PageCursor::Start, + PageLimit::new(PageLimit::MAX).unwrap(), + SortDir::Desc, + ) + .items; + + let (page_items, next_cursor) = + paginate_vouches(received.into_iter().chain(sent), q.cursor.as_deref(), limit); + + let permit = state.heavy_permit()?; + let views = hydrate_record_stream::>( + &state, + vouch_nsid, + page_items, + HitProvenance::Indexed, + ); + Ok(json_stream::>, _>( + "items", + views, + paged_tail(next_cursor), + permit, + )) } async fn list_stars_by( @@ -2290,19 +2368,6 @@ async fn count_follows_by( count_mirror::(&state, q).map(Json) } -async fn list_vouches_by( - State(state): State, - XrpcQuery(q): XrpcQuery>, -) -> Result { - list_mirror::, _>(&state, q).await -} -async fn count_vouches_by( - State(state): State, - XrpcQuery(q): XrpcQuery, -) -> Result, XrpcError> { - count_mirror::(&state, q).map(Json) -} - async fn list_ref_updates_by( State(state): State, XrpcQuery(q): XrpcQuery>, diff --git a/lexicons/graph/listNetworkVouches.json b/lexicons/graph/listNetworkVouches.json new file mode 100644 index 00000000..92f8473d --- /dev/null +++ b/lexicons/graph/listNetworkVouches.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "sh.tangled.graph.listNetworkVouches", + "defs": { + "main": { + "type": "query", + "description": "List vouches involving an actor as seen through a viewer's vouch network. Requires authentication.", + "parameters": { + "type": "params", + "required": ["actor"], + "properties": { + "actor": { + "type": "string", + "format": "did", + "description": "The DID of the profile whose network vouches to list." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "type": "ref", "ref": "#listItem" } + }, + "cursor": { "type": "string" } + } + } + } + }, + "listItem": { + "type": "object", + "required": ["uri", "value"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "value": { + "type": "unknown", + "description": "Embedded sh.tangled.graph.vouch record" + } + } + } + } +} diff --git a/web/src/routes/timeline/+page.svelte b/web/src/routes/timeline/+page.svelte index a045354d..c483d835 100644 --- a/web/src/routes/timeline/+page.svelte +++ b/web/src/routes/timeline/+page.svelte @@ -13,9 +13,9 @@ Timeline · Tangled -
+
-

+

-- 2.51.2