diff --git a/src/api/client_server/message.rs b/src/api/client_server/message.rs index 3a30ed15..7033a768 100644 --- a/src/api/client_server/message.rs +++ b/src/api/client_server/message.rs @@ -16,7 +16,9 @@ use serde_json::{from_str, Value}; use crate::{ service::{pdu::PduBuilder, rooms::timeline::PduCount}, - services, utils, Error, PduEvent, Result, Ruma, + services, utils, + utils::filter::CompiledRoomEventFilter, + Error, PduEvent, Result, Ruma, }; /// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}` @@ -125,6 +127,10 @@ pub(crate) async fn get_message_events_route( let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated"); + let Ok(filter) = CompiledRoomEventFilter::try_from(&body.filter) else { + return Err(Error::BadRequest(ErrorKind::InvalidParam, "invalid 'filter' parameter")); + }; + let from = match body.from.clone() { Some(from) => PduCount::try_from_string(&from)?, None => match body.dir { @@ -133,6 +139,15 @@ pub(crate) async fn get_message_events_route( }, }; + if !filter.room_allowed(&body.room_id) { + return Ok(get_message_events::v3::Response { + start: from.stringify(), + end: None, + chunk: vec![], + state: vec![], + }); + } + let to = body .to .as_ref() diff --git a/src/utils/filter.rs b/src/utils/filter.rs new file mode 100644 index 00000000..3e5b4131 --- /dev/null +++ b/src/utils/filter.rs @@ -0,0 +1,84 @@ +//! Helper tools for implementing filtering in the `/client/v3/sync` and +//! `/client/v3/rooms/:roomId/messages` endpoints. +//! +//! The default strategy for filtering is to generate all events, check them +//! against the filter, and drop events that were rejected. When significant +//! fraction of events are rejected, this results in a large amount of wasted +//! work computing events that will be dropped. In most cases, the structure of +//! our database doesn't allow for anything fancier, with only a few exceptions. +//! +//! 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. + +use std::{collections::HashSet, hash::Hash}; + +use ruma::{api::client::filter::RoomEventFilter, RoomId}; + +use crate::Error; + +/// Structure for testing against an allowlist and a denylist with a single +/// `HashSet` lookup. +/// +/// The denylist takes precedence (an item included in both the allowlist and +/// the denylist is denied). +pub(crate) enum AllowDenyList<'a, T: ?Sized> { + /// TODO: fast-paths for allow-all and deny-all? + Allow(HashSet<&'a T>), + Deny(HashSet<&'a T>), +} + +impl<'a, T: ?Sized + Hash + PartialEq + Eq> AllowDenyList<'a, T> { + fn new(allow: Option, deny: D) -> AllowDenyList<'a, T> + where + A: Iterator, + D: Iterator, + { + let deny_set = deny.collect::>(); + if let Some(allow) = allow { + AllowDenyList::Allow(allow.filter(|x| !deny_set.contains(x)).collect()) + } else { + AllowDenyList::Deny(deny_set) + } + } + + fn from_slices>(allow: Option<&'a [O]>, deny: &'a [O]) -> AllowDenyList<'a, T> { + AllowDenyList::new( + allow.map(|allow| allow.iter().map(AsRef::as_ref)), + deny.iter().map(AsRef::as_ref), + ) + } + + pub(crate) fn allowed(&self, value: &T) -> bool { + match self { + AllowDenyList::Allow(allow) => allow.contains(value), + AllowDenyList::Deny(deny) => !deny.contains(value), + } + } +} + +pub(crate) struct CompiledRoomEventFilter<'a> { + rooms: AllowDenyList<'a, RoomId>, +} + +impl<'a> TryFrom<&'a RoomEventFilter> for CompiledRoomEventFilter<'a> { + type Error = Error; + + fn try_from(source: &'a RoomEventFilter) -> Result, Error> { + Ok(CompiledRoomEventFilter { + rooms: AllowDenyList::from_slices(source.rooms.as_deref(), &source.not_rooms), + }) + } +} + +impl CompiledRoomEventFilter<'_> { + /// Returns `true` if a room is allowed by the `rooms` and `not_rooms` + /// fields. + /// + /// This does *not* test the room against the top-level `rooms` filter. + /// It is expected that callers have already filtered rooms that are + /// rejected by the top-level filter using + /// [`CompiledRoomFilter::room_allowed`], if applicable. + pub(crate) fn room_allowed(&self, room_id: &RoomId) -> bool { self.rooms.allowed(room_id) } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 6b98baa1..55bd1502 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod clap; pub(crate) mod debug; pub(crate) mod error; +pub(crate) mod filter; use std::{ cmp,