Watch
0
0
Fork
You've already forked hyperhive
0

matrix: one joined-room walk for the Space and the chat room lookups

find_space_by_name and find_chat_room_by_name carried the same 52-line
body and differed only in the space test and the name. Both now call
find_joined_room(base, want_space, name). Same requests, same order,
same return values.
This commit is contained in:
atlas 2026-10-16 13:32:13 +02:00 • committed by mara
commit 99785989b9

View file

@ -307,16 +307,17 @@ fn persist_space_room_id(room_id: &str) -> Result<()> {
.with_context(|| format!("matrix: write space room_id to {}", path.display()))
}
/// 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> {
let base = matrix_http()?;
/// 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 joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
let joined: serde_json::Value = client
.get(&joined_url)
@ -333,7 +334,7 @@ async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Opt
continue;
};
let encoded = encode_room_id_for_url(room_id);
// Must be an m.space (m.room.create `type`).
// m.room.create `type` says whether this is a Space.
let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
let is_space = match client
.get(&create_url)
@ -348,17 +349,17 @@ async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Opt
.is_some_and(|c| c["type"].as_str() == Some("m.space")),
_ => false,
};
if !is_space {
if is_space != want_space {
continue;
}
// …and named HIVE_SPACE_NAME (m.room.name `name`).
// …and named `name` (m.room.name `name`).
let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
let name_matches = match client.get(&name_url).bearer_auth(sender_token).send().await {
Ok(r) if r.status().is_success() => r
.json::<serde_json::Value>()
.await
.ok()
.is_some_and(|n| n["name"].as_str() == Some(HIVE_SPACE_NAME)),
.is_some_and(|n| n["name"].as_str() == Some(name)),
_ => false,
};
if name_matches {
@ -368,6 +369,18 @@ async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Opt
None
}
/// 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).
@ -448,56 +461,14 @@ fn persist_chat_room_id(room_id: &str) -> Result<()> {
/// room instead of spawning a duplicate. `None` if the homeserver is
/// unreachable or no match exists.
async fn find_chat_room_by_name(client: &reqwest::Client, sender_token: &str) -> Option<String> {
let base = matrix_http()?;
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
let joined: serde_json::Value = client
.get(&joined_url)
.bearer_auth(sender_token)
.send()
.await
.ok()?
.json()
.await
.ok()?;
let rooms = joined["joined_rooms"].as_array()?;
for room in rooms {
let Some(room_id) = room.as_str() else {
continue;
};
let encoded = encode_room_id_for_url(room_id);
// Skip the Space itself (and any other m.space).
let create_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
let is_space = match client
.get(&create_url)
.bearer_auth(sender_token)
.send()
.await
{
Ok(r) if r.status().is_success() => r
.json::<serde_json::Value>()
.await
.ok()
.is_some_and(|c| c["type"].as_str() == Some("m.space")),
_ => false,
};
if is_space {
continue;
}
// …and named HIVE_CHAT_ROOM_NAME (m.room.name `name`).
let name_url = format!("{base}/_matrix/client/v3/rooms/{encoded}/state/m.room.name/");
let name_matches = match client.get(&name_url).bearer_auth(sender_token).send().await {
Ok(r) if r.status().is_success() => r
.json::<serde_json::Value>()
.await
.ok()
.is_some_and(|n| n["name"].as_str() == Some(HIVE_CHAT_ROOM_NAME)),
_ => false,
};
if name_matches {
return Some(room_id.to_owned());
}
}
None
find_joined_room(
client,
matrix_http()?,
sender_token,
false,
HIVE_CHAT_ROOM_NAME,
)
.await
}
/// Whether the state event still has to be written.
@ -1181,4 +1152,53 @@ mod tests {
let outcome = refused_invite(response(500, "{}"), Some("join"), "@a:x", "!r:x").await;
assert!(outcome.is_err(), "a 500 is not excused by membership");
}
/// 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);
}
}