Skip to main content

matrix_sdk_base/store/
send_queue.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//! All data types related to the send queue.
16
17use std::{collections::BTreeMap, fmt, ops::Deref};
18
19use as_variant::as_variant;
20#[cfg(feature = "unstable-msc4354")]
21use ruma::events::sticky::StickyDurationMs;
22use ruma::{
23    MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId,
24    TransactionId, UInt,
25    events::{
26        AnyMessageLikeEventContent, MessageLikeEventContent as _, RawExt as _,
27        room::{MediaSource, message::RoomMessageEventContent},
28    },
29    serde::Raw,
30};
31use serde::{Deserialize, Serialize};
32
33use crate::media::MediaRequestParameters;
34
35/// A thin wrapper to serialize a `AnyMessageLikeEventContent`.
36#[derive(Clone, Serialize, Deserialize)]
37pub struct SerializableEventContent {
38    event: Raw<AnyMessageLikeEventContent>,
39    event_type: String,
40}
41
42#[cfg(not(tarpaulin_include))]
43impl fmt::Debug for SerializableEventContent {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        // Don't include the event in the debug display.
46        f.debug_struct("SerializedEventContent")
47            .field("event_type", &self.event_type)
48            .finish_non_exhaustive()
49    }
50}
51
52impl SerializableEventContent {
53    /// Create a [`SerializableEventContent`] from a raw
54    /// [`AnyMessageLikeEventContent`] along with its type.
55    pub fn from_raw(event: Raw<AnyMessageLikeEventContent>, event_type: String) -> Self {
56        Self { event_type, event }
57    }
58
59    /// Create a [`SerializableEventContent`] from an
60    /// [`AnyMessageLikeEventContent`].
61    pub fn new(event: &AnyMessageLikeEventContent) -> Result<Self, serde_json::Error> {
62        Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string()))
63    }
64
65    /// Convert a [`SerializableEventContent`] back into a
66    /// [`AnyMessageLikeEventContent`].
67    pub fn deserialize(&self) -> Result<AnyMessageLikeEventContent, serde_json::Error> {
68        self.event.deserialize_with_type(&self.event_type)
69    }
70
71    /// Returns the raw event content along with its type, borrowed variant.
72    ///
73    /// Useful for callers manipulating custom events.
74    pub fn raw(&self) -> (&Raw<AnyMessageLikeEventContent>, &str) {
75        (&self.event, &self.event_type)
76    }
77
78    /// Returns the raw event content along with its type, owned variant.
79    ///
80    /// Useful for callers manipulating custom events.
81    pub fn into_raw(self) -> (Raw<AnyMessageLikeEventContent>, String) {
82        (self.event, self.event_type)
83    }
84}
85
86/// The kind of a send queue request.
87#[derive(Clone, Debug, Serialize, Deserialize)]
88pub enum QueuedRequestKind {
89    /// An event to be sent via the send queue.
90    Event {
91        /// The content of the message-like event we'd like to send.
92        content: SerializableEventContent,
93
94        /// How long the event should be sticky for, if it is to be sent as a
95        /// sticky event.
96        #[cfg(feature = "unstable-msc4354")]
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        sticky_duration: Option<StickyDurationMs>,
99    },
100
101    /// Content to upload on the media server.
102    ///
103    /// The bytes must be stored in the media cache, and are identified by the
104    /// cache key.
105    MediaUpload {
106        /// Content type of the media to be uploaded.
107        ///
108        /// Stored as a `String` because `Mime` which we'd really want to use
109        /// here, is not serializable. Oh well.
110        content_type: String,
111
112        /// The cache key used to retrieve the media's bytes in the event cache
113        /// store.
114        cache_key: MediaRequestParameters,
115
116        /// An optional media source for a thumbnail already uploaded.
117        thumbnail_source: Option<MediaSource>,
118
119        /// To which media event transaction does this upload relate?
120        related_to: OwnedTransactionId,
121
122        /// Accumulated list of infos for previously uploaded files and
123        /// thumbnails if used during a gallery transaction. Otherwise empty.
124        #[cfg(feature = "unstable-msc4274")]
125        #[serde(default)]
126        accumulated: Vec<AccumulatedSentMediaInfo>,
127    },
128
129    /// A redaction of another event to send.
130    Redaction {
131        /// The ID of the event to redact.
132        redacts: OwnedEventId,
133        /// The reason for the event being redacted.
134        reason: Option<String>,
135    },
136}
137
138impl From<SerializableEventContent> for QueuedRequestKind {
139    fn from(content: SerializableEventContent) -> Self {
140        Self::Event {
141            content,
142            #[cfg(feature = "unstable-msc4354")]
143            sticky_duration: None,
144        }
145    }
146}
147
148/// A request to be sent with a send queue.
149#[derive(Clone)]
150pub struct QueuedRequest {
151    /// The kind of queued request we're going to send.
152    pub kind: QueuedRequestKind,
153
154    /// Unique transaction id for the queued request, acting as a key.
155    pub transaction_id: OwnedTransactionId,
156
157    /// Error returned when the request couldn't be sent and is stuck in the
158    /// unrecoverable state.
159    ///
160    /// `None` if the request is in the queue, waiting to be sent.
161    pub error: Option<QueueWedgeError>,
162
163    /// At which priority should this be handled?
164    ///
165    /// The bigger the value, the higher the priority at which this request
166    /// should be handled.
167    pub priority: usize,
168
169    /// The time that the request was originally attempted.
170    pub created_at: MilliSecondsSinceUnixEpoch,
171}
172
173impl QueuedRequest {
174    /// Returns `Some` if the queued request is about sending an event.
175    pub fn as_event(&self) -> Option<&SerializableEventContent> {
176        as_variant!(&self.kind, QueuedRequestKind::Event { content, .. } => content)
177    }
178
179    /// True if the request couldn't be sent because of an unrecoverable API
180    /// error. See [`Self::error`] for more details on the reason.
181    pub fn is_wedged(&self) -> bool {
182        self.error.is_some()
183    }
184}
185
186/// Represents a failed to send unrecoverable error of an event sent via the
187/// send queue.
188///
189/// It is a serializable representation of a client error, see `From`
190/// implementation for more details. These errors can not be automatically
191/// retried, but yet some manual action can be taken before retry sending. If
192/// not the only solution is to delete the local event.
193#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
194pub enum QueueWedgeError {
195    /// This error occurs when there are some insecure devices in the room, and
196    /// the current encryption setting prohibits sharing with them.
197    #[error("There are insecure devices in the room")]
198    InsecureDevices {
199        /// The insecure devices as a Map of userID to deviceID.
200        user_device_map: BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
201    },
202
203    /// This error occurs when a previously verified user is not anymore, and
204    /// the current encryption setting prohibits sharing when it happens.
205    #[error("Some users that were previously verified are not anymore")]
206    IdentityViolations {
207        /// The users that are expected to be verified but are not.
208        users: Vec<OwnedUserId>,
209    },
210
211    /// It is required to set up cross-signing and properly verify the current
212    /// session before sending.
213    #[error("Own verification is required")]
214    CrossVerificationRequired,
215
216    /// The homeserver rejected the session's access token and the request can't
217    /// go out until the session has a working one again.
218    #[error("The access token was rejected")]
219    ExpiredAccessToken,
220
221    /// Media content was cached in the media store, but has disappeared before
222    /// we could upload it.
223    #[error("Media content disappeared")]
224    MissingMediaContent,
225
226    /// We tried to upload some media content with an unknown mime type.
227    #[error("Invalid mime type '{mime_type}' for media")]
228    InvalidMimeType {
229        /// The observed mime type that's expected to be invalid.
230        mime_type: String,
231    },
232
233    /// Other errors.
234    #[error("Other unrecoverable error: {msg}")]
235    GenericApiError {
236        /// Description of the error.
237        msg: String,
238    },
239}
240
241/// The specific user intent that characterizes a [`DependentQueuedRequest`].
242#[derive(Clone, Debug, Serialize, Deserialize)]
243pub enum DependentQueuedRequestKind {
244    /// The event should be edited.
245    EditEvent {
246        /// The new event for the content.
247        new_content: SerializableEventContent,
248    },
249
250    /// The event should be redacted/aborted/removed.
251    RedactEvent,
252
253    /// The event should be redacted/aborted/removed, with a reason applied to
254    /// the redaction if the event was sent by the time the abort was processed
255    /// and must be redacted server-side.
256    RedactEventWithReason {
257        /// Reason for the redaction, if any.
258        #[serde(default, skip_serializing_if = "Option::is_none")]
259        reason: Option<String>,
260    },
261
262    /// The event should be reacted to, with the given key.
263    ReactEvent {
264        /// Key used for the reaction.
265        key: String,
266    },
267
268    /// Upload a file or thumbnail depending on another file or thumbnail
269    /// upload.
270    #[serde(alias = "UploadFileWithThumbnail")]
271    UploadFileOrThumbnail {
272        /// Content type for the file or thumbnail.
273        content_type: String,
274
275        /// Media request necessary to retrieve the file or thumbnail itself.
276        cache_key: MediaRequestParameters,
277
278        /// To which media transaction id does this upload relate to?
279        related_to: OwnedTransactionId,
280
281        /// Whether the depended upon request was a thumbnail or a file upload.
282        #[serde(default = "default_parent_is_thumbnail_upload")]
283        parent_is_thumbnail_upload: bool,
284    },
285
286    /// Finish an upload by updating references to the media cache and sending
287    /// the final media event with the remote MXC URIs.
288    FinishUpload {
289        /// Local echo for the event (containing the local MXC URIs).
290        ///
291        /// `Box` the local echo so that it reduces the size of the whole enum.
292        local_echo: Box<RoomMessageEventContent>,
293
294        /// Transaction id for the file upload.
295        file_upload: OwnedTransactionId,
296
297        /// Information about the thumbnail, if present.
298        thumbnail_info: Option<FinishUploadThumbnailInfo>,
299
300        /// Additional top-level fields to merge into the final event content
301        /// before it is sent.
302        #[serde(default, skip_serializing_if = "Option::is_none")]
303        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
304    },
305
306    /// Finish a gallery upload by updating references to the media cache and
307    /// sending the final gallery event with the remote MXC URIs.
308    #[cfg(feature = "unstable-msc4274")]
309    FinishGallery {
310        /// Local echo for the event (containing the local MXC URIs).
311        ///
312        /// `Box` the local echo so that it reduces the size of the whole enum.
313        local_echo: Box<RoomMessageEventContent>,
314
315        /// Metadata about the gallery items.
316        item_infos: Vec<FinishGalleryItemInfo>,
317
318        /// Additional top-level fields to merge into the final event content
319        /// before it is sent.
320        #[serde(default, skip_serializing_if = "Option::is_none")]
321        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
322    },
323}
324
325/// If parent_is_thumbnail_upload is missing, we assume the request is for a
326/// file upload following a thumbnail upload. This was the only possible case
327/// before parent_is_thumbnail_upload was introduced.
328fn default_parent_is_thumbnail_upload() -> bool {
329    true
330}
331
332/// Detailed record about a thumbnail used when finishing a media upload.
333#[derive(Clone, Debug, Serialize, Deserialize)]
334pub struct FinishUploadThumbnailInfo {
335    /// Transaction id for the thumbnail upload.
336    pub txn: OwnedTransactionId,
337    /// Thumbnail's width.
338    #[serde(default, skip_serializing_if = "Option::is_none")]
339    pub width: Option<UInt>,
340    /// Thumbnail's height.
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub height: Option<UInt>,
343}
344
345/// Detailed record about a file and thumbnail. When finishing a gallery upload,
346/// one [`FinishGalleryItemInfo`] will be used for each media in the gallery.
347#[cfg(feature = "unstable-msc4274")]
348#[derive(Clone, Debug, Serialize, Deserialize)]
349pub struct FinishGalleryItemInfo {
350    /// Transaction id for the file upload.
351    pub file_upload: OwnedTransactionId,
352    /// Information about the thumbnail, if present.
353    pub thumbnail_info: Option<FinishUploadThumbnailInfo>,
354    /// Additional fields to merge into this item's content before sending.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub extra_content: Option<serde_json::Map<String, serde_json::Value>>,
357}
358
359/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
360/// parent [`QueuedRequest`].
361///
362/// This thin wrapper adds some safety to some APIs, making it possible to
363/// distinguish between the parent's `TransactionId` and the dependent event's
364/// own `TransactionId`.
365#[repr(transparent)]
366#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
367#[serde(transparent)]
368pub struct ChildTransactionId(OwnedTransactionId);
369
370impl ChildTransactionId {
371    /// Returns a new [`ChildTransactionId`].
372    #[allow(clippy::new_without_default)]
373    pub fn new() -> Self {
374        Self(TransactionId::new())
375    }
376}
377
378impl Deref for ChildTransactionId {
379    type Target = TransactionId;
380
381    fn deref(&self) -> &Self::Target {
382        &self.0
383    }
384}
385
386impl From<String> for ChildTransactionId {
387    fn from(val: String) -> Self {
388        Self(val.into())
389    }
390}
391
392impl From<ChildTransactionId> for OwnedTransactionId {
393    fn from(val: ChildTransactionId) -> Self {
394        val.0
395    }
396}
397
398impl From<OwnedTransactionId> for ChildTransactionId {
399    fn from(val: OwnedTransactionId) -> Self {
400        Self(val)
401    }
402}
403
404/// Information about a media (and its thumbnail) that have been sent to a
405/// homeserver.
406#[derive(Clone, Debug, Serialize, Deserialize)]
407pub struct SentMediaInfo {
408    /// File that was uploaded by this request.
409    ///
410    /// If the request related to a thumbnail upload, this contains the
411    /// thumbnail media source.
412    pub file: MediaSource,
413
414    /// Optional thumbnail previously uploaded, when uploading a file.
415    ///
416    /// When uploading a thumbnail, this is set to `None`.
417    pub thumbnail: Option<MediaSource>,
418
419    /// Accumulated list of infos for previously uploaded files and thumbnails
420    /// if used during a gallery transaction. Otherwise empty.
421    #[cfg(feature = "unstable-msc4274")]
422    #[serde(default)]
423    pub accumulated: Vec<AccumulatedSentMediaInfo>,
424}
425
426/// Accumulated information about a media (and its thumbnail) that have been
427/// sent to a homeserver.
428#[cfg(feature = "unstable-msc4274")]
429#[derive(Clone, Debug, Serialize, Deserialize)]
430pub struct AccumulatedSentMediaInfo {
431    /// File that was uploaded by this request.
432    ///
433    /// If the request related to a thumbnail upload, this contains the
434    /// thumbnail media source.
435    pub file: MediaSource,
436
437    /// Optional thumbnail previously uploaded, when uploading a file.
438    ///
439    /// When uploading a thumbnail, this is set to `None`.
440    pub thumbnail: Option<MediaSource>,
441}
442
443#[cfg(feature = "unstable-msc4274")]
444impl From<AccumulatedSentMediaInfo> for SentMediaInfo {
445    fn from(value: AccumulatedSentMediaInfo) -> Self {
446        Self { file: value.file, thumbnail: value.thumbnail, accumulated: vec![] }
447    }
448}
449
450/// A unique key (identifier) indicating that a transaction has been
451/// successfully sent to the server.
452///
453/// The owning child transactions can now be resolved.
454#[derive(Clone, Debug, Serialize, Deserialize)]
455pub enum SentRequestKey {
456    /// The parent transaction returned an event when it succeeded.
457    Event {
458        /// The event ID returned by the server.
459        event_id: OwnedEventId,
460
461        /// The sent event.
462        event: Raw<AnyMessageLikeEventContent>,
463
464        /// The type of the sent event.
465        event_type: String,
466    },
467
468    /// The parent transaction returned an uploaded resource URL.
469    Media(SentMediaInfo),
470
471    /// The parent transaction returned a redaction event when it succeeded.
472    Redaction {
473        /// The event ID returned by the server.
474        event_id: OwnedEventId,
475
476        /// The ID of the redacted event.
477        redacts: OwnedEventId,
478
479        /// The reason for the event being redacted.
480        reason: Option<String>,
481    },
482}
483
484impl SentRequestKey {
485    /// Converts the current parent key into an event id, if possible.
486    pub fn into_event_id(self) -> Option<OwnedEventId> {
487        match self {
488            Self::Event { event_id, .. } | Self::Redaction { event_id, .. } => Some(event_id),
489            _ => None,
490        }
491    }
492
493    /// Converts the current parent key into information about a sent media, if
494    /// possible.
495    pub fn into_media(self) -> Option<SentMediaInfo> {
496        as_variant!(self, Self::Media)
497    }
498}
499
500/// A request to be sent, depending on a [`QueuedRequest`] to be sent first.
501///
502/// Depending on whether the parent request has been sent or not, this will
503/// either update the local echo in the storage, or materialize an equivalent
504/// request implementing the user intent to the homeserver.
505#[derive(Clone, Debug, Serialize, Deserialize)]
506pub struct DependentQueuedRequest {
507    /// Unique identifier for this dependent queued request.
508    ///
509    /// Useful for deletion.
510    pub own_transaction_id: ChildTransactionId,
511
512    /// The kind of user intent.
513    pub kind: DependentQueuedRequestKind,
514
515    /// Transaction id for the parent's local echo / used in the server request.
516    ///
517    /// Note: this is the transaction id used for the depended-on request, i.e.
518    /// the one that was originally sent and that's being modified with this
519    /// dependent request.
520    pub parent_transaction_id: OwnedTransactionId,
521
522    /// If the parent request has been sent, the parent's request identifier
523    /// returned by the server once the local echo has been sent out.
524    pub parent_key: Option<SentRequestKey>,
525
526    /// The time that the request was originally attempted.
527    pub created_at: MilliSecondsSinceUnixEpoch,
528}
529
530impl DependentQueuedRequest {
531    /// Does the dependent request represent a new event that is _not_
532    /// aggregated, aka it is going to be its own item in a timeline?
533    pub fn is_own_event(&self) -> bool {
534        match self.kind {
535            DependentQueuedRequestKind::EditEvent { .. }
536            | DependentQueuedRequestKind::RedactEvent
537            | DependentQueuedRequestKind::RedactEventWithReason { .. }
538            | DependentQueuedRequestKind::ReactEvent { .. }
539            | DependentQueuedRequestKind::UploadFileOrThumbnail { .. } => {
540                // These are all aggregated events, or non-visible items (file
541                // upload producing a new MXC ID).
542                false
543            }
544            DependentQueuedRequestKind::FinishUpload { .. } => {
545                // This one graduates into a new media event.
546                true
547            }
548            #[cfg(feature = "unstable-msc4274")]
549            DependentQueuedRequestKind::FinishGallery { .. } => {
550                // This one graduates into a new gallery event.
551                true
552            }
553        }
554    }
555}
556
557#[cfg(not(tarpaulin_include))]
558impl fmt::Debug for QueuedRequest {
559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560        // Hide the content from the debug log.
561        f.debug_struct("QueuedRequest")
562            .field("transaction_id", &self.transaction_id)
563            .field("is_wedged", &self.is_wedged())
564            .finish_non_exhaustive()
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use std::assert_matches;
571
572    use strass::assert_let;
573
574    use super::DependentQueuedRequestKind;
575
576    #[cfg(feature = "unstable-msc4274")]
577    #[test]
578    fn test_gallery_extra_content_storage_compatibility() {
579        let legacy = serde_json::json!({
580            "FinishGallery": {
581                "local_echo": { "msgtype": "dm.filament.gallery", "body": "gallery", "itemtypes": [] },
582                "item_infos": [{ "file_upload": "upload", "thumbnail_info": null }],
583            }
584        });
585        let kind: DependentQueuedRequestKind = serde_json::from_value(legacy.clone()).unwrap();
586        assert_let!(
587            DependentQueuedRequestKind::FinishGallery { item_infos, extra_content, .. } = &kind
588        );
589        assert!(extra_content.is_none());
590        assert!(item_infos[0].extra_content.is_none());
591        assert_eq!(serde_json::to_value(&kind).unwrap(), legacy);
592
593        let mut with_extra = legacy;
594        with_extra["FinishGallery"]["extra_content"] =
595            serde_json::json!({ "com.example.gallery": true });
596        with_extra["FinishGallery"]["item_infos"][0]["extra_content"] =
597            serde_json::json!({ "org.matrix.msc2448.is_spoiler": true });
598        let kind: DependentQueuedRequestKind = serde_json::from_value(with_extra.clone()).unwrap();
599        assert_eq!(serde_json::to_value(kind).unwrap(), with_extra);
600    }
601
602    #[test]
603    fn test_deserialize_legacy_redact_event() {
604        // `RedactEvent` is a unit variant, and must stay one for as long as it
605        // exists: requests persisted before `RedactEventWithReason` are
606        // serialized as a plain string, and this is the only thing that still
607        // reads them.
608        let deserialized: DependentQueuedRequestKind =
609            serde_json::from_str("\"RedactEvent\"").unwrap();
610        assert_matches!(deserialized, DependentQueuedRequestKind::RedactEvent);
611    }
612
613    #[test]
614    fn test_redact_event_with_reason_round_trip() {
615        for reason in [None, Some("spam".to_owned())] {
616            let kind = DependentQueuedRequestKind::RedactEventWithReason { reason: reason.clone() };
617            let serialized = serde_json::to_string(&kind).unwrap();
618            let deserialized: DependentQueuedRequestKind =
619                serde_json::from_str(&serialized).unwrap();
620            assert_let!(
621                DependentQueuedRequestKind::RedactEventWithReason { reason: deserialized } =
622                    deserialized
623            );
624            assert_eq!(deserialized, reason);
625        }
626    }
627}