atagen

malice

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