diff --git a/src/api/client_server/sync.rs b/src/api/client_server/sync.rs index 0bb7708c..d7dc5a17 100644 --- a/src/api/client_server/sync.rs +++ b/src/api/client_server/sync.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, sync::Arc, time::Duration, @@ -6,6 +7,7 @@ use std::{ use ruma::{ api::client::{ + error::ErrorKind, filter::{FilterDefinition, LazyLoadOptions}, sync::sync_events::{ self, @@ -30,7 +32,12 @@ use tracing::{debug, error}; use crate::{ service::{pdu::EventHash, rooms::timeline::PduCount}, - services, utils, Ar, Error, PduEvent, Ra, Result, + services, + utils::{ + self, + filter::{AllowDenyList, CompiledFilterDefinition}, + }, + Ar, Error, PduEvent, Ra, Result, }; /// # `GET /_matrix/client/r0/sync` @@ -96,6 +103,14 @@ pub(crate) async fn sync_events_route( .get_filter(&sender_user, &filter_id)? .unwrap_or_default(), }; + let Ok(compiled_filter) = CompiledFilterDefinition::try_from(&filter) + else { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "invalid 'filter' parameter", + ) + .into()); + }; let (lazy_load_enabled, lazy_load_send_redundant) = match filter.room.state.lazy_load_options { @@ -125,13 +140,25 @@ pub(crate) async fn sync_events_route( .filter_map(Result::ok), ); - let all_joined_rooms = services() - .rooms - .state_cache - .rooms_joined(&sender_user) - .collect::>(); + let room_filter = compiled_filter.room.rooms(); + + let mut all_joined_rooms = Vec::new(); + if let AllowDenyList::Allow(allow_set) = room_filter { + for &room_id in allow_set { + if services().rooms.state_cache.is_joined(&sender_user, room_id)? { + all_joined_rooms.push(Cow::Borrowed(room_id)); + } + } + } else { + for result in services().rooms.state_cache.rooms_joined(&sender_user) { + let room_id = result?; + if room_filter.allowed(&room_id) { + all_joined_rooms.push(Cow::Owned(room_id)); + } + } + } + for room_id in all_joined_rooms { - let room_id = room_id?; if let Ok(joined_room) = load_joined_room( &sender_user, &sender_device, @@ -149,17 +176,31 @@ pub(crate) async fn sync_events_route( .await { if !joined_room.is_empty() { - joined_rooms.insert(room_id.clone(), joined_room); + joined_rooms.insert(room_id.into_owned(), joined_room); } } } let mut left_rooms = BTreeMap::new(); - let all_left_rooms: Vec<_> = - services().rooms.state_cache.rooms_left(&sender_user).collect(); - for result in all_left_rooms { + let mut all_left_rooms = Vec::new(); + if let AllowDenyList::Allow(allow_set) = room_filter { + for &room_id in allow_set { + if services().rooms.state_cache.is_left(&sender_user, room_id)? { + all_left_rooms.push(room_id.to_owned()); + } + } + } else { + for result in services().rooms.state_cache.rooms_left(&sender_user) { + let (room_id, _) = result?; + if room_filter.allowed(&room_id) { + all_left_rooms.push(room_id); + } + } + } + + for room_id in all_left_rooms { handle_left_room( - result?.0, + room_id, &sender_user, &mut left_rooms, since, @@ -171,11 +212,29 @@ pub(crate) async fn sync_events_route( } let mut invited_rooms = BTreeMap::new(); - let all_invited_rooms: Vec<_> = - services().rooms.state_cache.rooms_invited(&sender_user).collect(); - for result in all_invited_rooms { - let (room_id, invite_state_events) = result?; + let mut all_invited_rooms = Vec::new(); + if let AllowDenyList::Allow(allow_set) = room_filter { + for &room_id in allow_set { + if let Some(invite_state_events) = services() + .rooms + .state_cache + .invite_state(&sender_user, room_id)? + { + all_invited_rooms + .push((Cow::Borrowed(room_id), invite_state_events)); + } + } + } else { + for result in services().rooms.state_cache.rooms_invited(&sender_user) { + let (room_id, invite_state_events) = result?; + if room_filter.allowed(&room_id) { + all_invited_rooms + .push((Cow::Owned(room_id), invite_state_events)); + } + } + } + for (room_id, invite_state_events) in all_invited_rooms { { // Get and drop the lock to wait for remaining operations to finish let mutex_insert = Arc::clone( @@ -184,7 +243,7 @@ pub(crate) async fn sync_events_route( .roomid_mutex_insert .write() .await - .entry(room_id.clone()) + .entry(room_id.clone().into_owned()) .or_default(), ); let insert_lock = mutex_insert.lock().await; @@ -202,7 +261,7 @@ pub(crate) async fn sync_events_route( } invited_rooms.insert( - room_id.clone(), + room_id.into_owned(), InvitedRoom { invite_state: InviteState { events: invite_state_events, diff --git a/src/utils/filter.rs b/src/utils/filter.rs index d3a193d8..dded8f89 100644 --- a/src/utils/filter.rs +++ b/src/utils/filter.rs @@ -10,13 +10,17 @@ //! The first exception is room filters (`room`/`not_room` pairs in //! `filter.rooms` and `filter.rooms.{account_data,timeline,ephemeral,state}`). //! In `/messages`, if the room is rejected by the filter, we can skip the -//! entire request. +//! entire request. The outer loop of our `/sync` implementation is over rooms, +//! and so we are able to skip work for an entire room if it is rejected by the +//! top-level `filter.rooms.room`. use std::{collections::HashSet, hash::Hash}; use regex::RegexSet; use ruma::{ - api::client::filter::{RoomEventFilter, UrlFilter}, + api::client::filter::{ + FilterDefinition, RoomEventFilter, RoomFilter, UrlFilter, + }, RoomId, UserId, }; @@ -139,6 +143,21 @@ impl WildcardAllowDenyList { } } +/// Wrapper for a [`ruma::api::client::filter::FilterDefinition`], preprocessed +/// to allow checking against the filter efficiently. +/// +/// The preprocessing consists of merging the `X` and `not_X` pairs into +/// combined structures. For most fields, this is a [`AllowDenyList`]. For +/// `types`/`not_types`, this is a [`WildcardAllowDenyList`], because the type +/// filter fields support `'*'` wildcards. +pub(crate) struct CompiledFilterDefinition<'a> { + pub(crate) room: CompiledRoomFilter<'a>, +} + +pub(crate) struct CompiledRoomFilter<'a> { + rooms: AllowDenyList<'a, RoomId>, +} + pub(crate) struct CompiledRoomEventFilter<'a> { // TODO: consider falling back a more-efficient // AllowDenyList when none of the type patterns @@ -149,6 +168,35 @@ pub(crate) struct CompiledRoomEventFilter<'a> { url_filter: Option, } +impl<'a> TryFrom<&'a FilterDefinition> for CompiledFilterDefinition<'a> { + type Error = Error; + + fn try_from( + source: &'a FilterDefinition, + ) -> Result, Error> { + Ok(CompiledFilterDefinition { + room: (&source.room).try_into()?, + }) + } +} + +impl<'a> TryFrom<&'a RoomFilter> for CompiledRoomFilter<'a> { + type Error = Error; + + fn try_from( + source: &'a RoomFilter, + ) -> Result, Error> { + Ok(CompiledRoomFilter { + // TODO: consider calculating the intersection of room filters in + // all of the sub-filters + rooms: AllowDenyList::from_slices( + source.rooms.as_deref(), + &source.not_rooms, + ), + }) + } +} + impl<'a> TryFrom<&'a RoomEventFilter> for CompiledRoomEventFilter<'a> { type Error = Error; @@ -173,6 +221,18 @@ impl<'a> TryFrom<&'a RoomEventFilter> for CompiledRoomEventFilter<'a> { } } +impl CompiledRoomFilter<'_> { + /// Returns the top-level [`AllowDenyList`] for rooms (`rooms`/`not_rooms` + /// in `filter.room`). + /// + /// This is useful because, with an allowlist, iterating over allowed rooms + /// and checking whether they are visible to a user can be faster than + /// iterating over visible rooms and checking whether they are allowed. + pub(crate) fn rooms(&self) -> &AllowDenyList<'_, RoomId> { + &self.rooms + } +} + impl CompiledRoomEventFilter<'_> { /// Returns `true` if a room is allowed by the `rooms` and `not_rooms` /// fields.