//! matrix-sdk `Client` setup for the daemon: read the per-agent access //! token from the state-dir file `hive-c0re::matrix::ensure_user_for` //! wrote, probe `whoami` to recover the agent's matrix `user_id` + //! `device_id`, restore the matrix-sdk session, return the Client ready //! to start sync. //! //! No OAuth dance / cross-signing setup (in contrast to damocles-daemon's //! ccc.de connection): for the in-hive tuwunel the `registration_token` //! UIAA flow already minted the token + user/device, hive-c0re just //! handed us the bearer in a file. matrix-sdk's `restore_session` with //! a constructed `MatrixSession` skips the login flow entirely. use std::path::Path; use anyhow::{Context, Result, anyhow}; use matrix_sdk::{ Client, SessionMeta, SessionTokens, authentication::matrix::MatrixSession, ruma::{OwnedDeviceId, OwnedUserId}, }; use serde::Deserialize; use tokio::fs; /// Subset of the `/_matrix/client/v3/account/whoami` response we care /// about. matrix-spec field names; `device_id` is optional per spec /// (servers MAY omit it for legacy bearer scopes) but tuwunel always /// returns it. #[derive(Debug, Deserialize)] struct WhoamiResponse { user_id: String, device_id: Option, } /// Build + restore a matrix-sdk `Client` for the per-agent bearer /// token at `token_file`. The Client points at `homeserver`, persists /// its sqlite cache under `state_dir`, and is ready for sync once /// returned. /// /// Steps: /// 1. Read the bearer token from `token_file` (trim trailing whitespace). /// 2. Plain reqwest GET to `/_matrix/client/v3/account/whoami` with /// the bearer — this gives us back the matrix `user_id` + /// `device_id` (the registration response had them but hive-c0re /// only persisted the token; whoami is the cheapest recovery path /// and avoids matrix-sdk's circular requirement of needing a /// session to call whoami). /// 3. Build the real Client with the sqlite store + `restore_session` /// using a synthetic `MatrixSession`. pub async fn build_and_restore( homeserver: &str, token_file: &Path, state_dir: &Path, ) -> Result { let token = fs::read_to_string(token_file) .await .with_context(|| format!("read matrix token from {}", token_file.display()))? .trim() .to_owned(); if token.is_empty() { return Err(anyhow!( "matrix token at {} is empty", token_file.display() )); } let (user_id, device_id) = whoami(homeserver, &token).await?; fs::create_dir_all(state_dir) .await .with_context(|| format!("mkdir matrix state dir {}", state_dir.display()))?; let client = Client::builder() .homeserver_url(homeserver) .sqlite_store(state_dir, None) .build() .await .with_context(|| format!("build matrix client for {homeserver}"))?; let session = MatrixSession { meta: SessionMeta { user_id, device_id }, tokens: SessionTokens { access_token: token, refresh_token: None, }, }; client .restore_session(session) .await .context("restore matrix session")?; tracing::info!( user = %client.user_id().map(ToString::to_string).unwrap_or_default(), device = %client.device_id().map(ToString::to_string).unwrap_or_default(), "matrix session restored" ); Ok(client) } /// Bare-reqwest whoami probe — used at startup to recover the /// `user_id` + `device_id` the registration response carried but /// hive-c0re didn't persist alongside the access token. Cheaper than /// teaching the hive-c0re side to persist them, plus matches what a /// fresh deployment with a hand-rolled token (`HIVE_MATRIX_TOKEN_FILE` /// pointing somewhere unexpected) needs anyway. async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> { let url = format!("{homeserver}/_matrix/client/v3/account/whoami"); let resp = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() .context("build whoami reqwest client")? .get(&url) .bearer_auth(token) .send() .await .with_context(|| format!("GET {url}"))?; let status = resp.status(); if !status.is_success() { let body = resp.text().await.unwrap_or_default(); return Err(anyhow!("whoami GET {url} → HTTP {status}, body: {body}")); } let body: WhoamiResponse = resp.json().await.context("parse whoami response")?; let user_id: OwnedUserId = body .user_id .parse() .with_context(|| format!("invalid user_id in whoami: {}", body.user_id))?; let device_id_raw = body .device_id .ok_or_else(|| anyhow!("whoami response missing device_id"))?; let device_id: OwnedDeviceId = device_id_raw.into(); Ok((user_id, device_id)) }