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
29/// Matrix 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.
43    /// A filter input can be create by using the `From` trait on FilterInput
44    /// for [`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.
161    // We first need to check if we can deserialize as a 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 `as_str()`
194    // as soon as ruma supports `as_str()` on `StateEventType` we can use it here.
195    pub(super) event_type: &'a str,
196    pub(super) state_key: &'a str,
197}
198
199// Filter input message like:
200#[derive(Debug, Default, Deserialize)]
201pub(super) struct MessageLikeFilterEventContent<'a> {
202    #[serde(borrow)]
203    pub(super) msgtype: Option<&'a str>,
204}
205
206#[derive(Debug, Deserialize)]
207pub struct FilterInputMessageLike<'a> {
208    // TODO: This wants to be `StateEventType` but we need a type which supports `as_str()`
209    // as soon as ruma supports `as_str()` on `StateEventType` we can use it here.
210    #[serde(rename = "type")]
211    pub(super) event_type: &'a str,
212    pub(super) content: MessageLikeFilterEventContent<'a>,
213}
214
215/// Create a filter input based on [`AnyTimelineEvent`].
216/// This will create a [`FilterInput::State`] or [`FilterInput::MessageLike`]
217/// depending on the event type.
218impl<'a> TryFrom<&'a Raw<AnyTimelineEvent>> for FilterInput<'a> {
219    type Error = serde_json::Error;
220
221    fn try_from(raw_event: &'a Raw<AnyTimelineEvent>) -> Result<Self, Self::Error> {
222        // FilterInput first checks if it can deserialize as a state event (state_key
223        // exists) and then as a message-like event.
224        raw_event.deserialize_as()
225    }
226}
227
228/// Create a filter input based on [`AnyStateEvent`].
229/// This will create a [`FilterInput::State`].
230impl<'a> TryFrom<&'a Raw<AnyStateEvent>> for FilterInput<'a> {
231    type Error = serde_json::Error;
232
233    fn try_from(raw_event: &'a Raw<AnyStateEvent>) -> Result<Self, Self::Error> {
234        raw_event.deserialize_as()
235    }
236}
237
238impl<'a> JsonCastable<FilterInput<'a>> for AnyTimelineEvent {}
239
240impl<'a> JsonCastable<FilterInput<'a>> for AnyStateEvent {}
241
242impl<'a> JsonCastable<FilterInput<'a>> for AnyMessageLikeEvent {}
243
244#[derive(Debug, Deserialize)]
245pub struct FilterInputToDevice<'a> {
246    #[serde(rename = "type")]
247    pub(super) event_type: &'a str,
248}
249
250/// Create a filter input of type [`FilterInput::ToDevice`]`.
251impl<'a> TryFrom<&'a Raw<AnyToDeviceEvent>> for FilterInput<'a> {
252    type Error = serde_json::Error;
253    fn try_from(raw_event: &'a Raw<AnyToDeviceEvent>) -> Result<Self, Self::Error> {
254        // deserialize_as::<FilterInput> will first try state, message-like and then
255        // to-device. The `AnyToDeviceEvent` would match message like first, so
256        // we need to explicitly deserialize as `FilterInputToDevice`.
257        raw_event.deserialize_as::<FilterInputToDevice<'a>>().map(FilterInput::ToDevice)
258    }
259}
260
261impl<'a> JsonCastable<FilterInputToDevice<'a>> for AnyToDeviceEvent {}
262
263impl<'a> From<&'a SendToDeviceRequest> for FilterInput<'a> {
264    fn from(request: &'a SendToDeviceRequest) -> Self {
265        FilterInput::ToDevice(FilterInputToDevice { event_type: &request.event_type })
266    }
267}
268
269impl<'a> From<&'a SendEventRequest> for FilterInput<'a> {
270    fn from(request: &'a SendEventRequest) -> Self {
271        match &request.state_key {
272            None => match request.event_type.as_str() {
273                "m.room.message" => {
274                    if let Some(msgtype) =
275                        serde_json::from_str::<MessageLikeFilterEventContent<'a>>(
276                            request.content.get(),
277                        )
278                        .unwrap_or_else(|e| {
279                            debug!("Failed to deserialize event content for filter: {e}");
280                            // Fallback to empty content is safe.
281                            // If we do have a filter matching any content type, it will match
282                            // independent of the body.
283                            // Any filter that does only match a specific content type will not
284                            // match the empty content.
285                            Default::default()
286                        })
287                        .msgtype
288                    {
289                        FilterInput::message_with_msgtype(msgtype)
290                    } else {
291                        FilterInput::message_like("m.room.message")
292                    }
293                }
294                _ => FilterInput::message_like(&request.event_type),
295            },
296            Some(state_key) => FilterInput::state(&request.event_type, state_key),
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use ruma::{
304        events::{AnyTimelineEvent, MessageLikeEventType, StateEventType, TimelineEventType},
305        serde::Raw,
306    };
307
308    use super::{
309        Filter, FilterInput, FilterInputMessageLike, MessageLikeEventFilter, StateEventFilter,
310    };
311    use crate::widget::filter::{
312        FilterInputToDevice, MessageLikeFilterEventContent, ToDeviceEventFilter,
313    };
314
315    fn message_event(event_type: &str) -> FilterInput<'_> {
316        FilterInput::MessageLike(FilterInputMessageLike { event_type, content: Default::default() })
317    }
318
319    // Tests against a `m.room.message` filter with `msgtype = m.text`
320    fn room_message_text_event_filter() -> Filter {
321        Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.text".to_owned()))
322    }
323
324    #[test]
325    fn test_text_event_filter_matches_text_event() {
326        assert!(
327            room_message_text_event_filter().matches(&FilterInput::message_with_msgtype("m.text")),
328        );
329    }
330
331    #[test]
332    fn test_text_event_filter_does_not_match_image_event() {
333        assert!(!room_message_text_event_filter()
334            .matches(&FilterInput::message_with_msgtype("m.image")));
335    }
336
337    #[test]
338    fn test_text_event_filter_does_not_match_custom_event_with_msgtype() {
339        assert!(!room_message_text_event_filter().matches(&FilterInput::MessageLike(
340            FilterInputMessageLike {
341                event_type: "io.element.message",
342                content: MessageLikeFilterEventContent { msgtype: Some("m.text") }
343            }
344        )));
345    }
346
347    // Tests against an `m.reaction` filter
348    fn reaction_event_filter() -> Filter {
349        Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
350    }
351
352    #[test]
353    fn test_reaction_event_filter_matches_reaction() {
354        assert!(reaction_event_filter()
355            .matches(&message_event(&MessageLikeEventType::Reaction.to_string())));
356    }
357
358    #[test]
359    fn test_reaction_event_filter_does_not_match_room_message() {
360        assert!(!reaction_event_filter().matches(&FilterInput::message_with_msgtype("m.text")));
361    }
362
363    #[test]
364    fn test_reaction_event_filter_does_not_match_state_event_any_key() {
365        assert!(!reaction_event_filter().matches(&FilterInput::state("m.reaction", "")));
366    }
367
368    // Tests against an `m.room.member` filter with `state_key = "@self:example.me"`
369    fn self_member_event_filter() -> Filter {
370        Filter::State(StateEventFilter::WithTypeAndStateKey(
371            StateEventType::RoomMember,
372            "@self:example.me".to_owned(),
373        ))
374    }
375
376    #[test]
377    fn test_self_member_event_filter_matches_self_member_event() {
378        assert!(self_member_event_filter().matches(&FilterInput::state(
379            &TimelineEventType::RoomMember.to_string(),
380            "@self:example.me"
381        )));
382    }
383
384    #[test]
385    fn test_self_member_event_filter_does_not_match_somebody_elses_member_event() {
386        assert!(!self_member_event_filter().matches(&FilterInput::state(
387            &TimelineEventType::RoomMember.to_string(),
388            "@somebody_else.example.me"
389        )));
390    }
391
392    #[test]
393    fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
394        assert!(!self_member_event_filter()
395            .matches(&FilterInput::state("io.element.test_state_event", "@self.example.me")));
396    }
397
398    #[test]
399    fn test_self_member_event_filter_does_not_match_reaction_event() {
400        assert!(!self_member_event_filter()
401            .matches(&message_event(&MessageLikeEventType::Reaction.to_string())));
402    }
403
404    #[test]
405    fn test_self_member_event_filter_only_matches_specific_state_key() {
406        assert!(!self_member_event_filter()
407            .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), "")));
408    }
409
410    // Tests against an `m.room.member` filter with any `state_key`.
411    fn member_event_filter() -> Filter {
412        Filter::State(StateEventFilter::WithType(StateEventType::RoomMember))
413    }
414
415    #[test]
416    fn test_member_event_filter_matches_some_member_event() {
417        assert!(member_event_filter().matches(&FilterInput::state(
418            &TimelineEventType::RoomMember.to_string(),
419            "@foo.bar.baz"
420        )));
421    }
422
423    #[test]
424    fn test_member_event_filter_does_not_match_room_name_event() {
425        assert!(!member_event_filter()
426            .matches(&FilterInput::state(&TimelineEventType::RoomName.to_string(), "")));
427    }
428
429    #[test]
430    fn test_member_event_filter_does_not_match_reaction_event() {
431        assert!(!member_event_filter()
432            .matches(&message_event(&MessageLikeEventType::Reaction.to_string())));
433    }
434
435    #[test]
436    fn test_member_event_filter_matches_any_state_key() {
437        assert!(member_event_filter()
438            .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), "")));
439    }
440
441    // Tests against an `m.room.topic` filter with `state_key = ""`
442    fn topic_event_filter() -> Filter {
443        Filter::State(StateEventFilter::WithTypeAndStateKey(
444            StateEventType::RoomTopic,
445            "".to_owned(),
446        ))
447    }
448
449    #[test]
450    fn test_topic_event_filter_does_match() {
451        assert!(topic_event_filter()
452            .matches(&FilterInput::state(&StateEventType::RoomTopic.to_string(), "")));
453    }
454
455    // Tests against an `m.room.message` filter with `msgtype = m.custom`
456    fn room_message_custom_event_filter() -> Filter {
457        Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.custom".to_owned()))
458    }
459
460    // Tests against an `m.room.message` filter without a `msgtype`
461    fn room_message_filter() -> Filter {
462        Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::RoomMessage))
463    }
464
465    #[test]
466    fn test_reaction_event_type_does_not_match_room_message_text_event_filter() {
467        assert!(!room_message_text_event_filter()
468            .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string())));
469    }
470
471    #[test]
472    fn test_room_message_event_without_msgtype_does_not_match_custom_msgtype_filter() {
473        assert!(!room_message_custom_event_filter()
474            .matches(&FilterInput::message_like(&MessageLikeEventType::RoomMessage.to_string())));
475    }
476
477    #[test]
478    fn test_reaction_event_type_does_not_match_room_message_custom_event_filter() {
479        assert!(!room_message_custom_event_filter()
480            .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string())));
481    }
482
483    #[test]
484    fn test_room_message_event_type_matches_room_message_event_filter() {
485        assert!(room_message_filter()
486            .matches(&FilterInput::message_like(&MessageLikeEventType::RoomMessage.to_string())));
487    }
488
489    #[test]
490    fn test_reaction_event_type_does_not_match_room_message_event_filter() {
491        assert!(!room_message_filter()
492            .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string())));
493    }
494    #[test]
495    fn test_convert_raw_event_into_message_like_filter_input() {
496        let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
497            r#"{"type":"m.room.message","content":{"msgtype":"m.text"}}"#.to_owned(),
498        )
499        .unwrap();
500        let filter_input: FilterInput<'_> =
501            raw_event.try_into().expect("convert to FilterInput failed");
502        assert!(matches!(filter_input, FilterInput::MessageLike(_)));
503        if let FilterInput::MessageLike(message_like) = filter_input {
504            assert_eq!(message_like.event_type, "m.room.message");
505            assert_eq!(message_like.content.msgtype, Some("m.text"));
506        }
507    }
508    #[test]
509    fn test_convert_raw_event_into_state_filter_input() {
510        let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
511            r#"{"type":"m.room.member","state_key":"@alice:example.com"}"#.to_owned(),
512        )
513        .unwrap();
514        let filter_input: FilterInput<'_> =
515            raw_event.try_into().expect("convert to FilterInput failed");
516        assert!(matches!(filter_input, FilterInput::State(_)));
517        if let FilterInput::State(state) = filter_input {
518            assert_eq!(state.event_type, "m.room.member");
519            assert_eq!(state.state_key, "@alice:example.com");
520        }
521    }
522
523    #[test]
524    fn test_to_device_filter_does_match() {
525        let f = Filter::ToDevice(ToDeviceEventFilter::new("my.custom.to.device".into()));
526        assert!(f.matches(&FilterInput::ToDevice(FilterInputToDevice {
527            event_type: "my.custom.to.device",
528        })));
529    }
530}