use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
use serde::Deserialize;
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum EventFilter {
MessageLike(MessageLikeEventFilter),
State(StateEventFilter),
}
impl EventFilter {
pub(super) fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
match self {
EventFilter::MessageLike(message_filter) => message_filter.matches(matrix_event),
EventFilter::State(state_filter) => state_filter.matches(matrix_event),
}
}
pub(super) fn matches_state_event_with_any_state_key(
&self,
event_type: &StateEventType,
) -> bool {
matches!(
self,
Self::State(filter) if filter.matches_state_event_with_any_state_key(event_type)
)
}
pub(super) fn matches_message_like_event_type(
&self,
event_type: &MessageLikeEventType,
) -> bool {
matches!(
self,
Self::MessageLike(filter) if filter.matches_message_like_event_type(event_type)
)
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum MessageLikeEventFilter {
WithType(MessageLikeEventType),
RoomMessageWithMsgtype(String),
}
impl MessageLikeEventFilter {
fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
if matrix_event.state_key.is_some() {
return false;
}
match self {
MessageLikeEventFilter::WithType(event_type) => {
matrix_event.event_type == TimelineEventType::from(event_type.clone())
}
MessageLikeEventFilter::RoomMessageWithMsgtype(msgtype) => {
matrix_event.event_type == TimelineEventType::RoomMessage
&& matrix_event.content.msgtype.as_ref() == Some(msgtype)
}
}
}
fn matches_message_like_event_type(&self, event_type: &MessageLikeEventType) -> bool {
match self {
MessageLikeEventFilter::WithType(filter_event_type) => filter_event_type == event_type,
MessageLikeEventFilter::RoomMessageWithMsgtype(_) => {
event_type == &MessageLikeEventType::RoomMessage
}
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum StateEventFilter {
WithType(StateEventType),
WithTypeAndStateKey(StateEventType, String),
}
impl StateEventFilter {
fn matches(&self, matrix_event: &MatrixEventFilterInput) -> bool {
let Some(state_key) = &matrix_event.state_key else {
return false;
};
match self {
StateEventFilter::WithType(event_type) => {
matrix_event.event_type == TimelineEventType::from(event_type.clone())
}
StateEventFilter::WithTypeAndStateKey(event_type, filter_state_key) => {
matrix_event.event_type == TimelineEventType::from(event_type.clone())
&& state_key == filter_state_key
}
}
}
fn matches_state_event_with_any_state_key(&self, event_type: &StateEventType) -> bool {
matches!(self, Self::WithType(ty) if ty == event_type)
}
}
#[derive(Debug, Deserialize)]
pub(super) struct MatrixEventFilterInput {
#[serde(rename = "type")]
pub(super) event_type: TimelineEventType,
pub(super) state_key: Option<String>,
pub(super) content: MatrixEventContent,
}
#[derive(Debug, Default, Deserialize)]
pub(super) struct MatrixEventContent {
pub(super) msgtype: Option<String>,
}
#[cfg(test)]
mod tests {
use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
use super::{
EventFilter, MatrixEventContent, MatrixEventFilterInput, MessageLikeEventFilter,
StateEventFilter,
};
fn message_event(event_type: TimelineEventType) -> MatrixEventFilterInput {
MatrixEventFilterInput { event_type, state_key: None, content: Default::default() }
}
fn message_event_with_msgtype(
event_type: TimelineEventType,
msgtype: String,
) -> MatrixEventFilterInput {
MatrixEventFilterInput {
event_type,
state_key: None,
content: MatrixEventContent { msgtype: Some(msgtype) },
}
}
fn state_event(event_type: TimelineEventType, state_key: String) -> MatrixEventFilterInput {
MatrixEventFilterInput {
event_type,
state_key: Some(state_key),
content: Default::default(),
}
}
fn room_message_text_event_filter() -> EventFilter {
EventFilter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(
"m.text".to_owned(),
))
}
#[test]
fn text_event_filter_matches_text_event() {
assert!(room_message_text_event_filter().matches(&message_event_with_msgtype(
TimelineEventType::RoomMessage,
"m.text".to_owned()
)));
}
#[test]
fn text_event_filter_does_not_match_image_event() {
assert!(!room_message_text_event_filter().matches(&message_event_with_msgtype(
TimelineEventType::RoomMessage,
"m.image".to_owned()
)));
}
#[test]
fn text_event_filter_does_not_match_custom_event_with_msgtype() {
assert!(!room_message_text_event_filter().matches(&message_event_with_msgtype(
"io.element.message".into(),
"m.text".to_owned()
)));
}
fn reaction_event_filter() -> EventFilter {
EventFilter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
}
#[test]
fn reaction_event_filter_matches_reaction() {
assert!(reaction_event_filter().matches(&message_event(TimelineEventType::Reaction)));
}
#[test]
fn reaction_event_filter_does_not_match_room_message() {
assert!(!reaction_event_filter().matches(&message_event_with_msgtype(
TimelineEventType::RoomMessage,
"m.text".to_owned()
)));
}
#[test]
fn reaction_event_filter_does_not_match_state_event() {
assert!(!reaction_event_filter().matches(&state_event(
TimelineEventType::Reaction,
"".to_owned()
)));
}
#[test]
fn reaction_event_filter_does_not_match_state_event_any_key() {
assert!(
!reaction_event_filter().matches_state_event_with_any_state_key(&"m.reaction".into())
);
}
fn self_member_event_filter() -> EventFilter {
EventFilter::State(StateEventFilter::WithTypeAndStateKey(
StateEventType::RoomMember,
"@self:example.me".to_owned(),
))
}
#[test]
fn self_member_event_filter_matches_self_member_event() {
assert!(self_member_event_filter()
.matches(&state_event(TimelineEventType::RoomMember, "@self:example.me".to_owned())));
}
#[test]
fn self_member_event_filter_does_not_match_somebody_elses_member_event() {
assert!(!self_member_event_filter().matches(&state_event(
TimelineEventType::RoomMember,
"@somebody_else.example.me".to_owned()
)));
}
#[test]
fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
assert!(!self_member_event_filter().matches(&state_event(
TimelineEventType::from("io.element.test_state_event"),
"@self.example.me".to_owned()
)));
}
#[test]
fn self_member_event_filter_does_not_match_reaction_event() {
assert!(!self_member_event_filter().matches(&message_event(TimelineEventType::Reaction)));
}
#[test]
fn self_member_event_filter_only_matches_specific_state_key() {
assert!(!self_member_event_filter()
.matches_state_event_with_any_state_key(&StateEventType::RoomMember));
}
fn member_event_filter() -> EventFilter {
EventFilter::State(StateEventFilter::WithType(StateEventType::RoomMember))
}
#[test]
fn member_event_filter_matches_some_member_event() {
assert!(member_event_filter()
.matches(&state_event(TimelineEventType::RoomMember, "@foo.bar.baz".to_owned())));
}
#[test]
fn member_event_filter_does_not_match_room_name_event() {
assert!(!member_event_filter()
.matches(&state_event(TimelineEventType::RoomName, "".to_owned())));
}
#[test]
fn member_event_filter_does_not_match_reaction_event() {
assert!(!member_event_filter().matches(&message_event(TimelineEventType::Reaction)));
}
#[test]
fn member_event_filter_matches_any_state_key() {
assert!(member_event_filter()
.matches_state_event_with_any_state_key(&StateEventType::RoomMember));
}
fn topic_event_filter() -> EventFilter {
EventFilter::State(StateEventFilter::WithTypeAndStateKey(
StateEventType::RoomTopic,
"".to_owned(),
))
}
#[test]
fn topic_event_filter_does_not_match_any_state_key() {
assert!(!topic_event_filter()
.matches_state_event_with_any_state_key(&StateEventType::RoomTopic));
}
fn room_message_custom_event_filter() -> EventFilter {
EventFilter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype(
"m.custom".to_owned(),
))
}
fn room_message_filter() -> EventFilter {
EventFilter::MessageLike(MessageLikeEventFilter::WithType(
MessageLikeEventType::RoomMessage,
))
}
#[test]
fn room_message_event_type_matches_room_message_text_event_filter() {
assert!(room_message_text_event_filter()
.matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
}
#[test]
fn reaction_event_type_does_not_match_room_message_text_event_filter() {
assert!(!room_message_text_event_filter()
.matches_message_like_event_type(&MessageLikeEventType::Reaction));
}
#[test]
fn room_message_event_type_matches_room_message_custom_event_filter() {
assert!(room_message_custom_event_filter()
.matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
}
#[test]
fn reaction_event_type_does_not_match_room_message_custom_event_filter() {
assert!(!room_message_custom_event_filter()
.matches_message_like_event_type(&MessageLikeEventType::Reaction));
}
#[test]
fn room_message_event_type_matches_room_message_event_filter() {
assert!(room_message_filter()
.matches_message_like_event_type(&MessageLikeEventType::RoomMessage));
}
#[test]
fn reaction_event_type_does_not_match_room_message_event_filter() {
assert!(
!room_message_filter().matches_message_like_event_type(&MessageLikeEventType::Reaction)
);
}
}