atagen

malice

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