fix(#2860): hive-c0re stops assuming matrix is on localhost

`MATRIX_HTTP` was `http://localhost:8008`, compiled in, used at 18 call
sites. That address is right only while the homeserver happens to share
this daemon's netns, and its doc comment asserted exactly that as a
general fact. A hive whose homeserver lives anywhere else builds fine
and then talks to the wrong machine.

It now reads `HIVE_MATRIX_API_URL`, which `hive-c0re.nix` sets from
`hyperhive.matrix.apiUrl`. The matrix module fills that in with its own
loopback listener when it is the thing running tuwunel — there it is not
a guess but a fact about what it just started — and the operator sets it
by hand otherwise. There is no compiled-in fallback, for the same reason
`forge_http_base()` has none.

`is_present()` follows. It used to scan `nixos-container list` for
`hive-matrix`, which answers "is the homeserver a container on this
host" — a different question, and the reason a remote homeserver would
silently no-op no matter how it was addressed. It now asks whether a URL
is configured. A co-located hive is unaffected: the module supplies the
loopback URL whenever it runs tuwunel itself. It also stops being
`async`, since it no longer does IO, and `require_matrix_present`'s
message names both ways to have a homeserver rather than only the local
container.

Absent a URL, every matrix path no-ops exactly as it did with no
container, and the two accessors make that structural: `Option` for the
callers that fall back to `None`, a `Result` flavour naming the skipped
`is_present()` gate for the ones that propagate.
This commit is contained in:
atlas 2026-08-03 20:09:56 +02:00 committed by mara
commit d3f2d246e3
2 changed files with 92 additions and 57 deletions

View file

@ -14,19 +14,39 @@ use reqwest::StatusCode;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
/// nspawn container name for the matrix homeserver — mirrors /// Client-server API base this daemon provisions against, from
/// `hive-forge` and matches the bare-name allow-list the lifecycle /// `HIVE_MATRIX_API_URL` (set by `hive-c0re.nix` from
/// scanner skips over. /// `hyperhive.matrix.apiUrl`).
const MATRIX_CONTAINER: &str = "hive-matrix";
/// Local-host URL of the tuwunel client-server API, for **this daemon
/// only**: hive-c0re and the homeserver share the host netns, so
/// `localhost:<port>` reaches it from here.
/// ///
/// Deliberately not an agent-facing address. An agent has its own netns, /// `None` means **this hive has no homeserver to provision against** and
/// where `localhost` is the agent — agents are handed the gateway vhost /// every matrix path no-ops — see [`is_present`]. There is deliberately no
/// (`matrix.<domain>`) via `HIVE_MATRIX_URL` instead, and get nothing at /// fallback: `localhost:8008` is right only when the homeserver happens to
/// all when the hive has no vhost to offer. /// share this daemon's netns, and an address baked into the binary is one
const MATRIX_HTTP: &str = "http://localhost:8008"; /// that builds fine and then talks to the wrong machine. The nix module
/// supplies the loopback address when it is itself the thing running
/// tuwunel, where it is not a guess but a fact about what it just started.
///
/// Also not an agent-facing address either way. An agent has its own netns;
/// agents are handed the gateway vhost via `HIVE_MATRIX_URL`, and get
/// nothing at all when the hive has no vhost to offer.
fn matrix_http() -> Option<&'static str> {
static BASE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
BASE.get_or_init(|| std::env::var("HIVE_MATRIX_API_URL").ok())
.as_deref()
}
/// [`matrix_http`] for the call sites that propagate with `?`.
///
/// # Errors
/// When no homeserver is configured. Reaching one of these paths at all
/// means an [`is_present`] gate was skipped, so the message names that
/// rather than the missing variable.
fn matrix_base() -> Result<&'static str> {
matrix_http().context(
"matrix: no homeserver configured (hyperhive.matrix.apiUrl / HIVE_MATRIX_API_URL) — \
this path should have been gated on matrix::is_present()",
)
}
/// Length (bytes) of the random registration token. 32 raw bytes ⇒ /// Length (bytes) of the random registration token. 32 raw bytes ⇒
/// 64-char hex string; comfortable for a long-lived shared secret. /// 64-char hex string; comfortable for a long-lived shared secret.
const REGISTER_TOKEN_BYTES: usize = 32; const REGISTER_TOKEN_BYTES: usize = 32;
@ -112,15 +132,17 @@ pub fn hive_chat_room_id_path() -> PathBuf {
crate::paths::matrix_chat_room_id() crate::paths::matrix_chat_room_id()
} }
/// Probe whether `hive-matrix` exists as a nixos-container. Cheap — /// Whether this hive has a homeserver to provision against.
/// `nixos-container list` is just a directory scan in /etc. Same shape ///
/// as `forge::is_present` — routed through hive-priv since /// **A configured API URL, not a local container.** It used to scan
/// `nixos-container` needs root and hive-c0re runs unprivileged. /// `nixos-container list` for `hive-matrix`, which answers a different
pub async fn is_present() -> bool { /// question — "is the homeserver a container on this host" — and so made a
let Ok(stdout) = crate::priv_client::list_containers().await else { /// remote homeserver silently no-op no matter how it was addressed. The
return false; /// nix module still supplies the loopback URL whenever it runs tuwunel
}; /// itself, so a co-located hive behaves exactly as before.
stdout.lines().any(|l| l.trim() == MATRIX_CONTAINER) #[must_use]
pub fn is_present() -> bool {
matrix_http().is_some()
} }
/// Read `n` cryptographic-quality bytes from `/dev/urandom` and return /// Read `n` cryptographic-quality bytes from `/dev/urandom` and return
@ -183,7 +205,8 @@ async fn register_post(
client: &reqwest::Client, client: &reqwest::Client,
body: &serde_json::Value, body: &serde_json::Value,
) -> Result<(StatusCode, serde_json::Value)> { ) -> Result<(StatusCode, serde_json::Value)> {
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/register"); let base = matrix_base()?;
let url = format!("{base}/_matrix/client/v3/register");
let resp = client let resp = client
.post(&url) .post(&url)
.json(body) .json(body)
@ -280,7 +303,8 @@ fn extract_access_token(body: &serde_json::Value) -> Result<String> {
/// in which case manual recovery via `hivectl matrix create-user` is /// in which case manual recovery via `hivectl matrix create-user` is
/// required. /// required.
async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Result<String> { async fn login_user(client: &reqwest::Client, agent: &str, password: &str) -> Result<String> {
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/login"); let base = matrix_base()?;
let url = format!("{base}/_matrix/client/v3/login");
let body = serde_json::json!({ let body = serde_json::json!({
"type": "m.login.password", "type": "m.login.password",
"identifier": { "identifier": {
@ -345,9 +369,10 @@ async fn discover_admin_room_id(
admin_token: &str, admin_token: &str,
server_name: &str, server_name: &str,
) -> Result<String> { ) -> Result<String> {
let base = matrix_base()?;
// #admins:server → %23admins%3A<server> // #admins:server → %23admins%3A<server>
let encoded_alias = format!("%23admins%3A{server_name}"); let encoded_alias = format!("%23admins%3A{server_name}");
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded_alias}"); let url = format!("{base}/_matrix/client/v3/directory/room/{encoded_alias}");
let resp = client let resp = client
.get(&url) .get(&url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -484,10 +509,11 @@ async fn admin_room_send_and_poll<T>(
command: &str, command: &str,
check: impl Fn(&str) -> Option<T>, check: impl Fn(&str) -> Option<T>,
) -> Result<T> { ) -> Result<T> {
let base = matrix_base()?;
// Send the command; record the event_id so we can use it as an anchor. // Send the command; record the event_id so we can use it as an anchor.
let txn_id = random_hex(8)?; let txn_id = random_hex(8)?;
let send_url = let send_url =
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/send/m.room.message/{txn_id}"); format!("{base}/_matrix/client/v3/rooms/{room_url}/send/m.room.message/{txn_id}");
let send_resp = client let send_resp = client
.put(&send_url) .put(&send_url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -512,8 +538,7 @@ async fn admin_room_send_and_poll<T>(
// on each tick. Walk the list until we hit our own command event_id; // on each tick. Walk the list until we hit our own command event_id;
// everything *before* that marker arrived after our command. // everything *before* that marker arrived after our command.
let own_user_id = format!("@{HIVE_ADMIN_LOCALPART}:{server_name}"); let own_user_id = format!("@{HIVE_ADMIN_LOCALPART}:{server_name}");
let poll_url = let poll_url = format!("{base}/_matrix/client/v3/rooms/{room_url}/messages?dir=b&limit=20");
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/messages?dir=b&limit=20");
for _ in 0..15_u8 { for _ in 0..15_u8 {
tokio::time::sleep(std::time::Duration::from_secs(1)).await; tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let poll_json = client let poll_json = client
@ -768,7 +793,7 @@ pub async fn sync_agent(client: &reqwest::Client, name: &str, register_token: &s
/// setup in [`ensure_all`] so the rebuild path and the startup sweep /// setup in [`ensure_all`] so the rebuild path and the startup sweep
/// stay equivalent. No-op when the matrix container is absent. /// stay equivalent. No-op when the matrix container is absent.
pub async fn sync_agent_standalone(name: &str) { pub async fn sync_agent_standalone(name: &str) {
if !is_present().await { if !is_present() {
return; return;
} }
let register_token = match ensure_register_token() { let register_token = match ensure_register_token() {
@ -938,7 +963,8 @@ fn persist_password(localpart: &str, password: &str) {
/// `GET /_matrix/key/v2/server` (unauthenticated federation key endpoint). /// `GET /_matrix/key/v2/server` (unauthenticated federation key endpoint).
/// The response JSON always includes `"server_name"` per the matrix spec. /// The response JSON always includes `"server_name"` per the matrix spec.
pub async fn discover_server_name(client: &reqwest::Client) -> Result<String> { pub async fn discover_server_name(client: &reqwest::Client) -> Result<String> {
let url = format!("{MATRIX_HTTP}/_matrix/key/v2/server"); let base = matrix_base()?;
let url = format!("{base}/_matrix/key/v2/server");
let resp = client let resp = client
.get(&url) .get(&url)
.send() .send()
@ -1000,7 +1026,8 @@ fn persist_space_room_id(room_id: &str) -> Result<()> {
/// Name-based (not alias-based) rediscovery keeps the Space free of any /// Name-based (not alias-based) rediscovery keeps the Space free of any
/// special-char room alias — the hardcoded plain name is the anchor. /// special-char room alias — the hardcoded plain name is the anchor.
async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> { async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> {
let joined_url = format!("{MATRIX_HTTP}/_matrix/client/v3/joined_rooms"); let base = matrix_http()?;
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
let joined: serde_json::Value = client let joined: serde_json::Value = client
.get(&joined_url) .get(&joined_url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1017,8 +1044,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
}; };
let encoded = encode_room_id_for_url(room_id); let encoded = encode_room_id_for_url(room_id);
// Must be an m.space (m.room.create `type`). // Must be an m.space (m.room.create `type`).
let create_url = let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
let is_space = match client let is_space = match client
.get(&create_url) .get(&create_url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1036,8 +1062,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
continue; continue;
} }
// …and named HIVE_SPACE_NAME (m.room.name `name`). // …and named HIVE_SPACE_NAME (m.room.name `name`).
let name_url = let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await { let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await {
Ok(r) if r.status().is_success() => r Ok(r) if r.status().is_success() => r
.json::<serde_json::Value>() .json::<serde_json::Value>()
@ -1070,6 +1095,7 @@ async fn find_space_by_name(client: &reqwest::Client, admin_token: &str) -> Opti
/// Returns an error if the homeserver is unreachable, `createRoom` fails, /// Returns an error if the homeserver is unreachable, `createRoom` fails,
/// or the room-ID file cannot be written. /// or the room-ID file cannot be written.
pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> { pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> {
let base = matrix_base()?;
// 1. Stored room id wins (fast path). // 1. Stored room id wins (fast path).
if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) { if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) {
let trimmed = existing.trim().to_owned(); let trimmed = existing.trim().to_owned();
@ -1088,7 +1114,7 @@ pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> R
} }
// 3. Create the space (plain hardcoded name, no alias). // 3. Create the space (plain hardcoded name, no alias).
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom"); let url = format!("{base}/_matrix/client/v3/createRoom");
let body = serde_json::json!({ let body = serde_json::json!({
"name": HIVE_SPACE_NAME, "name": HIVE_SPACE_NAME,
"creation_content": { "type": "m.space" }, "creation_content": { "type": "m.space" },
@ -1138,7 +1164,8 @@ fn persist_chat_room_id(room_id: &str) -> Result<()> {
/// room instead of spawning a duplicate. `None` if the homeserver is /// room instead of spawning a duplicate. `None` if the homeserver is
/// unreachable or no match exists. /// unreachable or no match exists.
async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> { async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) -> Option<String> {
let joined_url = format!("{MATRIX_HTTP}/_matrix/client/v3/joined_rooms"); let base = matrix_http()?;
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
let joined: serde_json::Value = client let joined: serde_json::Value = client
.get(&joined_url) .get(&joined_url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1155,8 +1182,7 @@ async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) ->
}; };
let encoded = encode_room_id_for_url(room_id); let encoded = encode_room_id_for_url(room_id);
// Skip the Space itself (and any other m.space). // Skip the Space itself (and any other m.space).
let create_url = let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
let is_space = match client let is_space = match client
.get(&create_url) .get(&create_url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1174,8 +1200,7 @@ async fn find_chat_room_by_name(client: &reqwest::Client, admin_token: &str) ->
continue; continue;
} }
// …and named HIVE_CHAT_ROOM_NAME (m.room.name `name`). // …and named HIVE_CHAT_ROOM_NAME (m.room.name `name`).
let name_url = let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await { let name_matches = match client.get(&name_url).bearer_auth(admin_token).send().await {
Ok(r) if r.status().is_success() => r Ok(r) if r.status().is_success() => r
.json::<serde_json::Value>() .json::<serde_json::Value>()
@ -1201,11 +1226,11 @@ async fn set_room_state(
state_key: &str, state_key: &str,
content: &serde_json::Value, content: &serde_json::Value,
) -> Result<()> { ) -> Result<()> {
let base = matrix_base()?;
let encoded_room = encode_room_id_for_url(room_id); let encoded_room = encode_room_id_for_url(room_id);
let encoded_key = encode_room_id_for_url(state_key); let encoded_key = encode_room_id_for_url(state_key);
let url = format!( let url =
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room}/state/{event_type}/{encoded_key}" format!("{base}/_matrix/client/v3/rooms/{encoded_room}/state/{event_type}/{encoded_key}");
);
let resp = client let resp = client
.put(&url) .put(&url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1248,6 +1273,7 @@ pub async fn ensure_hive_chat_room(
space_room_id: &str, space_room_id: &str,
server_name: &str, server_name: &str,
) -> Result<String> { ) -> Result<String> {
let base = matrix_base()?;
// 1. Stored room id wins (fast path). 2. Rediscover by name before // 1. Stored room id wins (fast path). 2. Rediscover by name before
// creating (prevents duplicates after a state wipe). 3. Create. // creating (prevents duplicates after a state wipe). 3. Create.
let room_id = if let Some(id) = std::fs::read_to_string(hive_chat_room_id_path()) let room_id = if let Some(id) = std::fs::read_to_string(hive_chat_room_id_path())
@ -1264,7 +1290,7 @@ pub async fn ensure_hive_chat_room(
} else { } else {
// `initial_state` is applied after the preset-derived state, so the // `initial_state` is applied after the preset-derived state, so the
// restricted join rule overrides private_chat's invite-only default. // restricted join rule overrides private_chat's invite-only default.
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom"); let url = format!("{base}/_matrix/client/v3/createRoom");
let body = serde_json::json!({ let body = serde_json::json!({
"name": HIVE_CHAT_ROOM_NAME, "name": HIVE_CHAT_ROOM_NAME,
"topic": HIVE_CHAT_ROOM_TOPIC, "topic": HIVE_CHAT_ROOM_TOPIC,
@ -1364,11 +1390,12 @@ async fn room_membership(
encoded_room_id: &str, encoded_room_id: &str,
user_id: &str, user_id: &str,
) -> Option<String> { ) -> Option<String> {
let base = matrix_http()?;
// `:` must be percent-encoded in both the room-id and user-id path // `:` must be percent-encoded in both the room-id and user-id path
// segments; `@` and `!` are permitted path characters per RFC 3986. // segments; `@` and `!` are permitted path characters per RFC 3986.
let encoded_user = user_id.replace(':', "%3A"); let encoded_user = user_id.replace(':', "%3A");
let url = format!( let url = format!(
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/state/m.room.member/{encoded_user}" "{base}/_matrix/client/v3/rooms/{encoded_room_id}/state/m.room.member/{encoded_user}"
); );
let resp = client let resp = client
.get(&url) .get(&url)
@ -1396,6 +1423,7 @@ async fn invite_user_id(
room_id: &str, room_id: &str,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
let base = matrix_base()?;
// `:` must be percent-encoded in the room-id path segment; `!` is // `:` must be percent-encoded in the room-id path segment; `!` is
// permitted in URL path characters per RFC 3986. // permitted in URL path characters per RFC 3986.
let encoded_room_id = room_id.replace(':', "%3A"); let encoded_room_id = room_id.replace(':', "%3A");
@ -1410,7 +1438,7 @@ async fn invite_user_id(
return Ok(()); return Ok(());
} }
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/invite"); let url = format!("{base}/_matrix/client/v3/rooms/{encoded_room_id}/invite");
let resp = client let resp = client
.post(&url) .post(&url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1490,8 +1518,9 @@ async fn resolve_room_alias(
admin_token: &str, admin_token: &str,
alias: &str, alias: &str,
) -> Result<String> { ) -> Result<String> {
let base = matrix_base()?;
let encoded = alias.replace('#', "%23").replace(':', "%3A"); let encoded = alias.replace('#', "%23").replace(':', "%3A");
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded}"); let url = format!("{base}/_matrix/client/v3/directory/room/{encoded}");
let resp = client let resp = client
.get(&url) .get(&url)
.bearer_auth(admin_token) .bearer_auth(admin_token)
@ -1522,7 +1551,7 @@ async fn resolve_room_alias(
/// dashboard banner on persistent failure (this sweep re-runs every 30 /// dashboard banner on persistent failure (this sweep re-runs every 30
/// minutes, so a one-off blip self-heals without ever bannering). /// minutes, so a one-off blip self-heals without ever bannering).
pub async fn ensure_all() -> bool { pub async fn ensure_all() -> bool {
if !is_present().await { if !is_present() {
tracing::debug!("matrix: hive-matrix container absent, skipping user sweep"); tracing::debug!("matrix: hive-matrix container absent, skipping user sweep");
return true; return true;
} }

View file

@ -463,13 +463,19 @@ async fn handle_set_resource_limits(
)])) )]))
} }
/// Guard: matrix provisioning needs the homeserver container running. /// Guard: matrix provisioning needs a homeserver to provision against.
async fn require_matrix_present() -> Result<()> { ///
if crate::matrix::is_present().await { /// Answers "is one configured", not "is one running here" — the message names
/// both ways to get there, since a hive that talks to someone else's
/// homeserver never enables the local container at all.
fn require_matrix_present() -> Result<()> {
if crate::matrix::is_present() {
return Ok(()); return Ok(());
} }
anyhow::bail!( anyhow::bail!(
"hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) before provisioning matrix users" "no matrix homeserver configured — set services.hyperhive.matrix.enable = true to run one \
here, or services.hyperhive.matrix.apiUrl to point at an existing one, before \
provisioning matrix users"
) )
} }
@ -477,7 +483,7 @@ async fn handle_matrix_create_user(
name: &hive_types::Ident, name: &hive_types::Ident,
password: Option<&str>, password: Option<&str>,
) -> Result<HostResponse> { ) -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present()?;
let register_token = let register_token =
crate::matrix::ensure_register_token().context("read matrix register token")?; crate::matrix::ensure_register_token().context("read matrix register token")?;
let client = matrix_http_client()?; let client = matrix_http_client()?;
@ -689,7 +695,7 @@ async fn handle_push_snapshot(
} }
async fn handle_matrix_sync_admin() -> Result<HostResponse> { async fn handle_matrix_sync_admin() -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present()?;
let register_token = let register_token =
crate::matrix::ensure_register_token().context("read matrix register token")?; crate::matrix::ensure_register_token().context("read matrix register token")?;
let client = matrix_http_client()?; let client = matrix_http_client()?;
@ -707,7 +713,7 @@ async fn handle_matrix_sync_admin() -> Result<HostResponse> {
} }
async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> { async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present()?;
let admin_token = crate::matrix::read_admin_token()?; let admin_token = crate::matrix::read_admin_token()?;
let client = matrix_http_client()?; let client = matrix_http_client()?;
let server_name = crate::matrix::discover_server_name(&client) let server_name = crate::matrix::discover_server_name(&client)
@ -722,7 +728,7 @@ async fn handle_matrix_promote_user(name: &str) -> Result<HostResponse> {
} }
async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResponse> { async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present()?;
let admin_token = crate::matrix::read_admin_token()?; let admin_token = crate::matrix::read_admin_token()?;
let client = matrix_http_client()?; let client = matrix_http_client()?;
let server_name = crate::matrix::discover_server_name(&client) let server_name = crate::matrix::discover_server_name(&client)
@ -742,7 +748,7 @@ async fn handle_matrix_invite(user: &str, room: Option<&str>) -> Result<HostResp
} }
async fn handle_matrix_reset_password(name: &str) -> Result<HostResponse> { async fn handle_matrix_reset_password(name: &str) -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present()?;
let admin_token = crate::matrix::read_admin_token()?; let admin_token = crate::matrix::read_admin_token()?;
let client = matrix_http_client()?; let client = matrix_http_client()?;
let server_name = crate::matrix::discover_server_name(&client) let server_name = crate::matrix::discover_server_name(&client)