matrix: skip room invite when the user is already invited or joined

This commit is contained in:
damocles 2026-06-13 11:00:28 +02:00 committed by mara
commit d090df3c36

View file

@ -1353,9 +1353,41 @@ async fn invite_to_room(
invite_user_id(client, admin_token, room_id, &user_id).await
}
/// Fetch a user's current membership in a room via the admin token, or
/// `None` if there is no membership event (never invited) or the lookup
/// fails. Returns the raw membership string (`invite`, `join`, `leave`, …).
async fn room_membership(
client: &reqwest::Client,
admin_token: &str,
encoded_room_id: &str,
user_id: &str,
) -> Option<String> {
// `:` must be percent-encoded in both the room-id and user-id path
// segments; `@` and `!` are permitted path characters per RFC 3986.
let encoded_user = user_id.replace(':', "%3A");
let url = format!(
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/state/m.room.member/{encoded_user}"
);
let resp = client
.get(&url)
.bearer_auth(admin_token)
.send()
.await
.ok()?;
if !resp.status().is_success() {
// 404 = no membership event yet; anything else we treat as "unknown"
// and let the caller fall through to the invite attempt.
return None;
}
let body = resp.json::<serde_json::Value>().await.ok()?;
body["membership"].as_str().map(ToOwned::to_owned)
}
/// 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.
/// using the admin token. Idempotent: a user who is already a member or
/// already has a pending invite is left untouched (no fresh invite is sent,
/// so they are not re-notified), and a 403 `M_FORBIDDEN` / `M_BAD_STATE`
/// from a racing invite is still treated as success.
async fn invite_user_id(
client: &reqwest::Client,
admin_token: &str,
@ -1365,6 +1397,17 @@ async fn invite_user_id(
// `:` 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");
// Skip the invite entirely when the user is already invited or joined.
// Re-POSTing an invite to a pending member re-sends the invite event,
// which re-notifies the agent on every provisioning sweep.
if let Some(membership) = room_membership(client, admin_token, &encoded_room_id, user_id).await
&& matches!(membership.as_str(), "invite" | "join")
{
tracing::debug!(%user_id, %room_id, %membership, "matrix: invite skipped (already a member/invited)");
return Ok(());
}
let url = format!("{MATRIX_HTTP}/_matrix/client/v3/rooms/{encoded_room_id}/invite");
let resp = client
.post(&url)