atagen

malice

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