atagen

malice

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