atagen

malice

public
Star0
Watch0
1b586ca058592ee4ddfa70958d7c0c4ff0c90c69
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/routes.rsRust1194 lines32.6 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::Kick { .. }
558
            | Request::Ban { .. }
559
            | Request::Unban { .. }
560
            | Request::SetPowerLevel { .. }
561
            | Request::SetDisplayName { .. }
562
            | Request::SetAvatar { .. }
563
            | Request::Invite { .. }
564
            | Request::Direct { .. }
565
            | Request::Redact { .. }
566
            | Request::SetFavourite { .. }
567
            | Request::ReorderSpaces { .. }
568
            | Request::SetRoomDisplayName { .. }
569
            | Request::SetRoomAvatar { .. }
570
            | Request::Mute { .. }
571
            | Request::Ignore { .. }
572
            | Request::Unignore { .. }
573
            | Request::PowerLevels { .. }
574
            | Request::OpenPinned { .. }
575
            | Request::ClosePinned { .. }
576
            | Request::SetPinned { .. }
577
            | Request::SendSticker { .. }
578
            | Request::CreateRoomPack { .. }
579
            | Request::DeletePack { .. }
580
            | Request::SetPackMeta { .. }
581
            | Request::PutPackImage { .. }
582
            | Request::RenamePackImage { .. }
583
            | Request::RemovePackImage { .. }
584
            | Request::SetPackEnabled { .. }
585
            | Request::ClonePackImage { .. }
586
            | Request::CloneSticker { .. }
587
            | Request::ClonePack { .. }
588
    ) {
589
        return ApiError::bad_request("op is not available as a command").into_response();
590
    }
591
    respond(dispatch(&user.handle, &user.mirror, request).await)
592
}
593
594
async fn profile_avatar(
595
    State(state): State<AppState>,
596
    headers: HeaderMap,
597
    bytes: axum::body::Bytes,
598
) -> Response {
599
    let (user, _) = match auth(&state, &headers).await {
600
        Ok(pair) => pair,
601
        Err(error) => return error.into_response(),
602
    };
603
    respond(
604
        dispatch(
605
            &user.handle,
606
            &user.mirror,
607
            Request::UploadAvatar {
608
                bytes: bytes.to_vec(),
609
            },
610
        )
611
        .await,
612
    )
613
}
614
615
#[derive(Debug, Deserialize)]
616
struct RoomAvatarQuery {
617
    room_id: String,
618
}
619
620
async fn rooms_avatar(
621
    State(state): State<AppState>,
622
    headers: HeaderMap,
623
    Query(query): Query<RoomAvatarQuery>,
624
    bytes: axum::body::Bytes,
625
) -> Response {
626
    let (user, _) = match auth(&state, &headers).await {
627
        Ok(pair) => pair,
628
        Err(error) => return error.into_response(),
629
    };
630
    respond(
631
        dispatch(
632
            &user.handle,
633
            &user.mirror,
634
            Request::UploadRoomAvatar {
635
                room_id: query.room_id,
636
                bytes: bytes.to_vec(),
637
            },
638
        )
639
        .await,
640
    )
641
}
642
643
async fn media_upload(
644
    State(state): State<AppState>,
645
    headers: HeaderMap,
646
    bytes: axum::body::Bytes,
647
) -> Response {
648
    let (user, _) = match auth(&state, &headers).await {
649
        Ok(pair) => pair,
650
        Err(error) => return error.into_response(),
651
    };
652
    respond(
653
        dispatch(
654
            &user.handle,
655
            &user.mirror,
656
            Request::UploadMedia {
657
                bytes: bytes.to_vec(),
658
            },
659
        )
660
        .await,
661
    )
662
}
663
664
async fn rooms_paginate(
665
    State(state): State<AppState>,
666
    headers: HeaderMap,
667
    body: axum::Json<PaginateBody>,
668
) -> Response {
669
    let (user, _) = match auth(&state, &headers).await {
670
        Ok(pair) => pair,
671
        Err(error) => return error.into_response(),
672
    };
673
    respond(
674
        dispatch(
675
            &user.handle,
676
            &user.mirror,
677
            Request::Paginate {
678
                room_id: body.0.room_id,
679
            },
680
        )
681
        .await,
682
    )
683
}
684
685
async fn rooms_context(
686
    State(state): State<AppState>,
687
    headers: HeaderMap,
688
    body: axum::Json<EventContextBody>,
689
) -> Response {
690
    let (user, _) = match auth(&state, &headers).await {
691
        Ok(pair) => pair,
692
        Err(error) => return error.into_response(),
693
    };
694
    respond(
695
        dispatch(
696
            &user.handle,
697
            &user.mirror,
698
            Request::EventContext {
699
                room_id: body.0.room_id,
700
                event_id: body.0.event_id,
701
                before_limit: body.0.before_limit,
702
                after_limit: body.0.after_limit,
703
            },
704
        )
705
        .await,
706
    )
707
}
708
709
async fn rooms_context_before(
710
    State(state): State<AppState>,
711
    headers: HeaderMap,
712
    body: axum::Json<EventContextPageBody>,
713
) -> Response {
714
    let (user, _) = match auth(&state, &headers).await {
715
        Ok(pair) => pair,
716
        Err(error) => return error.into_response(),
717
    };
718
    respond(
719
        dispatch(
720
            &user.handle,
721
            &user.mirror,
722
            Request::EventContextBefore {
723
                room_id: body.0.room_id,
724
                token: body.0.token,
725
                limit: body.0.limit,
726
            },
727
        )
728
        .await,
729
    )
730
}
731
732
async fn rooms_context_after(
733
    State(state): State<AppState>,
734
    headers: HeaderMap,
735
    body: axum::Json<EventContextPageBody>,
736
) -> Response {
737
    let (user, _) = match auth(&state, &headers).await {
738
        Ok(pair) => pair,
739
        Err(error) => return error.into_response(),
740
    };
741
    respond(
742
        dispatch(
743
            &user.handle,
744
            &user.mirror,
745
            Request::EventContextAfter {
746
                room_id: body.0.room_id,
747
                token: body.0.token,
748
                limit: body.0.limit,
749
            },
750
        )
751
        .await,
752
    )
753
}
754
755
async fn rooms_join(
756
    State(state): State<AppState>,
757
    headers: HeaderMap,
758
    body: axum::Json<JoinBody>,
759
) -> Response {
760
    let (user, _) = match auth(&state, &headers).await {
761
        Ok(pair) => pair,
762
        Err(error) => return error.into_response(),
763
    };
764
    respond(
765
        dispatch(
766
            &user.handle,
767
            &user.mirror,
768
            Request::Join { room: body.0.room },
769
        )
770
        .await,
771
    )
772
}
773
774
async fn rooms_create(
775
    State(state): State<AppState>,
776
    headers: HeaderMap,
777
    body: axum::Json<CreateBody>,
778
) -> Response {
779
    let (user, _) = match auth(&state, &headers).await {
780
        Ok(pair) => pair,
781
        Err(error) => return error.into_response(),
782
    };
783
    respond(
784
        dispatch(
785
            &user.handle,
786
            &user.mirror,
787
            Request::Create {
788
                name: body.0.name,
789
                topic: body.0.topic,
790
            },
791
        )
792
        .await,
793
    )
794
}
795
796
async fn rooms_leave(
797
    State(state): State<AppState>,
798
    headers: HeaderMap,
799
    body: axum::Json<RoomIdBody>,
800
) -> Response {
801
    let (user, _) = match auth(&state, &headers).await {
802
        Ok(pair) => pair,
803
        Err(error) => return error.into_response(),
804
    };
805
    respond(
806
        dispatch(
807
            &user.handle,
808
            &user.mirror,
809
            Request::Leave {
810
                room_id: body.0.room_id,
811
            },
812
        )
813
        .await,
814
    )
815
}
816
817
async fn rooms_edit(
818
    State(state): State<AppState>,
819
    headers: HeaderMap,
820
    body: axum::Json<EditBody>,
821
) -> Response {
822
    let (user, _) = match auth(&state, &headers).await {
823
        Ok(pair) => pair,
824
        Err(error) => return error.into_response(),
825
    };
826
    respond(
827
        dispatch(
828
            &user.handle,
829
            &user.mirror,
830
            Request::Edit {
831
                room_id: body.0.room_id,
832
                event_id: body.0.event_id,
833
                body: body.0.body,
834
                formatted_body: body.0.formatted_body,
835
                mentions: body.0.mentions,
836
            },
837
        )
838
        .await,
839
    )
840
}
841
842
async fn rooms_react(
843
    State(state): State<AppState>,
844
    headers: HeaderMap,
845
    body: axum::Json<ReactBody>,
846
) -> Response {
847
    let (user, _) = match auth(&state, &headers).await {
848
        Ok(pair) => pair,
849
        Err(error) => return error.into_response(),
850
    };
851
    respond(
852
        dispatch(
853
            &user.handle,
854
            &user.mirror,
855
            Request::React {
856
                room_id: body.0.room_id,
857
                event_id: body.0.event_id,
858
                key: body.0.key,
859
                shortcode: body.0.shortcode,
860
            },
861
        )
862
        .await,
863
    )
864
}
865
866
async fn rooms_read(
867
    State(state): State<AppState>,
868
    headers: HeaderMap,
869
    body: axum::Json<ReadBody>,
870
) -> Response {
871
    let (user, _) = match auth(&state, &headers).await {
872
        Ok(pair) => pair,
873
        Err(error) => return error.into_response(),
874
    };
875
    respond(
876
        dispatch(
877
            &user.handle,
878
            &user.mirror,
879
            Request::Read {
880
                room_id: body.0.room_id,
881
                event_id: body.0.event_id,
882
            },
883
        )
884
        .await,
885
    )
886
}
887
888
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
889
    let (user, _) = match auth(&state, &headers).await {
890
        Ok(pair) => pair,
891
        Err(error) => return error.into_response(),
892
    };
893
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
894
}
895
896
async fn get_space_children(
897
    State(state): State<AppState>,
898
    headers: HeaderMap,
899
    Query(query): Query<MembersQuery>,
900
) -> Response {
901
    let (user, _) = match auth(&state, &headers).await {
902
        Ok(pair) => pair,
903
        Err(error) => return error.into_response(),
904
    };
905
    respond(
906
        dispatch(
907
            &user.handle,
908
            &user.mirror,
909
            Request::SpaceChildren {
910
                room_id: query.room_id,
911
            },
912
        )
913
        .await,
914
    )
915
}
916
917
async fn get_media(
918
    State(state): State<AppState>,
919
    headers: HeaderMap,
920
    Query(query): Query<MediaQuery>,
921
) -> Response {
922
    let (user, _) = match auth(&state, &headers).await {
923
        Ok(pair) => pair,
924
        Err(error) => return error.into_response(),
925
    };
926
    respond(
927
        dispatch(
928
            &user.handle,
929
            &user.mirror,
930
            Request::Avatar { mxc: query.mxc },
931
        )
932
        .await,
933
    )
934
}
935
936
async fn get_message_media(
937
    State(state): State<AppState>,
938
    headers: HeaderMap,
939
    Query(query): Query<MessageMediaQuery>,
940
) -> Response {
941
    let (user, _) = match auth(&state, &headers).await {
942
        Ok(pair) => pair,
943
        Err(error) => return error.into_response(),
944
    };
945
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
946
    respond(
947
        dispatch(
948
            &user.handle,
949
            &user.mirror,
950
            Request::Media {
951
                media_id: query.media_id,
952
                size: query.size,
953
            },
954
        )
955
        .await,
956
    )
957
}
958
959
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
960
    let (user, _) = match auth(&state, &headers).await {
961
        Ok(pair) => pair,
962
        Err(error) => return error.into_response(),
963
    };
964
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
965
}
966
967
async fn recover_encryption(
968
    State(state): State<AppState>,
969
    headers: HeaderMap,
970
    body: axum::Json<RecoverBody>,
971
) -> Response {
972
    let (user, _) = match auth(&state, &headers).await {
973
        Ok(pair) => pair,
974
        Err(error) => return error.into_response(),
975
    };
976
    respond(
977
        dispatch(
978
            &user.handle,
979
            &user.mirror,
980
            Request::Recover {
981
                recovery_key: body.0.recovery_key,
982
            },
983
        )
984
        .await,
985
    )
986
}
987
988
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
989
    let (user, _) = match auth(&state, &headers).await {
990
        Ok(pair) => pair,
991
        Err(error) => return error.into_response(),
992
    };
993
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
994
}
995
996
async fn verify_encryption(
997
    State(state): State<AppState>,
998
    headers: HeaderMap,
999
    body: axum::Json<VerifyBody>,
1000
) -> Response {
1001
    let (user, _) = match auth(&state, &headers).await {
1002
        Ok(pair) => pair,
1003
        Err(error) => return error.into_response(),
1004
    };
1005
    respond(
1006
        dispatch(
1007
            &user.handle,
1008
            &user.mirror,
1009
            Request::Verify {
1010
                action: body.0.action,
1011
            },
1012
        )
1013
        .await,
1014
    )
1015
}
1016
1017
async fn rooms_typing(
1018
    State(state): State<AppState>,
1019
    headers: HeaderMap,
1020
    body: axum::Json<TypingBody>,
1021
) -> Response {
1022
    let (user, _) = match auth(&state, &headers).await {
1023
        Ok(pair) => pair,
1024
        Err(error) => return error.into_response(),
1025
    };
1026
    respond(
1027
        dispatch(
1028
            &user.handle,
1029
            &user.mirror,
1030
            Request::Typing {
1031
                room_id: body.0.room_id,
1032
                typing: body.0.typing,
1033
            },
1034
        )
1035
        .await,
1036
    )
1037
}
1038
1039
async fn get_account_data(
1040
    State(state): State<AppState>,
1041
    headers: HeaderMap,
1042
    Query(query): Query<AccountDataQuery>,
1043
) -> Response {
1044
    let (user, _) = match auth(&state, &headers).await {
1045
        Ok(pair) => pair,
1046
        Err(error) => return error.into_response(),
1047
    };
1048
    respond(
1049
        dispatch(
1050
            &user.handle,
1051
            &user.mirror,
1052
            Request::GetAccountData {
1053
                event_type: query.event_type,
1054
            },
1055
        )
1056
        .await,
1057
    )
1058
}
1059
1060
async fn get_url_preview(
1061
    State(state): State<AppState>,
1062
    headers: HeaderMap,
1063
    Query(query): Query<UrlPreviewQuery>,
1064
) -> Response {
1065
    let (user, _) = match auth(&state, &headers).await {
1066
        Ok(pair) => pair,
1067
        Err(error) => return error.into_response(),
1068
    };
1069
    respond(
1070
        dispatch(
1071
            &user.handle,
1072
            &user.mirror,
1073
            Request::UrlPreview {
1074
                room_id: query.room_id,
1075
                url: query.url,
1076
            },
1077
        )
1078
        .await,
1079
    )
1080
}
1081
1082
async fn set_account_data(
1083
    State(state): State<AppState>,
1084
    headers: HeaderMap,
1085
    body: axum::Json<AccountDataBody>,
1086
) -> Response {
1087
    let (user, _) = match auth(&state, &headers).await {
1088
        Ok(pair) => pair,
1089
        Err(error) => return error.into_response(),
1090
    };
1091
    let body = body.0;
1092
    respond(
1093
        dispatch(
1094
            &user.handle,
1095
            &user.mirror,
1096
            Request::SetAccountData {
1097
                event_type: body.event_type,
1098
                content: body.content,
1099
            },
1100
        )
1101
        .await,
1102
    )
1103
}
1104
1105
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
1106
    let (user, _) = match auth(&state, &headers).await {
1107
        Ok(pair) => pair,
1108
        Err(error) => return error.into_response(),
1109
    };
1110
    let body = subscribe(&user.mirror).map(|event| {
1111
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
1112
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
1113
    });
1114
    Sse::new(body).into_response()
1115
}
1116
1117
async fn get_health() -> Response {
1118
    json_ok(&serde_json::json!({ "status": "ok" }), None)
1119
}
1120
1121
async fn static_shell() -> Response {
1122
    (
1123
        StatusCode::SERVICE_UNAVAILABLE,
1124
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
1125
    )
1126
        .into_response()
1127
}
1128
1129
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
1130
    let api = Router::new()
1131
        .route("/session", get(get_session))
1132
        .route("/login", post(post_login))
1133
        .route("/logout", post(post_logout))
1134
        .route("/rooms", get(get_rooms))
1135
        .route("/events", get(get_events))
1136
        .route("/health", get(get_health))
1137
        .route("/rooms/open", post(rooms_open))
1138
        .route("/rooms/close", post(rooms_close))
1139
        .route("/rooms/send", post(rooms_send))
1140
        .route("/rooms/paginate", post(rooms_paginate))
1141
        .route("/rooms/context", post(rooms_context))
1142
        .route("/rooms/context/before", post(rooms_context_before))
1143
        .route("/rooms/context/after", post(rooms_context_after))
1144
        .route(
1145
            "/rooms/upload",
1146
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
1147
        )
1148
        .route("/rooms/join", post(rooms_join))
1149
        .route("/rooms/create", post(rooms_create))
1150
        .route("/rooms/leave", post(rooms_leave))
1151
        .route("/rooms/react", post(rooms_react))
1152
        .route("/rooms/edit", post(rooms_edit))
1153
        .route("/rooms/read", post(rooms_read))
1154
        .route("/rooms/typing", post(rooms_typing))
1155
        .route(
1156
            "/account-data",
1157
            get(get_account_data).post(set_account_data),
1158
        )
1159
        .route("/url-preview", get(get_url_preview))
1160
        .route("/profile", get(get_profile))
1161
        .route(
1162
            "/profile/avatar",
1163
            post(profile_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1164
        )
1165
        .route("/command", post(post_command))
1166
        .route(
1167
            "/rooms/avatar",
1168
            post(rooms_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1169
        )
1170
        .route(
1171
            "/media/upload",
1172
            post(media_upload).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1173
        )
1174
        .route("/media", get(get_media))
1175
        .route("/message-media", get(get_message_media))
1176
        .route("/spaces/children", get(get_space_children))
1177
        .route("/rooms/members", get(get_members))
1178
        .route("/encryption", get(get_encryption))
1179
        .route("/encryption/recover", post(recover_encryption))
1180
        .route("/encryption/start", post(start_verification))
1181
        .route("/encryption/verify", post(verify_encryption));
1182
    let api = Router::new()
1183
        .nest("/api", api)
1184
        .layer(middleware::from_fn(csrf_guard));
1185
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
1186
    match web_dir {
1187
        Some(dir) => {
1188
            let files =
1189
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
1190
            router.fallback_service(files)
1191
        }
1192
        None => router.fallback(static_shell),
1193
    }
1194
}