1pub 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;
43pub const MAX_SYNC_WAIT: Duration = Duration::from_secs(30);
45pub const MAX_TXNS: usize = 10_000;
47pub 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
53type 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
230struct 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
318fn 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 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}