e071e91a Andy McCall 2 days ago ↶
1
2
3
4
5
77df591d Andy McCall 1 day ago ↶
6
use crate :: http:: { AppState, PeerInfo} ;
e071e91a Andy McCall 2 days ago ↶
7
use axum:: body:: Bytes;
8
use axum:: extract:: connect_info:: ConnectInfo;
9
use axum:: extract:: { Path, Query, State} ;
10
use axum:: http:: { header, HeaderMap, StatusCode} ;
11
use axum:: response:: sse:: { Event as SseEvent, KeepAlive, Sse} ;
12
use axum:: response:: { IntoResponse, Response} ;
13
use axum:: routing:: { get, post} ;
14
use axum:: Router;
15
use futures:: StreamExt;
16
use serde:: Deserialize;
17
use spectra_api as api;
18
use spectra_core:: authn:: Caller;
19
use spectra_core:: conversation:: { self as conv, Author, Post, Posted, Signer} ;
20
use spectra_core:: ids:: now_ms;
21
use spectra_core:: principals:: Principal;
22
use std:: time:: Duration;
23
24
pub fn router ( state : AppState) -> Router {
25
Router:: new( )
26
.route ( " /api/v1/health" , get ( health) )
27
.route ( " /api/v1/version" , get ( version) )
28
.route ( " /api/v1/brief" , get ( brief) )
29
.route ( " /api/v1/agents" , get ( agents) )
30
.route ( " /api/v1/projects" , get ( projects) )
31
.route ( " /api/v1/channels" , get ( channels) )
32
.route ( " /api/v1/channels/{name}/messages" , get ( messages) .post ( post_message) )
33
.route ( " /api/v1/threads/{id}" , get ( thread) )
34
.route ( " /api/v1/inbox" , get ( inbox) )
35
.route ( " /api/v1/inbox/ack" , post ( ack) )
36
.route ( " /api/v1/search" , get ( search) )
37
.route ( " /api/v1/events" , get ( events) )
67bfa06a Andy McCall 1 day ago ↶
38
.route ( " /api/v1/admin/verify" , get ( admin_verify) )
b044d909 Andy McCall 1 day ago ↶
39
.route ( " /api/v1/briefs" , get ( briefs) .put ( set_brief) )
e071e91a Andy McCall 2 days ago ↶
40
.with_state ( state)
41
}
42
43
44
pub fn problem ( status : StatusCode, title : & str , detail : Option< String> ) -> Response {
45
let p = api:: Problem { kind: " about:blank" .into ( ) , title: title.into ( ) , status: status.as_u16 ( ) , detail } ;
46
let mut r = ( status, axum:: Json( p) ) .into_response ( ) ;
47
r.headers_mut ( ) .insert ( header:: CONTENT_TYPE , " application/problem+json" .parse ( ) .unwrap ( ) ) ;
48
r
49
}
50
51
fn bad ( e : anyhow:: Error) -> Response {
52
problem ( StatusCode:: BAD_REQUEST , " the request cannot be done" , Some ( format! ( " {e:#} " ) ) )
53
}
54
55
56
fn json_etag < T: serde:: Serialize> ( headers : & HeaderMap, v : & T) -> Response {
57
let body = serde_json:: to_vec( v) .unwrap_or_default ( ) ;
58
let tag = format! ( " \" {} \" " , & blake3_hex ( & body) [ .. 32 ] ) ;
59
if headers.get ( header:: IF_NONE_MATCH ) .and_then ( | h | h.to_str ( ) .ok ( ) ) = = Some ( tag.as_str ( ) ) {
60
return StatusCode:: NOT_MODIFIED .into_response ( ) ;
61
}
62
( [ ( header:: CONTENT_TYPE , " application/json" .to_string ( ) ) , ( header:: ETAG , tag) ] , body) .into_response ( )
63
}
64
65
fn blake3_hex ( b : & [u8 ]) -> String {
66
hex:: encode( * spectra_core:: event:: hash_body_plain( b) )
67
}
68
862f0db2 Andy McCall 1 day ago ↶
69
e071e91a Andy McCall 2 days ago ↶
70
async fn caller (
71
st : & AppState,
72
peer : PeerInfo,
73
headers : & HeaderMap,
74
method_path : & str ,
75
body : & [u8 ],
76
read : bool ,
77
) -> Result< Caller, Response> {
862f0db2 Andy McCall 1 day ago ↶
78
let _ = read;
77df591d Andy McCall 1 day ago ↶
79
let signed = headers.get ( " spectra-key" ) .is_some ( ) ;
80
crate :: http:: authenticate_why( st, peer, headers, method_path, body) .await.map_err ( | ( status , why ) | {
81
if status = = StatusCode:: FORBIDDEN {
82
problem ( status, " this origin may not call Spectra" , None )
83
} else if signed {
84
85
problem ( StatusCode:: UNAUTHORIZED , " not authenticated" , Some ( why) )
86
} else {
87
problem (
88
StatusCode:: UNAUTHORIZED ,
89
" not authenticated" ,
862f0db2 Andy McCall 1 day ago ↶
90
Some ( " sign the request with your key: spectra's own client, its shims and `spectra bridge` do" .into ( ) ) ,
77df591d Andy McCall 1 day ago ↶
91
)
92
}
e071e91a Andy McCall 2 days ago ↶
93
} )
94
}
95
96
fn path_of ( uri : & axum:: http:: Uri) -> String {
97
uri.path_and_query ( ) .map ( | p | p.as_str ( ) .to_string ( ) ) .unwrap_or_else ( | | uri.path ( ) .to_string ( ) )
98
}
99
100
macro_rules! who {
101
( $st : expr , $peer : expr , $headers : expr , $uri : expr , $method:literal, $body : expr , $read : expr ) => {
102
match caller ( & $st , $peer , & $headers , & format! ( " {} {} " , $method , path_of ( & $uri ) ) , $body , $read ) .await {
103
Ok ( c) => c,
104
Err ( r) => return r,
105
}
106
} ;
107
}
108
67bfa06a Andy McCall 1 day ago ↶
109
110
111
async fn admin_verify (
112
State( st ) : State< AppState> ,
113
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
114
headers : HeaderMap,
115
uri : axum:: http:: Uri,
116
) -> Response {
117
let c = who! ( st, peer, headers, uri, " GET" , b " " , false ) ;
118
if ! matches! ( c.principal, Principal:: Owner { .. } ) {
119
return problem ( StatusCode:: FORBIDDEN , " only the owner verifies the ledger" , None ) ;
120
}
121
let ledger = match spectra_core:: ledger:: Ledger:: open_read_only( & st.daemon.data.join ( " spectra.db" ) ) .await {
122
Ok ( l) => l,
123
Err ( e) => return bad ( e) ,
124
} ;
125
match ledger.verify ( & mut spectra_core:: principals:: Principals:: default( ) ) .await {
126
Ok ( v) => axum:: Json( v) .into_response ( ) ,
127
Err ( e) => bad ( e) ,
128
}
129
}
130
b044d909 Andy McCall 1 day ago ↶
131
132
async fn briefs (
133
State( st ) : State< AppState> ,
134
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
135
headers : HeaderMap,
136
uri : axum:: http:: Uri,
137
) -> Response {
138
let _ = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
139
let pool = st.daemon.ledger.pool ( ) ;
140
let overall = spectra_core:: rules:: layer( pool, None ) .await.ok ( ) .flatten ( ) ;
141
let rows: Vec< ( String, String) > = sqlx:: query_as( " SELECT project, text FROM briefs WHERE layer = 'project' ORDER BY project" )
142
.fetch_all ( pool)
143
.await
144
.unwrap_or_default ( ) ;
145
json_etag (
146
& headers,
147
& api:: Briefs { overall, projects: rows.into_iter ( ) .map ( | ( project , text ) | api:: ProjectBrief { project, text } ) .collect ( ) } ,
148
)
149
}
150
151
152
153
async fn set_brief (
154
State( st ) : State< AppState> ,
155
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
156
headers : HeaderMap,
157
uri : axum:: http:: Uri,
158
body : Bytes,
159
) -> Response {
160
let c = who! ( st, peer, headers, uri, " PUT" , & body, false ) ;
161
if ! matches! ( c.principal, Principal:: Owner { .. } ) {
162
return problem ( StatusCode:: FORBIDDEN , " only the owner sets a brief" , None ) ;
163
}
164
let b: api:: SetBrief = match serde_json:: from_slice( & body) {
165
Ok ( b) => b,
166
Err ( e) => return problem ( StatusCode:: BAD_REQUEST , " not a brief" , Some ( e.to_string ( ) ) ) ,
167
} ;
168
if let Err ( e) = spectra_core:: rules:: check( & b.text) {
169
return problem ( StatusCode:: BAD_REQUEST , " too long" , Some ( e.to_string ( ) ) ) ;
170
}
171
let project = b.project.filter ( | p | ! p.is_empty ( ) ) ;
172
if let Some ( p) = & project {
173
let known: Option< String> = sqlx:: query_scalar( " SELECT name FROM projects WHERE name = ?" )
174
.bind ( p)
175
.fetch_optional ( st.daemon.ledger.pool ( ) )
176
.await
177
.unwrap_or ( None ) ;
178
if known.is_none ( ) {
179
return problem ( StatusCode:: NOT_FOUND , " no such project" , Some ( p.clone ( ) ) ) ;
180
}
181
}
182
let mut e = spectra_core:: event:: NewEvent:: system( " brief.set" , serde_json:: json! ( { " project" : project, " text" : b.text } ) )
183
.by ( c.principal.actor ( ) , c.auth.clone ( ) ) ;
184
e.project = project.clone ( ) ;
185
match st.daemon.ledger.append_one ( e) .await {
186
Ok ( ev) => axum:: Json( serde_json:: json! ( { " seq" : ev.seq } ) ) .into_response ( ) ,
187
Err ( e) => bad ( e) ,
188
}
189
}
190
e071e91a Andy McCall 2 days ago ↶
191
async fn health ( ) -> Response {
192
axum:: Json( serde_json:: json! ( { " ok" : true } ) ) .into_response ( )
193
}
194
195
async fn version ( ) -> Response {
196
axum:: Json( api:: Version {
197
version: env! ( " CARGO_PKG_VERSION" ) .into ( ) ,
198
ledger_format: spectra_core:: LEDGER_FORMAT ,
199
mcp_protocol_versions: crate :: PROTOCOL_VERSIONS .iter ( ) .map ( | v | v.to_string ( ) ) .collect ( ) ,
200
} )
201
.into_response ( )
202
}
203
204
async fn agent_list ( st : & AppState) -> Vec< api:: Agent> {
205
# [ allow ( clippy::type_complexity ) ]
1876f2ce Andy McCall 1 day ago ↶
206
let rows: Vec< ( String, String, String, String, String, Option< String> , Option< i64 > , String) > = sqlx:: query_as(
207
" SELECT a.name, a.owner_name, a.key, a.kind, a.project, p.status_text, a.disabled_ms, a.scopes FROM agents a LEFT JOIN presence p ON p.agent = a.name ORDER BY a.name" ,
e071e91a Andy McCall 2 days ago ↶
208
)
209
.fetch_all ( st.daemon.ledger.pool ( ) )
210
.await
211
.unwrap_or_default ( ) ;
212
let presence = st.daemon.presence.lock ( ) .await;
67bfa06a Andy McCall 1 day ago ↶
213
let principals = st.daemon.principals.read ( ) .await;
e071e91a Andy McCall 2 days ago ↶
214
let now = now_ms ( ) ;
215
rows.into_iter ( )
1876f2ce Andy McCall 1 day ago ↶
216
.map ( | ( name , owner , key , kind , project , status , disabled , scopes ) | {
217
let channels = serde_json:: from_str:: < Vec< String> > ( & scopes)
218
.unwrap_or_default ( )
219
.into_iter ( )
220
.filter_map ( | s | s.strip_prefix ( " channel:" ) .map ( String :: from) )
221
.collect ( ) ;
98a0fb9f Andy McCall 1 day ago ↶
222
let k = spectra_core:: identity:: KeyId:: from_hex( & key) ;
67bfa06a Andy McCall 1 day ago ↶
223
let state = match k.map ( | k | principals.principal ( & k, now) ) {
224
Some ( Ok ( _ ) ) => " ok" .to_string ( ) ,
225
Some ( Err ( why) ) => why,
226
None => " a malformed key" .into ( ) ,
227
} ;
228
let delegation_expires = k.and_then ( | k | principals.owner ( ) .and_then ( | o | o.delegate ( & k) ) .and_then ( | d | d.expires ) ) ;
229
api:: Agent {
230
display: format! ( " {name} · {owner} 's agent" ) ,
231
presence: if disabled.is_some ( ) { " offline" .into ( ) } else { presence.state ( & name, now) .into ( ) } ,
232
disabled: disabled.is_some ( ) ,
233
name,
234
owner,
235
key,
236
kind,
237
project,
238
status,
239
state,
240
delegation_expires,
1876f2ce Andy McCall 1 day ago ↶
241
channels,
67bfa06a Andy McCall 1 day ago ↶
242
}
e071e91a Andy McCall 2 days ago ↶
243
} )
244
.collect ( )
245
}
246
247
async fn agents (
248
State( st ) : State< AppState> ,
249
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
250
headers : HeaderMap,
251
uri : axum:: http:: Uri,
252
) -> Response {
253
let _ = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
254
json_etag ( & headers, & agent_list ( & st) .await)
255
}
256
257
async fn projects (
258
State( st ) : State< AppState> ,
259
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
260
headers : HeaderMap,
261
uri : axum:: http:: Uri,
262
) -> Response {
263
let _ = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
264
let pool = st.daemon.ledger.pool ( ) ;
98a0fb9f Andy McCall 1 day ago ↶
265
let rows: Vec< ( String, Option< String> , String, Option< String> ) > =
266
sqlx:: query_as( " SELECT name, path, description, url FROM projects ORDER BY name" ) .fetch_all ( pool) .await.unwrap_or_default ( ) ;
e071e91a Andy McCall 2 days ago ↶
267
let mut out = Vec :: new( ) ;
98a0fb9f Andy McCall 1 day ago ↶
268
for ( name, path, description, url) in rows {
e071e91a Andy McCall 2 days ago ↶
269
let depends_on = sqlx:: query_scalar( " SELECT depends_on FROM project_relations WHERE project = ? ORDER BY 1" )
270
.bind ( & name)
271
.fetch_all ( pool)
272
.await
273
.unwrap_or_default ( ) ;
274
let depended_on_by = sqlx:: query_scalar( " SELECT project FROM project_relations WHERE depends_on = ? ORDER BY 1" )
275
.bind ( & name)
276
.fetch_all ( pool)
277
.await
278
.unwrap_or_default ( ) ;
67bfa06a Andy McCall 1 day ago ↶
279
let agents = sqlx:: query_scalar( " SELECT name FROM agents WHERE project = ? AND disabled_ms IS NULL ORDER BY name" )
280
.bind ( & name)
281
.fetch_all ( pool)
282
.await
283
.unwrap_or_default ( ) ;
98a0fb9f Andy McCall 1 day ago ↶
284
out.push ( api:: Project { name, path, description, url, depends_on, depended_on_by, agents } ) ;
e071e91a Andy McCall 2 days ago ↶
285
}
286
json_etag ( & headers, & out)
287
}
288
289
async fn channels (
290
State( st ) : State< AppState> ,
291
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
292
headers : HeaderMap,
293
uri : axum:: http:: Uri,
294
) -> Response {
295
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
296
let all = match conv:: channels( & st.daemon.ledger) .await {
297
Ok ( a) => a,
298
Err ( e) => return bad ( e) ,
299
} ;
300
let mut out = Vec :: new( ) ;
301
for ch in all.into_iter ( ) .filter ( | ch | conv:: can_read( & c.principal, ch) ) {
302
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM messages WHERE channel = ?" )
303
.bind ( & ch.id)
304
.fetch_one ( st.daemon.ledger.pool ( ) )
305
.await
306
.unwrap_or ( 0 ) ;
307
out.push ( api:: Channel { display: ch.display ( ) , id: ch.id, name: ch.name, kind: ch.kind, purpose: ch.purpose, messages: n } ) ;
308
}
309
json_etag ( & headers, & out)
310
}
311
312
async fn to_api ( st : & AppState, m : conv:: Message, ch : & str ) -> api:: Message {
313
api:: Message {
314
author_display: conv:: display_name( & st.daemon.ledger, & m.author) .await,
315
mentions: serde_json:: from_str( & m.mentions) .unwrap_or_default ( ) ,
316
refs: serde_json:: from_str( & m.refs) .unwrap_or_default ( ) ,
317
redacted: m.redacted.is_some ( ) ,
318
id: m.id,
319
channel: ch.to_string ( ) ,
320
reply_to: m.reply_to,
321
author: m.author,
322
kind: m.kind,
323
body: m.body,
324
at_ms: m.at_ms,
325
replies: m.replies,
326
}
327
}
328
329
# [ derive ( Deserialize ) ]
330
struct PageQuery {
331
after : Option< String> ,
332
limit : Option< u32 > ,
333
}
334
335
async fn messages (
336
State( st ) : State< AppState> ,
337
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
338
headers : HeaderMap,
339
uri : axum:: http:: Uri,
340
Path( name ) : Path< String> ,
341
Query( q ) : Query< PageQuery> ,
342
) -> Response {
343
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
344
let limit = q.limit.unwrap_or ( 50 ) .clamp ( 1 , 500 ) ;
345
match conv:: read( & st.daemon.ledger, & c.principal, Some ( & name) , None , q.after.as_deref ( ) , limit) .await {
346
Ok ( ( ch, msgs) ) => {
347
let next = ( msgs.len ( ) as u32 = = limit) .then ( | | msgs.last ( ) .map ( | m | m.id.clone ( ) ) ) .flatten ( ) ;
348
let mut items = Vec :: new( ) ;
349
for m in msgs {
350
items.push ( to_api ( & st, m, & ch.display ( ) ) .await) ;
351
}
352
json_etag ( & headers, & api:: Messages { channel: ch.display ( ) , items, next } )
353
}
354
Err ( e) => problem ( StatusCode:: NOT_FOUND , " no such channel, or not yours to read" , Some ( format! ( " {e:#} " ) ) ) ,
355
}
356
}
357
358
async fn thread (
359
State( st ) : State< AppState> ,
360
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
361
headers : HeaderMap,
362
uri : axum:: http:: Uri,
363
Path( id ) : Path< String> ,
364
Query( q ) : Query< PageQuery> ,
365
) -> Response {
366
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
367
let limit = q.limit.unwrap_or ( 200 ) .clamp ( 1 , 500 ) ;
368
match conv:: read( & st.daemon.ledger, & c.principal, None , Some ( & id) , q.after.as_deref ( ) , limit) .await {
369
Ok ( ( ch, msgs) ) => {
370
let next = ( msgs.len ( ) as u32 = = limit) .then ( | | msgs.last ( ) .map ( | m | m.id.clone ( ) ) ) .flatten ( ) ;
371
let mut items = Vec :: new( ) ;
372
for m in msgs {
373
items.push ( to_api ( & st, m, & ch.display ( ) ) .await) ;
374
}
375
json_etag ( & headers, & api:: Messages { channel: ch.display ( ) , items, next } )
376
}
377
Err ( e) => problem ( StatusCode:: NOT_FOUND , " no such thread, or not yours to read" , Some ( format! ( " {e:#} " ) ) ) ,
378
}
379
}
380
381
async fn post_message (
382
State( st ) : State< AppState> ,
383
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
384
headers : HeaderMap,
385
uri : axum:: http:: Uri,
386
Path( name ) : Path< String> ,
387
body : Bytes,
388
) -> Response {
389
let c = who! ( st, peer, headers, uri, " POST" , & body, false ) ;
390
let n: api:: NewMessage = match serde_json:: from_slice( & body) {
391
Ok ( n) => n,
392
Err ( e) => return problem ( StatusCode:: BAD_REQUEST , " not a message" , Some ( e.to_string ( ) ) ) ,
393
} ;
394
let author = Author { principal: c.principal.clone ( ) , signer: Signer:: Request( c.auth.clone ( ) ) } ;
395
let post = Post {
396
channel: if n.reply_to.is_some ( ) { None } else { Some ( name) } ,
397
body: n.body,
398
reply_to: n.reply_to,
399
kind: n.kind,
400
.. Default :: default( )
401
} ;
402
match conv:: post( & st.daemon.ledger, & author, post, now_ms ( ) ) .await {
403
Ok ( Posted:: Message { id, channel } ) => {
404
( StatusCode:: CREATED , axum:: Json( api:: Posted { id, channel, duplicate: false } ) ) .into_response ( )
405
}
406
Ok ( Posted:: Duplicate { id } ) => axum:: Json( api:: Posted { id, channel: String :: new( ) , duplicate: true } ) .into_response ( ) ,
407
Err ( e) => bad ( e) ,
408
}
409
}
410
411
async fn inbox_items ( st : & AppState, actor : & str ) -> Vec< api:: InboxItem> {
67bfa06a Andy McCall 1 day ago ↶
412
let mut out = Vec :: new( ) ;
413
for i in conv:: inbox( & st.daemon.ledger, actor, None ) .await.unwrap_or_default ( ) {
414
let from_display = conv:: display_name( & st.daemon.ledger, & i.from) .await;
415
out.push ( api:: InboxItem {
416
id: i.id,
417
kind: i.kind,
418
at_ms: i.at_ms,
419
from: i.from,
420
from_display,
421
channel: i.channel,
422
summary: i.summary,
423
} ) ;
424
}
425
out
e071e91a Andy McCall 2 days ago ↶
426
}
427
428
async fn inbox ( State( st ) : State< AppState> , ConnectInfo( peer ) : ConnectInfo< PeerInfo> , headers : HeaderMap, uri : axum:: http:: Uri) -> Response {
429
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
430
json_etag ( & headers, & inbox_items ( & st, & c.principal.actor ( ) ) .await)
431
}
432
433
async fn ack (
434
State( st ) : State< AppState> ,
435
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
436
headers : HeaderMap,
437
uri : axum:: http:: Uri,
438
body : Bytes,
439
) -> Response {
440
let c = who! ( st, peer, headers, uri, " POST" , & body, false ) ;
441
let a: api:: Ack = match serde_json:: from_slice( & body) {
442
Ok ( a) => a,
443
Err ( e) => return problem ( StatusCode:: BAD_REQUEST , " not an ack" , Some ( e.to_string ( ) ) ) ,
444
} ;
445
let author = Author { principal: c.principal, signer: Signer:: Request( c.auth) } ;
446
match conv:: ack( & st.daemon.ledger, & author, & a.ids, a.reaction.as_deref ( ) ) .await {
447
Ok ( n) => axum:: Json( serde_json:: json! ( { " read" : n } ) ) .into_response ( ) ,
448
Err ( e) => bad ( e) ,
449
}
450
}
451
452
async fn brief ( State( st ) : State< AppState> , ConnectInfo( peer ) : ConnectInfo< PeerInfo> , headers : HeaderMap, uri : axum:: http:: Uri) -> Response {
453
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
454
let project = match & c.principal {
455
Principal:: Agent { project, .. } => Some ( project.clone ( ) ) ,
456
_ => None ,
457
} ;
b044d909 Andy McCall 1 day ago ↶
458
let rules = spectra_core:: rules:: compile( st.daemon.ledger.pool ( ) , project.as_deref ( ) ) .await.map ( | r | r.version ) .unwrap_or_default ( ) ;
e071e91a Andy McCall 2 days ago ↶
459
let inbox = inbox_items ( & st, & c.principal.actor ( ) ) .await;
460
let b = api:: Brief {
461
you: c.principal.display ( ) ,
462
actor: c.principal.actor ( ) ,
463
project,
464
rules_version: rules,
465
unread: inbox.len ( ) as i64 ,
466
inbox,
467
agents: agent_list ( & st) .await,
468
head: st.daemon.ledger.head ( ) .await.0 ,
469
} ;
470
json_etag ( & headers, & b)
471
}
472
473
# [ derive ( Deserialize ) ]
474
struct SearchQuery {
475
q : String,
476
channel : Option< String> ,
477
author : Option< String> ,
478
since : Option< String> ,
479
limit : Option< u32 > ,
480
}
481
482
async fn search (
483
State( st ) : State< AppState> ,
484
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
485
headers : HeaderMap,
486
uri : axum:: http:: Uri,
487
Query( q ) : Query< SearchQuery> ,
488
) -> Response {
489
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
490
let since = q.since.as_deref ( ) .and_then ( | s | crate :: tools:: parse_since( s, now_ms ( ) ) ) ;
491
match conv:: search( & st.daemon.ledger, & c.principal, & q.q, q.channel.as_deref ( ) , q.author.as_deref ( ) , since, q.limit.unwrap_or ( 20 ) ) .await
492
{
493
Ok ( hits) => {
67bfa06a Andy McCall 1 day ago ↶
494
let mut out: Vec< api:: Hit> = Vec :: new( ) ;
495
for h in hits {
496
let author_display = conv:: display_name( & st.daemon.ledger, & h.author) .await;
497
out.push ( api:: Hit {
498
id: h.id,
499
channel: h.channel,
500
author: h.author,
501
at_ms: h.at_ms,
502
snippet: h.snippet,
503
thread: h.thread,
504
author_display,
505
} ) ;
506
}
e071e91a Andy McCall 2 days ago ↶
507
json_etag ( & headers, & out)
508
}
509
Err ( e) => bad ( e) ,
510
}
511
}
512
513
# [ derive ( Deserialize ) ]
514
struct EventsQuery {
515
after : Option< u64 > ,
516
kind : Option< String> ,
517
limit : Option< u32 > ,
518
follow : Option< u8 > ,
519
520
max : Option< usize > ,
521
}
522
523
fn summary ( e : & spectra_core:: event:: Event) -> api:: EventSummary {
524
api:: EventSummary {
525
seq: e.seq,
526
id: e.id.clone ( ) ,
527
at_ms: e.at_ms,
528
kind: e.kind.clone ( ) ,
529
actor: e.actor.clone ( ) ,
530
subject: e.subject.clone ( ) ,
531
method: e.auth.method.name ( ) .into ( ) ,
532
assurance: e.auth.assurance,
533
hash: hex:: encode( e.hash) ,
534
}
535
}
536
537
538
async fn events (
539
State( st ) : State< AppState> ,
540
ConnectInfo( peer ) : ConnectInfo< PeerInfo> ,
541
headers : HeaderMap,
542
uri : axum:: http:: Uri,
543
Query( q ) : Query< EventsQuery> ,
544
) -> Response {
545
let c = who! ( st, peer, headers, uri, " GET" , b " " , true ) ;
546
547
548
if ! matches! ( c.principal, Principal:: Owner { .. } ) {
549
return problem ( StatusCode:: FORBIDDEN , " the event log is the owner's" , None ) ;
550
}
551
if q.follow = = Some ( 1 ) {
552
let kind = q.kind.clone ( ) ;
553
let rx = st.daemon.ledger.subscribe ( ) ;
554
let stream = tokio_stream:: wrappers:: BroadcastStream:: new( rx)
555
.filter_map ( move | e| {
556
let kind = kind.clone ( ) ;
557
async move {
558
let e = e.ok ( ) ? ;
559
if kind.as_ref ( ) .is_some_and ( | k | * k ! = e.kind) {
560
return None ;
561
}
562
Some ( Ok : : < _ , std:: convert:: Infallible> (
563
SseEvent:: default( ) .data ( serde_json:: to_string( & summary ( & e) ) .unwrap_or_default ( ) ) ,
564
) )
565
}
566
} )
567
.take ( q.max.unwrap_or ( usize :: MAX ) ) ;
98a0fb9f Andy McCall 1 day ago ↶
568
569
570
let hello = futures:: stream:: once( async { Ok : : < _ , std:: convert:: Infallible> ( SseEvent:: default( ) .comment ( " subscribed" ) ) } ) ;
571
return Sse:: new( hello.chain ( stream) ) .keep_alive ( KeepAlive:: new( ) .interval ( Duration:: from_secs( 15 ) ) ) .into_response ( ) ;
e071e91a Andy McCall 2 days ago ↶
572
}
573
let limit = q.limit.unwrap_or ( 100 ) .clamp ( 1 , 1000 ) ;
574
let from = q.after.map ( | a | a + 1 ) .unwrap_or ( 1 ) ;
575
let evs = match & q.kind {
576
Some ( k) => st.daemon.ledger.events_of_kinds ( from, & [ k.as_str ( ) ] , limit) .await,
577
None => st.daemon.ledger.events ( from, limit) .await,
578
} ;
579
match evs {
580
Ok ( evs) => {
581
let next = ( evs.len ( ) as u32 = = limit) .then ( | | evs.last ( ) .map ( | e | e.seq ) ) .flatten ( ) ;
582
json_etag ( & headers, & api:: Events { items: evs.iter ( ) .map ( summary) .collect ( ) , next } )
583
}
584
Err ( e) => bad ( e) ,
585
}
586
}