mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2025-12-19 16:51:24 +01:00
implement top-level (not_)rooms filter on /sync
These are the fields at filter.room.{rooms,not_rooms}, that apply to all
categories. The category-specific room filters are in
filter.room.{state,timeline,ephemeral}.{rooms,not_rooms}.
This commit is contained in:
parent
5d4aa35463
commit
458d6842fb
2 changed files with 139 additions and 20 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
|
@ -6,6 +7,7 @@ use std::{
|
||||||
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::{
|
api::client::{
|
||||||
|
error::ErrorKind,
|
||||||
filter::{FilterDefinition, LazyLoadOptions},
|
filter::{FilterDefinition, LazyLoadOptions},
|
||||||
sync::sync_events::{
|
sync::sync_events::{
|
||||||
self,
|
self,
|
||||||
|
|
@ -30,7 +32,12 @@ use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
service::{pdu::EventHash, rooms::timeline::PduCount},
|
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`
|
/// # `GET /_matrix/client/r0/sync`
|
||||||
|
|
@ -96,6 +103,14 @@ pub(crate) async fn sync_events_route(
|
||||||
.get_filter(&sender_user, &filter_id)?
|
.get_filter(&sender_user, &filter_id)?
|
||||||
.unwrap_or_default(),
|
.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) =
|
let (lazy_load_enabled, lazy_load_send_redundant) =
|
||||||
match filter.room.state.lazy_load_options {
|
match filter.room.state.lazy_load_options {
|
||||||
|
|
@ -125,13 +140,25 @@ pub(crate) async fn sync_events_route(
|
||||||
.filter_map(Result::ok),
|
.filter_map(Result::ok),
|
||||||
);
|
);
|
||||||
|
|
||||||
let all_joined_rooms = services()
|
let room_filter = compiled_filter.room.rooms();
|
||||||
.rooms
|
|
||||||
.state_cache
|
let mut all_joined_rooms = Vec::new();
|
||||||
.rooms_joined(&sender_user)
|
if let AllowDenyList::Allow(allow_set) = room_filter {
|
||||||
.collect::<Vec<_>>();
|
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 {
|
for room_id in all_joined_rooms {
|
||||||
let room_id = room_id?;
|
|
||||||
if let Ok(joined_room) = load_joined_room(
|
if let Ok(joined_room) = load_joined_room(
|
||||||
&sender_user,
|
&sender_user,
|
||||||
&sender_device,
|
&sender_device,
|
||||||
|
|
@ -149,17 +176,31 @@ pub(crate) async fn sync_events_route(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !joined_room.is_empty() {
|
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 mut left_rooms = BTreeMap::new();
|
||||||
let all_left_rooms: Vec<_> =
|
let mut all_left_rooms = Vec::new();
|
||||||
services().rooms.state_cache.rooms_left(&sender_user).collect();
|
if let AllowDenyList::Allow(allow_set) = room_filter {
|
||||||
for result in all_left_rooms {
|
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(
|
handle_left_room(
|
||||||
result?.0,
|
room_id,
|
||||||
&sender_user,
|
&sender_user,
|
||||||
&mut left_rooms,
|
&mut left_rooms,
|
||||||
since,
|
since,
|
||||||
|
|
@ -171,11 +212,29 @@ pub(crate) async fn sync_events_route(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut invited_rooms = BTreeMap::new();
|
let mut invited_rooms = BTreeMap::new();
|
||||||
let all_invited_rooms: Vec<_> =
|
let mut all_invited_rooms = Vec::new();
|
||||||
services().rooms.state_cache.rooms_invited(&sender_user).collect();
|
if let AllowDenyList::Allow(allow_set) = room_filter {
|
||||||
for result in all_invited_rooms {
|
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?;
|
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
|
// Get and drop the lock to wait for remaining operations to finish
|
||||||
let mutex_insert = Arc::clone(
|
let mutex_insert = Arc::clone(
|
||||||
|
|
@ -184,7 +243,7 @@ pub(crate) async fn sync_events_route(
|
||||||
.roomid_mutex_insert
|
.roomid_mutex_insert
|
||||||
.write()
|
.write()
|
||||||
.await
|
.await
|
||||||
.entry(room_id.clone())
|
.entry(room_id.clone().into_owned())
|
||||||
.or_default(),
|
.or_default(),
|
||||||
);
|
);
|
||||||
let insert_lock = mutex_insert.lock().await;
|
let insert_lock = mutex_insert.lock().await;
|
||||||
|
|
@ -202,7 +261,7 @@ pub(crate) async fn sync_events_route(
|
||||||
}
|
}
|
||||||
|
|
||||||
invited_rooms.insert(
|
invited_rooms.insert(
|
||||||
room_id.clone(),
|
room_id.into_owned(),
|
||||||
InvitedRoom {
|
InvitedRoom {
|
||||||
invite_state: InviteState {
|
invite_state: InviteState {
|
||||||
events: invite_state_events,
|
events: invite_state_events,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,17 @@
|
||||||
//! The first exception is room filters (`room`/`not_room` pairs in
|
//! The first exception is room filters (`room`/`not_room` pairs in
|
||||||
//! `filter.rooms` and `filter.rooms.{account_data,timeline,ephemeral,state}`).
|
//! `filter.rooms` and `filter.rooms.{account_data,timeline,ephemeral,state}`).
|
||||||
//! In `/messages`, if the room is rejected by the filter, we can skip the
|
//! 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 std::{collections::HashSet, hash::Hash};
|
||||||
|
|
||||||
use regex::RegexSet;
|
use regex::RegexSet;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::filter::{RoomEventFilter, UrlFilter},
|
api::client::filter::{
|
||||||
|
FilterDefinition, RoomEventFilter, RoomFilter, UrlFilter,
|
||||||
|
},
|
||||||
RoomId, UserId,
|
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> {
|
pub(crate) struct CompiledRoomEventFilter<'a> {
|
||||||
// TODO: consider falling back a more-efficient
|
// TODO: consider falling back a more-efficient
|
||||||
// AllowDenyList<TimelineEventType> when none of the type patterns
|
// AllowDenyList<TimelineEventType> when none of the type patterns
|
||||||
|
|
@ -149,6 +168,35 @@ pub(crate) struct CompiledRoomEventFilter<'a> {
|
||||||
url_filter: Option<UrlFilter>,
|
url_filter: Option<UrlFilter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a FilterDefinition> for CompiledFilterDefinition<'a> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(
|
||||||
|
source: &'a FilterDefinition,
|
||||||
|
) -> Result<CompiledFilterDefinition<'a>, 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<CompiledRoomFilter<'a>, 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> {
|
impl<'a> TryFrom<&'a RoomEventFilter> for CompiledRoomEventFilter<'a> {
|
||||||
type Error = Error;
|
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<'_> {
|
impl CompiledRoomEventFilter<'_> {
|
||||||
/// Returns `true` if a room is allowed by the `rooms` and `not_rooms`
|
/// Returns `true` if a room is allowed by the `rooms` and `not_rooms`
|
||||||
/// fields.
|
/// fields.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue