|
|
|
|
@ -1,8 +1,7 @@
|
|
|
|
|
//! Optional matrix-tuwunel wiring: the hive's `@hive-<hive>:` sender token,
|
|
|
|
|
//! taken from the swarm secret store, and the hive Space and
|
|
|
|
|
//! invites provisioned with it. No-op when no homeserver is configured, so
|
|
|
|
|
//! operators who haven't flipped `services.hyperhive.deploy.matrix.enable =
|
|
|
|
|
//! true` pay nothing.
|
|
|
|
|
//! taken from the swarm secret store and kept on disk for `hivectl matrix
|
|
|
|
|
//! invite`. No-op when no homeserver is configured, so operators who haven't
|
|
|
|
|
//! flipped `services.hyperhive.deploy.matrix.enable = true` pay nothing.
|
|
|
|
|
//!
|
|
|
|
|
//! This module creates no accounts: `swarm-controller` mints every hive's
|
|
|
|
|
//! sender account and every agent's account with the swarm's appservice token.
|
|
|
|
|
@ -46,46 +45,6 @@ fn matrix_base() -> Result<&'static str> {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Matrix localpart this hive acts as. Not an agent; has no state dir.
|
|
|
|
|
///
|
|
|
|
|
/// **Per hive, derived from the hive's name** — `hive-<name>`, built by
|
|
|
|
|
/// `swarm_secret_client::matrix::hive_localpart`, which is the one place the
|
|
|
|
|
/// derivation lives. It used to be the bare constant `hive`, and a swarm runs
|
|
|
|
|
/// one homeserver: every hive on it logged in as the same `@hive:`, so the
|
|
|
|
|
/// homeserver could not attribute an action to a hive and revoking one hive's
|
|
|
|
|
/// standing revoked all of them.
|
|
|
|
|
///
|
|
|
|
|
/// `swarm-controller` creates this account with the swarm's appservice token
|
|
|
|
|
/// (`matrix_account::hive_sender`) and stores its token where
|
|
|
|
|
/// [`stored_sender_token`] reads it; hive-c0re never creates it.
|
|
|
|
|
///
|
|
|
|
|
/// An ordinary account, with no homeserver-admin standing: what it
|
|
|
|
|
/// provisions — the Space and the invites — it provisions as the
|
|
|
|
|
/// creator of the Space.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// When `HYPERHIVE_HIVE_NAME` is unset or empty. That is the same variable
|
|
|
|
|
/// [`stored_sender_token`] logs in with and the swarm meta flake asserts
|
|
|
|
|
/// for every hive, so its absence is a broken deployment rather than a
|
|
|
|
|
/// supported one — and there is no safe default to fall back to: the only one
|
|
|
|
|
/// available is the shared account this replaced.
|
|
|
|
|
pub fn hive_localpart() -> Result<String> {
|
|
|
|
|
let hive = std::env::var("HYPERHIVE_HIVE_NAME")
|
|
|
|
|
.ok()
|
|
|
|
|
.filter(|h| !h.is_empty())
|
|
|
|
|
.context(
|
|
|
|
|
"matrix: HYPERHIVE_HIVE_NAME is unset, so this hive has no matrix account of its own \
|
|
|
|
|
— the localpart is derived from the hive name and has no swarm-wide fallback",
|
|
|
|
|
)?;
|
|
|
|
|
Ok(swarm_secret_client::matrix::hive_localpart(&hive))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Display name of the hive Space. Plain text, no special characters, so
|
|
|
|
|
/// the Space stays rediscoverable by name (no room alias needed) even when
|
|
|
|
|
/// the persisted room-id file is lost — preventing duplicate spaces from
|
|
|
|
|
/// being created on the next sweep.
|
|
|
|
|
pub const HIVE_SPACE_NAME: &str = "hive";
|
|
|
|
|
|
|
|
|
|
/// Host path for the appservice sender account's matrix access token. Outside every
|
|
|
|
|
/// purgeable path — not deleted by `destroy --purge` on any agent.
|
|
|
|
|
#[must_use]
|
|
|
|
|
@ -93,13 +52,6 @@ pub fn sender_token_path() -> PathBuf {
|
|
|
|
|
crate::paths::matrix_sender_token()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Host path where the hive Matrix Space room ID is persisted.
|
|
|
|
|
/// Outside every purgeable path — not deleted by `destroy --purge`.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn hive_space_room_id_path() -> PathBuf {
|
|
|
|
|
crate::paths::matrix_space_room_id()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether this hive has a homeserver to provision against.
|
|
|
|
|
///
|
|
|
|
|
/// **A configured API URL, not a local container**: whether the homeserver
|
|
|
|
|
@ -274,136 +226,6 @@ pub fn read_sender_token() -> Result<String> {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Persist the hive Space room id to [`hive_space_room_id_path()`] (0600).
|
|
|
|
|
fn persist_space_room_id(room_id: &str) -> Result<()> {
|
|
|
|
|
let path = hive_space_room_id_path();
|
|
|
|
|
crate::secret_file::write_secret_0600(&path, format!("{room_id}\n").as_bytes())
|
|
|
|
|
.with_context(|| format!("matrix: write space room_id to {}", path.display()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Walk the admin account's joined rooms at `base` and return the first room
|
|
|
|
|
/// named `name` that is an `m.space` exactly when `want_space` is set (the
|
|
|
|
|
/// `m.room.create` `type`). `None` if the homeserver is unreachable or no
|
|
|
|
|
/// room matches.
|
|
|
|
|
async fn find_joined_room(
|
|
|
|
|
client: &reqwest::Client,
|
|
|
|
|
base: &str,
|
|
|
|
|
sender_token: &str,
|
|
|
|
|
want_space: bool,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let rooms = swarm_matrix_client::joined_rooms(client, base, sender_token)
|
|
|
|
|
.await
|
|
|
|
|
.ok()?;
|
|
|
|
|
for room_id in rooms {
|
|
|
|
|
// m.room.create `type` says whether this is a Space.
|
|
|
|
|
let is_space = keyless_state(client, base, sender_token, &room_id, "m.room.create")
|
|
|
|
|
.await
|
|
|
|
|
.is_some_and(|c| c["type"].as_str() == Some("m.space"));
|
|
|
|
|
if is_space != want_space {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// …and named `name` (m.room.name `name`).
|
|
|
|
|
let name_matches = keyless_state(client, base, sender_token, &room_id, "m.room.name")
|
|
|
|
|
.await
|
|
|
|
|
.is_some_and(|n| n["name"].as_str() == Some(name));
|
|
|
|
|
if name_matches {
|
|
|
|
|
return Some(room_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The content of a state event with an empty state key, or `None` when the
|
|
|
|
|
/// room has none or the read failed.
|
|
|
|
|
async fn keyless_state(
|
|
|
|
|
client: &reqwest::Client,
|
|
|
|
|
base: &str,
|
|
|
|
|
sender_token: &str,
|
|
|
|
|
room_id: &str,
|
|
|
|
|
event_type: &str,
|
|
|
|
|
) -> Option<serde_json::Value> {
|
|
|
|
|
swarm_matrix_client::room_state(client, base, sender_token, room_id, event_type, "")
|
|
|
|
|
.await
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Scan the admin account's joined rooms for the canonical hive Space: the
|
|
|
|
|
/// `m.space` whose name is [`HIVE_SPACE_NAME`]. Returns the first match
|
|
|
|
|
/// (deterministic per the homeserver's joined-rooms order) so a lost
|
|
|
|
|
/// room-id file recovers the existing space instead of spawning a
|
|
|
|
|
/// duplicate. `None` if the homeserver is unreachable or no match exists.
|
|
|
|
|
///
|
|
|
|
|
/// Name-based (not alias-based) rediscovery keeps the Space free of any
|
|
|
|
|
/// special-char room alias — the hardcoded plain name is the anchor.
|
|
|
|
|
async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Option<String> {
|
|
|
|
|
find_joined_room(client, matrix_http()?, sender_token, true, HIVE_SPACE_NAME).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create (or recover) the hive Matrix Space and persist its room ID to
|
|
|
|
|
/// [`hive_space_room_id_path()`]. The Space is a private `m.space` owned by
|
|
|
|
|
/// `@hive-<hive>:`, identified by its hardcoded name [`HIVE_SPACE_NAME`] (no alias).
|
|
|
|
|
///
|
|
|
|
|
/// Dedup strategy (single canonical space):
|
|
|
|
|
/// 1. If the room-id file exists, reuse it.
|
|
|
|
|
/// 2. Otherwise, rediscover by scanning the admin's joined rooms for the
|
|
|
|
|
/// `m.space` named [`HIVE_SPACE_NAME`] and adopt it (re-persisting the
|
|
|
|
|
/// file). This recovers the existing space after a state wipe instead
|
|
|
|
|
/// of creating a duplicate.
|
|
|
|
|
/// 3. Only if neither yields a room do we `createRoom`.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if the homeserver is unreachable, `createRoom` fails,
|
|
|
|
|
/// or the room-ID file cannot be written.
|
|
|
|
|
pub async fn ensure_hive_space(client: &reqwest::Client, sender_token: &str) -> Result<String> {
|
|
|
|
|
let base = matrix_base()?;
|
|
|
|
|
// 1. Stored room id wins (fast path).
|
|
|
|
|
if let Ok(existing) = std::fs::read_to_string(hive_space_room_id_path()) {
|
|
|
|
|
let trimmed = existing.trim().to_owned();
|
|
|
|
|
if !trimmed.is_empty() {
|
|
|
|
|
tracing::debug!(room_id = %trimmed, "matrix: hive space already provisioned");
|
|
|
|
|
return Ok(trimmed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. No stored id — rediscover the existing space by its hardcoded name
|
|
|
|
|
// before creating a new one (prevents duplicate spaces after a wipe).
|
|
|
|
|
if let Some(room_id) = find_space_by_name(client, sender_token).await {
|
|
|
|
|
persist_space_room_id(&room_id)?;
|
|
|
|
|
tracing::info!(%room_id, "matrix: recovered hive space by name");
|
|
|
|
|
return Ok(room_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Create the space (plain hardcoded name, no alias).
|
|
|
|
|
let body = serde_json::json!({
|
|
|
|
|
"name": HIVE_SPACE_NAME,
|
|
|
|
|
"creation_content": { "type": "m.space" },
|
|
|
|
|
"preset": "private_chat",
|
|
|
|
|
"visibility": "private",
|
|
|
|
|
});
|
|
|
|
|
let room_id = swarm_matrix_client::create_room(client, base, sender_token, &body)
|
|
|
|
|
.await
|
|
|
|
|
.context("matrix: create hive space")?;
|
|
|
|
|
persist_space_room_id(&room_id)?;
|
|
|
|
|
tracing::info!(%room_id, "matrix: created hive space");
|
|
|
|
|
Ok(room_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Invite `@{localpart}:{server_name}` to `room_id` using the admin
|
|
|
|
|
/// account. Idempotent — treats already-member responses as success.
|
|
|
|
|
async fn invite_to_room(
|
|
|
|
|
client: &reqwest::Client,
|
|
|
|
|
sender_token: &str,
|
|
|
|
|
room_id: &str,
|
|
|
|
|
localpart: &str,
|
|
|
|
|
server_name: &str,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let user_id = format!("@{localpart}:{server_name}");
|
|
|
|
|
invite_user_id(client, sender_token, room_id, &user_id).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fetch a user's current membership in a room via the sender token, or
|
|
|
|
|
/// `None` if there is no membership event (never invited) or the lookup
|
|
|
|
|
/// fails. Returns the raw membership string (`invite`, `join`, `leave`, …).
|
|
|
|
|
@ -484,23 +306,21 @@ fn forbidden_invite(membership: Option<&str>, user_id: &str, room_id: &str) -> R
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Invite an arbitrary Matrix user to the hive Space (default) or an
|
|
|
|
|
/// explicit `room_id` / alias. `user` may be a fully-qualified id
|
|
|
|
|
/// (`@name:server`) or a bare localpart, which is qualified with the
|
|
|
|
|
/// homeserver's `server_name`. Returns the resolved room id the invite
|
|
|
|
|
/// targeted. Used by `hivectl matrix invite`.
|
|
|
|
|
/// Invite an arbitrary Matrix user to `room` (a room id or alias). `user`
|
|
|
|
|
/// may be a fully-qualified id (`@name:server`) or a bare localpart, which
|
|
|
|
|
/// is qualified with the homeserver's `server_name`. Returns the resolved
|
|
|
|
|
/// room id the invite targeted. Used by `hivectl matrix invite`.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Returns an error if the sender token or `server_name` can't be read,
|
|
|
|
|
/// the target room can't be resolved (no `--room` and no persisted hive
|
|
|
|
|
/// space), or the invite POST fails for a reason other than the user
|
|
|
|
|
/// already being a member / invited.
|
|
|
|
|
/// Returns an error if `room` is neither a room id nor an alias, or the
|
|
|
|
|
/// invite POST fails for a reason other than the user already being a
|
|
|
|
|
/// member / invited.
|
|
|
|
|
pub async fn invite_user(
|
|
|
|
|
client: &reqwest::Client,
|
|
|
|
|
sender_token: &str,
|
|
|
|
|
user: &str,
|
|
|
|
|
room_override: Option<&str>,
|
|
|
|
|
room: &str,
|
|
|
|
|
server_name: &str,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
// Qualify a bare localpart to a full user id on the hive homeserver.
|
|
|
|
|
@ -509,39 +329,29 @@ pub async fn invite_user(
|
|
|
|
|
} else {
|
|
|
|
|
format!("@{user}:{server_name}")
|
|
|
|
|
};
|
|
|
|
|
// Resolve the room: explicit override (id or #alias) wins; otherwise
|
|
|
|
|
// the persisted hive Space.
|
|
|
|
|
let room_id = match room_override {
|
|
|
|
|
Some(r) if r.starts_with('#') => {
|
|
|
|
|
swarm_matrix_client::resolve_alias(client, matrix_base()?, sender_token, r).await?
|
|
|
|
|
}
|
|
|
|
|
Some(r) if r.starts_with('!') => r.to_owned(),
|
|
|
|
|
Some(r) => anyhow::bail!(
|
|
|
|
|
"matrix: --room {r:?} is neither a room id nor an alias; \
|
|
|
|
|
let room_id = if room.starts_with('#') {
|
|
|
|
|
swarm_matrix_client::resolve_alias(client, matrix_base()?, sender_token, room).await?
|
|
|
|
|
} else if room.starts_with('!') {
|
|
|
|
|
room.to_owned()
|
|
|
|
|
} else {
|
|
|
|
|
anyhow::bail!(
|
|
|
|
|
"matrix: --room {room:?} is neither a room id nor an alias; \
|
|
|
|
|
prefix with '!' for a room id (!abc:server) or '#' for an \
|
|
|
|
|
alias (#name:server)"
|
|
|
|
|
),
|
|
|
|
|
None => std::fs::read_to_string(hive_space_room_id_path())
|
|
|
|
|
.map(|s| s.trim().to_owned())
|
|
|
|
|
.context(
|
|
|
|
|
"matrix: no --room given and no persisted hive space \
|
|
|
|
|
(run the hive-c0re matrix sweep first)",
|
|
|
|
|
)?,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
invite_user_id(client, sender_token, &room_id, &user_id).await?;
|
|
|
|
|
Ok(room_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Make sure the hive's own `@hive-<hive>:` account exists, then provision
|
|
|
|
|
/// the hive Space and invite every agent container to it.
|
|
|
|
|
/// Agents' own accounts are not created here: `swarm-controller` mints them
|
|
|
|
|
/// with the swarm's appservice token. Runs only as a `MatrixSweep` job node
|
|
|
|
|
/// (see `job_queue::templates::matrix_sweep`). No-op when the
|
|
|
|
|
/// hive-matrix container isn't running. Per-step failures are logged
|
|
|
|
|
/// but don't abort the sweep.
|
|
|
|
|
/// Make sure the hive's own `@hive-<hive>:` sender token is on disk.
|
|
|
|
|
/// Accounts are not created here: `swarm-controller` mints them with the
|
|
|
|
|
/// swarm's appservice token. Runs only as a `MatrixSweep` job node (see
|
|
|
|
|
/// `job_queue::templates::matrix_sweep`). No-op when no homeserver is
|
|
|
|
|
/// configured.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `true` when every step of the sweep succeeded, `false` when at
|
|
|
|
|
/// least one step failed — the caller feeds this into a
|
|
|
|
|
/// Returns `true` when the token is in place, `false` when it could not be
|
|
|
|
|
/// fetched — the caller feeds this into a
|
|
|
|
|
/// [`crate::stats::sweep_health::SweepHealth`] to raise a debounced
|
|
|
|
|
/// hive warning on persistent failure (this sweep re-runs every 30
|
|
|
|
|
/// minutes, so a one-off blip self-heals without ever warning).
|
|
|
|
|
@ -550,90 +360,11 @@ pub async fn ensure_all() -> bool {
|
|
|
|
|
tracing::debug!("matrix: hive-matrix container absent, skipping user sweep");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
let mut ok = true;
|
|
|
|
|
// One HTTP client for the whole sweep.
|
|
|
|
|
let client = match swarm_matrix_client::client() {
|
|
|
|
|
Ok(c) => c,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: build HTTP client failed; skipping sweep");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// The hive's `@hive-<hive>:` account first, because everything below provisions
|
|
|
|
|
// THROUGH it (the Space and every invite are sent with its token) — as
|
|
|
|
|
// an ordinary user that created the Space, not as a homeserver admin.
|
|
|
|
|
if let Err(e) = ensure_hive_user().await {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: ensure_hive_user failed");
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
let Ok(containers) = crate::lifecycle::list().await else {
|
|
|
|
|
tracing::warn!("matrix: nixos-container list failed; skipping user sweep");
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
let mut agent_names: Vec<String> = Vec::new();
|
|
|
|
|
for c in &containers {
|
|
|
|
|
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
agent_names.push(name.to_owned());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !provision_space(&client, &agent_names).await {
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Provision the hive Space and invite every agent (+ the admin account)
|
|
|
|
|
/// to it. Split out of [`ensure_all`] purely to
|
|
|
|
|
/// keep that function under the `too_many_lines` threshold — this is the
|
|
|
|
|
/// tail half of the same sequential sweep and shares its aggregate-bool,
|
|
|
|
|
/// log-and-continue failure handling.
|
|
|
|
|
async fn provision_space(client: &reqwest::Client, agent_names: &[String]) -> bool {
|
|
|
|
|
let mut ok = true;
|
|
|
|
|
// server_name is needed to form full Matrix user IDs for invites.
|
|
|
|
|
let sender_token = match read_sender_token() {
|
|
|
|
|
Ok(t) => t,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: skipping hive space provisioning (no matrix sender token)");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// server_name first — the agent invites need it (fully-qualified user ids).
|
|
|
|
|
let server_name = match discover_server_name(client).await {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: discover_server_name failed; skipping space provisioning");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let room_id = match ensure_hive_space(client, &sender_token).await {
|
|
|
|
|
Ok(id) => id,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: ensure_hive_space failed");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// This hive's own account first, then all agents.
|
|
|
|
|
let localpart = match hive_localpart() {
|
|
|
|
|
Ok(l) => l,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "matrix: skipping hive space provisioning");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if let Err(e) = invite_to_room(client, &sender_token, &room_id, &localpart, &server_name).await
|
|
|
|
|
{
|
|
|
|
|
tracing::warn!(error = ?e, %localpart, "matrix: invite the hive account to the space failed");
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
for name in agent_names {
|
|
|
|
|
if let Err(e) = invite_to_room(client, &sender_token, &room_id, name, &server_name).await {
|
|
|
|
|
tracing::warn!(%name, error = ?e, "matrix: invite agent to space failed");
|
|
|
|
|
ok = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ok
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
@ -729,53 +460,4 @@ mod tests {
|
|
|
|
|
assert!(!message.contains("free-form-marker"), "{message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Serve a fake homeserver on loopback: `!space` is an `m.space` and
|
|
|
|
|
/// `!chat` a plain room, both named "shared"; `!decoy` is a plain room
|
|
|
|
|
/// named "other". Returns the base URL and the server task.
|
|
|
|
|
async fn fake_homeserver() -> (String, tokio::task::JoinHandle<()>) {
|
|
|
|
|
let app = axum::Router::new().fallback(|uri: axum::http::Uri| async move {
|
|
|
|
|
let path = uri.path();
|
|
|
|
|
let body = if path.ends_with("/joined_rooms") {
|
|
|
|
|
serde_json::json!({"joined_rooms": ["!decoy", "!space", "!chat"]})
|
|
|
|
|
} else if path.ends_with("/m.room.create/") {
|
|
|
|
|
if path.contains("space") {
|
|
|
|
|
serde_json::json!({"type": "m.space"})
|
|
|
|
|
} else {
|
|
|
|
|
serde_json::json!({})
|
|
|
|
|
}
|
|
|
|
|
} else if path.ends_with("/m.room.name/") {
|
|
|
|
|
let name = if path.contains("decoy") {
|
|
|
|
|
"other"
|
|
|
|
|
} else {
|
|
|
|
|
"shared"
|
|
|
|
|
};
|
|
|
|
|
serde_json::json!({"name": name})
|
|
|
|
|
} else {
|
|
|
|
|
serde_json::json!({})
|
|
|
|
|
};
|
|
|
|
|
axum::Json(body)
|
|
|
|
|
});
|
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
|
|
|
.await
|
|
|
|
|
.expect("bind");
|
|
|
|
|
let base = format!("http://{}", listener.local_addr().expect("local addr"));
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
axum::serve(listener, app).await.expect("serve");
|
|
|
|
|
});
|
|
|
|
|
(base, server)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn find_joined_room_matches_a_space_only_when_a_space_is_wanted() {
|
|
|
|
|
let (base, server) = fake_homeserver().await;
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let want_space = find_joined_room(&client, &base, "tok", true, "shared").await;
|
|
|
|
|
let want_chat = find_joined_room(&client, &base, "tok", false, "shared").await;
|
|
|
|
|
let no_name = find_joined_room(&client, &base, "tok", true, "absent").await;
|
|
|
|
|
server.abort();
|
|
|
|
|
assert_eq!(want_space.as_deref(), Some("!space"));
|
|
|
|
|
assert_eq!(want_chat.as_deref(), Some("!chat"));
|
|
|
|
|
assert_eq!(no_name, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|