use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyTimelineEvent, AnyToDeviceEvent,
MessageLikeEventType, StateEventType, ToDeviceEventType,
},
serde::{JsonCastable, Raw},
};
use serde::Deserialize;
use tracing::debug;
use super::machine::{SendEventRequest, SendToDeviceRequest};
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Filter {
MessageLike(MessageLikeEventFilter),
State(StateEventFilter),
ToDevice(ToDeviceEventFilter),
}
impl Filter {
pub(super) fn matches(&self, filter_input: &FilterInput<'_>) -> bool {
match self {
Self::MessageLike(filter) => filter.matches(filter_input),
Self::State(filter) => filter.matches(filter_input),
Self::ToDevice(filter) => filter.matches(filter_input),
}
}
pub(super) fn filter_event_type(&self) -> String {
match self {
Self::MessageLike(filter) => filter.filter_event_type(),
Self::State(filter) => filter.filter_event_type(),
Self::ToDevice(filter) => filter.event_type.to_string(),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum MessageLikeEventFilter {
WithType(MessageLikeEventType),
RoomMessageWithMsgtype(String),
}
impl<'a> MessageLikeEventFilter {
fn matches(&self, filter_input: &FilterInput<'a>) -> bool {
let FilterInput::MessageLike(message_like_filter_input) = filter_input else {
return false;
};
match self {
Self::WithType(filter_event_type) => {
message_like_filter_input.event_type == filter_event_type.to_string()
}
Self::RoomMessageWithMsgtype(msgtype) => {
message_like_filter_input.event_type == "m.room.message"
&& message_like_filter_input.content.msgtype == Some(msgtype)
}
}
}
fn filter_event_type(&self) -> String {
match self {
Self::WithType(filter_event_type) => filter_event_type.to_string(),
Self::RoomMessageWithMsgtype(_) => MessageLikeEventType::RoomMessage.to_string(),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum StateEventFilter {
WithType(StateEventType),
WithTypeAndStateKey(StateEventType, String),
}
impl<'a> StateEventFilter {
fn matches(&self, filter_input: &FilterInput<'a>) -> bool {
let FilterInput::State(state_filter_input) = filter_input else {
return false;
};
match self {
StateEventFilter::WithType(filter_type) => {
state_filter_input.event_type == filter_type.to_string()
}
StateEventFilter::WithTypeAndStateKey(event_type, filter_state_key) => {
state_filter_input.event_type == event_type.to_string()
&& state_filter_input.state_key == *filter_state_key
}
}
}
fn filter_event_type(&self) -> String {
match self {
Self::WithType(filter_event_type) => filter_event_type.to_string(),
Self::WithTypeAndStateKey(event_type, _) => event_type.to_string(),
}
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ToDeviceEventFilter {
pub event_type: ToDeviceEventType,
}
impl ToDeviceEventFilter {
pub fn new(event_type: ToDeviceEventType) -> Self {
Self { event_type }
}
fn matches(&self, filter_input: &FilterInput<'_>) -> bool {
matches!(filter_input,FilterInput::ToDevice(f_in) if f_in.event_type == self.event_type.to_string())
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum FilterInput<'a> {
#[serde(borrow)]
State(FilterInputState<'a>),
MessageLike(FilterInputMessageLike<'a>),
ToDevice(FilterInputToDevice<'a>),
}
impl<'a> FilterInput<'a> {
pub fn message_like(event_type: &'a str) -> Self {
Self::MessageLike(FilterInputMessageLike {
event_type,
content: MessageLikeFilterEventContent { msgtype: None },
})
}
pub(super) fn message_with_msgtype(msgtype: &'a str) -> Self {
Self::MessageLike(FilterInputMessageLike {
event_type: "m.room.message",
content: MessageLikeFilterEventContent { msgtype: Some(msgtype) },
})
}
pub fn state(event_type: &'a str, state_key: &'a str) -> Self {
Self::State(FilterInputState { event_type, state_key })
}
}
#[derive(Debug, Deserialize)]
pub struct FilterInputState<'a> {
#[serde(rename = "type")]
pub(super) event_type: &'a str,
pub(super) state_key: &'a str,
}
#[derive(Debug, Default, Deserialize)]
pub(super) struct MessageLikeFilterEventContent<'a> {
#[serde(borrow)]
pub(super) msgtype: Option<&'a str>,
}
#[derive(Debug, Deserialize)]
pub struct FilterInputMessageLike<'a> {
#[serde(rename = "type")]
pub(super) event_type: &'a str,
pub(super) content: MessageLikeFilterEventContent<'a>,
}
impl<'a> TryFrom<&'a Raw<AnyTimelineEvent>> for FilterInput<'a> {
type Error = serde_json::Error;
fn try_from(raw_event: &'a Raw<AnyTimelineEvent>) -> Result<Self, Self::Error> {
raw_event.deserialize_as()
}
}
impl<'a> TryFrom<&'a Raw<AnyStateEvent>> for FilterInput<'a> {
type Error = serde_json::Error;
fn try_from(raw_event: &'a Raw<AnyStateEvent>) -> Result<Self, Self::Error> {
raw_event.deserialize_as()
}
}
impl<'a> JsonCastable<FilterInput<'a>> for AnyTimelineEvent {}
impl<'a> JsonCastable<FilterInput<'a>> for AnyStateEvent {}
impl<'a> JsonCastable<FilterInput<'a>> for AnyMessageLikeEvent {}
#[derive(Debug, Deserialize)]
pub struct FilterInputToDevice<'a> {
#[serde(rename = "type")]
pub(super) event_type: &'a str,
}
impl<'a> TryFrom<&'a Raw<AnyToDeviceEvent>> for FilterInput<'a> {
type Error = serde_json::Error;
fn try_from(raw_event: &'a Raw<AnyToDeviceEvent>) -> Result<Self, Self::Error> {
raw_event.deserialize_as::<FilterInputToDevice<'a>>().map(FilterInput::ToDevice)
}
}
impl<'a> JsonCastable<FilterInputToDevice<'a>> for AnyToDeviceEvent {}
impl<'a> From<&'a SendToDeviceRequest> for FilterInput<'a> {
fn from(request: &'a SendToDeviceRequest) -> Self {
FilterInput::ToDevice(FilterInputToDevice { event_type: &request.event_type })
}
}
impl<'a> From<&'a SendEventRequest> for FilterInput<'a> {
fn from(request: &'a SendEventRequest) -> Self {
match &request.state_key {
None => match request.event_type.as_str() {
"m.room.message" => {
if let Some(msgtype) =
serde_json::from_str::<MessageLikeFilterEventContent<'a>>(
request.content.get(),
)
.unwrap_or_else(|e| {
debug!("Failed to deserialize event content for filter: {e}");
Default::default()
})
.msgtype
{
FilterInput::message_with_msgtype(msgtype)
} else {
FilterInput::message_like("m.room.message")
}
}
_ => FilterInput::message_like(&request.event_type),
},
Some(state_key) => FilterInput::state(&request.event_type, state_key),
}
}
}
#[cfg(test)]
mod tests {
use ruma::{
events::{AnyTimelineEvent, MessageLikeEventType, StateEventType, TimelineEventType},
serde::Raw,
};
use super::{
Filter, FilterInput, FilterInputMessageLike, MessageLikeEventFilter, StateEventFilter,
};
use crate::widget::filter::{
FilterInputToDevice, MessageLikeFilterEventContent, ToDeviceEventFilter,
};
fn message_event(event_type: &str) -> FilterInput<'_> {
FilterInput::MessageLike(FilterInputMessageLike { event_type, content: Default::default() })
}
fn room_message_text_event_filter() -> Filter {
Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.text".to_owned()))
}
#[test]
fn test_text_event_filter_matches_text_event() {
assert!(
room_message_text_event_filter().matches(&FilterInput::message_with_msgtype("m.text")),
);
}
#[test]
fn test_text_event_filter_does_not_match_image_event() {
assert!(
!room_message_text_event_filter()
.matches(&FilterInput::message_with_msgtype("m.image"))
);
}
#[test]
fn test_text_event_filter_does_not_match_custom_event_with_msgtype() {
assert!(!room_message_text_event_filter().matches(&FilterInput::MessageLike(
FilterInputMessageLike {
event_type: "io.element.message",
content: MessageLikeFilterEventContent { msgtype: Some("m.text") }
}
)));
}
fn reaction_event_filter() -> Filter {
Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::Reaction))
}
#[test]
fn test_reaction_event_filter_matches_reaction() {
assert!(
reaction_event_filter()
.matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_reaction_event_filter_does_not_match_room_message() {
assert!(!reaction_event_filter().matches(&FilterInput::message_with_msgtype("m.text")));
}
#[test]
fn test_reaction_event_filter_does_not_match_state_event_any_key() {
assert!(!reaction_event_filter().matches(&FilterInput::state("m.reaction", "")));
}
fn self_member_event_filter() -> Filter {
Filter::State(StateEventFilter::WithTypeAndStateKey(
StateEventType::RoomMember,
"@self:example.me".to_owned(),
))
}
#[test]
fn test_self_member_event_filter_matches_self_member_event() {
assert!(self_member_event_filter().matches(&FilterInput::state(
&TimelineEventType::RoomMember.to_string(),
"@self:example.me"
)));
}
#[test]
fn test_self_member_event_filter_does_not_match_somebody_elses_member_event() {
assert!(!self_member_event_filter().matches(&FilterInput::state(
&TimelineEventType::RoomMember.to_string(),
"@somebody_else.example.me"
)));
}
#[test]
fn self_member_event_filter_does_not_match_unrelated_state_event_with_same_state_key() {
assert!(
!self_member_event_filter()
.matches(&FilterInput::state("io.element.test_state_event", "@self.example.me"))
);
}
#[test]
fn test_self_member_event_filter_does_not_match_reaction_event() {
assert!(
!self_member_event_filter()
.matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_self_member_event_filter_only_matches_specific_state_key() {
assert!(
!self_member_event_filter()
.matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
);
}
fn member_event_filter() -> Filter {
Filter::State(StateEventFilter::WithType(StateEventType::RoomMember))
}
#[test]
fn test_member_event_filter_matches_some_member_event() {
assert!(member_event_filter().matches(&FilterInput::state(
&TimelineEventType::RoomMember.to_string(),
"@foo.bar.baz"
)));
}
#[test]
fn test_member_event_filter_does_not_match_room_name_event() {
assert!(
!member_event_filter()
.matches(&FilterInput::state(&TimelineEventType::RoomName.to_string(), ""))
);
}
#[test]
fn test_member_event_filter_does_not_match_reaction_event() {
assert!(
!member_event_filter()
.matches(&message_event(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_member_event_filter_matches_any_state_key() {
assert!(
member_event_filter()
.matches(&FilterInput::state(&StateEventType::RoomMember.to_string(), ""))
);
}
fn topic_event_filter() -> Filter {
Filter::State(StateEventFilter::WithTypeAndStateKey(
StateEventType::RoomTopic,
"".to_owned(),
))
}
#[test]
fn test_topic_event_filter_does_match() {
assert!(
topic_event_filter()
.matches(&FilterInput::state(&StateEventType::RoomTopic.to_string(), ""))
);
}
fn room_message_custom_event_filter() -> Filter {
Filter::MessageLike(MessageLikeEventFilter::RoomMessageWithMsgtype("m.custom".to_owned()))
}
fn room_message_filter() -> Filter {
Filter::MessageLike(MessageLikeEventFilter::WithType(MessageLikeEventType::RoomMessage))
}
#[test]
fn test_reaction_event_type_does_not_match_room_message_text_event_filter() {
assert!(
!room_message_text_event_filter()
.matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_room_message_event_without_msgtype_does_not_match_custom_msgtype_filter() {
assert!(
!room_message_custom_event_filter().matches(&FilterInput::message_like(
&MessageLikeEventType::RoomMessage.to_string()
))
);
}
#[test]
fn test_reaction_event_type_does_not_match_room_message_custom_event_filter() {
assert!(
!room_message_custom_event_filter()
.matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_room_message_event_type_matches_room_message_event_filter() {
assert!(
room_message_filter().matches(&FilterInput::message_like(
&MessageLikeEventType::RoomMessage.to_string()
))
);
}
#[test]
fn test_reaction_event_type_does_not_match_room_message_event_filter() {
assert!(
!room_message_filter()
.matches(&FilterInput::message_like(&MessageLikeEventType::Reaction.to_string()))
);
}
#[test]
fn test_convert_raw_event_into_message_like_filter_input() {
let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
r#"{"type":"m.room.message","content":{"msgtype":"m.text"}}"#.to_owned(),
)
.unwrap();
let filter_input: FilterInput<'_> =
raw_event.try_into().expect("convert to FilterInput failed");
assert!(matches!(filter_input, FilterInput::MessageLike(_)));
if let FilterInput::MessageLike(message_like) = filter_input {
assert_eq!(message_like.event_type, "m.room.message");
assert_eq!(message_like.content.msgtype, Some("m.text"));
}
}
#[test]
fn test_convert_raw_event_into_state_filter_input() {
let raw_event = &Raw::<AnyTimelineEvent>::from_json_string(
r#"{"type":"m.room.member","state_key":"@alice:example.com"}"#.to_owned(),
)
.unwrap();
let filter_input: FilterInput<'_> =
raw_event.try_into().expect("convert to FilterInput failed");
assert!(matches!(filter_input, FilterInput::State(_)));
if let FilterInput::State(state) = filter_input {
assert_eq!(state.event_type, "m.room.member");
assert_eq!(state.state_key, "@alice:example.com");
}
}
#[test]
fn test_to_device_filter_does_match() {
let f = Filter::ToDevice(ToDeviceEventFilter::new("my.custom.to.device".into()));
assert!(f.matches(&FilterInput::ToDevice(FilterInputToDevice {
event_type: "my.custom.to.device",
})));
}
}