atagen

malice

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