use matrix_sdk_common::deserialized_responses::TimelineEvent;
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
#[cfg(feature = "e2e-encryption")]
use ruma::{
UserId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
call::invite::SyncCallInviteEvent,
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::{MessageType, SyncRoomMessageEvent},
power_levels::RoomPowerLevels,
},
rtc::notification::SyncRtcNotificationEvent,
sticker::SyncStickerEvent,
},
};
use serde::{Deserialize, Serialize};
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub enum LatestEventValue {
#[default]
None,
Remote(RemoteLatestEventValue),
LocalIsSending(LocalLatestEventValue),
LocalCannotBeSent(LocalLatestEventValue),
}
impl LatestEventValue {
pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
match self {
Self::None => None,
Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(),
Self::LocalIsSending(LocalLatestEventValue { timestamp, .. })
| Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp),
}
}
pub fn is_local(&self) -> bool {
match self {
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true,
Self::None | Self::Remote(_) => false,
}
}
}
pub type RemoteLatestEventValue = TimelineEvent;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalLatestEventValue {
pub timestamp: MilliSecondsSinceUnixEpoch,
pub content: SerializableEventContent,
}
#[cfg(test)]
mod tests_latest_event_value {
use ruma::{
MilliSecondsSinceUnixEpoch,
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
serde::Raw,
uint,
};
use serde_json::json;
use super::{LatestEventValue, LocalLatestEventValue, RemoteLatestEventValue};
use crate::store::SerializableEventContent;
#[test]
fn test_timestamp_with_none() {
let value = LatestEventValue::None;
assert_eq!(value.timestamp(), None);
}
#[test]
fn test_timestamp_with_remote() {
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
Raw::from_json_string(
json!({
"content": RoomMessageEventContent::text_plain("raclette"),
"type": "m.room.message",
"event_id": "$ev0",
"room_id": "!r0",
"origin_server_ts": 42,
"sender": "@mnt_io:matrix.org",
})
.to_string(),
)
.unwrap(),
));
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
#[test]
fn test_timestamp_with_local_is_sending() {
let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
});
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
#[test]
fn test_timestamp_with_local_cannot_be_sent() {
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
});
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
}
#[cfg(feature = "e2e-encryption")]
#[derive(Debug)]
pub enum PossibleLatestEvent<'a> {
YesRoomMessage(&'a SyncRoomMessageEvent),
YesSticker(&'a SyncStickerEvent),
YesPoll(&'a SyncUnstablePollStartEvent),
YesCallInvite(&'a SyncCallInviteEvent),
YesRtcNotification(&'a SyncRtcNotificationEvent),
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
NoUnsupportedEventType,
NoUnsupportedMessageLikeType,
NoEncrypted,
}
#[cfg(feature = "e2e-encryption")]
pub fn is_suitable_for_latest_event<'a>(
event: &'a AnySyncTimelineEvent,
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
) -> PossibleLatestEvent<'a> {
match event {
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
if let Some(original_message) = message.as_original() {
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
}
let is_replacement =
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
relation_type == RelationType::Replacement
} else {
false
}
});
if is_replacement {
PossibleLatestEvent::NoUnsupportedMessageLikeType
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
PossibleLatestEvent::YesPoll(poll)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
PossibleLatestEvent::YesCallInvite(invite)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
PossibleLatestEvent::YesRtcNotification(notify)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
PossibleLatestEvent::YesSticker(sticker)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
PossibleLatestEvent::NoEncrypted
}
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
AnySyncTimelineEvent::State(state) => {
if let AnySyncStateEvent::RoomMember(member) = state
&& matches!(member.membership(), MembershipState::Knock)
{
let can_accept_or_decline_knocks = match power_levels_info {
Some((own_user_id, room_power_levels)) => {
room_power_levels.user_can_invite(own_user_id)
|| room_power_levels.user_can_kick(own_user_id)
}
_ => false,
};
if can_accept_or_decline_knocks {
return PossibleLatestEvent::YesKnockedStateEvent(member);
}
}
PossibleLatestEvent::NoUnsupportedEventType
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct LatestEvent {
event: TimelineEvent,
#[serde(skip_serializing_if = "Option::is_none")]
sender_profile: Option<MinimalRoomMemberEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
sender_name_is_ambiguous: Option<bool>,
}
#[derive(Deserialize)]
struct SerializedLatestEvent {
event: TimelineEvent,
#[serde(skip_serializing_if = "Option::is_none")]
sender_profile: Option<MinimalRoomMemberEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
sender_name_is_ambiguous: Option<bool>,
}
impl<'de> Deserialize<'de> for LatestEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
let mut variant_errors = Vec::new();
match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
Ok(value) => {
return Ok(LatestEvent {
event: value.event,
sender_profile: value.sender_profile,
sender_name_is_ambiguous: value.sender_name_is_ambiguous,
});
}
Err(err) => variant_errors.push(err),
}
match serde_json::from_str::<TimelineEvent>(raw.get()) {
Ok(value) => {
return Ok(LatestEvent {
event: value,
sender_profile: None,
sender_name_is_ambiguous: None,
});
}
Err(err) => variant_errors.push(err),
}
Err(serde::de::Error::custom(format!(
"data did not match any variant of serialized LatestEvent (using serde_json). \
Observed errors: {variant_errors:?}"
)))
}
}
impl LatestEvent {
pub fn new(event: TimelineEvent) -> Self {
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
}
pub fn new_with_sender_details(
event: TimelineEvent,
sender_profile: Option<MinimalRoomMemberEvent>,
sender_name_is_ambiguous: Option<bool>,
) -> Self {
Self { event, sender_profile, sender_name_is_ambiguous }
}
pub fn into_event(self) -> TimelineEvent {
self.event
}
pub fn event(&self) -> &TimelineEvent {
&self.event
}
pub fn event_mut(&mut self) -> &mut TimelineEvent {
&mut self.event
}
pub fn event_id(&self) -> Option<OwnedEventId> {
self.event.event_id()
}
pub fn has_sender_profile(&self) -> bool {
self.sender_profile.is_some()
}
pub fn sender_display_name(&self) -> Option<&str> {
self.sender_profile.as_ref().and_then(|profile| {
profile.as_original().and_then(|event| event.content.displayname.as_deref())
})
}
pub fn sender_name_ambiguous(&self) -> Option<bool> {
self.sender_name_is_ambiguous
}
pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
self.sender_profile.as_ref().and_then(|profile| {
profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
})
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeMap;
use std::time::Duration;
#[cfg(feature = "e2e-encryption")]
use assert_matches::assert_matches;
#[cfg(feature = "e2e-encryption")]
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_test::event_factory::EventFactory;
#[cfg(feature = "e2e-encryption")]
use ruma::{
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
events::{
SyncMessageLikeEvent,
call::{SessionDescription, invite::CallInviteEventContent},
poll::{
unstable_response::UnstablePollResponseEventContent,
unstable_start::{
NewUnstablePollStartEventContent, UnstablePollAnswer,
UnstablePollStartContentBlock,
},
},
relation::Replacement,
room::{
ImageInfo, MediaSource,
encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
},
message::{
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
Relation, RoomMessageEventContent,
},
topic::RoomTopicEventContent,
},
sticker::{StickerEventContent, SyncStickerEvent},
},
owned_event_id, owned_mxc_uri, user_id,
};
use ruma::{
events::rtc::notification::{NotificationType, RtcNotificationEventContent},
serde::Raw,
};
use serde_json::json;
use super::LatestEvent;
#[cfg(feature = "e2e-encryption")]
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_room_messages_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
"".to_owned(),
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
))))
.into();
assert_let!(
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.msgtype.msgtype(), "m.image");
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_polls_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
"do you like rust?",
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
)))
.into();
assert_let!(
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_invites_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(CallInviteEventContent::new(
"call_id".into(),
UInt::new(123).unwrap(),
SessionDescription::new("".into(), "".into()),
VoipVersionId::V1,
))
.into();
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_notifications_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RtcNotificationEventContent::new(
MilliSecondsSinceUnixEpoch::now(),
Duration::new(30, 0),
NotificationType::Ring,
))
.into();
assert_let!(
PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_stickers_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(StickerEventContent::new(
"sticker!".to_owned(),
ImageInfo::new(),
owned_mxc_uri!("mxc://example.com/1"),
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(UnstablePollResponseEventContent::new(
vec![String::from("option1")],
owned_event_id!("$1"),
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_redacted_messages_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_encrypted_messages_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomEncryptedEventContent::new(
EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
BTreeMap::new(),
"".to_owned(),
)),
None,
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoEncrypted
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_state_events_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomTopicEventContent::new("".to_owned()))
.state_key("")
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_replacement_events_are_unsuitable() {
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
owned_event_id!("$1"),
RoomMessageEventContent::text_plain("Hello, world!").into(),
)));
let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_verification_requests_are_unsuitable() {
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomMessageEventContent::new(MessageType::VerificationRequest(
KeyVerificationRequestEventContent::new(
"body".to_owned(),
vec![],
device_id!("device_id").to_owned(),
user_id!("@user_id:example.com").to_owned(),
),
)))
.into();
assert_let!(
PossibleLatestEvent::NoUnsupportedMessageLikeType =
is_suitable_for_latest_event(&event, None)
);
}
#[test]
fn test_deserialize_latest_event() {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct TestStruct {
latest_event: LatestEvent,
}
let event = TimelineEvent::from_plaintext(
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
);
let initial = TestStruct {
latest_event: LatestEvent {
event: event.clone(),
sender_profile: None,
sender_name_is_ambiguous: None,
},
};
let serialized = serde_json::to_value(&initial).unwrap();
assert_eq!(
serialized,
json!({
"latest_event": {
"event": {
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
},
"thread_summary": "None",
"timestamp": null,
}
}
})
);
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
let serialized = json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
}
},
}
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
let serialized = json!({
"latest_event": event
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
}
}