matrix_sdk_base/
latest_event.rs

1//! Utilities for working with events to decide whether they are suitable for
2//! use as a [crate::Room::latest_event].
3
4use 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/// Represents a decision about whether an event could be stored as the latest
27/// event in a room. Variants starting with Yes indicate that this message could
28/// be stored, and provide the inner event information, and those starting with
29/// a No indicate that it could not, and give a reason.
30#[cfg(feature = "e2e-encryption")]
31#[derive(Debug)]
32pub enum PossibleLatestEvent<'a> {
33    /// This message is suitable - it is an m.room.message
34    YesRoomMessage(&'a SyncRoomMessageEvent),
35    /// This message is suitable - it is a sticker
36    YesSticker(&'a SyncStickerEvent),
37    /// This message is suitable - it is a poll
38    YesPoll(&'a SyncUnstablePollStartEvent),
39
40    /// This message is suitable - it is a call invite
41    YesCallInvite(&'a SyncCallInviteEvent),
42
43    /// This message is suitable - it's a call notification
44    YesCallNotify(&'a SyncCallNotifyEvent),
45
46    /// This state event is suitable - it's a knock membership change
47    /// that can be handled by the current user.
48    YesKnockedStateEvent(&'a SyncRoomMemberEvent),
49
50    // Later: YesState(),
51    // Later: YesReaction(),
52    /// Not suitable - it's a state event
53    NoUnsupportedEventType,
54    /// Not suitable - it's not a m.room.message or an edit/replacement
55    NoUnsupportedMessageLikeType,
56    /// Not suitable - it's encrypted
57    NoEncrypted,
58}
59
60/// Decide whether an event could be stored as the latest event in a room.
61/// Returns a LatestEvent representing our decision.
62#[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        // Suitable - we have an m.room.message that was not redacted or edited
69        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
70            if let Some(original_message) = message.as_original() {
71                // Don't show incoming verification requests
72                if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
73                    return PossibleLatestEvent::NoUnsupportedMessageLikeType;
74                }
75
76                // Check if this is a replacement for another message. If it is, ignore it
77                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        // Encrypted events are not suitable
113        AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
114            PossibleLatestEvent::NoEncrypted
115        }
116
117        // Later, if we support reactions:
118        // AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_))
119
120        // MessageLike, but not one of the types we want to show in message previews, so not
121        // suitable
122        AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
123
124        // We don't currently support most state events
125        AnySyncTimelineEvent::State(state) => {
126            // But we make an exception for knocked state events *if* the current user
127            // can either accept or decline them
128            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                    // The current user can act on the knock changes, so they should be
139                    // displayed
140                    if can_accept_or_decline_knocks {
141                        return PossibleLatestEvent::YesKnockedStateEvent(member);
142                    }
143                }
144            }
145            PossibleLatestEvent::NoUnsupportedEventType
146        }
147    }
148}
149
150/// Represent all information required to represent a latest event in an
151/// efficient way.
152///
153/// ## Implementation details
154///
155/// Serialization and deserialization should be a breeze, but we introduced a
156/// change in the format without realizing, and without a migration. Ideally,
157/// this would be handled with a `serde(untagged)` enum that would be used to
158/// deserialize in either the older format, or to the new format. Unfortunately,
159/// untagged enums don't play nicely with `serde_json::value::RawValue`,
160/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that
161/// first deserializes the thing as a raw JSON value, and then deserializes the
162/// JSON string as one variant or the other.
163///
164/// Because of that, `LatestEvent` should only be (de)serialized using
165/// serde_json.
166///
167/// Whenever you introduce new fields to `LatestEvent` make sure to add them to
168/// `SerializedLatestEvent` too.
169#[derive(Clone, Debug, Serialize)]
170pub struct LatestEvent {
171    /// The actual event.
172    event: TimelineEvent,
173
174    /// The member profile of the event' sender.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    sender_profile: Option<MinimalRoomMemberEvent>,
177
178    /// The name of the event' sender is ambiguous.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    sender_name_is_ambiguous: Option<bool>,
181}
182
183#[derive(Deserialize)]
184struct SerializedLatestEvent {
185    /// The actual event.
186    event: TimelineEvent,
187
188    /// The member profile of the event' sender.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    sender_profile: Option<MinimalRoomMemberEvent>,
191
192    /// The name of the event' sender is ambiguous.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    sender_name_is_ambiguous: Option<bool>,
195}
196
197// Note: this deserialize implementation for LatestEvent will *only* work with
198// serde_json.
199impl<'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    /// Create a new [`LatestEvent`] without the sender's profile.
238    pub fn new(event: TimelineEvent) -> Self {
239        Self { event, sender_profile: None, sender_name_is_ambiguous: None }
240    }
241
242    /// Create a new [`LatestEvent`] with maybe the sender's profile.
243    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    /// Transform [`Self`] into an event.
252    pub fn into_event(self) -> TimelineEvent {
253        self.event
254    }
255
256    /// Get a reference to the event.
257    pub fn event(&self) -> &TimelineEvent {
258        &self.event
259    }
260
261    /// Get a mutable reference to the event.
262    pub fn event_mut(&mut self) -> &mut TimelineEvent {
263        &mut self.event
264    }
265
266    /// Get the event ID.
267    pub fn event_id(&self) -> Option<OwnedEventId> {
268        self.event.event_id()
269    }
270
271    /// Check whether [`Self`] has a sender profile.
272    pub fn has_sender_profile(&self) -> bool {
273        self.sender_profile.is_some()
274    }
275
276    /// Return the sender's display name if it was known at the time [`Self`]
277    /// was built.
278    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    /// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
285    /// it isn't, `None` if ambiguity detection wasn't possible at the time
286    /// [`Self`] was built.
287    pub fn sender_name_ambiguous(&self) -> Option<bool> {
288        self.sender_name_is_ambiguous
289    }
290
291    /// Return the sender's avatar URL if it was known at the time [`Self`] was
292    /// built.
293    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        // Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
503        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        // When serialized, LatestEvent always uses the new format.
645        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        // And it can be properly deserialized from the new format.
664        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        // The previous format can also be deserialized.
670        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        // The even older format can also be deserialized.
687        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}