Skip to main content

netget/server/matrix/
mod.rs

1//! Matrix homeserver, client-server API v3. Rust owns HTTP, the API's JSON shapes, access
2//! tokens, room membership and per-user `/sync` delivery (`hub.rs`); the handler decides
3//! logins (unless `user_passwords` is given), room creation, joins and every message, and
4//! speaks in rooms as its own user.
5pub mod actions;
6pub mod hub;
7
8use crate::llm::action_helper::call_llm;
9use crate::llm::actions::protocol_trait::ActionResult;
10use crate::logging::emit::Log;
11use crate::protocol::{Event, EventType, SpawnContext};
12use crate::server::{
13    accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS},
14    connection::ConnectionId,
15};
16use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
17use anyhow::Result;
18use bytes::Bytes;
19use http_body_util::{BodyExt, Full, Limited};
20use hub::Hub;
21use hyper::{
22    body::Incoming,
23    header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE},
24    server::conn::http1,
25    service::service_fn,
26    Method, Request, Response, StatusCode,
27};
28use hyper_util::rt::{TokioIo, TokioTimer};
29use serde_json::{json, Value};
30use std::{
31    collections::{HashMap, VecDeque},
32    convert::Infallible,
33    net::SocketAddr,
34    sync::{Arc, Mutex},
35    time::Duration,
36};
37use tokio::sync::Notify;
38
39pub const HEADER_TIMEOUT: Duration = Duration::from_secs(30);
40pub const BODY_TIMEOUT: Duration = Duration::from_secs(30);
41pub const MAX_HEADER_BYTES: usize = 32 * 1024;
42pub const MAX_HEADERS: usize = 64;
43/// The longest a `/sync` waits for something new, whatever `timeout` the client asks for.
44pub const MAX_SYNC_WAIT: Duration = Duration::from_secs(30);
45/// Transaction ids remembered for idempotent sends.
46pub const MAX_TXNS: usize = 10_000;
47/// `/messages` returns at most this many events per page.
48pub const MAX_PAGE: usize = 100;
49const CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/json\r\nContent-Length: 61\r\nConnection: close\r\nRetry-After: 5\r\n\r\n{\"errcode\":\"M_LIMIT_EXCEEDED\",\"error\":\"Too many connections\"}";
50
51type Reply = Response<Full<Bytes>>;
52
53/// Sent transactions: (user, txn id) → event id, and their order for eviction.
54type TxnLog = (
55    HashMap<(String, String), String>,
56    VecDeque<(String, String)>,
57);
58
59struct Shared {
60    hub: Mutex<Hub>,
61    wake: Notify,
62    passwords: Option<HashMap<String, String>>,
63    bot: String,
64    txns: Mutex<TxnLog>,
65}
66
67pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
68    let params = ctx.startup_params.as_ref();
69    let server_name = params
70        .map(|p| p.get_optional_string("server_name"))
71        .transpose()?
72        .flatten()
73        .unwrap_or_else(|| actions::DEFAULT_SERVER_NAME.to_string());
74    let bot = params
75        .map(|p| p.get_optional_string("bot_user"))
76        .transpose()?
77        .flatten()
78        .unwrap_or_else(|| actions::DEFAULT_BOT_USER.to_string());
79    let passwords = params
80        .map(|p| p.get_optional_object("user_passwords"))
81        .transpose()?
82        .flatten()
83        .map(|m| {
84            m.iter()
85                .map(|(k, v)| {
86                    let pw = v.as_str().map(str::to_string).ok_or_else(|| {
87                        anyhow::anyhow!("user_passwords: the password for {k:?} must be a string")
88                    })?;
89                    Ok((
90                        k.trim_start_matches('@')
91                            .split(':')
92                            .next()
93                            .unwrap_or(k)
94                            .to_lowercase(),
95                        pw,
96                    ))
97                })
98                .collect::<Result<HashMap<_, _>>>()
99        })
100        .transpose()?;
101    let hub = Hub::new(&server_name);
102    let bot = hub.user_id(&bot);
103    let shared = Arc::new(Shared {
104        hub: Mutex::new(hub),
105        wake: Notify::new(),
106        passwords,
107        bot,
108        txns: Mutex::new(Default::default()),
109    });
110
111    let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
112    let local = listener.local_addr()?;
113    Log::new(Some(&ctx.status_tx)).info(format!(
114        "Matrix homeserver {server_name} listening on {local}"
115    ));
116    let server_id = ctx.server_id;
117    let state = ctx.state.clone();
118    let accept = tokio::spawn(async move {
119        let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
120        loop {
121            let (stream, peer, permit) = match accept_bounded(
122                &listener,
123                &limiter,
124                CAP_REFUSAL,
125                "Matrix",
126                Some(&ctx.status_tx),
127            )
128            .await
129            {
130                Ok(v) => v,
131                Err(_) => break,
132            };
133            let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
134            let now = crate::utils::clock::Instant::now();
135            ctx.state
136                .add_connection_to_server(
137                    server_id,
138                    ConnectionState {
139                        id,
140                        remote_addr: peer,
141                        local_addr: local,
142                        bytes_sent: 0,
143                        bytes_received: 0,
144                        packets_sent: 0,
145                        packets_received: 0,
146                        last_activity: now,
147                        status: ConnectionStatus::Active,
148                        status_changed_at: now,
149                        protocol_info: ProtocolConnectionInfo::empty(),
150                    },
151                )
152                .await;
153            let child = ctx.clone();
154            let shared = shared.clone();
155            ctx.state
156                .spawn_server_task(server_id, async move {
157                    let _permit = permit;
158                    let request_ctx = child.clone();
159                    let service = service_fn(move |request| {
160                        let ctx = request_ctx.clone();
161                        let shared = shared.clone();
162                        async move { Ok::<_, Infallible>(handle(request, id, &ctx, &shared).await) }
163                    });
164                    let mut builder = http1::Builder::new();
165                    builder
166                        .timer(TokioTimer::new())
167                        .header_read_timeout(HEADER_TIMEOUT)
168                        .max_headers(MAX_HEADERS)
169                        .max_buf_size(MAX_HEADER_BYTES);
170                    if let Err(e) = builder
171                        .serve_connection(TokioIo::new(stream), service)
172                        .await
173                    {
174                        Log::new(Some(&child.status_tx))
175                            .debug(format!("Matrix connection {id} HTTP error: {e}"));
176                    }
177                    child
178                        .state
179                        .update_connection_status(server_id, id, ConnectionStatus::Closed)
180                        .await;
181                    let _ = child.status_tx.send("__UPDATE_UI__".into());
182                })
183                .await;
184        }
185    });
186    state.register_server_task(server_id, accept).await;
187    Ok(local)
188}
189
190fn reply(status: u16, body: &Value) -> Reply {
191    let mut r = Response::new(Full::new(Bytes::from(
192        serde_json::to_vec(body).unwrap_or_default(),
193    )));
194    *r.status_mut() = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
195    r.headers_mut()
196        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
197    r.headers_mut()
198        .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
199    r
200}
201
202fn error(status: u16, errcode: &str, message: &str) -> Reply {
203    reply(status, &json!({"errcode": errcode, "error": message}))
204}
205
206fn failure(e: Option<&anyhow::Error>) -> Reply {
207    let kind = e
208        .map(crate::utils::WireFailure::classify)
209        .unwrap_or(crate::utils::WireFailure::Unavailable);
210    if kind.is_overloaded() {
211        let mut r = error(503, "M_LIMIT_EXCEEDED", kind.prefixed_text());
212        r.headers_mut()
213            .insert("Retry-After", HeaderValue::from_static("5"));
214        r
215    } else {
216        error(500, "M_UNKNOWN", kind.prefixed_text())
217    }
218}
219
220fn outcome(ctx: &SpawnContext, id: ConnectionId, op: &str, decision: &str) {
221    let line = format!("Matrix connection {id} operation={op} decision={decision}");
222    let log = Log::new(Some(&ctx.status_tx));
223    if decision.starts_with("fail_closed") || decision == "model_silent" {
224        log.error(line)
225    } else {
226        log.info(line)
227    }
228}
229
230/// What the handler said about a request: allowed (with any messages to post), or refused.
231struct Decision {
232    refusal: Option<(String, String)>,
233    sends: Vec<Value>,
234}
235
236async fn ask(
237    ctx: &SpawnContext,
238    id: ConnectionId,
239    event_type: &'static EventType,
240    data: Value,
241    op: &str,
242) -> std::result::Result<Decision, Reply> {
243    let event = Event::new(event_type, data);
244    let result = match call_llm(
245        &ctx.llm_client,
246        &ctx.state,
247        ctx.server_id,
248        Some(id),
249        &event,
250        &actions::MatrixProtocol,
251    )
252    .await
253    {
254        Ok(r) => r,
255        Err(e) => {
256            outcome(ctx, id, op, "fail_closed_llm_error");
257            return Err(failure(Some(&e)));
258        }
259    };
260    if !result.failures.is_empty() {
261        outcome(ctx, id, op, "fail_closed_invalid_reply");
262        return Err(failure(None));
263    }
264    let mut accepted = false;
265    let mut refusal = None;
266    let mut sends = Vec::new();
267    let mut stack = result.protocol_results;
268    while let Some(r) = stack.pop() {
269        match r {
270            ActionResult::Custom { name, data } => match name.as_str() {
271                actions::ACCEPT => accepted = true,
272                actions::SEND => sends.push(data),
273                actions::REJECT => {
274                    refusal = Some((
275                        data["errcode"]
276                            .as_str()
277                            .unwrap_or("M_FORBIDDEN")
278                            .to_string(),
279                        data["error"]
280                            .as_str()
281                            .unwrap_or("Refused by the server")
282                            .to_string(),
283                    ))
284                }
285                _ => {}
286            },
287            ActionResult::Multiple(items) => stack.extend(items),
288            _ => {}
289        }
290    }
291    sends.reverse();
292    match (refusal, accepted || !sends.is_empty()) {
293        (Some(_), true) => {
294            outcome(ctx, id, op, "fail_closed_invalid_reply");
295            Err(failure(None))
296        }
297        (Some(r), false) => {
298            outcome(ctx, id, op, "model_reject");
299            Ok(Decision {
300                refusal: Some(r),
301                sends,
302            })
303        }
304        (None, true) => {
305            outcome(ctx, id, op, "model_answer");
306            Ok(Decision {
307                refusal: None,
308                sends,
309            })
310        }
311        (None, false) => {
312            outcome(ctx, id, op, "model_silent");
313            Err(failure(None))
314        }
315    }
316}
317
318/// Post the handler's messages as its own user, joining it to each room first.
319fn apply_sends(ctx: &SpawnContext, shared: &Shared, default_room: &str, sends: &[Value]) {
320    if sends.is_empty() {
321        return;
322    }
323    {
324        let mut hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
325        for s in sends {
326            let room = s["room_id"].as_str().unwrap_or(default_room).to_string();
327            if !hub.rooms.contains_key(&room) {
328                Log::new(Some(&ctx.status_tx)).warn(format!(
329                    "Matrix: matrix_send to unknown room {room}; dropped"
330                ));
331                continue;
332            }
333            if !hub.is_member(&room, &shared.bot) {
334                hub.join(&room, &shared.bot);
335            }
336            let kind = s["event_type"].as_str().unwrap_or("m.room.message");
337            hub.post(&room, &shared.bot, kind, actions::send_content(s), None);
338        }
339    }
340    shared.wake.notify_waiters();
341}
342
343fn query(q: Option<&str>) -> HashMap<String, String> {
344    q.unwrap_or_default()
345        .split('&')
346        .filter(|p| !p.is_empty())
347        .map(|p| {
348            let (k, v) = p.split_once('=').unwrap_or((p, ""));
349            let d = |s: &str| {
350                urlencoding::decode(&s.replace('+', " "))
351                    .map(|c| c.into_owned())
352                    .unwrap_or_default()
353            };
354            (d(k), d(v))
355        })
356        .collect()
357}
358
359fn segment(s: &str) -> String {
360    urlencoding::decode(s)
361        .map(|c| c.into_owned())
362        .unwrap_or_default()
363}
364
365async fn handle(
366    request: Request<Incoming>,
367    id: ConnectionId,
368    ctx: &SpawnContext,
369    shared: &Shared,
370) -> Reply {
371    let (parts, body) = request.into_parts();
372    let bytes = match tokio::time::timeout(
373        BODY_TIMEOUT,
374        Limited::new(body, actions::MAX_BODY_BYTES).collect(),
375    )
376    .await
377    {
378        Err(_) => return error(408, "M_UNKNOWN", "request body deadline exceeded"),
379        Ok(Err(e)) => {
380            return if e
381                .downcast_ref::<http_body_util::LengthLimitError>()
382                .is_some()
383            {
384                error(413, "M_TOO_LARGE", "Request body too large")
385            } else {
386                error(400, "M_UNKNOWN", "incomplete HTTP body")
387            }
388        }
389        Ok(Ok(b)) => b.to_bytes(),
390    };
391    ctx.state
392        .update_connection_stats(
393            ctx.server_id,
394            id,
395            Some(bytes.len() as u64),
396            None,
397            Some(1),
398            None,
399        )
400        .await;
401    let q = query(parts.uri.query());
402    let path = parts.uri.path().to_string();
403    let method = parts.method.clone();
404    if method == Method::OPTIONS {
405        let mut r = reply(200, &json!({}));
406        let h = r.headers_mut();
407        h.insert(
408            "Access-Control-Allow-Methods",
409            HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"),
410        );
411        h.insert(
412            "Access-Control-Allow-Headers",
413            HeaderValue::from_static("Authorization, Content-Type"),
414        );
415        return r;
416    }
417    if path == "/_matrix/client/versions" {
418        return reply(
419            200,
420            &json!({"versions": ["r0.6.1", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9", "v1.10", "v1.11"],
421                    "unstable_features": {}}),
422        );
423    }
424    let Some(rest) = path
425        .strip_prefix("/_matrix/client/v3/")
426        .or_else(|| path.strip_prefix("/_matrix/client/r0/"))
427    else {
428        return error(404, "M_UNRECOGNIZED", "Unrecognized request");
429    };
430    let body: Value = if bytes.is_empty() {
431        json!({})
432    } else {
433        match serde_json::from_slice(&bytes) {
434            Ok(v @ Value::Object(_)) => v,
435            _ => return error(400, "M_NOT_JSON", "Content not JSON."),
436        }
437    };
438    match (&method, rest) {
439        (&Method::GET, "login") => {
440            return reply(200, &json!({"flows": [{"type": "m.login.password"}]}))
441        }
442        (&Method::POST, "login") => return login(ctx, id, shared, &body).await,
443        _ => {}
444    }
445
446    // Everything else needs an access token.
447    let token = parts
448        .headers
449        .get(AUTHORIZATION)
450        .and_then(|v| v.to_str().ok())
451        .and_then(|v| v.strip_prefix("Bearer "))
452        .map(str::to_string)
453        .or_else(|| q.get("access_token").cloned());
454    let Some(token) = token else {
455        return error(401, "M_MISSING_TOKEN", "Missing access token");
456    };
457    let Some((user, device)) = shared
458        .hub
459        .lock()
460        .unwrap_or_else(|e| e.into_inner())
461        .whoami(&token)
462    else {
463        return error(401, "M_UNKNOWN_TOKEN", "Unrecognised access token");
464    };
465    let segs: Vec<&str> = rest.split('/').collect();
466    match (&method, segs.as_slice()) {
467        (&Method::POST, ["logout"]) => {
468            shared
469                .hub
470                .lock()
471                .unwrap_or_else(|e| e.into_inner())
472                .logout(&token);
473            reply(200, &json!({}))
474        }
475        (&Method::GET, ["account", "whoami"]) => {
476            reply(200, &json!({"user_id": user, "device_id": device}))
477        }
478        (&Method::GET, ["sync"]) => sync(shared, &user, &q).await,
479        (&Method::GET, ["joined_rooms"]) => {
480            let rooms = shared
481                .hub
482                .lock()
483                .unwrap_or_else(|e| e.into_inner())
484                .joined(&user);
485            reply(200, &json!({"joined_rooms": rooms}))
486        }
487        (&Method::POST, ["user", _, "filter"]) => reply(200, &json!({"filter_id": "0"})),
488        (&Method::GET, ["user", _, "filter", _]) => reply(200, &json!({})),
489        (&Method::POST, ["createRoom"]) => create_room(ctx, id, shared, &user, &body).await,
490        (&Method::POST, ["join", room]) | (&Method::POST, ["rooms", room, "join"]) => {
491            join(ctx, id, shared, &user, &segment(room)).await
492        }
493        (&Method::POST, ["rooms", room, "leave"]) => {
494            let room = segment(room);
495            if !shared
496                .hub
497                .lock()
498                .unwrap_or_else(|e| e.into_inner())
499                .leave(&room, &user)
500            {
501                return error(403, "M_FORBIDDEN", "You are not in this room");
502            }
503            shared.wake.notify_waiters();
504            reply(200, &json!({}))
505        }
506        (&Method::POST, ["rooms", room, "invite"]) => {
507            let room = segment(room);
508            let Some(invitee) = body["user_id"].as_str() else {
509                return error(400, "M_MISSING_PARAM", "user_id is required");
510            };
511            let mut hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
512            if !hub.is_member(&room, &user) {
513                return error(403, "M_FORBIDDEN", "You are not in this room");
514            }
515            hub.invite(&room, &user, invitee);
516            drop(hub);
517            shared.wake.notify_waiters();
518            reply(200, &json!({}))
519        }
520        (&Method::PUT, ["rooms", room, "send", kind, txn]) => {
521            send(
522                ctx,
523                id,
524                shared,
525                &user,
526                &segment(room),
527                &segment(kind),
528                &segment(txn),
529                body,
530            )
531            .await
532        }
533        (&Method::GET, ["rooms", room, "messages"]) => {
534            let room = segment(room);
535            let hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
536            if !hub.is_member(&room, &user) {
537                return error(403, "M_FORBIDDEN", "You are not in this room");
538            }
539            let limit = q
540                .get("limit")
541                .and_then(|l| l.parse::<usize>().ok())
542                .unwrap_or(10)
543                .min(MAX_PAGE);
544            let history: Vec<Value> = hub.rooms[&room].history.iter().cloned().collect();
545            let chunk: Vec<Value> = if q.get("dir").map(String::as_str) == Some("f") {
546                history.into_iter().take(limit).collect()
547            } else {
548                history.into_iter().rev().take(limit).collect()
549            };
550            reply(200, &json!({"chunk": chunk, "start": "t0", "end": "t0"}))
551        }
552        (&Method::GET, ["rooms", room, "joined_members"]) => {
553            let room = segment(room);
554            let hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
555            if !hub.is_member(&room, &user) {
556                return error(403, "M_FORBIDDEN", "You are not in this room");
557            }
558            let joined: serde_json::Map<String, Value> = hub
559                .members(&room)
560                .into_iter()
561                .map(|m| (m, json!({})))
562                .collect();
563            reply(200, &json!({"joined": joined}))
564        }
565        _ => error(404, "M_UNRECOGNIZED", "Unrecognized request"),
566    }
567}
568
569async fn login(ctx: &SpawnContext, id: ConnectionId, shared: &Shared, body: &Value) -> Reply {
570    if body["type"] != "m.login.password" {
571        return error(400, "M_UNKNOWN", "Only m.login.password is supported");
572    }
573    let user = body["identifier"]["user"]
574        .as_str()
575        .or_else(|| body["user"].as_str());
576    let (Some(user), Some(password)) = (user, body["password"].as_str()) else {
577        return error(
578            400,
579            "M_MISSING_PARAM",
580            "identifier.user and password are required",
581        );
582    };
583    let user_id = shared
584        .hub
585        .lock()
586        .unwrap_or_else(|e| e.into_inner())
587        .user_id(user);
588    let device = body["device_id"]
589        .as_str()
590        .filter(|d| !d.is_empty() && d.len() <= 64)
591        .map(str::to_string)
592        .unwrap_or_else(|| hub::random_id(10).to_uppercase());
593    match &shared.passwords {
594        Some(table) => {
595            let local = user_id
596                .trim_start_matches('@')
597                .split(':')
598                .next()
599                .unwrap_or_default();
600            if table.get(local).map(String::as_str) != Some(password) {
601                outcome(ctx, id, "login", "password_mismatch");
602                return error(403, "M_FORBIDDEN", "Invalid username or password");
603            }
604            outcome(ctx, id, "login", "password_match");
605        }
606        None => {
607            let decision = match ask(
608                ctx,
609                id,
610                &actions::LOGIN_EVENT,
611                json!({"user_id": user_id, "device_id": device}),
612                "login",
613            )
614            .await
615            {
616                Ok(d) => d,
617                Err(r) => return r,
618            };
619            if let Some((code, msg)) = decision.refusal {
620                return error(403, &code, &msg);
621            }
622        }
623    }
624    let token = shared
625        .hub
626        .lock()
627        .unwrap_or_else(|e| e.into_inner())
628        .issue_token(&user_id, &device);
629    let Some(token) = token else {
630        return error(429, "M_LIMIT_EXCEEDED", "Too many sessions");
631    };
632    let server_name = shared
633        .hub
634        .lock()
635        .unwrap_or_else(|e| e.into_inner())
636        .server_name
637        .clone();
638    reply(
639        200,
640        &json!({"user_id": user_id, "access_token": token, "device_id": device, "home_server": server_name}),
641    )
642}
643
644async fn create_room(
645    ctx: &SpawnContext,
646    id: ConnectionId,
647    shared: &Shared,
648    user: &str,
649    body: &Value,
650) -> Reply {
651    let name = body["name"].as_str().map(str::to_string);
652    let invite: Vec<String> = body["invite"]
653        .as_array()
654        .map(|a| {
655            a.iter()
656                .filter_map(Value::as_str)
657                .filter(|u| u.starts_with('@') && u.contains(':'))
658                .map(str::to_string)
659                .collect()
660        })
661        .unwrap_or_default();
662    let decision = match ask(
663        ctx,
664        id,
665        &actions::CREATE_ROOM_EVENT,
666        json!({"user_id": user, "name": name, "invite": invite}),
667        "create_room",
668    )
669    .await
670    {
671        Ok(d) => d,
672        Err(r) => return r,
673    };
674    if let Some((code, msg)) = decision.refusal {
675        return error(403, &code, &msg);
676    }
677    let room = shared
678        .hub
679        .lock()
680        .unwrap_or_else(|e| e.into_inner())
681        .create_room(user, name, &invite);
682    let Some(room) = room else {
683        return error(429, "M_LIMIT_EXCEEDED", "Too many rooms");
684    };
685    shared.wake.notify_waiters();
686    apply_sends(ctx, shared, &room, &decision.sends);
687    reply(200, &json!({"room_id": room}))
688}
689
690async fn join(
691    ctx: &SpawnContext,
692    id: ConnectionId,
693    shared: &Shared,
694    user: &str,
695    room: &str,
696) -> Reply {
697    let (exists, member, invited, name) = {
698        let hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
699        match hub.rooms.get(room) {
700            Some(r) => (
701                true,
702                r.members.contains(user),
703                r.invited.contains(user),
704                r.name.clone(),
705            ),
706            None => (false, false, false, None),
707        }
708    };
709    if !exists {
710        return error(404, "M_NOT_FOUND", "No such room");
711    }
712    if member {
713        return reply(200, &json!({"room_id": room}));
714    }
715    let decision = match ask(
716        ctx,
717        id,
718        &actions::JOIN_EVENT,
719        json!({"user_id": user, "room_id": room, "room_name": name, "invited": invited}),
720        "join",
721    )
722    .await
723    {
724        Ok(d) => d,
725        Err(r) => return r,
726    };
727    if let Some((code, msg)) = decision.refusal {
728        return error(403, &code, &msg);
729    }
730    shared
731        .hub
732        .lock()
733        .unwrap_or_else(|e| e.into_inner())
734        .join(room, user);
735    shared.wake.notify_waiters();
736    apply_sends(ctx, shared, room, &decision.sends);
737    reply(200, &json!({"room_id": room}))
738}
739
740#[allow(clippy::too_many_arguments)]
741async fn send(
742    ctx: &SpawnContext,
743    id: ConnectionId,
744    shared: &Shared,
745    user: &str,
746    room: &str,
747    kind: &str,
748    txn: &str,
749    content: Value,
750) -> Reply {
751    let key = (user.to_string(), txn.to_string());
752    if let Some(event_id) = shared
753        .txns
754        .lock()
755        .unwrap_or_else(|e| e.into_inner())
756        .0
757        .get(&key)
758    {
759        return reply(200, &json!({"event_id": event_id}));
760    }
761    let (member, name, members) = {
762        let hub = shared.hub.lock().unwrap_or_else(|e| e.into_inner());
763        (
764            hub.is_member(room, user),
765            hub.room_name(room),
766            hub.members(room),
767        )
768    };
769    if !member {
770        return error(403, "M_FORBIDDEN", "You are not in this room");
771    }
772    let decision = match ask(
773        ctx,
774        id,
775        &actions::MESSAGE_EVENT,
776        json!({"room_id": room, "room_name": name, "sender": user, "event_type": kind,
777               "content": content, "members": members}),
778        "send",
779    )
780    .await
781    {
782        Ok(d) => d,
783        Err(r) => return r,
784    };
785    if let Some((code, msg)) = decision.refusal {
786        return error(403, &code, &msg);
787    }
788    let event_id = shared
789        .hub
790        .lock()
791        .unwrap_or_else(|e| e.into_inner())
792        .post(room, user, kind, content, None);
793    let Some(event_id) = event_id else {
794        return error(404, "M_NOT_FOUND", "No such room");
795    };
796    {
797        let mut t = shared.txns.lock().unwrap_or_else(|e| e.into_inner());
798        t.0.insert(key.clone(), event_id.clone());
799        t.1.push_back(key);
800        while t.1.len() > MAX_TXNS {
801            if let Some(old) = t.1.pop_front() {
802                t.0.remove(&old);
803            }
804        }
805    }
806    shared.wake.notify_waiters();
807    apply_sends(ctx, shared, room, &decision.sends);
808    reply(200, &json!({"event_id": event_id}))
809}
810
811async fn sync(shared: &Shared, user: &str, q: &HashMap<String, String>) -> Reply {
812    let since = hub::parse_batch(q.get("since").map(String::as_str));
813    let wait = q
814        .get("timeout")
815        .and_then(|t| t.parse::<u64>().ok())
816        .map(Duration::from_millis)
817        .unwrap_or_default()
818        .min(MAX_SYNC_WAIT);
819    let deadline = tokio::time::Instant::now() + wait;
820    loop {
821        let notified = shared.wake.notified();
822        tokio::pin!(notified);
823        notified.as_mut().enable();
824        let body = shared
825            .hub
826            .lock()
827            .unwrap_or_else(|e| e.into_inner())
828            .sync(user, since);
829        if let Some(body) = body {
830            return reply(200, &body);
831        }
832        if tokio::time::timeout_at(deadline, notified).await.is_err() {
833            return reply(
834                200,
835                &json!({"next_batch": format!("s{}", since.unwrap_or(0)),
836                        "rooms": {"join": {}, "invite": {}, "leave": {}},
837                        "presence": {"events": []}, "account_data": {"events": []},
838                        "to_device": {"events": []}, "device_lists": {"changed": [], "left": []},
839                        "device_one_time_keys_count": {}}),
840            );
841        }
842    }
843}