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    /// Media content was cached in the media store, but has disappeared before
217    /// we could upload it.
218    #[error("Media content disappeared")]
219    MissingMediaContent,
220
221    /// We tried to upload some media content with an unknown mime type.
222    #[error("Invalid mime type '{mime_type}' for media")]
223    InvalidMimeType {
224        /// The observed mime type that's expected to be invalid.
225        mime_type: String,
226    },
227
228    /// Other errors.
229    #[error("Other unrecoverable error: {msg}")]
230    GenericApiError {
231        /// Description of the error.
232        msg: String,
233    },
234}
235
236/// The specific user intent that characterizes a [`DependentQueuedRequest`].
237#[derive(Clone, Debug, Serialize, Deserialize)]
238pub enum DependentQueuedRequestKind {
239    /// The event should be edited.
240    EditEvent {
241        /// The new event for the content.
242        new_content: SerializableEventContent,
243    },
244
245    /// The event should be redacted/aborted/removed.
246    RedactEvent,
247
248    /// The event should be redacted/aborted/removed, with a reason applied to
249    /// the redaction if the event was sent by the time the abort was processed
250    /// and must be redacted server-side.
251    RedactEventWithReason {
252        /// Reason for the redaction, if any.
253        #[serde(default, skip_serializing_if = "Option::is_none")]
254        reason: Option<String>,
255    },
256
257    /// The event should be reacted to, with the given key.
258    ReactEvent {
259        /// Key used for the reaction.
260        key: String,
261    },
262
263    /// Upload a file or thumbnail depending on another file or thumbnail
264    /// upload.
265    #[serde(alias = "UploadFileWithThumbnail")]
266    UploadFileOrThumbnail {
267        /// Content type for the file or thumbnail.
268        content_type: String,
269
270        /// Media request necessary to retrieve the file or thumbnail itself.
271        cache_key: MediaRequestParameters,
272
273        /// To which media transaction id does this upload relate to?
274        related_to: OwnedTransactionId,
275
276        /// Whether the depended upon request was a thumbnail or a file upload.
277        #[serde(default = "default_parent_is_thumbnail_upload")]
278        parent_is_thumbnail_upload: bool,
279    },
280
281    /// Finish an upload by updating references to the media cache and sending
282    /// the final media event with the remote MXC URIs.
283    FinishUpload {
284        /// Local echo for the event (containing the local MXC URIs).
285        ///
286        /// `Box` the local echo so that it reduces the size of the whole enum.
287        local_echo: Box<RoomMessageEventContent>,
288
289        /// Transaction id for the file upload.
290        file_upload: OwnedTransactionId,
291
292        /// Information about the thumbnail, if present.
293        thumbnail_info: Option<FinishUploadThumbnailInfo>,
294
295        /// Additional top-level fields to merge into the final event content
296        /// before it is sent.
297        #[serde(default, skip_serializing_if = "Option::is_none")]
298        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
299    },
300
301    /// Finish a gallery upload by updating references to the media cache and
302    /// sending the final gallery event with the remote MXC URIs.
303    #[cfg(feature = "unstable-msc4274")]
304    FinishGallery {
305        /// Local echo for the event (containing the local MXC URIs).
306        ///
307        /// `Box` the local echo so that it reduces the size of the whole enum.
308        local_echo: Box<RoomMessageEventContent>,
309
310        /// Metadata about the gallery items.
311        item_infos: Vec<FinishGalleryItemInfo>,
312    },
313}
314
315/// If parent_is_thumbnail_upload is missing, we assume the request is for a
316/// file upload following a thumbnail upload. This was the only possible case
317/// before parent_is_thumbnail_upload was introduced.
318fn default_parent_is_thumbnail_upload() -> bool {
319    true
320}
321
322/// Detailed record about a thumbnail used when finishing a media upload.
323#[derive(Clone, Debug, Serialize, Deserialize)]
324pub struct FinishUploadThumbnailInfo {
325    /// Transaction id for the thumbnail upload.
326    pub txn: OwnedTransactionId,
327    /// Thumbnail's width.
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub width: Option<UInt>,
330    /// Thumbnail's height.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub height: Option<UInt>,
333}
334
335/// Detailed record about a file and thumbnail. When finishing a gallery upload,
336/// one [`FinishGalleryItemInfo`] will be used for each media in the gallery.
337#[cfg(feature = "unstable-msc4274")]
338#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct FinishGalleryItemInfo {
340    /// Transaction id for the file upload.
341    pub file_upload: OwnedTransactionId,
342    /// Information about the thumbnail, if present.
343    pub thumbnail_info: Option<FinishUploadThumbnailInfo>,
344}
345
346/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
347/// parent [`QueuedRequest`].
348///
349/// This thin wrapper adds some safety to some APIs, making it possible to
350/// distinguish between the parent's `TransactionId` and the dependent event's
351/// own `TransactionId`.
352#[repr(transparent)]
353#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
354#[serde(transparent)]
355pub struct ChildTransactionId(OwnedTransactionId);
356
357impl ChildTransactionId {
358    /// Returns a new [`ChildTransactionId`].
359    #[allow(clippy::new_without_default)]
360    pub fn new() -> Self {
361        Self(TransactionId::new())
362    }
363}
364
365impl Deref for ChildTransactionId {
366    type Target = TransactionId;
367
368    fn deref(&self) -> &Self::Target {
369        &self.0
370    }
371}
372
373impl From<String> for ChildTransactionId {
374    fn from(val: String) -> Self {
375        Self(val.into())
376    }
377}
378
379impl From<ChildTransactionId> for OwnedTransactionId {
380    fn from(val: ChildTransactionId) -> Self {
381        val.0
382    }
383}
384
385impl From<OwnedTransactionId> for ChildTransactionId {
386    fn from(val: OwnedTransactionId) -> Self {
387        Self(val)
388    }
389}
390
391/// Information about a media (and its thumbnail) that have been sent to a
392/// homeserver.
393#[derive(Clone, Debug, Serialize, Deserialize)]
394pub struct SentMediaInfo {
395    /// File that was uploaded by this request.
396    ///
397    /// If the request related to a thumbnail upload, this contains the
398    /// thumbnail media source.
399    pub file: MediaSource,
400
401    /// Optional thumbnail previously uploaded, when uploading a file.
402    ///
403    /// When uploading a thumbnail, this is set to `None`.
404    pub thumbnail: Option<MediaSource>,
405
406    /// Accumulated list of infos for previously uploaded files and thumbnails
407    /// if used during a gallery transaction. Otherwise empty.
408    #[cfg(feature = "unstable-msc4274")]
409    #[serde(default)]
410    pub accumulated: Vec<AccumulatedSentMediaInfo>,
411}
412
413/// Accumulated information about a media (and its thumbnail) that have been
414/// sent to a homeserver.
415#[cfg(feature = "unstable-msc4274")]
416#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct AccumulatedSentMediaInfo {
418    /// File that was uploaded by this request.
419    ///
420    /// If the request related to a thumbnail upload, this contains the
421    /// thumbnail media source.
422    pub file: MediaSource,
423
424    /// Optional thumbnail previously uploaded, when uploading a file.
425    ///
426    /// When uploading a thumbnail, this is set to `None`.
427    pub thumbnail: Option<MediaSource>,
428}
429
430#[cfg(feature = "unstable-msc4274")]
431impl From<AccumulatedSentMediaInfo> for SentMediaInfo {
432    fn from(value: AccumulatedSentMediaInfo) -> Self {
433        Self { file: value.file, thumbnail: value.thumbnail, accumulated: vec![] }
434    }
435}
436
437/// A unique key (identifier) indicating that a transaction has been
438/// successfully sent to the server.
439///
440/// The owning child transactions can now be resolved.
441#[derive(Clone, Debug, Serialize, Deserialize)]
442pub enum SentRequestKey {
443    /// The parent transaction returned an event when it succeeded.
444    Event {
445        /// The event ID returned by the server.
446        event_id: OwnedEventId,
447
448        /// The sent event.
449        event: Raw<AnyMessageLikeEventContent>,
450
451        /// The type of the sent event.
452        event_type: String,
453    },
454
455    /// The parent transaction returned an uploaded resource URL.
456    Media(SentMediaInfo),
457
458    /// The parent transaction returned a redaction event when it succeeded.
459    Redaction {
460        /// The event ID returned by the server.
461        event_id: OwnedEventId,
462
463        /// The ID of the redacted event.
464        redacts: OwnedEventId,
465
466        /// The reason for the event being redacted.
467        reason: Option<String>,
468    },
469}
470
471impl SentRequestKey {
472    /// Converts the current parent key into an event id, if possible.
473    pub fn into_event_id(self) -> Option<OwnedEventId> {
474        match self {
475            Self::Event { event_id, .. } | Self::Redaction { event_id, .. } => Some(event_id),
476            _ => None,
477        }
478    }
479
480    /// Converts the current parent key into information about a sent media, if
481    /// possible.
482    pub fn into_media(self) -> Option<SentMediaInfo> {
483        as_variant!(self, Self::Media)
484    }
485}
486
487/// A request to be sent, depending on a [`QueuedRequest`] to be sent first.
488///
489/// Depending on whether the parent request has been sent or not, this will
490/// either update the local echo in the storage, or materialize an equivalent
491/// request implementing the user intent to the homeserver.
492#[derive(Clone, Debug, Serialize, Deserialize)]
493pub struct DependentQueuedRequest {
494    /// Unique identifier for this dependent queued request.
495    ///
496    /// Useful for deletion.
497    pub own_transaction_id: ChildTransactionId,
498
499    /// The kind of user intent.
500    pub kind: DependentQueuedRequestKind,
501
502    /// Transaction id for the parent's local echo / used in the server request.
503    ///
504    /// Note: this is the transaction id used for the depended-on request, i.e.
505    /// the one that was originally sent and that's being modified with this
506    /// dependent request.
507    pub parent_transaction_id: OwnedTransactionId,
508
509    /// If the parent request has been sent, the parent's request identifier
510    /// returned by the server once the local echo has been sent out.
511    pub parent_key: Option<SentRequestKey>,
512
513    /// The time that the request was originally attempted.
514    pub created_at: MilliSecondsSinceUnixEpoch,
515}
516
517impl DependentQueuedRequest {
518    /// Does the dependent request represent a new event that is _not_
519    /// aggregated, aka it is going to be its own item in a timeline?
520    pub fn is_own_event(&self) -> bool {
521        match self.kind {
522            DependentQueuedRequestKind::EditEvent { .. }
523            | DependentQueuedRequestKind::RedactEvent
524            | DependentQueuedRequestKind::RedactEventWithReason { .. }
525            | DependentQueuedRequestKind::ReactEvent { .. }
526            | DependentQueuedRequestKind::UploadFileOrThumbnail { .. } => {
527                // These are all aggregated events, or non-visible items (file
528                // upload producing a new MXC ID).
529                false
530            }
531            DependentQueuedRequestKind::FinishUpload { .. } => {
532                // This one graduates into a new media event.
533                true
534            }
535            #[cfg(feature = "unstable-msc4274")]
536            DependentQueuedRequestKind::FinishGallery { .. } => {
537                // This one graduates into a new gallery event.
538                true
539            }
540        }
541    }
542}
543
544#[cfg(not(tarpaulin_include))]
545impl fmt::Debug for QueuedRequest {
546    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547        // Hide the content from the debug log.
548        f.debug_struct("QueuedRequest")
549            .field("transaction_id", &self.transaction_id)
550            .field("is_wedged", &self.is_wedged())
551            .finish_non_exhaustive()
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use std::assert_matches;
558
559    use strass::assert_let;
560
561    use super::DependentQueuedRequestKind;
562
563    #[test]
564    fn test_deserialize_legacy_redact_event() {
565        // `RedactEvent` is a unit variant, and must stay one for as long as it
566        // exists: requests persisted before `RedactEventWithReason` are
567        // serialized as a plain string, and this is the only thing that still
568        // reads them.
569        let deserialized: DependentQueuedRequestKind =
570            serde_json::from_str("\"RedactEvent\"").unwrap();
571        assert_matches!(deserialized, DependentQueuedRequestKind::RedactEvent);
572    }
573
574    #[test]
575    fn test_redact_event_with_reason_round_trip() {
576        for reason in [None, Some("spam".to_owned())] {
577            let kind = DependentQueuedRequestKind::RedactEventWithReason { reason: reason.clone() };
578            let serialized = serde_json::to_string(&kind).unwrap();
579            let deserialized: DependentQueuedRequestKind =
580                serde_json::from_str(&serialized).unwrap();
581            assert_let!(
582                DependentQueuedRequestKind::RedactEventWithReason { reason: deserialized } =
583                    deserialized
584            );
585            assert_eq!(deserialized, reason);
586        }
587    }
588}