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