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    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::from_plaintext(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_or_default();
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                                        e2ee.clone(),
103                                        timeline_event.raw(),
104                                        room_id,
105                                    ))
106                                    .await?
107                                {
108                                    timeline_event = decrypted_timeline_event;
109                                }
110                            }
111
112                            _ => {
113                                Box::pin(verification::process_if_relevant(
114                                    &sync_timeline_event,
115                                    e2ee.clone(),
116                                    room_id,
117                                ))
118                                .await?;
119                            }
120                        }
121                    }
122
123                    // Nothing particular to do.
124                    #[cfg(not(feature = "e2e-encryption"))]
125                    AnySyncTimelineEvent::MessageLike(_) => (),
126                }
127
128                if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
129                    update_push_room_context(
130                        context,
131                        push_condition_room_ctx,
132                        room.own_user_id(),
133                        room_info,
134                    )
135                } else {
136                    push_condition_room_ctx =
137                        get_push_room_context(context, room, room_info, notification.state_store)
138                            .await?;
139                }
140
141                if let Some(push_condition_room_ctx) = &push_condition_room_ctx {
142                    let actions = notification.push_notification_from_event_if(
143                        room_id,
144                        push_condition_room_ctx,
145                        timeline_event.raw(),
146                        Action::should_notify,
147                    );
148
149                    timeline_event.set_push_actions(actions.to_owned());
150                }
151            }
152            Err(error) => {
153                warn!("Error deserializing event: {error}");
154            }
155        }
156
157        // Finally, we have process the timeline event. We can collect it.
158        timeline.events.push(timeline_event);
159    }
160
161    Ok(timeline)
162}
163
164/// Set of types used by [`build`] to reduce the number of arguments by grouping
165/// them by thematics.
166pub mod builder {
167    use ruma::{
168        api::client::sync::sync_events::{v3, v5},
169        events::AnySyncTimelineEvent,
170        serde::Raw,
171    };
172
173    pub struct Timeline {
174        pub limited: bool,
175        pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
176        pub prev_batch: Option<String>,
177    }
178
179    impl From<v3::Timeline> for Timeline {
180        fn from(value: v3::Timeline) -> Self {
181            Self { limited: value.limited, raw_events: value.events, prev_batch: value.prev_batch }
182        }
183    }
184
185    impl From<&v5::response::Room> for Timeline {
186        fn from(value: &v5::response::Room) -> Self {
187            Self {
188                limited: value.limited,
189                raw_events: value.timeline.clone(),
190                prev_batch: value.prev_batch.clone(),
191            }
192        }
193    }
194}
195
196/// Update the push context for the given room.
197///
198/// Updates the context data from `context.state_changes` or `room_info`.
199fn update_push_room_context(
200    context: &Context,
201    push_rules: &mut PushConditionRoomCtx,
202    user_id: &UserId,
203    room_info: &RoomInfo,
204) {
205    let room_id = &*room_info.room_id;
206
207    push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
208
209    // TODO: Use if let chain once stable
210    if let Some(AnySyncStateEvent::RoomMember(member)) =
211        context.state_changes.state.get(room_id).and_then(|events| {
212            events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
213        })
214    {
215        push_rules.user_display_name = member
216            .as_original()
217            .and_then(|ev| ev.content.displayname.clone())
218            .unwrap_or_else(|| user_id.localpart().to_owned())
219    }
220
221    if let Some(AnySyncStateEvent::RoomPowerLevels(event)) =
222        context.state_changes.state.get(room_id).and_then(|types| {
223            types.get(&StateEventType::RoomPowerLevels)?.get("")?.deserialize().ok()
224        })
225    {
226        push_rules.power_levels = Some(event.power_levels().into());
227    }
228}
229
230/// Get the push context for the given room.
231///
232/// Tries to get the data from `changes` or the up to date `room_info`.
233/// Loads the data from the store otherwise.
234///
235/// Returns `None` if some data couldn't be found. This should only happen
236/// in brand new rooms, while we process its state.
237pub async fn get_push_room_context(
238    context: &Context,
239    room: &Room,
240    room_info: &RoomInfo,
241    state_store: &BaseStateStore,
242) -> Result<Option<PushConditionRoomCtx>> {
243    let room_id = room.room_id();
244    let user_id = room.own_user_id();
245
246    let member_count = room_info.active_members_count();
247
248    // TODO: Use if let chain once stable
249    let user_display_name = if let Some(AnySyncStateEvent::RoomMember(member)) =
250        context.state_changes.state.get(room_id).and_then(|events| {
251            events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
252        }) {
253        member
254            .as_original()
255            .and_then(|ev| ev.content.displayname.clone())
256            .unwrap_or_else(|| user_id.localpart().to_owned())
257    } else if let Some(AnyStrippedStateEvent::RoomMember(member)) =
258        context.state_changes.stripped_state.get(room_id).and_then(|events| {
259            events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
260        })
261    {
262        member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
263    } else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
264        member.name().to_owned()
265    } else {
266        trace!("Couldn't get push context because of missing own member information");
267        return Ok(None);
268    };
269
270    let power_levels = if let Some(event) =
271        context.state_changes.state.get(room_id).and_then(|types| {
272            types
273                .get(&StateEventType::RoomPowerLevels)?
274                .get("")?
275                .deserialize_as::<RoomPowerLevelsEvent>()
276                .ok()
277        }) {
278        Some(event.power_levels().into())
279    } else if let Some(event) =
280        context.state_changes.stripped_state.get(room_id).and_then(|types| {
281            types
282                .get(&StateEventType::RoomPowerLevels)?
283                .get("")?
284                .deserialize_as::<StrippedRoomPowerLevelsEvent>()
285                .ok()
286        })
287    {
288        Some(event.power_levels().into())
289    } else {
290        state_store
291            .get_state_event_static::<RoomPowerLevelsEventContent>(room_id)
292            .await?
293            .and_then(|e| e.deserialize().ok())
294            .map(|event| event.power_levels().into())
295    };
296
297    Ok(Some(PushConditionRoomCtx {
298        user_id: user_id.to_owned(),
299        room_id: room_id.to_owned(),
300        member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
301        user_display_name,
302        power_levels,
303    }))
304}