atagen

malice

public
Star0
Watch0
988fbf982ada53ef8961e1532f3755112731c232
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-server/src/routes.rs

973f8fbdaf80atagen2026-09-28 09:10 UTC1
use std::sync::Arc;
973f8fbdaf80atagen2026-09-28 09:10 UTC2
973f8fbdaf80atagen2026-09-28 09:10 UTC3
use axum::{
973f8fbdaf80atagen2026-09-28 09:10 UTC4
    Router,
973f8fbdaf80atagen2026-09-28 09:10 UTC5
    extract::{DefaultBodyLimit, Multipart, Query, State},
dffd63b58af4atagen2026-09-28 09:10 UTC6
    http::{HeaderMap, StatusCode},
973f8fbdaf80atagen2026-09-28 09:10 UTC7
    middleware,
973f8fbdaf80atagen2026-09-28 09:10 UTC8
    response::{IntoResponse, Response, Sse, sse::Event as SseEvent},
dffd63b58af4atagen2026-09-28 09:10 UTC9
    routing::{get, post},
973f8fbdaf80atagen2026-09-28 09:10 UTC10
};
973f8fbdaf80atagen2026-09-28 09:10 UTC11
use futures_util::StreamExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC12
use macltok::Retain;
973f8fbdaf80atagen2026-09-28 09:10 UTC13
use malice_core::{
973f8fbdaf80atagen2026-09-28 09:10 UTC14
    feed::subscribe,
544a170ead2catagen2026-10-20 11:47 UTC15
    login::LoginInput,
7cca1d5628d1atagen2026-10-24 07:36 UTC16
    request::{Reply, Request, SendKind, dispatch},
973f8fbdaf80atagen2026-09-28 09:10 UTC17
};
973f8fbdaf80atagen2026-09-28 09:10 UTC18
use serde::{Deserialize, Serialize};
973f8fbdaf80atagen2026-09-28 09:10 UTC19
use tower_http::{
54e1929170a7atagen2026-10-26 03:48 UTC20
    compression::{
54e1929170a7atagen2026-10-26 03:48 UTC21
        CompressionLayer,
54e1929170a7atagen2026-10-26 03:48 UTC22
        predicate::{DefaultPredicate, NotForContentType, Predicate},
54e1929170a7atagen2026-10-26 03:48 UTC23
    },
973f8fbdaf80atagen2026-09-28 09:10 UTC24
    services::{ServeDir, ServeFile},
973f8fbdaf80atagen2026-09-28 09:10 UTC25
    trace::TraceLayer,
973f8fbdaf80atagen2026-09-28 09:10 UTC26
};
973f8fbdaf80atagen2026-09-28 09:10 UTC27
973f8fbdaf80atagen2026-09-28 09:10 UTC28
use crate::{
973f8fbdaf80atagen2026-09-28 09:10 UTC29
    csrf::{csrf_guard, session_sid, set_cookie_value},
973f8fbdaf80atagen2026-09-28 09:10 UTC30
    error::{ApiError, json_ok, no_content},
973f8fbdaf80atagen2026-09-28 09:10 UTC31
    state::{AppState, RestoreError, UserSession},
973f8fbdaf80atagen2026-09-28 09:10 UTC32
};
973f8fbdaf80atagen2026-09-28 09:10 UTC33
973f8fbdaf80atagen2026-09-28 09:10 UTC34
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC35
struct RoomIdBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC36
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC37
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC38
}
973f8fbdaf80atagen2026-09-28 09:10 UTC39
973f8fbdaf80atagen2026-09-28 09:10 UTC40
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC41
struct SendBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC43
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC44
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC45
    body: String,
df3c0f2b3da4atagen2026-10-22 13:07 UTC46
    #[serde(default)]
df3c0f2b3da4atagen2026-10-22 13:07 UTC47
    formatted_body: Option<String>,
df3c0f2b3da4atagen2026-10-22 13:07 UTC48
    #[serde(default)]
df3c0f2b3da4atagen2026-10-22 13:07 UTC49
    reply_to: Option<String>,
cf23dbf4e77catagen2026-10-23 06:36 UTC50
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC51
    mentions: Vec<String>,
e4c64c36f105atagen2026-10-24 07:09 UTC52
    #[serde(default)]
7cca1d5628d1atagen2026-10-24 07:36 UTC53
    kind: SendKind,
7ebc2472a9e3atagen2026-10-25 09:53 UTC54
    #[serde(default)]
7ebc2472a9e3atagen2026-10-25 09:53 UTC55
    thread_root: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC56
}
973f8fbdaf80atagen2026-09-28 09:10 UTC57
973f8fbdaf80atagen2026-09-28 09:10 UTC58
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC59
struct PaginateBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC60
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC61
    room_id: String,
7ebc2472a9e3atagen2026-10-25 09:53 UTC62
    #[serde(default)]
7ebc2472a9e3atagen2026-10-25 09:53 UTC63
    thread_root: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC64
}
973f8fbdaf80atagen2026-09-28 09:10 UTC65
973f8fbdaf80atagen2026-09-28 09:10 UTC66
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC67
struct JoinBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC68
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    room: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC70
}
973f8fbdaf80atagen2026-09-28 09:10 UTC71
973f8fbdaf80atagen2026-09-28 09:10 UTC72
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC73
struct CreateBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC74
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC75
    name: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC76
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    topic: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC78
}
973f8fbdaf80atagen2026-09-28 09:10 UTC79
973f8fbdaf80atagen2026-09-28 09:10 UTC80
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC81
struct ReactBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC83
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC84
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC85
    event_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC86
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC87
    key: String,
ecb8598b38afatagen2026-10-22 09:19 UTC88
    #[serde(default)]
ecb8598b38afatagen2026-10-22 09:19 UTC89
    shortcode: Option<String>,
7ebc2472a9e3atagen2026-10-25 09:53 UTC90
    #[serde(default)]
7ebc2472a9e3atagen2026-10-25 09:53 UTC91
    thread_root: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC92
}
973f8fbdaf80atagen2026-09-28 09:10 UTC93
973f8fbdaf80atagen2026-09-28 09:10 UTC94
#[derive(Debug, Deserialize)]
cf23dbf4e77catagen2026-10-23 06:36 UTC95
struct EditBody {
cf23dbf4e77catagen2026-10-23 06:36 UTC96
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC97
    room_id: String,
cf23dbf4e77catagen2026-10-23 06:36 UTC98
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC99
    event_id: String,
cf23dbf4e77catagen2026-10-23 06:36 UTC100
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC101
    body: String,
cf23dbf4e77catagen2026-10-23 06:36 UTC102
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC103
    formatted_body: Option<String>,
cf23dbf4e77catagen2026-10-23 06:36 UTC104
    #[serde(default)]
cf23dbf4e77catagen2026-10-23 06:36 UTC105
    mentions: Vec<String>,
7ebc2472a9e3atagen2026-10-25 09:53 UTC106
    #[serde(default)]
7ebc2472a9e3atagen2026-10-25 09:53 UTC107
    thread_root: Option<String>,
cf23dbf4e77catagen2026-10-23 06:36 UTC108
}
cf23dbf4e77catagen2026-10-23 06:36 UTC109
cf23dbf4e77catagen2026-10-23 06:36 UTC110
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC111
struct ReadBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC112
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC113
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC114
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC115
    event_id: String,
7ebc2472a9e3atagen2026-10-25 09:53 UTC116
    #[serde(default)]
7ebc2472a9e3atagen2026-10-25 09:53 UTC117
    thread_root: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC118
}
973f8fbdaf80atagen2026-09-28 09:10 UTC119
973f8fbdaf80atagen2026-09-28 09:10 UTC120
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC121
struct MembersQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC122
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC123
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC124
}
973f8fbdaf80atagen2026-09-28 09:10 UTC125
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC126
struct RecoverBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC127
    recovery_key: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC128
}
973f8fbdaf80atagen2026-09-28 09:10 UTC129
973f8fbdaf80atagen2026-09-28 09:10 UTC130
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC131
struct VerifyBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC132
    action: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC133
}
973f8fbdaf80atagen2026-09-28 09:10 UTC134
973f8fbdaf80atagen2026-09-28 09:10 UTC135
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC136
struct MediaQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC137
    mxc: String,
7643577c76bbatagen2026-10-25 07:37 UTC138
    size: Option<u32>,
973f8fbdaf80atagen2026-09-28 09:10 UTC139
}
973f8fbdaf80atagen2026-09-28 09:10 UTC140
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC141
struct MessageMediaQuery {
973f8fbdaf80atagen2026-09-28 09:10 UTC142
    media_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC143
    size: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC144
}
973f8fbdaf80atagen2026-09-28 09:10 UTC145
973f8fbdaf80atagen2026-09-28 09:10 UTC146
#[derive(Debug, Deserialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC147
struct TypingBody {
973f8fbdaf80atagen2026-09-28 09:10 UTC148
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC149
    room_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC150
    #[serde(default)]
973f8fbdaf80atagen2026-09-28 09:10 UTC151
    typing: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC152
}
973f8fbdaf80atagen2026-09-28 09:10 UTC153
544a170ead2catagen2026-10-20 11:47 UTC154
#[derive(Debug, Deserialize)]
544a170ead2catagen2026-10-20 11:47 UTC155
struct AccountDataQuery {
544a170ead2catagen2026-10-20 11:47 UTC156
    event_type: String,
544a170ead2catagen2026-10-20 11:47 UTC157
}
544a170ead2catagen2026-10-20 11:47 UTC158
544a170ead2catagen2026-10-20 11:47 UTC159
#[derive(Debug, Deserialize)]
ecb8598b38afatagen2026-10-22 09:19 UTC160
struct UrlPreviewQuery {
ecb8598b38afatagen2026-10-22 09:19 UTC161
    #[serde(default)]
ecb8598b38afatagen2026-10-22 09:19 UTC162
    room_id: String,
ecb8598b38afatagen2026-10-22 09:19 UTC163
    #[serde(default)]
ecb8598b38afatagen2026-10-22 09:19 UTC164
    url: String,
ecb8598b38afatagen2026-10-22 09:19 UTC165
}
ecb8598b38afatagen2026-10-22 09:19 UTC166
ecb8598b38afatagen2026-10-22 09:19 UTC167
#[derive(Debug, Deserialize)]
544a170ead2catagen2026-10-20 11:47 UTC168
struct AccountDataBody {
544a170ead2catagen2026-10-20 11:47 UTC169
    event_type: String,
544a170ead2catagen2026-10-20 11:47 UTC170
    content: serde_json::Value,
544a170ead2catagen2026-10-20 11:47 UTC171
}
544a170ead2catagen2026-10-20 11:47 UTC172
973f8fbdaf80atagen2026-09-28 09:10 UTC173
#[derive(Debug, Serialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC174
struct SoftLogoutInfo {
973f8fbdaf80atagen2026-09-28 09:10 UTC175
    user_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC176
    homeserver: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC177
}
973f8fbdaf80atagen2026-09-28 09:10 UTC178
973f8fbdaf80atagen2026-09-28 09:10 UTC179
#[derive(Debug, Serialize)]
973f8fbdaf80atagen2026-09-28 09:10 UTC180
struct SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC181
    user_id: Option<String>,
973f8fbdaf80atagen2026-09-28 09:10 UTC182
    #[serde(skip_serializing_if = "Option::is_none")]
973f8fbdaf80atagen2026-09-28 09:10 UTC183
    soft_logout: Option<SoftLogoutInfo>,
973f8fbdaf80atagen2026-09-28 09:10 UTC184
}
973f8fbdaf80atagen2026-09-28 09:10 UTC185
aa3dce5114d0atagen2026-10-26 06:09 UTC186
pub(crate) async fn auth(
973f8fbdaf80atagen2026-09-28 09:10 UTC187
    state: &AppState,
973f8fbdaf80atagen2026-09-28 09:10 UTC188
    headers: &HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC189
) -> Result<(Arc<UserSession>, String), ApiError> {
973f8fbdaf80atagen2026-09-28 09:10 UTC190
    let Some(sid) = session_sid(headers) else {
973f8fbdaf80atagen2026-09-28 09:10 UTC191
        return Err(ApiError::unauthorized("not signed in"));
973f8fbdaf80atagen2026-09-28 09:10 UTC192
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC193
    match state.get_or_restore(&sid).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC194
        Ok(user) => Ok((user, sid)),
973f8fbdaf80atagen2026-09-28 09:10 UTC195
        Err(RestoreError::NotFound) => Err(ApiError::unauthorized_stale(
973f8fbdaf80atagen2026-09-28 09:10 UTC196
            "session expired; sign in again",
973f8fbdaf80atagen2026-09-28 09:10 UTC197
            state.cookie_secure(),
973f8fbdaf80atagen2026-09-28 09:10 UTC198
        )),
973f8fbdaf80atagen2026-09-28 09:10 UTC199
        Err(RestoreError::SoftLoggedOut { .. }) => Err(ApiError::unauthorized(
973f8fbdaf80atagen2026-09-28 09:10 UTC200
            "signed out by the server, sign in again",
973f8fbdaf80atagen2026-09-28 09:10 UTC201
        )),
973f8fbdaf80atagen2026-09-28 09:10 UTC202
        Err(RestoreError::Stale(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC203
            state.remove_session(&sid, Retain::Nothing).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC204
            Err(ApiError::unauthorized_stale(message, state.cookie_secure()))
973f8fbdaf80atagen2026-09-28 09:10 UTC205
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC206
        Err(RestoreError::Unavailable(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC207
            Err(ApiError::new(StatusCode::BAD_GATEWAY, message))
973f8fbdaf80atagen2026-09-28 09:10 UTC208
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC209
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC210
}
973f8fbdaf80atagen2026-09-28 09:10 UTC211
7643577c76bbatagen2026-10-25 07:37 UTC212
fn avatar_response(bytes: Vec<u8>, content_type: &'static str) -> Response {
7643577c76bbatagen2026-10-25 07:37 UTC213
    (
7643577c76bbatagen2026-10-25 07:37 UTC214
        [
7643577c76bbatagen2026-10-25 07:37 UTC215
            (axum::http::header::CONTENT_TYPE, content_type),
7643577c76bbatagen2026-10-25 07:37 UTC216
            // mxc uris name immutable content, a changed avatar gets a new uri
7643577c76bbatagen2026-10-25 07:37 UTC217
            (
7643577c76bbatagen2026-10-25 07:37 UTC218
                axum::http::header::CACHE_CONTROL,
7643577c76bbatagen2026-10-25 07:37 UTC219
                "private, max-age=31536000, immutable",
7643577c76bbatagen2026-10-25 07:37 UTC220
            ),
7643577c76bbatagen2026-10-25 07:37 UTC221
            (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
7643577c76bbatagen2026-10-25 07:37 UTC222
        ],
7643577c76bbatagen2026-10-25 07:37 UTC223
        bytes,
7643577c76bbatagen2026-10-25 07:37 UTC224
    )
7643577c76bbatagen2026-10-25 07:37 UTC225
        .into_response()
7643577c76bbatagen2026-10-25 07:37 UTC226
}
7643577c76bbatagen2026-10-25 07:37 UTC227
973f8fbdaf80atagen2026-09-28 09:10 UTC228
/// commands answer when they're done, so the browser gets the outcome in the response
973f8fbdaf80atagen2026-09-28 09:10 UTC229
fn respond(result: Result<Reply, malice_core::error::CoreError>) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC230
    match result {
973f8fbdaf80atagen2026-09-28 09:10 UTC231
        Ok(Reply::Done) => no_content(None),
973f8fbdaf80atagen2026-09-28 09:10 UTC232
        Ok(Reply::Json(value)) => json_ok(&value, None),
7643577c76bbatagen2026-10-25 07:37 UTC233
        Ok(Reply::Avatar(source)) => {
7643577c76bbatagen2026-10-25 07:37 UTC234
            let (bytes, content_type) = source.render();
7643577c76bbatagen2026-10-25 07:37 UTC235
            avatar_response(bytes, content_type)
7643577c76bbatagen2026-10-25 07:37 UTC236
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC237
        Ok(Reply::Attachment {
973f8fbdaf80atagen2026-09-28 09:10 UTC238
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC239
            content_type,
973f8fbdaf80atagen2026-09-28 09:10 UTC240
        }) => (
973f8fbdaf80atagen2026-09-28 09:10 UTC241
            [
973f8fbdaf80atagen2026-09-28 09:10 UTC242
                (
973f8fbdaf80atagen2026-09-28 09:10 UTC243
                    axum::http::header::CONTENT_TYPE,
973f8fbdaf80atagen2026-09-28 09:10 UTC244
                    content_type.unwrap_or("application/octet-stream"),
973f8fbdaf80atagen2026-09-28 09:10 UTC245
                ),
973f8fbdaf80atagen2026-09-28 09:10 UTC246
                (
973f8fbdaf80atagen2026-09-28 09:10 UTC247
                    axum::http::header::CONTENT_DISPOSITION,
973f8fbdaf80atagen2026-09-28 09:10 UTC248
                    if content_type.is_some() {
973f8fbdaf80atagen2026-09-28 09:10 UTC249
                        "inline"
973f8fbdaf80atagen2026-09-28 09:10 UTC250
                    } else {
973f8fbdaf80atagen2026-09-28 09:10 UTC251
                        "attachment"
973f8fbdaf80atagen2026-09-28 09:10 UTC252
                    },
973f8fbdaf80atagen2026-09-28 09:10 UTC253
                ),
973f8fbdaf80atagen2026-09-28 09:10 UTC254
                (axum::http::header::CACHE_CONTROL, "private, max-age=3600"),
973f8fbdaf80atagen2026-09-28 09:10 UTC255
                (axum::http::header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
973f8fbdaf80atagen2026-09-28 09:10 UTC256
                (axum::http::header::CONTENT_SECURITY_POLICY, "sandbox"),
973f8fbdaf80atagen2026-09-28 09:10 UTC257
            ],
973f8fbdaf80atagen2026-09-28 09:10 UTC258
            bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC259
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC260
            .into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC261
        Err(error) => ApiError::from(error).into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC262
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC263
}
973f8fbdaf80atagen2026-09-28 09:10 UTC264
973f8fbdaf80atagen2026-09-28 09:10 UTC265
async fn get_session(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC266
    let Some(sid) = session_sid(&headers) else {
973f8fbdaf80atagen2026-09-28 09:10 UTC267
        return json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC268
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC269
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC270
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC271
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC272
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC273
        );
973f8fbdaf80atagen2026-09-28 09:10 UTC274
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC275
    match state.get_or_restore(&sid).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC276
        Ok(user) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC277
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC278
                user_id: Some(user.user_id.clone()),
973f8fbdaf80atagen2026-09-28 09:10 UTC279
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC280
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC281
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC282
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC283
        Err(RestoreError::NotFound) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC284
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC285
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC286
                soft_logout: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC287
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC288
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC289
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC290
        Err(RestoreError::SoftLoggedOut {
973f8fbdaf80atagen2026-09-28 09:10 UTC291
            user_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC292
            homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC293
        }) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC294
            &SessionResponse {
973f8fbdaf80atagen2026-09-28 09:10 UTC295
                user_id: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC296
                soft_logout: Some(SoftLogoutInfo {
973f8fbdaf80atagen2026-09-28 09:10 UTC297
                    user_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC298
                    homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC299
                }),
973f8fbdaf80atagen2026-09-28 09:10 UTC300
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC301
            None,
973f8fbdaf80atagen2026-09-28 09:10 UTC302
        ),
973f8fbdaf80atagen2026-09-28 09:10 UTC303
        Err(RestoreError::Stale(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC304
            state.remove_session(&sid, Retain::Nothing).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC305
            ApiError::unauthorized_stale(message, state.cookie_secure()).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC306
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC307
        Err(RestoreError::Unavailable(message)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC308
            ApiError::new(StatusCode::BAD_GATEWAY, message).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC309
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC310
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC311
}
973f8fbdaf80atagen2026-09-28 09:10 UTC312
973f8fbdaf80atagen2026-09-28 09:10 UTC313
async fn post_login(
973f8fbdaf80atagen2026-09-28 09:10 UTC314
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC315
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC316
    body: axum::Json<LoginInput>,
973f8fbdaf80atagen2026-09-28 09:10 UTC317
) -> Response {
544a170ead2catagen2026-10-20 11:47 UTC318
    let username = body.0.username.trim().to_owned();
544a170ead2catagen2026-10-20 11:47 UTC319
    let password = body.0.password.clone();
544a170ead2catagen2026-10-20 11:47 UTC320
    let login = match body.0.into_login(state.allow_http_homeserver()) {
544a170ead2catagen2026-10-20 11:47 UTC321
        Ok(login) => login,
973f8fbdaf80atagen2026-09-28 09:10 UTC322
        Err(error) => return ApiError::from(error).into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC323
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC324
    let cookie_sid = session_sid(&headers);
973f8fbdaf80atagen2026-09-28 09:10 UTC325
    match state
544a170ead2catagen2026-10-20 11:47 UTC326
        .login(cookie_sid.as_deref(), login, &username, password)
973f8fbdaf80atagen2026-09-28 09:10 UTC327
        .await
973f8fbdaf80atagen2026-09-28 09:10 UTC328
    {
973f8fbdaf80atagen2026-09-28 09:10 UTC329
        Ok(user) => json_ok(
973f8fbdaf80atagen2026-09-28 09:10 UTC330
            &serde_json::json!({ "user_id": user.user_id }),
973f8fbdaf80atagen2026-09-28 09:10 UTC331
            Some(set_cookie_value(&user.sid, state.cookie_secure())),
973f8fbdaf80atagen2026-09-28 09:10 UTC332
        ),
544a170ead2catagen2026-10-20 11:47 UTC333
        Err(error) => ApiError::from(error).into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC334
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC335
}
973f8fbdaf80atagen2026-09-28 09:10 UTC336
973f8fbdaf80atagen2026-09-28 09:10 UTC337
async fn post_logout(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC338
    if let Some(sid) = session_sid(&headers) {
973f8fbdaf80atagen2026-09-28 09:10 UTC339
        state.logout(&sid).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC340
    }
cf23dbf4e77catagen2026-10-23 06:36 UTC341
    let mut response = no_content(Some(crate::csrf::clear_cookie_value(state.cookie_secure())));
cf23dbf4e77catagen2026-10-23 06:36 UTC342
    response.headers_mut().insert(
cf23dbf4e77catagen2026-10-23 06:36 UTC343
        axum::http::header::HeaderName::from_static("clear-site-data"),
cf23dbf4e77catagen2026-10-23 06:36 UTC344
        axum::http::HeaderValue::from_static("\"cache\""),
cf23dbf4e77catagen2026-10-23 06:36 UTC345
    );
cf23dbf4e77catagen2026-10-23 06:36 UTC346
    response
973f8fbdaf80atagen2026-09-28 09:10 UTC347
}
973f8fbdaf80atagen2026-09-28 09:10 UTC348
973f8fbdaf80atagen2026-09-28 09:10 UTC349
async fn get_rooms(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC350
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC351
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC352
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC353
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC354
    respond(dispatch(&user.handle, &user.mirror, Request::Rooms).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC355
}
973f8fbdaf80atagen2026-09-28 09:10 UTC356
973f8fbdaf80atagen2026-09-28 09:10 UTC357
async fn get_members(
973f8fbdaf80atagen2026-09-28 09:10 UTC358
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC359
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC360
    Query(query): Query<MembersQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC361
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC362
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC363
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC364
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC365
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC366
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC367
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC368
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC369
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC370
            Request::Members {
973f8fbdaf80atagen2026-09-28 09:10 UTC371
                room_id: query.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC372
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC373
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC374
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC375
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC376
}
973f8fbdaf80atagen2026-09-28 09:10 UTC377
973f8fbdaf80atagen2026-09-28 09:10 UTC378
async fn rooms_open(
973f8fbdaf80atagen2026-09-28 09:10 UTC379
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC380
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC381
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC382
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC383
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC384
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC385
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC386
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC387
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC388
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC389
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC390
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC391
            Request::Open {
973f8fbdaf80atagen2026-09-28 09:10 UTC392
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC393
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC394
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC395
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC396
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC397
}
973f8fbdaf80atagen2026-09-28 09:10 UTC398
973f8fbdaf80atagen2026-09-28 09:10 UTC399
async fn rooms_close(
973f8fbdaf80atagen2026-09-28 09:10 UTC400
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC401
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC402
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC403
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC404
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC405
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC406
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC407
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC408
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC409
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC410
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC411
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC412
            Request::Close {
973f8fbdaf80atagen2026-09-28 09:10 UTC413
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC414
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC415
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC416
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC417
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC418
}
973f8fbdaf80atagen2026-09-28 09:10 UTC419
973f8fbdaf80atagen2026-09-28 09:10 UTC420
async fn rooms_send(
973f8fbdaf80atagen2026-09-28 09:10 UTC421
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC422
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC423
    body: axum::Json<SendBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC424
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC425
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC426
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC427
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC428
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC429
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC430
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC431
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC432
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC433
            Request::Send {
973f8fbdaf80atagen2026-09-28 09:10 UTC434
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC435
                body: body.0.body,
df3c0f2b3da4atagen2026-10-22 13:07 UTC436
                formatted_body: body.0.formatted_body,
df3c0f2b3da4atagen2026-10-22 13:07 UTC437
                reply_to: body.0.reply_to,
cf23dbf4e77catagen2026-10-23 06:36 UTC438
                mentions: body.0.mentions,
7cca1d5628d1atagen2026-10-24 07:36 UTC439
                kind: body.0.kind,
7ebc2472a9e3atagen2026-10-25 09:53 UTC440
                thread_root: body.0.thread_root,
973f8fbdaf80atagen2026-09-28 09:10 UTC441
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC442
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC443
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC444
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC445
}
973f8fbdaf80atagen2026-09-28 09:10 UTC446
973f8fbdaf80atagen2026-09-28 09:10 UTC447
async fn rooms_upload(
973f8fbdaf80atagen2026-09-28 09:10 UTC448
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC449
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC450
    mut upload: Multipart,
973f8fbdaf80atagen2026-09-28 09:10 UTC451
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC452
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC453
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC454
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC455
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC456
    let mut room_id = None;
cf23dbf4e77catagen2026-10-23 06:36 UTC457
    let mut caption = None;
cf23dbf4e77catagen2026-10-23 06:36 UTC458
    let mut formatted_caption = None;
cf23dbf4e77catagen2026-10-23 06:36 UTC459
    let mut reply_to = None;
dda384a360e0atagen2026-10-25 10:27 UTC460
    let mut thread_root = None;
cf23dbf4e77catagen2026-10-23 06:36 UTC461
    let mut mentions = Vec::new();
973f8fbdaf80atagen2026-09-28 09:10 UTC462
    let mut file = None;
973f8fbdaf80atagen2026-09-28 09:10 UTC463
    loop {
973f8fbdaf80atagen2026-09-28 09:10 UTC464
        let field = match upload.next_field().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC465
            Ok(Some(field)) => field,
973f8fbdaf80atagen2026-09-28 09:10 UTC466
            Ok(None) => break,
973f8fbdaf80atagen2026-09-28 09:10 UTC467
            Err(_) => return ApiError::bad_request("invalid upload form").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC468
        };
973f8fbdaf80atagen2026-09-28 09:10 UTC469
        match field.name() {
973f8fbdaf80atagen2026-09-28 09:10 UTC470
            Some("room_id") if room_id.is_none() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC471
                room_id = match field.text().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC472
                    Ok(value) => Some(value),
973f8fbdaf80atagen2026-09-28 09:10 UTC473
                    Err(_) => return ApiError::bad_request("invalid room_id").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC474
                };
973f8fbdaf80atagen2026-09-28 09:10 UTC475
            }
cf23dbf4e77catagen2026-10-23 06:36 UTC476
            Some("caption") if caption.is_none() => {
cf23dbf4e77catagen2026-10-23 06:36 UTC477
                caption = match field.text().await {
cf23dbf4e77catagen2026-10-23 06:36 UTC478
                    Ok(value) => Some(value),
cf23dbf4e77catagen2026-10-23 06:36 UTC479
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
cf23dbf4e77catagen2026-10-23 06:36 UTC480
                };
cf23dbf4e77catagen2026-10-23 06:36 UTC481
            }
cf23dbf4e77catagen2026-10-23 06:36 UTC482
            Some("formatted_caption") if formatted_caption.is_none() => {
cf23dbf4e77catagen2026-10-23 06:36 UTC483
                formatted_caption = match field.text().await {
cf23dbf4e77catagen2026-10-23 06:36 UTC484
                    Ok(value) => Some(value),
cf23dbf4e77catagen2026-10-23 06:36 UTC485
                    Err(_) => return ApiError::bad_request("invalid caption").into_response(),
cf23dbf4e77catagen2026-10-23 06:36 UTC486
                };
cf23dbf4e77catagen2026-10-23 06:36 UTC487
            }
cf23dbf4e77catagen2026-10-23 06:36 UTC488
            Some("reply_to") if reply_to.is_none() => {
cf23dbf4e77catagen2026-10-23 06:36 UTC489
                reply_to = match field.text().await {
cf23dbf4e77catagen2026-10-23 06:36 UTC490
                    Ok(value) => Some(value),
cf23dbf4e77catagen2026-10-23 06:36 UTC491
                    Err(_) => return ApiError::bad_request("invalid reply_to").into_response(),
cf23dbf4e77catagen2026-10-23 06:36 UTC492
                };
cf23dbf4e77catagen2026-10-23 06:36 UTC493
            }
dda384a360e0atagen2026-10-25 10:27 UTC494
            Some("thread_root") if thread_root.is_none() => {
dda384a360e0atagen2026-10-25 10:27 UTC495
                thread_root = match field.text().await {
dda384a360e0atagen2026-10-25 10:27 UTC496
                    Ok(value) => Some(value),
dda384a360e0atagen2026-10-25 10:27 UTC497
                    Err(_) => return ApiError::bad_request("invalid thread_root").into_response(),
dda384a360e0atagen2026-10-25 10:27 UTC498
                };
dda384a360e0atagen2026-10-25 10:27 UTC499
            }
cf23dbf4e77catagen2026-10-23 06:36 UTC500
            Some("mentions") => match field.text().await {
cf23dbf4e77catagen2026-10-23 06:36 UTC501
                Ok(value) => mentions.push(value),
cf23dbf4e77catagen2026-10-23 06:36 UTC502
                Err(_) => return ApiError::bad_request("invalid mention").into_response(),
cf23dbf4e77catagen2026-10-23 06:36 UTC503
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC504
            Some("file") if file.is_none() => {
973f8fbdaf80atagen2026-09-28 09:10 UTC505
                let filename = field.file_name().unwrap_or("attachment").to_owned();
973f8fbdaf80atagen2026-09-28 09:10 UTC506
                let mimetype = field
973f8fbdaf80atagen2026-09-28 09:10 UTC507
                    .content_type()
973f8fbdaf80atagen2026-09-28 09:10 UTC508
                    .unwrap_or("application/octet-stream")
973f8fbdaf80atagen2026-09-28 09:10 UTC509
                    .to_owned();
973f8fbdaf80atagen2026-09-28 09:10 UTC510
                let bytes = match field.bytes().await {
973f8fbdaf80atagen2026-09-28 09:10 UTC511
                    Ok(bytes) => bytes,
973f8fbdaf80atagen2026-09-28 09:10 UTC512
                    Err(_) => return ApiError::bad_request("invalid file").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC513
                };
973f8fbdaf80atagen2026-09-28 09:10 UTC514
                if bytes.is_empty() || bytes.len() > 20 * 1024 * 1024 {
973f8fbdaf80atagen2026-09-28 09:10 UTC515
                    return ApiError::bad_request("file must be between 1 byte and 20 MiB")
973f8fbdaf80atagen2026-09-28 09:10 UTC516
                        .into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC517
                }
973f8fbdaf80atagen2026-09-28 09:10 UTC518
                file = Some((filename, mimetype, bytes));
973f8fbdaf80atagen2026-09-28 09:10 UTC519
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC520
            _ => return ApiError::bad_request("unexpected upload field").into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC521
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC522
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC523
    let Some(room_id) = room_id else {
973f8fbdaf80atagen2026-09-28 09:10 UTC524
        return ApiError::bad_request("room_id is required").into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC525
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC526
    let Some((filename, mimetype, bytes)) = file else {
973f8fbdaf80atagen2026-09-28 09:10 UTC527
        return ApiError::bad_request("file is required").into_response();
973f8fbdaf80atagen2026-09-28 09:10 UTC528
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC529
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC530
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC531
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC532
            &user.mirror,
cf23dbf4e77catagen2026-10-23 06:36 UTC533
            Request::Upload {
cf23dbf4e77catagen2026-10-23 06:36 UTC534
                room_id,
cf23dbf4e77catagen2026-10-23 06:36 UTC535
                filename,
cf23dbf4e77catagen2026-10-23 06:36 UTC536
                mimetype,
cf23dbf4e77catagen2026-10-23 06:36 UTC537
                caption,
cf23dbf4e77catagen2026-10-23 06:36 UTC538
                formatted_caption,
cf23dbf4e77catagen2026-10-23 06:36 UTC539
                reply_to,
cf23dbf4e77catagen2026-10-23 06:36 UTC540
                mentions,
dda384a360e0atagen2026-10-25 10:27 UTC541
                thread_root,
cf23dbf4e77catagen2026-10-23 06:36 UTC542
                bytes: bytes.to_vec(),
cf23dbf4e77catagen2026-10-23 06:36 UTC543
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC544
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC545
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC546
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC547
}
973f8fbdaf80atagen2026-09-28 09:10 UTC548
e4c64c36f105atagen2026-10-24 07:09 UTC549
/// Ops added after the per-route handlers, taken as the core request JSON. Anything
d8624c214c04atagen2026-10-24 08:24 UTC550
/// carrying bytes stays on its own route.
e4c64c36f105atagen2026-10-24 07:09 UTC551
async fn post_command(
e4c64c36f105atagen2026-10-24 07:09 UTC552
    State(state): State<AppState>,
e4c64c36f105atagen2026-10-24 07:09 UTC553
    headers: HeaderMap,
e4c64c36f105atagen2026-10-24 07:09 UTC554
    body: axum::Json<Request>,
e4c64c36f105atagen2026-10-24 07:09 UTC555
) -> Response {
e4c64c36f105atagen2026-10-24 07:09 UTC556
    let (user, _) = match auth(&state, &headers).await {
e4c64c36f105atagen2026-10-24 07:09 UTC557
        Ok(pair) => pair,
e4c64c36f105atagen2026-10-24 07:09 UTC558
        Err(error) => return error.into_response(),
e4c64c36f105atagen2026-10-24 07:09 UTC559
    };
e4c64c36f105atagen2026-10-24 07:09 UTC560
    let request = body.0;
e4c64c36f105atagen2026-10-24 07:09 UTC561
    if !matches!(
e4c64c36f105atagen2026-10-24 07:09 UTC562
        request,
ed3363baaf01atagen2026-10-25 03:09 UTC563
        Request::MentionsPage { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC564
            | Request::MentionContext { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC565
            | Request::RetryMentionContext { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC566
            | Request::SetMentionsRead { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC567
            | Request::MarkMentionsReadThrough { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC568
            | Request::RecoverMentions
4929dcd34937atagen2026-10-26 04:43 UTC569
            | Request::CreateKeyBackup
4929dcd34937atagen2026-10-26 04:43 UTC570
            | Request::EnableKeyBackup
4929dcd34937atagen2026-10-26 04:43 UTC571
            | Request::DisableKeyBackup { .. }
4929dcd34937atagen2026-10-26 04:43 UTC572
            | Request::RecoverKeyBackup { .. }
20fe182f3204atagen2026-10-26 05:50 UTC573
            | Request::EnableRecovery { .. }
20fe182f3204atagen2026-10-26 05:50 UTC574
            | Request::ResetRecoveryKey { .. }
20fe182f3204atagen2026-10-26 05:50 UTC575
            | Request::DisableRecovery
20fe182f3204atagen2026-10-26 05:50 UTC576
            | Request::VerifyRecoveryKey { .. }
249e439c27e5atagen2026-10-25 13:26 UTC577
            | Request::RetryRecentHistory { .. }
249e439c27e5atagen2026-10-25 13:26 UTC578
            | Request::EventContext { .. }
249e439c27e5atagen2026-10-25 13:26 UTC579
            | Request::EventContextBefore { .. }
249e439c27e5atagen2026-10-25 13:26 UTC580
            | Request::EventContextAfter { .. }
249e439c27e5atagen2026-10-25 13:26 UTC581
            | Request::CloseEventContext { .. }
249e439c27e5atagen2026-10-25 13:26 UTC582
            | Request::OpenThreadList { .. }
249e439c27e5atagen2026-10-25 13:26 UTC583
            | Request::PaginateThreadList { .. }
249e439c27e5atagen2026-10-25 13:26 UTC584
            | Request::CloseThreadList { .. }
249e439c27e5atagen2026-10-25 13:26 UTC585
            | Request::Search { .. }
249e439c27e5atagen2026-10-25 13:26 UTC586
            | Request::SearchMore { .. }
249e439c27e5atagen2026-10-25 13:26 UTC587
            | Request::CloseSearch { .. }
249e439c27e5atagen2026-10-25 13:26 UTC588
            | Request::SearchCoverage { .. }
43c2cc753052atagen2026-10-25 07:50 UTC589
            | Request::FillHistoryGap { .. }
ed3363baaf01atagen2026-10-25 03:09 UTC590
            | Request::Kick { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC591
            | Request::Ban { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC592
            | Request::Unban { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC593
            | Request::SetPowerLevel { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC594
            | Request::SetDisplayName { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC595
            | Request::SetAvatar { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC596
            | Request::Invite { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC597
            | Request::Direct { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC598
            | Request::Redact { .. }
7ebc2472a9e3atagen2026-10-25 09:53 UTC599
            | Request::OpenThread { .. }
7ebc2472a9e3atagen2026-10-25 09:53 UTC600
            | Request::CloseThread { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC601
            | Request::SetFavourite { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC602
            | Request::ReorderSpaces { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC603
            | Request::SetRoomDisplayName { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC604
            | Request::SetRoomAvatar { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC605
            | Request::Mute { .. }
20fe182f3204atagen2026-10-26 05:50 UTC606
            | Request::SetRoomNotificationMode { .. }
a4555a7eae2datagen2026-10-25 09:18 UTC607
            | Request::NotificationKeywords
a4555a7eae2datagen2026-10-25 09:18 UTC608
            | Request::AddNotificationKeyword { .. }
a4555a7eae2datagen2026-10-25 09:18 UTC609
            | Request::RemoveNotificationKeyword { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC610
            | Request::Ignore { .. }
7cca1d5628d1atagen2026-10-24 07:36 UTC611
            | Request::Unignore { .. }
d8624c214c04atagen2026-10-24 08:24 UTC612
            | Request::PowerLevels { .. }
77eddb6b710fatagen2026-10-24 09:32 UTC613
            | Request::OpenPinned { .. }
77eddb6b710fatagen2026-10-24 09:32 UTC614
            | Request::ClosePinned { .. }
77eddb6b710fatagen2026-10-24 09:32 UTC615
            | Request::SetPinned { .. }
d095131eec0eatagen2026-10-24 08:39 UTC616
            | Request::SendSticker { .. }
d095131eec0eatagen2026-10-24 08:39 UTC617
            | Request::CreateRoomPack { .. }
d095131eec0eatagen2026-10-24 08:39 UTC618
            | Request::DeletePack { .. }
d095131eec0eatagen2026-10-24 08:39 UTC619
            | Request::SetPackMeta { .. }
d095131eec0eatagen2026-10-24 08:39 UTC620
            | Request::PutPackImage { .. }
d095131eec0eatagen2026-10-24 08:39 UTC621
            | Request::RenamePackImage { .. }
d095131eec0eatagen2026-10-24 08:39 UTC622
            | Request::RemovePackImage { .. }
d095131eec0eatagen2026-10-24 08:39 UTC623
            | Request::SetPackEnabled { .. }
d095131eec0eatagen2026-10-24 08:39 UTC624
            | Request::ClonePackImage { .. }
d095131eec0eatagen2026-10-24 08:39 UTC625
            | Request::CloneSticker { .. }
d095131eec0eatagen2026-10-24 08:39 UTC626
            | Request::ClonePack { .. }
e4c64c36f105atagen2026-10-24 07:09 UTC627
    ) {
e4c64c36f105atagen2026-10-24 07:09 UTC628
        return ApiError::bad_request("op is not available as a command").into_response();
e4c64c36f105atagen2026-10-24 07:09 UTC629
    }
e4c64c36f105atagen2026-10-24 07:09 UTC630
    respond(dispatch(&user.handle, &user.mirror, request).await)
e4c64c36f105atagen2026-10-24 07:09 UTC631
}
e4c64c36f105atagen2026-10-24 07:09 UTC632
e4c64c36f105atagen2026-10-24 07:09 UTC633
async fn profile_avatar(
e4c64c36f105atagen2026-10-24 07:09 UTC634
    State(state): State<AppState>,
e4c64c36f105atagen2026-10-24 07:09 UTC635
    headers: HeaderMap,
e4c64c36f105atagen2026-10-24 07:09 UTC636
    bytes: axum::body::Bytes,
e4c64c36f105atagen2026-10-24 07:09 UTC637
) -> Response {
e4c64c36f105atagen2026-10-24 07:09 UTC638
    let (user, _) = match auth(&state, &headers).await {
e4c64c36f105atagen2026-10-24 07:09 UTC639
        Ok(pair) => pair,
e4c64c36f105atagen2026-10-24 07:09 UTC640
        Err(error) => return error.into_response(),
e4c64c36f105atagen2026-10-24 07:09 UTC641
    };
e4c64c36f105atagen2026-10-24 07:09 UTC642
    respond(
e4c64c36f105atagen2026-10-24 07:09 UTC643
        dispatch(
e4c64c36f105atagen2026-10-24 07:09 UTC644
            &user.handle,
e4c64c36f105atagen2026-10-24 07:09 UTC645
            &user.mirror,
e4c64c36f105atagen2026-10-24 07:09 UTC646
            Request::UploadAvatar {
e4c64c36f105atagen2026-10-24 07:09 UTC647
                bytes: bytes.to_vec(),
e4c64c36f105atagen2026-10-24 07:09 UTC648
            },
e4c64c36f105atagen2026-10-24 07:09 UTC649
        )
e4c64c36f105atagen2026-10-24 07:09 UTC650
        .await,
e4c64c36f105atagen2026-10-24 07:09 UTC651
    )
e4c64c36f105atagen2026-10-24 07:09 UTC652
}
e4c64c36f105atagen2026-10-24 07:09 UTC653
7cca1d5628d1atagen2026-10-24 07:36 UTC654
#[derive(Debug, Deserialize)]
7cca1d5628d1atagen2026-10-24 07:36 UTC655
struct RoomAvatarQuery {
7cca1d5628d1atagen2026-10-24 07:36 UTC656
    room_id: String,
7cca1d5628d1atagen2026-10-24 07:36 UTC657
}
7cca1d5628d1atagen2026-10-24 07:36 UTC658
7cca1d5628d1atagen2026-10-24 07:36 UTC659
async fn rooms_avatar(
7cca1d5628d1atagen2026-10-24 07:36 UTC660
    State(state): State<AppState>,
7cca1d5628d1atagen2026-10-24 07:36 UTC661
    headers: HeaderMap,
7cca1d5628d1atagen2026-10-24 07:36 UTC662
    Query(query): Query<RoomAvatarQuery>,
7cca1d5628d1atagen2026-10-24 07:36 UTC663
    bytes: axum::body::Bytes,
7cca1d5628d1atagen2026-10-24 07:36 UTC664
) -> Response {
7cca1d5628d1atagen2026-10-24 07:36 UTC665
    let (user, _) = match auth(&state, &headers).await {
7cca1d5628d1atagen2026-10-24 07:36 UTC666
        Ok(pair) => pair,
7cca1d5628d1atagen2026-10-24 07:36 UTC667
        Err(error) => return error.into_response(),
7cca1d5628d1atagen2026-10-24 07:36 UTC668
    };
7cca1d5628d1atagen2026-10-24 07:36 UTC669
    respond(
7cca1d5628d1atagen2026-10-24 07:36 UTC670
        dispatch(
7cca1d5628d1atagen2026-10-24 07:36 UTC671
            &user.handle,
7cca1d5628d1atagen2026-10-24 07:36 UTC672
            &user.mirror,
7cca1d5628d1atagen2026-10-24 07:36 UTC673
            Request::UploadRoomAvatar {
7cca1d5628d1atagen2026-10-24 07:36 UTC674
                room_id: query.room_id,
7cca1d5628d1atagen2026-10-24 07:36 UTC675
                bytes: bytes.to_vec(),
7cca1d5628d1atagen2026-10-24 07:36 UTC676
            },
7cca1d5628d1atagen2026-10-24 07:36 UTC677
        )
7cca1d5628d1atagen2026-10-24 07:36 UTC678
        .await,
7cca1d5628d1atagen2026-10-24 07:36 UTC679
    )
7cca1d5628d1atagen2026-10-24 07:36 UTC680
}
7cca1d5628d1atagen2026-10-24 07:36 UTC681
d095131eec0eatagen2026-10-24 08:39 UTC682
async fn media_upload(
d095131eec0eatagen2026-10-24 08:39 UTC683
    State(state): State<AppState>,
d095131eec0eatagen2026-10-24 08:39 UTC684
    headers: HeaderMap,
d095131eec0eatagen2026-10-24 08:39 UTC685
    bytes: axum::body::Bytes,
d095131eec0eatagen2026-10-24 08:39 UTC686
) -> Response {
d095131eec0eatagen2026-10-24 08:39 UTC687
    let (user, _) = match auth(&state, &headers).await {
d095131eec0eatagen2026-10-24 08:39 UTC688
        Ok(pair) => pair,
d095131eec0eatagen2026-10-24 08:39 UTC689
        Err(error) => return error.into_response(),
d095131eec0eatagen2026-10-24 08:39 UTC690
    };
d095131eec0eatagen2026-10-24 08:39 UTC691
    respond(
d095131eec0eatagen2026-10-24 08:39 UTC692
        dispatch(
d095131eec0eatagen2026-10-24 08:39 UTC693
            &user.handle,
d095131eec0eatagen2026-10-24 08:39 UTC694
            &user.mirror,
d095131eec0eatagen2026-10-24 08:39 UTC695
            Request::UploadMedia {
d095131eec0eatagen2026-10-24 08:39 UTC696
                bytes: bytes.to_vec(),
d095131eec0eatagen2026-10-24 08:39 UTC697
            },
d095131eec0eatagen2026-10-24 08:39 UTC698
        )
d095131eec0eatagen2026-10-24 08:39 UTC699
        .await,
d095131eec0eatagen2026-10-24 08:39 UTC700
    )
d095131eec0eatagen2026-10-24 08:39 UTC701
}
d095131eec0eatagen2026-10-24 08:39 UTC702
973f8fbdaf80atagen2026-09-28 09:10 UTC703
async fn rooms_paginate(
973f8fbdaf80atagen2026-09-28 09:10 UTC704
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC705
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC706
    body: axum::Json<PaginateBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC707
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC708
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC709
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC710
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC711
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC712
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC713
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC714
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC715
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC716
            Request::Paginate {
973f8fbdaf80atagen2026-09-28 09:10 UTC717
                room_id: body.0.room_id,
7ebc2472a9e3atagen2026-10-25 09:53 UTC718
                thread_root: body.0.thread_root,
973f8fbdaf80atagen2026-09-28 09:10 UTC719
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC720
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC721
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC722
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC723
}
973f8fbdaf80atagen2026-09-28 09:10 UTC724
973f8fbdaf80atagen2026-09-28 09:10 UTC725
async fn rooms_join(
973f8fbdaf80atagen2026-09-28 09:10 UTC726
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC727
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC728
    body: axum::Json<JoinBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC729
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC730
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC731
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC732
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC733
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC734
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC735
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC736
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC737
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC738
            Request::Join { room: body.0.room },
973f8fbdaf80atagen2026-09-28 09:10 UTC739
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC740
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC741
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC742
}
973f8fbdaf80atagen2026-09-28 09:10 UTC743
973f8fbdaf80atagen2026-09-28 09:10 UTC744
async fn rooms_create(
973f8fbdaf80atagen2026-09-28 09:10 UTC745
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC746
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC747
    body: axum::Json<CreateBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC748
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC749
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC750
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC751
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC752
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC753
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC754
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC755
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC756
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC757
            Request::Create {
973f8fbdaf80atagen2026-09-28 09:10 UTC758
                name: body.0.name,
973f8fbdaf80atagen2026-09-28 09:10 UTC759
                topic: body.0.topic,
973f8fbdaf80atagen2026-09-28 09:10 UTC760
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC761
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC762
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC763
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC764
}
973f8fbdaf80atagen2026-09-28 09:10 UTC765
973f8fbdaf80atagen2026-09-28 09:10 UTC766
async fn rooms_leave(
973f8fbdaf80atagen2026-09-28 09:10 UTC767
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC768
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC769
    body: axum::Json<RoomIdBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC770
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC771
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC772
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC773
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC774
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC775
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC776
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC777
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC778
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC779
            Request::Leave {
973f8fbdaf80atagen2026-09-28 09:10 UTC780
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC781
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC782
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC783
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC784
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC785
}
973f8fbdaf80atagen2026-09-28 09:10 UTC786
cf23dbf4e77catagen2026-10-23 06:36 UTC787
async fn rooms_edit(
cf23dbf4e77catagen2026-10-23 06:36 UTC788
    State(state): State<AppState>,
cf23dbf4e77catagen2026-10-23 06:36 UTC789
    headers: HeaderMap,
cf23dbf4e77catagen2026-10-23 06:36 UTC790
    body: axum::Json<EditBody>,
cf23dbf4e77catagen2026-10-23 06:36 UTC791
) -> Response {
cf23dbf4e77catagen2026-10-23 06:36 UTC792
    let (user, _) = match auth(&state, &headers).await {
cf23dbf4e77catagen2026-10-23 06:36 UTC793
        Ok(pair) => pair,
cf23dbf4e77catagen2026-10-23 06:36 UTC794
        Err(error) => return error.into_response(),
cf23dbf4e77catagen2026-10-23 06:36 UTC795
    };
cf23dbf4e77catagen2026-10-23 06:36 UTC796
    respond(
cf23dbf4e77catagen2026-10-23 06:36 UTC797
        dispatch(
cf23dbf4e77catagen2026-10-23 06:36 UTC798
            &user.handle,
cf23dbf4e77catagen2026-10-23 06:36 UTC799
            &user.mirror,
cf23dbf4e77catagen2026-10-23 06:36 UTC800
            Request::Edit {
cf23dbf4e77catagen2026-10-23 06:36 UTC801
                room_id: body.0.room_id,
cf23dbf4e77catagen2026-10-23 06:36 UTC802
                event_id: body.0.event_id,
cf23dbf4e77catagen2026-10-23 06:36 UTC803
                body: body.0.body,
cf23dbf4e77catagen2026-10-23 06:36 UTC804
                formatted_body: body.0.formatted_body,
cf23dbf4e77catagen2026-10-23 06:36 UTC805
                mentions: body.0.mentions,
7ebc2472a9e3atagen2026-10-25 09:53 UTC806
                thread_root: body.0.thread_root,
cf23dbf4e77catagen2026-10-23 06:36 UTC807
            },
cf23dbf4e77catagen2026-10-23 06:36 UTC808
        )
cf23dbf4e77catagen2026-10-23 06:36 UTC809
        .await,
cf23dbf4e77catagen2026-10-23 06:36 UTC810
    )
cf23dbf4e77catagen2026-10-23 06:36 UTC811
}
cf23dbf4e77catagen2026-10-23 06:36 UTC812
973f8fbdaf80atagen2026-09-28 09:10 UTC813
async fn rooms_react(
973f8fbdaf80atagen2026-09-28 09:10 UTC814
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC815
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC816
    body: axum::Json<ReactBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC817
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC818
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC819
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC820
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC821
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC822
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC823
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC824
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC825
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC826
            Request::React {
973f8fbdaf80atagen2026-09-28 09:10 UTC827
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC828
                event_id: body.0.event_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC829
                key: body.0.key,
ecb8598b38afatagen2026-10-22 09:19 UTC830
                shortcode: body.0.shortcode,
7ebc2472a9e3atagen2026-10-25 09:53 UTC831
                thread_root: body.0.thread_root,
973f8fbdaf80atagen2026-09-28 09:10 UTC832
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC833
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC834
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC835
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC836
}
973f8fbdaf80atagen2026-09-28 09:10 UTC837
973f8fbdaf80atagen2026-09-28 09:10 UTC838
async fn rooms_read(
973f8fbdaf80atagen2026-09-28 09:10 UTC839
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC840
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC841
    body: axum::Json<ReadBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC842
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC843
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC844
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC845
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC846
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC847
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC848
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC849
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC850
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC851
            Request::Read {
973f8fbdaf80atagen2026-09-28 09:10 UTC852
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC853
                event_id: body.0.event_id,
7ebc2472a9e3atagen2026-10-25 09:53 UTC854
                thread_root: body.0.thread_root,
973f8fbdaf80atagen2026-09-28 09:10 UTC855
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC856
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC857
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC858
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC859
}
973f8fbdaf80atagen2026-09-28 09:10 UTC860
973f8fbdaf80atagen2026-09-28 09:10 UTC861
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC862
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC863
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC864
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC865
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC866
    respond(dispatch(&user.handle, &user.mirror, Request::Profile).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC867
}
973f8fbdaf80atagen2026-09-28 09:10 UTC868
973f8fbdaf80atagen2026-09-28 09:10 UTC869
async fn get_space_children(
973f8fbdaf80atagen2026-09-28 09:10 UTC870
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC871
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC872
    Query(query): Query<MembersQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC873
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC874
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC875
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC876
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC877
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC878
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC879
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC880
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC881
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC882
            Request::SpaceChildren {
973f8fbdaf80atagen2026-09-28 09:10 UTC883
                room_id: query.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC884
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC885
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC886
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC887
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC888
}
973f8fbdaf80atagen2026-09-28 09:10 UTC889
973f8fbdaf80atagen2026-09-28 09:10 UTC890
async fn get_media(
973f8fbdaf80atagen2026-09-28 09:10 UTC891
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC892
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC893
    Query(query): Query<MediaQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC894
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC895
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC896
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC897
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC898
    };
7643577c76bbatagen2026-10-25 07:37 UTC899
    let request = Request::Avatar {
7643577c76bbatagen2026-10-25 07:37 UTC900
        mxc: query.mxc,
7643577c76bbatagen2026-10-25 07:37 UTC901
        size: query.size.unwrap_or(malice_core::media::AVATAR_SIZE),
7643577c76bbatagen2026-10-25 07:37 UTC902
    };
7643577c76bbatagen2026-10-25 07:37 UTC903
    match dispatch(&user.handle, &user.mirror, request).await {
7643577c76bbatagen2026-10-25 07:37 UTC904
        Ok(Reply::Avatar(source)) => {
7643577c76bbatagen2026-10-25 07:37 UTC905
            match tokio::task::spawn_blocking(move || source.render()).await {
7643577c76bbatagen2026-10-25 07:37 UTC906
                Ok((bytes, content_type)) => avatar_response(bytes, content_type),
7643577c76bbatagen2026-10-25 07:37 UTC907
                Err(_) => ApiError::new(
7643577c76bbatagen2026-10-25 07:37 UTC908
                    StatusCode::INTERNAL_SERVER_ERROR,
7643577c76bbatagen2026-10-25 07:37 UTC909
                    "avatar could not be rendered",
7643577c76bbatagen2026-10-25 07:37 UTC910
                )
7643577c76bbatagen2026-10-25 07:37 UTC911
                .into_response(),
7643577c76bbatagen2026-10-25 07:37 UTC912
            }
7643577c76bbatagen2026-10-25 07:37 UTC913
        }
7643577c76bbatagen2026-10-25 07:37 UTC914
        other => respond(other),
7643577c76bbatagen2026-10-25 07:37 UTC915
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC916
}
973f8fbdaf80atagen2026-09-28 09:10 UTC917
973f8fbdaf80atagen2026-09-28 09:10 UTC918
async fn get_message_media(
973f8fbdaf80atagen2026-09-28 09:10 UTC919
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC920
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC921
    Query(query): Query<MessageMediaQuery>,
973f8fbdaf80atagen2026-09-28 09:10 UTC922
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC923
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC924
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC925
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC926
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC927
    // the declared mimetype is the sender's claim, so the bytes get sniffed instead
973f8fbdaf80atagen2026-09-28 09:10 UTC928
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC929
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC930
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC931
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC932
            Request::Media {
973f8fbdaf80atagen2026-09-28 09:10 UTC933
                media_id: query.media_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC934
                size: query.size,
973f8fbdaf80atagen2026-09-28 09:10 UTC935
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC936
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC937
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC938
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC939
}
973f8fbdaf80atagen2026-09-28 09:10 UTC940
973f8fbdaf80atagen2026-09-28 09:10 UTC941
async fn get_encryption(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC942
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC943
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC944
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC945
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC946
    respond(dispatch(&user.handle, &user.mirror, Request::Encryption).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC947
}
973f8fbdaf80atagen2026-09-28 09:10 UTC948
973f8fbdaf80atagen2026-09-28 09:10 UTC949
async fn recover_encryption(
973f8fbdaf80atagen2026-09-28 09:10 UTC950
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC951
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC952
    body: axum::Json<RecoverBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC953
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC954
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC955
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC956
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC957
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC958
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC959
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC960
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC961
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC962
            Request::Recover {
973f8fbdaf80atagen2026-09-28 09:10 UTC963
                recovery_key: body.0.recovery_key,
973f8fbdaf80atagen2026-09-28 09:10 UTC964
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC965
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC966
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC967
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC968
}
973f8fbdaf80atagen2026-09-28 09:10 UTC969
973f8fbdaf80atagen2026-09-28 09:10 UTC970
async fn start_verification(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC971
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC972
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC973
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC974
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC975
    respond(dispatch(&user.handle, &user.mirror, Request::StartVerification).await)
973f8fbdaf80atagen2026-09-28 09:10 UTC976
}
973f8fbdaf80atagen2026-09-28 09:10 UTC977
973f8fbdaf80atagen2026-09-28 09:10 UTC978
async fn verify_encryption(
973f8fbdaf80atagen2026-09-28 09:10 UTC979
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC980
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC981
    body: axum::Json<VerifyBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC982
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC983
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC984
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC985
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC986
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC987
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC988
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC989
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC990
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC991
            Request::Verify {
973f8fbdaf80atagen2026-09-28 09:10 UTC992
                action: body.0.action,
973f8fbdaf80atagen2026-09-28 09:10 UTC993
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC994
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC995
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC996
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC997
}
973f8fbdaf80atagen2026-09-28 09:10 UTC998
973f8fbdaf80atagen2026-09-28 09:10 UTC999
async fn rooms_typing(
973f8fbdaf80atagen2026-09-28 09:10 UTC1000
    State(state): State<AppState>,
973f8fbdaf80atagen2026-09-28 09:10 UTC1001
    headers: HeaderMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC1002
    body: axum::Json<TypingBody>,
973f8fbdaf80atagen2026-09-28 09:10 UTC1003
) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC1004
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC1005
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC1006
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC1007
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC1008
    respond(
973f8fbdaf80atagen2026-09-28 09:10 UTC1009
        dispatch(
973f8fbdaf80atagen2026-09-28 09:10 UTC1010
            &user.handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC1011
            &user.mirror,
973f8fbdaf80atagen2026-09-28 09:10 UTC1012
            Request::Typing {
973f8fbdaf80atagen2026-09-28 09:10 UTC1013
                room_id: body.0.room_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC1014
                typing: body.0.typing,
973f8fbdaf80atagen2026-09-28 09:10 UTC1015
            },
973f8fbdaf80atagen2026-09-28 09:10 UTC1016
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC1017
        .await,
973f8fbdaf80atagen2026-09-28 09:10 UTC1018
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC1019
}
973f8fbdaf80atagen2026-09-28 09:10 UTC1020
544a170ead2catagen2026-10-20 11:47 UTC1021
async fn get_account_data(
544a170ead2catagen2026-10-20 11:47 UTC1022
    State(state): State<AppState>,
544a170ead2catagen2026-10-20 11:47 UTC1023
    headers: HeaderMap,
544a170ead2catagen2026-10-20 11:47 UTC1024
    Query(query): Query<AccountDataQuery>,
544a170ead2catagen2026-10-20 11:47 UTC1025
) -> Response {
544a170ead2catagen2026-10-20 11:47 UTC1026
    let (user, _) = match auth(&state, &headers).await {
544a170ead2catagen2026-10-20 11:47 UTC1027
        Ok(pair) => pair,
544a170ead2catagen2026-10-20 11:47 UTC1028
        Err(error) => return error.into_response(),
544a170ead2catagen2026-10-20 11:47 UTC1029
    };
544a170ead2catagen2026-10-20 11:47 UTC1030
    respond(
544a170ead2catagen2026-10-20 11:47 UTC1031
        dispatch(
544a170ead2catagen2026-10-20 11:47 UTC1032
            &user.handle,
544a170ead2catagen2026-10-20 11:47 UTC1033
            &user.mirror,
544a170ead2catagen2026-10-20 11:47 UTC1034
            Request::GetAccountData {
544a170ead2catagen2026-10-20 11:47 UTC1035
                event_type: query.event_type,
544a170ead2catagen2026-10-20 11:47 UTC1036
            },
544a170ead2catagen2026-10-20 11:47 UTC1037
        )
544a170ead2catagen2026-10-20 11:47 UTC1038
        .await,
544a170ead2catagen2026-10-20 11:47 UTC1039
    )
544a170ead2catagen2026-10-20 11:47 UTC1040
}
544a170ead2catagen2026-10-20 11:47 UTC1041
ecb8598b38afatagen2026-10-22 09:19 UTC1042
async fn get_url_preview(
ecb8598b38afatagen2026-10-22 09:19 UTC1043
    State(state): State<AppState>,
ecb8598b38afatagen2026-10-22 09:19 UTC1044
    headers: HeaderMap,
ecb8598b38afatagen2026-10-22 09:19 UTC1045
    Query(query): Query<UrlPreviewQuery>,
ecb8598b38afatagen2026-10-22 09:19 UTC1046
) -> Response {
ecb8598b38afatagen2026-10-22 09:19 UTC1047
    let (user, _) = match auth(&state, &headers).await {
ecb8598b38afatagen2026-10-22 09:19 UTC1048
        Ok(pair) => pair,
ecb8598b38afatagen2026-10-22 09:19 UTC1049
        Err(error) => return error.into_response(),
ecb8598b38afatagen2026-10-22 09:19 UTC1050
    };
ecb8598b38afatagen2026-10-22 09:19 UTC1051
    respond(
ecb8598b38afatagen2026-10-22 09:19 UTC1052
        dispatch(
ecb8598b38afatagen2026-10-22 09:19 UTC1053
            &user.handle,
ecb8598b38afatagen2026-10-22 09:19 UTC1054
            &user.mirror,
ecb8598b38afatagen2026-10-22 09:19 UTC1055
            Request::UrlPreview {
ecb8598b38afatagen2026-10-22 09:19 UTC1056
                room_id: query.room_id,
ecb8598b38afatagen2026-10-22 09:19 UTC1057
                url: query.url,
ecb8598b38afatagen2026-10-22 09:19 UTC1058
            },
ecb8598b38afatagen2026-10-22 09:19 UTC1059
        )
ecb8598b38afatagen2026-10-22 09:19 UTC1060
        .await,
ecb8598b38afatagen2026-10-22 09:19 UTC1061
    )
ecb8598b38afatagen2026-10-22 09:19 UTC1062
}
ecb8598b38afatagen2026-10-22 09:19 UTC1063
544a170ead2catagen2026-10-20 11:47 UTC1064
async fn set_account_data(
544a170ead2catagen2026-10-20 11:47 UTC1065
    State(state): State<AppState>,
544a170ead2catagen2026-10-20 11:47 UTC1066
    headers: HeaderMap,
544a170ead2catagen2026-10-20 11:47 UTC1067
    body: axum::Json<AccountDataBody>,
544a170ead2catagen2026-10-20 11:47 UTC1068
) -> Response {
544a170ead2catagen2026-10-20 11:47 UTC1069
    let (user, _) = match auth(&state, &headers).await {
544a170ead2catagen2026-10-20 11:47 UTC1070
        Ok(pair) => pair,
544a170ead2catagen2026-10-20 11:47 UTC1071
        Err(error) => return error.into_response(),
544a170ead2catagen2026-10-20 11:47 UTC1072
    };
544a170ead2catagen2026-10-20 11:47 UTC1073
    let body = body.0;
544a170ead2catagen2026-10-20 11:47 UTC1074
    respond(
544a170ead2catagen2026-10-20 11:47 UTC1075
        dispatch(
544a170ead2catagen2026-10-20 11:47 UTC1076
            &user.handle,
544a170ead2catagen2026-10-20 11:47 UTC1077
            &user.mirror,
544a170ead2catagen2026-10-20 11:47 UTC1078
            Request::SetAccountData {
544a170ead2catagen2026-10-20 11:47 UTC1079
                event_type: body.event_type,
544a170ead2catagen2026-10-20 11:47 UTC1080
                content: body.content,
544a170ead2catagen2026-10-20 11:47 UTC1081
            },
544a170ead2catagen2026-10-20 11:47 UTC1082
        )
544a170ead2catagen2026-10-20 11:47 UTC1083
        .await,
544a170ead2catagen2026-10-20 11:47 UTC1084
    )
544a170ead2catagen2026-10-20 11:47 UTC1085
}
544a170ead2catagen2026-10-20 11:47 UTC1086
973f8fbdaf80atagen2026-09-28 09:10 UTC1087
async fn get_events(State(state): State<AppState>, headers: HeaderMap) -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC1088
    let (user, _) = match auth(&state, &headers).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC1089
        Ok(pair) => pair,
973f8fbdaf80atagen2026-09-28 09:10 UTC1090
        Err(error) => return error.into_response(),
973f8fbdaf80atagen2026-09-28 09:10 UTC1091
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC1092
    let body = subscribe(&user.mirror).map(|event| {
973f8fbdaf80atagen2026-09-28 09:10 UTC1093
        let json = serde_json::to_string(&event).unwrap_or_else(|_| "{\"type\":\"error\"}".into());
973f8fbdaf80atagen2026-09-28 09:10 UTC1094
        Ok::<_, std::convert::Infallible>(SseEvent::default().data(json))
973f8fbdaf80atagen2026-09-28 09:10 UTC1095
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC1096
    Sse::new(body).into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC1097
}
973f8fbdaf80atagen2026-09-28 09:10 UTC1098
973f8fbdaf80atagen2026-09-28 09:10 UTC1099
async fn get_health() -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC1100
    json_ok(&serde_json::json!({ "status": "ok" }), None)
973f8fbdaf80atagen2026-09-28 09:10 UTC1101
}
973f8fbdaf80atagen2026-09-28 09:10 UTC1102
973f8fbdaf80atagen2026-09-28 09:10 UTC1103
async fn static_shell() -> Response {
973f8fbdaf80atagen2026-09-28 09:10 UTC1104
    (
973f8fbdaf80atagen2026-09-28 09:10 UTC1105
        StatusCode::SERVICE_UNAVAILABLE,
973f8fbdaf80atagen2026-09-28 09:10 UTC1106
        axum::Json(serde_json::json!({ "message": "frontend assets missing; run npm --prefix web run build or use the Nix package" })),
973f8fbdaf80atagen2026-09-28 09:10 UTC1107
    )
973f8fbdaf80atagen2026-09-28 09:10 UTC1108
        .into_response()
973f8fbdaf80atagen2026-09-28 09:10 UTC1109
}
973f8fbdaf80atagen2026-09-28 09:10 UTC1110
973f8fbdaf80atagen2026-09-28 09:10 UTC1111
pub fn router(state: AppState, web_dir: Option<String>) -> Router {
973f8fbdaf80atagen2026-09-28 09:10 UTC1112
    let api = Router::new()
973f8fbdaf80atagen2026-09-28 09:10 UTC1113
        .route("/session", get(get_session))
973f8fbdaf80atagen2026-09-28 09:10 UTC1114
        .route("/login", post(post_login))
973f8fbdaf80atagen2026-09-28 09:10 UTC1115
        .route("/logout", post(post_logout))
973f8fbdaf80atagen2026-09-28 09:10 UTC1116
        .route("/rooms", get(get_rooms))
973f8fbdaf80atagen2026-09-28 09:10 UTC1117
        .route("/events", get(get_events))
973f8fbdaf80atagen2026-09-28 09:10 UTC1118
        .route("/health", get(get_health))
973f8fbdaf80atagen2026-09-28 09:10 UTC1119
        .route("/rooms/open", post(rooms_open))
973f8fbdaf80atagen2026-09-28 09:10 UTC1120
        .route("/rooms/close", post(rooms_close))
973f8fbdaf80atagen2026-09-28 09:10 UTC1121
        .route("/rooms/send", post(rooms_send))
973f8fbdaf80atagen2026-09-28 09:10 UTC1122
        .route("/rooms/paginate", post(rooms_paginate))
973f8fbdaf80atagen2026-09-28 09:10 UTC1123
        .route(
973f8fbdaf80atagen2026-09-28 09:10 UTC1124
            "/rooms/upload",
973f8fbdaf80atagen2026-09-28 09:10 UTC1125
            post(rooms_upload).layer(DefaultBodyLimit::max(21 * 1024 * 1024)),
973f8fbdaf80atagen2026-09-28 09:10 UTC1126
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC1127
        .route("/rooms/join", post(rooms_join))
973f8fbdaf80atagen2026-09-28 09:10 UTC1128
        .route("/rooms/create", post(rooms_create))
973f8fbdaf80atagen2026-09-28 09:10 UTC1129
        .route("/rooms/leave", post(rooms_leave))
973f8fbdaf80atagen2026-09-28 09:10 UTC1130
        .route("/rooms/react", post(rooms_react))
cf23dbf4e77catagen2026-10-23 06:36 UTC1131
        .route("/rooms/edit", post(rooms_edit))
973f8fbdaf80atagen2026-09-28 09:10 UTC1132
        .route("/rooms/read", post(rooms_read))
973f8fbdaf80atagen2026-09-28 09:10 UTC1133
        .route("/rooms/typing", post(rooms_typing))
544a170ead2catagen2026-10-20 11:47 UTC1134
        .route(
544a170ead2catagen2026-10-20 11:47 UTC1135
            "/account-data",
544a170ead2catagen2026-10-20 11:47 UTC1136
            get(get_account_data).post(set_account_data),
544a170ead2catagen2026-10-20 11:47 UTC1137
        )
ecb8598b38afatagen2026-10-22 09:19 UTC1138
        .route("/url-preview", get(get_url_preview))
973f8fbdaf80atagen2026-09-28 09:10 UTC1139
        .route("/profile", get(get_profile))
e4c64c36f105atagen2026-10-24 07:09 UTC1140
        .route(
e4c64c36f105atagen2026-10-24 07:09 UTC1141
            "/profile/avatar",
e4c64c36f105atagen2026-10-24 07:09 UTC1142
            post(profile_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
e4c64c36f105atagen2026-10-24 07:09 UTC1143
        )
e4c64c36f105atagen2026-10-24 07:09 UTC1144
        .route("/command", post(post_command))
7cca1d5628d1atagen2026-10-24 07:36 UTC1145
        .route(
7cca1d5628d1atagen2026-10-24 07:36 UTC1146
            "/rooms/avatar",
7cca1d5628d1atagen2026-10-24 07:36 UTC1147
            post(rooms_avatar).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
7cca1d5628d1atagen2026-10-24 07:36 UTC1148
        )
d095131eec0eatagen2026-10-24 08:39 UTC1149
        .route(
d095131eec0eatagen2026-10-24 08:39 UTC1150
            "/media/upload",
d095131eec0eatagen2026-10-24 08:39 UTC1151
            post(media_upload).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
d095131eec0eatagen2026-10-24 08:39 UTC1152
        )
973f8fbdaf80atagen2026-09-28 09:10 UTC1153
        .route("/media", get(get_media))
973f8fbdaf80atagen2026-09-28 09:10 UTC1154
        .route("/message-media", get(get_message_media))
973f8fbdaf80atagen2026-09-28 09:10 UTC1155
        .route("/spaces/children", get(get_space_children))
973f8fbdaf80atagen2026-09-28 09:10 UTC1156
        .route("/rooms/members", get(get_members))
973f8fbdaf80atagen2026-09-28 09:10 UTC1157
        .route("/encryption", get(get_encryption))
973f8fbdaf80atagen2026-09-28 09:10 UTC1158
        .route("/encryption/recover", post(recover_encryption))
973f8fbdaf80atagen2026-09-28 09:10 UTC1159
        .route("/encryption/start", post(start_verification))
aa3dce5114d0atagen2026-10-26 06:09 UTC1160
        .route("/encryption/verify", post(verify_encryption))
aa3dce5114d0atagen2026-10-26 06:09 UTC1161
        .merge(crate::push::api_routes());
54e1929170a7atagen2026-10-26 03:48 UTC1162
    let compression = CompressionLayer::new().compress_when(
54e1929170a7atagen2026-10-26 03:48 UTC1163
        DefaultPredicate::new()
54e1929170a7atagen2026-10-26 03:48 UTC1164
            .and(NotForContentType::const_new("video/"))
54e1929170a7atagen2026-10-26 03:48 UTC1165
            .and(NotForContentType::const_new("audio/"))
54e1929170a7atagen2026-10-26 03:48 UTC1166
            .and(NotForContentType::const_new("application/octet-stream")),
54e1929170a7atagen2026-10-26 03:48 UTC1167
    );
973f8fbdaf80atagen2026-09-28 09:10 UTC1168
    let api = Router::new()
54e1929170a7atagen2026-10-26 03:48 UTC1169
        .nest("/api", api.layer(compression))
aa3dce5114d0atagen2026-10-26 06:09 UTC1170
        .layer(middleware::from_fn(csrf_guard))
aa3dce5114d0atagen2026-10-26 06:09 UTC1171
        .merge(crate::push::notify_route());
973f8fbdaf80atagen2026-09-28 09:10 UTC1172
    let router = api.with_state(state).layer(TraceLayer::new_for_http());
973f8fbdaf80atagen2026-09-28 09:10 UTC1173
    match web_dir {
973f8fbdaf80atagen2026-09-28 09:10 UTC1174
        Some(dir) => {
54e1929170a7atagen2026-10-26 03:48 UTC1175
            let files = ServeDir::new(&dir)
54e1929170a7atagen2026-10-26 03:48 UTC1176
                .precompressed_br()
54e1929170a7atagen2026-10-26 03:48 UTC1177
                .precompressed_gzip()
54e1929170a7atagen2026-10-26 03:48 UTC1178
                .not_found_service(ServeFile::new(format!("{dir}/index.html")));
54e1929170a7atagen2026-10-26 03:48 UTC1179
            router
54e1929170a7atagen2026-10-26 03:48 UTC1180
                .fallback_service(files)
54e1929170a7atagen2026-10-26 03:48 UTC1181
                .layer(middleware::from_fn(malice_static::static_headers))
973f8fbdaf80atagen2026-09-28 09:10 UTC1182
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC1183
        None => router.fallback(static_shell),
973f8fbdaf80atagen2026-09-28 09:10 UTC1184
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC1185
}