matrix_sdk/widget/
filter.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
16use serde::Deserialize;
17
18/// Different kinds of filters for timeline events.
19#[derive(Clone, Debug)]
20#[cfg_attr(test, derive(PartialEq))]
21pub enum EventFilter {
22    /// Filter for message-like events.
23    MessageLike(MessageLikeEventFilter),
24    /// Filter for state events.
25    State(StateEventFilter),
26}
27
28impl EventFilter {
29    pub(super) fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
30        match self {
31            EventFilter::MessageLike(message_filter) => message_filter.matches(matrix_event),
32            EventFilter::State(state_filter) => state_filter.matches(matrix_event),
33        }
34    }
35
36    pub(super) fn matches_state_event_with_any_state_key(
37        &self,
38        event_type: &StateEventType,
39    ) -> bool {
40        matches!(
41            self,
42            Self::State(filter) if filter.matches_state_event_with_any_state_key(event_type)
43        )
44    }
45
46    pub(super) fn matches_message_like_event_type(
47        &self,
48        event_type: &MessageLikeEventType,
49    ) -> bool {
50        matches!(
51            self,
52            Self::MessageLike(filter) if filter.matches_message_like_event_type(event_type)
53        )
54    }
55}
56
57/// Filter for message-like events.
58#[derive(Clone, Debug)]
59#[cfg_attr(test, derive(PartialEq))]
60pub enum MessageLikeEventFilter {
61    /// Matches message-like events with the given `type`.
62    WithType(MessageLikeEventType),
63    /// Matches `m.room.message` events with the given `msgtype`.
64    RoomMessageWithMsgtype(String),
65}
66
67impl MessageLikeEventFilter {
68    fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
69        if matrix_event.state_key.is_some() {
70            // State event doesn't match a message-like event filter.
71            return false;
72        }
73
74        match self {
75            MessageLikeEventFilter::WithType(event_type) => {
76                matrix_event.event_type == TimelineEventType::from(event_type.clone())
77            }
78            MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype) => {
79                matrix_event.event_type == TimelineEventType::RoomMessage
80                    && matrix_event.content.msgtype.as_ref() == Some(msgtype)
81            }
82        }
83    }
84
85    fn matches_message_like_event_type(&self, event_type: &MessageLikeEventType) -> bool {
86        match self {
87            MessageLikeEventFilter::WithType(filter_event_type) => filter_event_type == event_type,
88            MessageLikeEventFilter::RoomMessageWithMsgtype(_) => {
89                event_type == &MessageLikeEventType::RoomMessage
90            }
91        }
92    }
93}
94
95/// Filter for state events.
96#[derive(Clone, Debug)]
97#[cfg_attr(test, derive(PartialEq))]
98pub enum StateEventFilter {
99    /// Matches state events with the given `type`, regardless of `state_key`.
100    WithType(StateEventType),
101    /// Matches state events with the given `type` and `state_key`.
102    WithTypeAndStateKey(StateEventType, String),
103}
104
105impl StateEventFilter {
106    fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
107        let Some(state_key) = &matrix_event.state_key else {
108            // Message-like event doesn't match a state event filter.
109            return false;
110        };
111
112        match self {
113            StateEventFilter::WithType(event_type) => {
114                matrix_event.event_type == TimelineEventType::from(event_type.clone())
115            }
116            StateEventFilter::WithTypeAndStateKey(event_type, filter_state_key) => {
117                matrix_event.event_type == TimelineEventType::from(event_type.clone())
118                    && state_key == filter_state_key
119            }
120        }
121    }
122
123    fn matches_state_event_with_any_state_key(&self, event_type: &StateEventType) -> bool {
124        matches!(self, Self::WithType(ty) if ty == event_type)
125    }
126}
127
128#[derive(Debug, Deserialize)]
129pub(super) struct MatrixEventFilterInput {
130    #[serde(rename = "type")]
131    pub(super) event_type: TimelineEventType,
132    pub(super) state_key: Option<String>,
133    pub(super) content: MatrixEventContent,
134}
135
136#[derive(Debug, Default, Deserialize)]
137pub(super) struct MatrixEventContent {
138    pub(super) msgtype: Option<String>,
139}
140
141#[cfg(test)]
142mod tests {
143    use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
144
145    use super::{
146        EventFilter, MatrixEventContent, MatrixEventFilterInput, MessageLikeEventFilter,
147        StateEventFilter,
148    };
149
150    fn message_event(event_type: TimelineEventType) -> MatrixEventFilterInput {
151        MatrixEventFilterInput { event_type, state_key: None, content: Default::default() }
152    }
153
154    fn message_event_with_msgtype(
155        event_type: TimelineEventType,
156        msgtype: String,
157    ) -> MatrixEventFilterInput {
158        MatrixEventFilterInput {
159            event_type,
160            state_key: None,
161            content: MatrixEventContent { msgtype: Some(msgtype) },
162        }
163    }
164
165    fn state_event(event_type: TimelineEventType, state_key: String) -> MatrixEventFilterInput {
166        MatrixEventFilterInput {
167            event_type,
168            state_key: Some(state_key),
169            content: Default::default(),
170        }
171    }
172
173    // Tests against an `m.room.message` filter with `msgtype = m.text`
174    fn room_message_text_event_filter() -> EventFilter {
175        EventFilter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(
176            "m.text".to_owned(),
177        ))
178    }
179
180    #[test]
181    fn text_event_filter_matches_text_event() {
182        assert!(room_message_text_event_filter().matches(&message_event_with_msgtype(
183            TimelineEventType::RoomMessage,
184            "m.text".to_owned()
185        )));
186    }
187
188    #[test]
189    fn text_event_filter_does_not_match_image_event() {
190        assert!(!room_message_text_event_filter().matches(&message_event_with_msgtype(
191            TimelineEventType::RoomMessage,
192            "m.image".to_owned()
193        )));
194    }
195
196    #[test]
197    fn text_event_filter_does_not_match_custom_event_with_msgtype() {
198        assert!(!room_message_text_event_filter().matches(&message_event_with_msgtype(
199            "io.element.message".into(),
200            "m.text".to_owned()
201        )));
202    }
203
204    // Tests against an `m.reaction` filter
205    fn reaction_event_filter() -> EventFilter {
206        EventFilter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
207    }
208
209    #[test]
210    fn reaction_event_filter_matches_reaction() {
211        assert!(reaction_event_filter().matches(&message_event(TimelineEventType::Reaction)));
212    }
213
214    #[test]
215    fn reaction_event_filter_does_not_match_room_message() {
216        assert!(!reaction_event_filter().matches(&message_event_with_msgtype(
217            TimelineEventType::RoomMessage,
218            "m.text".to_owned()
219        )));
220    }
221
222    #[test]
223    fn reaction_event_filter_does_not_match_state_event() {
224        assert!(!reaction_event_filter().matches(&state_event(
225            // Use the `m.reaction` event type to make sure the event would pass
226            // the filter without state event checks, even though in practice
227            // that event type won't be used for a state event.
228            TimelineEventType::Reaction,
229            "".to_owned()
230        )));
231    }
232
233    #[test]
234    fn reaction_event_filter_does_not_match_state_event_any_key() {
235        assert!(
236            !reaction_event_filter().matches_state_event_with_any_state_key(&"m.reaction".into())
237        );
238    }
239
240    // Tests against an `m.room.member` filter with `state_key = "@self:example.me"`
241    fn self_member_event_filter() -> EventFilter {
242        EventFilter::State(StateEventFilter::WithTypeAndStateKey(
243            StateEventType::RoomMember,
244            "@self:example.me".to_owned(),
245        ))
246    }
247
248    #[test]
249    fn self_member_event_filter_matches_self_member_event() {
250        assert!(self_member_event_filter()
251            .matches(&state_event(TimelineEventType::RoomMember, "@self:example.me".to_owned())));
252    }
253
254    #[test]
255    fn self_member_event_filter_does_not_match_somebody_elses_member_event() {
256        assert!(!self_member_event_filter().matches(&state_event(
257            TimelineEventType::RoomMember,
258            "@somebody_else.example.me".to_owned()
259        )));
260    }
261
262    #[test]
263    fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
264        assert!(!self_member_event_filter().matches(&state_event(
265            TimelineEventType::from("io.element.test_state_event"),
266            "@self.example.me".to_owned()
267        )));
268    }
269
270    #[test]
271    fn self_member_event_filter_does_not_match_reaction_event() {
272        assert!(!self_member_event_filter().matches(&message_event(TimelineEventType::Reaction)));
273    }
274
275    #[test]
276    fn self_member_event_filter_only_matches_specific_state_key() {
277        assert!(!self_member_event_filter()
278            .matches_state_event_with_any_state_key(&StateEventType::RoomMember));
279    }
280
281    // Tests against an `m.room.member` filter with any `state_key`.
282    fn member_event_filter() -> EventFilter {
283        EventFilter::State(StateEventFilter::WithType(StateEventType::RoomMember))
284    }
285
286    #[test]
287    fn member_event_filter_matches_some_member_event() {
288        assert!(member_event_filter()
289            .matches(&state_event(TimelineEventType::RoomMember, "@foo.bar.baz".to_owned())));
290    }
291
292    #[test]
293    fn member_event_filter_does_not_match_room_name_event() {
294        assert!(!member_event_filter()
295            .matches(&state_event(TimelineEventType::RoomName, "".to_owned())));
296    }
297
298    #[test]
299    fn member_event_filter_does_not_match_reaction_event() {
300        assert!(!member_event_filter().matches(&message_event(TimelineEventType::Reaction)));
301    }
302
303    #[test]
304    fn member_event_filter_matches_any_state_key() {
305        assert!(member_event_filter()
306            .matches_state_event_with_any_state_key(&StateEventType::RoomMember));
307    }
308
309    // Tests against an `m.room.topic` filter with `state_key = ""`
310    fn topic_event_filter() -> EventFilter {
311        EventFilter::State(StateEventFilter::WithTypeAndStateKey(
312            StateEventType::RoomTopic,
313            "".to_owned(),
314        ))
315    }
316
317    #[test]
318    fn topic_event_filter_does_not_match_any_state_key() {
319        assert!(!topic_event_filter()
320            .matches_state_event_with_any_state_key(&StateEventType::RoomTopic));
321    }
322
323    // Tests against an `m.room.message` filter with `msgtype = m.custom`
324    fn room_message_custom_event_filter() -> EventFilter {
325        EventFilter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(
326            "m.custom".to_owned(),
327        ))
328    }
329
330    // Tests against an `m.room.message` filter without a `msgtype`
331    fn room_message_filter() -> EventFilter {
332        EventFilter::MessageLike(MessageLikeEventFilter::WithType(
333            MessageLikeEventType::RoomMessage,
334        ))
335    }
336
337    #[test]
338    fn room_message_event_type_matches_room_message_text_event_filter() {
339        assert!(room_message_text_event_filter()
340            .matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
341    }
342
343    #[test]
344    fn reaction_event_type_does_not_match_room_message_text_event_filter() {
345        assert!(!room_message_text_event_filter()
346            .matches_message_like_event_type(&MessageLikeEventType::Reaction));
347    }
348
349    #[test]
350    fn room_message_event_type_matches_room_message_custom_event_filter() {
351        assert!(room_message_custom_event_filter()
352            .matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
353    }
354
355    #[test]
356    fn reaction_event_type_does_not_match_room_message_custom_event_filter() {
357        assert!(!room_message_custom_event_filter()
358            .matches_message_like_event_type(&MessageLikeEventType::Reaction));
359    }
360
361    #[test]
362    fn room_message_event_type_matches_room_message_event_filter() {
363        assert!(room_message_filter()
364            .matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
365    }
366
367    #[test]
368    fn reaction_event_type_does_not_match_room_message_event_filter() {
369        assert!(
370            !room_message_filter().matches_message_like_event_type(&MessageLikeEventType::Reaction)
371        );
372    }
373}