Skip to main content

matrix_sdk_base/response_processors/room/msc4186/
mod.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
15pub mod extensions;
16
17use std::collections::BTreeMap;
18#[cfg(feature = "e2e-encryption")]
19use std::collections::BTreeSet;
20
21use as_variant::as_variant;
22use matrix_sdk_common::timer;
23use ruma::{
24    JsOption, OwnedRoomId, RoomId, UserId,
25    api::client::sync::sync_events::{
26        v3::{InviteState, InvitedRoom, KnockState, KnockedRoom},
27        v5 as http,
28    },
29    assign,
30    events::{
31        AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, StateEventType,
32        room::member::MembershipState,
33    },
34    serde::Raw,
35};
36
37#[cfg(feature = "e2e-encryption")]
38use super::super::e2ee;
39use super::{
40    super::{Context, notification, state_events, timeline},
41    RoomCreationData,
42};
43use crate::{
44    Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdateReasons, RoomState,
45    store::BaseStateStore,
46    sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
47    utils::RawStateEventWithKeys,
48};
49
50/// Represent any kind of room updates.
51pub enum RoomUpdateKind {
52    Joined(JoinedRoomUpdate),
53    Left(LeftRoomUpdate),
54    Invited(InvitedRoomUpdate),
55    Knocked(KnockedRoomUpdate),
56}
57
58pub async fn update_any_room(
59    context: &mut Context,
60    user_id: &UserId,
61    room_creation_data: RoomCreationData<'_>,
62    room_response: &http::response::Room,
63    rooms_account_data: &BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
64    #[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
65    notification: notification::Notification<'_>,
66) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
67    let _timer = timer!(tracing::Level::TRACE, "update_any_room");
68
69    let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
70        room_creation_data;
71
72    // Read state events from the `required_state` field.
73    //
74    // Don't read state events from the `timeline` field, because they might be
75    // incomplete or staled already. We must only read state events from
76    // `required_state`.
77    let state = State::from_msc4186(room_response.required_state.clone());
78    let mut raw_state_events = state.collect(&[]);
79
80    let state_store = notification.state_store;
81
82    // Find or create the room in the store
83    let is_new_room = !state_store.room_exists(room_id);
84
85    let mut raw_invite_state_events =
86        room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
87
88    #[allow(unused_mut)] // Required for some feature flag combinations
89    let (mut room, mut room_info, maybe_room_update_kind) = membership(
90        context,
91        &mut raw_state_events,
92        raw_invite_state_events.as_deref_mut(),
93        state_store,
94        user_id,
95        room_id,
96    );
97
98    // If we are not joined to the room, we cannot be waiting for a key bundle:
99    // clear any flag that we are.
100    #[cfg(feature = "e2e-encryption")]
101    if room_info.state() != RoomState::Joined
102        && let Some(olm) = e2ee.olm_machine
103    {
104        olm.store().clear_room_pending_key_bundle(room_info.room_id()).await?
105    }
106
107    room_info.mark_state_partially_synced();
108    room_info.handle_encryption_state(requested_required_states.for_room(room_id));
109
110    #[cfg(feature = "e2e-encryption")]
111    let mut new_user_ids = BTreeSet::new();
112
113    #[cfg(not(feature = "e2e-encryption"))]
114    let mut new_user_ids = ();
115
116    state_events::sync::dispatch(
117        context,
118        raw_state_events,
119        &mut room_info,
120        ambiguity_cache,
121        &mut new_user_ids,
122        state_store,
123        #[cfg(feature = "experimental-encrypted-state-events")]
124        &e2ee,
125    )
126    .await?;
127
128    // This will be used for both invited and knocked rooms.
129    if let Some(raw_state_events) = raw_invite_state_events {
130        state_events::stripped::dispatch_invite_or_knock(
131            context,
132            raw_state_events,
133            &room,
134            &mut room_info,
135            user_id,
136            notification::Notification::new(
137                notification.push_rules,
138                notification.notifications,
139                notification.state_store,
140            ),
141        )
142        .await?;
143    }
144
145    properties(context, room_id, room_response, &mut room_info, is_new_room);
146
147    let timeline = timeline::build(
148        context,
149        &room,
150        &mut room_info,
151        timeline::builder::Timeline::from(room_response),
152        notification,
153        #[cfg(feature = "e2e-encryption")]
154        &e2ee,
155    )
156    .await?;
157
158    #[cfg(feature = "e2e-encryption")]
159    e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
160        e2ee.olm_machine,
161        &new_user_ids,
162        room_info.encryption_state(),
163        room.encryption_state(),
164        room_id,
165        state_store,
166    )
167    .await?;
168
169    let notification_count = room_response.unread_notifications.clone().into();
170    room_info.update_notification_count(notification_count);
171
172    let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
173    let room_account_data = rooms_account_data.get(room_id);
174
175    match (room_info.state(), maybe_room_update_kind) {
176        (RoomState::Joined, None) => {
177            // Ephemeral events are added separately, because we might not
178            // have a room subsection in the response, yet we may have receipts for
179            // that room.
180            let ephemeral = Vec::new();
181
182            Ok(Some((
183                room_info,
184                RoomUpdateKind::Joined(JoinedRoomUpdate::new(
185                    timeline,
186                    state,
187                    room_account_data.cloned().unwrap_or_default(),
188                    ephemeral,
189                    notification_count,
190                    ambiguity_changes,
191                )),
192            )))
193        }
194
195        (RoomState::Left, None) | (RoomState::Banned, None) => Ok(Some((
196            room_info,
197            RoomUpdateKind::Left(LeftRoomUpdate::new(
198                timeline,
199                state,
200                room_account_data.cloned().unwrap_or_default(),
201                ambiguity_changes,
202            )),
203        ))),
204
205        (RoomState::Invited, Some(update @ RoomUpdateKind::Invited(_)))
206        | (RoomState::Knocked, Some(update @ RoomUpdateKind::Knocked(_))) => {
207            Ok(Some((room_info, update)))
208        }
209
210        (RoomState::Invited, None) => {
211            Ok(Some((room_info, RoomUpdateKind::Invited(InvitedRoom::default()))))
212        }
213        (RoomState::Knocked, None) => {
214            Ok(Some((room_info, RoomUpdateKind::Knocked(KnockedRoom::default()))))
215        }
216
217        _ => Ok(None),
218    }
219}
220
221/// Look through the sliding sync data for this room, find/create it in the
222/// store, and process any invite information.
223///
224/// If there is any invite state events, the room can be considered an invited
225/// or knocked room, depending of the membership event (if any).
226fn membership(
227    context: &mut Context,
228    state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
229    invite_state_events: Option<&mut [RawStateEventWithKeys<AnyStrippedStateEvent>]>,
230    store: &BaseStateStore,
231    user_id: &UserId,
232    room_id: &RoomId,
233) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
234    // There are invite state events. It means the room can be:
235    //
236    // 1. either an invited room,
237    // 2. or a knocked room.
238    //
239    // Let's find out.
240    if let Some(state_events) = invite_state_events {
241        // We need to find the membership event since it could be for either an invited
242        // or knocked room.
243        let own_membership = state_events.iter_mut().find_map(|raw_event| {
244            if raw_event.event_type == StateEventType::RoomMember
245                && raw_event.state_key == user_id.as_str()
246            {
247                raw_event
248                    .deserialize_as(|any_event| {
249                        as_variant!(any_event, AnyStrippedStateEvent::RoomMember)
250                    })
251                    .map(|event| event.content.membership.clone())
252            } else {
253                None
254            }
255        });
256
257        let raw_events = state_events.iter().map(|event| event.raw.clone()).collect();
258
259        match own_membership {
260            // There is a membership event indicating it's a knocked room.
261            Some(MembershipState::Knock) => {
262                let room = store.get_or_create_room(room_id, RoomState::Knocked);
263                let mut room_info = room.clone_info();
264                // Override the room state if the room already exists.
265                room_info.mark_as_knocked();
266
267                let knock_state = assign!(KnockState::default(), { events: raw_events });
268                let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
269
270                (room, room_info, Some(RoomUpdateKind::Knocked(knocked_room)))
271            }
272
273            // Otherwise, assume it's an invited room because there are invite state events.
274            _ => {
275                let room = store.get_or_create_room(room_id, RoomState::Invited);
276                let mut room_info = room.clone_info();
277                // Override the room state if the room already exists.
278                room_info.mark_as_invited();
279
280                let invited_room = InvitedRoom::from(InviteState::from(raw_events));
281
282                (room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
283            }
284        }
285    }
286    // No invite state events. We assume this is a joined room for the moment. See this block to
287    // learn more.
288    else {
289        let room = store.get_or_create_room(room_id, RoomState::Joined);
290        let mut room_info = room.clone_info();
291
292        // We default to considering this room joined if it's not an invite. If it's
293        // actually left (and we remembered to request membership events in our sync
294        // request), then we can find this out from the events in required_state by
295        // calling handle_own_room_membership.
296        room_info.mark_as_joined();
297
298        // We don't need to do this in a v2 sync, because the membership of a room can
299        // be figured out by whether the room is in the `join`, `leave` etc. property.
300        // In sliding sync we only have `invite_state`, `required_state` and `timeline`,
301        // so we must process `required_state` and `timeline` looking for relevant
302        // membership events.
303        state_events::sync::own_membership_and_update_room_state(
304            context,
305            user_id,
306            state_events,
307            &mut room_info,
308        );
309
310        (room, room_info, None)
311    }
312}
313
314fn properties(
315    context: &mut Context,
316    room_id: &RoomId,
317    room_response: &http::response::Room,
318    room_info: &mut RoomInfo,
319    is_new_room: bool,
320) {
321    // Handle the room's avatar.
322    //
323    // It can be updated via the state events, or via the
324    // [`http::ResponseRoom::avatar`] field. This part of the code handles the
325    // latter case. The former case is handled by [`BaseClient::handle_state`].
326    match &room_response.avatar {
327        // A new avatar!
328        JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
329        // Avatar must be removed.
330        JsOption::Null => room_info.update_avatar(None),
331        // Nothing to do.
332        JsOption::Undefined => {}
333    }
334
335    // Sliding sync doesn't have a room summary, nevertheless it contains the joined
336    // and invited member counts, in addition to the heroes.
337    if let Some(count) = room_response.joined_count {
338        room_info.update_joined_member_count(count.into());
339    }
340    if let Some(count) = room_response.invited_count {
341        room_info.update_invited_member_count(count.into());
342    }
343
344    if let Some(heroes) = &room_response.heroes {
345        room_info.update_heroes(
346            heroes
347                .iter()
348                .map(|hero| RoomHero {
349                    user_id: hero.user_id.clone(),
350                    display_name: hero.name.clone(),
351                    avatar_url: hero.avatar.clone(),
352                })
353                .collect(),
354        );
355    }
356
357    room_info.set_prev_batch(room_response.prev_batch.as_deref());
358
359    if room_response.limited {
360        room_info.mark_members_missing();
361    }
362
363    if let Some(recency_stamp) = &room_response.bump_stamp {
364        let recency_stamp = u64::from(*recency_stamp).into();
365
366        if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
367            room_info.update_recency_stamp(recency_stamp);
368
369            // If it's not a new room, let's emit a `RECENCY_STAMP` update.
370            // For a new room, the room will appear as new, so we don't care about this
371            // update.
372            if !is_new_room {
373                context
374                    .room_info_notable_updates
375                    .entry(room_id.to_owned())
376                    .or_default()
377                    .insert(RoomInfoNotableUpdateReasons::RECENCY_STAMP);
378            }
379        }
380    }
381}
382
383impl State {
384    /// Construct a [`State`] from the state changes for a joined or left room
385    /// from a response of the Simplified Sliding Sync endpoint.
386    fn from_msc4186(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
387        Self::After(events)
388    }
389}