Skip to main content

matrix_sdk_base/response_processors/room/
sync_v2.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 std::collections::{BTreeMap, BTreeSet};
16
17use ruma::{
18    OwnedRoomId, OwnedUserId, RoomId, UserId,
19    api::client::sync::sync_events::v3::{
20        InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, State as RumaState,
21    },
22};
23use tracing::error;
24
25#[cfg(feature = "e2e-encryption")]
26use super::super::e2ee;
27use super::{
28    super::{Context, account_data, ephemeral_events, notification, state_events, timeline},
29    RoomCreationData,
30};
31use crate::{
32    Result, RoomState,
33    sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
34};
35
36/// Process updates of a joined room.
37#[allow(clippy::too_many_arguments)]
38pub async fn update_joined_room(
39    context: &mut Context,
40    room_creation_data: RoomCreationData<'_>,
41    joined_room: JoinedRoom,
42    updated_members_in_room: &mut BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>>,
43    notification: notification::Notification<'_>,
44    #[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
45) -> Result<JoinedRoomUpdate> {
46    let RoomCreationData { room_id, requested_required_states, ambiguity_cache, avatar_cache } =
47        room_creation_data;
48
49    let state_store = notification.state_store;
50
51    let room = state_store.get_or_create_room(room_id, RoomState::Joined);
52
53    let mut room_info = room.clone_info();
54
55    room_info.mark_as_joined();
56    room_info.update_from_ruma_summary(&joined_room.summary);
57    room_info.set_prev_batch(joined_room.timeline.prev_batch.as_deref());
58    room_info.mark_state_fully_synced();
59    room_info.handle_encryption_state(requested_required_states.for_room(room_id));
60
61    let mut new_user_ids = BTreeSet::new();
62
63    let state = State::from_sync_v2(joined_room.state);
64    let raw_state_events = state.collect(&joined_room.timeline.events);
65
66    state_events::sync::dispatch(
67        context,
68        raw_state_events,
69        &mut room_info,
70        ambiguity_cache,
71        avatar_cache,
72        &mut new_user_ids,
73        state_store,
74        #[cfg(feature = "experimental-encrypted-state-events")]
75        e2ee,
76    )
77    .await?;
78
79    ephemeral_events::dispatch(context, &joined_room.ephemeral.events, room_id);
80
81    if joined_room.timeline.limited {
82        room_info.mark_members_missing();
83    }
84
85    #[cfg(feature = "e2e-encryption")]
86    let olm_machine = e2ee.olm_machine;
87
88    let timeline = timeline::build(
89        context,
90        &room,
91        &mut room_info,
92        timeline::builder::Timeline::from(joined_room.timeline),
93        notification,
94        #[cfg(feature = "e2e-encryption")]
95        e2ee,
96    )
97    .await?;
98
99    // Save the new `RoomInfo`.
100    context.state_changes.add_room(room_info);
101
102    account_data::for_room(context, room_id, &joined_room.account_data.events, state_store);
103
104    // `processors::account_data::from_room` might have updated the `RoomInfo`.
105    // Let's fetch it again.
106    //
107    // SAFETY: `expect` is safe because the `RoomInfo` has been inserted 2 lines
108    // above.
109    let mut room_info = context
110        .state_changes
111        .room_infos
112        .get(room_id)
113        .expect("`RoomInfo` must exist in `StateChanges` at this point")
114        .clone();
115
116    #[cfg(feature = "e2e-encryption")]
117    e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
118        olm_machine,
119        &new_user_ids,
120        room_info.encryption_state(),
121        room.encryption_state(),
122        room_id,
123        state_store,
124    )
125    .await?;
126
127    updated_members_in_room.insert(room_id.to_owned(), new_user_ids);
128
129    let notification_count = joined_room.unread_notifications.into();
130    room_info.update_notification_count(notification_count);
131
132    context.state_changes.add_room(room_info);
133
134    Ok(JoinedRoomUpdate::new(
135        timeline,
136        state,
137        joined_room.account_data.events,
138        joined_room.ephemeral.events,
139        notification_count,
140        ambiguity_cache.changes.remove(room_id).unwrap_or_default(),
141        avatar_cache.remove_changes(room_id),
142    ))
143}
144
145/// Process historical updates of a left room.
146#[allow(clippy::too_many_arguments)]
147pub async fn update_left_room(
148    context: &mut Context,
149    room_creation_data: RoomCreationData<'_>,
150    left_room: LeftRoom,
151    notification: notification::Notification<'_>,
152    #[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
153) -> Result<LeftRoomUpdate> {
154    let RoomCreationData { room_id, requested_required_states, ambiguity_cache, avatar_cache } =
155        room_creation_data;
156
157    #[cfg(feature = "e2e-encryption")]
158    let olm_machine = e2ee.olm_machine;
159
160    let state_store = notification.state_store;
161
162    let room = state_store.get_or_create_room(room_id, RoomState::Left);
163
164    let mut room_info = room.clone_info();
165    room_info.mark_as_left();
166    room_info.mark_state_partially_synced();
167    room_info.handle_encryption_state(requested_required_states.for_room(room_id));
168
169    let state = State::from_sync_v2(left_room.state);
170    let raw_state_events = state.collect(&left_room.timeline.events);
171
172    state_events::sync::dispatch(
173        context,
174        raw_state_events,
175        &mut room_info,
176        ambiguity_cache,
177        avatar_cache,
178        &mut (),
179        state_store,
180        #[cfg(feature = "experimental-encrypted-state-events")]
181        e2ee,
182    )
183    .await?;
184
185    let timeline = timeline::build(
186        context,
187        &room,
188        &mut room_info,
189        timeline::builder::Timeline::from(left_room.timeline),
190        notification,
191        #[cfg(feature = "e2e-encryption")]
192        e2ee,
193    )
194    .await?;
195
196    // Since we are no longer joined to the room, we cannot be waiting for a key
197    // bundle: clear any flag that we are.
198    #[cfg(feature = "e2e-encryption")]
199    if let Some(olm_machine) = olm_machine {
200        olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
201    }
202
203    // Save the new `RoomInfo`.
204    context.state_changes.add_room(room_info);
205
206    account_data::for_room(context, room_id, &left_room.account_data.events, state_store);
207
208    let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
209
210    Ok(LeftRoomUpdate::new(timeline, state, left_room.account_data.events, ambiguity_changes))
211}
212
213/// Process updates of an invited room.
214pub async fn update_invited_room(
215    context: &mut Context,
216    room_id: &RoomId,
217    user_id: &UserId,
218    invited_room: InvitedRoom,
219    notification: notification::Notification<'_>,
220    #[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
221) -> Result<InvitedRoomUpdate> {
222    let state_store = notification.state_store;
223
224    let room = state_store.get_or_create_room(room_id, RoomState::Invited);
225
226    let raw_state_events = state_events::stripped::collect(&invited_room.invite_state.events);
227
228    let mut room_info = room.clone_info();
229    room_info.mark_as_invited();
230    room_info.mark_state_fully_synced();
231
232    state_events::stripped::dispatch_invite_or_knock(
233        context,
234        raw_state_events,
235        &room,
236        &mut room_info,
237        user_id,
238        notification,
239    )
240    .await?;
241
242    // Since we are no longer joined to the room, we cannot be waiting for a key
243    // bundle: clear any flag that we are.
244    #[cfg(feature = "e2e-encryption")]
245    if let Some(olm_machine) = e2ee.olm_machine {
246        olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
247    }
248
249    context.state_changes.add_room(room_info);
250
251    Ok(invited_room)
252}
253
254/// Process updates of a knocked room.
255pub async fn update_knocked_room(
256    context: &mut Context,
257    room_id: &RoomId,
258    user_id: &UserId,
259    knocked_room: KnockedRoom,
260    notification: notification::Notification<'_>,
261    #[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
262) -> Result<KnockedRoomUpdate> {
263    let state_store = notification.state_store;
264
265    let room = state_store.get_or_create_room(room_id, RoomState::Knocked);
266
267    let raw_state_events = state_events::stripped::collect(&knocked_room.knock_state.events);
268
269    let mut room_info = room.clone_info();
270    room_info.mark_as_knocked();
271    room_info.mark_state_fully_synced();
272
273    state_events::stripped::dispatch_invite_or_knock(
274        context,
275        raw_state_events,
276        &room,
277        &mut room_info,
278        user_id,
279        notification,
280    )
281    .await?;
282
283    // Since we are not joined to the room, we cannot be waiting for a key
284    // bundle: clear any flag that we are.
285    #[cfg(feature = "e2e-encryption")]
286    if let Some(olm_machine) = e2ee.olm_machine {
287        olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
288    }
289
290    context.state_changes.add_room(room_info);
291
292    Ok(knocked_room)
293}
294
295impl State {
296    /// Construct a [`State`] from the state changes for a joined or left room
297    /// from a response of the sync v2 endpoint.
298    fn from_sync_v2(state: RumaState) -> Self {
299        match state {
300            RumaState::Before(state) => Self::Before(state.events),
301            RumaState::After(state) => Self::After(state.events),
302            // We shouldn't receive other variants because they are opt-in.
303            state => {
304                error!("Unsupported State variant received for joined room: {state:?}");
305                Self::default()
306            }
307        }
308    }
309}