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

Configure Feed

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

api: serve distinct dids

phil db35300a ad544d4d

+273 -15
+110 -1
constellation/src/server/mod.rs
··· 9 9 use tokio_util::sync::CancellationToken; 10 10 11 11 use crate::storage::LinkReader; 12 - use constellation::RecordId; 12 + use constellation::{Did, RecordId}; 13 13 14 14 mod acceptable; 15 15 mod filters; ··· 34 34 }), 35 35 ) 36 36 .route( 37 + "/links/count/distinct-dids", 38 + get({ 39 + let store = store.clone(); 40 + move |accept, query| async { 41 + block_in_place(|| count_distinct_dids(accept, query, store)) 42 + } 43 + }), 44 + ) 45 + .route( 37 46 "/links", 38 47 get({ 39 48 let store = store.clone(); 40 49 move |accept, query| async { block_in_place(|| get_links(accept, query, store)) } 50 + }), 51 + ) 52 + .route( 53 + "/links/distinct-dids", 54 + get({ 55 + let store = store.clone(); 56 + move |accept, query| async { 57 + block_in_place(|| get_distinct_dids(accept, query, store)) 58 + } 41 59 }), 42 60 ) 43 61 .route( ··· 103 121 } 104 122 105 123 #[derive(Clone, Deserialize)] 124 + struct GetDidsCountQuery { 125 + target: String, 126 + collection: String, 127 + path: String, 128 + } 129 + #[derive(Template, Serialize)] 130 + #[template(path = "dids-count.html.j2")] 131 + struct GetDidsCountResponse { 132 + total: u64, 133 + #[serde(skip_serializing)] 134 + query: GetDidsCountQuery, 135 + } 136 + fn count_distinct_dids( 137 + accept: ExtractAccept, 138 + query: Query<GetDidsCountQuery>, 139 + store: impl LinkReader, 140 + ) -> Result<impl IntoResponse, http::StatusCode> { 141 + let total = store 142 + .get_distinct_did_count(&query.target, &query.collection, &query.path) 143 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 144 + Ok(acceptable( 145 + accept, 146 + GetDidsCountResponse { 147 + total, 148 + query: (*query).clone(), 149 + }, 150 + )) 151 + } 152 + 153 + #[derive(Clone, Deserialize)] 106 154 struct GetLinkItemsQuery { 107 155 target: String, 108 156 collection: String, ··· 157 205 GetLinkItemsResponse { 158 206 total: paged.version.0, 159 207 linking_records: paged.items, 208 + cursor, 209 + query: (*query).clone(), 210 + }, 211 + )) 212 + } 213 + 214 + #[derive(Clone, Deserialize)] 215 + struct GetDidItemsQuery { 216 + target: String, 217 + collection: String, 218 + path: String, 219 + cursor: Option<OpaqueApiCursor>, 220 + limit: Option<u64>, 221 + // TODO: allow reverse (er, forward) order as well 222 + } 223 + #[derive(Template, Serialize)] 224 + #[template(path = "dids.html.j2")] 225 + struct GetDidItemsResponse { 226 + // what does staleness mean? 227 + // - new links have appeared. would be nice to offer a `since` cursor to fetch these. and/or, 228 + // - links have been deleted. hmm. 229 + total: u64, 230 + linking_dids: Vec<Did>, 231 + cursor: Option<OpaqueApiCursor>, 232 + #[serde(skip_serializing)] 233 + query: GetDidItemsQuery, 234 + } 235 + fn get_distinct_dids( 236 + accept: ExtractAccept, 237 + query: Query<GetDidItemsQuery>, 238 + store: impl LinkReader, 239 + ) -> Result<impl IntoResponse, http::StatusCode> { 240 + let until = query 241 + .cursor 242 + .clone() 243 + .map(|oc| ApiCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) 244 + .transpose()? 245 + .map(|c| c.next); 246 + 247 + let limit = query.limit.unwrap_or(DEFAULT_CURSOR_LIMIT); 248 + if limit > DEFAULT_CURSOR_LIMIT_MAX { 249 + return Err(http::StatusCode::BAD_REQUEST); 250 + } 251 + 252 + let paged = store 253 + .get_distinct_dids(&query.target, &query.collection, &query.path, limit, until) 254 + .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 255 + 256 + let cursor = paged.next.map(|next| { 257 + ApiCursor { 258 + version: paged.version, 259 + next, 260 + } 261 + .into() 262 + }); 263 + 264 + Ok(acceptable( 265 + accept, 266 + GetDidItemsResponse { 267 + total: paged.version.0, 268 + linking_dids: paged.items, 160 269 cursor, 161 270 query: (*query).clone(), 162 271 },
+11 -1
constellation/templates/base.html.j2
··· 14 14 h2, h3 { 15 15 margin-top: 2.4em; 16 16 } 17 + h3.route { 18 + font-size: 1.618em; 19 + border-bottom: 2px solid #def; 20 + } 21 + h3.route code { 22 + display: block; 23 + width: max-content; 24 + border-bottom-left-radius: 0; 25 + border-bottom-right-radius: 0; 26 + } 17 27 code, .code { 18 28 background: #def; 19 29 display: inline-block; ··· 39 49 </style> 40 50 </head> 41 51 <body class="{% block body_classes %}{% endblock %}"> 42 - <h1><a href="/">This</a> is an <a href="https://github.com/at-ucosm/links/tree/main/constellation">atproto link aggregator</a> server from <a href="https://github.com/at-ucosm">µcosm</a>!</h1> 52 + <h1><a href="/">This</a> is an <a href="https://github.com/at-ucosm/links/tree/main/constellation">atproto link aggregator</a> server from <a href="https://github.com/at-ucosm">microcosm</a>!</h1> 43 53 {% block content %}{% endblock %} 44 54 45 55 <footer>
+29
constellation/templates/dids-count.html.j2
··· 1 + {% extends "base.html.j2" %} 2 + {% import "try-it-macros.html.j2" as try_it %} 3 + 4 + {% block title %}Link count{% endblock %} 5 + 6 + {% block content %} 7 + 8 + {% call try_it::dids_count(query.target, query.collection, query.path) %} 9 + 10 + <h2> 11 + Total DIDs linking to <code>{{ query.target }}</code> 12 + {% if let Some(browseable_uri) = query.target|to_browseable %} 13 + <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 14 + {% endif %} 15 + </h2> 16 + 17 + <p><strong><code>{{ total }}</code></strong> total linking DIDs from <code>{{ query.collection }}</code> at <code>{{ query.path }}</code></p> 18 + 19 + <ul> 20 + <li>See these dids at <code>/links/distinct-dids</code>: <a href="/links/distinct-dids?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode() }}">/links/distinct-dids?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode }}</a></li> 21 + <li>See the linking records at <code>/links</code>: <a href="/links?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode() }}">/links/distinct-dids?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode }}</a></li> 22 + </ul> 23 + 24 + <details> 25 + <summary>Raw JSON response</summary> 26 + <pre class="code">{{ self|tojson }}</pre> 27 + </details> 28 + 29 + {% endblock %}
+49
constellation/templates/dids.html.j2
··· 1 + {% extends "base.html.j2" %} 2 + {% import "try-it-macros.html.j2" as try_it %} 3 + 4 + {% block title %}DIDs{% endblock %} 5 + 6 + {% block content %} 7 + 8 + {% call try_it::dids(query.target, query.collection, query.path) %} 9 + 10 + <h2> 11 + DIDs with links to <code>{{ query.target }}</code> 12 + {% if let Some(browseable_uri) = query.target|to_browseable %} 13 + <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 14 + {% endif %} 15 + </h2> 16 + 17 + <p><strong>{{ total }} dids</strong> from <code>{{ query.collection }}</code> at <code>{{ query.path }}</code></p> 18 + 19 + <ul> 20 + <li>See linking records to this target <code>/links</code>: <a href="/links?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode }}">/links?target={{ query.target }}&collection={{ query.collection }}&path={{ query.path }}</a></li> 21 + <li>See all links to this target <code>/links/all/count</code>: <a href="/links/all/count?target={{ query.target|urlencode }}">/links/all/count?target={{ query.target }}</a></li> 22 + </ul> 23 + 24 + <h3>DIDs, most recent first:</h3> 25 + 26 + {% for did in linking_dids %} 27 + <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ did.0 }} 28 + -> see <a href="/links/all/count?target={{ did.0|urlencode }}">links to this DID</a> 29 + -> browse <a href="https://atproto-browser-plus-links.vercel.app/at/{{ did.0|urlencode }}">this DID record</a></pre> 30 + {% endfor %} 31 + 32 + {% if let Some(c) = cursor %} 33 + <form method="get" action="/links"> 34 + <input type="hidden" name="target" value="{{ query.target }}" /> 35 + <input type="hidden" name="collection" value="{{ query.collection }}" /> 36 + <input type="hidden" name="path" value="{{ query.path }}" /> 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 %}
+36 -3
constellation/templates/hello.html.j2
··· 14 14 15 15 <h2>Endpoints</h2> 16 16 17 - <h3><code>GET /links</code></h3> 17 + <h3 class="route"><code>GET /links</code></h3> 18 18 19 19 <p>A list of records linking to a target.</p> 20 20 ··· 30 30 {% call try_it::links("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like", ".subject.uri") %} 31 31 32 32 33 - <h3><code>GET /links/count</code></h3> 33 + <h3 class="route"><code>GET /links/distinct-dids</code></h3> 34 + 35 + <p>A list of distinct DIDs (identities) with links to a target.</p> 36 + 37 + <h4>Query parameters:</h4> 38 + 39 + <ul> 40 + <li><code>target</code>: required, must url-encode. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></li> 41 + <li><code>collection</code>: required. Example: <code>app.bsky.feed.like</code></li> 42 + <li><code>path</code>: required, must url-encode. Example: <code>.subject.uri</code></li> 43 + </ul> 44 + 45 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 46 + {% call try_it::dids("at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r", "app.bsky.feed.like", ".subject.uri") %} 47 + 48 + 49 + <h3 class="route"><code>GET /links/count</code></h3> 34 50 35 51 <p>The total number of links pointing at a given target.</p> 36 52 ··· 47 63 {% call try_it::links_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block", ".subject") %} 48 64 49 65 50 - <h3><code>GET /links/all/count</code></h3> 66 + <h3 class="route"><code>GET /links/count/distinct-dids</code></h3> 67 + 68 + <p>The total number of DIDs (identities) with links to at a given target.</p> 69 + 70 + <h4>Query parameters:</h4> 71 + 72 + <ul> 73 + <li><code>target</code>: required, must url-encode. Example: <code>did:plc:vc7f4oafdgxsihk4cry2xpze</code></li> 74 + <li><code>collection</code>: required. Example: <code>app.bsky.graph.block</code></li> 75 + <li><code>path</code>: required, must url-encode. Example: <code>.subject</code></li> 76 + <li><code>cursor</code>: optional, see Definitions.</li> 77 + </ul> 78 + 79 + <p style="margin-bottom: 0"><strong>Try it:</strong></p> 80 + {% call try_it::dids_count("did:plc:vc7f4oafdgxsihk4cry2xpze", "app.bsky.graph.block", ".subject") %} 81 + 82 + 83 + <h3 class="route"><code>GET /links/all/count</code></h3> 51 84 52 85 <p>The total counts of all links pointing at a given target, by collection and path.</p> 53 86
+7 -4
constellation/templates/links.html.j2
··· 5 5 6 6 {% block content %} 7 7 8 + {% call try_it::links(query.target, query.collection, query.path) %} 9 + 8 10 <h2> 9 11 Links to <code>{{ query.target }}</code> 10 12 {% if let Some(browseable_uri) = query.target|to_browseable %} ··· 14 16 15 17 <p><strong>{{ total }} links</strong> from <code>{{ query.collection }}</code> at <code>{{ query.path }}</code></p> 16 18 17 - {% call try_it::links(query.target, query.collection, query.path) %} 18 - 19 - <p>See all links to this target <code>/links/all/count</code>: <a href="/links/all/count?target={{ query.target|urlencode }}">/links/all/count?target={{ query.target }}</a></p> 19 + <ul> 20 + <li>See distinct DIDs <code>/links/distinct-dids</code>: <a href="/links/distinct-dids?target={{ query.target|urlencode }}&collection={{ query.collection|urlencode }}&path={{ query.path|urlencode }}">/links/distinct-dids?target={{ query.target }}&collection={{ query.collection }}&path={{ query.path }}</a></li> 21 + <li>See all links to this target <code>/links/all/count</code>: <a href="/links/all/count?target={{ query.target|urlencode }}">/links/all/count?target={{ query.target }}</a></li> 22 + </ul> 20 23 21 24 <h3>Links, most recent first:</h3> 22 25 ··· 24 27 <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ record.did().0 }} (<a href="/links/all/count?target={{ record.did().0|urlencode }}">DID links</a>) 25 28 <strong>Collection</strong>: {{ record.collection }} 26 29 <strong>RKey</strong>: {{ record.rkey }} 27 - -> <a href="https://atproto-browser-plus-links.vercel.app/at/{{ record.did().0 }}/{{ record.collection }}/{{ record.rkey }}">browse record</a></pre> 30 + -> <a href="https://atproto-browser-plus-links.vercel.app/at/{{ record.did().0|urlencode }}/{{ record.collection }}/{{ record.rkey }}">browse record</a></pre> 28 31 {% endfor %} 29 32 30 33 {% if let Some(c) = cursor %}
+21 -1
constellation/templates/try-it-macros.html.j2
··· 8 8 {% endmacro %} 9 9 10 10 11 + {% macro dids(target, collection, path) %} 12 + <form method="get" action="/links/distinct-dids"> 13 + <pre class="code"><strong>GET</strong> /links/distinct-dids 14 + ?target= <input type="text" name="target" value="{{ target }}" placeholder="target" /> 15 + &collection= <input type="text" name="collection" value="{{ collection }}" placeholder="collection" /> 16 + &path= <input type="text" name="path" value="{{ path }}" placeholder="path" /> <button type="submit">get links</button></pre> 17 + </form> 18 + {% endmacro %} 19 + 20 + 11 21 {% macro links_count(target, collection, path) %} 12 22 <form method="get" action="/links/count"> 13 23 <pre class="code"><strong>GET</strong> /links/count ··· 18 28 {% endmacro %} 19 29 20 30 31 + {% macro dids_count(target, collection, path) %} 32 + <form method="get" action="/links/count/distinct-dids"> 33 + <pre class="code"><strong>GET</strong> /links/count/distinct-dids 34 + ?target= <input type="text" name="target" value="{{ target }}" placeholder="target" /> 35 + &collection= <input type="text" name="collection" value="{{ collection }}" placeholder="collection" /> 36 + &path= <input type="text" name="path" value="{{ path }}" placeholder="path" /> <button type="submit">get links count</button></pre> 37 + </form> 38 + {% endmacro %} 39 + 40 + 21 41 {% macro links_all_count(target) %} 22 42 <form method="get" action="/links/all/count"> 23 - <pre class="code"><strong>GET</strong> /links?target=<input type="text" name="target" value="{{ target }}" placeholder="target" /> <button type="submit">get all target link counts</button></pre> 43 + <pre class="code"><strong>GET</strong> /links/all/count?target=<input type="text" name="target" value="{{ target }}" placeholder="target" /> <button type="submit">get all target link counts</button></pre> 24 44 </form> 25 45 {% endmacro %}