Skip to main content

matrix_sdk/send_queue/
upload.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//! Private implementations of the media upload mechanism.
16
17#[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
70/// Replace the source by the final ones in all the media types handled by
71/// [`Room::make_attachment_type()`].
72fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) {
73    update_media_msgtype_after_upload(&mut echo.msgtype, &sent);
74
75    // A media edit (see `RoomSendQueue::edit_with_attachment`) keeps the
76    // canonical copy of the new content inside the replacement relation;
77    // patch it too, or the two copies would point at different files.
78    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
83/// Replace the source by the final ones in a single [`MessageType`], for all
84/// the media types handled by [`Room::make_attachment_type()`].
85fn update_media_msgtype_after_upload(msgtype: &mut MessageType, sent: &SentMediaInfo) {
86    // Some variants look really similar below, but the `event` and `info` are
87    // all different types…
88    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            // All `MessageType` created by `Room::make_attachment_type` should
113            // be handled here. The only way to end up here is that a message
114            // type has been tampered with in the database.
115            error!("Invalid message type in database: {}", msgtype.msgtype());
116            // Only crash debug builds.
117            debug_assert!(false, "invalid message type in database");
118        }
119    }
120}
121
122/// Replace the sources by the final ones in all the media types handled by
123/// [`Room::make_gallery_item_type()`].
124#[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        // All `GalleryItemType` created by `Room::make_gallery_item_type`
131        // should be handled here. The only way to end up here is that a item
132        // type has been tampered with in the database.
133        error!("Invalid gallery item types in database");
134        // Only crash debug builds.
135        debug_assert!(false, "invalid item type in database {:?}", echo.msgtype());
136        return;
137    };
138
139    // Some variants look really similar below, but the `event` and `info` are
140    // all different types…
141    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                // All `GalleryItemType` created by
177                // `Room::make_gallery_item_type` should be handled here. The
178                // only way to end up here is that a item type has been tampered
179                // with in the database.
180                error!("Invalid gallery item types in database");
181                // Only crash debug builds.
182                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    /// Queues an attachment to be sent to the room, using the send queue.
197    ///
198    /// This returns quickly (without sending or uploading anything), and will
199    /// push the event to be sent into a queue, handled in the background.
200    ///
201    /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling the
202    /// [`Self::subscribe()`] method to get updates about the sending of that
203    /// event.
204    ///
205    /// By default, if sending failed on the first attempt, it will be retried a
206    /// few times. If sending failed after those retries, the entire client's
207    /// sending queue will be disabled, and it will need to be manually
208    /// re-enabled by the caller (e.g. after network is back, or when something
209    /// has been done about the faulty requests).
210    ///
211    /// The attachment and its optional thumbnail are stored in the media cache
212    /// and can be retrieved at any time, by calling
213    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
214    /// in the local or remote echo, and using a `MediaFormat::File`.
215    #[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    /// Queues an edit replacing the attachment of a message the current user
227    /// sent, or adding one to a message which had none.
228    ///
229    /// The upload and the `m.replace` it resolves into go through the send
230    /// queue like [`Self::send_attachment`], so they survive a restart.
231    /// Nothing is queued until the edited event has been read, though: if it
232    /// isn't cached while offline, this fails with
233    /// [`EditError::Fetch`](crate::room::edit::EditError::Fetch).
234    ///
235    /// Nothing of the original content is carried over: the caption in
236    /// `config` is the whole new text, and its `reply` is ignored, as a
237    /// replacement carries no other relation. The previous attachment stays
238    /// in the room's edit history.
239    ///
240    /// Aborting the returned handle cancels the upload and drops the edit.
241    #[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        // A replacement carries no other relation.
262        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    /// Shared implementation of [`Self::send_attachment`] and
275    /// [`Self::edit_with_attachment`]: queue the uploads and the event they
276    /// resolve into, either a new media event or an `m.replace` of `replaces`.
277    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        // Create the content for the media event.
307        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        // For an edit, wrap the media content into a replacement of the edited
324        // event. The upload chain doesn't care about the relation; once
325        // the upload is done, `update_media_event_after_upload` patches
326        // both copies of the content.
327        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        // Save requests in the queue storage.
337        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    /// Queues a gallery to be sent to the room, using the send queue.
379    ///
380    /// This returns quickly (without sending or uploading anything), and will
381    /// push the event to be sent into a queue, handled in the background.
382    ///
383    /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling the
384    /// [`Self::subscribe()`] method to get updates about the sending of that
385    /// event.
386    ///
387    /// By default, if sending failed on the first attempt, it will be retried a
388    /// few times. If sending failed after those retries, the entire client's
389    /// sending queue will be disabled, and it will need to be manually
390    /// re-enabled by the caller (e.g. after network is back, or when something
391    /// has been done about the faulty requests).
392    ///
393    /// The attachments and their optional thumbnails are stored in the media
394    /// cache and can be retrieved at any time, by calling
395    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
396    /// in the local or remote echo, and using a `MediaFormat::File`.
397    #[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        // Create the content for the gallery event.
462        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        // Save requests in the queue storage.
476        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        // Cache the file itself in the cache store.
529        media_store
530            .add_media_content(
531                file_media_request,
532                data,
533                // Make sure that the file is stored until it has been uploaded.
534                IgnoreMediaRetentionPolicy::Yes,
535            )
536            .await
537            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
538
539        // Process the thumbnail, if it's been provided.
540        if let Some(thumbnail) = thumbnail {
541            let txn = TransactionId::new();
542            trace!(upload_thumbnail_txn = %txn, "media has a thumbnail");
543
544            // Create the information required for filling the thumbnail section
545            // of the event.
546            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            // Cache thumbnail in the cache store.
553            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                    // Make sure that the thumbnail is stored until it has been uploaded.
559                    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
611/// Merge additional fields into serialized event content.
612///
613/// Objects present on both sides are merged recursively; otherwise the
614/// serialized content's own fields win.
615pub(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/// Merge custom fields into the gallery and its items without replacing
641/// generated fields.
642#[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
679/// Updates the caption of a queued media event.
680///
681/// The caption is edited on the typed content, which drops the fields it
682/// doesn't know about, such as the extra content of an attachment or of a
683/// gallery's items; they are put back on the edited content.
684fn 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
711/// Returns the fields of a serialized content that its typed form can't hold,
712/// like the extra content of an attachment.
713pub(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
722/// Returns the parts of `full` that `known` doesn't have. Arrays are compared
723/// item by item, with `null` standing for an item with nothing to add.
724fn 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
755/// Puts back what [`unknown_fields`] found, without replacing any field.
756pub(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    /// Consumes a finished upload and queues sending of the final media event.
783    #[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        // Both uploads are ready: enqueue the event with its final data.
796        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        // Indicates observers that the upload finished, by editing the local
811        // echo for the event into its final form before sending.
812        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    /// Consumes a finished gallery upload and queues sending of the final
835    /// gallery event.
836    #[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        // All uploads are ready: enqueue the event with its final data.
849        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            // Store the sent media under the original cache key for later
869            // insertion into the local echo.
870            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        // Indicates observers that the upload finished, by editing the local
892        // echo for the event into its final form before sending.
893        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    /// Consumes a finished file or thumbnail upload and queues the dependent
916    /// file or thumbnail upload.
917    #[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        // The previous file or thumbnail has been sent, now transform the
929        // dependent file or thumbnail upload request into a ready one.
930        let sent_media = parent_key
931            .into_media()
932            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
933
934        // If the previous upload was a thumbnail, it shouldn't have a thumbnail
935        // itself.
936        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        // If the parent request was a thumbnail upload, don't add it to the
950        // list of accumulated medias yet because its dependent file upload is
951        // still pending. If the parent request was a file upload, we know that
952        // both the file and its thumbnail (if any) have finished uploading and
953        // we can add them to the accumulated sent media.
954        #[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            // If the previous upload was a thumbnail, it becomes the thumbnail
970            // source for the next upload.
971            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    /// Try to abort an upload that would be ongoing.
993    ///
994    /// Return true if any media (media itself or its thumbnail) was being
995    /// uploaded. In this case, the media event has also been removed from the
996    /// send queue. If it returns false, then the uploads already happened, and
997    /// the event sending _may_ have started.
998    #[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        // Keep the lock until we're done touching the storage.
1008        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            // The thumbnail upload existed as a request: either it was pending
1022            // (something else was being sent), or it was actively being sent.
1023            trace!("could remove thumbnail request, removing 2 dependent requests now");
1024
1025            // 1. Try to abort sending using the being_sent info, in case it was
1026            //    active.
1027            if let Some(info) = guard.being_sent.as_ref()
1028                && info.transaction_id == *thumbnail_txn
1029            {
1030                // SAFETY: we knew it was Some(), two lines above.
1031                let info = guard.being_sent.take().unwrap();
1032                if info.cancel_upload() {
1033                    trace!("aborted ongoing thumbnail upload");
1034                }
1035            }
1036
1037            // 2. Remove the dependent requests.
1038            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 we're here:
1055        //
1056        // - either there was no thumbnail to upload,
1057        // - or the thumbnail request has terminated already.
1058        //
1059        // So the next target is the upload request itself, in both cases.
1060
1061        if !removed_dependent_upload {
1062            if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
1063                // The upload existed as a request: either it was pending
1064                // (something else was being sent), or it was actively being
1065                // sent.
1066                trace!("could remove file upload request, removing 1 dependent request");
1067
1068                // 1. Try to abort sending using the being_sent info, in case it
1069                //    was active.
1070                if let Some(info) = guard.being_sent.as_ref()
1071                    && info.transaction_id == handles.upload_file_txn
1072                {
1073                    // SAFETY: we knew it was Some(), two lines above.
1074                    let info = guard.being_sent.take().unwrap();
1075                    if info.cancel_upload() {
1076                        trace!("aborted ongoing file upload");
1077                    }
1078                }
1079
1080                // 2. Remove the dependent request.
1081                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                // The upload was not in the send queue, so it's completed.
1089                //
1090                // It means the event sending is either still queued as a
1091                // dependent request, or it's graduated into a request.
1092                if !removed_dependent_event
1093                    && !store
1094                        .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
1095                        .await?
1096                {
1097                    // The media event has been promoted into a request, or the
1098                    // promoted request has been sent already: we couldn't
1099                    // abort, let the caller decide what to do.
1100                    debug!("uploads already happened => deferring to aborting an event sending");
1101                    return Ok(false);
1102                }
1103            }
1104        }
1105
1106        // At this point, all the requests and dependent requests have been
1107        // cleaned up. Perform the final step: empty the cache from the local
1108        // items.
1109        {
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        // This error will be popular here.
1132        use RoomSendQueueStorageError::InvalidMediaCaptionEdit;
1133
1134        let guard = self.store.lock().await;
1135        let client = guard.client()?;
1136        let store = client.state_store();
1137
1138        // The media event can be in one of three states:
1139        //
1140        // - still stored as a dependent request,
1141        // - stored as a queued request, active (aka it's being sent).
1142        // - stored as a queued request, not active yet (aka it's not being sent
1143        //   yet),
1144        //
1145        // We'll handle each of these cases one by one.
1146
1147        {
1148            // If the event can be found as a dependent event, update the
1149            // captions, save it back into the database, and return early.
1150            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            // Couldn't be found anymore, it's not possible to update captions.
1197            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 the request is active (being sent), send a dependent request.
1214        if let Some(being_sent) = guard.being_sent.as_ref()
1215            && being_sent.transaction_id == *txn
1216        {
1217            // Record a dependent request to edit, and exit.
1218            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        // The request is not active: edit the local echo.
1233        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
1240/// Update cache keys in the cache store after uploading a media file /
1241/// thumbnail.
1242async 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    // Do it for the file itself.
1249    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    // The media file can now be removed during cleanups.
1256    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    // Rename the thumbnail too, if needs be.
1270    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            // Try to reload the content of the thumbnail from the media store.
1285            // As it's not a strong requirement to store the thumbnail a second
1286            // time, we silently log errors instead of propagating them to the
1287            // caller.
1288            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                    // Also cache this as a thumbnail for the thumbnail, in the
1297                    // media store; the ElementX apps expect that specific
1298                    // format for thumbnails.
1299                    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                    // Silently log the error, but proceed, as storing the
1323                    // thumbnail as such isn't a strong requirement.
1324                    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        // The thumbnail file can now be removed during cleanups.
1335        media_store
1336            .set_ignore_media_retention_policy(&from_request_params, IgnoreMediaRetentionPolicy::No)
1337            .await
1338            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1339
1340        // Save the thumbnail as a file as well.
1341        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        // Fields the edit removes don't come back.
1397        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        // A pending media edit: its canonical content is in `m.new_content`.
1405        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}