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