matrix_sdk_base/response_processors/
timeline.rs

1// Copyright 2025 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 matrix_sdk_common::{deserialized_responses::TimelineEvent, timer};
16#[cfg(feature = "e2e-encryption")]
17use ruma::events::SyncMessageLikeEvent;
18use ruma::{
19    UInt, UserId, assign,
20    events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
21    push::{Action, PushConditionRoomCtx},
22};
23use tracing::{instrument, trace, warn};
24
25use super::{Context, notification};
26#[cfg(feature = "e2e-encryption")]
27use super::{e2ee, verification};
28use crate::{Result, Room, RoomInfo, sync::Timeline};
29
30/// Process a set of sync timeline event, and create a [`Timeline`].
31///
32/// For each event:
33/// - will try to decrypt it,
34/// - will process verification,
35/// - will process redaction,
36/// - will process notification.
37#[allow(clippy::extra_unused_lifetimes)]
38#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
39pub async fn build<'notification, 'e2ee>(
40    context: &mut Context,
41    room: &Room,
42    room_info: &mut RoomInfo,
43    timeline_inputs: builder::Timeline,
44    mut notification: notification::Notification<'notification>,
45    #[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
46) -> Result<Timeline> {
47    let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
48
49    let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
50    let mut push_condition_room_ctx = get_push_room_context(context, room, room_info).await?;
51    let room_id = room.room_id();
52
53    for raw_event in timeline_inputs.raw_events {
54        // Start by assuming we have a plaintext event. We'll replace it with a
55        // decrypted or UTD event below if necessary.
56        let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
57
58        // Do some special stuff on the `timeline_event` before collecting it.
59        match timeline_event.raw().deserialize() {
60            Ok(sync_timeline_event) => {
61                match &sync_timeline_event {
62                    // State events are ignored. They must be processed separately.
63                    AnySyncTimelineEvent::State(_) => {
64                        // do nothing
65                    }
66
67                    // A room redaction.
68                    AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
69                        redaction_event,
70                    )) => {
71                        let redaction_rules = room_info.room_version_rules_or_default().redaction;
72
73                        if let Some(redacts) = redaction_event.redacts(&redaction_rules) {
74                            room_info.handle_redaction(
75                                redaction_event,
76                                timeline_event.raw().cast_ref_unchecked(),
77                            );
78
79                            context.state_changes.add_redaction(
80                                room_id,
81                                redacts,
82                                timeline_event.raw().clone().cast_unchecked(),
83                            );
84                        }
85                    }
86
87                    // Decrypt encrypted event, or process verification event.
88                    #[cfg(feature = "e2e-encryption")]
89                    AnySyncTimelineEvent::MessageLike(sync_message_like_event) => {
90                        match sync_message_like_event {
91                            AnySyncMessageLikeEvent::RoomEncrypted(
92                                SyncMessageLikeEvent::Original(_),
93                            ) => {
94                                if let Some(decrypted_timeline_event) =
95                                    Box::pin(e2ee::decrypt::sync_timeline_event(
96                                        e2ee.clone(),
97                                        timeline_event.raw(),
98                                        room_id,
99                                    ))
100                                    .await?
101                                {
102                                    timeline_event = decrypted_timeline_event;
103                                }
104                            }
105
106                            _ => {
107                                Box::pin(verification::process_if_relevant(
108                                    &sync_timeline_event,
109                                    e2ee.clone(),
110                                    room_id,
111                                ))
112                                .await?;
113                            }
114                        }
115                    }
116
117                    // Nothing particular to do.
118                    #[cfg(not(feature = "e2e-encryption"))]
119                    AnySyncTimelineEvent::MessageLike(_) => (),
120                }
121
122                if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
123                    update_push_room_context(
124                        context,
125                        push_condition_room_ctx,
126                        room.own_user_id(),
127                        room_info,
128                    )
129                } else {
130                    push_condition_room_ctx =
131                        get_push_room_context(context, room, room_info).await?;
132                }
133
134                if let Some(push_condition_room_ctx) = &push_condition_room_ctx {
135                    let actions = notification
136                        .push_notification_from_event_if(
137                            push_condition_room_ctx,
138                            timeline_event.raw(),
139                            Action::should_notify,
140                        )
141                        .await;
142
143                    timeline_event.set_push_actions(actions.to_owned());
144                }
145            }
146            Err(error) => {
147                warn!("Error deserializing event: {error}");
148            }
149        }
150
151        // Finally, we have process the timeline event. We can collect it.
152        timeline.events.push(timeline_event);
153    }
154
155    Ok(timeline)
156}
157
158/// Set of types used by [`build`] to reduce the number of arguments by grouping
159/// them by thematics.
160pub mod builder {
161    use ruma::{
162        api::client::sync::sync_events::{v3, v5},
163        events::AnySyncTimelineEvent,
164        serde::Raw,
165    };
166
167    pub struct Timeline {
168        pub limited: bool,
169        pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
170        pub prev_batch: Option<String>,
171    }
172
173    impl From<v3::Timeline> for Timeline {
174        fn from(value: v3::Timeline) -> Self {
175            Self { limited: value.limited, raw_events: value.events, prev_batch: value.prev_batch }
176        }
177    }
178
179    impl From<&v5::response::Room> for Timeline {
180        fn from(value: &v5::response::Room) -> Self {
181            Self {
182                limited: value.limited,
183                raw_events: value.timeline.clone(),
184                prev_batch: value.prev_batch.clone(),
185            }
186        }
187    }
188}
189
190/// Update the push context for the given room.
191///
192/// Updates the context data from `context.state_changes` or `room_info`.
193fn update_push_room_context(
194    context: &Context,
195    push_rules: &mut PushConditionRoomCtx,
196    user_id: &UserId,
197    room_info: &RoomInfo,
198) {
199    let room_id = &*room_info.room_id;
200
201    push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
202
203    // TODO: Use if let chain once stable
204    if let Some(member) = context.state_changes.member(room_id, user_id) {
205        push_rules.user_display_name =
206            member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
207    }
208
209    if let Some(power_levels) = context.state_changes.power_levels(room_id) {
210        push_rules.power_levels = Some(power_levels.into());
211    }
212}
213
214/// Get the push context for the given room.
215///
216/// Tries to get the data from `changes` or the up to date `room_info`.
217/// Loads the data from the store otherwise.
218///
219/// Returns `None` if some data couldn't be found. This should only happen
220/// in brand new rooms, while we process its state.
221pub async fn get_push_room_context(
222    context: &Context,
223    room: &Room,
224    room_info: &RoomInfo,
225) -> Result<Option<PushConditionRoomCtx>> {
226    let room_id = room.room_id();
227    let user_id = room.own_user_id();
228
229    let member_count = room_info.active_members_count();
230
231    // TODO: Use if let chain once stable
232    let user_display_name = if let Some(member) = context.state_changes.member(room_id, user_id) {
233        member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
234    } else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
235        member.name().to_owned()
236    } else {
237        trace!("Couldn't get push context because of missing own member information");
238        return Ok(None);
239    };
240
241    let power_levels = if let Some(power_levels) = context.state_changes.power_levels(room_id) {
242        Some(power_levels)
243    } else {
244        room.power_levels().await.ok()
245    };
246
247    Ok(Some(assign!(
248        PushConditionRoomCtx::new(
249            room_id.to_owned(),
250            UInt::new(member_count).unwrap_or(UInt::MAX),
251            user_id.to_owned(),
252            user_display_name
253        ),
254        { power_levels: power_levels.map(Into::into) }
255    )))
256}