Skip to main content

tuwunel_matrix/event/
filter.rs

1use std::mem::take;
2
3use ruma::{
4	RoomId, UserId,
5	api::client::filter::{Filter, RoomEventFilter, RoomFilter, UrlFilter},
6	serde::Raw,
7};
8use serde_json::{Map, Value};
9use smallvec::SmallVec;
10use tuwunel_core::is_equal_to;
11
12use super::Event;
13
14/// Segments of one `event_fields` entry; most paths are one or two deep.
15type FieldPath = SmallVec<[String; 2]>;
16
17/// Tests values against a Matrix filter.
18///
19/// Implementations apply the allow and deny lists relevant to the value being
20/// tested. A value matches only when every applicable filter condition accepts
21/// it.
22pub trait Matches<T> {
23	/// Returns whether the supplied value satisfies this filter.
24	///
25	/// The value type selects which filter fields participate in the
26	/// comparison. Built-in implementations reject deny-list matches before
27	/// accepting allow-list matches.
28	fn matches(&self, t: T) -> bool;
29}
30
31impl<E: Event> Matches<&E> for RoomEventFilter {
32	#[inline]
33	fn matches(&self, event: &E) -> bool {
34		if !matches_sender(event, self) {
35			return false;
36		}
37
38		if !matches_room(event, self) {
39			return false;
40		}
41
42		if !matches_type(event, self) {
43			return false;
44		}
45
46		if !matches_url(event, self) {
47			return false;
48		}
49
50		true
51	}
52}
53
54impl Matches<&RoomId> for RoomFilter {
55	#[inline]
56	fn matches(&self, room_id: &RoomId) -> bool { matches_room_id(room_id, self) }
57}
58
59impl Matches<&UserId> for Filter {
60	#[inline]
61	fn matches(&self, user_id: &UserId) -> bool { matches_user_id(user_id, self) }
62}
63
64fn matches_user_id(user_id: &UserId, filter: &Filter) -> bool {
65	if filter
66		.not_senders
67		.iter()
68		.any(is_equal_to!(user_id))
69	{
70		return false;
71	}
72
73	if let Some(senders) = filter.senders.as_ref()
74		&& !senders.iter().any(is_equal_to!(user_id))
75	{
76		return false;
77	}
78
79	true
80}
81
82fn matches_room_id(room_id: &RoomId, filter: &RoomFilter) -> bool {
83	if filter.not_rooms.iter().any(is_equal_to!(room_id)) {
84		return false;
85	}
86
87	if let Some(rooms) = filter.rooms.as_ref()
88		&& !rooms.iter().any(is_equal_to!(room_id))
89	{
90		return false;
91	}
92
93	true
94}
95
96fn matches_room<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
97	if filter
98		.not_rooms
99		.iter()
100		.any(is_equal_to!(event.room_id()))
101	{
102		return false;
103	}
104
105	if let Some(rooms) = filter.rooms.as_ref()
106		&& !rooms.iter().any(is_equal_to!(event.room_id()))
107	{
108		return false;
109	}
110
111	true
112}
113
114fn matches_sender<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
115	if filter
116		.not_senders
117		.iter()
118		.any(is_equal_to!(event.sender()))
119	{
120		return false;
121	}
122
123	if let Some(senders) = filter.senders.as_ref()
124		&& !senders.iter().any(is_equal_to!(event.sender()))
125	{
126		return false;
127	}
128
129	true
130}
131
132fn matches_type<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
133	let kind = event.kind().to_cow_str();
134
135	if filter.not_types.iter().any(is_equal_to!(&kind)) {
136		return false;
137	}
138
139	if let Some(types) = filter.types.as_ref()
140		&& !types.iter().any(is_equal_to!(&kind))
141	{
142		return false;
143	}
144
145	true
146}
147
148fn matches_url<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
149	let Some(url_filter) = filter.url_filter.as_ref() else {
150		return true;
151	};
152
153	//TODO: might be better to use Ruma's Raw rather than serde here
154	let url = event
155		.get_content_as_value()
156		.get("url")
157		.is_some_and(Value::is_string);
158
159	match url_filter {
160		| UrlFilter::EventsWithUrl => url,
161		| UrlFilter::EventsWithoutUrl => !url,
162	}
163}
164
165/// Restrict a serialized event to the requested `event_fields` per MSC3980,
166/// returning it unchanged when no filter is set so the common sync pays
167/// nothing.
168///
169/// The deserialize and reserialize happen only when `event_fields` is `Some`.
170#[must_use]
171pub fn trim_event_fields<T>(raw: Raw<T>, event_fields: Option<&[String]>) -> Raw<T> {
172	// Identity keys clients rely on are always kept; the spec permits a superset.
173	const ALWAYS_KEEP: [&str; 6] =
174		["event_id", "type", "sender", "origin_server_ts", "state_key", "room_id"];
175
176	let Some(fields) = event_fields else {
177		return raw;
178	};
179
180	let Ok(Value::Object(source)) = raw.deserialize_as::<Value>() else {
181		return raw;
182	};
183
184	let mut out: Map<String, Value> = ALWAYS_KEEP
185		.into_iter()
186		.filter_map(|key| {
187			source
188				.get(key)
189				.map(|value| (key.to_owned(), value.clone()))
190		})
191		.collect();
192
193	for field in fields {
194		copy_field_path(&source, &mut out, &split_field(field));
195	}
196
197	Raw::from_json_value(&Value::Object(out))
198}
199
200/// Copy the leaf or subtree at `segments` from `source` into the same nested
201/// path in `out`, creating intermediate objects as needed; an absent or
202/// non-object path is skipped.
203fn copy_field_path(
204	source: &Map<String, Value>,
205	out: &mut Map<String, Value>,
206	segments: &[String],
207) {
208	let Some((head, rest)) = segments.split_first() else {
209		return;
210	};
211
212	let Some(value) = source.get(head) else {
213		return;
214	};
215
216	if rest.is_empty() {
217		out.insert(head.clone(), value.clone());
218		return;
219	}
220
221	let Value::Object(child) = value else {
222		return;
223	};
224
225	if let Value::Object(child_out) = out
226		.entry(head.clone())
227		.or_insert_with(|| Value::Object(Map::new()))
228	{
229		copy_field_path(child, child_out, rest);
230	}
231}
232
233/// Split one `event_fields` entry into its segments, inverting the MSC3873
234/// escape grammar used by ruma's `escape_key`: `\.` is a literal dot inside the
235/// current segment, `\\` a literal backslash, any other `\X` is kept verbatim,
236/// and an unescaped `.` ends the segment.
237fn split_field(field: &str) -> FieldPath {
238	let mut segments = FieldPath::new();
239	let mut current = String::new();
240	let mut chars = field.chars();
241	while let Some(c) = chars.next() {
242		match c {
243			| '\\' => match chars.next() {
244				| Some('.') => current.push('.'),
245				| Some('\\') | None => current.push('\\'),
246				| Some(other) => {
247					current.push('\\');
248					current.push(other);
249				},
250			},
251			| '.' => {
252				segments.push(take(&mut current));
253			},
254			| other => current.push(other),
255		}
256	}
257
258	segments.push(current);
259	segments
260}
261
262#[cfg(test)]
263mod tests {
264	use super::split_field;
265
266	fn segments(field: &str) -> Vec<String> { split_field(field).into_vec() }
267
268	#[test]
269	fn split_simple_path() {
270		assert_eq!(segments("content.body"), ["content", "body"]);
271	}
272
273	#[test]
274	fn split_escaped_dot() {
275		assert_eq!(segments(r"content.m\.relates_to"), ["content", "m.relates_to"]);
276	}
277
278	#[test]
279	fn split_escaped_backslash_then_dot() {
280		assert_eq!(segments(r"a\\.b"), [r"a\", "b"]);
281	}
282
283	#[test]
284	fn split_reserved_escape_kept_verbatim() {
285		assert_eq!(segments(r"weird\x"), [r"weird\x"]);
286	}
287
288	#[test]
289	fn split_trailing_backslash() {
290		assert_eq!(segments(r"foo\"), [r"foo\"]);
291	}
292}