atagen

malice

public
Star0
Watch0
787cdbbf0721a1b004712020969dadaa42f35e32
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/routes.rsRust962 lines26.3 KB
1
use std::sync::Arc;
2
3
use axum::{
4
    Router,
5
    extract::{DefaultBodyLimit, Multipart, Query, State},
6
    http::{HeaderMap, StatusCode},
7
    middleware,
8
    response::{IntoResponse, Response, Sse, sse::Event as SseEvent},
9
    routing::{get, post},
10
};
11
use futures_util::StreamExt;
12
use macltok::Retain;
13
use malice_core::{
14
    feed::subscribe,
15
    login::LoginInput,
16
    request::{Reply, Request, dispatch},
17
};
18
use serde::{Deserialize, Serialize};
19
use tower_http::{
20
    services::{ServeDir, ServeFile},
21
    trace::TraceLayer,
22
};
23
24
use crate::{
25
    csrf::{csrf_guard, session_sid, set_cookie_value},
26
    error::{ApiError, json_ok, no_content},
27
    state::{AppState, RestoreError, UserSession},
28
};
29
30
#[derive(Debug, Deserialize)]
31
struct RoomIdBody {
32
    #[serde(default)]
33
    room_id: String,
34
}
35
36
#[derive(Debug, Deserialize)]
37
struct SendBody {
38
    #[serde(default)]
39
    room_id: String,
40
    #[serde(default)]
41
    body: String,
42
    #[serde(default)]
43
    formatted_body: Option<String>,
44
    #[serde(default)]
45
    reply_to: Option<String>,
46
    #[serde(default)]
47
    mentions: Vec<String>,
48
}
49
50
#[derive(Debug, Deserialize)]
51
struct PaginateBody {
52
    #[serde(default)]
53
    room_id: String,
54
}
55
56
#[derive(Debug, Deserialize)]
57
struct JoinBody {
58
    #[serde(default)]
59
    room: String,
60
}
61
62
#[derive(Debug, Deserialize)]
63
struct CreateBody {
64
    #[serde(default)]
65
    name: String,
66
    #[serde(default)]
67
    topic: Option<String>,
68
}
69
70
#[derive(Debug, Deserialize)]
71
struct ReactBody {
72
    #[serde(default)]
73
    room_id: String,
74
    #[serde(default)]
75
    event_id: String,
76
    #[serde(default)]
77
    key: String,
78
    #[serde(default)]
79
    shortcode: Option<String>,
80
}
81
82
#[derive(Debug, Deserialize)]
83
struct EditBody {
84
    #[serde(default)]
85
    room_id: String,
86
    #[serde(default)]
87
    event_id: String,
88
    #[serde(default)]
89
    body: String,
90
    #[serde(default)]
91
    formatted_body: Option<String>,
92
    #[serde(default)]
93
    mentions: Vec<String>,
94
}
95
96
#[derive(Debug, Deserialize)]
97
struct ReadBody {
98
    #[serde(default)]
99
    room_id: String,
100
    #[serde(default)]
101
    event_id: String,
102
}
103
104
#[derive(Debug, Deserialize)]
105
struct MembersQuery {
106
    #[serde(default)]
107
    room_id: String,
108
}
109
#[derive(Debug, Deserialize)]
110
struct RecoverBody {
111
    recovery_key: String,
112
}
113
114
#[derive(Debug, Deserialize)]
115
struct VerifyBody {
116
    action: String,
117
}
118
119
#[derive(Debug, Deserialize)]
120
struct MediaQuery {
121
    mxc: String,
122
}
123
#[derive(Debug, Deserialize)]
124
struct MessageMediaQuery {
125
    media_id: String,
126
    size: String,
127
}
128
129
#[derive(Debug, Deserialize)]
130
struct TypingBody {
131
    #[serde(default)]
132
    room_id: String,
133
    #[serde(default)]
134
    typing: bool,
135
}
136
137
#[derive(Debug, Deserialize)]
138
struct AccountDataQuery {
139
    event_type: String,
140
}
141
142
#[derive(Debug, Deserialize)]
143
struct UrlPreviewQuery {
144
    #[serde(default)]
145
    room_id: String,
146
    #[serde(default)]
147
    url: String,
148
}
149
150
#[derive(Debug, Deserialize)]
151
struct AccountDataBody {
152
    event_type: String,
153
    content: serde_json::Value,
154
}
155
156
#[derive(Debug, Serialize)]
157
struct SoftLogoutInfo {
158
    user_id: String,
159
    homeserver: String,
160
}
161
162
#[derive(Debug, Serialize)]
163
struct SessionResponse {
164
    user_id: Option<String>,
165
    #[serde(skip_serializing_if = "Option::is_none")]
166
    soft_logout: Option<SoftLogoutInfo>,
167
}
168
169
async fn auth(
170
    state: &AppState,
171
    headers: &HeaderMap,
172
) -> Result<(Arc<UserSession>, String), ApiError> {
173
    let Some(sid) = session_sid(headers) else {
174
        return Err(ApiError::unauthorized("not signed in"));
175
    };
176
    match state.get_or_restore(&sid).await {
177
        Ok(user) => Ok((user, sid)),
178
        Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(
179
            "session expired; sign in again",
180
            state.cookie_secure(),
181
        )),
182
        Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(
183
            "signed out by the server, sign in again",
184
        )),
185
        Err(RestoreError::Stale(message)) => {
186
            state.remove_session(&sid, Retain::Nothing).await;
187
            Err(ApiError::unauthorized_stale(message, state.cookie_secure()))
188
        }
189
        Err(RestoreError::Unavailable(message)) => {
190
            Err(ApiError::new(StatusCode::BAD_GATEWAY, message))
191
        }
192
    }
193
}
194
195
/// commands answer when they're done, so the browser gets the outcome in the response
196
fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {
197
    match result {
198
        Ok(Reply::Done) => no_content(None),
199
        Ok(Reply::Json(value)) => json_ok(&value, None),
200
        Ok(Reply::Avatar {
201
            bytes,
202
            content_type,
203
        }) => (
204
            [
205
                (axum::http::header::CONTENT_TYPE, content_type),
206
                // mxc uris name immutable content, a changed avatar gets a new uri
207
                (
208
                    axum::http::header::CACHE_CONTROL,
209
                    "private, max-age=31536000, immutable",
210
                ),
211
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
212
            ],
213
            bytes,
214
        )
215
            .into_response(),
216
        Ok(Reply::Attachment {
217
            bytes,
218
            content_type,
219
        }) => (
220
            [
221
                (
222
                    axum::http::header::CONTENT_TYPE,
223
                    content_type.unwrap_or("application/octet-stream"),
224
                ),
225
                (
226
                    axum::http::header::CONTENT_DISPOSITION,
227
                    if content_type.is_some() {
228
                        "inline"
229
                    } else {
230
                        "attachment"
231
                    },
232
                ),
233
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
234
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
235
                (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),
236
            ],
237
            bytes,
238
        )
239
            .into_response(),
240
        Err(error) => ApiError::from(error).into_response(),
241
    }
242
}
243
244
async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {
245
    let Some(sid) = session_sid(&headers) else {
246
        return json_ok(
247
            &SessionResponse {
248
                user_id: None,
249
                soft_logout: None,
250
            },
251
            None,
252
        );
253
    };
254
    match state.get_or_restore(&sid).await {
255
        Ok(user) => json_ok(
256
            &SessionResponse {
257
                user_id: Some(user.user_id.clone()),
258
                soft_logout: None,
259
            },
260
            None,
261
        ),
262
        Err(RestoreError::NotFound) => json_ok(
263
            &SessionResponse {
264
                user_id: None,
265
                soft_logout: None,
266
            },
267
            None,
268
        ),
269
        Err(RestoreError::SoftLoggedOut {
270
            user_id,
271
            homeserver,
272
        }) => json_ok(
273
            &SessionResponse {
274
                user_id: None,
275
                soft_logout: Some(SoftLogoutInfo {
276
                    user_id,
277
                    homeserver,
278
                }),
279
            },
280
            None,
281
        ),
282
        Err(RestoreError::Stale(message)) => {
283
            state.remove_session(&sid, Retain::Nothing).await;
284
            ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()
285
        }
286
        Err(RestoreError::Unavailable(message)) => {
287
            ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()
288
        }
289
    }
290
}
291
292
async fn post_login(
293
    State(state): State<AppState>,
294
    headers: HeaderMap,
295
    body: axum::Json<LoginInput>,
296
) -> Response {
297
    let username = body.0.username.trim().to_owned();
298
    let password = body.0.password.clone();
299
    let login = match body.0.into_login(state.allow_http_homeserver()) {
300
        Ok(login) => login,
301
        Err(error) => return ApiError::from(error).into_response(),
302
    };
303
    let cookie_sid = session_sid(&headers);
304
    match state
305
        .login(cookie_sid.as_deref(), login, &username, password)
306
        .await
307
    {
308
        Ok(user) => json_ok(
309
            &serde_json::json!({ "user_id": user.user_id }),
310
            Some(set_cookie_value(&user.sid, state.cookie_secure())),
311
        ),
312
        Err(error) => ApiError::from(error).into_response(),
313
    }
314
}
315
316
async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {
317
    if let Some(sid) = session_sid(&headers) {
318
        state.logout(&sid).await;
319
    }
320
    let mut response = no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())));
321
    response.headers_mut().insert(
322
        axum::http::header::HeaderName::from_static("clear-site-data"),
323
        axum::http::HeaderValue::from_static("\"cache\""),
324
    );
325
    response
326
}
327
328
async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {
329
    let (user, _) = match auth(&state, &headers).await {
330
        Ok(pair) => pair,
331
        Err(error) => return error.into_response(),
332
    };
333
    respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)
334
}
335
336
async fn get_members(
337
    State(state): State<AppState>,
338
    headers: HeaderMap,
339
    Query(query): Query<MembersQuery>,
340
) -> Response {
341
    let (user, _) = match auth(&state, &headers).await {
342
        Ok(pair) => pair,
343
        Err(error) => return error.into_response(),
344
    };
345
    respond(
346
        dispatch(
347
            &user.handle,
348
            &user.mirror,
349
            Request::Members {
350
                room_id: query.room_id,
351
            },
352
        )
353
        .await,
354
    )
355
}
356
357
async fn rooms_open(
358
    State(state): State<AppState>,
359
    headers: HeaderMap,
360
    body: axum::Json<RoomIdBody>,
361
) -> Response {
362
    let (user, _) = match auth(&state, &headers).await {
363
        Ok(pair) => pair,
364
        Err(error) => return error.into_response(),
365
    };
366
    respond(
367
        dispatch(
368
            &user.handle,
369
            &user.mirror,
370
            Request::Open {
371
                room_id: body.0.room_id,
372
            },
373
        )
374
        .await,
375
    )
376
}
377
378
async fn rooms_close(
379
    State(state): State<AppState>,
380
    headers: HeaderMap,
381
    body: axum::Json<RoomIdBody>,
382
) -> Response {
383
    let (user, _) = match auth(&state, &headers).await {
384
        Ok(pair) => pair,
385
        Err(error) => return error.into_response(),
386
    };
387
    respond(
388
        dispatch(
389
            &user.handle,
390
            &user.mirror,
391
            Request::Close {
392
                room_id: body.0.room_id,
393
            },
394
        )
395
        .await,
396
    )
397
}
398
399
async fn rooms_send(
400
    State(state): State<AppState>,
401
    headers: HeaderMap,
402
    body: axum::Json<SendBody>,
403
) -> Response {
404
    let (user, _) = match auth(&state, &headers).await {
405
        Ok(pair) => pair,
406
        Err(error) => return error.into_response(),
407
    };
408
    respond(
409
        dispatch(
410
            &user.handle,
411
            &user.mirror,
412
            Request::Send {
413
                room_id: body.0.room_id,
414
                body: body.0.body,
415
                formatted_body: body.0.formatted_body,
416
                reply_to: body.0.reply_to,
417
                mentions: body.0.mentions,
418
            },
419
        )
420
        .await,
421
    )
422
}
423
424
async fn rooms_upload(
425
    State(state): State<AppState>,
426
    headers: HeaderMap,
427
    mut upload: Multipart,
428
) -> Response {
429
    let (user, _) = match auth(&state, &headers).await {
430
        Ok(pair) => pair,
431
        Err(error) => return error.into_response(),
432
    };
433
    let mut room_id = None;
434
    let mut caption = None;
435
    let mut formatted_caption = None;
436
    let mut reply_to = None;
437
    let mut mentions = Vec::new();
438
    let mut file = None;
439
    loop {
440
        let field = match upload.next_field().await {
441
            Ok(Some(field)) => field,
442
            Ok(None) => break,
443
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
444
        };
445
        match field.name() {
446
            Some("room_id") if room_id.is_none() => {
447
                room_id = match field.text().await {
448
                    Ok(value) => Some(value),
449
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
450
                };
451
            }
452
            Some("caption") if caption.is_none() => {
453
                caption = match field.text().await {
454
                    Ok(value) => Some(value),
455
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
456
                };
457
            }
458
            Some("formatted_caption") if formatted_caption.is_none() => {
459
                formatted_caption = match field.text().await {
460
                    Ok(value) => Some(value),
461
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
462
                };
463
            }
464
            Some("reply_to") if reply_to.is_none() => {
465
                reply_to = match field.text().await {
466
                    Ok(value) => Some(value),
467
                    Err(_) => return ApiError::bad_request("invalid reply_to").into_response(),
468
                };
469
            }
470
            Some("mentions") => match field.text().await {
471
                Ok(value) => mentions.push(value),
472
                Err(_) => return ApiError::bad_request("invalid mention").into_response(),
473
            },
474
            Some("file") if file.is_none() => {
475
                let filename = field.file_name().unwrap_or("attachment").to_owned();
476
                let mimetype = field
477
                    .content_type()
478
                    .unwrap_or("application/octet-stream")
479
                    .to_owned();
480
                let bytes = match field.bytes().await {
481
                    Ok(bytes) => bytes,
482
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
483
                };
484
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
485
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
486
                        .into_response();
487
                }
488
                file = Some((filename, mimetype, bytes));
489
            }
490
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
491
        }
492
    }
493
    let Some(room_id) = room_id else {
494
        return ApiError::bad_request("room_id is required").into_response();
495
    };
496
    let Some((filename, mimetype, bytes)) = file else {
497
        return ApiError::bad_request("file is required").into_response();
498
    };
499
    respond(
500
        dispatch(
501
            &user.handle,
502
            &user.mirror,
503
            Request::Upload {
504
                room_id,
505
                filename,
506
                mimetype,
507
                caption,
508
                formatted_caption,
509
                reply_to,
510
                mentions,
511
                bytes: bytes.to_vec(),
512
            },
513
        )
514
        .await,
515
    )
516
}
517
518
async fn rooms_paginate(
519
    State(state): State<AppState>,
520
    headers: HeaderMap,
521
    body: axum::Json<PaginateBody>,
522
) -> Response {
523
    let (user, _) = match auth(&state, &headers).await {
524
        Ok(pair) => pair,
525
        Err(error) => return error.into_response(),
526
    };
527
    respond(
528
        dispatch(
529
            &user.handle,
530
            &user.mirror,
531
            Request::Paginate {
532
                room_id: body.0.room_id,
533
            },
534
        )
535
        .await,
536
    )
537
}
538
539
async fn rooms_join(
540
    State(state): State<AppState>,
541
    headers: HeaderMap,
542
    body: axum::Json<JoinBody>,
543
) -> Response {
544
    let (user, _) = match auth(&state, &headers).await {
545
        Ok(pair) => pair,
546
        Err(error) => return error.into_response(),
547
    };
548
    respond(
549
        dispatch(
550
            &user.handle,
551
            &user.mirror,
552
            Request::Join { room: body.0.room },
553
        )
554
        .await,
555
    )
556
}
557
558
async fn rooms_create(
559
    State(state): State<AppState>,
560
    headers: HeaderMap,
561
    body: axum::Json<CreateBody>,
562
) -> Response {
563
    let (user, _) = match auth(&state, &headers).await {
564
        Ok(pair) => pair,
565
        Err(error) => return error.into_response(),
566
    };
567
    respond(
568
        dispatch(
569
            &user.handle,
570
            &user.mirror,
571
            Request::Create {
572
                name: body.0.name,
573
                topic: body.0.topic,
574
            },
575
        )
576
        .await,
577
    )
578
}
579
580
async fn rooms_leave(
581
    State(state): State<AppState>,
582
    headers: HeaderMap,
583
    body: axum::Json<RoomIdBody>,
584
) -> Response {
585
    let (user, _) = match auth(&state, &headers).await {
586
        Ok(pair) => pair,
587
        Err(error) => return error.into_response(),
588
    };
589
    respond(
590
        dispatch(
591
            &user.handle,
592
            &user.mirror,
593
            Request::Leave {
594
                room_id: body.0.room_id,
595
            },
596
        )
597
        .await,
598
    )
599
}
600
601
async fn rooms_edit(
602
    State(state): State<AppState>,
603
    headers: HeaderMap,
604
    body: axum::Json<EditBody>,
605
) -> Response {
606
    let (user, _) = match auth(&state, &headers).await {
607
        Ok(pair) => pair,
608
        Err(error) => return error.into_response(),
609
    };
610
    respond(
611
        dispatch(
612
            &user.handle,
613
            &user.mirror,
614
            Request::Edit {
615
                room_id: body.0.room_id,
616
                event_id: body.0.event_id,
617
                body: body.0.body,
618
                formatted_body: body.0.formatted_body,
619
                mentions: body.0.mentions,
620
            },
621
        )
622
        .await,
623
    )
624
}
625
626
async fn rooms_react(
627
    State(state): State<AppState>,
628
    headers: HeaderMap,
629
    body: axum::Json<ReactBody>,
630
) -> Response {
631
    let (user, _) = match auth(&state, &headers).await {
632
        Ok(pair) => pair,
633
        Err(error) => return error.into_response(),
634
    };
635
    respond(
636
        dispatch(
637
            &user.handle,
638
            &user.mirror,
639
            Request::React {
640
                room_id: body.0.room_id,
641
                event_id: body.0.event_id,
642
                key: body.0.key,
643
                shortcode: body.0.shortcode,
644
            },
645
        )
646
        .await,
647
    )
648
}
649
650
async fn rooms_read(
651
    State(state): State<AppState>,
652
    headers: HeaderMap,
653
    body: axum::Json<ReadBody>,
654
) -> Response {
655
    let (user, _) = match auth(&state, &headers).await {
656
        Ok(pair) => pair,
657
        Err(error) => return error.into_response(),
658
    };
659
    respond(
660
        dispatch(
661
            &user.handle,
662
            &user.mirror,
663
            Request::Read {
664
                room_id: body.0.room_id,
665
                event_id: body.0.event_id,
666
            },
667
        )
668
        .await,
669
    )
670
}
671
672
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
673
    let (user, _) = match auth(&state, &headers).await {
674
        Ok(pair) => pair,
675
        Err(error) => return error.into_response(),
676
    };
677
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
678
}
679
680
async fn get_space_children(
681
    State(state): State<AppState>,
682
    headers: HeaderMap,
683
    Query(query): Query<MembersQuery>,
684
) -> Response {
685
    let (user, _) = match auth(&state, &headers).await {
686
        Ok(pair) => pair,
687
        Err(error) => return error.into_response(),
688
    };
689
    respond(
690
        dispatch(
691
            &user.handle,
692
            &user.mirror,
693
            Request::SpaceChildren {
694
                room_id: query.room_id,
695
            },
696
        )
697
        .await,
698
    )
699
}
700
701
async fn get_media(
702
    State(state): State<AppState>,
703
    headers: HeaderMap,
704
    Query(query): Query<MediaQuery>,
705
) -> Response {
706
    let (user, _) = match auth(&state, &headers).await {
707
        Ok(pair) => pair,
708
        Err(error) => return error.into_response(),
709
    };
710
    respond(
711
        dispatch(
712
            &user.handle,
713
            &user.mirror,
714
            Request::Avatar { mxc: query.mxc },
715
        )
716
        .await,
717
    )
718
}
719
720
async fn get_message_media(
721
    State(state): State<AppState>,
722
    headers: HeaderMap,
723
    Query(query): Query<MessageMediaQuery>,
724
) -> Response {
725
    let (user, _) = match auth(&state, &headers).await {
726
        Ok(pair) => pair,
727
        Err(error) => return error.into_response(),
728
    };
729
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
730
    respond(
731
        dispatch(
732
            &user.handle,
733
            &user.mirror,
734
            Request::Media {
735
                media_id: query.media_id,
736
                size: query.size,
737
            },
738
        )
739
        .await,
740
    )
741
}
742
743
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
744
    let (user, _) = match auth(&state, &headers).await {
745
        Ok(pair) => pair,
746
        Err(error) => return error.into_response(),
747
    };
748
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
749
}
750
751
async fn recover_encryption(
752
    State(state): State<AppState>,
753
    headers: HeaderMap,
754
    body: axum::Json<RecoverBody>,
755
) -> Response {
756
    let (user, _) = match auth(&state, &headers).await {
757
        Ok(pair) => pair,
758
        Err(error) => return error.into_response(),
759
    };
760
    respond(
761
        dispatch(
762
            &user.handle,
763
            &user.mirror,
764
            Request::Recover {
765
                recovery_key: body.0.recovery_key,
766
            },
767
        )
768
        .await,
769
    )
770
}
771
772
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
773
    let (user, _) = match auth(&state, &headers).await {
774
        Ok(pair) => pair,
775
        Err(error) => return error.into_response(),
776
    };
777
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
778
}
779
780
async fn verify_encryption(
781
    State(state): State<AppState>,
782
    headers: HeaderMap,
783
    body: axum::Json<VerifyBody>,
784
) -> Response {
785
    let (user, _) = match auth(&state, &headers).await {
786
        Ok(pair) => pair,
787
        Err(error) => return error.into_response(),
788
    };
789
    respond(
790
        dispatch(
791
            &user.handle,
792
            &user.mirror,
793
            Request::Verify {
794
                action: body.0.action,
795
            },
796
        )
797
        .await,
798
    )
799
}
800
801
async fn rooms_typing(
802
    State(state): State<AppState>,
803
    headers: HeaderMap,
804
    body: axum::Json<TypingBody>,
805
) -> Response {
806
    let (user, _) = match auth(&state, &headers).await {
807
        Ok(pair) => pair,
808
        Err(error) => return error.into_response(),
809
    };
810
    respond(
811
        dispatch(
812
            &user.handle,
813
            &user.mirror,
814
            Request::Typing {
815
                room_id: body.0.room_id,
816
                typing: body.0.typing,
817
            },
818
        )
819
        .await,
820
    )
821
}
822
823
async fn get_account_data(
824
    State(state): State<AppState>,
825
    headers: HeaderMap,
826
    Query(query): Query<AccountDataQuery>,
827
) -> Response {
828
    let (user, _) = match auth(&state, &headers).await {
829
        Ok(pair) => pair,
830
        Err(error) => return error.into_response(),
831
    };
832
    respond(
833
        dispatch(
834
            &user.handle,
835
            &user.mirror,
836
            Request::GetAccountData {
837
                event_type: query.event_type,
838
            },
839
        )
840
        .await,
841
    )
842
}
843
844
async fn get_url_preview(
845
    State(state): State<AppState>,
846
    headers: HeaderMap,
847
    Query(query): Query<UrlPreviewQuery>,
848
) -> Response {
849
    let (user, _) = match auth(&state, &headers).await {
850
        Ok(pair) => pair,
851
        Err(error) => return error.into_response(),
852
    };
853
    respond(
854
        dispatch(
855
            &user.handle,
856
            &user.mirror,
857
            Request::UrlPreview {
858
                room_id: query.room_id,
859
                url: query.url,
860
            },
861
        )
862
        .await,
863
    )
864
}
865
866
async fn set_account_data(
867
    State(state): State<AppState>,
868
    headers: HeaderMap,
869
    body: axum::Json<AccountDataBody>,
870
) -> Response {
871
    let (user, _) = match auth(&state, &headers).await {
872
        Ok(pair) => pair,
873
        Err(error) => return error.into_response(),
874
    };
875
    let body = body.0;
876
    respond(
877
        dispatch(
878
            &user.handle,
879
            &user.mirror,
880
            Request::SetAccountData {
881
                event_type: body.event_type,
882
                content: body.content,
883
            },
884
        )
885
        .await,
886
    )
887
}
888
889
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
890
    let (user, _) = match auth(&state, &headers).await {
891
        Ok(pair) => pair,
892
        Err(error) => return error.into_response(),
893
    };
894
    let body = subscribe(&user.mirror).map(|event| {
895
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
896
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
897
    });
898
    Sse::new(body).into_response()
899
}
900
901
async fn get_health() -> Response {
902
    json_ok(&serde_json::json!({ "status": "ok" }), None)
903
}
904
905
async fn static_shell() -> Response {
906
    (
907
        StatusCode::SERVICE_UNAVAILABLE,
908
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
909
    )
910
        .into_response()
911
}
912
913
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
914
    let api = Router::new()
915
        .route("/session", get(get_session))
916
        .route("/login", post(post_login))
917
        .route("/logout", post(post_logout))
918
        .route("/rooms", get(get_rooms))
919
        .route("/events", get(get_events))
920
        .route("/health", get(get_health))
921
        .route("/rooms/open", post(rooms_open))
922
        .route("/rooms/close", post(rooms_close))
923
        .route("/rooms/send", post(rooms_send))
924
        .route("/rooms/paginate", post(rooms_paginate))
925
        .route(
926
            "/rooms/upload",
927
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
928
        )
929
        .route("/rooms/join", post(rooms_join))
930
        .route("/rooms/create", post(rooms_create))
931
        .route("/rooms/leave", post(rooms_leave))
932
        .route("/rooms/react", post(rooms_react))
933
        .route("/rooms/edit", post(rooms_edit))
934
        .route("/rooms/read", post(rooms_read))
935
        .route("/rooms/typing", post(rooms_typing))
936
        .route(
937
            "/account-data",
938
            get(get_account_data).post(set_account_data),
939
        )
940
        .route("/url-preview", get(get_url_preview))
941
        .route("/profile", get(get_profile))
942
        .route("/media", get(get_media))
943
        .route("/message-media", get(get_message_media))
944
        .route("/spaces/children", get(get_space_children))
945
        .route("/rooms/members", get(get_members))
946
        .route("/encryption", get(get_encryption))
947
        .route("/encryption/recover", post(recover_encryption))
948
        .route("/encryption/start", post(start_verification))
949
        .route("/encryption/verify", post(verify_encryption));
950
    let api = Router::new()
951
        .nest("/api", api)
952
        .layer(middleware::from_fn(csrf_guard));
953
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
954
    match web_dir {
955
        Some(dir) => {
956
            let files =
957
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
958
            router.fallback_service(files)
959
        }
960
        None => router.fallback(static_shell),
961
    }
962
}