atagen

malice

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