From 2728a02369d41b6242471043832400ab2c968819 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 7 Nov 2026 10:50:32 +0100 Subject: [PATCH] matrix-mcp: join invites from the own homeserver automatically --- hive-matrix-mcp/src/timeline.rs | 66 ++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/hive-matrix-mcp/src/timeline.rs b/hive-matrix-mcp/src/timeline.rs index 56b2decd..fc7233d7 100644 --- a/hive-matrix-mcp/src/timeline.rs +++ b/hive-matrix-mcp/src/timeline.rs @@ -17,7 +17,10 @@ use std::collections::HashSet; use std::hash::BuildHasher; -use matrix_sdk::{Client, ruma::OwnedRoomId}; +use matrix_sdk::{ + Client, + ruma::{OwnedRoomId, ServerName, UserId}, +}; use tokio::sync::Mutex; use crate::{handlers, wake}; @@ -127,13 +130,41 @@ pub async fn sweep_invites( // from the unread sweep's `` key) so the two never collide; the // harness coalesces the wake iff the summary is new or changed, so // re-upserting an unchanged invite every tick is a cheap no-op. + let own_server = client.user_id().map(|u| u.server_name().to_owned()); for room in ¤t { let room_id = room.room_id().to_owned(); + let inviter = room + .invite_details() + .await + .ok() + .and_then(|d| d.inviter) + .map(|m| m.user_id().to_owned()); + // An invite from the swarm's own homeserver is trusted: join it + // without waking the agent. A failed join falls through to the todo + // below, so the invite is never lost. + if is_local(own_server.as_deref(), inviter.as_deref()) { + match room.join().await { + Ok(()) => { + tracing::info!(room = %room_id, inviter = ?inviter, "matrix: auto-joined an invite from the swarm homeserver"); + continue; + } + Err(e) => { + tracing::warn!(error = %e, room = %room_id, "matrix: auto-join failed; leaving the invite as a todo"); + } + } + } let label = room.name().unwrap_or_else(|| room_id.to_string()); + let source = match &inviter { + Some(u) if !is_local(own_server.as_deref(), Some(u)) => { + format!("an external matrix user ({u})") + } + Some(u) => u.to_string(), + None => "an unknown inviter".to_owned(), + }; let summary = wake::tag_account( account_tag, format!( - "[matrix] invited to {label} ({room_id}) — \ + "[matrix] invited to {label} ({room_id}) by {source} — \ use list_invites to see pending invites, resolve_invite to accept or reject" ), ); @@ -148,6 +179,12 @@ pub async fn sweep_invites( } } +/// Whether `inviter` is a user of the agent's own homeserver. No known +/// inviter or no known own server counts as external. +fn is_local(own: Option<&ServerName>, inviter: Option<&UserId>) -> bool { + matches!((own, inviter), (Some(own), Some(u)) if u.server_name() == own) +} + /// Todo dedup key for a pending invite. Namespaced with an `invite:` /// prefix so an invited room and an unread room (which the unread sweep /// keys by bare room id) never share a todo row. `pub(crate)` so the @@ -155,3 +192,28 @@ pub async fn sweep_invites( pub(crate) fn invite_key(room_id: &matrix_sdk::ruma::RoomId) -> String { format!("invite:{room_id}") } + +#[cfg(test)] +mod invite_tests { + use matrix_sdk::ruma::{server_name, user_id}; + + use super::is_local; + + #[test] + fn only_an_inviter_on_the_own_homeserver_is_local() { + let own = server_name!("pr1ma.darkest.space"); + assert!(is_local( + Some(own), + Some(user_id!("@mara:pr1ma.darkest.space")) + )); + assert!(!is_local(Some(own), Some(user_id!("@mara:matrix.org")))); + // A look-alike server name is not the own homeserver. + assert!(!is_local( + Some(own), + Some(user_id!("@x:pr1ma.darkest.space.evil.example")) + )); + // Anything unknown is external. + assert!(!is_local(Some(own), None)); + assert!(!is_local(None, Some(user_id!("@mara:pr1ma.darkest.space")))); + } +}