atagen

malice

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