1use matrix_sdk_common::deserialized_responses::TimelineEvent;
5use ruma::{MxcUri, OwnedEventId};
6#[cfg(feature = "e2e-encryption")]
7use ruma::{
8 UserId,
9 events::{
10 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
11 call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
12 poll::unstable_start::SyncUnstablePollStartEvent,
13 relation::RelationType,
14 room::{
15 member::{MembershipState, SyncRoomMemberEvent},
16 message::{MessageType, SyncRoomMessageEvent},
17 power_levels::RoomPowerLevels,
18 },
19 sticker::SyncStickerEvent,
20 },
21};
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 && matches!(member.membership(), MembershipState::Knock)
130 {
131 let can_accept_or_decline_knocks = match power_levels_info {
132 Some((own_user_id, room_power_levels)) => {
133 room_power_levels.user_can_invite(own_user_id)
134 || room_power_levels.user_can_kick(own_user_id)
135 }
136 _ => false,
137 };
138
139 if can_accept_or_decline_knocks {
142 return PossibleLatestEvent::YesKnockedStateEvent(member);
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 MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
315 events::{
316 AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
317 Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
318 RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
319 call::{
320 SessionDescription,
321 invite::{CallInviteEventContent, SyncCallInviteEvent},
322 notify::{
323 ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
324 },
325 },
326 poll::{
327 unstable_response::{
328 SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
329 },
330 unstable_start::{
331 NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
332 UnstablePollAnswer, UnstablePollStartContentBlock,
333 },
334 },
335 relation::Replacement,
336 room::{
337 ImageInfo, MediaSource,
338 encrypted::{
339 EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
340 SyncRoomEncryptedEvent,
341 },
342 message::{
343 ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
344 Relation, RoomMessageEventContent, SyncRoomMessageEvent,
345 },
346 topic::{RoomTopicEventContent, SyncRoomTopicEvent},
347 },
348 sticker::{StickerEventContent, SyncStickerEvent},
349 },
350 owned_event_id, owned_mxc_uri, owned_user_id,
351 };
352 use serde_json::json;
353
354 use super::LatestEvent;
355 #[cfg(feature = "e2e-encryption")]
356 use super::{PossibleLatestEvent, is_suitable_for_latest_event};
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 = 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::from_plaintext(
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 "thread_summary": "None",
659 }
660 }
661 })
662 );
663
664 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
666 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
667 assert!(deserialized.latest_event.sender_profile.is_none());
668 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
669
670 let serialized = json!({
672 "latest_event": {
673 "event": {
674 "encryption_info": null,
675 "event": {
676 "event_id": "$1"
677 }
678 },
679 }
680 });
681
682 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
683 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
684 assert!(deserialized.latest_event.sender_profile.is_none());
685 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
686
687 let serialized = json!({
689 "latest_event": event
690 });
691
692 let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
693 assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
694 assert!(deserialized.latest_event.sender_profile.is_none());
695 assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
696 }
697}