implement rooms and not_rooms filters on /message

I really doubt anybody is sending /message requests with a filter that
rejects the entire request, but it's the first step in the filter
implementation.
This commit is contained in:
Benjamin Lee 2024-05-02 18:23:19 -07:00
parent cc77d47adc
commit 296b777c04
No known key found for this signature in database
GPG key ID: FB9624E2885D55A4
3 changed files with 101 additions and 1 deletions

View file

@ -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()

84
src/utils/filter.rs Normal file
View file

@ -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<A, D>(allow: Option<A>, deny: D) -> AllowDenyList<'a, T>
where
A: Iterator<Item = &'a T>,
D: Iterator<Item = &'a T>,
{
let deny_set = deny.collect::<HashSet<_>>();
if let Some(allow) = allow {
AllowDenyList::Allow(allow.filter(|x| !deny_set.contains(x)).collect())
} else {
AllowDenyList::Deny(deny_set)
}
}
fn from_slices<O: AsRef<T>>(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<CompiledRoomEventFilter<'a>, 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) }
}

View file

@ -1,6 +1,7 @@
pub(crate) mod clap;
pub(crate) mod debug;
pub(crate) mod error;
pub(crate) mod filter;
use std::{
cmp,