Skip to main content

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::{
16    events::{
17        AnyMessageLikeEvent, AnyStateEvent, AnyTimelineEvent, AnyToDeviceEvent,
18        MessageLikeEventType, StateEventType, ToDeviceEventType,
19    },
20    serde::{JsonCastable, Raw},
21};
22use serde::Deserialize;
23use tracing::debug;
24
25use super::machine::{SendEventRequest, SendToDeviceRequest};
26
27/// A Filter for Matrix events. It is used to decide if a given event can be
28/// sent to the widget and if a widget is allowed to send an event to a Matrix
29/// room.
30#[derive(Clone, Debug)]
31#[cfg_attr(test, derive(PartialEq))]
32pub enum Filter {
33    /// Filter for message-like events.
34    MessageLike(MessageLikeEventFilter),
35    /// Filter for state events.
36    State(StateEventFilter),
37    /// Filter for to device events.
38    ToDevice(ToDeviceEventFilter),
39}
40
41impl Filter {
42    /// Checks if this filter matches with the given filter_input. A filter
43    /// input can be create by using the `From` trait on FilterInput for
44    /// [`Raw<AnyTimelineEvent>`] or [`SendEventRequest`].
45    pub(super) fn matches(&self, filter_input: &FilterInput<'_>) -> bool {
46        match self {
47            Self::MessageLike(filter) => filter.matches(filter_input),
48            Self::State(filter) => filter.matches(filter_input),
49            Self::ToDevice(filter) => filter.matches(filter_input),
50        }
51    }
52    /// Returns the event type that this filter is configured to match.
53    ///
54    /// This method provides a string representation of the event type
55    /// associated with the filter.
56    pub(super) fn filter_event_type(&self) -> String {
57        match self {
58            Self::MessageLike(filter) => filter.filter_event_type(),
59            Self::State(filter) => filter.filter_event_type(),
60            Self::ToDevice(filter) => filter.event_type.to_string(),
61        }
62    }
63}
64
65/// Filter for message-like events.
66#[derive(Clone, Debug)]
67#[cfg_attr(test, derive(PartialEq))]
68pub enum MessageLikeEventFilter {
69    /// Matches message-like events with the given `type`.
70    WithType(MessageLikeEventType),
71    /// Matches `m.room.message` events with the given `msgtype`.
72    RoomMessageWithMsgtype(String),
73}
74
75impl<'a> MessageLikeEventFilter {
76    fn matches(&self, filter_input: &FilterInput<'a>) -> bool {
77        let FilterInput::MessageLike(message_like_filter_input) = filter_input else {
78            return false;
79        };
80        match self {
81            Self::WithType(filter_event_type) => {
82                message_like_filter_input.event_type == filter_event_type.to_string()
83            }
84            Self::RoomMessageWithMsgtype(msgtype) => {
85                message_like_filter_input.event_type == "m.room.message"
86                    && message_like_filter_input.content.msgtype == Some(msgtype)
87            }
88        }
89    }
90
91    fn filter_event_type(&self) -> String {
92        match self {
93            Self::WithType(filter_event_type) => filter_event_type.to_string(),
94            Self::RoomMessageWithMsgtype(_) => MessageLikeEventType::RoomMessage.to_string(),
95        }
96    }
97}
98
99/// Filter for state events.
100#[derive(Clone, Debug)]
101#[cfg_attr(test, derive(PartialEq))]
102pub enum StateEventFilter {
103    /// Matches state events with the given `type`, regardless of `state_key`.
104    WithType(StateEventType),
105    /// Matches state events with the given `type` and `state_key`.
106    WithTypeAndStateKey(StateEventType, String),
107}
108
109impl<'a> StateEventFilter {
110    fn matches(&self, filter_input: &FilterInput<'a>) -> bool {
111        let FilterInput::State(state_filter_input) = filter_input else {
112            return false;
113        };
114
115        match self {
116            StateEventFilter::WithType(filter_type) => {
117                state_filter_input.event_type == filter_type.to_string()
118            }
119            StateEventFilter::WithTypeAndStateKey(event_type, filter_state_key) => {
120                state_filter_input.event_type == event_type.to_string()
121                    && state_filter_input.state_key == *filter_state_key
122            }
123        }
124    }
125    fn filter_event_type(&self) -> String {
126        match self {
127            Self::WithType(filter_event_type) => filter_event_type.to_string(),
128            Self::WithTypeAndStateKey(event_type, _) => event_type.to_string(),
129        }
130    }
131}
132
133/// Filter for to-device events.
134#[derive(Clone, Debug)]
135#[cfg_attr(test, derive(PartialEq))]
136pub struct ToDeviceEventFilter {
137    /// The event type this to-device-filter filters for.
138    pub event_type: ToDeviceEventType,
139}
140
141impl ToDeviceEventFilter {
142    /// Create a new `ToDeviceEventFilter` with the given event type.
143    pub fn new(event_type: ToDeviceEventType) -> Self {
144        Self { event_type }
145    }
146
147    fn matches(&self, filter_input: &FilterInput<'_>) -> bool {
148        matches!(filter_input,FilterInput::ToDevice(f_in) if f_in.event_type == self.event_type.to_string())
149    }
150}
151
152// Filter input:
153
154/// The input data for the filter. This can either be constructed from a
155/// [`Raw<AnyTimelineEvent>`] or a [`SendEventRequest`].
156#[derive(Debug, Deserialize)]
157#[serde(untagged)]
158pub enum FilterInput<'a> {
159    #[serde(borrow)]
160    // The order is important. We first need to check if we can deserialize as a
161    // state (state_key exists)
162    State(FilterInputState<'a>),
163    // only then we can check if we can deserialize as a message-like.
164    MessageLike(FilterInputMessageLike<'a>),
165    // ToDevice will need to be done explicitly since it looks the same as a message-like.
166    ToDevice(FilterInputToDevice<'a>),
167}
168
169impl<'a> FilterInput<'a> {
170    pub fn message_like(event_type: &'a str) -> Self {
171        Self::MessageLike(FilterInputMessageLike {
172            event_type,
173            content: MessageLikeFilterEventContent { msgtype: None },
174        })
175    }
176
177    pub(super) fn message_with_msgtype(msgtype: &'a str) -> Self {
178        Self::MessageLike(FilterInputMessageLike {
179            event_type: "m.room.message",
180            content: MessageLikeFilterEventContent { msgtype: Some(msgtype) },
181        })
182    }
183
184    pub fn state(event_type: &'a str, state_key: &'a str) -> Self {
185        Self::State(FilterInputState { event_type, state_key })
186    }
187}
188
189/// Filter input data that is used for a [`FilterInput::State`] filter.
190#[derive(Debug, Deserialize)]
191pub struct FilterInputState<'a> {
192    #[serde(rename = "type")]
193    // TODO: This wants to be `StateEventType` but we need a type which supports
194    // `as_str()` as soon as ruma supports `as_str()` on `StateEventType` we can
195    // use it here.
196    pub(super) event_type: &'a str,
197    pub(super) state_key: &'a str,
198}
199
200// Filter input message like:
201#[derive(Debug, Default, Deserialize)]
202pub(super) struct MessageLikeFilterEventContent<'a> {
203    #[serde(borrow)]
204    pub(super) msgtype: Option<&'a str>,
205}
206
207#[derive(Debug, Deserialize)]
208pub struct FilterInputMessageLike<'a> {
209    // TODO: This wants to be `StateEventType` but we need a type which supports
210    // `as_str()` as soon as ruma supports `as_str()` on `StateEventType` we can
211    // use it here.
212    #[serde(rename = "type")]
213    pub(super) event_type: &'a str,
214    pub(super) content: MessageLikeFilterEventContent<'a>,
215}
216
217/// Create a filter input based on [`AnyTimelineEvent`]. This will create a
218/// [`FilterInput::State`] or [`FilterInput::MessageLike`] depending on the
219/// event type.
220impl<'a> TryFrom<&'a Raw<AnyTimelineEvent>> for FilterInput<'a> {
221    type Error = serde_json::Error;
222
223    fn try_from(raw_event: &'a Raw<AnyTimelineEvent>) -> Result<Self, Self::Error> {
224        // FilterInput first checks if it can deserialize as a state event
225        // (state_key exists) and then as a message-like event.
226        raw_event.deserialize_as()
227    }
228}
229
230/// Create a filter input based on [`AnyStateEvent`]. This will create a
231/// [`FilterInput::State`].
232impl<'a> TryFrom<&'a Raw<AnyStateEvent>> for FilterInput<'a> {
233    type Error = serde_json::Error;
234
235    fn try_from(raw_event: &'a Raw<AnyStateEvent>) -> Result<Self, Self::Error> {
236        raw_event.deserialize_as()
237    }
238}
239
240impl<'a> JsonCastable<FilterInput<'a>> for AnyTimelineEvent {}
241
242impl<'a> JsonCastable<FilterInput<'a>> for AnyStateEvent {}
243
244impl<'a> JsonCastable<FilterInput<'a>> for AnyMessageLikeEvent {}
245
246#[derive(Debug, Deserialize)]
247pub struct FilterInputToDevice<'a> {
248    #[serde(rename = "type")]
249    pub(super) event_type: &'a str,
250}
251
252/// Create a filter input of type [`FilterInput::ToDevice`]`.
253impl<'a> TryFrom<&'a Raw<AnyToDeviceEvent>> for FilterInput<'a> {
254    type Error = serde_json::Error;
255    fn try_from(raw_event: &'a Raw<AnyToDeviceEvent>) -> Result<Self, Self::Error> {
256        // deserialize_as::<FilterInput> will first try state, message-like and
257        // then to-device. The `AnyToDeviceEvent` would match message like
258        // first, so we need to explicitly deserialize as `FilterInputToDevice`.
259        raw_event.deserialize_as::<FilterInputToDevice<'a>>().map(FilterInput::ToDevice)
260    }
261}
262
263impl<'a> JsonCastable<FilterInputToDevice<'a>> for AnyToDeviceEvent {}
264
265impl<'a> From<&'a SendToDeviceRequest> for FilterInput<'a> {
266    fn from(request: &'a SendToDeviceRequest) -> Self {
267        FilterInput::ToDevice(FilterInputToDevice { event_type: &request.event_type })
268    }
269}
270
271impl<'a> From<&'a SendEventRequest> for FilterInput<'a> {
272    fn from(request: &'a SendEventRequest) -> Self {
273        match &request.state_key {
274            None => match request.event_type.as_str() {
275                "m.room.message" => {
276                    if let Some(msgtype) =
277                        serde_json::from_str::<MessageLikeFilterEventContent<'a>>(
278                            request.content.get(),
279                        )
280                        .unwrap_or_else(|e| {
281                            debug!("Failed to deserialize event content for filter: {e}");
282                            // Fallback to empty content is safe. If we do have
283                            // a filter matching any content type, it will match
284                            // independent of the body. Any filter that does
285                            // only match a specific content type will not match
286                            // the empty content.
287                            Default::default()
288                        })
289                        .msgtype
290                    {
291                        FilterInput::message_with_msgtype(msgtype)
292                    } else {
293                        FilterInput::message_like("m.room.message")
294                    }
295                }
296                _ => FilterInput::message_like(&request.event_type),
297            },
298            Some(state_key) => FilterInput::state(&request.event_type, state_key),
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use ruma::{
306        events::{AnyTimelineEvent, MessageLikeEventType, StateEventType, TimelineEventType},
307        serde::Raw,
308    };
309
310    use super::{
311        Filter, FilterInput, FilterInputMessageLike, MessageLikeEventFilter, StateEventFilter,
312    };
313    use crate::widget::filter::{
314        FilterInputToDevice, MessageLikeFilterEventContent, ToDeviceEventFilter,
315    };
316
317    fn message_event(event_type: &str) -> FilterInput<'_> {
318        FilterInput::MessageLike(FilterInputMessageLike { event_type, content: Default::default() })
319    }
320
321    // Tests against a `m.room.message` filter with `msgtype = m.text`
322    fn room_message_text_event_filter() -> Filter {
323        Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.text".to_owned()))
324    }
325
326    #[test]
327    fn test_text_event_filter_matches_text_event() {
328        assert!(
329            room_message_text_event_filter().matches(&FilterInput::message_with_msgtype("m.text")),
330        );
331    }
332
333    #[test]
334    fn test_text_event_filter_does_not_match_image_event() {
335        assert!(
336            !room_message_text_event_filter()
337                .matches(&FilterInput::message_with_msgtype("m.image"))
338        );
339    }
340
341    #[test]
342    fn test_text_event_filter_does_not_match_custom_event_with_msgtype() {
343        assert!(!room_message_text_event_filter().matches(&FilterInput::MessageLike(
344            FilterInputMessageLike {
345                event_type: "io.element.message",
346                content: MessageLikeFilterEventContent { msgtype: Some("m.text") }
347            }
348        )));
349    }
350
351    // Tests against an `m.reaction` filter
352    fn reaction_event_filter() -> Filter {
353        Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
354    }
355
356    #[test]
357    fn test_reaction_event_filter_matches_reaction() {
358        assert!(
359            reaction_event_filter()
360                .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
361        );
362    }
363
364    #[test]
365    fn test_reaction_event_filter_does_not_match_room_message() {
366        assert!(!reaction_event_filter().matches(&FilterInput::message_with_msgtype("m.text")));
367    }
368
369    #[test]
370    fn test_reaction_event_filter_does_not_match_state_event_any_key() {
371        assert!(!reaction_event_filter().matches(&FilterInput::state("m.reaction", "")));
372    }
373
374    // Tests against an `m.room.member` filter with `state_key =
375    // "@self:example.me"`
376    fn self_member_event_filter() -> Filter {
377        Filter::State(StateEventFilter::WithTypeAndStateKey(
378            StateEventType::RoomMember,
379            "@self:example.me".to_owned(),
380        ))
381    }
382
383    #[test]
384    fn test_self_member_event_filter_matches_self_member_event() {
385        assert!(self_member_event_filter().matches(&FilterInput::state(
386            &TimelineEventType::RoomMember.to_string(),
387            "@self:example.me"
388        )));
389    }
390
391    #[test]
392    fn test_self_member_event_filter_does_not_match_somebody_elses_member_event() {
393        assert!(!self_member_event_filter().matches(&FilterInput::state(
394            &TimelineEventType::RoomMember.to_string(),
395            "@somebody_else.example.me"
396        )));
397    }
398
399    #[test]
400    fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
401        assert!(
402            !self_member_event_filter()
403                .matches(&FilterInput::state("io.element.test_state_event", "@self.example.me"))
404        );
405    }
406
407    #[test]
408    fn test_self_member_event_filter_does_not_match_reaction_event() {
409        assert!(
410            !self_member_event_filter()
411                .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
412        );
413    }
414
415    #[test]
416    fn test_self_member_event_filter_only_matches_specific_state_key() {
417        assert!(
418            !self_member_event_filter()
419                .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
420        );
421    }
422
423    // Tests against an `m.room.member` filter with any `state_key`.
424    fn member_event_filter() -> Filter {
425        Filter::State(StateEventFilter::WithType(StateEventType::RoomMember))
426    }
427
428    #[test]
429    fn test_member_event_filter_matches_some_member_event() {
430        assert!(member_event_filter().matches(&FilterInput::state(
431            &TimelineEventType::RoomMember.to_string(),
432            "@foo.bar.baz"
433        )));
434    }
435
436    #[test]
437    fn test_member_event_filter_does_not_match_room_name_event() {
438        assert!(
439            !member_event_filter()
440                .matches(&FilterInput::state(&TimelineEventType::RoomName.to_string(), ""))
441        );
442    }
443
444    #[test]
445    fn test_member_event_filter_does_not_match_reaction_event() {
446        assert!(
447            !member_event_filter()
448                .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
449        );
450    }
451
452    #[test]
453    fn test_member_event_filter_matches_any_state_key() {
454        assert!(
455            member_event_filter()
456                .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
457        );
458    }
459
460    // Tests against an `m.room.topic` filter with `state_key = ""`
461    fn topic_event_filter() -> Filter {
462        Filter::State(StateEventFilter::WithTypeAndStateKey(
463            StateEventType::RoomTopic,
464            "".to_owned(),
465        ))
466    }
467
468    #[test]
469    fn test_topic_event_filter_does_match() {
470        assert!(
471            topic_event_filter()
472                .matches(&FilterInput::state(&StateEventType::RoomTopic.to_string(), ""))
473        );
474    }
475
476    // Tests against an `m.room.message` filter with `msgtype = m.custom`
477    fn room_message_custom_event_filter() -> Filter {
478        Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.custom".to_owned()))
479    }
480
481    // Tests against an `m.room.message` filter without a `msgtype`
482    fn room_message_filter() -> Filter {
483        Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::RoomMessage))
484    }
485
486    #[test]
487    fn test_reaction_event_type_does_not_match_room_message_text_event_filter() {
488        assert!(
489            !room_message_text_event_filter()
490                .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
491        );
492    }
493
494    #[test]
495    fn test_room_message_event_without_msgtype_does_not_match_custom_msgtype_filter() {
496        assert!(
497            !room_message_custom_event_filter().matches(&FilterInput::message_like(
498                &MessageLikeEventType::RoomMessage.to_string()
499            ))
500        );
501    }
502
503    #[test]
504    fn test_reaction_event_type_does_not_match_room_message_custom_event_filter() {
505        assert!(
506            !room_message_custom_event_filter()
507                .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
508        );
509    }
510
511    #[test]
512    fn test_room_message_event_type_matches_room_message_event_filter() {
513        assert!(
514            room_message_filter().matches(&FilterInput::message_like(
515                &MessageLikeEventType::RoomMessage.to_string()
516            ))
517        );
518    }
519
520    #[test]
521    fn test_reaction_event_type_does_not_match_room_message_event_filter() {
522        assert!(
523            !room_message_filter()
524                .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
525        );
526    }
527    #[test]
528    fn test_convert_raw_event_into_message_like_filter_input() {
529        let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
530            r#"{"type":"m.room.message","content":{"msgtype":"m.text"}}"#.to_owned(),
531        )
532        .unwrap();
533        let filter_input: FilterInput<'_> =
534            raw_event.try_into().expect("convert to FilterInput failed");
535        assert!(matches!(filter_input, FilterInput::MessageLike(_)));
536        if let FilterInput::MessageLike(message_like) = filter_input {
537            assert_eq!(message_like.event_type, "m.room.message");
538            assert_eq!(message_like.content.msgtype, Some("m.text"));
539        }
540    }
541    #[test]
542    fn test_convert_raw_event_into_state_filter_input() {
543        let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
544            r#"{"type":"m.room.member","state_key":"@alice:example.com"}"#.to_owned(),
545        )
546        .unwrap();
547        let filter_input: FilterInput<'_> =
548            raw_event.try_into().expect("convert to FilterInput failed");
549        assert!(matches!(filter_input, FilterInput::State(_)));
550        if let FilterInput::State(state) = filter_input {
551            assert_eq!(state.event_type, "m.room.member");
552            assert_eq!(state.state_key, "@alice:example.com");
553        }
554    }
555
556    #[test]
557    fn test_to_device_filter_does_match() {
558        let f = Filter::ToDevice(ToDeviceEventFilter::new("my.custom.to.device".into()));
559        assert!(f.matches(&FilterInput::ToDevice(FilterInputToDevice {
560            event_type: "my.custom.to.device",
561        })));
562    }
563}