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}