1pub mod actions;
5use crate::client::llm_budget::call_llm_for_client;
6use crate::llm::actions::client_trait::{Client, ClientActionResult};
7use crate::logging::emit::Log;
8use crate::protocol::{ConnectContext, Event};
9use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
10use crate::state::{AccessLogOwner, ClientStatus};
11pub use actions::MatrixClientProtocol;
12use anyhow::{bail, ensure, Context, Result};
13use bytes::Bytes;
14use http_body_util::{BodyExt, Full, Limited};
15use hyper::{
16 client::conn::http1,
17 header::{AUTHORIZATION, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, HOST},
18 Request, Uri,
19};
20use hyper_util::rt::TokioIo;
21use serde_json::{json, Value};
22use std::time::Duration;
23use tokio::sync::mpsc;
24
25pub const MAX_ANSWER_BYTES: usize = 4 * 1024 * 1024;
27pub const IO_TIMEOUT: Duration = Duration::from_secs(15);
29pub const SYNC_WAIT: Duration = Duration::from_secs(25);
31pub const SYNC_BACKOFF: Duration = Duration::from_secs(2);
33pub const MAX_FOLLOWUP_DEPTH: usize = 8;
35
36#[derive(Clone)]
37struct Origin {
38 authority: String,
39 connect_addr: String,
40}
41
42fn origin(value: &str) -> Result<Origin> {
43 let value = if value.starts_with("http://") {
44 value.to_string()
45 } else {
46 format!("http://{value}")
47 };
48 let uri: Uri = value.parse().context("invalid homeserver address")?;
49 ensure!(
50 matches!(uri.path(), "" | "/") && uri.query().is_none(),
51 "give the homeserver as host:port or http://host:port, without a path"
52 );
53 let authority = uri.authority().context("homeserver host required")?;
54 Ok(Origin {
55 authority: authority.as_str().to_string(),
56 connect_addr: format!(
57 "{}:{}",
58 authority.host(),
59 authority.port_u16().unwrap_or(8008)
60 ),
61 })
62}
63
64struct Answer {
65 status: u16,
66 body: Value,
67}
68
69async fn exchange(
71 origin: &Origin,
72 token: Option<&str>,
73 method: &str,
74 path: &str,
75 body: Option<&Value>,
76 deadline: Duration,
77) -> Result<Answer> {
78 tokio::time::timeout(deadline, async {
79 let socket = tokio::net::TcpStream::connect(&origin.connect_addr).await?;
80 let (mut sender, connection) = http1::Builder::new()
81 .max_headers(64)
82 .max_buf_size(64 * 1024)
83 .handshake(TokioIo::new(socket))
84 .await?;
85 let payload = body.map(|b| b.to_string()).unwrap_or_default();
86 let mut request = Request::builder()
87 .method(method)
88 .uri(format!("/_matrix/client/v3/{path}"))
89 .header(HOST, &origin.authority)
90 .header(CONNECTION, "close")
91 .header(CONTENT_LENGTH, payload.len());
92 if body.is_some() {
93 request = request.header(CONTENT_TYPE, "application/json");
94 }
95 if let Some(t) = token {
96 request = request.header(AUTHORIZATION, format!("Bearer {t}"));
97 }
98 let request = request.body(Full::new(Bytes::from(payload)))?;
99 let exchange = async {
100 let response = sender.send_request(request).await?;
101 let (parts, body) = response.into_parts();
102 let bytes = Limited::new(body, MAX_ANSWER_BYTES)
103 .collect()
104 .await
105 .map_err(|_| anyhow::anyhow!("response body exceeds 4 MiB or is incomplete"))?
106 .to_bytes();
107 Ok::<_, anyhow::Error>(Answer {
108 status: parts.status.as_u16(),
109 body: serde_json::from_slice(&bytes).unwrap_or(Value::Null),
110 })
111 };
112 tokio::pin!(connection);
113 tokio::pin!(exchange);
114 tokio::select! {
115 r = &mut exchange => r,
116 r = &mut connection => {
117 r.context("HTTP connection failed")?;
118 exchange.await
119 }
120 }
121 })
122 .await
123 .context("Matrix request deadline exceeded")?
124}
125
126fn matrix_error(answer: &Answer) -> (String, String) {
127 (
128 answer.body["errcode"]
129 .as_str()
130 .unwrap_or("M_UNKNOWN")
131 .to_string(),
132 crate::utils::truncate::truncate_for_log(
133 answer.body["error"].as_str().unwrap_or_default(),
134 512,
135 )
136 .to_string(),
137 )
138}
139
140struct Login {
141 user_id: String,
142 device_id: String,
143 token: String,
144}
145
146async fn login(origin: &Origin, ctx: &ConnectContext) -> Result<Login> {
147 let params = ctx
148 .startup_params
149 .as_ref()
150 .context("startup_params user and password are required")?;
151 let user = params.get_string("user")?;
152 let password = params.get_string("password")?;
153 let device = params.get_optional_string("device_id")?;
154 let mut body = json!({"type": "m.login.password", "identifier": {"type": "m.id.user", "user": user},
155 "password": password, "initial_device_display_name": "NetGet"});
156 if let Some(d) = device {
157 body["device_id"] = json!(d);
158 }
159 let answer = exchange(origin, None, "POST", "login", Some(&body), IO_TIMEOUT).await?;
160 if answer.status != 200 {
161 let (code, error) = matrix_error(&answer);
162 bail!("login refused: {} {code} {error}", answer.status);
163 }
164 let field = |k: &str| -> Result<String> {
165 answer.body[k]
166 .as_str()
167 .map(str::to_string)
168 .with_context(|| format!("login answer has no {k}"))
169 };
170 Ok(Login {
171 user_id: field("user_id")?,
172 device_id: field("device_id")?,
173 token: field("access_token")?,
174 })
175}
176
177fn sync_events(body: &Value, me: &str, first: bool) -> Vec<Event> {
180 let mut out = Vec::new();
181 if let Some(invites) = body["rooms"]["invite"].as_object() {
182 for (room, data) in invites {
183 let state = data["invite_state"]["events"]
184 .as_array()
185 .cloned()
186 .unwrap_or_default();
187 let sender = state
188 .iter()
189 .find(|e| e["type"] == "m.room.member" && e["state_key"] == me)
190 .and_then(|e| e["sender"].as_str())
191 .map(str::to_string);
192 let name = state
193 .iter()
194 .find(|e| e["type"] == "m.room.name")
195 .and_then(|e| e["content"]["name"].as_str())
196 .map(str::to_string);
197 out.push(Event::new(
198 &actions::INVITE_EVENT,
199 json!({"room_id": room, "sender": sender, "room_name": name}),
200 ));
201 }
202 }
203 if first {
204 return out;
205 }
206 if let Some(joined) = body["rooms"]["join"].as_object() {
207 for (room, data) in joined {
208 for e in data["timeline"]["events"].as_array().into_iter().flatten() {
209 let kind = e["type"].as_str().unwrap_or_default();
210 if kind == "m.room.member" {
211 let user = e["state_key"].as_str().unwrap_or_default();
212 if user != me {
213 out.push(Event::new(
214 &actions::MEMBER_EVENT,
215 json!({"room_id": room, "user_id": user,
216 "membership": e["content"]["membership"]}),
217 ));
218 }
219 continue;
220 }
221 if e.get("state_key").is_some() || e["sender"] == me {
222 continue;
223 }
224 out.push(Event::new(
225 &actions::MESSAGE_EVENT,
226 json!({"room_id": room, "sender": e["sender"], "event_type": kind,
227 "content": e["content"], "event_id": e["event_id"]}),
228 ));
229 }
230 }
231 }
232 out
233}
234
235pub async fn connect(ctx: ConnectContext) -> Result<std::net::SocketAddr> {
236 let origin = origin(&ctx.remote_addr)?;
237 let login = login(&origin, &ctx).await?;
238 let first = exchange(
239 &origin,
240 Some(&login.token),
241 "GET",
242 "sync?timeout=0",
243 None,
244 IO_TIMEOUT,
245 )
246 .await?;
247 ensure!(
248 first.status == 200,
249 "first /sync failed: {} {:?}",
250 first.status,
251 matrix_error(&first)
252 );
253 let mut since = first.body["next_batch"]
254 .as_str()
255 .context("/sync answer has no next_batch")?
256 .to_string();
257 let joined: Vec<String> = first.body["rooms"]["join"]
258 .as_object()
259 .map(|m| m.keys().cloned().collect())
260 .unwrap_or_default();
261 Log::new(Some(&ctx.status_tx)).info(format!(
262 "Matrix client logged in as {} ({}) at {}",
263 login.user_id, login.device_id, origin.authority
264 ));
265 ctx.state
266 .update_client_status(ctx.client_id, ClientStatus::Connected)
267 .await;
268 let external =
269 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
270 let (internal_tx, internal_rx) = mpsc::channel::<(Value, usize)>(32);
271 let (event_tx, mut event_rx) = mpsc::channel::<(Event, usize)>(64);
272 event_tx.try_send((
273 Event::new(
274 &actions::CONNECTED_EVENT,
275 json!({"user_id": login.user_id, "device_id": login.device_id, "joined_rooms": joined}),
276 ),
277 0,
278 ))?;
279 for e in sync_events(&first.body, &login.user_id, true) {
280 event_tx.try_send((e, 0))?;
281 }
282
283 let events_ctx = ctx.clone();
284 let dispatcher = tokio::spawn(async move {
285 while let Some((event, depth)) = event_rx.recv().await {
286 events_ctx
287 .state
288 .record_access_log(
289 AccessLogOwner::Client(events_ctx.client_id.as_u32()),
290 "Matrix",
291 None,
292 event.id(),
293 event.data.clone(),
294 vec![],
295 )
296 .await;
297 let instruction = events_ctx
298 .state
299 .get_instruction_for_client(events_ctx.client_id)
300 .await
301 .unwrap_or_default();
302 let memory = events_ctx
303 .state
304 .get_memory_for_client(events_ctx.client_id)
305 .await
306 .unwrap_or_default();
307 match call_llm_for_client(
308 &events_ctx.llm_client,
309 &events_ctx.state,
310 events_ctx.client_id.to_string(),
311 &instruction,
312 &memory,
313 Some(&event),
314 &MatrixClientProtocol,
315 &events_ctx.status_tx,
316 )
317 .await
318 {
319 Ok(result) => {
320 if let Some(memory) = result.memory_updates {
321 events_ctx
322 .state
323 .set_memory_for_client(events_ctx.client_id, memory)
324 .await;
325 }
326 for action in result.actions {
327 if internal_tx.send((action, depth + 1)).await.is_err() {
328 return;
329 }
330 }
331 }
332 Err(e) => Log::new(Some(&events_ctx.status_tx))
333 .warn(format!("Matrix client handler: {e}")),
334 }
335 }
336 });
337 let dispatcher_abort = dispatcher.abort_handle();
338 ctx.state
339 .register_client_task(ctx.client_id, dispatcher)
340 .await;
341
342 let sync_ctx = ctx.clone();
343 let sync_origin = origin.clone();
344 let sync_token = login.token.clone();
345 let me = login.user_id.clone();
346 let sync_events_tx = event_tx.clone();
347 let syncer = tokio::spawn(async move {
348 let log = Log::new(Some(&sync_ctx.status_tx));
349 loop {
350 let path = format!(
351 "sync?timeout={}&since={}",
352 SYNC_WAIT.as_millis(),
353 urlencoding::encode(&since)
354 );
355 match exchange(
356 &sync_origin,
357 Some(&sync_token),
358 "GET",
359 &path,
360 None,
361 SYNC_WAIT + IO_TIMEOUT,
362 )
363 .await
364 {
365 Ok(a) if a.status == 200 => {
366 if let Some(next) = a.body["next_batch"].as_str() {
367 since = next.to_string();
368 }
369 for e in sync_events(&a.body, &me, false) {
370 if sync_events_tx.send((e, 0)).await.is_err() {
371 return;
372 }
373 }
374 }
375 Ok(a) => {
376 let (code, error) = matrix_error(&a);
377 log.warn(format!("Matrix /sync answered {} {code} {error}", a.status));
378 if a.status == 401 {
379 return;
380 }
381 tokio::time::sleep(SYNC_BACKOFF).await;
382 }
383 Err(e) => {
384 log.warn(format!("Matrix /sync failed: {e:#}"));
385 tokio::time::sleep(SYNC_BACKOFF).await;
386 }
387 }
388 }
389 });
390 let syncer_abort = syncer.abort_handle();
391 ctx.state.register_client_task(ctx.client_id, syncer).await;
392
393 let session_ctx = ctx.clone();
394 let task = tokio::spawn(async move {
395 let result = session(
396 &session_ctx,
397 &origin,
398 &login,
399 external,
400 internal_rx,
401 event_tx,
402 )
403 .await;
404 syncer_abort.abort();
405 dispatcher_abort.abort();
406 let _ = exchange(
407 &origin,
408 Some(&login.token),
409 "POST",
410 "logout",
411 Some(&json!({})),
412 IO_TIMEOUT,
413 )
414 .await;
415 let status = match result {
416 Ok(()) => ClientStatus::Disconnected,
417 Err(e) => {
418 Log::new(Some(&session_ctx.status_tx)).warn(format!("Matrix client ended: {e}"));
419 ClientStatus::Error(e.to_string())
420 }
421 };
422 session_ctx
423 .state
424 .update_client_status(session_ctx.client_id, status)
425 .await;
426 session_ctx
427 .state
428 .remove_client_handle(session_ctx.client_id)
429 .await;
430 let _ = session_ctx.status_tx.send("__UPDATE_UI__".into());
431 });
432 ctx.state.register_client_task(ctx.client_id, task).await;
433 Ok("0.0.0.0:0".parse()?)
434}
435
436async fn session(
437 ctx: &ConnectContext,
438 origin: &Origin,
439 login: &Login,
440 mut external: mpsc::Receiver<ClientCommand>,
441 mut internal: mpsc::Receiver<(Value, usize)>,
442 events: mpsc::Sender<(Event, usize)>,
443) -> Result<()> {
444 let log = Log::new(Some(&ctx.status_tx));
445 let txn_prefix = format!("ng{}", crate::server::matrix::hub::random_id(8));
446 let mut txn = 0u64;
447 loop {
448 let (action, depth, mut injected) = tokio::select! {
449 command = external.recv() => match command {
450 Some(c) => (c.action.clone(), 0, Some(c)),
451 None => return Ok(()),
452 },
453 action = internal.recv() => match action {
454 Some((a, depth)) => (a, depth, None),
455 None => return Ok(()),
456 },
457 };
458 let reply = |injected: &mut Option<ClientCommand>, outcome: ClientSendOutcome| {
459 if let Some(command) = injected.take() {
460 crate::client::command_support::reply(command, Ok(outcome));
461 }
462 };
463 match MatrixClientProtocol.execute_action(action.clone()) {
464 Ok(ClientActionResult::Disconnect) => {
465 reply(&mut injected, ClientSendOutcome::Disconnected);
466 return Ok(());
467 }
468 Ok(_) => {}
469 Err(e) => {
470 log.warn(format!("Matrix client action refused: {e:#}"));
471 reply(
472 &mut injected,
473 ClientSendOutcome::Rejected {
474 error: e.to_string(),
475 },
476 );
477 continue;
478 }
479 }
480 if depth > MAX_FOLLOWUP_DEPTH {
481 log.warn(format!(
482 "Matrix client: handler chain stopped after {MAX_FOLLOWUP_DEPTH} follow-ups"
483 ));
484 continue;
485 }
486 if injected.is_some() {
487 ctx.state
488 .record_access_log(
489 AccessLogOwner::Client(ctx.client_id.as_u32()),
490 "Matrix",
491 None,
492 "injected_action",
493 action.clone(),
494 vec![],
495 )
496 .await;
497 }
498 txn += 1;
499 let (method, path, body) = actions::request(&action, &format!("{txn_prefix}.{txn}"))?;
500 let operation = action["type"].as_str().unwrap_or_default().to_string();
501 match exchange(
502 origin,
503 Some(&login.token),
504 method,
505 &path,
506 body.as_ref(),
507 IO_TIMEOUT,
508 )
509 .await
510 {
511 Ok(answer) => {
512 let ok = answer.status == 200;
513 let mut data = json!({"operation": operation, "status": answer.status});
514 if ok {
515 data["result"] = match operation.as_str() {
516 "matrix_messages" => json!({"events": answer.body["chunk"]}),
517 _ => answer.body.clone(),
518 };
519 } else {
520 let (code, error) = matrix_error(&answer);
521 data["result"] = json!({});
522 data["errcode"] = json!(code);
523 data["error"] = json!(error);
524 }
525 reply(
526 &mut injected,
527 ClientSendOutcome::Executed {
528 detail: data.to_string(),
529 },
530 );
531 if ok && operation == "matrix_send" {
533 ctx.state
534 .record_access_log(
535 AccessLogOwner::Client(ctx.client_id.as_u32()),
536 "Matrix",
537 None,
538 "matrix_sent",
539 data,
540 vec![],
541 )
542 .await;
543 continue;
544 }
545 events
546 .try_send((Event::new(&actions::RESPONSE_EVENT, data), depth))
547 .context("Matrix event queue full; consumer stalled")?;
548 }
549 Err(e) => {
550 log.warn(format!("Matrix request failed: {e:#}"));
551 reply(
552 &mut injected,
553 ClientSendOutcome::Rejected {
554 error: format!("{e:#}"),
555 },
556 );
557 }
558 }
559 }
560}