Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

Add getDistinct XRPC equivalent to REST /links/distinct-dids

Simple conversion of the existing endpoint from REST to XRPC.
Consequtively marked the pre-existing REST endpoint as deprecated.

+244 -15
+80
constellation/src/server/mod.rs
··· 100 100 }), 101 101 ) 102 102 .route( 103 + // deprecated 103 104 "/links", 104 105 get({ 105 106 let store = store.clone(); ··· 111 112 }), 112 113 ) 113 114 .route( 115 + "/xrpc/blue.microcosm.links.getDistinct", 116 + get({ 117 + let store = store.clone(); 118 + move |accept, query| async { 119 + spawn_blocking(|| get_distinct(accept, query, store)) 120 + .await 121 + .map_err(to500)? 122 + } 123 + }), 124 + ) 125 + // deprecated 126 + .route( 114 127 "/links/distinct-dids", 115 128 get({ 116 129 let store = store.clone(); ··· 133 146 } 134 147 }), 135 148 ) 149 + // deprecated 136 150 .route( 137 151 "/links/all", 138 152 get({ ··· 522 536 #[serde(skip_serializing)] 523 537 query: GetLinkItemsQuery, 524 538 } 539 + #[deprecated] 525 540 fn get_links( 526 541 accept: ExtractAccept, 527 542 query: axum_extra::extract::Query<GetLinkItemsQuery>, // supports multiple param occurrences ··· 581 596 GetLinkItemsResponse { 582 597 total: paged.total, 583 598 linking_records: paged.items, 599 + cursor, 600 + query: (*query).clone(), 601 + }, 602 + )) 603 + } 604 + 605 + #[derive(Clone, Deserialize)] 606 + struct GetDistinctItemsQuery { 607 + subject: String, 608 + source: String, 609 + cursor: Option<OpaqueApiCursor>, 610 + limit: Option<u64>, 611 + // TODO: allow reverse (er, forward) order as well 612 + } 613 + #[derive(Template, Serialize)] 614 + #[template(path = "get-distinct.html.j2")] 615 + struct GetDistinctItemsResponse { 616 + // what does staleness mean? 617 + // - new links have appeared. would be nice to offer a `since` cursor to fetch these. and/or, 618 + // - links have been deleted. hmm. 619 + total: u64, 620 + linking_dids: Vec<Did>, 621 + cursor: Option<OpaqueApiCursor>, 622 + #[serde(skip_serializing)] 623 + query: GetDistinctItemsQuery, 624 + } 625 + fn get_distinct( 626 + accept: ExtractAccept, 627 + query: Query<GetDistinctItemsQuery>, 628 + store: impl LinkReader, 629 + ) -> Result<impl IntoResponse, http::StatusCode> { 630 + let until = query 631 + .cursor 632 + .clone() 633 + .map(|oc| ApiCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) 634 + .transpose()? 635 + .map(|c| c.next); 636 + 637 + let limit = query.limit.unwrap_or(DEFAULT_CURSOR_LIMIT); 638 + if limit > DEFAULT_CURSOR_LIMIT_MAX { 639 + return Err(http::StatusCode::BAD_REQUEST); 640 + } 641 + 642 + let Some((collection, path)) = query.source.split_once(':') else { 643 + return Err(http::StatusCode::BAD_REQUEST); 644 + }; 645 + let path = format!(".{path}"); 646 + 647 + let paged = store 648 + .get_distinct_dids(&query.subject, &collection, &path, limit, until) 649 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 650 + 651 + let cursor = paged.next.map(|next| { 652 + ApiCursor { 653 + version: paged.version, 654 + next, 655 + } 656 + .into() 657 + }); 658 + 659 + Ok(acceptable( 660 + accept, 661 + GetDistinctItemsResponse { 662 + total: paged.total, 663 + linking_dids: paged.items, 584 664 cursor, 585 665 query: (*query).clone(), 586 666 },
+2 -2
constellation/src/storage/mem_store.rs
··· 394 394 ) -> Result<HashMap<String, HashMap<String, CountsByCount>>> { 395 395 let data = self.0.lock().unwrap(); 396 396 let mut out: HashMap<String, HashMap<String, CountsByCount>> = HashMap::new(); 397 - if let Some(asdf) = data.targets.get(&Target::new(target)) { 398 - for (Source { collection, path }, linkers) in asdf { 397 + if let Some(source_linker_pairs) = data.targets.get(&Target::new(target)) { 398 + for (Source { collection, path }, linkers) in source_linker_pairs { 399 399 let records = linkers.iter().flatten().count() as u64; 400 400 let distinct_dids = linkers 401 401 .iter()
+49
constellation/templates/get-distinct.html.j2
··· 1 + {% extends "base.html.j2" %} 2 + {% import "try-it-macros.html.j2" as try_it %} 3 + 4 + {% block title %}DIDs{% endblock %} 5 + {% block description %}Distinct DIDs with records in {{ query.source }} linking to {{ query.subject }} {% endblock %} 6 + 7 + {% block content %} 8 + 9 + {% call try_it::get_distinct(query.subject, query.source) %} 10 + 11 + <h2> 12 + Distinct DIDs with links to <code>{{ query.subject }}</code> from <code>{{ query.source }}</code> 13 + {% if let Some(browseable_uri) = query.subject|to_browseable %} 14 + <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 15 + {% endif %} 16 + </h2> 17 + 18 + <p><strong>{{ total|human_number }} distinct DIDs</strong> with links to <code>{{ query.subject }}</code> from <code>{{ query.source }}</code></p> 19 + 20 + <ul> 21 + <li>See direct backlinks at <code>/xrpc/blue.microcosm.links.getBacklinks</code>: <a href="/xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject|urlencode }}&source={{ query.source|urlencode }}">/xrpc/blue.microcosm.links.getBacklinks?subject={{ query.subject }}&source={{ query.source }}</a></li> 22 + <li>See all links to this target at <code>/links/all</code>: <a href="/links/all?target={{ query.subject|urlencode }}">/links/all?target={{ query.subject }}</a></li> 23 + </ul> 24 + 25 + <h3>DIDs, most recent first:</h3> 26 + 27 + {% for did in linking_dids %} 28 + <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ did.0 }} 29 + -> see <a href="/links/all?target={{ did.0|urlencode }}">links to this DID</a> 30 + -> browse <a href="https://pdsls.dev/at://{{ did.0 }}">this DID record</a></pre> 31 + {% endfor %} 32 + 33 + {% if let Some(c) = cursor %} 34 + <form method="get" action="/xrpc/blue.microcosm.links.getDistinct"> 35 + <input type="hidden" name="subject" value="{{ query.subject }}" /> 36 + <input type="hidden" name="source" value="{{ query.source }}" /> 37 + <input type="hidden" name="cursor" value={{ c|json|safe }} /> 38 + <button type="submit">next page&hellip;</button> 39 + </form> 40 + {% else %} 41 + <button disabled><em>end of results</em></button> 42 + {% endif %} 43 + 44 + <details> 45 + <summary>Raw JSON response</summary> 46 + <pre class="code">{{ self|tojson }}</pre> 47 + </details> 48 + 49 + {% endblock %}
+45
constellation/templates/hello.html.j2
··· 81 81 ) %} 82 82 83 83 84 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getCounts</code></h3> 85 + 86 + <p>The total number of links pointing at a given target.</p> 87 + 88 + <h4>Query parameters:</h4> 89 + 90 + <ul> 91 + <li><code>subject</code>: required, must url-encode. The target being linked to. Example: <code>did:plc:vc7f4oafdgxsihk4cry2xpze</code> or <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 92 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 93 + </ul> 94 + 95 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 96 + {% call try_it::get_counts("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block:subject.uri") %} 97 + 98 + 99 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getDistinct</code></h3> 100 + 101 + <p>A list of distinct DIDs (identities) with links to a target.</p> 102 + 103 + <h4>Query parameters:</h4> 104 + 105 + <ul> 106 + <li><code>subject</code>: required, must url-encode. The target being linked to. Example: <code>did:plc:vc7f4oafdgxsihk4cry2xpze</code> or <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 107 + <li><code>source</code>: required. Collection and path specification for the primary link. Example: <code>app.bsky.feed.like:subject.uri</code></li> 108 + <li><code>limit</code>: optional. Number of results to return. Default: <code>16</code>. Maximum: <code>100</code></li> 109 + <li><code>cursor</code>: optional, see Definitions.</li> 110 + </ul> 111 + 112 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 113 + {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:subject.uri") %} 114 + 115 + 84 116 <h3 class="route"><code>GET /links</code></h3> 85 117 86 118 <p>A list of records linking to a target.</p> ··· 101 133 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 102 134 {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} 103 135 136 + <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getBacklinks</code></h3> 137 + 138 + <p>A list of distinct DIDs (identities) with links to a target.</p> 139 + 140 + <h4>Query parameters:</h4> 141 + 142 + <ul> 143 + <li><code>subject</code>: required, must url-encode. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 144 + <li><code>source</code>: required, must url-encode. Example: <code>app.bsky.feed.post:.subject.uri</code></li> 145 + </ul> 146 + 147 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 148 + {% call try_it::get_distinct("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like:.subject.uri") %} 104 149 105 150 <h3 class="route"><code>GET /links/distinct-dids</code></h3> 106 151
+9
constellation/templates/try-it-macros.html.j2
··· 102 102 </form> 103 103 {% endmacro %} 104 104 105 + {% macro get_distinct(subject, source) %} 106 + <form method="get" action="/xrpc/blue.microcosm.links.getDistinct"> 107 + <pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getDistinct 108 + ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="subject" /> 109 + &source= <input type="text" name="source" value="{{ source }}" placeholder="source" /> 110 + <button type="submit">get links</button> 111 + </pre> 112 + </form> 113 + {% endmacro %} 105 114 106 115 {% macro links_count(target, collection, path) %} 107 116 <form method="get" action="/links/count">
+3 -13
lexicons/blue.microcosm/links/getBacklinks.json
··· 7 7 "description": "a list of records linking to any record, identity, or uri", 8 8 "parameters": { 9 9 "type": "params", 10 - "required": [ 11 - "subject", 12 - "source" 13 - ], 10 + "required": ["subject", "source"], 14 11 "properties": { 15 12 "subject": { 16 13 "type": "string", ··· 42 39 "encoding": "application/json", 43 40 "schema": { 44 41 "type": "object", 45 - "required": [ 46 - "total", 47 - "records" 48 - ], 42 + "required": ["total", "records"], 49 43 "properties": { 50 44 "total": { 51 45 "type": "integer", ··· 68 62 }, 69 63 "linkRecord": { 70 64 "type": "object", 71 - "required": [ 72 - "did", 73 - "collection", 74 - "rkey" 75 - ], 65 + "required": ["did", "collection", "rkey"], 76 66 "properties": { 77 67 "did": { 78 68 "type": "string",
+56
lexicons/blue.microcosm/links/getDistinct.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "blue.microcosm.links.getDistinct", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "a list of distinct dids with a specific records linking to a target at a specified path", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["subject", "source"], 11 + "properties": { 12 + "subject": { 13 + "type": "string", 14 + "format": "uri", 15 + "description": "the target being linked to (at-uri, did, or uri)" 16 + }, 17 + "source": { 18 + "type": "string", 19 + "description": "collection and path specification (e.g., 'app.bsky.feed.like:subject.uri')" 20 + }, 21 + "limit": { 22 + "type": "integer", 23 + "minimum": 1, 24 + "maximum": 100, 25 + "default": 16, 26 + "description": "number of results to return" 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["total", "linking_dids"], 35 + "properties": { 36 + "total": { 37 + "type": "integer", 38 + "description": "total number of matching links" 39 + }, 40 + "linking_dids": { 41 + "type": "array", 42 + "items": { 43 + "type": "string", 44 + "format": "did" 45 + } 46 + }, 47 + "cursor": { 48 + "type": "string", 49 + "description": "pagination cursor" 50 + } 51 + } 52 + } 53 + } 54 + } 55 + } 56 + }