1use ruma::{
18 events::{
19 poll::unstable_start::{
20 ReplacementUnstablePollStartEventContent, UnstablePollStartContentBlock,
21 UnstablePollStartEventContent,
22 },
23 room::message::{
24 FormattedBody, MessageType, ReplacementMetadata, RoomMessageEventContent,
25 RoomMessageEventContentWithoutRelation,
26 },
27 AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, Mentions,
28 SyncMessageLikeEvent,
29 },
30 EventId, UserId,
31};
32use thiserror::Error;
33use tracing::instrument;
34
35use super::EventSource;
36use crate::Room;
37
38pub enum EditedContent {
40 RoomMessage(RoomMessageEventContentWithoutRelation),
42
43 MediaCaption {
45 caption: Option<String>,
49
50 formatted_caption: Option<FormattedBody>,
54
55 mentions: Option<Mentions>,
58 },
59
60 PollStart {
62 fallback_text: String,
64 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#[derive(Debug, Error)]
82pub enum EditError {
83 #[error("State events can't be edited")]
85 StateEvent,
86
87 #[error("You're not the author of the event you'd like to edit.")]
90 NotAuthor,
91
92 #[error("Couldn't fetch the remote event: {0}")]
94 Fetch(Box<crate::Error>),
95
96 #[error(transparent)]
98 Deserialize(#[from] serde_json::Error),
99
100 #[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 target: String,
108 new_content: &'static str,
110 },
111}
112
113impl Room {
114 #[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 let AnySyncTimelineEvent::MessageLike(message_like_event) = event else {
140 return Err(EditError::StateEvent);
141 };
142
143 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 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 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
220macro_rules! set_caption {
226 ($event:expr, $caption:expr) => {
227 let filename = $event.filename().to_owned();
228 if let Some(caption) = $caption {
233 $event.filename = Some(filename);
234 $event.body = caption;
235 } else {
236 $event.filename = None;
237 $event.body = filename;
238 }
239 };
240}
241
242pub(crate) fn update_media_caption(
247 content: &mut RoomMessageEventContent,
248 caption: Option<String>,
249 formatted_caption: Option<FormattedBody>,
250 mentions: Option<Mentions>,
251) -> bool {
252 content.mentions = mentions;
253
254 match &mut content.msgtype {
255 MessageType::Audio(event) => {
256 set_caption!(event, caption);
257 event.formatted = formatted_caption;
258 true
259 }
260 MessageType::File(event) => {
261 set_caption!(event, caption);
262 event.formatted = formatted_caption;
263 true
264 }
265 #[cfg(feature = "unstable-msc4274")]
266 MessageType::Gallery(event) => {
267 event.body = caption.unwrap_or_default();
268 event.formatted = formatted_caption;
269 true
270 }
271 MessageType::Image(event) => {
272 set_caption!(event, caption);
273 event.formatted = formatted_caption;
274 true
275 }
276 MessageType::Video(event) => {
277 set_caption!(event, caption);
278 event.formatted = formatted_caption;
279 true
280 }
281 _ => false,
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use std::collections::BTreeMap;
288
289 use assert_matches2::{assert_let, assert_matches};
290 use matrix_sdk_base::deserialized_responses::TimelineEvent;
291 use matrix_sdk_test::{async_test, event_factory::EventFactory};
292 use ruma::{
293 event_id,
294 events::{
295 room::message::{MessageType, Relation, RoomMessageEventContentWithoutRelation},
296 AnyMessageLikeEventContent, AnySyncTimelineEvent, Mentions,
297 },
298 owned_mxc_uri, owned_user_id, user_id, EventId, OwnedEventId,
299 };
300
301 use super::{make_edit_event, EditError, EventSource};
302 use crate::{room::edit::EditedContent, Error};
303
304 #[derive(Default)]
305 struct TestEventCache {
306 events: BTreeMap<OwnedEventId, TimelineEvent>,
307 }
308
309 impl EventSource for TestEventCache {
310 async fn get_event(&self, event_id: &EventId) -> Result<TimelineEvent, Error> {
311 Ok(self.events.get(event_id).unwrap().clone())
312 }
313 }
314
315 #[async_test]
316 async fn test_edit_state_event() {
317 let event_id = event_id!("$1");
318 let own_user_id = user_id!("@me:saucisse.bzh");
319
320 let mut cache = TestEventCache::default();
321 let f = EventFactory::new();
322 cache.events.insert(
323 event_id.to_owned(),
324 f.room_name("The room name").event_id(event_id).sender(own_user_id).into(),
325 );
326
327 let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
328
329 assert_matches!(
330 make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content),)
331 .await,
332 Err(EditError::StateEvent)
333 );
334 }
335
336 #[async_test]
337 async fn test_edit_event_other_user() {
338 let event_id = event_id!("$1");
339 let f = EventFactory::new();
340
341 let mut cache = TestEventCache::default();
342
343 cache.events.insert(
344 event_id.to_owned(),
345 f.text_msg("hi").event_id(event_id).sender(user_id!("@other:saucisse.bzh")).into(),
346 );
347
348 let own_user_id = user_id!("@me:saucisse.bzh");
349 let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
350
351 assert_matches!(
352 make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content),)
353 .await,
354 Err(EditError::NotAuthor)
355 );
356 }
357
358 #[async_test]
359 async fn test_make_edit_event_success() {
360 let event_id = event_id!("$1");
361 let own_user_id = user_id!("@me:saucisse.bzh");
362
363 let mut cache = TestEventCache::default();
364 let f = EventFactory::new();
365 cache.events.insert(
366 event_id.to_owned(),
367 f.text_msg("hi").event_id(event_id).sender(own_user_id).into(),
368 );
369
370 let new_content = RoomMessageEventContentWithoutRelation::text_plain("the edit");
371
372 let edit_event =
373 make_edit_event(cache, own_user_id, event_id, EditedContent::RoomMessage(new_content))
374 .await
375 .unwrap();
376
377 assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &edit_event);
378 assert_eq!(msg.body(), "* the edit");
380 assert_let!(Some(Relation::Replacement(repl)) = &msg.relates_to);
381
382 assert_eq!(repl.event_id, event_id);
383 assert_eq!(repl.new_content.msgtype.body(), "the edit");
384 }
385
386 #[async_test]
387 async fn test_make_edit_caption_for_non_media_room_message() {
388 let event_id = event_id!("$1");
389 let own_user_id = user_id!("@me:saucisse.bzh");
390
391 let mut cache = TestEventCache::default();
392 let f = EventFactory::new();
393 cache.events.insert(
394 event_id.to_owned(),
395 f.text_msg("hello world").event_id(event_id).sender(own_user_id).into(),
396 );
397
398 let err = make_edit_event(
399 cache,
400 own_user_id,
401 event_id,
402 EditedContent::MediaCaption {
403 caption: Some("yo".to_owned()),
404 formatted_caption: None,
405 mentions: None,
406 },
407 )
408 .await
409 .unwrap_err();
410
411 assert_let!(EditError::IncompatibleEditType { target, new_content } = err);
412 assert_eq!(target, "m.text");
413 assert_eq!(new_content, "caption for a media room message");
414 }
415
416 #[async_test]
417 async fn test_add_caption_for_media() {
418 let event_id = event_id!("$1");
419 let own_user_id = user_id!("@me:saucisse.bzh");
420
421 let filename = "rickroll.gif";
422
423 let mut cache = TestEventCache::default();
424 let f = EventFactory::new();
425 cache.events.insert(
426 event_id.to_owned(),
427 f.image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
428 .event_id(event_id)
429 .sender(own_user_id)
430 .into(),
431 );
432
433 let edit_event = make_edit_event(
434 cache,
435 own_user_id,
436 event_id,
437 EditedContent::MediaCaption {
438 caption: Some("Best joke ever".to_owned()),
439 formatted_caption: None,
440 mentions: None,
441 },
442 )
443 .await
444 .unwrap();
445
446 assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
447 assert_let!(MessageType::Image(image) = msg.msgtype);
448
449 assert_eq!(image.filename(), filename);
450 assert_eq!(image.caption(), Some("* Best joke ever")); assert!(image.formatted_caption().is_none());
452
453 assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
454 assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
455 assert_eq!(new_image.filename(), filename);
456 assert_eq!(new_image.caption(), Some("Best joke ever"));
457 assert!(new_image.formatted_caption().is_none());
458 }
459
460 #[async_test]
461 async fn test_remove_caption_for_media() {
462 let event_id = event_id!("$1");
463 let own_user_id = user_id!("@me:saucisse.bzh");
464
465 let filename = "rickroll.gif";
466
467 let mut cache = TestEventCache::default();
468 let f = EventFactory::new();
469
470 let event = f
471 .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
472 .caption(Some("caption".to_owned()), None)
473 .event_id(event_id)
474 .sender(own_user_id)
475 .into_event();
476
477 {
478 let event = event.raw().deserialize().unwrap();
480 assert_let!(AnySyncTimelineEvent::MessageLike(event) = event);
481 assert_let!(
482 AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap()
483 );
484 assert_let!(MessageType::Image(image) = msg.msgtype);
485 assert_eq!(image.filename(), filename);
486 assert_eq!(image.caption(), Some("caption"));
487 assert!(image.formatted_caption().is_none());
488 }
489
490 cache.events.insert(event_id.to_owned(), event);
491
492 let edit_event = make_edit_event(
493 cache,
494 own_user_id,
495 event_id,
496 EditedContent::MediaCaption { caption: None, formatted_caption: None, mentions: None },
498 )
499 .await
500 .unwrap();
501
502 assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
503 assert_let!(MessageType::Image(image) = msg.msgtype);
504
505 assert_eq!(image.filename(), "* rickroll.gif"); assert!(image.caption().is_none());
507 assert!(image.formatted_caption().is_none());
508
509 assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
510 assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
511 assert_eq!(new_image.filename(), "rickroll.gif");
512 assert!(new_image.caption().is_none());
513 assert!(new_image.formatted_caption().is_none());
514 }
515
516 #[async_test]
517 async fn test_add_media_caption_mention() {
518 let event_id = event_id!("$1");
519 let own_user_id = user_id!("@me:saucisse.bzh");
520
521 let filename = "rickroll.gif";
522
523 let mut cache = TestEventCache::default();
524 let f = EventFactory::new();
525
526 let event = f
528 .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll"))
529 .event_id(event_id)
530 .sender(own_user_id)
531 .into_event();
532
533 {
534 let event = event.raw().deserialize().unwrap();
536 assert_let!(AnySyncTimelineEvent::MessageLike(event) = event);
537 assert_let!(
538 AnyMessageLikeEventContent::RoomMessage(msg) = event.original_content().unwrap()
539 );
540 assert_matches!(msg.mentions, None);
541 }
542
543 cache.events.insert(event_id.to_owned(), event);
544
545 let mentioned_user_id = owned_user_id!("@crepe:saucisse.bzh");
547 let edit_event = {
548 let mentions = Mentions::with_user_ids([mentioned_user_id.clone()]);
549 make_edit_event(
550 cache,
551 own_user_id,
552 event_id,
553 EditedContent::MediaCaption {
554 caption: None,
555 formatted_caption: None,
556 mentions: Some(mentions),
557 },
558 )
559 .await
560 .unwrap()
561 };
562
563 assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = edit_event);
564 assert_let!(MessageType::Image(image) = msg.msgtype);
565
566 assert!(image.caption().is_none());
567 assert!(image.formatted_caption().is_none());
568
569 assert_let!(Some(mentions) = msg.mentions);
571 assert!(!mentions.room);
572 assert_eq!(
573 mentions.user_ids.into_iter().collect::<Vec<_>>(),
574 vec![mentioned_user_id.clone()]
575 );
576
577 assert_let!(Some(Relation::Replacement(repl)) = msg.relates_to);
578 assert_let!(MessageType::Image(new_image) = repl.new_content.msgtype);
579 assert!(new_image.caption().is_none());
580 assert!(new_image.formatted_caption().is_none());
581
582 assert_let!(Some(mentions) = repl.new_content.mentions);
584 assert!(!mentions.room);
585 assert_eq!(mentions.user_ids.into_iter().collect::<Vec<_>>(), vec![mentioned_user_id]);
586 }
587
588 #[async_test]
589 async fn test_make_edit_event_success_with_response() {
590 let event_id = event_id!("$1");
591 let resp_event_id = event_id!("$resp");
592 let own_user_id = user_id!("@me:saucisse.bzh");
593
594 let mut cache = TestEventCache::default();
595 let f = EventFactory::new();
596
597 cache.events.insert(
598 event_id.to_owned(),
599 f.text_msg("hi").event_id(event_id).sender(user_id!("@steb:saucisse.bzh")).into(),
600 );
601
602 cache.events.insert(
603 resp_event_id.to_owned(),
604 f.text_msg("you're the hi")
605 .event_id(resp_event_id)
606 .sender(own_user_id)
607 .reply_to(event_id)
608 .into(),
609 );
610
611 let new_content = RoomMessageEventContentWithoutRelation::text_plain("uh i mean hi too");
612
613 let edit_event = make_edit_event(
614 cache,
615 own_user_id,
616 resp_event_id,
617 EditedContent::RoomMessage(new_content),
618 )
619 .await
620 .unwrap();
621
622 assert_let!(AnyMessageLikeEventContent::RoomMessage(msg) = &edit_event);
623 assert_eq!(msg.body(), "* uh i mean hi too");
625 assert_let!(Some(Relation::Replacement(repl)) = &msg.relates_to);
626
627 assert_eq!(repl.event_id, resp_event_id);
628 assert_eq!(repl.new_content.msgtype.body(), "uh i mean hi too");
629 }
630}