diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 2570510a..644efc9d 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -44,8 +44,6 @@ hivectl matrix create-user mara --password hunter2 # set a client-login passwor hivectl matrix sync-admin # provision / refresh the hive internal admin account hivectl matrix promote-user mara # promote an existing matrix user to homeserver admin hivectl matrix reset-password iris # generate and set a new random password for `iris`; prints it -hivectl matrix invite mara # invite a user to the hive Space -hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a specific room/alias ``` - `create-user`: for agents, persists the `access_token` to @@ -60,12 +58,6 @@ hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a spec - `reset-password`: calls the matrix admin API to set a new random password and prints it to stdout. Useful if an agent or human lost credentials. -- `invite`: invites a matrix user (full `@user:server` or a bare - localpart, qualified with the homeserver's `server_name`) to the hive - Space by default, or to a `--room` id / `#alias`. Uses the hive admin - token; the admin account must be a member of the target room with - invite power (it owns the hive Space, so that case always works). - Idempotent — already-member / already-invited is a no-op. ## Gateway diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 3cb9f157..06f8208a 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -195,19 +195,6 @@ enum MatrixCmd { /// Matrix localpart of the account to reset (e.g. `argus`). name: String, }, - /// Invite a matrix user to the hive Space (default) or a specific - /// room. Uses the hive admin token; the admin account must be a - /// member of the target room with invite power (it owns the hive - /// Space). Idempotent — already-member / already-invited is a no-op. - Invite { - /// User to invite: a full id (`@mara:server`) or a bare - /// localpart (qualified with the homeserver's `server_name`). - user: String, - /// Target room id (`!abc:server`) or alias (`#name:server`). - /// Omit to invite to the hive Space. - #[arg(long)] - room: Option, - }, } /// Default htpasswd file path — the host-side location of the gateway's @@ -313,7 +300,6 @@ async fn main() -> Result<()> { MatrixCmd::SyncAdmin => matrix_sync_admin().await, MatrixCmd::PromoteUser { name } => matrix_promote_user(&name).await, MatrixCmd::ResetPassword { name } => matrix_reset_password(&name).await, - MatrixCmd::Invite { user, room } => matrix_invite(&user, room.as_deref()).await, }, Cmd::Gateway { cmd } => match cmd { GatewayCmd::CreateUser { @@ -536,32 +522,6 @@ async fn matrix_promote_user(name: &str) -> Result<()> { Ok(()) } -async fn matrix_invite(user: &str, room: Option<&str>) -> Result<()> { - if !hive_c0re::matrix::is_present().await { - bail!( - "hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) first" - ); - } - let admin_token = hive_c0re::matrix::read_admin_token()?; - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .context("build reqwest client")?; - let server_name = hive_c0re::matrix::discover_server_name(&client) - .await - .context("discover matrix server_name")?; - let room_id = hive_c0re::matrix::invite_user(&client, &admin_token, user, room, &server_name) - .await - .with_context(|| format!("matrix invite {user}"))?; - let target = if user.starts_with('@') { - user.to_owned() - } else { - format!("@{user}:{server_name}") - }; - println!("matrix: invited {target} to {room_id}"); - Ok(()) -} - async fn matrix_reset_password(name: &str) -> Result<()> { if !hive_c0re::matrix::is_present().await { bail!( diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index e0bf1ebf..32397c6e 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -1108,24 +1108,12 @@ async fn invite_to_room( room_id: &str, localpart: &str, server_name: &str, -) -> Result<()> { - let user_id = format!("@{localpart}:{server_name}"); - invite_user_id(client, admin_token, room_id, &user_id).await -} - -/// Invite a fully-qualified Matrix user id (`@user:server`) to `room_id` -/// using the admin token. Idempotent: a 403 `M_FORBIDDEN` / `M_BAD_STATE` -/// (already a member or pending invite) is treated as success. -async fn invite_user_id( - client: &reqwest::Client, - admin_token: &str, - room_id: &str, - user_id: &str, ) -> Result<()> { // `:` must be percent-encoded in the room-id path segment; `!` is // permitted in URL path characters per RFC 3986. let encoded_room_id = room_id.replace(':', "%3A"); let url = format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/invite"); + let user_id = format!("@{localpart}:{server_name}"); let resp = client .post(&url) .bearer_auth(admin_token) @@ -1135,7 +1123,7 @@ async fn invite_user_id( .with_context(|| format!("matrix: POST /rooms/.../invite for {user_id}"))?; let status = resp.status(); if status.is_success() { - tracing::debug!(%user_id, %room_id, "matrix: invited to room"); + tracing::debug!(%user_id, %room_id, "matrix: invited to hive space"); return Ok(()); } // 403 with M_FORBIDDEN or M_BAD_STATE typically means the user is @@ -1153,77 +1141,6 @@ async fn invite_user_id( anyhow::bail!("matrix: invite {user_id} to {room_id}: HTTP {status}, body: {body}") } -/// 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`. -/// -/// # Errors -/// -/// Returns an error if the admin 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. -pub async fn invite_user( - client: &reqwest::Client, - admin_token: &str, - user: &str, - room_override: Option<&str>, - server_name: &str, -) -> Result { - // Qualify a bare localpart to a full user id on the hive homeserver. - let user_id = if user.starts_with('@') { - user.to_owned() - } 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('#') => resolve_room_alias(client, admin_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; \ - 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, admin_token, &room_id, &user_id).await?; - Ok(room_id) -} - -/// Resolve a `#alias:server` to its room id via the directory API. -async fn resolve_room_alias( - client: &reqwest::Client, - admin_token: &str, - alias: &str, -) -> Result { - let encoded = alias.replace('#', "%23").replace(':', "%3A"); - let url = format!("{MATRIX_HTTP}/_matrix/client/v3/directory/room/{encoded}"); - let resp = client - .get(&url) - .bearer_auth(admin_token) - .send() - .await - .with_context(|| format!("matrix: GET directory for {alias}"))?; - let status = resp.status(); - let json = resp.json::().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("matrix: resolve alias {alias}: HTTP {status}, body: {json}"); - } - json["room_id"] - .as_str() - .map(str::to_owned) - .ok_or_else(|| anyhow::anyhow!("matrix: alias {alias} response missing room_id: {json}")) -} - /// Sweep every existing container (manager + sub-agents) and ensure /// each has a matrix user + token on the local homeserver. Called once /// at hive-c0re startup, alongside `forge::ensure_all`. No-op when the