matrix_sdk/room/
futures.rs

1// Copyright 2023 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//! Named futures returned from methods on types in [the `room` module][super].
16
17#![deny(unreachable_pub)]
18
19#[cfg(feature = "experimental-encrypted-state-events")]
20use std::borrow::Borrow;
21use std::future::IntoFuture;
22
23use eyeball::SharedObservable;
24use matrix_sdk_common::boxed_into_future;
25use mime::Mime;
26#[cfg(doc)]
27use ruma::events::{MessageLikeUnsigned, SyncMessageLikeEvent};
28use ruma::{
29    api::client::message::send_message_event,
30    assign,
31    events::{AnyMessageLikeEventContent, MessageLikeEventContent},
32    serde::Raw,
33    OwnedTransactionId, TransactionId,
34};
35#[cfg(feature = "experimental-encrypted-state-events")]
36use ruma::{
37    api::client::state::send_state_event,
38    events::{AnyStateEventContent, StateEventContent},
39};
40use tracing::{info, trace, Instrument, Span};
41
42use super::Room;
43#[cfg(feature = "experimental-encrypted-state-events")]
44use crate::utils::IntoRawStateEventContent;
45use crate::{
46    attachment::AttachmentConfig, config::RequestConfig, utils::IntoRawMessageLikeEventContent,
47    Result, TransmissionProgress,
48};
49
50/// Future returned by [`Room::send`].
51#[allow(missing_debug_implementations)]
52pub struct SendMessageLikeEvent<'a> {
53    room: &'a Room,
54    event_type: String,
55    content: serde_json::Result<serde_json::Value>,
56    transaction_id: Option<OwnedTransactionId>,
57    request_config: Option<RequestConfig>,
58}
59
60impl<'a> SendMessageLikeEvent<'a> {
61    pub(crate) fn new(room: &'a Room, content: impl MessageLikeEventContent) -> Self {
62        let event_type = content.event_type().to_string();
63        let content = serde_json::to_value(&content);
64        Self { room, event_type, content, transaction_id: None, request_config: None }
65    }
66
67    /// Set a transaction ID for this event.
68    ///
69    /// Since sending message-like events always requires a transaction ID, one
70    /// is generated if this method is not called.
71    ///
72    /// The transaction ID is a locally-unique ID describing a message
73    /// transaction with the homeserver.
74    ///
75    /// * On the sending side, this field is used for re-trying earlier failed
76    ///   transactions. Subsequent messages *must never* re-use an earlier
77    ///   transaction ID.
78    /// * On the receiving side, the field is used for recognizing our own
79    ///   messages when they arrive down the sync: the server includes the ID in
80    ///   the [`MessageLikeUnsigned`] field `transaction_id` of the
81    ///   corresponding [`SyncMessageLikeEvent`], but only for the *sending*
82    ///   device. Other devices will not see it. This is then used to ignore
83    ///   events sent by our own device and/or to implement local echo.
84    pub fn with_transaction_id(mut self, txn_id: OwnedTransactionId) -> Self {
85        self.transaction_id = Some(txn_id);
86        self
87    }
88
89    /// Assign a given [`RequestConfig`] to configure how this request should
90    /// behave with respect to the network.
91    pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
92        self.request_config = Some(request_config);
93        self
94    }
95}
96
97impl<'a> IntoFuture for SendMessageLikeEvent<'a> {
98    type Output = Result<send_message_event::v3::Response>;
99    boxed_into_future!(extra_bounds: 'a);
100
101    fn into_future(self) -> Self::IntoFuture {
102        let Self { room, event_type, content, transaction_id, request_config } = self;
103        Box::pin(async move {
104            let content = content?;
105            assign!(room.send_raw(&event_type, content), { transaction_id, request_config }).await
106        })
107    }
108}
109
110/// Future returned by [`Room::send_raw`].
111#[allow(missing_debug_implementations)]
112pub struct SendRawMessageLikeEvent<'a> {
113    room: &'a Room,
114    event_type: &'a str,
115    content: Raw<AnyMessageLikeEventContent>,
116    tracing_span: Span,
117    transaction_id: Option<OwnedTransactionId>,
118    request_config: Option<RequestConfig>,
119}
120
121impl<'a> SendRawMessageLikeEvent<'a> {
122    pub(crate) fn new(
123        room: &'a Room,
124        event_type: &'a str,
125        content: impl IntoRawMessageLikeEventContent,
126    ) -> Self {
127        let content = content.into_raw_message_like_event_content();
128        Self {
129            room,
130            event_type,
131            content,
132            tracing_span: Span::current(),
133            transaction_id: None,
134            request_config: None,
135        }
136    }
137
138    /// Set a transaction ID for this event.
139    ///
140    /// Since sending message-like events always requires a transaction ID, one
141    /// is generated if this method is not called.
142    ///
143    /// * On the sending side, this field is used for re-trying earlier failed
144    ///   transactions. Subsequent messages *must never* re-use an earlier
145    ///   transaction ID.
146    /// * On the receiving side, the field is used for recognizing our own
147    ///   messages when they arrive down the sync: the server includes the ID in
148    ///   the [`MessageLikeUnsigned`] field `transaction_id` of the
149    ///   corresponding [`SyncMessageLikeEvent`], but only for the *sending*
150    ///   device. Other devices will not see it. This is then used to ignore
151    ///   events sent by our own device and/or to implement local echo.
152    pub fn with_transaction_id(mut self, txn_id: &TransactionId) -> Self {
153        self.transaction_id = Some(txn_id.to_owned());
154        self
155    }
156
157    /// Assign a given [`RequestConfig`] to configure how this request should
158    /// behave with respect to the network.
159    pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
160        self.request_config = Some(request_config);
161        self
162    }
163}
164
165impl<'a> IntoFuture for SendRawMessageLikeEvent<'a> {
166    type Output = Result<send_message_event::v3::Response>;
167    boxed_into_future!(extra_bounds: 'a);
168
169    fn into_future(self) -> Self::IntoFuture {
170        #[cfg_attr(not(feature = "e2e-encryption"), allow(unused_mut))]
171        let Self {
172            room,
173            mut event_type,
174            mut content,
175            tracing_span,
176            transaction_id,
177            request_config,
178        } = self;
179
180        let fut = async move {
181            room.ensure_room_joined()?;
182
183            let txn_id = transaction_id.unwrap_or_else(TransactionId::new);
184            Span::current().record("transaction_id", tracing::field::debug(&txn_id));
185
186            #[cfg(not(feature = "e2e-encryption"))]
187            trace!("Sending plaintext event to room because we don't have encryption support.");
188
189            #[cfg(feature = "e2e-encryption")]
190            if room.latest_encryption_state().await?.is_encrypted() {
191                Span::current().record("is_room_encrypted", true);
192                // Reactions are currently famously not encrypted, skip encrypting
193                // them until they are.
194                if event_type == "m.reaction" {
195                    trace!("Sending plaintext event because of the event type.");
196                } else {
197                    trace!(
198                        room_id = ?room.room_id(),
199                        "Sending encrypted event because the room is encrypted.",
200                    );
201
202                    ensure_room_encryption_ready(room).await?;
203
204                    let olm = room.client.olm_machine().await;
205                    let olm = olm.as_ref().expect("Olm machine wasn't started");
206
207                    content = olm
208                        .encrypt_room_event_raw(room.room_id(), event_type, &content)
209                        .await?
210                        .cast();
211                    event_type = "m.room.encrypted";
212                }
213            } else {
214                Span::current().record("is_room_encrypted", false);
215                trace!("Sending plaintext event because the room is NOT encrypted.");
216            }
217
218            let request = send_message_event::v3::Request::new_raw(
219                room.room_id().to_owned(),
220                txn_id,
221                event_type.into(),
222                content,
223            );
224
225            let response = room.client.send(request).with_request_config(request_config).await?;
226
227            Span::current().record("event_id", tracing::field::debug(&response.event_id));
228            info!("Sent event in room");
229
230            Ok(response)
231        };
232
233        Box::pin(fut.instrument(tracing_span))
234    }
235}
236
237/// Future returned by [`Room::send_attachment`].
238#[allow(missing_debug_implementations)]
239pub struct SendAttachment<'a> {
240    room: &'a Room,
241    filename: String,
242    content_type: &'a Mime,
243    data: Vec<u8>,
244    config: AttachmentConfig,
245    tracing_span: Span,
246    send_progress: SharedObservable<TransmissionProgress>,
247    store_in_cache: bool,
248}
249
250impl<'a> SendAttachment<'a> {
251    pub(crate) fn new(
252        room: &'a Room,
253        filename: String,
254        content_type: &'a Mime,
255        data: Vec<u8>,
256        config: AttachmentConfig,
257    ) -> Self {
258        Self {
259            room,
260            filename,
261            content_type,
262            data,
263            config,
264            tracing_span: Span::current(),
265            send_progress: Default::default(),
266            store_in_cache: false,
267        }
268    }
269
270    /// Replace the default `SharedObservable` used for tracking upload
271    /// progress.
272    pub fn with_send_progress_observable(
273        mut self,
274        send_progress: SharedObservable<TransmissionProgress>,
275    ) -> Self {
276        self.send_progress = send_progress;
277        self
278    }
279
280    /// Whether the sent attachment should be stored in the cache or not.
281    ///
282    /// If set to true, then retrieving the data for the attachment will result
283    /// in a cache hit immediately after upload.
284    pub fn store_in_cache(mut self) -> Self {
285        self.store_in_cache = true;
286        self
287    }
288}
289
290impl<'a> IntoFuture for SendAttachment<'a> {
291    type Output = Result<send_message_event::v3::Response>;
292    boxed_into_future!(extra_bounds: 'a);
293
294    fn into_future(self) -> Self::IntoFuture {
295        let Self {
296            room,
297            filename,
298            content_type,
299            data,
300            config,
301            tracing_span,
302            send_progress,
303            store_in_cache,
304        } = self;
305        let fut = async move {
306            room.prepare_and_send_attachment(
307                filename,
308                content_type,
309                data,
310                config,
311                send_progress,
312                store_in_cache,
313            )
314            .await
315        };
316
317        Box::pin(fut.instrument(tracing_span))
318    }
319}
320
321/// Future returned by [`Room::send_state_event_raw`].
322#[cfg(feature = "experimental-encrypted-state-events")]
323#[allow(missing_debug_implementations)]
324pub struct SendRawStateEvent<'a> {
325    room: &'a Room,
326    event_type: &'a str,
327    state_key: &'a str,
328    content: Raw<AnyStateEventContent>,
329    tracing_span: Span,
330    request_config: Option<RequestConfig>,
331}
332
333#[cfg(feature = "experimental-encrypted-state-events")]
334impl<'a> SendRawStateEvent<'a> {
335    pub(crate) fn new(
336        room: &'a Room,
337        event_type: &'a str,
338        state_key: &'a str,
339        content: impl IntoRawStateEventContent,
340    ) -> Self {
341        let content = content.into_raw_state_event_content();
342        Self {
343            room,
344            event_type,
345            state_key,
346            content,
347            tracing_span: Span::current(),
348            request_config: None,
349        }
350    }
351
352    /// Assign a given [`RequestConfig`] to configure how this request should
353    /// behave with respect to the network.
354    pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
355        self.request_config = Some(request_config);
356        self
357    }
358
359    /// Determines whether the inner state event should be encrypted before
360    /// sending.
361    ///
362    /// This method checks two conditions:
363    /// 1. Whether the room supports encrypted state events, by inspecting the
364    ///    room's encryption state.
365    /// 2. Whether the event type is considered "critical" or excluded from
366    ///    encryption under MSC3414.
367    ///
368    /// # Returns
369    ///
370    /// Returns `true` if the event should be encrypted, otherwise returns
371    /// `false`.
372    fn should_encrypt(room: &Room, event_type: &str) -> bool {
373        if !room.encryption_state().is_state_encrypted() {
374            trace!("Sending plaintext event as the room does NOT support encrypted state events.");
375            return false;
376        }
377
378        // Check the event is not critical.
379        if matches!(
380            event_type,
381            "m.room.create"
382                | "m.room.member"
383                | "m.room.join_rules"
384                | "m.room.power_levels"
385                | "m.room.third_party_invite"
386                | "m.room.history_visibility"
387                | "m.room.guest_access"
388                | "m.room.encryption"
389                | "m.space.child"
390                | "m.space.parent"
391        ) {
392            trace!("Sending plaintext event as its type is excluded from encryption.");
393            return false;
394        }
395
396        true
397    }
398}
399
400#[cfg(feature = "experimental-encrypted-state-events")]
401impl<'a> IntoFuture for SendRawStateEvent<'a> {
402    type Output = Result<send_state_event::v3::Response>;
403    boxed_into_future!(extra_bounds: 'a);
404
405    fn into_future(self) -> Self::IntoFuture {
406        let Self { room, mut event_type, state_key, mut content, tracing_span, request_config } =
407            self;
408
409        let fut = async move {
410            room.ensure_room_joined()?;
411
412            let mut state_key = state_key.to_owned();
413
414            if Self::should_encrypt(room, event_type) {
415                use tracing::debug;
416
417                Span::current().record("should_encrypt", true);
418                debug!(
419                    room_id = ?room.room_id(),
420                    "Sending encrypted event because the room is encrypted.",
421                );
422
423                ensure_room_encryption_ready(room).await?;
424
425                let olm = room.client.olm_machine().await;
426                let olm = olm.as_ref().expect("Olm machine wasn't started");
427
428                content = olm
429                    .encrypt_state_event_raw(room.room_id(), event_type, &state_key, &content)
430                    .await?
431                    .cast_unchecked();
432
433                state_key = format!("{event_type}:{state_key}");
434                event_type = "m.room.encrypted";
435            } else {
436                Span::current().record("should_encrypt", false);
437            }
438
439            let request = send_state_event::v3::Request::new_raw(
440                room.room_id().to_owned(),
441                event_type.into(),
442                state_key.to_owned(),
443                content,
444            );
445
446            let response = room.client.send(request).with_request_config(request_config).await?;
447
448            Span::current().record("event_id", tracing::field::debug(&response.event_id));
449            info!("Sent event in room");
450
451            Ok(response)
452        };
453
454        Box::pin(fut.instrument(tracing_span))
455    }
456}
457
458/// Future returned by `Room::send_state_event`.
459#[allow(missing_debug_implementations)]
460#[cfg(feature = "experimental-encrypted-state-events")]
461pub struct SendStateEvent<'a> {
462    room: &'a Room,
463    event_type: String,
464    state_key: String,
465    content: serde_json::Result<serde_json::Value>,
466    request_config: Option<RequestConfig>,
467}
468
469#[cfg(feature = "experimental-encrypted-state-events")]
470impl<'a> SendStateEvent<'a> {
471    pub(crate) fn new<C, K>(room: &'a Room, state_key: &K, content: C) -> Self
472    where
473        C: StateEventContent,
474        C::StateKey: Borrow<K>,
475        K: AsRef<str> + ?Sized,
476    {
477        let event_type = content.event_type().to_string();
478        let state_key = state_key.as_ref().to_owned();
479        let content = serde_json::to_value(&content);
480        Self { room, event_type, state_key, content, request_config: None }
481    }
482
483    /// Assign a given [`RequestConfig`] to configure how this request should
484    /// behave with respect to the network.
485    pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
486        self.request_config = Some(request_config);
487        self
488    }
489}
490
491#[cfg(feature = "experimental-encrypted-state-events")]
492impl<'a> IntoFuture for SendStateEvent<'a> {
493    type Output = Result<send_state_event::v3::Response>;
494    boxed_into_future!(extra_bounds: 'a);
495
496    fn into_future(self) -> Self::IntoFuture {
497        let Self { room, state_key, event_type, content, request_config } = self;
498        Box::pin(async move {
499            let content = content?;
500            assign!(room.send_state_event_raw(&event_type, &state_key, content), { request_config })
501                .await
502        })
503    }
504}
505
506/// Ensures the room is ready for encrypted events to be sent.
507#[cfg(feature = "e2e-encryption")]
508async fn ensure_room_encryption_ready(room: &Room) -> Result<()> {
509    if !room.are_members_synced() {
510        room.sync_members().await?;
511    }
512
513    // Query keys in case we don't have them for newly synced members.
514    //
515    // Note we do it all the time, because we might have sync'd members before
516    // sending a message (so didn't enter the above branch), but
517    // could have not query their keys ever.
518    room.query_keys_for_untracked_or_dirty_users().await?;
519
520    room.preshare_room_key().await?;
521
522    Ok(())
523}