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