atagen

malice

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