matrix_sdk_ui/timeline/
traits.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
15use std::future::Future;
16
17use eyeball::Subscriber;
18use indexmap::IndexMap;
19#[cfg(test)]
20use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement};
21use matrix_sdk::{
22    crypto::types::events::CryptoContextInfo,
23    deserialized_responses::{EncryptionInfo, TimelineEvent},
24    event_cache::paginator::PaginableRoom,
25    AsyncTraitDeps, Result, Room, SendOutsideWasm,
26};
27use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo};
28use ruma::{
29    events::{
30        fully_read::FullyReadEventContent,
31        receipt::{Receipt, ReceiptThread, ReceiptType},
32        AnyMessageLikeEventContent, AnySyncTimelineEvent,
33    },
34    push::{PushConditionRoomCtx, Ruleset},
35    serde::Raw,
36    EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomVersionId, UserId,
37};
38use tracing::{debug, error};
39
40use super::{Profile, RedactError, TimelineBuilder};
41use crate::timeline::{self, pinned_events_loader::PinnedEventsRoom, Timeline};
42
43pub trait RoomExt {
44    /// Get a [`Timeline`] for this room.
45    ///
46    /// This offers a higher-level API than event handlers, in treating things
47    /// like edits and reactions as updates of existing items rather than new
48    /// independent events.
49    ///
50    /// This is the same as using `room.timeline_builder().build()`.
51    fn timeline(&self)
52        -> impl Future<Output = Result<Timeline, timeline::Error>> + SendOutsideWasm;
53
54    /// Get a [`TimelineBuilder`] for this room.
55    ///
56    /// [`Timeline`] offers a higher-level API than event handlers, in treating
57    /// things like edits and reactions as updates of existing items rather
58    /// than new independent events.
59    ///
60    /// This allows to customize settings of the [`Timeline`] before
61    /// constructing it.
62    fn timeline_builder(&self) -> TimelineBuilder;
63}
64
65impl RoomExt for Room {
66    async fn timeline(&self) -> Result<Timeline, timeline::Error> {
67        self.timeline_builder().build().await
68    }
69
70    fn timeline_builder(&self) -> TimelineBuilder {
71        Timeline::builder(self).track_read_marker_and_receipts()
72    }
73}
74
75pub(super) trait RoomDataProvider:
76    Clone + PaginableRoom + PinnedEventsRoom + 'static
77{
78    fn own_user_id(&self) -> &UserId;
79    fn room_version(&self) -> RoomVersionId;
80
81    fn crypto_context_info(&self)
82        -> impl Future<Output = CryptoContextInfo> + SendOutsideWasm + '_;
83
84    fn profile_from_user_id<'a>(
85        &'a self,
86        user_id: &'a UserId,
87    ) -> impl Future<Output = Option<Profile>> + SendOutsideWasm + 'a;
88    fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option<Profile>;
89
90    /// Loads a user receipt from the storage backend.
91    fn load_user_receipt<'a>(
92        &'a self,
93        receipt_type: ReceiptType,
94        thread: ReceiptThread,
95        user_id: &'a UserId,
96    ) -> impl Future<Output = Option<(OwnedEventId, Receipt)>> + SendOutsideWasm + 'a;
97
98    /// Loads read receipts for an event from the storage backend.
99    fn load_event_receipts<'a>(
100        &'a self,
101        event_id: &'a EventId,
102    ) -> impl Future<Output = IndexMap<OwnedUserId, Receipt>> + SendOutsideWasm + 'a;
103
104    /// Load the current fully-read event id, from storage.
105    fn load_fully_read_marker(&self) -> impl Future<Output = Option<OwnedEventId>> + '_;
106
107    fn push_rules_and_context(
108        &self,
109    ) -> impl Future<Output = Option<(Ruleset, PushConditionRoomCtx)>> + SendOutsideWasm + '_;
110
111    /// Send an event to that room.
112    fn send(
113        &self,
114        content: AnyMessageLikeEventContent,
115    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + '_;
116
117    /// Redact an event from that room.
118    fn redact<'a>(
119        &'a self,
120        event_id: &'a EventId,
121        reason: Option<&'a str>,
122        transaction_id: Option<OwnedTransactionId>,
123    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + 'a;
124
125    fn room_info(&self) -> Subscriber<RoomInfo>;
126
127    /// Return the encryption info for the Megolm session with the supplied
128    /// session ID.
129    fn get_encryption_info(
130        &self,
131        session_id: &str,
132        sender: &UserId,
133    ) -> impl Future<Output = Option<EncryptionInfo>> + SendOutsideWasm;
134}
135
136impl RoomDataProvider for Room {
137    fn own_user_id(&self) -> &UserId {
138        (**self).own_user_id()
139    }
140
141    fn room_version(&self) -> RoomVersionId {
142        (**self).clone_info().room_version_or_default()
143    }
144
145    async fn crypto_context_info(&self) -> CryptoContextInfo {
146        self.crypto_context_info().await
147    }
148
149    async fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> Option<Profile> {
150        match self.get_member_no_sync(user_id).await {
151            Ok(Some(member)) => Some(Profile {
152                display_name: member.display_name().map(ToOwned::to_owned),
153                display_name_ambiguous: member.name_ambiguous(),
154                avatar_url: member.avatar_url().map(ToOwned::to_owned),
155            }),
156            Ok(None) if self.are_members_synced() => Some(Profile::default()),
157            Ok(None) => None,
158            Err(e) => {
159                error!(%user_id, "Failed to fetch room member information: {e}");
160                None
161            }
162        }
163    }
164
165    fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option<Profile> {
166        if !latest_event.has_sender_profile() {
167            return None;
168        }
169
170        Some(Profile {
171            display_name: latest_event.sender_display_name().map(ToOwned::to_owned),
172            display_name_ambiguous: latest_event.sender_name_ambiguous().unwrap_or(false),
173            avatar_url: latest_event.sender_avatar_url().map(ToOwned::to_owned),
174        })
175    }
176
177    async fn load_user_receipt<'a>(
178        &'a self,
179        receipt_type: ReceiptType,
180        thread: ReceiptThread,
181        user_id: &'a UserId,
182    ) -> Option<(OwnedEventId, Receipt)> {
183        match self.load_user_receipt(receipt_type.clone(), thread.clone(), user_id).await {
184            Ok(receipt) => receipt,
185            Err(e) => {
186                error!(
187                    ?receipt_type,
188                    ?thread,
189                    ?user_id,
190                    "Failed to get read receipt for user: {e}"
191                );
192                None
193            }
194        }
195    }
196
197    async fn load_event_receipts<'a>(
198        &'a self,
199        event_id: &'a EventId,
200    ) -> IndexMap<OwnedUserId, Receipt> {
201        let mut unthreaded_receipts = match self
202            .load_event_receipts(ReceiptType::Read, ReceiptThread::Unthreaded, event_id)
203            .await
204        {
205            Ok(receipts) => receipts.into_iter().collect(),
206            Err(e) => {
207                error!(?event_id, "Failed to get unthreaded read receipts for event: {e}");
208                IndexMap::new()
209            }
210        };
211
212        let main_thread_receipts = match self
213            .load_event_receipts(ReceiptType::Read, ReceiptThread::Main, event_id)
214            .await
215        {
216            Ok(receipts) => receipts,
217            Err(e) => {
218                error!(?event_id, "Failed to get main thread read receipts for event: {e}");
219                Vec::new()
220            }
221        };
222
223        unthreaded_receipts.extend(main_thread_receipts);
224        unthreaded_receipts
225    }
226
227    async fn push_rules_and_context(&self) -> Option<(Ruleset, PushConditionRoomCtx)> {
228        match self.push_context().await {
229            Ok(Some(push_context)) => match self.client().account().push_rules().await {
230                Ok(push_rules) => Some((push_rules, push_context)),
231                Err(e) => {
232                    error!("Could not get push rules: {e}");
233                    None
234                }
235            },
236            Ok(None) => {
237                debug!("Could not aggregate push context");
238                None
239            }
240            Err(e) => {
241                error!("Could not get push context: {e}");
242                None
243            }
244        }
245    }
246
247    async fn load_fully_read_marker(&self) -> Option<OwnedEventId> {
248        match self.account_data_static::<FullyReadEventContent>().await {
249            Ok(Some(fully_read)) => match fully_read.deserialize() {
250                Ok(fully_read) => Some(fully_read.content.event_id),
251                Err(e) => {
252                    error!("Failed to deserialize fully-read account data: {e}");
253                    None
254                }
255            },
256            Err(e) => {
257                error!("Failed to get fully-read account data from the store: {e}");
258                None
259            }
260            _ => None,
261        }
262    }
263
264    async fn send(&self, content: AnyMessageLikeEventContent) -> Result<(), super::Error> {
265        let _ = self.send_queue().send(content).await?;
266        Ok(())
267    }
268
269    async fn redact<'a>(
270        &'a self,
271        event_id: &'a EventId,
272        reason: Option<&'a str>,
273        transaction_id: Option<OwnedTransactionId>,
274    ) -> Result<(), super::Error> {
275        let _ = self
276            .redact(event_id, reason, transaction_id)
277            .await
278            .map_err(RedactError::HttpError)
279            .map_err(super::Error::RedactError)?;
280        Ok(())
281    }
282
283    fn room_info(&self) -> Subscriber<RoomInfo> {
284        self.subscribe_info()
285    }
286
287    async fn get_encryption_info(
288        &self,
289        session_id: &str,
290        sender: &UserId,
291    ) -> Option<EncryptionInfo> {
292        // Pass directly on to `Room::get_encryption_info`
293        self.get_encryption_info(session_id, sender).await
294    }
295}
296
297// Internal helper to make most of retry_event_decryption independent of a room
298// object, which is annoying to create for testing and not really needed
299pub(super) trait Decryptor: AsyncTraitDeps + Clone + 'static {
300    fn decrypt_event_impl(
301        &self,
302        raw: &Raw<AnySyncTimelineEvent>,
303    ) -> impl Future<Output = Result<TimelineEvent>> + SendOutsideWasm;
304}
305
306impl Decryptor for Room {
307    async fn decrypt_event_impl(&self, raw: &Raw<AnySyncTimelineEvent>) -> Result<TimelineEvent> {
308        self.decrypt_event(raw.cast_ref()).await
309    }
310}
311
312#[cfg(test)]
313impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) {
314    async fn decrypt_event_impl(&self, raw: &Raw<AnySyncTimelineEvent>) -> Result<TimelineEvent> {
315        let (olm_machine, room_id) = self;
316        let decryption_settings =
317            DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
318        match olm_machine
319            .try_decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings)
320            .await?
321        {
322            RoomEventDecryptionResult::Decrypted(decrypted) => Ok(decrypted.into()),
323            RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
324                Ok(TimelineEvent::new_utd_event(raw.clone(), utd_info))
325            }
326        }
327    }
328}