1use 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#[derive(Clone, Debug)]
31#[cfg_attr(test, derive(PartialEq))]
32pub enum Filter {
33 MessageLike(MessageLikeEventFilter),
35 State(StateEventFilter),
37 ToDevice(ToDeviceEventFilter),
39}
40
41impl Filter {
42 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 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#[derive(Clone, Debug)]
67#[cfg_attr(test, derive(PartialEq))]
68pub enum MessageLikeEventFilter {
69 WithType(MessageLikeEventType),
71 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#[derive(Clone, Debug)]
101#[cfg_attr(test, derive(PartialEq))]
102pub enum StateEventFilter {
103 WithType(StateEventType),
105 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#[derive(Clone, Debug)]
135#[cfg_attr(test, derive(PartialEq))]
136pub struct ToDeviceEventFilter {
137 pub event_type: ToDeviceEventType,
139}
140
141impl ToDeviceEventFilter {
142 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#[derive(Debug, Deserialize)]
157#[serde(untagged)]
158pub enum FilterInput<'a> {
159 #[serde(borrow)]
160 State(FilterInputState<'a>),
163 MessageLike(FilterInputMessageLike<'a>),
165 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#[derive(Debug, Deserialize)]
191pub struct FilterInputState<'a> {
192 #[serde(rename = "type")]
193 pub(super) event_type: &'a str,
196 pub(super) state_key: &'a str,
197}
198
199#[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 #[serde(rename = "type")]
211 pub(super) event_type: &'a str,
212 pub(super) content: MessageLikeFilterEventContent<'a>,
213}
214
215impl<'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 raw_event.deserialize_as()
225 }
226}
227
228impl<'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
250impl<'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 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 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 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!(
334 !room_message_text_event_filter()
335 .matches(&FilterInput::message_with_msgtype("m.image"))
336 );
337 }
338
339 #[test]
340 fn test_text_event_filter_does_not_match_custom_event_with_msgtype() {
341 assert!(!room_message_text_event_filter().matches(&FilterInput::MessageLike(
342 FilterInputMessageLike {
343 event_type: "io.element.message",
344 content: MessageLikeFilterEventContent { msgtype: Some("m.text") }
345 }
346 )));
347 }
348
349 fn reaction_event_filter() -> Filter {
351 Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
352 }
353
354 #[test]
355 fn test_reaction_event_filter_matches_reaction() {
356 assert!(
357 reaction_event_filter()
358 .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
359 );
360 }
361
362 #[test]
363 fn test_reaction_event_filter_does_not_match_room_message() {
364 assert!(!reaction_event_filter().matches(&FilterInput::message_with_msgtype("m.text")));
365 }
366
367 #[test]
368 fn test_reaction_event_filter_does_not_match_state_event_any_key() {
369 assert!(!reaction_event_filter().matches(&FilterInput::state("m.reaction", "")));
370 }
371
372 fn self_member_event_filter() -> Filter {
374 Filter::State(StateEventFilter::WithTypeAndStateKey(
375 StateEventType::RoomMember,
376 "@self:example.me".to_owned(),
377 ))
378 }
379
380 #[test]
381 fn test_self_member_event_filter_matches_self_member_event() {
382 assert!(self_member_event_filter().matches(&FilterInput::state(
383 &TimelineEventType::RoomMember.to_string(),
384 "@self:example.me"
385 )));
386 }
387
388 #[test]
389 fn test_self_member_event_filter_does_not_match_somebody_elses_member_event() {
390 assert!(!self_member_event_filter().matches(&FilterInput::state(
391 &TimelineEventType::RoomMember.to_string(),
392 "@somebody_else.example.me"
393 )));
394 }
395
396 #[test]
397 fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
398 assert!(
399 !self_member_event_filter()
400 .matches(&FilterInput::state("io.element.test_state_event", "@self.example.me"))
401 );
402 }
403
404 #[test]
405 fn test_self_member_event_filter_does_not_match_reaction_event() {
406 assert!(
407 !self_member_event_filter()
408 .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
409 );
410 }
411
412 #[test]
413 fn test_self_member_event_filter_only_matches_specific_state_key() {
414 assert!(
415 !self_member_event_filter()
416 .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
417 );
418 }
419
420 fn member_event_filter() -> Filter {
422 Filter::State(StateEventFilter::WithType(StateEventType::RoomMember))
423 }
424
425 #[test]
426 fn test_member_event_filter_matches_some_member_event() {
427 assert!(member_event_filter().matches(&FilterInput::state(
428 &TimelineEventType::RoomMember.to_string(),
429 "@foo.bar.baz"
430 )));
431 }
432
433 #[test]
434 fn test_member_event_filter_does_not_match_room_name_event() {
435 assert!(
436 !member_event_filter()
437 .matches(&FilterInput::state(&TimelineEventType::RoomName.to_string(), ""))
438 );
439 }
440
441 #[test]
442 fn test_member_event_filter_does_not_match_reaction_event() {
443 assert!(
444 !member_event_filter()
445 .matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
446 );
447 }
448
449 #[test]
450 fn test_member_event_filter_matches_any_state_key() {
451 assert!(
452 member_event_filter()
453 .matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
454 );
455 }
456
457 fn topic_event_filter() -> Filter {
459 Filter::State(StateEventFilter::WithTypeAndStateKey(
460 StateEventType::RoomTopic,
461 "".to_owned(),
462 ))
463 }
464
465 #[test]
466 fn test_topic_event_filter_does_match() {
467 assert!(
468 topic_event_filter()
469 .matches(&FilterInput::state(&StateEventType::RoomTopic.to_string(), ""))
470 );
471 }
472
473 fn room_message_custom_event_filter() -> Filter {
475 Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.custom".to_owned()))
476 }
477
478 fn room_message_filter() -> Filter {
480 Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::RoomMessage))
481 }
482
483 #[test]
484 fn test_reaction_event_type_does_not_match_room_message_text_event_filter() {
485 assert!(
486 !room_message_text_event_filter()
487 .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
488 );
489 }
490
491 #[test]
492 fn test_room_message_event_without_msgtype_does_not_match_custom_msgtype_filter() {
493 assert!(
494 !room_message_custom_event_filter().matches(&FilterInput::message_like(
495 &MessageLikeEventType::RoomMessage.to_string()
496 ))
497 );
498 }
499
500 #[test]
501 fn test_reaction_event_type_does_not_match_room_message_custom_event_filter() {
502 assert!(
503 !room_message_custom_event_filter()
504 .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
505 );
506 }
507
508 #[test]
509 fn test_room_message_event_type_matches_room_message_event_filter() {
510 assert!(
511 room_message_filter().matches(&FilterInput::message_like(
512 &MessageLikeEventType::RoomMessage.to_string()
513 ))
514 );
515 }
516
517 #[test]
518 fn test_reaction_event_type_does_not_match_room_message_event_filter() {
519 assert!(
520 !room_message_filter()
521 .matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
522 );
523 }
524 #[test]
525 fn test_convert_raw_event_into_message_like_filter_input() {
526 let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
527 r#"{"type":"m.room.message","content":{"msgtype":"m.text"}}"#.to_owned(),
528 )
529 .unwrap();
530 let filter_input: FilterInput<'_> =
531 raw_event.try_into().expect("convert to FilterInput failed");
532 assert!(matches!(filter_input, FilterInput::MessageLike(_)));
533 if let FilterInput::MessageLike(message_like) = filter_input {
534 assert_eq!(message_like.event_type, "m.room.message");
535 assert_eq!(message_like.content.msgtype, Some("m.text"));
536 }
537 }
538 #[test]
539 fn test_convert_raw_event_into_state_filter_input() {
540 let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
541 r#"{"type":"m.room.member","state_key":"@alice:example.com"}"#.to_owned(),
542 )
543 .unwrap();
544 let filter_input: FilterInput<'_> =
545 raw_event.try_into().expect("convert to FilterInput failed");
546 assert!(matches!(filter_input, FilterInput::State(_)));
547 if let FilterInput::State(state) = filter_input {
548 assert_eq!(state.event_type, "m.room.member");
549 assert_eq!(state.state_key, "@alice:example.com");
550 }
551 }
552
553 #[test]
554 fn test_to_device_filter_does_match() {
555 let f = Filter::ToDevice(ToDeviceEventFilter::new("my.custom.to.device".into()));
556 assert!(f.matches(&FilterInput::ToDevice(FilterInputToDevice {
557 event_type: "my.custom.to.device",
558 })));
559 }
560}