From 7f37f7f8b08d980ff4dea6bef0d9a085169b0e0c Mon Sep 17 00:00:00 2001 From: Ginger Date: Thu, 11 Sep 2025 14:54:12 -0400 Subject: [PATCH] fix: Correctly filter out ignored invites in /sync --- src/api/client/membership/invite.rs | 31 +++++++++------- src/api/client/message.rs | 49 +++++++++++++------------ src/api/client/room/create.rs | 2 +- src/api/client/sync/v3.rs | 12 +++++- src/api/client/sync/v4.rs | 10 ++++- src/api/client/sync/v5.rs | 10 ++++- src/api/server/invite.rs | 9 ----- src/database/maps.rs | 4 ++ src/service/rooms/state_cache/mod.rs | 15 +++++++- src/service/rooms/state_cache/update.rs | 15 ++++++-- 10 files changed, 102 insertions(+), 55 deletions(-) diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index 1c124b5be..f0c412116 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -53,13 +53,8 @@ pub(crate) async fn invite_user_route( | invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => { let sender_filter_level = services .users - .invite_filter_level(recipient_user, sender_user); - let recipient_filter_level = services - .users - .invite_filter_level(sender_user, recipient_user); - - let (sender_filter_level, recipient_filter_level) = - join!(sender_filter_level, recipient_filter_level); + .invite_filter_level(recipient_user, sender_user) + .await; if !matches!(sender_filter_level, FilterLevel::Allow) { // drop invites if the sender has the recipient filtered @@ -77,13 +72,21 @@ pub(crate) async fn invite_user_route( } } - // return an error for blocked invites. ignored invites aren't handled here - // since the recipient's membership should still be changed to `invite`. - // they're filtered out in api::client::message::is_ignored_pdu - if matches!(recipient_filter_level, FilterLevel::Block) { - return Err!(Request(InviteBlocked( - "{recipient_user} has blocked invites from you." - ))); + // check for blocked invites if the recipient is a local user. + if services.globals.user_is_local(&recipient_user) { + let recipient_filter_level = services + .users + .invite_filter_level(sender_user, recipient_user) + .await; + + // ignored invites aren't handled here + // since the recipient's membership should still be changed to `invite`. + // they're filtered out in the individual /sync handlers. + if matches!(recipient_filter_level, FilterLevel::Block) { + return Err!(Request(InviteBlocked( + "{recipient_user} has blocked invites from you." + ))); + } } invite_helper( diff --git a/src/api/client/message.rs b/src/api/client/message.rs index 7adef55d5..7c1ee19f9 100644 --- a/src/api/client/message.rs +++ b/src/api/client/message.rs @@ -7,7 +7,7 @@ use conduwuit::{ }, ref_at, utils::{ - IterStream, ReadyExt, + IterStream, ReadyExt, TryFutureExtExt, result::LogErr, stream::{BroadbandExt, TryIgnore, WidebandExt}, }, @@ -22,7 +22,7 @@ use conduwuit_service::{ }; use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, pin_mut}; use ruma::{ - DeviceId, RoomId, UserId, + DeviceId, OwnedRoomId, RoomId, UserId, api::{ Direction, client::{filter::RoomEventFilter, message::get_message_events}, @@ -305,28 +305,6 @@ where return true; } - if *event.kind() == RoomMember { - if event - .get_content::() - .is_ok_and(|content| content.membership == MembershipState::Invite) - { - if event - .state_key() - .is_some_and(|key| key == recipient_user.as_str()) - { - if services - .users - .invite_filter_level(sender_user, recipient_user) - .await == FilterLevel::Ignore - { - // this PDU is inviting the recipient to a room and is from a sender that the - // recipient has ignored invites from - return true; - } - } - } - } - false } @@ -352,6 +330,29 @@ pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Opti filter.matches(pdu).then_some(item) } +#[inline] +pub(crate) async fn is_ignored_invite( + services: &Services, + recipient_user: &UserId, + room_id: &RoomId, +) -> bool { + let Ok(sender_user) = services + .rooms + .state_cache + .invite_sender(recipient_user, room_id) + .await + else { + // the invite may have been sent before the invite_sender table existed. + // assume it's not ignored + return false; + }; + + services + .users + .invite_filter_level(&sender_user, recipient_user) + .await == FilterLevel::Ignore +} + #[cfg_attr(debug_assertions, ctor::ctor)] fn _is_sorted() { debug_assert!( diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index fb9492906..6b7f32749 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -119,7 +119,7 @@ pub(crate) async fn create_room_route( // if the recipient of the invite is local and has the sender blocked, error // out. if the recipient is remote we can't tell yet, and if they're local and // have the sender _ignored_ their invite will be filtered out in - // api::client::message::is_ignored_pdu + // the handlers for the individual /sync endpoints if services.globals.user_is_local(recipient_user) && matches!( services diff --git a/src/api/client/sync/v3.rs b/src/api/client/sync/v3.rs index 298a6e4b3..d70cfe44a 100644 --- a/src/api/client/sync/v3.rs +++ b/src/api/client/sync/v3.rs @@ -60,7 +60,10 @@ use ruma::{ use service::rooms::short::{ShortEventId, ShortStateKey}; use super::{load_timeline, share_encrypted_room}; -use crate::{Ruma, RumaResponse, client::ignored_filter}; +use crate::{ + Ruma, RumaResponse, + client::{ignored_filter, is_ignored_invite}, +}; #[derive(Default)] struct StateChanges { @@ -238,6 +241,13 @@ pub(crate) async fn build_sync_events( .rooms .state_cache .rooms_invited(sender_user) + .wide_filter_map(async |(room_id, invite_state)| { + if is_ignored_invite(&services, sender_user, &room_id).await { + None + } else { + Some((room_id, invite_state)) + } + }) .fold_default(|mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| async move { let invite_count = services .rooms diff --git a/src/api/client/sync/v4.rs b/src/api/client/sync/v4.rs index a16e4526d..fcf317bd8 100644 --- a/src/api/client/sync/v4.rs +++ b/src/api/client/sync/v4.rs @@ -11,6 +11,7 @@ use conduwuit::{ utils::{ BoolExt, IterStream, ReadyExt, TryFutureExtExt, math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated}, + stream::WidebandExt, }, warn, }; @@ -39,7 +40,7 @@ use ruma::{ use super::{load_timeline, share_encrypted_room}; use crate::{ Ruma, - client::{DEFAULT_BUMP_TYPES, ignored_filter}, + client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite}, }; type TodoRooms = BTreeMap, usize, u64)>; @@ -102,6 +103,13 @@ pub(crate) async fn sync_events_v4_route( .rooms .state_cache .rooms_invited(sender_user) + .wide_filter_map(async |(room_id, invite_state)| { + if is_ignored_invite(&services, sender_user, &room_id).await { + None + } else { + Some((room_id, invite_state)) + } + }) .map(|r| r.0) .collect() .await; diff --git a/src/api/client/sync/v5.rs b/src/api/client/sync/v5.rs index e4cefba0f..445a93ac9 100644 --- a/src/api/client/sync/v5.rs +++ b/src/api/client/sync/v5.rs @@ -14,6 +14,7 @@ use conduwuit::{ BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt, future::ReadyEqExt, math::{ruma_from_usize, usize_from_ruma}, + stream::WidebandExt, }, warn, }; @@ -38,7 +39,7 @@ use ruma::{ use super::share_encrypted_room; use crate::{ Ruma, - client::{DEFAULT_BUMP_TYPES, ignored_filter, sync::load_timeline}, + client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite, sync::load_timeline}, }; type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request); @@ -106,6 +107,13 @@ pub(crate) async fn sync_events_v5_route( .rooms .state_cache .rooms_invited(sender_user) + .wide_filter_map(async |(room_id, invite_state)| { + if is_ignored_invite(&services, sender_user, &room_id).await { + None + } else { + Some((room_id, invite_state)) + } + }) .map(|r| r.0) .collect::>(); diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 261e2e099..1a75a2f71 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -116,15 +116,6 @@ pub(crate) async fn create_invite_route( .invite_filter_level(sender_user, &recipient_user) .await; - if matches!(recipient_invite_filter_level, FilterLevel::Block) { - // return an error for blocked invites. ignored invites aren't handled here - // since the recipient's membership should still be changed to `invite`. - // they're filtered out in api::client::message::is_ignored_pdu - return Err!(Request(InviteBlocked( - "{recipient_user} has blocked invites from {sender_user}." - ))); - } - let mut invite_state = body.invite_room_state.clone(); let mut event: JsonObject = serde_json::from_str(body.event.get()) diff --git a/src/database/maps.rs b/src/database/maps.rs index da97ef456..eee5f5e46 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -434,4 +434,8 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userroomid_notificationcount", ..descriptor::RANDOM }, + Descriptor { + name: "userroomid_invitesender", + ..descriptor::RANDOM_SMALL + }, ]; diff --git a/src/service/rooms/state_cache/mod.rs b/src/service/rooms/state_cache/mod.rs index 2d8f5cc59..6874cc801 100644 --- a/src/service/rooms/state_cache/mod.rs +++ b/src/service/rooms/state_cache/mod.rs @@ -12,7 +12,7 @@ use conduwuit::{ use database::{Deserialized, Ignore, Interfix, Map}; use futures::{Stream, StreamExt, future::join5, pin_mut}; use ruma::{ - OwnedRoomId, RoomId, ServerName, UserId, + OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId, events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState}, serde::Raw, }; @@ -49,6 +49,7 @@ struct Data { userroomid_joined: Arc, userroomid_leftstate: Arc, userroomid_knockedstate: Arc, + userroomid_invitesender: Arc, } type AppServiceInRoomCache = SyncRwLock>>; @@ -83,6 +84,7 @@ impl crate::Service for Service { userroomid_joined: args.db["userroomid_joined"].clone(), userroomid_leftstate: args.db["userroomid_leftstate"].clone(), userroomid_knockedstate: args.db["userroomid_knockedstate"].clone(), + userroomid_invitesender: args.db["userroomid_invitesender"].clone(), }, })) } @@ -523,3 +525,14 @@ pub async fn is_left(&self, user_id: &UserId, room_id: &RoomId) -> bool { let key = (user_id, room_id); self.db.userroomid_leftstate.qry(&key).await.is_ok() } + +#[implement(Service)] +#[tracing::instrument(skip(self), level = "trace")] +pub async fn invite_sender(&self, user_id: &UserId, room_id: &RoomId) -> Result { + let key = (user_id, room_id); + self.db + .userroomid_invitesender + .qry(&key) + .await + .deserialized() +} diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 6ecc9e54c..295bd9e75 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -124,7 +124,7 @@ pub async fn update_membership( | MembershipState::Invite => { // return an error for blocked invites. ignored invites aren't handled here // since the recipient's membership should still be changed to `invite`. - // they're filtered out in api::client::message::is_ignored_pdu + // they're filtered out in the individual /sync handlers if matches!( self.services .users @@ -132,9 +132,11 @@ pub async fn update_membership( .await, FilterLevel::Block ) { - return Err!(Request(InviteBlocked("{user_id} has blocked invites from you."))); + return Err!(Request(InviteBlocked( + "{user_id} has blocked invites from {sender}." + ))); } - self.mark_as_invited(user_id, room_id, last_state, invite_via) + self.mark_as_invited(user_id, room_id, sender, last_state, invite_via) .await; }, | MembershipState::Leave | MembershipState::Ban => { @@ -239,6 +241,7 @@ pub fn mark_as_joined(&self, user_id: &UserId, room_id: &RoomId) { self.db.userroomid_invitestate.remove(&userroom_id); self.db.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_id); self.db.roomuserid_leftcount.remove(&roomuser_id); @@ -276,6 +279,7 @@ pub fn mark_as_left(&self, user_id: &UserId, room_id: &RoomId) { self.db.userroomid_invitestate.remove(&userroom_id); self.db.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_knockedstate.remove(&userroom_id); self.db.roomuserid_knockedcount.remove(&roomuser_id); @@ -312,6 +316,7 @@ pub fn mark_as_knocked( self.db.userroomid_invitestate.remove(&userroom_id); self.db.roomuserid_invitecount.remove(&roomuser_id); + self.db.userroomid_invitesender.remove(&userroom_id); self.db.userroomid_leftstate.remove(&userroom_id); self.db.roomuserid_leftcount.remove(&roomuser_id); @@ -343,6 +348,7 @@ pub async fn mark_as_invited( &self, user_id: &UserId, room_id: &RoomId, + sender_user: &UserId, last_state: Option>>, invite_via: Option>, ) { @@ -358,6 +364,9 @@ pub async fn mark_as_invited( self.db .roomuserid_invitecount .raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap()); + self.db + .userroomid_invitesender + .raw_put(&userroom_id, sender_user); self.db.userroomid_joined.remove(&userroom_id); self.db.roomuserid_joined.remove(&roomuser_id);