fix: rediscover hive Space by hardcoded name, not a room alias
Per mara's review: drop the #hive:<server> room alias (special chars) and
rediscover the canonical Space by its hardcoded plain name instead.
ensure_hive_space dedup is now:
1. room-id file present -> reuse it
2. else scan the admin's joined rooms for the m.space named HIVE_SPACE_NAME
('hive') and adopt the first match (re-persisting the file) -> recovers
the existing space after a state wipe instead of creating a duplicate
3. else createRoom (plain name, no alias)
find_space_by_name walks /joined_rooms and checks each room's m.room.create
type == m.space and m.room.name == 'hive'. No alias, no special-char anchor.
server_name is no longer needed by ensure_hive_space (dropped the param).
This commit is contained in:
parent
134a40a5e2
commit
d043b1ed4e
1 changed files with 78 additions and 64 deletions
|
|
@ -45,11 +45,11 @@ const PASSWORD_BYTES: usize = 32;
|
|||
/// first registered user automatically. Not an agent; has no state dir.
|
||||
pub const HIVE_ADMIN_LOCALPART: &str = "hive";
|
||||
|
||||
/// Canonical room-alias localpart for the hive Space (`#hive:<server>`).
|
||||
/// The Space is anchored to this alias at creation so it stays discoverable
|
||||
/// even if the persisted room-id file is lost (a full state wipe), preventing
|
||||
/// duplicate spaces from being created on the next sweep.
|
||||
pub const HIVE_SPACE_ALIAS_LOCALPART: &str = "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 hive admin matrix access token. Outside every
|
||||
/// purgeable path — not deleted by `destroy --purge` on any agent.
|
||||
|
|
@ -964,95 +964,109 @@ fn persist_space_room_id(room_id: &str) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a room alias (`#name:server`) to its room id via the directory
|
||||
/// API. Returns `None` on any non-success (e.g. 404 = alias unknown).
|
||||
async fn resolve_room_alias(
|
||||
client: &reqwest::Client,
|
||||
admin_token: &str,
|
||||
server_name: &str,
|
||||
) -> Option<String> {
|
||||
let encoded = format!("%23{HIVE_SPACE_ALIAS_LOCALPART}%3A{server_name}");
|
||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded}");
|
||||
let resp = client.get(&url).bearer_auth(admin_token).send().await.ok()?;
|
||||
if !resp.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
let json = resp.json::<serde_json::Value>().await.ok()?;
|
||||
json["room_id"].as_str().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Best-effort: point the canonical `#hive:server` alias at `room_id`.
|
||||
/// Idempotent — a 409 (alias already maps here) is treated as success; any
|
||||
/// other failure is logged at debug and ignored (the room-id file remains
|
||||
/// the primary anchor).
|
||||
async fn ensure_room_alias(
|
||||
client: &reqwest::Client,
|
||||
admin_token: &str,
|
||||
server_name: &str,
|
||||
room_id: &str,
|
||||
) {
|
||||
let encoded = format!("%23{HIVE_SPACE_ALIAS_LOCALPART}%3A{server_name}");
|
||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded}");
|
||||
let resp = client
|
||||
.put(&url)
|
||||
/// 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, admin_token: &str) -> Option<String> {
|
||||
let joined_url = format!("{MATRIX_HTTP}/_matrix/client/v3/joined_rooms");
|
||||
let joined: serde_json::Value = client
|
||||
.get(&joined_url)
|
||||
.bearer_auth(admin_token)
|
||||
.json(&serde_json::json!({ "room_id": room_id }))
|
||||
.send()
|
||||
.await;
|
||||
match resp {
|
||||
Ok(r) if r.status().is_success() || r.status() == StatusCode::CONFLICT => {}
|
||||
Ok(r) => tracing::debug!(status = %r.status(), %room_id, "matrix: set hive space alias non-fatal"),
|
||||
Err(e) => tracing::debug!(error = ?e, "matrix: set hive space alias failed (non-fatal)"),
|
||||
.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);
|
||||
// Must be an m.space (m.room.create `type`).
|
||||
let create_url =
|
||||
format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded}/state/m.room.create/");
|
||||
let is_space = match client
|
||||
.get(&create_url)
|
||||
.bearer_auth(admin_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_SPACE_NAME (m.room.name `name`).
|
||||
let name_url =
|
||||
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 {
|
||||
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)),
|
||||
_ => false,
|
||||
};
|
||||
if name_matches {
|
||||
return Some(room_id.to_owned());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 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`, anchored to the canonical alias `#hive:<server>`.
|
||||
/// `@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 and heal the alias mapping
|
||||
/// best-effort (so the alias keeps pointing at the canonical room).
|
||||
/// 2. Otherwise, resolve `#hive:<server>` — if it exists, adopt it and
|
||||
/// re-persist 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` (with the alias).
|
||||
/// 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,
|
||||
admin_token: &str,
|
||||
server_name: &str,
|
||||
) -> Result<String> {
|
||||
// 1. Stored room id wins. Heal the alias so it stays discoverable.
|
||||
pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> {
|
||||
// 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() {
|
||||
ensure_room_alias(client, admin_token, server_name, &trimmed).await;
|
||||
tracing::debug!(room_id = %trimmed, "matrix: hive space already provisioned");
|
||||
return Ok(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. No stored id — recover the existing space via its canonical alias
|
||||
// 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) = resolve_room_alias(client, admin_token, server_name).await {
|
||||
if let Some(room_id) = find_space_by_name(client, admin_token).await {
|
||||
persist_space_room_id(&room_id)?;
|
||||
tracing::info!(%room_id, "matrix: recovered hive space via #hive alias");
|
||||
tracing::info!(%room_id, "matrix: recovered hive space by name");
|
||||
return Ok(room_id);
|
||||
}
|
||||
|
||||
// 3. Create the space, anchored to the canonical alias.
|
||||
// 3. Create the space (plain hardcoded name, no alias).
|
||||
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/createRoom");
|
||||
let body = serde_json::json!({
|
||||
"name": "hive",
|
||||
"name": HIVE_SPACE_NAME,
|
||||
"creation_content": { "type": "m.space" },
|
||||
"preset": "private_chat",
|
||||
"visibility": "private",
|
||||
"room_alias_name": HIVE_SPACE_ALIAS_LOCALPART,
|
||||
});
|
||||
let resp = client
|
||||
.post(&url)
|
||||
|
|
@ -1176,7 +1190,7 @@ pub async fn ensure_all() {
|
|||
return;
|
||||
}
|
||||
};
|
||||
// server_name first — both the space alias and the agent invites need it.
|
||||
// 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) => {
|
||||
|
|
@ -1184,7 +1198,7 @@ pub async fn ensure_all() {
|
|||
return;
|
||||
}
|
||||
};
|
||||
let room_id = match ensure_hive_space(&client, &admin_token, &server_name).await {
|
||||
let room_id = match ensure_hive_space(&client, &admin_token).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "matrix: ensure_hive_space failed");
|
||||
|
|
|
|||
Loading…
Reference in a new issue