1use matrix_sdk_common::deserialized_responses::TimelineEvent;
5#[cfg(feature = "e2e-encryption")]
6use ruma::{
7 events::{
8 call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
9 poll::unstable_start::SyncUnstablePollStartEvent,
10 relation::RelationType,
11 room::{
12 member::{MembershipState, SyncRoomMemberEvent},
13 message::{MessageType, SyncRoomMessageEvent},
14 power_levels::RoomPowerLevels,
15 },
16 sticker::SyncStickerEvent,
17 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
18 },
19 UserId,
20};
21use ruma::{MxcUri, OwnedEventId};
22use serde::{Deserialize, Serialize};
23
24use crate::MinimalRoomMemberEvent;
25
26#[cfg(feature = "e2e-encryption")]
31#[derive(Debug)]
32pub enum PossibleLatestEvent<'a> {
33 YesRoomMessage(&'a SyncRoomMessageEvent),
35 YesSticker(&'a SyncStickerEvent),
37 YesPoll(&'a SyncUnstablePollStartEvent),
39
40 YesCallInvite(&'a SyncCallInviteEvent),
42
43 YesCallNotify(&'a SyncCallNotifyEvent),
45
46 YesKnockedStateEvent(&'a SyncRoomMemberEvent),
49
50 NoUnsupportedEventType,
54 NoUnsupportedMessageLikeType,
56 NoEncrypted,
58}
59
60#[cfg(feature = "e2e-encryption")]
63pub fn is_suitable_for_latest_event<'a>(
64 event: &'a AnySyncTimelineEvent,
65 power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
66) -> PossibleLatestEvent<'a> {
67 match event {
68 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
70 if let Some(original_message) = message.as_original() {
71 if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
73 return PossibleLatestEvent::NoUnsupportedMessageLikeType;
74 }
75
76 let is_replacement =
78 original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
79 if let Some(relation_type) = relates_to.rel_type() {
80 relation_type == RelationType::Replacement
81 } else {
82 false
83 }
84 });
85
86 if is_replacement {
87 PossibleLatestEvent::NoUnsupportedMessageLikeType
88 } else {
89 PossibleLatestEvent::YesRoomMessage(message)
90 }
91 } else {
92 PossibleLatestEvent::YesRoomMessage(message)
93 }
94 }
95
96 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
97 PossibleLatestEvent::YesPoll(poll)
98 }
99
100 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
101 PossibleLatestEvent::YesCallInvite(invite)
102 }
103
104 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
105 PossibleLatestEvent::YesCallNotify(notify)
106 }
107
108 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
109 PossibleLatestEvent::YesSticker(sticker)
110 }
111
112 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
114 PossibleLatestEvent::NoEncrypted
115 }
116
117 AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
123
124 AnySyncTimelineEvent::State(state) => {
126 if let AnySyncStateEvent::RoomMember(member) = state {
129 if matches!(member.membership(), MembershipState::Knock) {
130 let can_accept_or_decline_knocks = match power_levels_info {
131 Some((own_user_id, room_power_levels)) => {
132 room_power_levels.user_can_invite(own_user_id)
133 || room_power_levels.user_can_kick(own_user_id)
134 }
135 _ => false,
136 };
137
138 if can_accept_or_decline_knocks {
141 return PossibleLatestEvent::YesKnockedStateEvent(member);
142 }
143 }
144 }
145 PossibleLatestEvent::NoUnsupportedEventType
146 }
147 }
148}
149
150#[derive(Clone, Debug, Serialize)]
170pub struct LatestEvent {
171 event: TimelineEvent,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 sender_profile: Option<MinimalRoomMemberEvent>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 sender_name_is_ambiguous: Option<bool>,
181}
182
183#[derive(Deserialize)]
184struct SerializedLatestEvent {
185 event: TimelineEvent,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 sender_profile: Option<MinimalRoomMemberEvent>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 sender_name_is_ambiguous: Option<bool>,
195}
196
197impl<'de> Deserialize<'de> for LatestEvent {
200 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
201 where
202 D: serde::Deserializer<'de>,
203 {
204 let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
205
206 let mut variant_errors = Vec::new();
207
208 match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
209 Ok(value) => {
210 return Ok(LatestEvent {
211 event: value.event,
212 sender_profile: value.sender_profile,
213 sender_name_is_ambiguous: value.sender_name_is_ambiguous,
214 });
215 }
216 Err(err) => variant_errors.push(err),
217 }
218
219 match serde_json::from_str::<TimelineEvent>(raw.get()) {
220 Ok(value) => {
221 return Ok(LatestEvent {
222 event: value,
223 sender_profile: None,
224 sender_name_is_ambiguous: None,
225 });
226 }
227 Err(err) => variant_errors.push(err),
228 }
229
230 Err(serde::de::Error::custom(
231 format!("data did not match any variant of serialized LatestEvent (using serde_json). Observed errors: {variant_errors:?}")
232 ))
233 }
234}
235
236impl LatestEvent {
237 pub fn new(event: TimelineEvent) -> Self {
239 Self { event, sender_profile: None, sender_name_is_ambiguous: None }
240 }
241
242 pub fn new_with_sender_details(
244 event: TimelineEvent,
245 sender_profile: Option<MinimalRoomMemberEvent>,
246 sender_name_is_ambiguous: Option<bool>,
247 ) -> Self {
248 Self { event, sender_profile, sender_name_is_ambiguous }
249 }
250
251 pub fn into_event(self) -> TimelineEvent {
253 self.event
254 }
255
256 pub fn event(&self) -> &TimelineEvent {
258 &self.event
259 }
260
261 pub fn event_mut(&mut self) -> &mut TimelineEvent {
263 &mut self.event
264 }
265
266 pub fn event_id(&self) -> Option<OwnedEventId> {
268 self.event.event_id()
269 }
270
271 pub fn has_sender_profile(&self) -> bool {
273 self.sender_profile.is_some()
274 }
275
276 pub fn sender_display_name(&self) -> Option<&str> {
279 self.sender_profile.as_ref().and_then(|profile| {
280 profile.as_original().and_then(|event| event.content.displayname.as_deref())
281 })
282 }
283
284 pub fn sender_name_ambiguous(&self) -> Option<bool> {
288 self.sender_name_is_ambiguous
289 }
290
291 pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
294 self.sender_profile.as_ref().and_then(|profile| {
295 profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
296 })
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 #[cfg(feature = "e2e-encryption")]
303 use std::collections::BTreeMap;
304
305 #[cfg(feature = "e2e-encryption")]
306 use assert_matches::assert_matches;
307 #[cfg(feature = "e2e-encryption")]
308 use assert_matches2::assert_let;
309 use matrix_sdk_common::deserialized_responses::TimelineEvent;
310 use ruma::serde::Raw;
311 #[cfg(feature = "e2e-encryption")]
312 use ruma::{
313 events::{
314 call::{
315 invite::{CallInviteEventContent, SyncCallInviteEvent},
316 notify::{
317 ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
318 },
319 SessionDescription,
320 },
321 poll::{
322 unstable_response::{
323 SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
324 },
325 unstable_start::{
326 NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
327 UnstablePollAnswer, UnstablePollStartContentBlock,
328 },
329 },
330 relation::Replacement,
331 room::{
332 encrypted::{
333 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
334 SyncRoomEncryptedEvent,
335 },
336 message::{
337 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
338 Relation, RoomMessageEventContent, SyncRoomMessageEvent,
339 },
340 topic::{RoomTopicEventContent, SyncRoomTopicEvent},
341 ImageInfo, MediaSource,
342 },
343 sticker::{StickerEventContent, SyncStickerEvent},
344 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
345 Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
346 RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
347 UnsignedRoomRedactionEvent,
348 },
349 owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
350 VoipVersionId,
351 };
352 use serde_json::json;
353
354 use super::LatestEvent;
355 #[cfg(feature = "e2e-encryption")]
356 use super::{is_suitable_for_latest_event, PossibleLatestEvent};
357
358 #[cfg(feature = "e2e-encryption")]
359 #[test]
360 fn test_room_messages_are_suitable() {
361 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
362 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
363 content: RoomMessageEventContent::new(MessageType::Image(
364 ImageMessageEventContent::new(
365 "".to_owned(),
366 MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
367 ),
368 )),
369 event_id: owned_event_id!("$1"),
370 sender: owned_user_id!("@a:b.c"),
371 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
372 unsigned: MessageLikeUnsigned::new(),
373 }),
374 ));
375 assert_let!(
376 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
377 is_suitable_for_latest_event(&event, None)
378 );
379
380 assert_eq!(m.content.msgtype.msgtype(), "m.image");
381 }
382
383 #[cfg(feature = "e2e-encryption")]
384 #[test]
385 fn test_polls_are_suitable() {
386 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
387 SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
388 content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
389 "do you like rust?",
390 vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
391 ))
392 .into(),
393 event_id: owned_event_id!("$1"),
394 sender: owned_user_id!("@a:b.c"),
395 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
396 unsigned: MessageLikeUnsigned::new(),
397 }),
398 ));
399 assert_let!(
400 PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
401 is_suitable_for_latest_event(&event, None)
402 );
403
404 assert_eq!(m.content.poll_start().question.text, "do you like rust?");
405 }
406
407 #[cfg(feature = "e2e-encryption")]
408 #[test]
409 fn test_call_invites_are_suitable() {
410 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
411 SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
412 content: CallInviteEventContent::new(
413 "call_id".into(),
414 UInt::new(123).unwrap(),
415 SessionDescription::new("".into(), "".into()),
416 VoipVersionId::V1,
417 ),
418 event_id: owned_event_id!("$1"),
419 sender: owned_user_id!("@a:b.c"),
420 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
421 unsigned: MessageLikeUnsigned::new(),
422 }),
423 ));
424 assert_let!(
425 PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
426 is_suitable_for_latest_event(&event, None)
427 );
428 }
429
430 #[cfg(feature = "e2e-encryption")]
431 #[test]
432 fn test_call_notifications_are_suitable() {
433 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
434 SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
435 content: CallNotifyEventContent::new(
436 "call_id".into(),
437 ApplicationType::Call,
438 NotifyType::Ring,
439 Mentions::new(),
440 ),
441 event_id: owned_event_id!("$1"),
442 sender: owned_user_id!("@a:b.c"),
443 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
444 unsigned: MessageLikeUnsigned::new(),
445 }),
446 ));
447 assert_let!(
448 PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
449 is_suitable_for_latest_event(&event, None)
450 );
451 }
452
453 #[cfg(feature = "e2e-encryption")]
454 #[test]
455 fn test_stickers_are_suitable() {
456 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
457 SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
458 content: StickerEventContent::new(
459 "sticker!".to_owned(),
460 ImageInfo::new(),
461 owned_mxc_uri!("mxc://example.com/1"),
462 ),
463 event_id: owned_event_id!("$1"),
464 sender: owned_user_id!("@a:b.c"),
465 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
466 unsigned: MessageLikeUnsigned::new(),
467 }),
468 ));
469
470 assert_matches!(
471 is_suitable_for_latest_event(&event, None),
472 PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
473 );
474 }
475
476 #[cfg(feature = "e2e-encryption")]
477 #[test]
478 fn test_different_types_of_messagelike_are_unsuitable() {
479 let event =
480 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
481 SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
482 content: UnstablePollResponseEventContent::new(
483 vec![String::from("option1")],
484 owned_event_id!("$1"),
485 ),
486 event_id: owned_event_id!("$2"),
487 sender: owned_user_id!("@a:b.c"),
488 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
489 unsigned: MessageLikeUnsigned::new(),
490 }),
491 ));
492
493 assert_matches!(
494 is_suitable_for_latest_event(&event, None),
495 PossibleLatestEvent::NoUnsupportedMessageLikeType
496 );
497 }
498
499 #[cfg(feature = "e2e-encryption")]
500 #[test]
501 fn test_redacted_messages_are_suitable() {
502 let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
504 "content": {},
505 "event_id": "$redaction",
506 "sender": "@x:y.za",
507 "origin_server_ts": 223543,
508 "unsigned": { "reason": "foo" }
509 }))
510 .unwrap();
511
512 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
513 SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
514 content: RedactedRoomMessageEventContent::new(),
515 event_id: owned_event_id!("$1"),
516 sender: owned_user_id!("@a:b.c"),
517 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
518 unsigned: RedactedUnsigned::new(room_redaction_event),
519 }),
520 ));
521
522 assert_matches!(
523 is_suitable_for_latest_event(&event, None),
524 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
525 );
526 }
527
528 #[cfg(feature = "e2e-encryption")]
529 #[test]
530 fn test_encrypted_messages_are_unsuitable() {
531 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
532 SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
533 content: RoomEncryptedEventContent::new(
534 EncryptedEventScheme::OlmV1Curve25519AesSha2(
535 OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
536 ),
537 None,
538 ),
539 event_id: owned_event_id!("$1"),
540 sender: owned_user_id!("@a:b.c"),
541 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
542 unsigned: MessageLikeUnsigned::new(),
543 }),
544 ));
545
546 assert_matches!(
547 is_suitable_for_latest_event(&event, None),
548 PossibleLatestEvent::NoEncrypted
549 );
550 }
551
552 #[cfg(feature = "e2e-encryption")]
553 #[test]
554 fn test_state_events_are_unsuitable() {
555 let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
556 SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
557 content: RoomTopicEventContent::new("".to_owned()),
558 event_id: owned_event_id!("$1"),
559 sender: owned_user_id!("@a:b.c"),
560 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
561 unsigned: StateUnsigned::new(),
562 state_key: EmptyStateKey,
563 }),
564 ));
565
566 assert_matches!(
567 is_suitable_for_latest_event(&event, None),
568 PossibleLatestEvent::NoUnsupportedEventType
569 );
570 }
571
572 #[cfg(feature = "e2e-encryption")]
573 #[test]
574 fn test_replacement_events_are_unsuitable() {
575 let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
576 event_content.relates_to = Some(Relation::Replacement(Replacement::new(
577 owned_event_id!("$1"),
578 RoomMessageEventContent::text_plain("Hello, world!").into(),
579 )));
580
581 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
582 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
583 content: event_content,
584 event_id: owned_event_id!("$2"),
585 sender: owned_user_id!("@a:b.c"),
586 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
587 unsigned: MessageLikeUnsigned::new(),
588 }),
589 ));
590
591 assert_matches!(
592 is_suitable_for_latest_event(&event, None),
593 PossibleLatestEvent::NoUnsupportedMessageLikeType
594 );
595 }
596
597 #[cfg(feature = "e2e-encryption")]
598 #[test]
599 fn test_verification_requests_are_unsuitable() {
600 use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
601
602 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
603 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
604 content: RoomMessageEventContent::new(MessageType::VerificationRequest(
605 KeyVerificationRequestEventContent::new(
606 "body".to_owned(),
607 vec![],
608 device_id!("device_id").to_owned(),
609 user_id!("@user_id:example.com").to_owned(),
610 ),
611 )),
612 event_id: owned_event_id!("$1"),
613 sender: owned_user_id!("@a:b.c"),
614 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
615 unsigned: MessageLikeUnsigned::new(),
616 }),
617 ));
618
619 assert_let!(
620 PossibleLatestEvent::NoUnsupportedMessageLikeType =
621 is_suitable_for_latest_event(&event, None)
622 );
623 }
624
625 #[test]
626 fn test_deserialize_latest_event() {
627 #[derive(Debug, serde::Serialize, serde::Deserialize)]
628 struct TestStruct {
629 latest_event: LatestEvent,
630 }
631
632 let event = TimelineEvent::new(
633 Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
634 );
635
636 let initial = TestStruct {
637 latest_event: LatestEvent {
638 event: event.clone(),
639 sender_profile: None,
640 sender_name_is_ambiguous: None,
641 },
642 };
643
644 let serialized = serde_json::to_value(&initial).unwrap();
646 assert_eq!(
647 serialized,
648 json!({
649 "latest_event": {
650 "event": {
651 "kind": {
652 "PlainText": {
653 "event": {
654 "event_id": "$1"
655 }
656 }
657 }
658 },
659 }
660 })
661 );
662
663 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
665 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
666 assert!(deserialized.latest_event.sender_profile.is_none());
667 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
668
669 let serialized = json!({
671 "latest_event": {
672 "event": {
673 "encryption_info": null,
674 "event": {
675 "event_id": "$1"
676 }
677 },
678 }
679 });
680
681 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
682 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
683 assert!(deserialized.latest_event.sender_profile.is_none());
684 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
685
686 let serialized = json!({
688 "latest_event": event
689 });
690
691 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
692 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
693 assert!(deserialized.latest_event.sender_profile.is_none());
694 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
695 }
696}