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(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    /// Create a new [`LatestEvent`] without the sender's profile.
239    pub fn new(event: TimelineEvent) -> Self {
240        Self { event, sender_profile: None, sender_name_is_ambiguous: None }
241    }
242
243    /// Create a new [`LatestEvent`] with maybe the sender's profile.
244    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    /// Transform [`Self`] into an event.
253    pub fn into_event(self) -> TimelineEvent {
254        self.event
255    }
256
257    /// Get a reference to the event.
258    pub fn event(&self) -> &TimelineEvent {
259        &self.event
260    }
261
262    /// Get a mutable reference to the event.
263    pub fn event_mut(&mut self) -> &mut TimelineEvent {
264        &mut self.event
265    }
266
267    /// Get the event ID.
268    pub fn event_id(&self) -> Option<OwnedEventId> {
269        self.event.event_id()
270    }
271
272    /// Check whether [`Self`] has a sender profile.
273    pub fn has_sender_profile(&self) -> bool {
274        self.sender_profile.is_some()
275    }
276
277    /// Return the sender's display name if it was known at the time [`Self`]
278    /// was built.
279    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    /// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
286    /// it isn't, `None` if ambiguity detection wasn't possible at the time
287    /// [`Self`] was built.
288    pub fn sender_name_ambiguous(&self) -> Option<bool> {
289        self.sender_name_is_ambiguous
290    }
291
292    /// Return the sender's avatar URL if it was known at the time [`Self`] was
293    /// built.
294    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        // Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
504        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        // When serialized, LatestEvent always uses the new format.
646        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        // And it can be properly deserialized from the new format.
666        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        // The previous format can also be deserialized.
672        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        // The even older format can also be deserialized.
689        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}