atagen

malice

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