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:
parent
68e30957e2
commit
99785989b9
1 changed files with 84 additions and 64 deletions
|
|
@ -307,16 +307,17 @@ fn persist_space_room_id(room_id: &str) -> Result<()> {
|
||||||
.with_context(|| format!("matrix: write space room_id to {}", path.display()))
|
.with_context(|| format!("matrix: write space room_id to {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan the admin account's joined rooms for the canonical hive Space: the
|
/// Walk the admin account's joined rooms at `base` and return the first room
|
||||||
/// `m.space` whose name is [`HIVE_SPACE_NAME`]. Returns the first match
|
/// named `name` that is an `m.space` exactly when `want_space` is set (the
|
||||||
/// (deterministic per the homeserver's joined-rooms order) so a lost
|
/// `m.room.create` `type`). `None` if the homeserver is unreachable or no
|
||||||
/// room-id file recovers the existing space instead of spawning a
|
/// room matches.
|
||||||
/// duplicate. `None` if the homeserver is unreachable or no match exists.
|
async fn find_joined_room(
|
||||||
///
|
client: &reqwest::Client,
|
||||||
/// Name-based (not alias-based) rediscovery keeps the Space free of any
|
base: &str,
|
||||||
/// special-char room alias — the hardcoded plain name is the anchor.
|
sender_token: &str,
|
||||||
async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Option<String> {
|
want_space: bool,
|
||||||
let base = matrix_http()?;
|
name: &str,
|
||||||
|
) -> Option<String> {
|
||||||
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
|
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)
|
||||||
|
|
@ -333,7 +334,7 @@ async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Opt
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
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`).
|
// 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 create_url = format!("{base}/_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)
|
||||||
|
|
@ -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")),
|
.is_some_and(|c| c["type"].as_str() == Some("m.space")),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if !is_space {
|
if is_space != want_space {
|
||||||
continue;
|
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_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 {
|
let name_matches = match client.get(&name_url).bearer_auth(sender_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>()
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.is_some_and(|n| n["name"].as_str() == Some(HIVE_SPACE_NAME)),
|
.is_some_and(|n| n["name"].as_str() == Some(name)),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if name_matches {
|
if name_matches {
|
||||||
|
|
@ -368,6 +369,18 @@ async fn find_space_by_name(client: &reqwest::Client, sender_token: &str) -> Opt
|
||||||
None
|
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
|
/// 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_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).
|
/// `@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
|
/// 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, sender_token: &str) -> Option<String> {
|
async fn find_chat_room_by_name(client: &reqwest::Client, sender_token: &str) -> Option<String> {
|
||||||
let base = matrix_http()?;
|
find_joined_room(
|
||||||
let joined_url = format!("{base}/_matrix/client/v3/joined_rooms");
|
client,
|
||||||
let joined: serde_json::Value = client
|
matrix_http()?,
|
||||||
.get(&joined_url)
|
sender_token,
|
||||||
.bearer_auth(sender_token)
|
false,
|
||||||
.send()
|
HIVE_CHAT_ROOM_NAME,
|
||||||
.await
|
)
|
||||||
.ok()?
|
.await
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the state event still has to be written.
|
/// 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;
|
let outcome = refused_invite(response(500, "{}"), Some("join"), "@a:x", "!r:x").await;
|
||||||
assert!(outcome.is_err(), "a 500 is not excused by membership");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue