Skip to main content

matrix_sdk/room/
edit.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Facilities to edit existing events.
16
17use ruma::{
18    EventId, UserId,
19    events::{
20        AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, Mentions,
21        SyncMessageLikeEvent,
22        poll::unstable_start::{
23            ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock,
24            UnstablePollStartEventContent,
25        },
26        room::message::{
27            FormattedBody, MessageType, Relation, ReplacementMetadata, RoomMessageEventContent,
28            RoomMessageEventContentWithoutRelation,
29        },
30    },
31};
32use thiserror::Error;
33use tracing::instrument;
34
35use super::EventSource;
36use crate::Room;
37
38/// The new content that will replace the previous event's content.
39pub enum EditedContent {
40    /// The content is a `m.room.message`.
41    RoomMessage(RoomMessageEventContentWithoutRelation),
42
43    /// Tweak a caption for a `m.room.message` that's a media.
44    MediaCaption {
45        /// New caption for the media.
46        ///
47        /// Set to `None` to remove an existing caption.
48        caption: Option<String>,
49
50        /// New formatted caption for the media.
51        ///
52        /// Set to `None` to remove an existing formatted caption.
53        formatted_caption: Option<FormattedBody>,
54
55        /// New set of intentional mentions to be included in the edited
56        /// caption.
57        mentions: Option<Mentions>,
58    },
59
60    /// The content is a new poll start.
61    PollStart {
62        /// New fallback text for the poll.
63        fallback_text: String,
64        /// New start block for the poll.
65        new_content: UnstablePollStartContentBlock,
66    },
67}
68
69#[cfg(not(tarpaulin_include))]
70impl std::fmt::Debug for EditedContent {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::RoomMessage(_) => f.debug_tuple("RoomMessage").finish(),
74            Self::MediaCaption { .. } => f.debug_tuple("MediaCaption").finish(),
75            Self::PollStart { .. } => f.debug_tuple("PollStart").finish(),
76        }
77    }
78}
79
80/// An error occurring while editing an event.
81#[derive(Debug, Error)]
82pub enum EditError {
83    /// We tried to edit a state event, which is not allowed, per spec.
84    #[error("State events can't be edited")]
85    StateEvent,
86
87    /// We tried to edit an event which sender isn't the current user, which is
88    /// forbidden, per spec.
89    #[error("You're not the author of the event you'd like to edit.")]
90    NotAuthor,
91
92    /// We couldn't fetch the remote event with /room/event.
93    #[error("Couldn't fetch the remote event: {0}")]
94    Fetch(Box<crate::Error>),
95
96    /// We couldn't properly deserialize the target event.
97    #[error(transparent)]
98    Deserialize(#[from] serde_json::Error),
99
100    /// We tried to edit an event of type A with content of type B.
101    #[error(
102        "The original event type ({target}) isn't the same as \
103         the parameter's new content type ({new_content})"
104    )]
105    IncompatibleEditType {
106        /// The type of the target event.
107        target: String,
108        /// The type of the new content.
109        new_content: &'static str,
110    },
111}
112
113impl Room {
114    /// Create a new edit event for the target event id with the new content.
115    ///
116    /// The event can then be sent with [`Room::send`] or a
117    /// [`crate::send_queue::RoomSendQueue`].
118    #[instrument(skip(self, new_content), fields(room = %self.room_id()))]
119    pub async fn make_edit_event(
120        &self,
121        event_id: &EventId,
122        new_content: EditedContent,
123    ) -> Result<AnyMessageLikeEventContent, EditError> {
124        make_edit_event(self, self.own_user_id(), event_id, new_content).await
125    }
126}
127
128async fn make_edit_event<S: EventSource>(
129    source: S,
130    own_user_id: &UserId,
131    event_id: &EventId,
132    new_content: EditedContent,
133) -> Result<AnyMessageLikeEventContent, EditError> {
134    let target = source.get_event(event_id).await.map_err(|err| EditError::Fetch(Box::new(err)))?;
135
136    let event = target.raw().deserialize().map_err(EditError::Deserialize)?;
137
138    // The event must be message-like.
139    let AnySyncTimelineEvent::MessageLike(message_like_event) = event else {
140        return Err(EditError::StateEvent);
141    };
142
143    // The event must have been sent by the current user.
144    if message_like_event.sender() != own_user_id {
145        return Err(EditError::NotAuthor);
146    }
147
148    match new_content {
149        EditedContent::RoomMessage(new_content) => {
150            // Handle edits of m.room.message.
151            let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) =
152                message_like_event
153            else {
154                return Err(EditError::IncompatibleEditType {
155                    target: message_like_event.event_type().to_string(),
156                    new_content: "room message",
157                });
158            };
159
160            let mentions = original.content.mentions;
161
162            let replacement = new_content
163                .make_replacement(ReplacementMetadata::new(event_id.to_owned(), mentions));
164
165            Ok(replacement.into())
166        }
167
168        EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
169            // Handle edits of m.room.message.
170            let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) =
171                message_like_event
172            else {
173                return Err(EditError::IncompatibleEditType {
174                    target: message_like_event.event_type().to_string(),
175                    new_content: "caption for a media room message",
176                });
177            };
178
179            let original_mentions = original.content.mentions.clone();
180
181            let mut prev_content = original.content;
182
183            if !update_media_caption(&mut prev_content, caption, formatted_caption, mentions) {
184                return Err(EditError::IncompatibleEditType {
185                    target: prev_content.msgtype.msgtype().to_owned(),
186                    new_content: "caption for a media room message",
187                });
188            }
189
190            let replacement = prev_content
191                .make_replacement(ReplacementMetadata::new(event_id.to_owned(), original_mentions));
192
193            Ok(replacement.into())
194        }
195
196        EditedContent::PollStart { fallback_text, new_content } => {
197            if !matches!(
198                message_like_event,
199                AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(_))
200            ) {
201                return Err(EditError::IncompatibleEditType {
202                    target: message_like_event.event_type().to_string(),
203                    new_content: "poll start",
204                });
205            }
206
207            let replacement = UnstablePollStartEventContent::Replacement(
208                ReplacementUnstablePollStartEventContent::plain_text(
209                    fallback_text,
210                    new_content,
211                    event_id.to_owned(),
212                ),
213            );
214
215            Ok(replacement.into())
216        }
217    }
218}
219
220/// Checks that `event_id` can be edited into a media message, and returns the
221/// original event's intentional mentions, to be carried into the
222/// replacement's metadata.
223///
224/// The target must be an `m.room.message` sent by the current user, holding
225/// media or not.
226pub(crate) async fn validate_attachment_edit<S: EventSource>(
227    source: S,
228    own_user_id: &UserId,
229    event_id: &EventId,
230) -> Result<Option<Mentions>, EditError> {
231    let target = source.get_event(event_id).await.map_err(|err| EditError::Fetch(Box::new(err)))?;
232
233    let event = target.raw().deserialize().map_err(EditError::Deserialize)?;
234
235    // The event must be message-like.
236    let AnySyncTimelineEvent::MessageLike(message_like_event) = event else {
237        return Err(EditError::StateEvent);
238    };
239
240    // The event must have been sent by the current user.
241    if message_like_event.sender() != own_user_id {
242        return Err(EditError::NotAuthor);
243    }
244
245    let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original)) =
246        message_like_event
247    else {
248        return Err(EditError::IncompatibleEditType {
249            target: message_like_event.event_type().to_string(),
250            new_content: "room message",
251        });
252    };
253
254    Ok(original.content.mentions)
255}
256
257/// Sets the caption of a media event content.
258///
259/// Why a macro over a plain function: the event content types all differ from
260/// each other, and it would require adding a trait and implementing it for all
261/// event types instead of having this simple macro.
262macro_rules! set_caption {
263    ($event:expr, $caption:expr) => {
264        let filename = $event.filename().to_owned();
265        // As a reminder:
266        //
267        // - body and no filename set means the body is the filename
268        // - body and filename set means the body is the caption, and filename is
269        //   the filename.
270        if let Some(caption) = $caption {
271            $event.filename = Some(filename);
272            $event.body = caption;
273        } else {
274            $event.filename = None;
275            $event.body = filename;
276        }
277    };
278}
279
280/// Sets the caption of a [`RoomMessageEventContent`].
281///
282/// Returns true if the event represented a media event (and thus the captions
283/// could be updated), false otherwise.
284pub(crate) fn update_media_caption(
285    content: &mut RoomMessageEventContent,
286    caption: Option<String>,
287    formatted_caption: Option<FormattedBody>,
288    mentions: Option<Mentions>,
289) -> bool {
290    // A media edit (see `RoomSendQueue::edit_with_attachment`): update its
291    // canonical content, and rebuild the edit around it like any other, so the
292    // fallback and the mentions it notifies follow the same rules.
293    if let Some(Relation::Replacement(replacement)) = &content.relates_to {
294        let edited_event_id = replacement.event_id.clone();
295        let original_mentions = mentions_of_edited_event(
296            replacement.new_content.mentions.as_ref(),
297            content.mentions.as_ref(),
298        );
299
300        let mut new_content = replacement.new_content.clone();
301        if !update_media_msgtype_caption(&mut new_content.msgtype, caption, formatted_caption) {
302            return false;
303        }
304        new_content.mentions = mentions;
305
306        *content = new_content
307            .make_replacement(ReplacementMetadata::new(edited_event_id, original_mentions));
308        return true;
309    }
310
311    content.mentions = mentions;
312    update_media_msgtype_caption(&mut content.msgtype, caption, formatted_caption)
313}
314
315/// Recovers the mentions of the event an edit replaces: the edit's canonical
316/// content mentions everybody, and its top-level mentions only the users the
317/// edited event didn't already mention (see `make_replacement`).
318fn mentions_of_edited_event(
319    canonical: Option<&Mentions>,
320    notified: Option<&Mentions>,
321) -> Option<Mentions> {
322    let (canonical, notified) = (canonical?, notified?);
323
324    let mut mentions = Mentions::new();
325    mentions.user_ids =
326        canonical.user_ids.iter().filter(|u| !notified.user_ids.contains(*u)).cloned().collect();
327    mentions.room = canonical.room && !notified.room;
328    Some(mentions)
329}
330
331/// Sets the caption of a single [`MessageType`].
332///
333/// Returns false if it's not a media message type.
334fn update_media_msgtype_caption(
335    msgtype: &mut MessageType,
336    caption: Option<String>,
337    formatted_caption: Option<FormattedBody>,
338) -> bool {
339    match msgtype {
340        MessageType::Audio(event) => {
341            set_caption!(event, caption);
342            event.formatted = formatted_caption;
343            true
344        }
345        MessageType::File(event) => {
346            set_caption!(event, caption);
347            event.formatted = formatted_caption;
348            true
349        }
350        #[cfg(feature = "unstable-msc4274")]
351        MessageType::Gallery(event) => {
352            event.body = caption.unwrap_or_default();
353            event.formatted = formatted_caption;
354            true
355        }
356        MessageType::Image(event) => {
357            set_caption!(event, caption);
358            event.formatted = formatted_caption;
359            true
360        }
361        MessageType::Video(event) => {
362            set_caption!(event, caption);
363            event.formatted = formatted_caption;
364            true
365        }
366        _ => false,
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use std::{assert_matches, collections::BTreeMap};
373
374    use matrix_sdk_base::deserialized_responses::TimelineEvent;
375    use matrix_sdk_test::{async_test, event_factory::EventFactory};
376    use ruma::{
377        EventId, OwnedEventId, event_id,
378        events::{
379            AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
380            room::message::{
381                ImageMessageEventContent, MessageType, Relation, ReplacementMetadata,
382                RoomMessageEventContentWithoutRelation,
383            },
384        },
385        owned_mxc_uri, owned_user_id, user_id,
386    };
387    use strass::assert_let;
388
389    use super::{
390        EditError, EventSource, make_edit_event, update_media_caption, validate_attachment_edit,
391    };
392    use crate::{Error, room::edit::EditedContent};
393
394    #[derive(Default)]
395    struct TestEventCache {
396        events: BTreeMap<OwnedEventId, TimelineEvent>,
397    }
398
399    impl EventSource for TestEventCache {
400        async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
401            Ok(self.events.get(event_id).unwrap().clone())
402        }
403    }
404
405    #[async_test]
406    async fn test_edit_state_event() {
407        let event_id = event_id!("$1");
408        let own_user_id = user_id!("@me:saucisse.bzh");
409
410        let mut cache = TestEventCache::default();
411        let f = EventFactory::new();
412        cache.events.insert(
413            event_id.to_owned(),
414            f.room_name("The room name").event_id(event_id).sender(own_user_id).into(),
415        );
416
417        let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
418
419        assert_matches!(
420            make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content),)
421                .await,
422            Err(EditError::StateEvent)
423        );
424    }
425
426    #[async_test]
427    async fn test_edit_event_other_user() {
428        let event_id = event_id!("$1");
429        let f = EventFactory::new();
430
431        let mut cache = TestEventCache::default();
432
433        cache.events.insert(
434            event_id.to_owned(),
435            f.text_msg("hi").event_id(event_id).sender(user_id!("@other:saucisse.bzh")).into(),
436        );
437
438        let own_user_id = user_id!("@me:saucisse.bzh");
439        let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
440
441        assert_matches!(
442            make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content),)
443                .await,
444            Err(EditError::NotAuthor)
445        );
446    }
447
448    #[async_test]
449    async fn test_make_edit_event_success() {
450        let event_id = event_id!("$1");
451        let own_user_id = user_id!("@me:saucisse.bzh");
452
453        let mut cache = TestEventCache::default();
454        let f = EventFactory::new();
455        cache.events.insert(
456            event_id.to_owned(),
457            f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
458        );
459
460        let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
461
462        let edit_event =
463            make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content))
464                .await
465                .unwrap();
466
467        assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &edit_event);
468        // This is the fallback text, for clients not supporting edits.
469        assert_eq!(msg.body(), "* the edit");
470        assert_let!(Some(Relation::Replacement(repl)) = &msg.relates_to);
471
472        assert_eq!(repl.event_id, event_id);
473        assert_eq!(repl.new_content.msgtype.body(), "the edit");
474    }
475
476    #[async_test]
477    async fn test_make_edit_caption_for_non_media_room_message() {
478        let event_id = event_id!("$1");
479        let own_user_id = user_id!("@me:saucisse.bzh");
480
481        let mut cache = TestEventCache::default();
482        let f = EventFactory::new();
483        cache.events.insert(
484            event_id.to_owned(),
485            f.text_msg("hello world").event_id(event_id).sender(own_user_id).into(),
486        );
487
488        let err = make_edit_event(
489            cache,
490            own_user_id,
491            event_id,
492            EditedContent::MediaCaption {
493                caption: Some("yo".to_owned()),
494                formatted_caption: None,
495                mentions: None,
496            },
497        )
498        .await
499        .unwrap_err();
500
501        assert_let!(EditError::IncompatibleEditType { target, new_content } = err);
502        assert_eq!(target, "m.text");
503        assert_eq!(new_content, "caption for a media room message");
504    }
505
506    #[async_test]
507    async fn test_add_caption_for_media() {
508        let event_id = event_id!("$1");
509        let own_user_id = user_id!("@me:saucisse.bzh");
510
511        let filename = "rickroll.gif";
512
513        let mut cache = TestEventCache::default();
514        let f = EventFactory::new();
515        cache.events.insert(
516            event_id.to_owned(),
517            f.image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
518                .event_id(event_id)
519                .sender(own_user_id)
520                .into(),
521        );
522
523        let edit_event = make_edit_event(
524            cache,
525            own_user_id,
526            event_id,
527            EditedContent::MediaCaption {
528                caption: Some("Best joke ever".to_owned()),
529                formatted_caption: None,
530                mentions: None,
531            },
532        )
533        .await
534        .unwrap();
535
536        assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
537        assert_let!(MessageType::Image(image) = msg.msgtype);
538
539        assert_eq!(image.filename(), filename);
540        assert_eq!(image.caption(), Some("* Best joke ever")); // Fallback for a replacement 🤷
541        assert!(image.formatted_caption().is_none());
542
543        assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
544        assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
545        assert_eq!(new_image.filename(), filename);
546        assert_eq!(new_image.caption(), Some("Best joke ever"));
547        assert!(new_image.formatted_caption().is_none());
548    }
549
550    #[async_test]
551    async fn test_remove_caption_for_media() {
552        let event_id = event_id!("$1");
553        let own_user_id = user_id!("@me:saucisse.bzh");
554
555        let filename = "rickroll.gif";
556
557        let mut cache = TestEventCache::default();
558        let f = EventFactory::new();
559
560        let event = f
561            .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
562            .caption(Some("caption".to_owned()), None)
563            .event_id(event_id)
564            .sender(own_user_id)
565            .into_event();
566
567        {
568            // Sanity checks.
569            let event = event.raw().deserialize().unwrap();
570            assert_let!(AnySyncTimelineEvent::MessageLike(event) = event);
571            assert_let!(
572                AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap()
573            );
574            assert_let!(MessageType::Image(image) = msg.msgtype);
575            assert_eq!(image.filename(), filename);
576            assert_eq!(image.caption(), Some("caption"));
577            assert!(image.formatted_caption().is_none());
578        }
579
580        cache.events.insert(event_id.to_owned(), event);
581
582        let edit_event = make_edit_event(
583            cache,
584            own_user_id,
585            event_id,
586            // Remove the caption by setting it to None.
587            EditedContent::MediaCaption { caption: None, formatted_caption: None, mentions: None },
588        )
589        .await
590        .unwrap();
591
592        assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
593        assert_let!(MessageType::Image(image) = msg.msgtype);
594
595        assert_eq!(image.filename(), "* rickroll.gif"); // Fallback for a replacement 🤷
596        assert!(image.caption().is_none());
597        assert!(image.formatted_caption().is_none());
598
599        assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
600        assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
601        assert_eq!(new_image.filename(), "rickroll.gif");
602        assert!(new_image.caption().is_none());
603        assert!(new_image.formatted_caption().is_none());
604    }
605
606    #[async_test]
607    async fn test_add_media_caption_mention() {
608        let event_id = event_id!("$1");
609        let own_user_id = user_id!("@me:saucisse.bzh");
610
611        let filename = "rickroll.gif";
612
613        let mut cache = TestEventCache::default();
614        let f = EventFactory::new();
615
616        // Start with a media event that has no mentions.
617        let event = f
618            .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
619            .event_id(event_id)
620            .sender(own_user_id)
621            .into_event();
622
623        {
624            // Sanity checks.
625            let event = event.raw().deserialize().unwrap();
626            assert_let!(AnySyncTimelineEvent::MessageLike(event) = event);
627            assert_let!(
628                AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap()
629            );
630            assert_matches!(msg.mentions, None);
631        }
632
633        cache.events.insert(event_id.to_owned(), event);
634
635        // Add an intentional mention in the caption.
636        let mentioned_user_id = owned_user_id!("@crepe:saucisse.bzh");
637        let edit_event = {
638            let mentions = Mentions::with_user_ids([mentioned_user_id.clone()]);
639            make_edit_event(
640                cache,
641                own_user_id,
642                event_id,
643                EditedContent::MediaCaption {
644                    caption: None,
645                    formatted_caption: None,
646                    mentions: Some(mentions),
647                },
648            )
649            .await
650            .unwrap()
651        };
652
653        assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
654        assert_let!(MessageType::Image(image) = msg.msgtype);
655
656        assert!(image.caption().is_none());
657        assert!(image.formatted_caption().is_none());
658
659        // The raw event contains the mention.
660        assert_let!(Some(mentions) = msg.mentions);
661        assert!(!mentions.room);
662        assert_eq!(
663            mentions.user_ids.into_iter().collect::<Vec<_>>(),
664            vec![mentioned_user_id.clone()]
665        );
666
667        assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
668        assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
669        assert!(new_image.caption().is_none());
670        assert!(new_image.formatted_caption().is_none());
671
672        // The replacement contains the mention.
673        assert_let!(Some(mentions) = repl.new_content.mentions);
674        assert!(!mentions.room);
675        assert_eq!(mentions.user_ids.into_iter().collect::<Vec<_>>(), vec![mentioned_user_id]);
676    }
677
678    #[test]
679    fn test_update_media_caption_of_a_replacement() {
680        let crepe = owned_user_id!("@crepe:saucisse.bzh");
681        let galette = owned_user_id!("@galette:saucisse.bzh");
682
683        // A pending media edit of an event which mentioned crepe already.
684        let image = MessageType::Image(ImageMessageEventContent::plain(
685            "rickroll.gif".to_owned(),
686            owned_mxc_uri!("mxc://sdk.rs/rickroll"),
687        ));
688        let mut content = RoomMessageEventContentWithoutRelation::new(image)
689            .add_mentions(Mentions::with_user_ids([crepe.clone()]))
690            .make_replacement(ReplacementMetadata::new(
691                event_id!("$1").to_owned(),
692                Some(Mentions::with_user_ids([crepe.clone()])),
693            ));
694
695        assert!(update_media_caption(
696            &mut content,
697            Some("Best joke ever".to_owned()),
698            None,
699            Some(Mentions::with_user_ids([crepe, galette.clone()]))
700        ));
701
702        // The fallback reads like any other caption edit, and only the newly
703        // mentioned user is notified.
704        assert_let!(MessageType::Image(image) = &content.msgtype);
705        assert_eq!(image.filename(), "rickroll.gif");
706        assert_eq!(image.caption(), Some("* Best joke ever"));
707        assert_let!(Some(notified) = &content.mentions);
708        assert_eq!(notified.user_ids.iter().collect::<Vec<_>>(), [&galette]);
709
710        // The canonical content has the new caption, and mentions both users.
711        assert_let!(Some(Relation::Replacement(repl)) = &content.relates_to);
712        assert_eq!(repl.event_id, event_id!("$1"));
713        assert_let!(MessageType::Image(new_image) = &repl.new_content.msgtype);
714        assert_eq!(new_image.filename(), "rickroll.gif");
715        assert_eq!(new_image.caption(), Some("Best joke ever"));
716        assert_let!(Some(mentions) = &repl.new_content.mentions);
717        assert_eq!(mentions.user_ids.len(), 2);
718    }
719
720    #[async_test]
721    async fn test_make_edit_event_success_with_response() {
722        let event_id = event_id!("$1");
723        let resp_event_id = event_id!("$resp");
724        let own_user_id = user_id!("@me:saucisse.bzh");
725
726        let mut cache = TestEventCache::default();
727        let f = EventFactory::new();
728
729        cache.events.insert(
730            event_id.to_owned(),
731            f.text_msg("hi").event_id(event_id).sender(user_id!("@steb:saucisse.bzh")).into(),
732        );
733
734        cache.events.insert(
735            resp_event_id.to_owned(),
736            f.text_msg("you're the hi")
737                .event_id(resp_event_id)
738                .sender(own_user_id)
739                .reply_to(event_id)
740                .into(),
741        );
742
743        let new_content = RoomMessageEventContentWithoutRelation::text_plain("uh i mean hi too");
744
745        let edit_event = make_edit_event(
746            cache,
747            own_user_id,
748            resp_event_id,
749            EditedContent::RoomMessage(new_content),
750        )
751        .await
752        .unwrap();
753
754        assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &edit_event);
755        // This is the fallback text, for clients not supporting edits.
756        assert_eq!(msg.body(), "* uh i mean hi too");
757        assert_let!(Some(Relation::Replacement(repl)) = &msg.relates_to);
758
759        assert_eq!(repl.event_id, resp_event_id);
760        assert_eq!(repl.new_content.msgtype.body(), "uh i mean hi too");
761    }
762
763    fn image_event_cache(event_id: &EventId, sender: &ruma::UserId) -> TestEventCache {
764        let mut cache = TestEventCache::default();
765        let f = EventFactory::new();
766        cache.events.insert(
767            event_id.to_owned(),
768            f.image("rickroll.gif".to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
769                .event_id(event_id)
770                .sender(sender)
771                .into(),
772        );
773        cache
774    }
775
776    #[async_test]
777    async fn test_validate_attachment_edit() {
778        let event_id = event_id!("$1");
779        let own_user_id = user_id!("@me:saucisse.bzh");
780
781        let cache = image_event_cache(event_id, own_user_id);
782
783        assert_matches!(validate_attachment_edit(cache, own_user_id, event_id).await, Ok(None));
784    }
785
786    #[async_test]
787    async fn test_validate_attachment_edit_of_a_text_message() {
788        let event_id = event_id!("$1");
789        let own_user_id = user_id!("@me:saucisse.bzh");
790
791        let mut cache = TestEventCache::default();
792        let f = EventFactory::new();
793        cache.events.insert(
794            event_id.to_owned(),
795            f.text_msg("this is not a media").event_id(event_id).sender(own_user_id).into(),
796        );
797
798        assert_matches!(validate_attachment_edit(cache, own_user_id, event_id).await, Ok(None));
799    }
800
801    #[async_test]
802    async fn test_validate_attachment_edit_state_event() {
803        let event_id = event_id!("$1");
804        let own_user_id = user_id!("@me:saucisse.bzh");
805
806        let mut cache = TestEventCache::default();
807        let f = EventFactory::new();
808        cache.events.insert(
809            event_id.to_owned(),
810            f.room_name("The room name").event_id(event_id).sender(own_user_id).into(),
811        );
812
813        assert_matches!(
814            validate_attachment_edit(cache, own_user_id, event_id).await,
815            Err(EditError::StateEvent)
816        );
817    }
818
819    #[async_test]
820    async fn test_validate_attachment_edit_not_a_room_message() {
821        let event_id = event_id!("$1");
822        let own_user_id = user_id!("@me:saucisse.bzh");
823
824        let mut cache = TestEventCache::default();
825        let f = EventFactory::new();
826        cache.events.insert(
827            event_id.to_owned(),
828            f.poll_start("poll", "question", vec!["a", "b"])
829                .event_id(event_id)
830                .sender(own_user_id)
831                .into(),
832        );
833
834        assert_matches!(
835            validate_attachment_edit(cache, own_user_id, event_id).await,
836            Err(EditError::IncompatibleEditType { .. })
837        );
838    }
839
840    #[async_test]
841    async fn test_validate_attachment_edit_other_user() {
842        let event_id = event_id!("$1");
843        let own_user_id = user_id!("@me:saucisse.bzh");
844
845        let cache = image_event_cache(event_id, user_id!("@other:saucisse.bzh"));
846
847        assert_matches!(
848            validate_attachment_edit(cache, own_user_id, event_id).await,
849            Err(EditError::NotAuthor)
850        );
851    }
852}