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 canonical
76    // copy of the new content inside the replacement relation; patch it too,
77    // 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 event.
324        // The upload chain doesn't care about the relation; once the upload is
325        // done, `update_media_event_after_upload` patches both copies of the
326        // 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
418        Span::current().record("event_txn", tracing::field::display(&*send_event_txn));
419
420        let mut item_types = Vec::with_capacity(gallery.len());
421        let mut item_queue_infos = Vec::with_capacity(gallery.len());
422        let mut media_handles = Vec::with_capacity(gallery.len());
423
424        for item_info in gallery.items {
425            let GalleryItemInfo { filename, content_type, data, .. } = item_info;
426
427            let upload_file_txn = TransactionId::new();
428
429            debug!(filename, %content_type, %upload_file_txn, "uploading a gallery attachment");
430
431            let file_media_request = Media::make_local_file_media_request(&upload_file_txn);
432
433            let MediaCacheResult {
434                upload_thumbnail_txn,
435                event_thumbnail_info,
436                queue_thumbnail_info,
437            } = RoomSendQueue::cache_media(&room, data, item_info.thumbnail, &file_media_request)
438                .await?;
439
440            item_types.push(Room::make_gallery_item_type(
441                &content_type,
442                filename,
443                file_media_request.source.clone(),
444                item_info.caption,
445                Some(item_info.attachment_info),
446                event_thumbnail_info,
447            ));
448
449            item_queue_infos.push(GalleryItemQueueInfo {
450                content_type,
451                upload_file_txn: upload_file_txn.clone(),
452                file_media_request,
453                thumbnail: queue_thumbnail_info,
454            });
455
456            media_handles.push(MediaHandles { upload_file_txn, upload_thumbnail_txn });
457        }
458
459        // Create the content for the gallery event.
460        let (body, formatted) =
461            gallery.caption.map(|caption| (caption.body, caption.formatted)).unwrap_or_default();
462        let event_content = room
463            .make_media_event(
464                MessageType::Gallery(GalleryMessageEventContent::new(body, formatted, item_types)),
465                gallery.mentions,
466                gallery.reply,
467            )
468            .await
469            .map_err(|_| RoomSendQueueError::FailedToCreateGallery)?;
470
471        let created_at = MilliSecondsSinceUnixEpoch::now();
472
473        // Save requests in the queue storage.
474        self.inner
475            .queue
476            .push_gallery(
477                event_content.clone(),
478                send_event_txn.clone().into(),
479                created_at,
480                item_queue_infos,
481            )
482            .await?;
483
484        trace!("manager sends a gallery to the background task");
485
486        self.inner.notifier.notify_one();
487
488        let send_handle = SendHandle {
489            room: self.clone(),
490            transaction_id: send_event_txn.clone().into(),
491            media_handles,
492            created_at,
493        };
494
495        self.send_update(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
496            transaction_id: send_event_txn.clone().into(),
497            content: LocalEchoContent::Event {
498                serialized_event: SerializableEventContent::new(&event_content.into())
499                    .map_err(RoomSendQueueStorageError::JsonSerialization)?,
500                send_handle: send_handle.clone(),
501                send_error: None,
502            },
503        }));
504
505        Ok(send_handle)
506    }
507
508    async fn cache_media(
509        room: &Room,
510        data: Vec<u8>,
511        thumbnail: Option<Thumbnail>,
512        file_media_request: &MediaRequestParameters,
513    ) -> Result<MediaCacheResult, RoomSendQueueError> {
514        let client = room.client();
515        let media_store =
516            client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
517
518        // Cache the file itself in the cache store.
519        media_store
520            .add_media_content(
521                file_media_request,
522                data,
523                // Make sure that the file is stored until it has been uploaded.
524                IgnoreMediaRetentionPolicy::Yes,
525            )
526            .await
527            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
528
529        // Process the thumbnail, if it's been provided.
530        if let Some(thumbnail) = thumbnail {
531            let txn = TransactionId::new();
532            trace!(upload_thumbnail_txn = %txn, "media has a thumbnail");
533
534            // Create the information required for filling the thumbnail section
535            // of the event.
536            let (data, content_type, thumbnail_info) = thumbnail.into_parts();
537            let file_size = data.len();
538
539            let thumbnail_height = thumbnail_info.height;
540            let thumbnail_width = thumbnail_info.width;
541
542            // Cache thumbnail in the cache store.
543            let thumbnail_media_request = Media::make_local_file_media_request(&txn);
544            media_store
545                .add_media_content(
546                    &thumbnail_media_request,
547                    data,
548                    // Make sure that the thumbnail is stored until it has been uploaded.
549                    IgnoreMediaRetentionPolicy::Yes,
550                )
551                .await
552                .map_err(RoomSendQueueStorageError::MediaStoreError)?;
553
554            Ok(MediaCacheResult {
555                upload_thumbnail_txn: Some(txn.clone()),
556                event_thumbnail_info: Some((
557                    thumbnail_media_request.source.clone(),
558                    thumbnail_info,
559                )),
560                queue_thumbnail_info: Some(QueueThumbnailInfo {
561                    finish_upload_thumbnail_info: FinishUploadThumbnailInfo {
562                        txn,
563                        width: thumbnail_width,
564                        height: thumbnail_height,
565                    },
566                    media_request_parameters: thumbnail_media_request,
567                    content_type,
568                    file_size,
569                }),
570            })
571        } else {
572            Ok(Default::default())
573        }
574    }
575}
576
577/// Merge additional top-level fields into serialized event content.
578///
579/// Fields already present in the serialized content always win over extra
580/// fields with the same name.
581pub(super) fn merge_extra_content(
582    content: SerializableEventContent,
583    extra_content: Option<serde_json::Map<String, serde_json::Value>>,
584) -> Result<SerializableEventContent, RoomSendQueueStorageError> {
585    let Some(extra_content) = extra_content else {
586        return Ok(content);
587    };
588    if extra_content.is_empty() {
589        return Ok(content);
590    }
591
592    let (raw, event_type) = content.into_raw();
593    let mut object: serde_json::Map<String, serde_json::Value> =
594        raw.deserialize_as().map_err(RoomSendQueueStorageError::JsonSerialization)?;
595
596    for (key, value) in extra_content {
597        match object.entry(key) {
598            serde_json::map::Entry::Occupied(entry) => {
599                warn!(key = entry.key(), "extra content field shadowed by the event's own field");
600            }
601            serde_json::map::Entry::Vacant(entry) => {
602                entry.insert(value);
603            }
604        }
605    }
606
607    let raw = ruma::serde::Raw::from_json(
608        serde_json::value::to_raw_value(&object)
609            .map_err(RoomSendQueueStorageError::JsonSerialization)?,
610    );
611
612    Ok(SerializableEventContent::from_raw(raw, event_type))
613}
614
615impl QueueStorage {
616    /// Consumes a finished upload and queues sending of the final media event.
617    #[allow(clippy::too_many_arguments)]
618    pub(super) async fn handle_dependent_finish_upload(
619        &self,
620        client: &Client,
621        event_txn: OwnedTransactionId,
622        parent_key: SentRequestKey,
623        mut local_echo: RoomMessageEventContent,
624        file_upload_txn: OwnedTransactionId,
625        thumbnail_info: Option<FinishUploadThumbnailInfo>,
626        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
627        new_updates: &mut Vec<RoomSendQueueUpdate>,
628    ) -> Result<(), RoomSendQueueError> {
629        // Both uploads are ready: enqueue the event with its final data.
630        let sent_media = parent_key
631            .into_media()
632            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
633
634        update_media_cache_keys_after_upload(client, &file_upload_txn, thumbnail_info, &sent_media)
635            .await?;
636        update_media_event_after_upload(&mut local_echo, sent_media);
637
638        let new_content = merge_extra_content(
639            SerializableEventContent::new(&local_echo.into())
640                .map_err(RoomSendQueueStorageError::JsonSerialization)?,
641            extra_content,
642        )?;
643
644        // Indicates observers that the upload finished, by editing the local
645        // echo for the event into its final form before sending.
646        new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
647            transaction_id: event_txn.clone(),
648            new_content: new_content.clone(),
649        });
650
651        trace!(%event_txn, "queueing media event after successfully uploading media(s)");
652
653        client
654            .state_store()
655            .save_send_queue_request(
656                &self.room_id,
657                event_txn,
658                MilliSecondsSinceUnixEpoch::now(),
659                new_content.into(),
660                Self::HIGH_PRIORITY,
661            )
662            .await
663            .map_err(RoomSendQueueStorageError::StateStoreError)?;
664
665        Ok(())
666    }
667
668    /// Consumes a finished gallery upload and queues sending of the final
669    /// gallery event.
670    #[cfg(feature = "unstable-msc4274")]
671    #[allow(clippy::too_many_arguments)]
672    pub(super) async fn handle_dependent_finish_gallery_upload(
673        &self,
674        client: &Client,
675        event_txn: OwnedTransactionId,
676        parent_key: SentRequestKey,
677        mut local_echo: RoomMessageEventContent,
678        item_infos: Vec<FinishGalleryItemInfo>,
679        new_updates: &mut Vec<RoomSendQueueUpdate>,
680    ) -> Result<(), RoomSendQueueError> {
681        // All uploads are ready: enqueue the event with its final data.
682        let sent_gallery = parent_key
683            .into_media()
684            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
685
686        let mut sent_media_vec = sent_gallery.accumulated;
687        sent_media_vec.push(AccumulatedSentMediaInfo {
688            file: sent_gallery.file,
689            thumbnail: sent_gallery.thumbnail,
690        });
691
692        let mut sent_infos = HashMap::new();
693
694        for (item_info, sent_media) in zip(item_infos, sent_media_vec) {
695            let FinishGalleryItemInfo { file_upload: file_upload_txn, thumbnail_info } = item_info;
696
697            // Store the sent media under the original cache key for later
698            // insertion into the local echo.
699            let from_req = Media::make_local_file_media_request(&file_upload_txn);
700            sent_infos.insert(from_req.source.unique_key(), sent_media.clone());
701
702            update_media_cache_keys_after_upload(
703                client,
704                &file_upload_txn,
705                thumbnail_info,
706                &sent_media.into(),
707            )
708            .await?;
709        }
710
711        update_gallery_event_after_upload(&mut local_echo, sent_infos);
712
713        let new_content = SerializableEventContent::new(&local_echo.into())
714            .map_err(RoomSendQueueStorageError::JsonSerialization)?;
715
716        // Indicates observers that the upload finished, by editing the local
717        // echo for the event into its final form before sending.
718        new_updates.push(RoomSendQueueUpdate::ReplacedLocalEvent {
719            transaction_id: event_txn.clone(),
720            new_content: new_content.clone(),
721        });
722
723        trace!(%event_txn, "queueing media event after successfully uploading media(s)");
724
725        client
726            .state_store()
727            .save_send_queue_request(
728                &self.room_id,
729                event_txn,
730                MilliSecondsSinceUnixEpoch::now(),
731                new_content.into(),
732                Self::HIGH_PRIORITY,
733            )
734            .await
735            .map_err(RoomSendQueueStorageError::StateStoreError)?;
736
737        Ok(())
738    }
739
740    /// Consumes a finished file or thumbnail upload and queues the dependent
741    /// file or thumbnail upload.
742    #[allow(clippy::too_many_arguments)]
743    pub(super) async fn handle_dependent_file_or_thumbnail_upload(
744        &self,
745        client: &Client,
746        next_upload_txn: OwnedTransactionId,
747        parent_key: SentRequestKey,
748        content_type: String,
749        cache_key: MediaRequestParameters,
750        event_txn: OwnedTransactionId,
751        parent_is_thumbnail_upload: bool,
752    ) -> Result<(), RoomSendQueueError> {
753        // The previous file or thumbnail has been sent, now transform the
754        // dependent file or thumbnail upload request into a ready one.
755        let sent_media = parent_key
756            .into_media()
757            .ok_or(RoomSendQueueError::StorageError(RoomSendQueueStorageError::InvalidParentKey))?;
758
759        // If the previous upload was a thumbnail, it shouldn't have a thumbnail
760        // itself.
761        if parent_is_thumbnail_upload {
762            debug_assert!(sent_media.thumbnail.is_none());
763            if sent_media.thumbnail.is_some() {
764                warn!("unexpected thumbnail for a thumbnail!");
765            }
766        }
767
768        trace!(
769            related_to = %event_txn,
770            "done uploading file or thumbnail, now queuing the dependent file \
771             or thumbnail upload request",
772        );
773
774        // If the parent request was a thumbnail upload, don't add it to the
775        // list of accumulated medias yet because its dependent file upload is
776        // still pending. If the parent request was a file upload, we know that
777        // both the file and its thumbnail (if any) have finished uploading and
778        // we can add them to the accumulated sent media.
779        #[cfg(feature = "unstable-msc4274")]
780        let accumulated = if parent_is_thumbnail_upload {
781            sent_media.accumulated
782        } else {
783            let mut accumulated = sent_media.accumulated;
784            accumulated.push(AccumulatedSentMediaInfo {
785                file: sent_media.file.clone(),
786                thumbnail: sent_media.thumbnail,
787            });
788            accumulated
789        };
790
791        let request = QueuedRequestKind::MediaUpload {
792            content_type,
793            cache_key,
794            // If the previous upload was a thumbnail, it becomes the thumbnail
795            // source for the next upload.
796            thumbnail_source: parent_is_thumbnail_upload.then_some(sent_media.file),
797            related_to: event_txn,
798            #[cfg(feature = "unstable-msc4274")]
799            accumulated,
800        };
801
802        client
803            .state_store()
804            .save_send_queue_request(
805                &self.room_id,
806                next_upload_txn,
807                MilliSecondsSinceUnixEpoch::now(),
808                request,
809                Self::HIGH_PRIORITY,
810            )
811            .await
812            .map_err(RoomSendQueueStorageError::StateStoreError)?;
813
814        Ok(())
815    }
816
817    /// Try to abort an upload that would be ongoing.
818    ///
819    /// Return true if any media (media itself or its thumbnail) was being
820    /// uploaded. In this case, the media event has also been removed from the
821    /// send queue. If it returns false, then the uploads already happened, and
822    /// the event sending _may_ have started.
823    #[instrument(skip(self, handles))]
824    pub(super) async fn abort_upload(
825        &self,
826        event_txn: &TransactionId,
827        handles: &MediaHandles,
828    ) -> Result<bool, RoomSendQueueStorageError> {
829        let mut guard = self.store.lock().await;
830        let client = guard.client()?;
831
832        // Keep the lock until we're done touching the storage.
833        debug!("trying to abort an upload");
834
835        let store = client.state_store();
836
837        let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone());
838        let event_as_dependent = ChildTransactionId::from(event_txn.to_owned());
839
840        let mut removed_dependent_upload = false;
841        let mut removed_dependent_event = false;
842
843        if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn
844            && store.remove_send_queue_request(&self.room_id, thumbnail_txn).await?
845        {
846            // The thumbnail upload existed as a request: either it was pending
847            // (something else was being sent), or it was actively being sent.
848            trace!("could remove thumbnail request, removing 2 dependent requests now");
849
850            // 1. Try to abort sending using the being_sent info, in case it was active.
851            if let Some(info) = guard.being_sent.as_ref()
852                && info.transaction_id == *thumbnail_txn
853            {
854                // SAFETY: we knew it was Some(), two lines above.
855                let info = guard.being_sent.take().unwrap();
856                if info.cancel_upload() {
857                    trace!("aborted ongoing thumbnail upload");
858                }
859            }
860
861            // 2. Remove the dependent requests.
862            removed_dependent_upload = store
863                .remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent)
864                .await?;
865
866            if !removed_dependent_upload {
867                warn!("unable to find the dependent file upload request");
868            }
869
870            removed_dependent_event =
871                store.remove_dependent_queued_request(&self.room_id, &event_as_dependent).await?;
872
873            if !removed_dependent_event {
874                warn!("unable to find the dependent media event upload request");
875            }
876        }
877
878        // If we're here:
879        //
880        // - either there was no thumbnail to upload,
881        // - or the thumbnail request has terminated already.
882        //
883        // So the next target is the upload request itself, in both cases.
884
885        if !removed_dependent_upload {
886            if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
887                // The upload existed as a request: either it was pending
888                // (something else was being sent), or it was actively being
889                // sent.
890                trace!("could remove file upload request, removing 1 dependent request");
891
892                // 1. Try to abort sending using the being_sent info, in case it was active.
893                if let Some(info) = guard.being_sent.as_ref()
894                    && info.transaction_id == handles.upload_file_txn
895                {
896                    // SAFETY: we knew it was Some(), two lines above.
897                    let info = guard.being_sent.take().unwrap();
898                    if info.cancel_upload() {
899                        trace!("aborted ongoing file upload");
900                    }
901                }
902
903                // 2. Remove the dependent request.
904                if !store
905                    .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
906                    .await?
907                {
908                    warn!("unable to find the dependent media event upload request");
909                }
910            } else {
911                // The upload was not in the send queue, so it's completed.
912                //
913                // It means the event sending is either still queued as a
914                // dependent request, or it's graduated into a request.
915                if !removed_dependent_event
916                    && !store
917                        .remove_dependent_queued_request(&self.room_id, &event_as_dependent)
918                        .await?
919                {
920                    // The media event has been promoted into a request, or the
921                    // promoted request has been sent already: we couldn't
922                    // abort, let the caller decide what to do.
923                    debug!("uploads already happened => deferring to aborting an event sending");
924                    return Ok(false);
925                }
926            }
927        }
928
929        // At this point, all the requests and dependent requests have been
930        // cleaned up. Perform the final step: empty the cache from the local
931        // items.
932        {
933            let media_store = client.media_store().lock().await?;
934            media_store
935                .remove_media_content_for_uri(&Media::make_local_uri(&handles.upload_file_txn))
936                .await?;
937            if let Some(txn) = &handles.upload_thumbnail_txn {
938                media_store.remove_media_content_for_uri(&Media::make_local_uri(txn)).await?;
939            }
940        }
941
942        debug!("successfully aborted!");
943        Ok(true)
944    }
945
946    #[instrument(skip(self, caption, formatted_caption))]
947    pub(super) async fn edit_media_caption(
948        &self,
949        txn: &TransactionId,
950        caption: Option<String>,
951        formatted_caption: Option<FormattedBody>,
952        mentions: Option<Mentions>,
953    ) -> Result<Option<AnyMessageLikeEventContent>, RoomSendQueueStorageError> {
954        // This error will be popular here.
955        use RoomSendQueueStorageError::InvalidMediaCaptionEdit;
956
957        let guard = self.store.lock().await;
958        let client = guard.client()?;
959        let store = client.state_store();
960
961        // The media event can be in one of three states:
962        //
963        // - still stored as a dependent request,
964        // - stored as a queued request, active (aka it's being sent).
965        // - stored as a queued request, not active yet (aka it's not being sent yet),
966        //
967        // We'll handle each of these cases one by one.
968
969        {
970            // If the event can be found as a dependent event, update the
971            // captions, save it back into the database, and return early.
972            let dependent_requests = store.load_dependent_queued_requests(&self.room_id).await?;
973
974            if let Some(found) =
975                dependent_requests.into_iter().find(|req| *req.own_transaction_id == *txn)
976            {
977                trace!("found the caption to edit in a dependent request");
978
979                let DependentQueuedRequestKind::FinishUpload {
980                    mut local_echo,
981                    file_upload,
982                    thumbnail_info,
983                    extra_content,
984                } = found.kind
985                else {
986                    return Err(InvalidMediaCaptionEdit);
987                };
988
989                if !update_media_caption(&mut local_echo, caption, formatted_caption, mentions) {
990                    return Err(InvalidMediaCaptionEdit);
991                }
992
993                let new_dependent_request = DependentQueuedRequestKind::FinishUpload {
994                    local_echo: local_echo.clone(),
995                    file_upload,
996                    thumbnail_info,
997                    extra_content,
998                };
999                store
1000                    .update_dependent_queued_request(
1001                        &self.room_id,
1002                        &found.own_transaction_id,
1003                        new_dependent_request,
1004                    )
1005                    .await?;
1006
1007                trace!("caption successfully updated");
1008                return Ok(Some((*local_echo).into()));
1009            }
1010        }
1011
1012        let requests = store.load_send_queue_requests(&self.room_id).await?;
1013        let Some(found) = requests.into_iter().find(|req| req.transaction_id == *txn) else {
1014            // Couldn't be found anymore, it's not possible to update captions.
1015            return Ok(None);
1016        };
1017
1018        trace!("found the caption to edit as a request");
1019
1020        let QueuedRequestKind::Event { content: serialized_content, .. } = found.kind else {
1021            return Err(InvalidMediaCaptionEdit);
1022        };
1023
1024        let deserialized = serialized_content.deserialize()?;
1025        let AnyMessageLikeEventContent::RoomMessage(mut content) = deserialized else {
1026            return Err(InvalidMediaCaptionEdit);
1027        };
1028
1029        if !update_media_caption(&mut content, caption, formatted_caption, mentions) {
1030            return Err(InvalidMediaCaptionEdit);
1031        }
1032
1033        let any_content: AnyMessageLikeEventContent = content.into();
1034        let new_serialized = SerializableEventContent::new(&any_content.clone())?;
1035
1036        // If the request is active (being sent), send a dependent request.
1037        if let Some(being_sent) = guard.being_sent.as_ref()
1038            && being_sent.transaction_id == *txn
1039        {
1040            // Record a dependent request to edit, and exit.
1041            store
1042                .save_dependent_queued_request(
1043                    &self.room_id,
1044                    txn,
1045                    ChildTransactionId::new(),
1046                    MilliSecondsSinceUnixEpoch::now(),
1047                    DependentQueuedRequestKind::EditEvent { new_content: new_serialized },
1048                )
1049                .await?;
1050
1051            trace!("media event was being sent, pushed a dependent edit");
1052            return Ok(Some(any_content));
1053        }
1054
1055        // The request is not active: edit the local echo.
1056        store.update_send_queue_request(&self.room_id, txn, new_serialized.into()).await?;
1057
1058        trace!("media event was not being sent, updated local echo");
1059        Ok(Some(any_content))
1060    }
1061}
1062
1063/// Update cache keys in the cache store after uploading a media file /
1064/// thumbnail.
1065async fn update_media_cache_keys_after_upload(
1066    client: &Client,
1067    file_upload_txn: &OwnedTransactionId,
1068    thumbnail_info: Option<FinishUploadThumbnailInfo>,
1069    sent_media: &SentMediaInfo,
1070) -> Result<(), RoomSendQueueError> {
1071    // Do it for the file itself.
1072    let from_req = Media::make_local_file_media_request(file_upload_txn);
1073
1074    trace!(from = ?from_req.source, to = ?sent_media.file, "renaming media file key in cache store");
1075    let media_store =
1076        client.media_store().lock().await.map_err(RoomSendQueueStorageError::LockError)?;
1077
1078    // The media file can now be removed during cleanups.
1079    media_store
1080        .set_ignore_media_retention_policy(&from_req, IgnoreMediaRetentionPolicy::No)
1081        .await
1082        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1083
1084    media_store
1085        .replace_media_key(
1086            &from_req,
1087            &MediaRequestParameters { source: sent_media.file.clone(), format: MediaFormat::File },
1088        )
1089        .await
1090        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1091
1092    // Rename the thumbnail too, if needs be.
1093    if let Some((info, remote_thumbnail_source)) =
1094        thumbnail_info.as_ref().zip(sent_media.thumbnail.clone())
1095    {
1096        let from_request_params = Media::make_local_file_media_request(&info.txn);
1097
1098        if let Some((height, width)) = info.height.zip(info.width) {
1099            trace!(
1100                from = ?from_req.source,
1101                to = ?remote_thumbnail_source,
1102                height = u64::from(height),
1103                width = u64::from(width),
1104                "storing thumbnail as a thumbnail for the uploaded media, and a file in itself, in cache store"
1105            );
1106
1107            // Try to reload the content of the thumbnail from the media store.
1108            // As it's not a strong requirement to store the thumbnail a second
1109            // time, we silently log errors instead of propagating them to the
1110            // caller.
1111            match media_store
1112                .get_media_content(&MediaRequestParameters {
1113                    source: from_request_params.source.clone(),
1114                    format: MediaFormat::File,
1115                })
1116                .await
1117            {
1118                Ok(Some(thumbnail_content)) => {
1119                    // Also cache this as a thumbnail for the thumbnail, in the
1120                    // media store; the ElementX apps expect that specific
1121                    // format for thumbnails.
1122                    media_store
1123                        .add_media_content(
1124                            &MediaRequestParameters {
1125                                source: remote_thumbnail_source.clone(),
1126                                format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
1127                                    width, height,
1128                                )),
1129                            },
1130                            thumbnail_content,
1131                            IgnoreMediaRetentionPolicy::No,
1132                        )
1133                        .await
1134                        .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1135                }
1136
1137                Ok(None) => {
1138                    warn!(
1139                        from = ?from_request_params.source,
1140                        "unable to reload thumbnail content from media store: no content found",
1141                    );
1142                }
1143
1144                Err(err) => {
1145                    // Silently log the error, but proceed, as storing the
1146                    // thumbnail as such isn't a strong requirement.
1147                    error!(
1148                        from = ?from_request_params.source,
1149                        "unable to reload thumbnail content from media store: {err}"
1150                    );
1151                }
1152            }
1153        } else {
1154            trace!(from = ?from_req.source, to = ?remote_thumbnail_source, "only renaming thumbnail key to file in cache store");
1155        }
1156
1157        // The thumbnail file can now be removed during cleanups.
1158        media_store
1159            .set_ignore_media_retention_policy(&from_request_params, IgnoreMediaRetentionPolicy::No)
1160            .await
1161            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1162
1163        // Save the thumbnail as a file as well.
1164        media_store
1165            .replace_media_key(
1166                &from_request_params,
1167                &MediaRequestParameters {
1168                    source: remote_thumbnail_source,
1169                    format: MediaFormat::File,
1170                },
1171            )
1172            .await
1173            .map_err(RoomSendQueueStorageError::MediaStoreError)?;
1174    }
1175
1176    Ok(())
1177}