atagen

malice

public
Star0
Watch0
668ab5c3053101aa5989c8060026884b34f53e57
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/routes.rsRust1200 lines32.8 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, SendKind, 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
    #[serde(default)]
49
    kind: SendKind,
50
}
51
52
#[derive(Debug, Deserialize)]
53
struct PaginateBody {
54
    #[serde(default)]
55
    room_id: String,
56
}
57
58
#[derive(Debug, Deserialize)]
59
struct EventContextBody {
60
    #[serde(default)]
61
    room_id: String,
62
    #[serde(default)]
63
    event_id: String,
64
    #[serde(default)]
65
    before_limit: u32,
66
    #[serde(default)]
67
    after_limit: u32,
68
}
69
70
#[derive(Debug, Deserialize)]
71
struct EventContextPageBody {
72
    #[serde(default)]
73
    room_id: String,
74
    #[serde(default)]
75
    token: String,
76
    #[serde(default)]
77
    limit: u32,
78
}
79
80
#[derive(Debug, Deserialize)]
81
struct JoinBody {
82
    #[serde(default)]
83
    room: String,
84
}
85
86
#[derive(Debug, Deserialize)]
87
struct CreateBody {
88
    #[serde(default)]
89
    name: String,
90
    #[serde(default)]
91
    topic: Option<String>,
92
}
93
94
#[derive(Debug, Deserialize)]
95
struct ReactBody {
96
    #[serde(default)]
97
    room_id: String,
98
    #[serde(default)]
99
    event_id: String,
100
    #[serde(default)]
101
    key: String,
102
    #[serde(default)]
103
    shortcode: Option<String>,
104
}
105
106
#[derive(Debug, Deserialize)]
107
struct EditBody {
108
    #[serde(default)]
109
    room_id: String,
110
    #[serde(default)]
111
    event_id: String,
112
    #[serde(default)]
113
    body: String,
114
    #[serde(default)]
115
    formatted_body: Option<String>,
116
    #[serde(default)]
117
    mentions: Vec<String>,
118
}
119
120
#[derive(Debug, Deserialize)]
121
struct ReadBody {
122
    #[serde(default)]
123
    room_id: String,
124
    #[serde(default)]
125
    event_id: String,
126
}
127
128
#[derive(Debug, Deserialize)]
129
struct MembersQuery {
130
    #[serde(default)]
131
    room_id: String,
132
}
133
#[derive(Debug, Deserialize)]
134
struct RecoverBody {
135
    recovery_key: String,
136
}
137
138
#[derive(Debug, Deserialize)]
139
struct VerifyBody {
140
    action: String,
141
}
142
143
#[derive(Debug, Deserialize)]
144
struct MediaQuery {
145
    mxc: String,
146
}
147
#[derive(Debug, Deserialize)]
148
struct MessageMediaQuery {
149
    media_id: String,
150
    size: String,
151
}
152
153
#[derive(Debug, Deserialize)]
154
struct TypingBody {
155
    #[serde(default)]
156
    room_id: String,
157
    #[serde(default)]
158
    typing: bool,
159
}
160
161
#[derive(Debug, Deserialize)]
162
struct AccountDataQuery {
163
    event_type: String,
164
}
165
166
#[derive(Debug, Deserialize)]
167
struct UrlPreviewQuery {
168
    #[serde(default)]
169
    room_id: String,
170
    #[serde(default)]
171
    url: String,
172
}
173
174
#[derive(Debug, Deserialize)]
175
struct AccountDataBody {
176
    event_type: String,
177
    content: serde_json::Value,
178
}
179
180
#[derive(Debug, Serialize)]
181
struct SoftLogoutInfo {
182
    user_id: String,
183
    homeserver: String,
184
}
185
186
#[derive(Debug, Serialize)]
187
struct SessionResponse {
188
    user_id: Option<String>,
189
    #[serde(skip_serializing_if = "Option::is_none")]
190
    soft_logout: Option<SoftLogoutInfo>,
191
}
192
193
async fn auth(
194
    state: &AppState,
195
    headers: &HeaderMap,
196
) -> Result<(Arc<UserSession>, String), ApiError> {
197
    let Some(sid) = session_sid(headers) else {
198
        return Err(ApiError::unauthorized("not signed in"));
199
    };
200
    match state.get_or_restore(&sid).await {
201
        Ok(user) => Ok((user, sid)),
202
        Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(
203
            "session expired; sign in again",
204
            state.cookie_secure(),
205
        )),
206
        Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(
207
            "signed out by the server, sign in again",
208
        )),
209
        Err(RestoreError::Stale(message)) => {
210
            state.remove_session(&sid, Retain::Nothing).await;
211
            Err(ApiError::unauthorized_stale(message, state.cookie_secure()))
212
        }
213
        Err(RestoreError::Unavailable(message)) => {
214
            Err(ApiError::new(StatusCode::BAD_GATEWAY, message))
215
        }
216
    }
217
}
218
219
/// commands answer when they're done, so the browser gets the outcome in the response
220
fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {
221
    match result {
222
        Ok(Reply::Done) => no_content(None),
223
        Ok(Reply::Json(value)) => json_ok(&value, None),
224
        Ok(Reply::Avatar {
225
            bytes,
226
            content_type,
227
        }) => (
228
            [
229
                (axum::http::header::CONTENT_TYPE, content_type),
230
                // mxc uris name immutable content, a changed avatar gets a new uri
231
                (
232
                    axum::http::header::CACHE_CONTROL,
233
                    "private, max-age=31536000, immutable",
234
                ),
235
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
236
            ],
237
            bytes,
238
        )
239
            .into_response(),
240
        Ok(Reply::Attachment {
241
            bytes,
242
            content_type,
243
        }) => (
244
            [
245
                (
246
                    axum::http::header::CONTENT_TYPE,
247
                    content_type.unwrap_or("application/octet-stream"),
248
                ),
249
                (
250
                    axum::http::header::CONTENT_DISPOSITION,
251
                    if content_type.is_some() {
252
                        "inline"
253
                    } else {
254
                        "attachment"
255
                    },
256
                ),
257
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
258
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
259
                (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),
260
            ],
261
            bytes,
262
        )
263
            .into_response(),
264
        Err(error) => ApiError::from(error).into_response(),
265
    }
266
}
267
268
async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {
269
    let Some(sid) = session_sid(&headers) else {
270
        return json_ok(
271
            &SessionResponse {
272
                user_id: None,
273
                soft_logout: None,
274
            },
275
            None,
276
        );
277
    };
278
    match state.get_or_restore(&sid).await {
279
        Ok(user) => json_ok(
280
            &SessionResponse {
281
                user_id: Some(user.user_id.clone()),
282
                soft_logout: None,
283
            },
284
            None,
285
        ),
286
        Err(RestoreError::NotFound) => json_ok(
287
            &SessionResponse {
288
                user_id: None,
289
                soft_logout: None,
290
            },
291
            None,
292
        ),
293
        Err(RestoreError::SoftLoggedOut {
294
            user_id,
295
            homeserver,
296
        }) => json_ok(
297
            &SessionResponse {
298
                user_id: None,
299
                soft_logout: Some(SoftLogoutInfo {
300
                    user_id,
301
                    homeserver,
302
                }),
303
            },
304
            None,
305
        ),
306
        Err(RestoreError::Stale(message)) => {
307
            state.remove_session(&sid, Retain::Nothing).await;
308
            ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()
309
        }
310
        Err(RestoreError::Unavailable(message)) => {
311
            ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()
312
        }
313
    }
314
}
315
316
async fn post_login(
317
    State(state): State<AppState>,
318
    headers: HeaderMap,
319
    body: axum::Json<LoginInput>,
320
) -> Response {
321
    let username = body.0.username.trim().to_owned();
322
    let password = body.0.password.clone();
323
    let login = match body.0.into_login(state.allow_http_homeserver()) {
324
        Ok(login) => login,
325
        Err(error) => return ApiError::from(error).into_response(),
326
    };
327
    let cookie_sid = session_sid(&headers);
328
    match state
329
        .login(cookie_sid.as_deref(), login, &username, password)
330
        .await
331
    {
332
        Ok(user) => json_ok(
333
            &serde_json::json!({ "user_id": user.user_id }),
334
            Some(set_cookie_value(&user.sid, state.cookie_secure())),
335
        ),
336
        Err(error) => ApiError::from(error).into_response(),
337
    }
338
}
339
340
async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {
341
    if let Some(sid) = session_sid(&headers) {
342
        state.logout(&sid).await;
343
    }
344
    let mut response = no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())));
345
    response.headers_mut().insert(
346
        axum::http::header::HeaderName::from_static("clear-site-data"),
347
        axum::http::HeaderValue::from_static("\"cache\""),
348
    );
349
    response
350
}
351
352
async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {
353
    let (user, _) = match auth(&state, &headers).await {
354
        Ok(pair) => pair,
355
        Err(error) => return error.into_response(),
356
    };
357
    respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)
358
}
359
360
async fn get_members(
361
    State(state): State<AppState>,
362
    headers: HeaderMap,
363
    Query(query): Query<MembersQuery>,
364
) -> Response {
365
    let (user, _) = match auth(&state, &headers).await {
366
        Ok(pair) => pair,
367
        Err(error) => return error.into_response(),
368
    };
369
    respond(
370
        dispatch(
371
            &user.handle,
372
            &user.mirror,
373
            Request::Members {
374
                room_id: query.room_id,
375
            },
376
        )
377
        .await,
378
    )
379
}
380
381
async fn rooms_open(
382
    State(state): State<AppState>,
383
    headers: HeaderMap,
384
    body: axum::Json<RoomIdBody>,
385
) -> Response {
386
    let (user, _) = match auth(&state, &headers).await {
387
        Ok(pair) => pair,
388
        Err(error) => return error.into_response(),
389
    };
390
    respond(
391
        dispatch(
392
            &user.handle,
393
            &user.mirror,
394
            Request::Open {
395
                room_id: body.0.room_id,
396
            },
397
        )
398
        .await,
399
    )
400
}
401
402
async fn rooms_close(
403
    State(state): State<AppState>,
404
    headers: HeaderMap,
405
    body: axum::Json<RoomIdBody>,
406
) -> Response {
407
    let (user, _) = match auth(&state, &headers).await {
408
        Ok(pair) => pair,
409
        Err(error) => return error.into_response(),
410
    };
411
    respond(
412
        dispatch(
413
            &user.handle,
414
            &user.mirror,
415
            Request::Close {
416
                room_id: body.0.room_id,
417
            },
418
        )
419
        .await,
420
    )
421
}
422
423
async fn rooms_send(
424
    State(state): State<AppState>,
425
    headers: HeaderMap,
426
    body: axum::Json<SendBody>,
427
) -> Response {
428
    let (user, _) = match auth(&state, &headers).await {
429
        Ok(pair) => pair,
430
        Err(error) => return error.into_response(),
431
    };
432
    respond(
433
        dispatch(
434
            &user.handle,
435
            &user.mirror,
436
            Request::Send {
437
                room_id: body.0.room_id,
438
                body: body.0.body,
439
                formatted_body: body.0.formatted_body,
440
                reply_to: body.0.reply_to,
441
                mentions: body.0.mentions,
442
                kind: body.0.kind,
443
            },
444
        )
445
        .await,
446
    )
447
}
448
449
async fn rooms_upload(
450
    State(state): State<AppState>,
451
    headers: HeaderMap,
452
    mut upload: Multipart,
453
) -> Response {
454
    let (user, _) = match auth(&state, &headers).await {
455
        Ok(pair) => pair,
456
        Err(error) => return error.into_response(),
457
    };
458
    let mut room_id = None;
459
    let mut caption = None;
460
    let mut formatted_caption = None;
461
    let mut reply_to = None;
462
    let mut mentions = Vec::new();
463
    let mut file = None;
464
    loop {
465
        let field = match upload.next_field().await {
466
            Ok(Some(field)) => field,
467
            Ok(None) => break,
468
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
469
        };
470
        match field.name() {
471
            Some("room_id") if room_id.is_none() => {
472
                room_id = match field.text().await {
473
                    Ok(value) => Some(value),
474
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
475
                };
476
            }
477
            Some("caption") if caption.is_none() => {
478
                caption = match field.text().await {
479
                    Ok(value) => Some(value),
480
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
481
                };
482
            }
483
            Some("formatted_caption") if formatted_caption.is_none() => {
484
                formatted_caption = match field.text().await {
485
                    Ok(value) => Some(value),
486
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
487
                };
488
            }
489
            Some("reply_to") if reply_to.is_none() => {
490
                reply_to = match field.text().await {
491
                    Ok(value) => Some(value),
492
                    Err(_) => return ApiError::bad_request("invalid reply_to").into_response(),
493
                };
494
            }
495
            Some("mentions") => match field.text().await {
496
                Ok(value) => mentions.push(value),
497
                Err(_) => return ApiError::bad_request("invalid mention").into_response(),
498
            },
499
            Some("file") if file.is_none() => {
500
                let filename = field.file_name().unwrap_or("attachment").to_owned();
501
                let mimetype = field
502
                    .content_type()
503
                    .unwrap_or("application/octet-stream")
504
                    .to_owned();
505
                let bytes = match field.bytes().await {
506
                    Ok(bytes) => bytes,
507
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
508
                };
509
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
510
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
511
                        .into_response();
512
                }
513
                file = Some((filename, mimetype, bytes));
514
            }
515
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
516
        }
517
    }
518
    let Some(room_id) = room_id else {
519
        return ApiError::bad_request("room_id is required").into_response();
520
    };
521
    let Some((filename, mimetype, bytes)) = file else {
522
        return ApiError::bad_request("file is required").into_response();
523
    };
524
    respond(
525
        dispatch(
526
            &user.handle,
527
            &user.mirror,
528
            Request::Upload {
529
                room_id,
530
                filename,
531
                mimetype,
532
                caption,
533
                formatted_caption,
534
                reply_to,
535
                mentions,
536
                bytes: bytes.to_vec(),
537
            },
538
        )
539
        .await,
540
    )
541
}
542
543
/// Ops added after the per-route handlers, taken as the core request JSON. Anything
544
/// carrying bytes stays on its own route.
545
async fn post_command(
546
    State(state): State<AppState>,
547
    headers: HeaderMap,
548
    body: axum::Json<Request>,
549
) -> Response {
550
    let (user, _) = match auth(&state, &headers).await {
551
        Ok(pair) => pair,
552
        Err(error) => return error.into_response(),
553
    };
554
    let request = body.0;
555
    if !matches!(
556
        request,
557
        Request::MentionsPage { .. }
558
            | Request::MentionContext { .. }
559
            | Request::RetryMentionContext { .. }
560
            | Request::SetMentionsRead { .. }
561
            | Request::MarkMentionsReadThrough { .. }
562
            | Request::RecoverMentions
563
            | Request::Kick { .. }
564
            | Request::Ban { .. }
565
            | Request::Unban { .. }
566
            | Request::SetPowerLevel { .. }
567
            | Request::SetDisplayName { .. }
568
            | Request::SetAvatar { .. }
569
            | Request::Invite { .. }
570
            | Request::Direct { .. }
571
            | Request::Redact { .. }
572
            | Request::SetFavourite { .. }
573
            | Request::ReorderSpaces { .. }
574
            | Request::SetRoomDisplayName { .. }
575
            | Request::SetRoomAvatar { .. }
576
            | Request::Mute { .. }
577
            | Request::Ignore { .. }
578
            | Request::Unignore { .. }
579
            | Request::PowerLevels { .. }
580
            | Request::OpenPinned { .. }
581
            | Request::ClosePinned { .. }
582
            | Request::SetPinned { .. }
583
            | Request::SendSticker { .. }
584
            | Request::CreateRoomPack { .. }
585
            | Request::DeletePack { .. }
586
            | Request::SetPackMeta { .. }
587
            | Request::PutPackImage { .. }
588
            | Request::RenamePackImage { .. }
589
            | Request::RemovePackImage { .. }
590
            | Request::SetPackEnabled { .. }
591
            | Request::ClonePackImage { .. }
592
            | Request::CloneSticker { .. }
593
            | Request::ClonePack { .. }
594
    ) {
595
        return ApiError::bad_request("op is not available as a command").into_response();
596
    }
597
    respond(dispatch(&user.handle, &user.mirror, request).await)
598
}
599
600
async fn profile_avatar(
601
    State(state): State<AppState>,
602
    headers: HeaderMap,
603
    bytes: axum::body::Bytes,
604
) -> Response {
605
    let (user, _) = match auth(&state, &headers).await {
606
        Ok(pair) => pair,
607
        Err(error) => return error.into_response(),
608
    };
609
    respond(
610
        dispatch(
611
            &user.handle,
612
            &user.mirror,
613
            Request::UploadAvatar {
614
                bytes: bytes.to_vec(),
615
            },
616
        )
617
        .await,
618
    )
619
}
620
621
#[derive(Debug, Deserialize)]
622
struct RoomAvatarQuery {
623
    room_id: String,
624
}
625
626
async fn rooms_avatar(
627
    State(state): State<AppState>,
628
    headers: HeaderMap,
629
    Query(query): Query<RoomAvatarQuery>,
630
    bytes: axum::body::Bytes,
631
) -> Response {
632
    let (user, _) = match auth(&state, &headers).await {
633
        Ok(pair) => pair,
634
        Err(error) => return error.into_response(),
635
    };
636
    respond(
637
        dispatch(
638
            &user.handle,
639
            &user.mirror,
640
            Request::UploadRoomAvatar {
641
                room_id: query.room_id,
642
                bytes: bytes.to_vec(),
643
            },
644
        )
645
        .await,
646
    )
647
}
648
649
async fn media_upload(
650
    State(state): State<AppState>,
651
    headers: HeaderMap,
652
    bytes: axum::body::Bytes,
653
) -> Response {
654
    let (user, _) = match auth(&state, &headers).await {
655
        Ok(pair) => pair,
656
        Err(error) => return error.into_response(),
657
    };
658
    respond(
659
        dispatch(
660
            &user.handle,
661
            &user.mirror,
662
            Request::UploadMedia {
663
                bytes: bytes.to_vec(),
664
            },
665
        )
666
        .await,
667
    )
668
}
669
670
async fn rooms_paginate(
671
    State(state): State<AppState>,
672
    headers: HeaderMap,
673
    body: axum::Json<PaginateBody>,
674
) -> Response {
675
    let (user, _) = match auth(&state, &headers).await {
676
        Ok(pair) => pair,
677
        Err(error) => return error.into_response(),
678
    };
679
    respond(
680
        dispatch(
681
            &user.handle,
682
            &user.mirror,
683
            Request::Paginate {
684
                room_id: body.0.room_id,
685
            },
686
        )
687
        .await,
688
    )
689
}
690
691
async fn rooms_context(
692
    State(state): State<AppState>,
693
    headers: HeaderMap,
694
    body: axum::Json<EventContextBody>,
695
) -> Response {
696
    let (user, _) = match auth(&state, &headers).await {
697
        Ok(pair) => pair,
698
        Err(error) => return error.into_response(),
699
    };
700
    respond(
701
        dispatch(
702
            &user.handle,
703
            &user.mirror,
704
            Request::EventContext {
705
                room_id: body.0.room_id,
706
                event_id: body.0.event_id,
707
                before_limit: body.0.before_limit,
708
                after_limit: body.0.after_limit,
709
            },
710
        )
711
        .await,
712
    )
713
}
714
715
async fn rooms_context_before(
716
    State(state): State<AppState>,
717
    headers: HeaderMap,
718
    body: axum::Json<EventContextPageBody>,
719
) -> Response {
720
    let (user, _) = match auth(&state, &headers).await {
721
        Ok(pair) => pair,
722
        Err(error) => return error.into_response(),
723
    };
724
    respond(
725
        dispatch(
726
            &user.handle,
727
            &user.mirror,
728
            Request::EventContextBefore {
729
                room_id: body.0.room_id,
730
                token: body.0.token,
731
                limit: body.0.limit,
732
            },
733
        )
734
        .await,
735
    )
736
}
737
738
async fn rooms_context_after(
739
    State(state): State<AppState>,
740
    headers: HeaderMap,
741
    body: axum::Json<EventContextPageBody>,
742
) -> Response {
743
    let (user, _) = match auth(&state, &headers).await {
744
        Ok(pair) => pair,
745
        Err(error) => return error.into_response(),
746
    };
747
    respond(
748
        dispatch(
749
            &user.handle,
750
            &user.mirror,
751
            Request::EventContextAfter {
752
                room_id: body.0.room_id,
753
                token: body.0.token,
754
                limit: body.0.limit,
755
            },
756
        )
757
        .await,
758
    )
759
}
760
761
async fn rooms_join(
762
    State(state): State<AppState>,
763
    headers: HeaderMap,
764
    body: axum::Json<JoinBody>,
765
) -> Response {
766
    let (user, _) = match auth(&state, &headers).await {
767
        Ok(pair) => pair,
768
        Err(error) => return error.into_response(),
769
    };
770
    respond(
771
        dispatch(
772
            &user.handle,
773
            &user.mirror,
774
            Request::Join { room: body.0.room },
775
        )
776
        .await,
777
    )
778
}
779
780
async fn rooms_create(
781
    State(state): State<AppState>,
782
    headers: HeaderMap,
783
    body: axum::Json<CreateBody>,
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::Create {
794
                name: body.0.name,
795
                topic: body.0.topic,
796
            },
797
        )
798
        .await,
799
    )
800
}
801
802
async fn rooms_leave(
803
    State(state): State<AppState>,
804
    headers: HeaderMap,
805
    body: axum::Json<RoomIdBody>,
806
) -> Response {
807
    let (user, _) = match auth(&state, &headers).await {
808
        Ok(pair) => pair,
809
        Err(error) => return error.into_response(),
810
    };
811
    respond(
812
        dispatch(
813
            &user.handle,
814
            &user.mirror,
815
            Request::Leave {
816
                room_id: body.0.room_id,
817
            },
818
        )
819
        .await,
820
    )
821
}
822
823
async fn rooms_edit(
824
    State(state): State<AppState>,
825
    headers: HeaderMap,
826
    body: axum::Json<EditBody>,
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::Edit {
837
                room_id: body.0.room_id,
838
                event_id: body.0.event_id,
839
                body: body.0.body,
840
                formatted_body: body.0.formatted_body,
841
                mentions: body.0.mentions,
842
            },
843
        )
844
        .await,
845
    )
846
}
847
848
async fn rooms_react(
849
    State(state): State<AppState>,
850
    headers: HeaderMap,
851
    body: axum::Json<ReactBody>,
852
) -> Response {
853
    let (user, _) = match auth(&state, &headers).await {
854
        Ok(pair) => pair,
855
        Err(error) => return error.into_response(),
856
    };
857
    respond(
858
        dispatch(
859
            &user.handle,
860
            &user.mirror,
861
            Request::React {
862
                room_id: body.0.room_id,
863
                event_id: body.0.event_id,
864
                key: body.0.key,
865
                shortcode: body.0.shortcode,
866
            },
867
        )
868
        .await,
869
    )
870
}
871
872
async fn rooms_read(
873
    State(state): State<AppState>,
874
    headers: HeaderMap,
875
    body: axum::Json<ReadBody>,
876
) -> Response {
877
    let (user, _) = match auth(&state, &headers).await {
878
        Ok(pair) => pair,
879
        Err(error) => return error.into_response(),
880
    };
881
    respond(
882
        dispatch(
883
            &user.handle,
884
            &user.mirror,
885
            Request::Read {
886
                room_id: body.0.room_id,
887
                event_id: body.0.event_id,
888
            },
889
        )
890
        .await,
891
    )
892
}
893
894
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
895
    let (user, _) = match auth(&state, &headers).await {
896
        Ok(pair) => pair,
897
        Err(error) => return error.into_response(),
898
    };
899
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
900
}
901
902
async fn get_space_children(
903
    State(state): State<AppState>,
904
    headers: HeaderMap,
905
    Query(query): Query<MembersQuery>,
906
) -> Response {
907
    let (user, _) = match auth(&state, &headers).await {
908
        Ok(pair) => pair,
909
        Err(error) => return error.into_response(),
910
    };
911
    respond(
912
        dispatch(
913
            &user.handle,
914
            &user.mirror,
915
            Request::SpaceChildren {
916
                room_id: query.room_id,
917
            },
918
        )
919
        .await,
920
    )
921
}
922
923
async fn get_media(
924
    State(state): State<AppState>,
925
    headers: HeaderMap,
926
    Query(query): Query<MediaQuery>,
927
) -> Response {
928
    let (user, _) = match auth(&state, &headers).await {
929
        Ok(pair) => pair,
930
        Err(error) => return error.into_response(),
931
    };
932
    respond(
933
        dispatch(
934
            &user.handle,
935
            &user.mirror,
936
            Request::Avatar { mxc: query.mxc },
937
        )
938
        .await,
939
    )
940
}
941
942
async fn get_message_media(
943
    State(state): State<AppState>,
944
    headers: HeaderMap,
945
    Query(query): Query<MessageMediaQuery>,
946
) -> Response {
947
    let (user, _) = match auth(&state, &headers).await {
948
        Ok(pair) => pair,
949
        Err(error) => return error.into_response(),
950
    };
951
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
952
    respond(
953
        dispatch(
954
            &user.handle,
955
            &user.mirror,
956
            Request::Media {
957
                media_id: query.media_id,
958
                size: query.size,
959
            },
960
        )
961
        .await,
962
    )
963
}
964
965
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
966
    let (user, _) = match auth(&state, &headers).await {
967
        Ok(pair) => pair,
968
        Err(error) => return error.into_response(),
969
    };
970
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
971
}
972
973
async fn recover_encryption(
974
    State(state): State<AppState>,
975
    headers: HeaderMap,
976
    body: axum::Json<RecoverBody>,
977
) -> Response {
978
    let (user, _) = match auth(&state, &headers).await {
979
        Ok(pair) => pair,
980
        Err(error) => return error.into_response(),
981
    };
982
    respond(
983
        dispatch(
984
            &user.handle,
985
            &user.mirror,
986
            Request::Recover {
987
                recovery_key: body.0.recovery_key,
988
            },
989
        )
990
        .await,
991
    )
992
}
993
994
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
995
    let (user, _) = match auth(&state, &headers).await {
996
        Ok(pair) => pair,
997
        Err(error) => return error.into_response(),
998
    };
999
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
1000
}
1001
1002
async fn verify_encryption(
1003
    State(state): State<AppState>,
1004
    headers: HeaderMap,
1005
    body: axum::Json<VerifyBody>,
1006
) -> Response {
1007
    let (user, _) = match auth(&state, &headers).await {
1008
        Ok(pair) => pair,
1009
        Err(error) => return error.into_response(),
1010
    };
1011
    respond(
1012
        dispatch(
1013
            &user.handle,
1014
            &user.mirror,
1015
            Request::Verify {
1016
                action: body.0.action,
1017
            },
1018
        )
1019
        .await,
1020
    )
1021
}
1022
1023
async fn rooms_typing(
1024
    State(state): State<AppState>,
1025
    headers: HeaderMap,
1026
    body: axum::Json<TypingBody>,
1027
) -> Response {
1028
    let (user, _) = match auth(&state, &headers).await {
1029
        Ok(pair) => pair,
1030
        Err(error) => return error.into_response(),
1031
    };
1032
    respond(
1033
        dispatch(
1034
            &user.handle,
1035
            &user.mirror,
1036
            Request::Typing {
1037
                room_id: body.0.room_id,
1038
                typing: body.0.typing,
1039
            },
1040
        )
1041
        .await,
1042
    )
1043
}
1044
1045
async fn get_account_data(
1046
    State(state): State<AppState>,
1047
    headers: HeaderMap,
1048
    Query(query): Query<AccountDataQuery>,
1049
) -> Response {
1050
    let (user, _) = match auth(&state, &headers).await {
1051
        Ok(pair) => pair,
1052
        Err(error) => return error.into_response(),
1053
    };
1054
    respond(
1055
        dispatch(
1056
            &user.handle,
1057
            &user.mirror,
1058
            Request::GetAccountData {
1059
                event_type: query.event_type,
1060
            },
1061
        )
1062
        .await,
1063
    )
1064
}
1065
1066
async fn get_url_preview(
1067
    State(state): State<AppState>,
1068
    headers: HeaderMap,
1069
    Query(query): Query<UrlPreviewQuery>,
1070
) -> Response {
1071
    let (user, _) = match auth(&state, &headers).await {
1072
        Ok(pair) => pair,
1073
        Err(error) => return error.into_response(),
1074
    };
1075
    respond(
1076
        dispatch(
1077
            &user.handle,
1078
            &user.mirror,
1079
            Request::UrlPreview {
1080
                room_id: query.room_id,
1081
                url: query.url,
1082
            },
1083
        )
1084
        .await,
1085
    )
1086
}
1087
1088
async fn set_account_data(
1089
    State(state): State<AppState>,
1090
    headers: HeaderMap,
1091
    body: axum::Json<AccountDataBody>,
1092
) -> Response {
1093
    let (user, _) = match auth(&state, &headers).await {
1094
        Ok(pair) => pair,
1095
        Err(error) => return error.into_response(),
1096
    };
1097
    let body = body.0;
1098
    respond(
1099
        dispatch(
1100
            &user.handle,
1101
            &user.mirror,
1102
            Request::SetAccountData {
1103
                event_type: body.event_type,
1104
                content: body.content,
1105
            },
1106
        )
1107
        .await,
1108
    )
1109
}
1110
1111
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
1112
    let (user, _) = match auth(&state, &headers).await {
1113
        Ok(pair) => pair,
1114
        Err(error) => return error.into_response(),
1115
    };
1116
    let body = subscribe(&user.mirror).map(|event| {
1117
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
1118
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
1119
    });
1120
    Sse::new(body).into_response()
1121
}
1122
1123
async fn get_health() -> Response {
1124
    json_ok(&serde_json::json!({ "status": "ok" }), None)
1125
}
1126
1127
async fn static_shell() -> Response {
1128
    (
1129
        StatusCode::SERVICE_UNAVAILABLE,
1130
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
1131
    )
1132
        .into_response()
1133
}
1134
1135
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
1136
    let api = Router::new()
1137
        .route("/session", get(get_session))
1138
        .route("/login", post(post_login))
1139
        .route("/logout", post(post_logout))
1140
        .route("/rooms", get(get_rooms))
1141
        .route("/events", get(get_events))
1142
        .route("/health", get(get_health))
1143
        .route("/rooms/open", post(rooms_open))
1144
        .route("/rooms/close", post(rooms_close))
1145
        .route("/rooms/send", post(rooms_send))
1146
        .route("/rooms/paginate", post(rooms_paginate))
1147
        .route("/rooms/context", post(rooms_context))
1148
        .route("/rooms/context/before", post(rooms_context_before))
1149
        .route("/rooms/context/after", post(rooms_context_after))
1150
        .route(
1151
            "/rooms/upload",
1152
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
1153
        )
1154
        .route("/rooms/join", post(rooms_join))
1155
        .route("/rooms/create", post(rooms_create))
1156
        .route("/rooms/leave", post(rooms_leave))
1157
        .route("/rooms/react", post(rooms_react))
1158
        .route("/rooms/edit", post(rooms_edit))
1159
        .route("/rooms/read", post(rooms_read))
1160
        .route("/rooms/typing", post(rooms_typing))
1161
        .route(
1162
            "/account-data",
1163
            get(get_account_data).post(set_account_data),
1164
        )
1165
        .route("/url-preview", get(get_url_preview))
1166
        .route("/profile", get(get_profile))
1167
        .route(
1168
            "/profile/avatar",
1169
            post(profile_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1170
        )
1171
        .route("/command", post(post_command))
1172
        .route(
1173
            "/rooms/avatar",
1174
            post(rooms_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1175
        )
1176
        .route(
1177
            "/media/upload",
1178
            post(media_upload).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1179
        )
1180
        .route("/media", get(get_media))
1181
        .route("/message-media", get(get_message_media))
1182
        .route("/spaces/children", get(get_space_children))
1183
        .route("/rooms/members", get(get_members))
1184
        .route("/encryption", get(get_encryption))
1185
        .route("/encryption/recover", post(recover_encryption))
1186
        .route("/encryption/start", post(start_verification))
1187
        .route("/encryption/verify", post(verify_encryption));
1188
    let api = Router::new()
1189
        .nest("/api", api)
1190
        .layer(middleware::from_fn(csrf_guard));
1191
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
1192
    match web_dir {
1193
        Some(dir) => {
1194
            let files =
1195
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
1196
            router.fallback_service(files)
1197
        }
1198
        None => router.fallback(static_shell),
1199
    }
1200
}