1#[cfg(feature = "unstable-msc4274")]
18use std::{collections::HashMap, iter::zip};
19
20use matrix_sdk_base::{
21 RoomState,
22 media::{
23 MediaFormat, MediaRequestParameters, MediaThumbnailSettings,
24 store::IgnoreMediaRetentionPolicy,
25 },
26 store::{
27 ChildTransactionId, DependentQueuedRequestKind, FinishUploadThumbnailInfo,
28 QueuedRequestKind, SentMediaInfo, SentRequestKey, SerializableEventContent,
29 },
30};
31#[cfg(feature = "unstable-msc4274")]
32use matrix_sdk_base::{
33 media::UniqueKey,
34 store::{AccumulatedSentMediaInfo, FinishGalleryItemInfo},
35};
36use mime::Mime;
37#[cfg(feature = "unstable-msc4274")]
38use ruma::events::room::message::{GalleryItemType, GalleryMessageEventContent};
39use ruma::{
40 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, TransactionId,
41 events::{
42 AnyMessageLikeEventContent, Mentions,
43 room::{
44 MediaSource, ThumbnailInfo,
45 message::{
46 FormattedBody, MessageType, Relation, ReplacementMetadata, RoomMessageEventContent,
47 RoomMessageEventContentWithoutRelation,
48 },
49 },
50 },
51};
52use tracing::{Span, debug, error, instrument, trace, warn};
53
54use super::{QueueStorage, QueueThumbnailInfo, RoomSendQueue, RoomSendQueueError};
55use crate::{
56 Client, Media, Room,
57 attachment::{AttachmentConfig, Thumbnail},
58 room::edit::{update_media_caption, validate_attachment_edit},
59 send_queue::{
60 LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate,
61 SendHandle,
62 },
63};
64#[cfg(feature = "unstable-msc4274")]
65use crate::{
66 attachment::{GalleryConfig, GalleryItemInfo},
67 send_queue::GalleryItemQueueInfo,
68};
69
70fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) {
73 update_media_msgtype_after_upload(&mut echo.msgtype, &sent);
74
75 if let Some(Relation::Replacement(replacement)) = &mut echo.relates_to {
79 update_media_msgtype_after_upload(&mut replacement.new_content.msgtype, &sent);
80 }
81}
82
83fn update_media_msgtype_after_upload(msgtype: &mut MessageType, sent: &SentMediaInfo) {
86 match msgtype {
89 MessageType::Audio(event) => {
90 event.source = sent.file.clone();
91 }
92 MessageType::File(event) => {
93 event.source = sent.file.clone();
94 if let Some(info) = event.info.as_mut() {
95 info.thumbnail_source = sent.thumbnail.clone();
96 }
97 }
98 MessageType::Image(event) => {
99 event.source = sent.file.clone();
100 if let Some(info) = event.info.as_mut() {
101 info.thumbnail_source = sent.thumbnail.clone();
102 }
103 }
104 MessageType::Video(event) => {
105 event.source = sent.file.clone();
106 if let Some(info) = event.info.as_mut() {
107 info.thumbnail_source = sent.thumbnail.clone();
108 }
109 }
110
111 _ => {
112 error!("Invalid message type in database: {}", msgtype.msgtype());
116 debug_assert!(false, "invalid message type in database");
118 }
119 }
120}
121
122#[cfg(feature = "unstable-msc4274")]
125fn update_gallery_event_after_upload(
126 echo: &mut RoomMessageEventContent,
127 sent: HashMap<String, AccumulatedSentMediaInfo>,
128) {
129 let MessageType::Gallery(gallery) = &mut echo.msgtype else {
130 error!("Invalid gallery item types in database");
134 debug_assert!(false, "invalid item type in database {:?}", echo.msgtype());
136 return;
137 };
138
139 for itemtype in gallery.itemtypes.iter_mut() {
142 match itemtype {
143 GalleryItemType::Audio(event) => match sent.get(&event.source.unique_key()) {
144 Some(sent) => event.source = sent.file.clone(),
145 None => error!("key for item {:?} does not exist on gallery event", event.source),
146 },
147 GalleryItemType::File(event) => match sent.get(&event.source.unique_key()) {
148 Some(sent) => {
149 event.source = sent.file.clone();
150 if let Some(info) = event.info.as_mut() {
151 info.thumbnail_source = sent.thumbnail.clone();
152 }
153 }
154 None => error!("key for item {:?} does not exist on gallery event", event.source),
155 },
156 GalleryItemType::Image(event) => match sent.get(&event.source.unique_key()) {
157 Some(sent) => {
158 event.source = sent.file.clone();
159 if let Some(info) = event.info.as_mut() {
160 info.thumbnail_source = sent.thumbnail.clone();
161 }
162 }
163 None => error!("key for item {:?} does not exist on gallery event", event.source),
164 },
165 GalleryItemType::Video(event) => match sent.get(&event.source.unique_key()) {
166 Some(sent) => {
167 event.source = sent.file.clone();
168 if let Some(info) = event.info.as_mut() {
169 info.thumbnail_source = sent.thumbnail.clone();
170 }
171 }
172 None => error!("key for item {:?} does not exist on gallery event", event.source),
173 },
174
175 _ => {
176 error!("Invalid gallery item types in database");
181 debug_assert!(false, "invalid gallery item type in database {itemtype:?}");
183 }
184 }
185 }
186}
187
188#[derive(Default)]
189struct MediaCacheResult {
190 upload_thumbnail_txn: Option<OwnedTransactionId>,
191 event_thumbnail_info: Option<(MediaSource, Box<ThumbnailInfo>)>,
192 queue_thumbnail_info: Option<QueueThumbnailInfo>,
193}
194
195impl RoomSendQueue {
196 #[instrument(skip_all, fields(event_txn))]
216 pub async fn send_attachment(
217 &self,
218 filename: impl Into<String>,
219 content_type: Mime,
220 data: Vec<u8>,
221 config: AttachmentConfig,
222 ) -> Result<SendHandle, RoomSendQueueError> {
223 self.send_attachment_impl(filename.into(), content_type, data, config, None).await
224 }
225
226 #[instrument(skip_all, fields(event_txn, %edited_event_id))]
242 pub async fn edit_with_attachment(
243 &self,
244 edited_event_id: &EventId,
245 filename: impl Into<String>,
246 content_type: Mime,
247 data: Vec<u8>,
248 mut config: AttachmentConfig,
249 ) -> Result<SendHandle, RoomSendQueueError> {
250 let Some(room) = self.inner.room.get() else {
251 return Err(RoomSendQueueError::RoomDisappeared);
252 };
253
254 if room.state() != RoomState::Joined {
255 return Err(RoomSendQueueError::RoomNotJoined);
256 }
257
258 let original_mentions =
259 validate_attachment_edit(&room, room.own_user_id(), edited_event_id).await?;
260
261 config.reply = None;
263
264 self.send_attachment_impl(
265 filename.into(),
266 content_type,
267 data,
268 config,
269 Some((edited_event_id.to_owned(), original_mentions)),
270 )
271 .await
272 }
273
274 async fn send_attachment_impl(
278 &self,
279 filename: String,
280 content_type: Mime,
281 data: Vec<u8>,
282 mut config: AttachmentConfig,
283 replaces: Option<(OwnedEventId, Option<Mentions>)>,
284 ) -> Result<SendHandle, RoomSendQueueError> {
285 let Some(room) = self.inner.room.get() else {
286 return Err(RoomSendQueueError::RoomDisappeared);
287 };
288
289 if room.state() != RoomState::Joined {
290 return Err(RoomSendQueueError::RoomNotJoined);
291 }
292
293 let extra_content = config.extra_content.take();
294 let upload_file_txn = TransactionId::new();
295 let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into);
296
297 Span::current().record("event_txn", tracing::field::display(&*send_event_txn));
298 debug!(filename, %content_type, %upload_file_txn, "sending an attachment");
299
300 let file_media_request = Media::make_local_file_media_request(&upload_file_txn);
301
302 let MediaCacheResult { upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info } =
303 RoomSendQueue::cache_media(&room, data, config.thumbnail.take(), &file_media_request)
304 .await?;
305
306 let event_content = room
308 .make_media_event(
309 Room::make_attachment_type(
310 &content_type,
311 filename,
312 file_media_request.source.clone(),
313 config.caption,
314 config.info,
315 event_thumbnail_info,
316 ),
317 config.mentions,
318 config.reply,
319 )
320 .await
321 .map_err(|_| RoomSendQueueError::FailedToCreateAttachment)?;
322
323 let event_content = if let Some((edited_event_id, original_mentions)) = replaces {
328 RoomMessageEventContentWithoutRelation::from(event_content)
329 .make_replacement(ReplacementMetadata::new(edited_event_id, original_mentions))
330 } else {
331 event_content
332 };
333
334 let created_at = MilliSecondsSinceUnixEpoch::now();
335
336 self.inner
338 .queue
339 .push_media(
340 event_content.clone(),
341 content_type,
342 send_event_txn.clone().into(),
343 created_at,
344 upload_file_txn.clone(),
345 file_media_request,
346 queue_thumbnail_info,
347 extra_content.clone(),
348 )
349 .await?;
350
351 trace!("manager sends a media to the background task");
352
353 self.inner.notifier.notify_one();
354
355 let send_handle = SendHandle {
356 room: self.clone(),
357 transaction_id: send_event_txn.clone().into(),
358 media_handles: vec![MediaHandles { upload_thumbnail_txn, upload_file_txn }],
359 created_at,
360 };
361
362 self.send_update(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
363 transaction_id: send_event_txn.clone().into(),
364 content: LocalEchoContent::Event {
365 serialized_event: merge_extra_content(
366 SerializableEventContent::new(&event_content.into())
367 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
368 extra_content,
369 )?,
370 send_handle: send_handle.clone(),
371 send_error: None,
372 },
373 }));
374
375 Ok(send_handle)
376 }
377
378 #[cfg(feature = "unstable-msc4274")]
398 #[instrument(skip_all, fields(event_txn))]
399 pub async fn send_gallery(
400 &self,
401 gallery: GalleryConfig,
402 ) -> Result<SendHandle, RoomSendQueueError> {
403 let Some(room) = self.inner.room.get() else {
404 return Err(RoomSendQueueError::RoomDisappeared);
405 };
406
407 if room.state() != RoomState::Joined {
408 return Err(RoomSendQueueError::RoomNotJoined);
409 }
410
411 if gallery.is_empty() {
412 return Err(RoomSendQueueError::EmptyGallery);
413 }
414
415 let send_event_txn =
416 gallery.txn_id.clone().map_or_else(ChildTransactionId::new, Into::into);
417 let extra_content = gallery.extra_content.clone();
418
419 Span::current().record("event_txn", tracing::field::display(&*send_event_txn));
420
421 let mut item_types = Vec::with_capacity(gallery.len());
422 let mut item_queue_infos = Vec::with_capacity(gallery.len());
423 let mut media_handles = Vec::with_capacity(gallery.len());
424
425 for item_info in gallery.items {
426 let GalleryItemInfo { filename, content_type, data, .. } = item_info;
427
428 let upload_file_txn = TransactionId::new();
429
430 debug!(filename, %content_type, %upload_file_txn, "uploading a gallery attachment");
431
432 let file_media_request = Media::make_local_file_media_request(&upload_file_txn);
433
434 let MediaCacheResult {
435 upload_thumbnail_txn,
436 event_thumbnail_info,
437 queue_thumbnail_info,
438 } = RoomSendQueue::cache_media(&room, data, item_info.thumbnail, &file_media_request)
439 .await?;
440
441 item_types.push(Room::make_gallery_item_type(
442 &content_type,
443 filename,
444 file_media_request.source.clone(),
445 item_info.caption,
446 Some(item_info.attachment_info),
447 event_thumbnail_info,
448 ));
449
450 item_queue_infos.push(GalleryItemQueueInfo {
451 content_type,
452 upload_file_txn: upload_file_txn.clone(),
453 file_media_request,
454 thumbnail: queue_thumbnail_info,
455 extra_content: item_info.extra_content,
456 });
457
458 media_handles.push(MediaHandles { upload_file_txn, upload_thumbnail_txn });
459 }
460
461 let (body, formatted) =
463 gallery.caption.map(|caption| (caption.body, caption.formatted)).unwrap_or_default();
464 let event_content = room
465 .make_media_event(
466 MessageType::Gallery(GalleryMessageEventContent::new(body, formatted, item_types)),
467 gallery.mentions,
468 gallery.reply,
469 )
470 .await
471 .map_err(|_| RoomSendQueueError::FailedToCreateGallery)?;
472
473 let created_at = MilliSecondsSinceUnixEpoch::now();
474
475 let item_extra_content: Vec<_> =
477 item_queue_infos.iter().map(|item| item.extra_content.clone()).collect();
478
479 self.inner
480 .queue
481 .push_gallery(
482 event_content.clone(),
483 send_event_txn.clone().into(),
484 created_at,
485 item_queue_infos,
486 extra_content.clone(),
487 )
488 .await?;
489
490 trace!("manager sends a gallery to the background task");
491
492 self.inner.notifier.notify_one();
493
494 let send_handle = SendHandle {
495 room: self.clone(),
496 transaction_id: send_event_txn.clone().into(),
497 media_handles,
498 created_at,
499 };
500
501 self.send_update(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
502 transaction_id: send_event_txn.clone().into(),
503 content: LocalEchoContent::Event {
504 serialized_event: merge_gallery_extra_content(
505 SerializableEventContent::new(&event_content.into())
506 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
507 extra_content,
508 item_extra_content,
509 )?,
510 send_handle: send_handle.clone(),
511 send_error: None,
512 },
513 }));
514
515 Ok(send_handle)
516 }
517
518 async fn cache_media(
519 room: &Room,
520 data: Vec<u8>,
521 thumbnail: Option<Thumbnail>,
522 file_media_request: &MediaRequestParameters,
523 ) -> Result<MediaCacheResult, RoomSendQueueError> {
524 let client = room.client();
525 let media_store =
526 client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
527
528 media_store
530 .add_media_content(
531 file_media_request,
532 data,
533 IgnoreMediaRetentionPolicy::Yes,
535 )
536 .await
537 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
538
539 if let Some(thumbnail) = thumbnail {
541 let txn = TransactionId::new();
542 trace!(upload_thumbnail_txn = %txn, "media has a thumbnail");
543
544 let (data, content_type, thumbnail_info) = thumbnail.into_parts();
547 let file_size = data.len();
548
549 let thumbnail_height = thumbnail_info.height;
550 let thumbnail_width = thumbnail_info.width;
551
552 let thumbnail_media_request = Media::make_local_file_media_request(&txn);
554 media_store
555 .add_media_content(
556 &thumbnail_media_request,
557 data,
558 IgnoreMediaRetentionPolicy::Yes,
560 )
561 .await
562 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
563
564 Ok(MediaCacheResult {
565 upload_thumbnail_txn: Some(txn.clone()),
566 event_thumbnail_info: Some((
567 thumbnail_media_request.source.clone(),
568 thumbnail_info,
569 )),
570 queue_thumbnail_info: Some(QueueThumbnailInfo {
571 finish_upload_thumbnail_info: FinishUploadThumbnailInfo {
572 txn,
573 width: thumbnail_width,
574 height: thumbnail_height,
575 },
576 media_request_parameters: thumbnail_media_request,
577 content_type,
578 file_size,
579 }),
580 })
581 } else {
582 Ok(Default::default())
583 }
584 }
585}
586
587fn merge_missing_fields(
588 target: &mut serde_json::Map<String, serde_json::Value>,
589 extra: serde_json::Map<String, serde_json::Value>,
590) {
591 for (key, value) in extra {
592 match target.entry(key) {
593 serde_json::map::Entry::Occupied(mut entry) => match (entry.get_mut(), value) {
594 (serde_json::Value::Object(existing), serde_json::Value::Object(nested)) => {
595 merge_missing_fields(existing, nested);
596 }
597 _ => {
598 warn!(
599 key = entry.key(),
600 "extra content field shadowed by the event's own field"
601 );
602 }
603 },
604 serde_json::map::Entry::Vacant(entry) => {
605 entry.insert(value);
606 }
607 }
608 }
609}
610
611pub(super) fn merge_extra_content(
616 content: SerializableEventContent,
617 extra_content: Option<serde_json::Map<String, serde_json::Value>>,
618) -> Result<SerializableEventContent, RoomSendQueueStorageError> {
619 let Some(extra_content) = extra_content else {
620 return Ok(content);
621 };
622 if extra_content.is_empty() {
623 return Ok(content);
624 }
625
626 let (raw, event_type) = content.into_raw();
627 let mut object: serde_json::Map<String, serde_json::Value> =
628 raw.deserialize_as().map_err(RoomSendQueueStorageError::JsonSerialization)?;
629
630 merge_missing_fields(&mut object, extra_content);
631
632 let raw = ruma::serde::Raw::from_json(
633 serde_json::value::to_raw_value(&object)
634 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
635 );
636
637 Ok(SerializableEventContent::from_raw(raw, event_type))
638}
639
640#[cfg(feature = "unstable-msc4274")]
643pub(super) fn merge_gallery_extra_content(
644 content: SerializableEventContent,
645 extra_content: Option<serde_json::Map<String, serde_json::Value>>,
646 item_extra_content: impl IntoIterator<Item = Option<serde_json::Map<String, serde_json::Value>>>,
647) -> Result<SerializableEventContent, RoomSendQueueStorageError> {
648 let content = merge_extra_content(content, extra_content)?;
649 let mut item_extra_content = item_extra_content
650 .into_iter()
651 .enumerate()
652 .filter_map(|(index, extra)| {
653 extra.filter(|extra| !extra.is_empty()).map(|extra| (index, extra))
654 })
655 .peekable();
656 if item_extra_content.peek().is_none() {
657 return Ok(content);
658 }
659
660 let (raw, event_type) = content.into_raw();
661 let mut object: serde_json::Map<String, serde_json::Value> =
662 raw.deserialize_as().map_err(RoomSendQueueStorageError::JsonSerialization)?;
663
664 if let Some(serde_json::Value::Array(items)) = object.get_mut("itemtypes") {
665 for (index, extra) in item_extra_content {
666 if let Some(serde_json::Value::Object(item)) = items.get_mut(index) {
667 merge_missing_fields(item, extra);
668 }
669 }
670 }
671
672 let raw = ruma::serde::Raw::from_json(
673 serde_json::value::to_raw_value(&object)
674 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
675 );
676 Ok(SerializableEventContent::from_raw(raw, event_type))
677}
678
679fn update_serialized_media_caption(
685 serialized: &SerializableEventContent,
686 caption: Option<String>,
687 formatted_caption: Option<FormattedBody>,
688 mentions: Option<Mentions>,
689) -> Result<SerializableEventContent, RoomSendQueueStorageError> {
690 let AnyMessageLikeEventContent::RoomMessage(mut content) = serialized.deserialize()? else {
691 return Err(RoomSendQueueStorageError::InvalidMediaCaptionEdit);
692 };
693
694 let unknown = unknown_fields(serialized, &content);
695
696 if !update_media_caption(&mut content, caption, formatted_caption, mentions) {
697 return Err(RoomSendQueueStorageError::InvalidMediaCaptionEdit);
698 }
699
700 let mut edited = serde_json::to_value(&content)?;
701 if let Some(unknown) = unknown {
702 restore_unknown_fields(&mut edited, unknown);
703 }
704
705 Ok(SerializableEventContent::from_raw(
706 ruma::serde::Raw::from_json(serde_json::value::to_raw_value(&edited)?),
707 serialized.raw().1.to_owned(),
708 ))
709}
710
711pub(super) fn unknown_fields(
714 serialized: &SerializableEventContent,
715 typed: &impl serde::Serialize,
716) -> Option<serde_json::Value> {
717 let full = serialized.raw().0.deserialize_as().ok()?;
718 let known = serde_json::to_value(typed).ok()?;
719 diff_unknown_fields(&full, &known)
720}
721
722fn diff_unknown_fields(
725 full: &serde_json::Value,
726 known: &serde_json::Value,
727) -> Option<serde_json::Value> {
728 use serde_json::Value;
729
730 match (full, known) {
731 (Value::Object(full), Value::Object(known)) => {
732 let unknown: serde_json::Map<_, _> = full
733 .iter()
734 .filter_map(|(key, value)| match known.get(key) {
735 Some(known) => {
736 diff_unknown_fields(value, known).map(|value| (key.clone(), value))
737 }
738 None => Some((key.clone(), value.clone())),
739 })
740 .collect();
741 (!unknown.is_empty()).then_some(Value::Object(unknown))
742 }
743 (Value::Array(full), Value::Array(known)) if full.len() == known.len() => {
744 let unknown: Vec<_> = full
745 .iter()
746 .zip(known)
747 .map(|(full, known)| diff_unknown_fields(full, known).unwrap_or(Value::Null))
748 .collect();
749 unknown.iter().any(|value| !value.is_null()).then_some(Value::Array(unknown))
750 }
751 _ => None,
752 }
753}
754
755pub(super) fn restore_unknown_fields(target: &mut serde_json::Value, unknown: serde_json::Value) {
757 use serde_json::Value;
758
759 match (target, unknown) {
760 (Value::Object(target), Value::Object(unknown)) => {
761 for (key, value) in unknown {
762 match target.get_mut(&key) {
763 Some(existing) => restore_unknown_fields(existing, value),
764 None => {
765 target.insert(key, value);
766 }
767 }
768 }
769 }
770 (Value::Array(target), Value::Array(unknown)) => {
771 for (item, value) in target.iter_mut().zip(unknown) {
772 if !value.is_null() {
773 restore_unknown_fields(item, value);
774 }
775 }
776 }
777 _ => {}
778 }
779}
780
781impl QueueStorage {
782 #[allow(clippy::too_many_arguments)]
784 pub(super) async fn handle_dependent_finish_upload(
785 &self,
786 client: &Client,
787 event_txn: OwnedTransactionId,
788 parent_key: SentRequestKey,
789 mut local_echo: RoomMessageEventContent,
790 file_upload_txn: OwnedTransactionId,
791 thumbnail_info: Option<FinishUploadThumbnailInfo>,
792 extra_content: Option<serde_json::Map<String, serde_json::Value>>,
793 new_updates: &mut Vec<RoomSendQueueUpdate>,
794 ) -> Result<(), RoomSendQueueError> {
795 let sent_media = parent_key
797 .into_media()
798 .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
799
800 update_media_cache_keys_after_upload(client, &file_upload_txn, thumbnail_info, &sent_media)
801 .await?;
802 update_media_event_after_upload(&mut local_echo, sent_media);
803
804 let new_content = merge_extra_content(
805 SerializableEventContent::new(&local_echo.into())
806 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
807 extra_content,
808 )?;
809
810 new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
813 transaction_id: event_txn.clone(),
814 new_content: new_content.clone(),
815 });
816
817 trace!(%event_txn, "queueing media event after successfully uploading media(s)");
818
819 client
820 .state_store()
821 .save_send_queue_request(
822 &self.room_id,
823 event_txn,
824 MilliSecondsSinceUnixEpoch::now(),
825 new_content.into(),
826 Self::HIGH_PRIORITY,
827 )
828 .await
829 .map_err(RoomSendQueueStorageError::StateStoreError)?;
830
831 Ok(())
832 }
833
834 #[cfg(feature = "unstable-msc4274")]
837 #[allow(clippy::too_many_arguments)]
838 pub(super) async fn handle_dependent_finish_gallery_upload(
839 &self,
840 client: &Client,
841 event_txn: OwnedTransactionId,
842 parent_key: SentRequestKey,
843 mut local_echo: RoomMessageEventContent,
844 item_infos: Vec<FinishGalleryItemInfo>,
845 extra_content: Option<serde_json::Map<String, serde_json::Value>>,
846 new_updates: &mut Vec<RoomSendQueueUpdate>,
847 ) -> Result<(), RoomSendQueueError> {
848 let sent_gallery = parent_key
850 .into_media()
851 .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
852
853 let mut sent_media_vec = sent_gallery.accumulated;
854 sent_media_vec.push(AccumulatedSentMediaInfo {
855 file: sent_gallery.file,
856 thumbnail: sent_gallery.thumbnail,
857 });
858
859 let mut sent_infos = HashMap::new();
860
861 let item_extra_content: Vec<_> =
862 item_infos.iter().map(|item| item.extra_content.clone()).collect();
863
864 for (item_info, sent_media) in zip(item_infos, sent_media_vec) {
865 let FinishGalleryItemInfo { file_upload: file_upload_txn, thumbnail_info, .. } =
866 item_info;
867
868 let from_req = Media::make_local_file_media_request(&file_upload_txn);
871 sent_infos.insert(from_req.source.unique_key(), sent_media.clone());
872
873 update_media_cache_keys_after_upload(
874 client,
875 &file_upload_txn,
876 thumbnail_info,
877 &sent_media.into(),
878 )
879 .await?;
880 }
881
882 update_gallery_event_after_upload(&mut local_echo, sent_infos);
883
884 let new_content = merge_gallery_extra_content(
885 SerializableEventContent::new(&local_echo.into())
886 .map_err(RoomSendQueueStorageError::JsonSerialization)?,
887 extra_content,
888 item_extra_content,
889 )?;
890
891 new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
894 transaction_id: event_txn.clone(),
895 new_content: new_content.clone(),
896 });
897
898 trace!(%event_txn, "queueing media event after successfully uploading media(s)");
899
900 client
901 .state_store()
902 .save_send_queue_request(
903 &self.room_id,
904 event_txn,
905 MilliSecondsSinceUnixEpoch::now(),
906 new_content.into(),
907 Self::HIGH_PRIORITY,
908 )
909 .await
910 .map_err(RoomSendQueueStorageError::StateStoreError)?;
911
912 Ok(())
913 }
914
915 #[allow(clippy::too_many_arguments)]
918 pub(super) async fn handle_dependent_file_or_thumbnail_upload(
919 &self,
920 client: &Client,
921 next_upload_txn: OwnedTransactionId,
922 parent_key: SentRequestKey,
923 content_type: String,
924 cache_key: MediaRequestParameters,
925 event_txn: OwnedTransactionId,
926 parent_is_thumbnail_upload: bool,
927 ) -> Result<(), RoomSendQueueError> {
928 let sent_media = parent_key
931 .into_media()
932 .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
933
934 if parent_is_thumbnail_upload {
937 debug_assert!(sent_media.thumbnail.is_none());
938 if sent_media.thumbnail.is_some() {
939 warn!("unexpected thumbnail for a thumbnail!");
940 }
941 }
942
943 trace!(
944 related_to = %event_txn,
945 "done uploading file or thumbnail, now queuing the dependent file \
946 or thumbnail upload request",
947 );
948
949 #[cfg(feature = "unstable-msc4274")]
955 let accumulated = if parent_is_thumbnail_upload {
956 sent_media.accumulated
957 } else {
958 let mut accumulated = sent_media.accumulated;
959 accumulated.push(AccumulatedSentMediaInfo {
960 file: sent_media.file.clone(),
961 thumbnail: sent_media.thumbnail,
962 });
963 accumulated
964 };
965
966 let request = QueuedRequestKind::MediaUpload {
967 content_type,
968 cache_key,
969 thumbnail_source: parent_is_thumbnail_upload.then_some(sent_media.file),
972 related_to: event_txn,
973 #[cfg(feature = "unstable-msc4274")]
974 accumulated,
975 };
976
977 client
978 .state_store()
979 .save_send_queue_request(
980 &self.room_id,
981 next_upload_txn,
982 MilliSecondsSinceUnixEpoch::now(),
983 request,
984 Self::HIGH_PRIORITY,
985 )
986 .await
987 .map_err(RoomSendQueueStorageError::StateStoreError)?;
988
989 Ok(())
990 }
991
992 #[instrument(skip(self, handles))]
999 pub(super) async fn abort_upload(
1000 &self,
1001 event_txn: &TransactionId,
1002 handles: &MediaHandles,
1003 ) -> Result<bool, RoomSendQueueStorageError> {
1004 let mut guard = self.store.lock().await;
1005 let client = guard.client()?;
1006
1007 debug!("trying to abort an upload");
1009
1010 let store = client.state_store();
1011
1012 let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone());
1013 let event_as_dependent = ChildTransactionId::from(event_txn.to_owned());
1014
1015 let mut removed_dependent_upload = false;
1016 let mut removed_dependent_event = false;
1017
1018 if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn
1019 && store.remove_send_queue_request(&self.room_id, thumbnail_txn).await?
1020 {
1021 trace!("could remove thumbnail request, removing 2 dependent requests now");
1024
1025 if let Some(info) = guard.being_sent.as_ref()
1028 && info.transaction_id == *thumbnail_txn
1029 {
1030 let info = guard.being_sent.take().unwrap();
1032 if info.cancel_upload() {
1033 trace!("aborted ongoing thumbnail upload");
1034 }
1035 }
1036
1037 removed_dependent_upload = store
1039 .remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent)
1040 .await?;
1041
1042 if !removed_dependent_upload {
1043 warn!("unable to find the dependent file upload request");
1044 }
1045
1046 removed_dependent_event =
1047 store.remove_dependent_queued_request(&self.room_id, &event_as_dependent).await?;
1048
1049 if !removed_dependent_event {
1050 warn!("unable to find the dependent media event upload request");
1051 }
1052 }
1053
1054 if !removed_dependent_upload {
1062 if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
1063 trace!("could remove file upload request, removing 1 dependent request");
1067
1068 if let Some(info) = guard.being_sent.as_ref()
1071 && info.transaction_id == handles.upload_file_txn
1072 {
1073 let info = guard.being_sent.take().unwrap();
1075 if info.cancel_upload() {
1076 trace!("aborted ongoing file upload");
1077 }
1078 }
1079
1080 if !store
1082 .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
1083 .await?
1084 {
1085 warn!("unable to find the dependent media event upload request");
1086 }
1087 } else {
1088 if !removed_dependent_event
1093 && !store
1094 .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
1095 .await?
1096 {
1097 debug!("uploads already happened => deferring to aborting an event sending");
1101 return Ok(false);
1102 }
1103 }
1104 }
1105
1106 {
1110 let media_store = client.media_store().lock().await?;
1111 media_store
1112 .remove_media_content_for_uri(&Media::make_local_uri(&handles.upload_file_txn))
1113 .await?;
1114 if let Some(txn) = &handles.upload_thumbnail_txn {
1115 media_store.remove_media_content_for_uri(&Media::make_local_uri(txn)).await?;
1116 }
1117 }
1118
1119 debug!("successfully aborted!");
1120 Ok(true)
1121 }
1122
1123 #[instrument(skip(self, caption, formatted_caption))]
1124 pub(super) async fn edit_media_caption(
1125 &self,
1126 txn: &TransactionId,
1127 caption: Option<String>,
1128 formatted_caption: Option<FormattedBody>,
1129 mentions: Option<Mentions>,
1130 ) -> Result<Option<SerializableEventContent>, RoomSendQueueStorageError> {
1131 use RoomSendQueueStorageError::InvalidMediaCaptionEdit;
1133
1134 let guard = self.store.lock().await;
1135 let client = guard.client()?;
1136 let store = client.state_store();
1137
1138 {
1148 let dependent_requests = store.load_dependent_queued_requests(&self.room_id).await?;
1151
1152 if let Some(found) =
1153 dependent_requests.into_iter().find(|req| *req.own_transaction_id == *txn)
1154 {
1155 trace!("found the caption to edit in a dependent request");
1156
1157 let DependentQueuedRequestKind::FinishUpload {
1158 mut local_echo,
1159 file_upload,
1160 thumbnail_info,
1161 extra_content,
1162 } = found.kind
1163 else {
1164 return Err(InvalidMediaCaptionEdit);
1165 };
1166
1167 if !update_media_caption(&mut local_echo, caption, formatted_caption, mentions) {
1168 return Err(InvalidMediaCaptionEdit);
1169 }
1170
1171 let new_dependent_request = DependentQueuedRequestKind::FinishUpload {
1172 local_echo: local_echo.clone(),
1173 file_upload,
1174 thumbnail_info,
1175 extra_content: extra_content.clone(),
1176 };
1177 store
1178 .update_dependent_queued_request(
1179 &self.room_id,
1180 &found.own_transaction_id,
1181 new_dependent_request,
1182 )
1183 .await?;
1184
1185 trace!("caption successfully updated");
1186 return merge_extra_content(
1187 SerializableEventContent::new(&(*local_echo).into())?,
1188 extra_content,
1189 )
1190 .map(Some);
1191 }
1192 }
1193
1194 let requests = store.load_send_queue_requests(&self.room_id).await?;
1195 let Some(found) = requests.into_iter().find(|req| req.transaction_id == *txn) else {
1196 return Ok(None);
1198 };
1199
1200 trace!("found the caption to edit as a request");
1201
1202 let QueuedRequestKind::Event { content: serialized_content, .. } = found.kind else {
1203 return Err(InvalidMediaCaptionEdit);
1204 };
1205
1206 let new_serialized = update_serialized_media_caption(
1207 &serialized_content,
1208 caption,
1209 formatted_caption,
1210 mentions,
1211 )?;
1212
1213 if let Some(being_sent) = guard.being_sent.as_ref()
1215 && being_sent.transaction_id == *txn
1216 {
1217 store
1219 .save_dependent_queued_request(
1220 &self.room_id,
1221 txn,
1222 ChildTransactionId::new(),
1223 MilliSecondsSinceUnixEpoch::now(),
1224 DependentQueuedRequestKind::EditEvent { new_content: new_serialized.clone() },
1225 )
1226 .await?;
1227
1228 trace!("media event was being sent, pushed a dependent edit");
1229 return Ok(Some(new_serialized));
1230 }
1231
1232 store.update_send_queue_request(&self.room_id, txn, new_serialized.clone().into()).await?;
1234
1235 trace!("media event was not being sent, updated local echo");
1236 Ok(Some(new_serialized))
1237 }
1238}
1239
1240async fn update_media_cache_keys_after_upload(
1243 client: &Client,
1244 file_upload_txn: &OwnedTransactionId,
1245 thumbnail_info: Option<FinishUploadThumbnailInfo>,
1246 sent_media: &SentMediaInfo,
1247) -> Result<(), RoomSendQueueError> {
1248 let from_req = Media::make_local_file_media_request(file_upload_txn);
1250
1251 trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store");
1252 let media_store =
1253 client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
1254
1255 media_store
1257 .set_ignore_media_retention_policy(&from_req, IgnoreMediaRetentionPolicy::No)
1258 .await
1259 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1260
1261 media_store
1262 .replace_media_key(
1263 &from_req,
1264 &MediaRequestParameters { source: sent_media.file.clone(), format: MediaFormat::File },
1265 )
1266 .await
1267 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1268
1269 if let Some((info, remote_thumbnail_source)) =
1271 thumbnail_info.as_ref().zip(sent_media.thumbnail.clone())
1272 {
1273 let from_request_params = Media::make_local_file_media_request(&info.txn);
1274
1275 if let Some((height, width)) = info.height.zip(info.width) {
1276 trace!(
1277 from = ?from_req.source,
1278 to = ?remote_thumbnail_source,
1279 height = u64::from(height),
1280 width = u64::from(width),
1281 "storing thumbnail as a thumbnail for the uploaded media, and a file in itself, in cache store"
1282 );
1283
1284 match media_store
1289 .get_media_content(&MediaRequestParameters {
1290 source: from_request_params.source.clone(),
1291 format: MediaFormat::File,
1292 })
1293 .await
1294 {
1295 Ok(Some(thumbnail_content)) => {
1296 media_store
1300 .add_media_content(
1301 &MediaRequestParameters {
1302 source: remote_thumbnail_source.clone(),
1303 format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
1304 width, height,
1305 )),
1306 },
1307 thumbnail_content,
1308 IgnoreMediaRetentionPolicy::No,
1309 )
1310 .await
1311 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1312 }
1313
1314 Ok(None) => {
1315 warn!(
1316 from = ?from_request_params.source,
1317 "unable to reload thumbnail content from media store: no content found",
1318 );
1319 }
1320
1321 Err(err) => {
1322 error!(
1325 from = ?from_request_params.source,
1326 "unable to reload thumbnail content from media store: {err}"
1327 );
1328 }
1329 }
1330 } else {
1331 trace!(from = ?from_req.source, to = ?remote_thumbnail_source, "only renaming thumbnail key to file in cache store");
1332 }
1333
1334 media_store
1336 .set_ignore_media_retention_policy(&from_request_params, IgnoreMediaRetentionPolicy::No)
1337 .await
1338 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1339
1340 media_store
1342 .replace_media_key(
1343 &from_request_params,
1344 &MediaRequestParameters {
1345 source: remote_thumbnail_source,
1346 format: MediaFormat::File,
1347 },
1348 )
1349 .await
1350 .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1351 }
1352
1353 Ok(())
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use matrix_sdk_base::store::SerializableEventContent;
1359 use serde_json::{Value, json};
1360
1361 use super::{RoomSendQueueStorageError, update_serialized_media_caption};
1362
1363 fn try_edit_caption(content: Value) -> Result<Value, RoomSendQueueStorageError> {
1364 let serialized = SerializableEventContent::from_raw(
1365 ruma::serde::Raw::from_json(serde_json::value::to_raw_value(&content).unwrap()),
1366 "m.room.message".to_owned(),
1367 );
1368 let edited = update_serialized_media_caption(
1369 &serialized,
1370 Some("new caption".to_owned()),
1371 None,
1372 None,
1373 )?;
1374 Ok(edited.raw().0.deserialize_as().unwrap())
1375 }
1376
1377 fn edit_caption(content: Value) -> Value {
1378 try_edit_caption(content).unwrap()
1379 }
1380
1381 #[test]
1382 fn test_caption_edit_keeps_extra_content() {
1383 let edited = edit_caption(json!({
1384 "msgtype": "m.image",
1385 "body": "old caption",
1386 "filename": "image.jpg",
1387 "format": "org.matrix.custom.html",
1388 "formatted_body": "<b>old caption</b>",
1389 "url": "mxc://sdk.rs/media",
1390 "info": { "mimetype": "image/jpeg", "com.example.info": 1 },
1391 "com.example.key": "kept",
1392 }));
1393
1394 assert_eq!(edited["body"], "new caption");
1395 assert_eq!(edited["filename"], "image.jpg");
1396 assert!(edited.get("formatted_body").is_none());
1398 assert_eq!(edited["com.example.key"], "kept");
1399 assert_eq!(edited["info"]["com.example.info"], 1);
1400 }
1401
1402 #[test]
1403 fn test_caption_edit_of_a_media_edit_keeps_extra_content() {
1404 let edited = edit_caption(json!({
1406 "msgtype": "m.image",
1407 "body": "* old caption",
1408 "filename": "image.jpg",
1409 "url": "mxc://sdk.rs/media",
1410 "m.new_content": {
1411 "msgtype": "m.image",
1412 "body": "old caption",
1413 "filename": "image.jpg",
1414 "url": "mxc://sdk.rs/media",
1415 "com.example.inner": "kept",
1416 },
1417 "m.relates_to": { "rel_type": "m.replace", "event_id": "$1" },
1418 "com.example.key": "kept",
1419 }));
1420
1421 assert_eq!(edited["m.new_content"]["body"], "new caption");
1422 assert_eq!(edited["m.new_content"]["com.example.inner"], "kept");
1423 assert_eq!(edited["com.example.key"], "kept");
1424 assert_eq!(edited["m.relates_to"]["event_id"], "$1");
1425 }
1426
1427 #[test]
1428 fn test_caption_edit_rejects_non_media() {
1429 let result = try_edit_caption(json!({ "msgtype": "m.text", "body": "hello" }));
1430 assert!(matches!(result, Err(RoomSendQueueStorageError::InvalidMediaCaptionEdit)));
1431 }
1432
1433 #[cfg(feature = "unstable-msc4274")]
1434 #[test]
1435 fn test_gallery_caption_edit_keeps_item_extra_content() {
1436 let edited = edit_caption(json!({
1437 "msgtype": "dm.filament.gallery",
1438 "body": "old caption",
1439 "itemtypes": [
1440 { "itemtype": "m.image", "body": "a.jpg", "url": "mxc://sdk.rs/a" },
1441 {
1442 "itemtype": "m.image",
1443 "body": "b.jpg",
1444 "url": "mxc://sdk.rs/b",
1445 "org.matrix.msc2448.is_spoiler": true,
1446 },
1447 ],
1448 "com.example.key": "kept",
1449 }));
1450
1451 assert_eq!(edited["body"], "new caption");
1452 assert_eq!(edited["com.example.key"], "kept");
1453 assert!(edited["itemtypes"][0].get("org.matrix.msc2448.is_spoiler").is_none());
1454 assert_eq!(edited["itemtypes"][1]["org.matrix.msc2448.is_spoiler"], true);
1455 }
1456}