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    MilliSecondsSinceUnixEpoch, OwnedTransactionId, TransactionId,
41    events::{
42        AnyMessageLikeEventContent, Mentions,
43        room::{
44            MediaSource, ThumbnailInfo,
45            message::{FormattedBody, MessageType, RoomMessageEventContent},
46        },
47    },
48};
49use tracing::{Span, debug, error, instrument, trace, warn};
50
51use super::{QueueStorage, QueueThumbnailInfo, RoomSendQueue, RoomSendQueueError};
52use crate::{
53    Client, Media, Room,
54    attachment::{AttachmentConfig, Thumbnail},
55    room::edit::update_media_caption,
56    send_queue::{
57        LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate,
58        SendHandle,
59    },
60};
61#[cfg(feature = "unstable-msc4274")]
62use crate::{
63    attachment::{GalleryConfig, GalleryItemInfo},
64    send_queue::GalleryItemQueueInfo,
65};
66
67/// Replace the source by the final ones in all the media types handled by
68/// [`Room::make_attachment_type()`].
69fn update_media_event_after_upload(echo: &mut RoomMessageEventContent, sent: SentMediaInfo) {
70    // Some variants look really similar below, but the `event` and `info` are all
71    // different types…
72    match &mut echo.msgtype {
73        MessageType::Audio(event) => {
74            event.source = sent.file;
75        }
76        MessageType::File(event) => {
77            event.source = sent.file;
78            if let Some(info) = event.info.as_mut() {
79                info.thumbnail_source = sent.thumbnail;
80            }
81        }
82        MessageType::Image(event) => {
83            event.source = sent.file;
84            if let Some(info) = event.info.as_mut() {
85                info.thumbnail_source = sent.thumbnail;
86            }
87        }
88        MessageType::Video(event) => {
89            event.source = sent.file;
90            if let Some(info) = event.info.as_mut() {
91                info.thumbnail_source = sent.thumbnail;
92            }
93        }
94
95        _ => {
96            // All `MessageType` created by `Room::make_attachment_type` should be
97            // handled here. The only way to end up here is that a message type has
98            // been tampered with in the database.
99            error!("Invalid message type in database: {}", echo.msgtype());
100            // Only crash debug builds.
101            debug_assert!(false, "invalid message type in database");
102        }
103    }
104}
105
106/// Replace the sources by the final ones in all the media types handled by
107/// [`Room::make_gallery_item_type()`].
108#[cfg(feature = "unstable-msc4274")]
109fn update_gallery_event_after_upload(
110    echo: &mut RoomMessageEventContent,
111    sent: HashMap<String, AccumulatedSentMediaInfo>,
112) {
113    let MessageType::Gallery(gallery) = &mut echo.msgtype else {
114        // All `GalleryItemType` created by `Room::make_gallery_item_type` should be
115        // handled here. The only way to end up here is that a item type has
116        // been tampered with in the database.
117        error!("Invalid gallery item types in database");
118        // Only crash debug builds.
119        debug_assert!(false, "invalid item type in database {:?}", echo.msgtype());
120        return;
121    };
122
123    // Some variants look really similar below, but the `event` and `info` are all
124    // different types…
125    for itemtype in gallery.itemtypes.iter_mut() {
126        match itemtype {
127            GalleryItemType::Audio(event) => match sent.get(&event.source.unique_key()) {
128                Some(sent) => event.source = sent.file.clone(),
129                None => error!("key for item {:?} does not exist on gallery event", event.source),
130            },
131            GalleryItemType::File(event) => match sent.get(&event.source.unique_key()) {
132                Some(sent) => {
133                    event.source = sent.file.clone();
134                    if let Some(info) = event.info.as_mut() {
135                        info.thumbnail_source = sent.thumbnail.clone();
136                    }
137                }
138                None => error!("key for item {:?} does not exist on gallery event", event.source),
139            },
140            GalleryItemType::Image(event) => match sent.get(&event.source.unique_key()) {
141                Some(sent) => {
142                    event.source = sent.file.clone();
143                    if let Some(info) = event.info.as_mut() {
144                        info.thumbnail_source = sent.thumbnail.clone();
145                    }
146                }
147                None => error!("key for item {:?} does not exist on gallery event", event.source),
148            },
149            GalleryItemType::Video(event) => match sent.get(&event.source.unique_key()) {
150                Some(sent) => {
151                    event.source = sent.file.clone();
152                    if let Some(info) = event.info.as_mut() {
153                        info.thumbnail_source = sent.thumbnail.clone();
154                    }
155                }
156                None => error!("key for item {:?} does not exist on gallery event", event.source),
157            },
158
159            _ => {
160                // All `GalleryItemType` created by `Room::make_gallery_item_type` should be
161                // handled here. The only way to end up here is that a item type has
162                // been tampered with in the database.
163                error!("Invalid gallery item types in database");
164                // Only crash debug builds.
165                debug_assert!(false, "invalid gallery item type in database {itemtype:?}");
166            }
167        }
168    }
169}
170
171#[derive(Default)]
172struct MediaCacheResult {
173    upload_thumbnail_txn: Option<OwnedTransactionId>,
174    event_thumbnail_info: Option<(MediaSource, Box<ThumbnailInfo>)>,
175    queue_thumbnail_info: Option<QueueThumbnailInfo>,
176}
177
178impl RoomSendQueue {
179    /// Queues an attachment to be sent to the room, using the send queue.
180    ///
181    /// This returns quickly (without sending or uploading anything), and will
182    /// push the event to be sent into a queue, handled in the background.
183    ///
184    /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling
185    /// the [`Self::subscribe()`] method to get updates about the sending of
186    /// that event.
187    ///
188    /// By default, if sending failed on the first attempt, it will be retried a
189    /// few times. If sending failed after those retries, the entire
190    /// client's sending queue will be disabled, and it will need to be
191    /// manually re-enabled by the caller (e.g. after network is back, or when
192    /// something has been done about the faulty requests).
193    ///
194    /// The attachment and its optional thumbnail are stored in the media cache
195    /// and can be retrieved at any time, by calling
196    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
197    /// in the local or remote echo, and using a `MediaFormat::File`.
198    #[instrument(skip_all, fields(event_txn))]
199    pub async fn send_attachment(
200        &self,
201        filename: impl Into<String>,
202        content_type: Mime,
203        data: Vec<u8>,
204        mut config: AttachmentConfig,
205    ) -> Result<SendHandle, RoomSendQueueError> {
206        let Some(room) = self.inner.room.get() else {
207            return Err(RoomSendQueueError::RoomDisappeared);
208        };
209
210        if room.state() != RoomState::Joined {
211            return Err(RoomSendQueueError::RoomNotJoined);
212        }
213
214        let filename = filename.into();
215        let extra_content = config.extra_content.take();
216        let upload_file_txn = TransactionId::new();
217        let send_event_txn = config.txn_id.map_or_else(ChildTransactionId::new, Into::into);
218
219        Span::current().record("event_txn", tracing::field::display(&*send_event_txn));
220        debug!(filename, %content_type, %upload_file_txn, "sending an attachment");
221
222        let file_media_request = Media::make_local_file_media_request(&upload_file_txn);
223
224        let MediaCacheResult { upload_thumbnail_txn, event_thumbnail_info, queue_thumbnail_info } =
225            RoomSendQueue::cache_media(&room, data, config.thumbnail.take(), &file_media_request)
226                .await?;
227
228        // Create the content for the media event.
229        let event_content = room
230            .make_media_event(
231                Room::make_attachment_type(
232                    &content_type,
233                    filename,
234                    file_media_request.source.clone(),
235                    config.caption,
236                    config.info,
237                    event_thumbnail_info,
238                ),
239                config.mentions,
240                config.reply,
241            )
242            .await
243            .map_err(|_| RoomSendQueueError::FailedToCreateAttachment)?;
244
245        let created_at = MilliSecondsSinceUnixEpoch::now();
246
247        // Save requests in the queue storage.
248        self.inner
249            .queue
250            .push_media(
251                event_content.clone(),
252                content_type,
253                send_event_txn.clone().into(),
254                created_at,
255                upload_file_txn.clone(),
256                file_media_request,
257                queue_thumbnail_info,
258                extra_content.clone(),
259            )
260            .await?;
261
262        trace!("manager sends a media to the background task");
263
264        self.inner.notifier.notify_one();
265
266        let send_handle = SendHandle {
267            room: self.clone(),
268            transaction_id: send_event_txn.clone().into(),
269            media_handles: vec![MediaHandles { upload_thumbnail_txn, upload_file_txn }],
270            created_at,
271        };
272
273        self.send_update(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
274            transaction_id: send_event_txn.clone().into(),
275            content: LocalEchoContent::Event {
276                serialized_event: merge_extra_content(
277                    SerializableEventContent::new(&event_content.into())
278                        .map_err(RoomSendQueueStorageError::JsonSerialization)?,
279                    extra_content,
280                )?,
281                send_handle: send_handle.clone(),
282                send_error: None,
283            },
284        }));
285
286        Ok(send_handle)
287    }
288
289    /// Queues a gallery to be sent to the room, using the send queue.
290    ///
291    /// This returns quickly (without sending or uploading anything), and will
292    /// push the event to be sent into a queue, handled in the background.
293    ///
294    /// Callers are expected to consume [`RoomSendQueueUpdate`] via calling
295    /// the [`Self::subscribe()`] method to get updates about the sending of
296    /// that event.
297    ///
298    /// By default, if sending failed on the first attempt, it will be retried a
299    /// few times. If sending failed after those retries, the entire
300    /// client's sending queue will be disabled, and it will need to be
301    /// manually re-enabled by the caller (e.g. after network is back, or when
302    /// something has been done about the faulty requests).
303    ///
304    /// The attachments and their optional thumbnails are stored in the media
305    /// cache and can be retrieved at any time, by calling
306    /// [`Media::get_media_content()`] with the `MediaSource` that can be found
307    /// in the local or remote echo, and using a `MediaFormat::File`.
308    #[cfg(feature = "unstable-msc4274")]
309    #[instrument(skip_all, fields(event_txn))]
310    pub async fn send_gallery(
311        &self,
312        gallery: GalleryConfig,
313    ) -> Result<SendHandle, RoomSendQueueError> {
314        let Some(room) = self.inner.room.get() else {
315            return Err(RoomSendQueueError::RoomDisappeared);
316        };
317
318        if room.state() != RoomState::Joined {
319            return Err(RoomSendQueueError::RoomNotJoined);
320        }
321
322        if gallery.is_empty() {
323            return Err(RoomSendQueueError::EmptyGallery);
324        }
325
326        let send_event_txn =
327            gallery.txn_id.clone().map_or_else(ChildTransactionId::new, Into::into);
328
329        Span::current().record("event_txn", tracing::field::display(&*send_event_txn));
330
331        let mut item_types = Vec::with_capacity(gallery.len());
332        let mut item_queue_infos = Vec::with_capacity(gallery.len());
333        let mut media_handles = Vec::with_capacity(gallery.len());
334
335        for item_info in gallery.items {
336            let GalleryItemInfo { filename, content_type, data, .. } = item_info;
337
338            let upload_file_txn = TransactionId::new();
339
340            debug!(filename, %content_type, %upload_file_txn, "uploading a gallery attachment");
341
342            let file_media_request = Media::make_local_file_media_request(&upload_file_txn);
343
344            let MediaCacheResult {
345                upload_thumbnail_txn,
346                event_thumbnail_info,
347                queue_thumbnail_info,
348            } = RoomSendQueue::cache_media(&room, data, item_info.thumbnail, &file_media_request)
349                .await?;
350
351            item_types.push(Room::make_gallery_item_type(
352                &content_type,
353                filename,
354                file_media_request.source.clone(),
355                item_info.caption,
356                Some(item_info.attachment_info),
357                event_thumbnail_info,
358            ));
359
360            item_queue_infos.push(GalleryItemQueueInfo {
361                content_type,
362                upload_file_txn: upload_file_txn.clone(),
363                file_media_request,
364                thumbnail: queue_thumbnail_info,
365            });
366
367            media_handles.push(MediaHandles { upload_file_txn, upload_thumbnail_txn });
368        }
369
370        // Create the content for the gallery event.
371        let (body, formatted) =
372            gallery.caption.map(|caption| (caption.body, caption.formatted)).unwrap_or_default();
373        let event_content = room
374            .make_media_event(
375                MessageType::Gallery(GalleryMessageEventContent::new(body, formatted, item_types)),
376                gallery.mentions,
377                gallery.reply,
378            )
379            .await
380            .map_err(|_| RoomSendQueueError::FailedToCreateGallery)?;
381
382        let created_at = MilliSecondsSinceUnixEpoch::now();
383
384        // Save requests in the queue storage.
385        self.inner
386            .queue
387            .push_gallery(
388                event_content.clone(),
389                send_event_txn.clone().into(),
390                created_at,
391                item_queue_infos,
392            )
393            .await?;
394
395        trace!("manager sends a gallery to the background task");
396
397        self.inner.notifier.notify_one();
398
399        let send_handle = SendHandle {
400            room: self.clone(),
401            transaction_id: send_event_txn.clone().into(),
402            media_handles,
403            created_at,
404        };
405
406        self.send_update(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
407            transaction_id: send_event_txn.clone().into(),
408            content: LocalEchoContent::Event {
409                serialized_event: SerializableEventContent::new(&event_content.into())
410                    .map_err(RoomSendQueueStorageError::JsonSerialization)?,
411                send_handle: send_handle.clone(),
412                send_error: None,
413            },
414        }));
415
416        Ok(send_handle)
417    }
418
419    async fn cache_media(
420        room: &Room,
421        data: Vec<u8>,
422        thumbnail: Option<Thumbnail>,
423        file_media_request: &MediaRequestParameters,
424    ) -> Result<MediaCacheResult, RoomSendQueueError> {
425        let client = room.client();
426        let media_store =
427            client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
428
429        // Cache the file itself in the cache store.
430        media_store
431            .add_media_content(
432                file_media_request,
433                data,
434                // Make sure that the file is stored until it has been uploaded.
435                IgnoreMediaRetentionPolicy::Yes,
436            )
437            .await
438            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
439
440        // Process the thumbnail, if it's been provided.
441        if let Some(thumbnail) = thumbnail {
442            let txn = TransactionId::new();
443            trace!(upload_thumbnail_txn = %txn, "media has a thumbnail");
444
445            // Create the information required for filling the thumbnail section of the
446            // event.
447            let (data, content_type, thumbnail_info) = thumbnail.into_parts();
448            let file_size = data.len();
449
450            let thumbnail_height = thumbnail_info.height;
451            let thumbnail_width = thumbnail_info.width;
452
453            // Cache thumbnail in the cache store.
454            let thumbnail_media_request = Media::make_local_file_media_request(&txn);
455            media_store
456                .add_media_content(
457                    &thumbnail_media_request,
458                    data,
459                    // Make sure that the thumbnail is stored until it has been uploaded.
460                    IgnoreMediaRetentionPolicy::Yes,
461                )
462                .await
463                .map_err(RoomSendQueueStorageError::MediaStoreError)?;
464
465            Ok(MediaCacheResult {
466                upload_thumbnail_txn: Some(txn.clone()),
467                event_thumbnail_info: Some((
468                    thumbnail_media_request.source.clone(),
469                    thumbnail_info,
470                )),
471                queue_thumbnail_info: Some(QueueThumbnailInfo {
472                    finish_upload_thumbnail_info: FinishUploadThumbnailInfo {
473                        txn,
474                        width: thumbnail_width,
475                        height: thumbnail_height,
476                    },
477                    media_request_parameters: thumbnail_media_request,
478                    content_type,
479                    file_size,
480                }),
481            })
482        } else {
483            Ok(Default::default())
484        }
485    }
486}
487
488/// Merge additional top-level fields into serialized event content.
489///
490/// Fields already present in the serialized content always win over extra
491/// fields with the same name.
492pub(super) fn merge_extra_content(
493    content: SerializableEventContent,
494    extra_content: Option<serde_json::Map<String, serde_json::Value>>,
495) -> Result<SerializableEventContent, RoomSendQueueStorageError> {
496    let Some(extra_content) = extra_content else {
497        return Ok(content);
498    };
499    if extra_content.is_empty() {
500        return Ok(content);
501    }
502
503    let (raw, event_type) = content.into_raw();
504    let mut object: serde_json::Map<String, serde_json::Value> =
505        raw.deserialize_as().map_err(RoomSendQueueStorageError::JsonSerialization)?;
506
507    for (key, value) in extra_content {
508        match object.entry(key) {
509            serde_json::map::Entry::Occupied(entry) => {
510                warn!(key = entry.key(), "extra content field shadowed by the event's own field");
511            }
512            serde_json::map::Entry::Vacant(entry) => {
513                entry.insert(value);
514            }
515        }
516    }
517
518    let raw = ruma::serde::Raw::from_json(
519        serde_json::value::to_raw_value(&object)
520            .map_err(RoomSendQueueStorageError::JsonSerialization)?,
521    );
522
523    Ok(SerializableEventContent::from_raw(raw, event_type))
524}
525
526impl QueueStorage {
527    /// Consumes a finished upload and queues sending of the final media event.
528    #[allow(clippy::too_many_arguments)]
529    pub(super) async fn handle_dependent_finish_upload(
530        &self,
531        client: &Client,
532        event_txn: OwnedTransactionId,
533        parent_key: SentRequestKey,
534        mut local_echo: RoomMessageEventContent,
535        file_upload_txn: OwnedTransactionId,
536        thumbnail_info: Option<FinishUploadThumbnailInfo>,
537        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
538        new_updates: &mut Vec<RoomSendQueueUpdate>,
539    ) -> Result<(), RoomSendQueueError> {
540        // Both uploads are ready: enqueue the event with its final data.
541        let sent_media = parent_key
542            .into_media()
543            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
544
545        update_media_cache_keys_after_upload(client, &file_upload_txn, thumbnail_info, &sent_media)
546            .await?;
547        update_media_event_after_upload(&mut local_echo, sent_media);
548
549        let new_content = merge_extra_content(
550            SerializableEventContent::new(&local_echo.into())
551                .map_err(RoomSendQueueStorageError::JsonSerialization)?,
552            extra_content,
553        )?;
554
555        // Indicates observers that the upload finished, by editing the local echo for
556        // the event into its final form before sending.
557        new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
558            transaction_id: event_txn.clone(),
559            new_content: new_content.clone(),
560        });
561
562        trace!(%event_txn, "queueing media event after successfully uploading media(s)");
563
564        client
565            .state_store()
566            .save_send_queue_request(
567                &self.room_id,
568                event_txn,
569                MilliSecondsSinceUnixEpoch::now(),
570                new_content.into(),
571                Self::HIGH_PRIORITY,
572            )
573            .await
574            .map_err(RoomSendQueueStorageError::StateStoreError)?;
575
576        Ok(())
577    }
578
579    /// Consumes a finished gallery upload and queues sending of the final
580    /// gallery event.
581    #[cfg(feature = "unstable-msc4274")]
582    #[allow(clippy::too_many_arguments)]
583    pub(super) async fn handle_dependent_finish_gallery_upload(
584        &self,
585        client: &Client,
586        event_txn: OwnedTransactionId,
587        parent_key: SentRequestKey,
588        mut local_echo: RoomMessageEventContent,
589        item_infos: Vec<FinishGalleryItemInfo>,
590        new_updates: &mut Vec<RoomSendQueueUpdate>,
591    ) -> Result<(), RoomSendQueueError> {
592        // All uploads are ready: enqueue the event with its final data.
593        let sent_gallery = parent_key
594            .into_media()
595            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
596
597        let mut sent_media_vec = sent_gallery.accumulated;
598        sent_media_vec.push(AccumulatedSentMediaInfo {
599            file: sent_gallery.file,
600            thumbnail: sent_gallery.thumbnail,
601        });
602
603        let mut sent_infos = HashMap::new();
604
605        for (item_info, sent_media) in zip(item_infos, sent_media_vec) {
606            let FinishGalleryItemInfo { file_upload: file_upload_txn, thumbnail_info } = item_info;
607
608            // Store the sent media under the original cache key for later insertion into
609            // the local echo.
610            let from_req = Media::make_local_file_media_request(&file_upload_txn);
611            sent_infos.insert(from_req.source.unique_key(), sent_media.clone());
612
613            update_media_cache_keys_after_upload(
614                client,
615                &file_upload_txn,
616                thumbnail_info,
617                &sent_media.into(),
618            )
619            .await?;
620        }
621
622        update_gallery_event_after_upload(&mut local_echo, sent_infos);
623
624        let new_content = SerializableEventContent::new(&local_echo.into())
625            .map_err(RoomSendQueueStorageError::JsonSerialization)?;
626
627        // Indicates observers that the upload finished, by editing the local echo for
628        // the event into its final form before sending.
629        new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
630            transaction_id: event_txn.clone(),
631            new_content: new_content.clone(),
632        });
633
634        trace!(%event_txn, "queueing media event after successfully uploading media(s)");
635
636        client
637            .state_store()
638            .save_send_queue_request(
639                &self.room_id,
640                event_txn,
641                MilliSecondsSinceUnixEpoch::now(),
642                new_content.into(),
643                Self::HIGH_PRIORITY,
644            )
645            .await
646            .map_err(RoomSendQueueStorageError::StateStoreError)?;
647
648        Ok(())
649    }
650
651    /// Consumes a finished file or thumbnail upload and queues the dependent
652    /// file or thumbnail upload.
653    #[allow(clippy::too_many_arguments)]
654    pub(super) async fn handle_dependent_file_or_thumbnail_upload(
655        &self,
656        client: &Client,
657        next_upload_txn: OwnedTransactionId,
658        parent_key: SentRequestKey,
659        content_type: String,
660        cache_key: MediaRequestParameters,
661        event_txn: OwnedTransactionId,
662        parent_is_thumbnail_upload: bool,
663    ) -> Result<(), RoomSendQueueError> {
664        // The previous file or thumbnail has been sent, now transform the dependent
665        // file or thumbnail upload request into a ready one.
666        let sent_media = parent_key
667            .into_media()
668            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
669
670        // If the previous upload was a thumbnail, it shouldn't have
671        // a thumbnail itself.
672        if parent_is_thumbnail_upload {
673            debug_assert!(sent_media.thumbnail.is_none());
674            if sent_media.thumbnail.is_some() {
675                warn!("unexpected thumbnail for a thumbnail!");
676            }
677        }
678
679        trace!(
680            related_to = %event_txn,
681            "done uploading file or thumbnail, now queuing the dependent file \
682             or thumbnail upload request",
683        );
684
685        // If the parent request was a thumbnail upload, don't add it to the list of
686        // accumulated medias yet because its dependent file upload is still
687        // pending. If the parent request was a file upload, we know that both
688        // the file and its thumbnail (if any) have finished uploading and we
689        // can add them to the accumulated sent media.
690        #[cfg(feature = "unstable-msc4274")]
691        let accumulated = if parent_is_thumbnail_upload {
692            sent_media.accumulated
693        } else {
694            let mut accumulated = sent_media.accumulated;
695            accumulated.push(AccumulatedSentMediaInfo {
696                file: sent_media.file.clone(),
697                thumbnail: sent_media.thumbnail,
698            });
699            accumulated
700        };
701
702        let request = QueuedRequestKind::MediaUpload {
703            content_type,
704            cache_key,
705            // If the previous upload was a thumbnail, it becomes the thumbnail source for the next
706            // upload.
707            thumbnail_source: parent_is_thumbnail_upload.then_some(sent_media.file),
708            related_to: event_txn,
709            #[cfg(feature = "unstable-msc4274")]
710            accumulated,
711        };
712
713        client
714            .state_store()
715            .save_send_queue_request(
716                &self.room_id,
717                next_upload_txn,
718                MilliSecondsSinceUnixEpoch::now(),
719                request,
720                Self::HIGH_PRIORITY,
721            )
722            .await
723            .map_err(RoomSendQueueStorageError::StateStoreError)?;
724
725        Ok(())
726    }
727
728    /// Try to abort an upload that would be ongoing.
729    ///
730    /// Return true if any media (media itself or its thumbnail) was being
731    /// uploaded. In this case, the media event has also been removed from
732    /// the send queue. If it returns false, then the uploads already
733    /// happened, and the event sending *may* have started.
734    #[instrument(skip(self, handles))]
735    pub(super) async fn abort_upload(
736        &self,
737        event_txn: &TransactionId,
738        handles: &MediaHandles,
739    ) -> Result<bool, RoomSendQueueStorageError> {
740        let mut guard = self.store.lock().await;
741        let client = guard.client()?;
742
743        // Keep the lock until we're done touching the storage.
744        debug!("trying to abort an upload");
745
746        let store = client.state_store();
747
748        let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone());
749        let event_as_dependent = ChildTransactionId::from(event_txn.to_owned());
750
751        let mut removed_dependent_upload = false;
752        let mut removed_dependent_event = false;
753
754        if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn
755            && store.remove_send_queue_request(&self.room_id, thumbnail_txn).await?
756        {
757            // The thumbnail upload existed as a request: either it was pending (something
758            // else was being sent), or it was actively being sent.
759            trace!("could remove thumbnail request, removing 2 dependent requests now");
760
761            // 1. Try to abort sending using the being_sent info, in case it was active.
762            if let Some(info) = guard.being_sent.as_ref()
763                && info.transaction_id == *thumbnail_txn
764            {
765                // SAFETY: we knew it was Some(), two lines above.
766                let info = guard.being_sent.take().unwrap();
767                if info.cancel_upload() {
768                    trace!("aborted ongoing thumbnail upload");
769                }
770            }
771
772            // 2. Remove the dependent requests.
773            removed_dependent_upload = store
774                .remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent)
775                .await?;
776
777            if !removed_dependent_upload {
778                warn!("unable to find the dependent file upload request");
779            }
780
781            removed_dependent_event =
782                store.remove_dependent_queued_request(&self.room_id, &event_as_dependent).await?;
783
784            if !removed_dependent_event {
785                warn!("unable to find the dependent media event upload request");
786            }
787        }
788
789        // If we're here:
790        // - either there was no thumbnail to upload,
791        // - or the thumbnail request has terminated already.
792        //
793        // So the next target is the upload request itself, in both cases.
794
795        if !removed_dependent_upload {
796            if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
797                // The upload existed as a request: either it was pending (something else was
798                // being sent), or it was actively being sent.
799                trace!("could remove file upload request, removing 1 dependent request");
800
801                // 1. Try to abort sending using the being_sent info, in case it was active.
802                if let Some(info) = guard.being_sent.as_ref()
803                    && info.transaction_id == handles.upload_file_txn
804                {
805                    // SAFETY: we knew it was Some(), two lines above.
806                    let info = guard.being_sent.take().unwrap();
807                    if info.cancel_upload() {
808                        trace!("aborted ongoing file upload");
809                    }
810                }
811
812                // 2. Remove the dependent request.
813                if !store
814                    .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
815                    .await?
816                {
817                    warn!("unable to find the dependent media event upload request");
818                }
819            } else {
820                // The upload was not in the send queue, so it's completed.
821                //
822                // It means the event sending is either still queued as a dependent request, or
823                // it's graduated into a request.
824                if !removed_dependent_event
825                    && !store
826                        .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
827                        .await?
828                {
829                    // The media event has been promoted into a request, or the promoted request
830                    // has been sent already: we couldn't abort, let the caller decide what to do.
831                    debug!("uploads already happened => deferring to aborting an event sending");
832                    return Ok(false);
833                }
834            }
835        }
836
837        // At this point, all the requests and dependent requests have been cleaned up.
838        // Perform the final step: empty the cache from the local items.
839        {
840            let media_store = client.media_store().lock().await?;
841            media_store
842                .remove_media_content_for_uri(&Media::make_local_uri(&handles.upload_file_txn))
843                .await?;
844            if let Some(txn) = &handles.upload_thumbnail_txn {
845                media_store.remove_media_content_for_uri(&Media::make_local_uri(txn)).await?;
846            }
847        }
848
849        debug!("successfully aborted!");
850        Ok(true)
851    }
852
853    #[instrument(skip(self, caption, formatted_caption))]
854    pub(super) async fn edit_media_caption(
855        &self,
856        txn: &TransactionId,
857        caption: Option<String>,
858        formatted_caption: Option<FormattedBody>,
859        mentions: Option<Mentions>,
860    ) -> Result<Option<AnyMessageLikeEventContent>, RoomSendQueueStorageError> {
861        // This error will be popular here.
862        use RoomSendQueueStorageError::InvalidMediaCaptionEdit;
863
864        let guard = self.store.lock().await;
865        let client = guard.client()?;
866        let store = client.state_store();
867
868        // The media event can be in one of three states:
869        // - still stored as a dependent request,
870        // - stored as a queued request, active (aka it's being sent).
871        // - stored as a queued request, not active yet (aka it's not being sent yet),
872        //
873        // We'll handle each of these cases one by one.
874
875        {
876            // If the event can be found as a dependent event, update the captions, save it
877            // back into the database, and return early.
878            let dependent_requests = store.load_dependent_queued_requests(&self.room_id).await?;
879
880            if let Some(found) =
881                dependent_requests.into_iter().find(|req| *req.own_transaction_id == *txn)
882            {
883                trace!("found the caption to edit in a dependent request");
884
885                let DependentQueuedRequestKind::FinishUpload {
886                    mut local_echo,
887                    file_upload,
888                    thumbnail_info,
889                    extra_content,
890                } = found.kind
891                else {
892                    return Err(InvalidMediaCaptionEdit);
893                };
894
895                if !update_media_caption(&mut local_echo, caption, formatted_caption, mentions) {
896                    return Err(InvalidMediaCaptionEdit);
897                }
898
899                let new_dependent_request = DependentQueuedRequestKind::FinishUpload {
900                    local_echo: local_echo.clone(),
901                    file_upload,
902                    thumbnail_info,
903                    extra_content,
904                };
905                store
906                    .update_dependent_queued_request(
907                        &self.room_id,
908                        &found.own_transaction_id,
909                        new_dependent_request,
910                    )
911                    .await?;
912
913                trace!("caption successfully updated");
914                return Ok(Some((*local_echo).into()));
915            }
916        }
917
918        let requests = store.load_send_queue_requests(&self.room_id).await?;
919        let Some(found) = requests.into_iter().find(|req| req.transaction_id == *txn) else {
920            // Couldn't be found anymore, it's not possible to update captions.
921            return Ok(None);
922        };
923
924        trace!("found the caption to edit as a request");
925
926        let QueuedRequestKind::Event { content: serialized_content } = found.kind else {
927            return Err(InvalidMediaCaptionEdit);
928        };
929
930        let deserialized = serialized_content.deserialize()?;
931        let AnyMessageLikeEventContent::RoomMessage(mut content) = deserialized else {
932            return Err(InvalidMediaCaptionEdit);
933        };
934
935        if !update_media_caption(&mut content, caption, formatted_caption, mentions) {
936            return Err(InvalidMediaCaptionEdit);
937        }
938
939        let any_content: AnyMessageLikeEventContent = content.into();
940        let new_serialized = SerializableEventContent::new(&any_content.clone())?;
941
942        // If the request is active (being sent), send a dependent request.
943        if let Some(being_sent) = guard.being_sent.as_ref()
944            && being_sent.transaction_id == *txn
945        {
946            // Record a dependent request to edit, and exit.
947            store
948                .save_dependent_queued_request(
949                    &self.room_id,
950                    txn,
951                    ChildTransactionId::new(),
952                    MilliSecondsSinceUnixEpoch::now(),
953                    DependentQueuedRequestKind::EditEvent { new_content: new_serialized },
954                )
955                .await?;
956
957            trace!("media event was being sent, pushed a dependent edit");
958            return Ok(Some(any_content));
959        }
960
961        // The request is not active: edit the local echo.
962        store
963            .update_send_queue_request(
964                &self.room_id,
965                txn,
966                QueuedRequestKind::Event { content: new_serialized },
967            )
968            .await?;
969
970        trace!("media event was not being sent, updated local echo");
971        Ok(Some(any_content))
972    }
973}
974
975/// Update cache keys in the cache store after uploading a media file /
976/// thumbnail.
977async fn update_media_cache_keys_after_upload(
978    client: &Client,
979    file_upload_txn: &OwnedTransactionId,
980    thumbnail_info: Option<FinishUploadThumbnailInfo>,
981    sent_media: &SentMediaInfo,
982) -> Result<(), RoomSendQueueError> {
983    // Do it for the file itself.
984    let from_req = Media::make_local_file_media_request(file_upload_txn);
985
986    trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store");
987    let media_store =
988        client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
989
990    // The media file can now be removed during cleanups.
991    media_store
992        .set_ignore_media_retention_policy(&from_req, IgnoreMediaRetentionPolicy::No)
993        .await
994        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
995
996    media_store
997        .replace_media_key(
998            &from_req,
999            &MediaRequestParameters { source: sent_media.file.clone(), format: MediaFormat::File },
1000        )
1001        .await
1002        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1003
1004    // Rename the thumbnail too, if needs be.
1005    if let Some((info, remote_thumbnail_source)) =
1006        thumbnail_info.as_ref().zip(sent_media.thumbnail.clone())
1007    {
1008        let from_request_params = Media::make_local_file_media_request(&info.txn);
1009
1010        if let Some((height, width)) = info.height.zip(info.width) {
1011            trace!(
1012                from = ?from_req.source,
1013                to = ?remote_thumbnail_source,
1014                height = u64::from(height),
1015                width = u64::from(width),
1016                "storing thumbnail as a thumbnail for the uploaded media, and a file in itself, in cache store"
1017            );
1018
1019            // Try to reload the content of the thumbnail from the media store. As it's not
1020            // a strong requirement to store the thumbnail a second time, we
1021            // silently log errors instead of propagating them to the caller.
1022            match media_store
1023                .get_media_content(&MediaRequestParameters {
1024                    source: from_request_params.source.clone(),
1025                    format: MediaFormat::File,
1026                })
1027                .await
1028            {
1029                Ok(Some(thumbnail_content)) => {
1030                    // Also cache this as a thumbnail for the thumbnail, in the media store; the
1031                    // ElementX apps expect that specific format for thumbnails.
1032                    media_store
1033                        .add_media_content(
1034                            &MediaRequestParameters {
1035                                source: remote_thumbnail_source.clone(),
1036                                format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
1037                                    width, height,
1038                                )),
1039                            },
1040                            thumbnail_content,
1041                            IgnoreMediaRetentionPolicy::No,
1042                        )
1043                        .await
1044                        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1045                }
1046
1047                Ok(None) => {
1048                    warn!(
1049                        from = ?from_request_params.source,
1050                        "unable to reload thumbnail content from media store: no content found",
1051                    );
1052                }
1053
1054                Err(err) => {
1055                    // Silently log the error, but proceed, as storing the thumbnail as such isn't
1056                    // a strong requirement.
1057                    error!(
1058                        from = ?from_request_params.source,
1059                        "unable to reload thumbnail content from media store: {err}"
1060                    );
1061                }
1062            }
1063        } else {
1064            trace!(from = ?from_req.source, to = ?remote_thumbnail_source, "only renaming thumbnail key to file in cache store");
1065        }
1066
1067        // The thumbnail file can now be removed during cleanups.
1068        media_store
1069            .set_ignore_media_retention_policy(&from_request_params, IgnoreMediaRetentionPolicy::No)
1070            .await
1071            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1072
1073        // Save the thumbnail as a file as well.
1074        media_store
1075            .replace_media_key(
1076                &from_request_params,
1077                &MediaRequestParameters {
1078                    source: remote_thumbnail_source,
1079                    format: MediaFormat::File,
1080                },
1081            )
1082            .await
1083            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1084    }
1085
1086    Ok(())
1087}