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}