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(format!(
231 "data did not match any variant of serialized LatestEvent (using serde_json). \
232 Observed errors: {variant_errors:?}"
233 )))
234 }
235}
236
237impl LatestEvent {
238 pub fn new(event: TimelineEvent) -> Self {
240 Self { event, sender_profile: None, sender_name_is_ambiguous: None }
241 }
242
243 pub fn new_with_sender_details(
245 event: TimelineEvent,
246 sender_profile: Option<MinimalRoomMemberEvent>,
247 sender_name_is_ambiguous: Option<bool>,
248 ) -> Self {
249 Self { event, sender_profile, sender_name_is_ambiguous }
250 }
251
252 pub fn into_event(self) -> TimelineEvent {
254 self.event
255 }
256
257 pub fn event(&self) -> &TimelineEvent {
259 &self.event
260 }
261
262 pub fn event_mut(&mut self) -> &mut TimelineEvent {
264 &mut self.event
265 }
266
267 pub fn event_id(&self) -> Option<OwnedEventId> {
269 self.event.event_id()
270 }
271
272 pub fn has_sender_profile(&self) -> bool {
274 self.sender_profile.is_some()
275 }
276
277 pub fn sender_display_name(&self) -> Option<&str> {
280 self.sender_profile.as_ref().and_then(|profile| {
281 profile.as_original().and_then(|event| event.content.displayname.as_deref())
282 })
283 }
284
285 pub fn sender_name_ambiguous(&self) -> Option<bool> {
289 self.sender_name_is_ambiguous
290 }
291
292 pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
295 self.sender_profile.as_ref().and_then(|profile| {
296 profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
297 })
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 #[cfg(feature = "e2e-encryption")]
304 use std::collections::BTreeMap;
305
306 #[cfg(feature = "e2e-encryption")]
307 use assert_matches::assert_matches;
308 #[cfg(feature = "e2e-encryption")]
309 use assert_matches2::assert_let;
310 use matrix_sdk_common::deserialized_responses::TimelineEvent;
311 use ruma::serde::Raw;
312 #[cfg(feature = "e2e-encryption")]
313 use ruma::{
314 events::{
315 call::{
316 invite::{CallInviteEventContent, SyncCallInviteEvent},
317 notify::{
318 ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
319 },
320 SessionDescription,
321 },
322 poll::{
323 unstable_response::{
324 SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
325 },
326 unstable_start::{
327 NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
328 UnstablePollAnswer, UnstablePollStartContentBlock,
329 },
330 },
331 relation::Replacement,
332 room::{
333 encrypted::{
334 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
335 SyncRoomEncryptedEvent,
336 },
337 message::{
338 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
339 Relation, RoomMessageEventContent, SyncRoomMessageEvent,
340 },
341 topic::{RoomTopicEventContent, SyncRoomTopicEvent},
342 ImageInfo, MediaSource,
343 },
344 sticker::{StickerEventContent, SyncStickerEvent},
345 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
346 Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
347 RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
348 UnsignedRoomRedactionEvent,
349 },
350 owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
351 VoipVersionId,
352 };
353 use serde_json::json;
354
355 use super::LatestEvent;
356 #[cfg(feature = "e2e-encryption")]
357 use super::{is_suitable_for_latest_event, PossibleLatestEvent};
358
359 #[cfg(feature = "e2e-encryption")]
360 #[test]
361 fn test_room_messages_are_suitable() {
362 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
363 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
364 content: RoomMessageEventContent::new(MessageType::Image(
365 ImageMessageEventContent::new(
366 "".to_owned(),
367 MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
368 ),
369 )),
370 event_id: owned_event_id!("$1"),
371 sender: owned_user_id!("@a:b.c"),
372 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
373 unsigned: MessageLikeUnsigned::new(),
374 }),
375 ));
376 assert_let!(
377 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
378 is_suitable_for_latest_event(&event, None)
379 );
380
381 assert_eq!(m.content.msgtype.msgtype(), "m.image");
382 }
383
384 #[cfg(feature = "e2e-encryption")]
385 #[test]
386 fn test_polls_are_suitable() {
387 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
388 SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
389 content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
390 "do you like rust?",
391 vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
392 ))
393 .into(),
394 event_id: owned_event_id!("$1"),
395 sender: owned_user_id!("@a:b.c"),
396 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
397 unsigned: MessageLikeUnsigned::new(),
398 }),
399 ));
400 assert_let!(
401 PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
402 is_suitable_for_latest_event(&event, None)
403 );
404
405 assert_eq!(m.content.poll_start().question.text, "do you like rust?");
406 }
407
408 #[cfg(feature = "e2e-encryption")]
409 #[test]
410 fn test_call_invites_are_suitable() {
411 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
412 SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
413 content: CallInviteEventContent::new(
414 "call_id".into(),
415 UInt::new(123).unwrap(),
416 SessionDescription::new("".into(), "".into()),
417 VoipVersionId::V1,
418 ),
419 event_id: owned_event_id!("$1"),
420 sender: owned_user_id!("@a:b.c"),
421 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
422 unsigned: MessageLikeUnsigned::new(),
423 }),
424 ));
425 assert_let!(
426 PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
427 is_suitable_for_latest_event(&event, None)
428 );
429 }
430
431 #[cfg(feature = "e2e-encryption")]
432 #[test]
433 fn test_call_notifications_are_suitable() {
434 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
435 SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
436 content: CallNotifyEventContent::new(
437 "call_id".into(),
438 ApplicationType::Call,
439 NotifyType::Ring,
440 Mentions::new(),
441 ),
442 event_id: owned_event_id!("$1"),
443 sender: owned_user_id!("@a:b.c"),
444 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
445 unsigned: MessageLikeUnsigned::new(),
446 }),
447 ));
448 assert_let!(
449 PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
450 is_suitable_for_latest_event(&event, None)
451 );
452 }
453
454 #[cfg(feature = "e2e-encryption")]
455 #[test]
456 fn test_stickers_are_suitable() {
457 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
458 SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
459 content: StickerEventContent::new(
460 "sticker!".to_owned(),
461 ImageInfo::new(),
462 owned_mxc_uri!("mxc://example.com/1"),
463 ),
464 event_id: owned_event_id!("$1"),
465 sender: owned_user_id!("@a:b.c"),
466 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
467 unsigned: MessageLikeUnsigned::new(),
468 }),
469 ));
470
471 assert_matches!(
472 is_suitable_for_latest_event(&event, None),
473 PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
474 );
475 }
476
477 #[cfg(feature = "e2e-encryption")]
478 #[test]
479 fn test_different_types_of_messagelike_are_unsuitable() {
480 let event =
481 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
482 SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
483 content: UnstablePollResponseEventContent::new(
484 vec![String::from("option1")],
485 owned_event_id!("$1"),
486 ),
487 event_id: owned_event_id!("$2"),
488 sender: owned_user_id!("@a:b.c"),
489 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
490 unsigned: MessageLikeUnsigned::new(),
491 }),
492 ));
493
494 assert_matches!(
495 is_suitable_for_latest_event(&event, None),
496 PossibleLatestEvent::NoUnsupportedMessageLikeType
497 );
498 }
499
500 #[cfg(feature = "e2e-encryption")]
501 #[test]
502 fn test_redacted_messages_are_suitable() {
503 let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
505 "content": {},
506 "event_id": "$redaction",
507 "sender": "@x:y.za",
508 "origin_server_ts": 223543,
509 "unsigned": { "reason": "foo" }
510 }))
511 .unwrap();
512
513 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
514 SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
515 content: RedactedRoomMessageEventContent::new(),
516 event_id: owned_event_id!("$1"),
517 sender: owned_user_id!("@a:b.c"),
518 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
519 unsigned: RedactedUnsigned::new(room_redaction_event),
520 }),
521 ));
522
523 assert_matches!(
524 is_suitable_for_latest_event(&event, None),
525 PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
526 );
527 }
528
529 #[cfg(feature = "e2e-encryption")]
530 #[test]
531 fn test_encrypted_messages_are_unsuitable() {
532 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
533 SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
534 content: RoomEncryptedEventContent::new(
535 EncryptedEventScheme::OlmV1Curve25519AesSha2(
536 OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
537 ),
538 None,
539 ),
540 event_id: owned_event_id!("$1"),
541 sender: owned_user_id!("@a:b.c"),
542 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
543 unsigned: MessageLikeUnsigned::new(),
544 }),
545 ));
546
547 assert_matches!(
548 is_suitable_for_latest_event(&event, None),
549 PossibleLatestEvent::NoEncrypted
550 );
551 }
552
553 #[cfg(feature = "e2e-encryption")]
554 #[test]
555 fn test_state_events_are_unsuitable() {
556 let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
557 SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
558 content: RoomTopicEventContent::new("".to_owned()),
559 event_id: owned_event_id!("$1"),
560 sender: owned_user_id!("@a:b.c"),
561 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
562 unsigned: StateUnsigned::new(),
563 state_key: EmptyStateKey,
564 }),
565 ));
566
567 assert_matches!(
568 is_suitable_for_latest_event(&event, None),
569 PossibleLatestEvent::NoUnsupportedEventType
570 );
571 }
572
573 #[cfg(feature = "e2e-encryption")]
574 #[test]
575 fn test_replacement_events_are_unsuitable() {
576 let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
577 event_content.relates_to = Some(Relation::Replacement(Replacement::new(
578 owned_event_id!("$1"),
579 RoomMessageEventContent::text_plain("Hello, world!").into(),
580 )));
581
582 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
583 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
584 content: event_content,
585 event_id: owned_event_id!("$2"),
586 sender: owned_user_id!("@a:b.c"),
587 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
588 unsigned: MessageLikeUnsigned::new(),
589 }),
590 ));
591
592 assert_matches!(
593 is_suitable_for_latest_event(&event, None),
594 PossibleLatestEvent::NoUnsupportedMessageLikeType
595 );
596 }
597
598 #[cfg(feature = "e2e-encryption")]
599 #[test]
600 fn test_verification_requests_are_unsuitable() {
601 use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
602
603 let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
604 SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
605 content: RoomMessageEventContent::new(MessageType::VerificationRequest(
606 KeyVerificationRequestEventContent::new(
607 "body".to_owned(),
608 vec![],
609 device_id!("device_id").to_owned(),
610 user_id!("@user_id:example.com").to_owned(),
611 ),
612 )),
613 event_id: owned_event_id!("$1"),
614 sender: owned_user_id!("@a:b.c"),
615 origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
616 unsigned: MessageLikeUnsigned::new(),
617 }),
618 ));
619
620 assert_let!(
621 PossibleLatestEvent::NoUnsupportedMessageLikeType =
622 is_suitable_for_latest_event(&event, None)
623 );
624 }
625
626 #[test]
627 fn test_deserialize_latest_event() {
628 #[derive(Debug, serde::Serialize, serde::Deserialize)]
629 struct TestStruct {
630 latest_event: LatestEvent,
631 }
632
633 let event = TimelineEvent::from_plaintext(
634 Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
635 );
636
637 let initial = TestStruct {
638 latest_event: LatestEvent {
639 event: event.clone(),
640 sender_profile: None,
641 sender_name_is_ambiguous: None,
642 },
643 };
644
645 let serialized = serde_json::to_value(&initial).unwrap();
647 assert_eq!(
648 serialized,
649 json!({
650 "latest_event": {
651 "event": {
652 "kind": {
653 "PlainText": {
654 "event": {
655 "event_id": "$1"
656 }
657 }
658 },
659 "thread_summary": "None",
660 }
661 }
662 })
663 );
664
665 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
667 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
668 assert!(deserialized.latest_event.sender_profile.is_none());
669 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
670
671 let serialized = json!({
673 "latest_event": {
674 "event": {
675 "encryption_info": null,
676 "event": {
677 "event_id": "$1"
678 }
679 },
680 }
681 });
682
683 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
684 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
685 assert!(deserialized.latest_event.sender_profile.is_none());
686 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
687
688 let serialized = json!({
690 "latest_event": event
691 });
692
693 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
694 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
695 assert!(deserialized.latest_event.sender_profile.is_none());
696 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
697 }
698}