atagen

malice

public
Star0
Watch0
dda384a360e028dc50d254f808a8d3fe8cc7a013
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-server/src/routes.rsRust1244 lines34.6 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 thread_root = None;
479
    let mut mentions = Vec::new();
480
    let mut file = None;
481
    loop {
482
        let field = match upload.next_field().await {
483
            Ok(Some(field)) => field,
484
            Ok(None) => break,
485
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
486
        };
487
        match field.name() {
488
            Some("room_id") if room_id.is_none() => {
489
                room_id = match field.text().await {
490
                    Ok(value) => Some(value),
491
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
492
                };
493
            }
494
            Some("caption") if caption.is_none() => {
495
                caption = match field.text().await {
496
                    Ok(value) => Some(value),
497
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
498
                };
499
            }
500
            Some("formatted_caption") if formatted_caption.is_none() => {
501
                formatted_caption = match field.text().await {
502
                    Ok(value) => Some(value),
503
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
504
                };
505
            }
506
            Some("reply_to") if reply_to.is_none() => {
507
                reply_to = match field.text().await {
508
                    Ok(value) => Some(value),
509
                    Err(_) => return ApiError::bad_request("invalid reply_to").into_response(),
510
                };
511
            }
512
            Some("thread_root") if thread_root.is_none() => {
513
                thread_root = match field.text().await {
514
                    Ok(value) => Some(value),
515
                    Err(_) => return ApiError::bad_request("invalid thread_root").into_response(),
516
                };
517
            }
518
            Some("mentions") => match field.text().await {
519
                Ok(value) => mentions.push(value),
520
                Err(_) => return ApiError::bad_request("invalid mention").into_response(),
521
            },
522
            Some("file") if file.is_none() => {
523
                let filename = field.file_name().unwrap_or("attachment").to_owned();
524
                let mimetype = field
525
                    .content_type()
526
                    .unwrap_or("application/octet-stream")
527
                    .to_owned();
528
                let bytes = match field.bytes().await {
529
                    Ok(bytes) => bytes,
530
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
531
                };
532
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
533
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
534
                        .into_response();
535
                }
536
                file = Some((filename, mimetype, bytes));
537
            }
538
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
539
        }
540
    }
541
    let Some(room_id) = room_id else {
542
        return ApiError::bad_request("room_id is required").into_response();
543
    };
544
    let Some((filename, mimetype, bytes)) = file else {
545
        return ApiError::bad_request("file is required").into_response();
546
    };
547
    respond(
548
        dispatch(
549
            &user.handle,
550
            &user.mirror,
551
            Request::Upload {
552
                room_id,
553
                filename,
554
                mimetype,
555
                caption,
556
                formatted_caption,
557
                reply_to,
558
                mentions,
559
                thread_root,
560
                bytes: bytes.to_vec(),
561
            },
562
        )
563
        .await,
564
    )
565
}
566
567
/// Ops added after the per-route handlers, taken as the core request JSON. Anything
568
/// carrying bytes stays on its own route.
569
async fn post_command(
570
    State(state): State<AppState>,
571
    headers: HeaderMap,
572
    body: axum::Json<Request>,
573
) -> Response {
574
    let (user, _) = match auth(&state, &headers).await {
575
        Ok(pair) => pair,
576
        Err(error) => return error.into_response(),
577
    };
578
    let request = body.0;
579
    if !matches!(
580
        request,
581
        Request::MentionsPage { .. }
582
            | Request::MentionContext { .. }
583
            | Request::RetryMentionContext { .. }
584
            | Request::SetMentionsRead { .. }
585
            | Request::MarkMentionsReadThrough { .. }
586
            | Request::RecoverMentions
587
            | Request::StartRecentHistory { .. }
588
            | Request::FillHistoryGap { .. }
589
            | Request::Kick { .. }
590
            | Request::Ban { .. }
591
            | Request::Unban { .. }
592
            | Request::SetPowerLevel { .. }
593
            | Request::SetDisplayName { .. }
594
            | Request::SetAvatar { .. }
595
            | Request::Invite { .. }
596
            | Request::Direct { .. }
597
            | Request::Redact { .. }
598
            | Request::OpenThread { .. }
599
            | Request::CloseThread { .. }
600
            | Request::SetFavourite { .. }
601
            | Request::ReorderSpaces { .. }
602
            | Request::SetRoomDisplayName { .. }
603
            | Request::SetRoomAvatar { .. }
604
            | Request::Mute { .. }
605
            | Request::NotificationKeywords
606
            | Request::AddNotificationKeyword { .. }
607
            | Request::RemoveNotificationKeyword { .. }
608
            | Request::Ignore { .. }
609
            | Request::Unignore { .. }
610
            | Request::PowerLevels { .. }
611
            | Request::OpenPinned { .. }
612
            | Request::ClosePinned { .. }
613
            | Request::SetPinned { .. }
614
            | Request::SendSticker { .. }
615
            | Request::CreateRoomPack { .. }
616
            | Request::DeletePack { .. }
617
            | Request::SetPackMeta { .. }
618
            | Request::PutPackImage { .. }
619
            | Request::RenamePackImage { .. }
620
            | Request::RemovePackImage { .. }
621
            | Request::SetPackEnabled { .. }
622
            | Request::ClonePackImage { .. }
623
            | Request::CloneSticker { .. }
624
            | Request::ClonePack { .. }
625
    ) {
626
        return ApiError::bad_request("op is not available as a command").into_response();
627
    }
628
    respond(dispatch(&user.handle, &user.mirror, request).await)
629
}
630
631
async fn profile_avatar(
632
    State(state): State<AppState>,
633
    headers: HeaderMap,
634
    bytes: axum::body::Bytes,
635
) -> Response {
636
    let (user, _) = match auth(&state, &headers).await {
637
        Ok(pair) => pair,
638
        Err(error) => return error.into_response(),
639
    };
640
    respond(
641
        dispatch(
642
            &user.handle,
643
            &user.mirror,
644
            Request::UploadAvatar {
645
                bytes: bytes.to_vec(),
646
            },
647
        )
648
        .await,
649
    )
650
}
651
652
#[derive(Debug, Deserialize)]
653
struct RoomAvatarQuery {
654
    room_id: String,
655
}
656
657
async fn rooms_avatar(
658
    State(state): State<AppState>,
659
    headers: HeaderMap,
660
    Query(query): Query<RoomAvatarQuery>,
661
    bytes: axum::body::Bytes,
662
) -> Response {
663
    let (user, _) = match auth(&state, &headers).await {
664
        Ok(pair) => pair,
665
        Err(error) => return error.into_response(),
666
    };
667
    respond(
668
        dispatch(
669
            &user.handle,
670
            &user.mirror,
671
            Request::UploadRoomAvatar {
672
                room_id: query.room_id,
673
                bytes: bytes.to_vec(),
674
            },
675
        )
676
        .await,
677
    )
678
}
679
680
async fn media_upload(
681
    State(state): State<AppState>,
682
    headers: HeaderMap,
683
    bytes: axum::body::Bytes,
684
) -> Response {
685
    let (user, _) = match auth(&state, &headers).await {
686
        Ok(pair) => pair,
687
        Err(error) => return error.into_response(),
688
    };
689
    respond(
690
        dispatch(
691
            &user.handle,
692
            &user.mirror,
693
            Request::UploadMedia {
694
                bytes: bytes.to_vec(),
695
            },
696
        )
697
        .await,
698
    )
699
}
700
701
async fn rooms_paginate(
702
    State(state): State<AppState>,
703
    headers: HeaderMap,
704
    body: axum::Json<PaginateBody>,
705
) -> Response {
706
    let (user, _) = match auth(&state, &headers).await {
707
        Ok(pair) => pair,
708
        Err(error) => return error.into_response(),
709
    };
710
    respond(
711
        dispatch(
712
            &user.handle,
713
            &user.mirror,
714
            Request::Paginate {
715
                room_id: body.0.room_id,
716
                thread_root: body.0.thread_root,
717
            },
718
        )
719
        .await,
720
    )
721
}
722
723
async fn rooms_context(
724
    State(state): State<AppState>,
725
    headers: HeaderMap,
726
    body: axum::Json<EventContextBody>,
727
) -> Response {
728
    let (user, _) = match auth(&state, &headers).await {
729
        Ok(pair) => pair,
730
        Err(error) => return error.into_response(),
731
    };
732
    respond(
733
        dispatch(
734
            &user.handle,
735
            &user.mirror,
736
            Request::EventContext {
737
                room_id: body.0.room_id,
738
                event_id: body.0.event_id,
739
                before_limit: body.0.before_limit,
740
                after_limit: body.0.after_limit,
741
            },
742
        )
743
        .await,
744
    )
745
}
746
747
async fn rooms_context_before(
748
    State(state): State<AppState>,
749
    headers: HeaderMap,
750
    body: axum::Json<EventContextPageBody>,
751
) -> Response {
752
    let (user, _) = match auth(&state, &headers).await {
753
        Ok(pair) => pair,
754
        Err(error) => return error.into_response(),
755
    };
756
    respond(
757
        dispatch(
758
            &user.handle,
759
            &user.mirror,
760
            Request::EventContextBefore {
761
                room_id: body.0.room_id,
762
                token: body.0.token,
763
                limit: body.0.limit,
764
            },
765
        )
766
        .await,
767
    )
768
}
769
770
async fn rooms_context_after(
771
    State(state): State<AppState>,
772
    headers: HeaderMap,
773
    body: axum::Json<EventContextPageBody>,
774
) -> Response {
775
    let (user, _) = match auth(&state, &headers).await {
776
        Ok(pair) => pair,
777
        Err(error) => return error.into_response(),
778
    };
779
    respond(
780
        dispatch(
781
            &user.handle,
782
            &user.mirror,
783
            Request::EventContextAfter {
784
                room_id: body.0.room_id,
785
                token: body.0.token,
786
                limit: body.0.limit,
787
            },
788
        )
789
        .await,
790
    )
791
}
792
793
async fn rooms_join(
794
    State(state): State<AppState>,
795
    headers: HeaderMap,
796
    body: axum::Json<JoinBody>,
797
) -> Response {
798
    let (user, _) = match auth(&state, &headers).await {
799
        Ok(pair) => pair,
800
        Err(error) => return error.into_response(),
801
    };
802
    respond(
803
        dispatch(
804
            &user.handle,
805
            &user.mirror,
806
            Request::Join { room: body.0.room },
807
        )
808
        .await,
809
    )
810
}
811
812
async fn rooms_create(
813
    State(state): State<AppState>,
814
    headers: HeaderMap,
815
    body: axum::Json<CreateBody>,
816
) -> Response {
817
    let (user, _) = match auth(&state, &headers).await {
818
        Ok(pair) => pair,
819
        Err(error) => return error.into_response(),
820
    };
821
    respond(
822
        dispatch(
823
            &user.handle,
824
            &user.mirror,
825
            Request::Create {
826
                name: body.0.name,
827
                topic: body.0.topic,
828
            },
829
        )
830
        .await,
831
    )
832
}
833
834
async fn rooms_leave(
835
    State(state): State<AppState>,
836
    headers: HeaderMap,
837
    body: axum::Json<RoomIdBody>,
838
) -> Response {
839
    let (user, _) = match auth(&state, &headers).await {
840
        Ok(pair) => pair,
841
        Err(error) => return error.into_response(),
842
    };
843
    respond(
844
        dispatch(
845
            &user.handle,
846
            &user.mirror,
847
            Request::Leave {
848
                room_id: body.0.room_id,
849
            },
850
        )
851
        .await,
852
    )
853
}
854
855
async fn rooms_edit(
856
    State(state): State<AppState>,
857
    headers: HeaderMap,
858
    body: axum::Json<EditBody>,
859
) -> Response {
860
    let (user, _) = match auth(&state, &headers).await {
861
        Ok(pair) => pair,
862
        Err(error) => return error.into_response(),
863
    };
864
    respond(
865
        dispatch(
866
            &user.handle,
867
            &user.mirror,
868
            Request::Edit {
869
                room_id: body.0.room_id,
870
                event_id: body.0.event_id,
871
                body: body.0.body,
872
                formatted_body: body.0.formatted_body,
873
                mentions: body.0.mentions,
874
                thread_root: body.0.thread_root,
875
            },
876
        )
877
        .await,
878
    )
879
}
880
881
async fn rooms_react(
882
    State(state): State<AppState>,
883
    headers: HeaderMap,
884
    body: axum::Json<ReactBody>,
885
) -> Response {
886
    let (user, _) = match auth(&state, &headers).await {
887
        Ok(pair) => pair,
888
        Err(error) => return error.into_response(),
889
    };
890
    respond(
891
        dispatch(
892
            &user.handle,
893
            &user.mirror,
894
            Request::React {
895
                room_id: body.0.room_id,
896
                event_id: body.0.event_id,
897
                key: body.0.key,
898
                shortcode: body.0.shortcode,
899
                thread_root: body.0.thread_root,
900
            },
901
        )
902
        .await,
903
    )
904
}
905
906
async fn rooms_read(
907
    State(state): State<AppState>,
908
    headers: HeaderMap,
909
    body: axum::Json<ReadBody>,
910
) -> Response {
911
    let (user, _) = match auth(&state, &headers).await {
912
        Ok(pair) => pair,
913
        Err(error) => return error.into_response(),
914
    };
915
    respond(
916
        dispatch(
917
            &user.handle,
918
            &user.mirror,
919
            Request::Read {
920
                room_id: body.0.room_id,
921
                event_id: body.0.event_id,
922
                thread_root: body.0.thread_root,
923
            },
924
        )
925
        .await,
926
    )
927
}
928
929
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
930
    let (user, _) = match auth(&state, &headers).await {
931
        Ok(pair) => pair,
932
        Err(error) => return error.into_response(),
933
    };
934
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
935
}
936
937
async fn get_space_children(
938
    State(state): State<AppState>,
939
    headers: HeaderMap,
940
    Query(query): Query<MembersQuery>,
941
) -> Response {
942
    let (user, _) = match auth(&state, &headers).await {
943
        Ok(pair) => pair,
944
        Err(error) => return error.into_response(),
945
    };
946
    respond(
947
        dispatch(
948
            &user.handle,
949
            &user.mirror,
950
            Request::SpaceChildren {
951
                room_id: query.room_id,
952
            },
953
        )
954
        .await,
955
    )
956
}
957
958
async fn get_media(
959
    State(state): State<AppState>,
960
    headers: HeaderMap,
961
    Query(query): Query<MediaQuery>,
962
) -> Response {
963
    let (user, _) = match auth(&state, &headers).await {
964
        Ok(pair) => pair,
965
        Err(error) => return error.into_response(),
966
    };
967
    let request = Request::Avatar {
968
        mxc: query.mxc,
969
        size: query.size.unwrap_or(malice_core::media::AVATAR_SIZE),
970
    };
971
    match dispatch(&user.handle, &user.mirror, request).await {
972
        Ok(Reply::Avatar(source)) => {
973
            match tokio::task::spawn_blocking(move || source.render()).await {
974
                Ok((bytes, content_type)) => avatar_response(bytes, content_type),
975
                Err(_) => ApiError::new(
976
                    StatusCode::INTERNAL_SERVER_ERROR,
977
                    "avatar could not be rendered",
978
                )
979
                .into_response(),
980
            }
981
        }
982
        other => respond(other),
983
    }
984
}
985
986
async fn get_message_media(
987
    State(state): State<AppState>,
988
    headers: HeaderMap,
989
    Query(query): Query<MessageMediaQuery>,
990
) -> Response {
991
    let (user, _) = match auth(&state, &headers).await {
992
        Ok(pair) => pair,
993
        Err(error) => return error.into_response(),
994
    };
995
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
996
    respond(
997
        dispatch(
998
            &user.handle,
999
            &user.mirror,
1000
            Request::Media {
1001
                media_id: query.media_id,
1002
                size: query.size,
1003
            },
1004
        )
1005
        .await,
1006
    )
1007
}
1008
1009
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
1010
    let (user, _) = match auth(&state, &headers).await {
1011
        Ok(pair) => pair,
1012
        Err(error) => return error.into_response(),
1013
    };
1014
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
1015
}
1016
1017
async fn recover_encryption(
1018
    State(state): State<AppState>,
1019
    headers: HeaderMap,
1020
    body: axum::Json<RecoverBody>,
1021
) -> Response {
1022
    let (user, _) = match auth(&state, &headers).await {
1023
        Ok(pair) => pair,
1024
        Err(error) => return error.into_response(),
1025
    };
1026
    respond(
1027
        dispatch(
1028
            &user.handle,
1029
            &user.mirror,
1030
            Request::Recover {
1031
                recovery_key: body.0.recovery_key,
1032
            },
1033
        )
1034
        .await,
1035
    )
1036
}
1037
1038
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
1039
    let (user, _) = match auth(&state, &headers).await {
1040
        Ok(pair) => pair,
1041
        Err(error) => return error.into_response(),
1042
    };
1043
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
1044
}
1045
1046
async fn verify_encryption(
1047
    State(state): State<AppState>,
1048
    headers: HeaderMap,
1049
    body: axum::Json<VerifyBody>,
1050
) -> Response {
1051
    let (user, _) = match auth(&state, &headers).await {
1052
        Ok(pair) => pair,
1053
        Err(error) => return error.into_response(),
1054
    };
1055
    respond(
1056
        dispatch(
1057
            &user.handle,
1058
            &user.mirror,
1059
            Request::Verify {
1060
                action: body.0.action,
1061
            },
1062
        )
1063
        .await,
1064
    )
1065
}
1066
1067
async fn rooms_typing(
1068
    State(state): State<AppState>,
1069
    headers: HeaderMap,
1070
    body: axum::Json<TypingBody>,
1071
) -> Response {
1072
    let (user, _) = match auth(&state, &headers).await {
1073
        Ok(pair) => pair,
1074
        Err(error) => return error.into_response(),
1075
    };
1076
    respond(
1077
        dispatch(
1078
            &user.handle,
1079
            &user.mirror,
1080
            Request::Typing {
1081
                room_id: body.0.room_id,
1082
                typing: body.0.typing,
1083
            },
1084
        )
1085
        .await,
1086
    )
1087
}
1088
1089
async fn get_account_data(
1090
    State(state): State<AppState>,
1091
    headers: HeaderMap,
1092
    Query(query): Query<AccountDataQuery>,
1093
) -> Response {
1094
    let (user, _) = match auth(&state, &headers).await {
1095
        Ok(pair) => pair,
1096
        Err(error) => return error.into_response(),
1097
    };
1098
    respond(
1099
        dispatch(
1100
            &user.handle,
1101
            &user.mirror,
1102
            Request::GetAccountData {
1103
                event_type: query.event_type,
1104
            },
1105
        )
1106
        .await,
1107
    )
1108
}
1109
1110
async fn get_url_preview(
1111
    State(state): State<AppState>,
1112
    headers: HeaderMap,
1113
    Query(query): Query<UrlPreviewQuery>,
1114
) -> Response {
1115
    let (user, _) = match auth(&state, &headers).await {
1116
        Ok(pair) => pair,
1117
        Err(error) => return error.into_response(),
1118
    };
1119
    respond(
1120
        dispatch(
1121
            &user.handle,
1122
            &user.mirror,
1123
            Request::UrlPreview {
1124
                room_id: query.room_id,
1125
                url: query.url,
1126
            },
1127
        )
1128
        .await,
1129
    )
1130
}
1131
1132
async fn set_account_data(
1133
    State(state): State<AppState>,
1134
    headers: HeaderMap,
1135
    body: axum::Json<AccountDataBody>,
1136
) -> Response {
1137
    let (user, _) = match auth(&state, &headers).await {
1138
        Ok(pair) => pair,
1139
        Err(error) => return error.into_response(),
1140
    };
1141
    let body = body.0;
1142
    respond(
1143
        dispatch(
1144
            &user.handle,
1145
            &user.mirror,
1146
            Request::SetAccountData {
1147
                event_type: body.event_type,
1148
                content: body.content,
1149
            },
1150
        )
1151
        .await,
1152
    )
1153
}
1154
1155
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
1156
    let (user, _) = match auth(&state, &headers).await {
1157
        Ok(pair) => pair,
1158
        Err(error) => return error.into_response(),
1159
    };
1160
    let body = subscribe(&user.mirror).map(|event| {
1161
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
1162
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
1163
    });
1164
    Sse::new(body).into_response()
1165
}
1166
1167
async fn get_health() -> Response {
1168
    json_ok(&serde_json::json!({ "status": "ok" }), None)
1169
}
1170
1171
async fn static_shell() -> Response {
1172
    (
1173
        StatusCode::SERVICE_UNAVAILABLE,
1174
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
1175
    )
1176
        .into_response()
1177
}
1178
1179
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
1180
    let api = Router::new()
1181
        .route("/session", get(get_session))
1182
        .route("/login", post(post_login))
1183
        .route("/logout", post(post_logout))
1184
        .route("/rooms", get(get_rooms))
1185
        .route("/events", get(get_events))
1186
        .route("/health", get(get_health))
1187
        .route("/rooms/open", post(rooms_open))
1188
        .route("/rooms/close", post(rooms_close))
1189
        .route("/rooms/send", post(rooms_send))
1190
        .route("/rooms/paginate", post(rooms_paginate))
1191
        .route("/rooms/context", post(rooms_context))
1192
        .route("/rooms/context/before", post(rooms_context_before))
1193
        .route("/rooms/context/after", post(rooms_context_after))
1194
        .route(
1195
            "/rooms/upload",
1196
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
1197
        )
1198
        .route("/rooms/join", post(rooms_join))
1199
        .route("/rooms/create", post(rooms_create))
1200
        .route("/rooms/leave", post(rooms_leave))
1201
        .route("/rooms/react", post(rooms_react))
1202
        .route("/rooms/edit", post(rooms_edit))
1203
        .route("/rooms/read", post(rooms_read))
1204
        .route("/rooms/typing", post(rooms_typing))
1205
        .route(
1206
            "/account-data",
1207
            get(get_account_data).post(set_account_data),
1208
        )
1209
        .route("/url-preview", get(get_url_preview))
1210
        .route("/profile", get(get_profile))
1211
        .route(
1212
            "/profile/avatar",
1213
            post(profile_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1214
        )
1215
        .route("/command", post(post_command))
1216
        .route(
1217
            "/rooms/avatar",
1218
            post(rooms_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1219
        )
1220
        .route(
1221
            "/media/upload",
1222
            post(media_upload).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
1223
        )
1224
        .route("/media", get(get_media))
1225
        .route("/message-media", get(get_message_media))
1226
        .route("/spaces/children", get(get_space_children))
1227
        .route("/rooms/members", get(get_members))
1228
        .route("/encryption", get(get_encryption))
1229
        .route("/encryption/recover", post(recover_encryption))
1230
        .route("/encryption/start", post(start_verification))
1231
        .route("/encryption/verify", post(verify_encryption));
1232
    let api = Router::new()
1233
        .nest("/api", api)
1234
        .layer(middleware::from_fn(csrf_guard));
1235
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
1236
    match web_dir {
1237
        Some(dir) => {
1238
            let files =
1239
                ServeDir::new(&dir).not_found_service(ServeFile::new(format!("{dir}/index.html")));
1240
            router.fallback_service(files)
1241
        }
1242
        None => router.fallback(static_shell),
1243
    }
1244
}