diff --git a/src/api/client_server/message.rs b/src/api/client_server/message.rs
index 162c2d63..2b8f7d5a 100644
--- a/src/api/client_server/message.rs
+++ b/src/api/client_server/message.rs
@@ -14,7 +14,9 @@ use ruma::{
use crate::{
service::{pdu::PduBuilder, rooms::timeline::PduCount},
- services, utils, Ar, Error, Ra, Result,
+ services, utils,
+ utils::filter::CompiledRoomEventFilter,
+ Ar, Error, Ra, Result,
};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`
@@ -136,6 +138,13 @@ pub(crate) async fn get_message_events_route(
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 {
@@ -144,6 +153,15 @@ pub(crate) async fn get_message_events_route(
},
};
+ if !filter.room_allowed(&body.room_id) {
+ return Ok(Ra(get_message_events::v3::Response {
+ start: from.stringify(),
+ end: None,
+ chunk: vec![],
+ state: vec![],
+ }));
+ }
+
let to = body.to.as_ref().and_then(|t| PduCount::try_from_string(t).ok());
services()
diff --git a/src/utils.rs b/src/utils.rs
index 746d0b84..cf84353a 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,4 +1,5 @@
pub(crate) mod error;
+pub(crate) mod filter;
use std::{
borrow::Cow,
diff --git a/src/utils/filter.rs b/src/utils/filter.rs
new file mode 100644
index 00000000..c5dc964d
--- /dev/null
+++ b/src/utils/filter.rs
@@ -0,0 +1,96 @@
+//! 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::rooms`], if
+ /// applicable.
+ pub(crate) fn room_allowed(&self, room_id: &RoomId) -> bool {
+ self.rooms.allowed(room_id)
+ }
+}