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