Matrix: invite somebody into a room

Closes #74. This client could accept an invitation and decline one, and
never send one - so a room created here could never gain a second member
here, and every other member of a room was somebody who had arrived by
some other route.

It sits beside kick and ban because it is the same shape of call decided
by the same authority: a room's power levels say who may invite exactly as
they say who may remove.

A Matrix address or nothing. The server would refuse anything else anyway,
but its refusal names an endpoint rather than the thing that was typed.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-02 08:22:35 -04:00
co-authored by Claude Opus 5
parent 78019ca762
commit 1ef66ab532
2 changed files with 43 additions and 0 deletions
+17
View File
@@ -85,6 +85,23 @@ pub async fn kick_member(state: &AppState, account_id: &str, buffer_id: &str, ta
Ok(())
}
/// Asks somebody into a room.
///
/// Lives here beside kick and ban because it is the same shape of call and
/// the same authority decides it - a room's power levels say who may invite
/// exactly as they say who may remove. It is the opposite action, which is
/// why its absence was odd: a room made here could gain no second member
/// from here.
pub async fn invite_member(state: &AppState, account_id: &str, buffer_id: &str, target_user_id: &str) -> Result<()> {
let (homeserver_url, access_token, room_id) = homeserver_token_room(state, account_id, buffer_id).await?;
let base = homeserver_url.trim_end_matches('/');
let url = format!("{base}/_matrix/client/v3/rooms/{}/invite", url::form_urlencoded::byte_serialize(room_id.as_bytes()).collect::<String>());
http::post_json(&url, Some(&access_token), serde_json::json!({ "user_id": target_user_id }))
.await
.context("inviting member")?;
Ok(())
}
pub async fn ban_member(state: &AppState, account_id: &str, buffer_id: &str, target_user_id: &str, reason: Option<&str>) -> Result<()> {
let (homeserver_url, access_token, room_id) = homeserver_token_room(state, account_id, buffer_id).await?;
let base = homeserver_url.trim_end_matches('/');
+26
View File
@@ -2223,6 +2223,32 @@ pub async fn dispatch(
}
}
// Asking somebody into a room. Its absence was the odd one out: this
// client could accept an invitation and decline one, and never send
// one - so a room created here could never gain a second member here.
"inviteMatrixMember" => {
let (account_id, buffer_id, user_id) =
match (p_str_opt(params, "accountId"), p_str_opt(params, "bufferId"), p_str_opt(params, "userId")) {
(Some(a), Some(b), Some(u)) => (a, b, u),
_ => {
return (
None,
Some("inviteMatrixMember requires \"accountId\", \"bufferId\" and \"userId\"".to_string()),
)
}
};
// A Matrix id or nothing. The server would refuse anything else
// anyway, but its error names an endpoint rather than the thing
// that was typed.
if !user_id.starts_with('@') || !user_id.contains(':') {
return (None, Some(format!("\"{user_id}\" is not a Matrix address - they look like @someone:server")));
}
match backend::matrix::moderation::invite_member(state, account_id, buffer_id, user_id).await {
Ok(()) => (Some(ok_node()), None),
Err(e) => (None, Some(format!("{e:#}"))),
}
}
"banMatrixMember" => {
let (account_id, buffer_id, user_id) =
match (p_str_opt(params, "accountId"), p_str_opt(params, "bufferId"), p_str_opt(params, "userId")) {