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, avatar_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        avatar_cache,
122        &mut new_user_ids,
123        state_store,
124        #[cfg(feature = "experimental-encrypted-state-events")]
125        &e2ee,
126    )
127    .await?;
128
129    // This will be used for both invited and knocked rooms.
130    if let Some(raw_state_events) = raw_invite_state_events {
131        state_events::stripped::dispatch_invite_or_knock(
132            context,
133            raw_state_events,
134            &room,
135            &mut room_info,
136            user_id,
137            notification::Notification::new(
138                notification.push_rules,
139                notification.notifications,
140                notification.state_store,
141            ),
142        )
143        .await?;
144    }
145
146    properties(context, room_id, room_response, &mut room_info, is_new_room);
147
148    let timeline = timeline::build(
149        context,
150        &room,
151        &mut room_info,
152        timeline::builder::Timeline::from(room_response),
153        notification,
154        #[cfg(feature = "e2e-encryption")]
155        &e2ee,
156    )
157    .await?;
158
159    #[cfg(feature = "e2e-encryption")]
160    e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
161        e2ee.olm_machine,
162        &new_user_ids,
163        room_info.encryption_state(),
164        room.encryption_state(),
165        room_id,
166        state_store,
167    )
168    .await?;
169
170    let notification_count = room_response.unread_notifications.clone().into();
171    room_info.update_notification_count(notification_count);
172
173    let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
174    let avatar_changes = avatar_cache.remove_changes(room_id);
175    let room_account_data = rooms_account_data.get(room_id);
176
177    match (room_info.state(), maybe_room_update_kind) {
178        (RoomState::Joined, None) => {
179            // Ephemeral events are added separately, because we might not
180            // have a room subsection in the response, yet we may have receipts for
181            // that room.
182            let ephemeral = Vec::new();
183
184            Ok(Some((
185                room_info,
186                RoomUpdateKind::Joined(JoinedRoomUpdate::new(
187                    timeline,
188                    state,
189                    room_account_data.cloned().unwrap_or_default(),
190                    ephemeral,
191                    notification_count,
192                    ambiguity_changes,
193                    avatar_changes,
194                )),
195            )))
196        }
197
198        (RoomState::Left, None) | (RoomState::Banned, None) => Ok(Some((
199            room_info,
200            RoomUpdateKind::Left(LeftRoomUpdate::new(
201                timeline,
202                state,
203                room_account_data.cloned().unwrap_or_default(),
204                ambiguity_changes,
205            )),
206        ))),
207
208        (RoomState::Invited, Some(update @ RoomUpdateKind::Invited(_)))
209        | (RoomState::Knocked, Some(update @ RoomUpdateKind::Knocked(_))) => {
210            Ok(Some((room_info, update)))
211        }
212
213        (RoomState::Invited, None) => {
214            Ok(Some((room_info, RoomUpdateKind::Invited(InvitedRoom::default()))))
215        }
216        (RoomState::Knocked, None) => {
217            Ok(Some((room_info, RoomUpdateKind::Knocked(KnockedRoom::default()))))
218        }
219
220        _ => Ok(None),
221    }
222}
223
224/// Look through the sliding sync data for this room, find/create it in the
225/// store, and process any invite information.
226///
227/// If there is any invite state events, the room can be considered an invited
228/// or knocked room, depending of the membership event (if any).
229fn membership(
230    context: &mut Context,
231    state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
232    invite_state_events: Option<&mut [RawStateEventWithKeys<AnyStrippedStateEvent>]>,
233    store: &BaseStateStore,
234    user_id: &UserId,
235    room_id: &RoomId,
236) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
237    // There are invite state events. It means the room can be:
238    //
239    // 1. either an invited room,
240    // 2. or a knocked room.
241    //
242    // Let's find out.
243    if let Some(state_events) = invite_state_events {
244        // We need to find the membership event since it could be for either an invited
245        // or knocked room.
246        let own_membership = state_events.iter_mut().find_map(|raw_event| {
247            if raw_event.event_type == StateEventType::RoomMember
248                && raw_event.state_key == user_id.as_str()
249            {
250                raw_event
251                    .deserialize_as(|any_event| {
252                        as_variant!(any_event, AnyStrippedStateEvent::RoomMember)
253                    })
254                    .map(|event| event.content.membership.clone())
255            } else {
256                None
257            }
258        });
259
260        let raw_events = state_events.iter().map(|event| event.raw.clone()).collect();
261
262        match own_membership {
263            // There is a membership event indicating it's a knocked room.
264            Some(MembershipState::Knock) => {
265                let room = store.get_or_create_room(room_id, RoomState::Knocked);
266                let mut room_info = room.clone_info();
267                // Override the room state if the room already exists.
268                room_info.mark_as_knocked();
269
270                let knock_state = assign!(KnockState::default(), { events: raw_events });
271                let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
272
273                (room, room_info, Some(RoomUpdateKind::Knocked(knocked_room)))
274            }
275
276            // Otherwise, assume it's an invited room because there are invite state events.
277            _ => {
278                let room = store.get_or_create_room(room_id, RoomState::Invited);
279                let mut room_info = room.clone_info();
280                // Override the room state if the room already exists.
281                room_info.mark_as_invited();
282
283                let invited_room = InvitedRoom::from(InviteState::from(raw_events));
284
285                (room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
286            }
287        }
288    }
289    // No invite state events. We assume this is a joined room for the moment. See this block to
290    // learn more.
291    else {
292        let room = store.get_or_create_room(room_id, RoomState::Joined);
293        let mut room_info = room.clone_info();
294
295        // We default to considering this room joined if it's not an invite. If it's
296        // actually left (and we remembered to request membership events in our sync
297        // request), then we can find this out from the events in required_state by
298        // calling handle_own_room_membership.
299        room_info.mark_as_joined();
300
301        // We don't need to do this in a v2 sync, because the membership of a room can
302        // be figured out by whether the room is in the `join`, `leave` etc. property.
303        // In sliding sync we only have `invite_state`, `required_state` and `timeline`,
304        // so we must process `required_state` and `timeline` looking for relevant
305        // membership events.
306        state_events::sync::own_membership_and_update_room_state(
307            context,
308            user_id,
309            state_events,
310            &mut room_info,
311        );
312
313        (room, room_info, None)
314    }
315}
316
317fn properties(
318    context: &mut Context,
319    room_id: &RoomId,
320    room_response: &http::response::Room,
321    room_info: &mut RoomInfo,
322    is_new_room: bool,
323) {
324    // Handle the room's avatar.
325    //
326    // It can be updated via the state events, or via the
327    // [`http::ResponseRoom::avatar`] field. This part of the code handles the
328    // latter case. The former case is handled by [`BaseClient::handle_state`].
329    match &room_response.avatar {
330        // A new avatar!
331        JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
332        // Avatar must be removed.
333        JsOption::Null => room_info.update_avatar(None),
334        // Nothing to do.
335        JsOption::Undefined => {}
336    }
337
338    // Sliding sync doesn't have a room summary, nevertheless it contains the joined
339    // and invited member counts, in addition to the heroes.
340    if let Some(count) = room_response.joined_count {
341        room_info.update_joined_member_count(count.into());
342    }
343    if let Some(count) = room_response.invited_count {
344        room_info.update_invited_member_count(count.into());
345    }
346
347    if let Some(heroes) = &room_response.heroes {
348        room_info.update_heroes(
349            heroes
350                .iter()
351                .map(|hero| RoomHero {
352                    user_id: hero.user_id.clone(),
353                    display_name: hero.name.clone(),
354                    avatar_url: hero.avatar.clone(),
355                })
356                .collect(),
357        );
358    }
359
360    room_info.set_prev_batch(room_response.prev_batch.as_deref());
361
362    if room_response.limited {
363        room_info.mark_members_missing();
364    }
365
366    if let Some(recency_stamp) = &room_response.bump_stamp {
367        let recency_stamp = u64::from(*recency_stamp).into();
368
369        if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
370            room_info.update_recency_stamp(recency_stamp);
371
372            // If it's not a new room, let's emit a `RECENCY_STAMP` update.
373            // For a new room, the room will appear as new, so we don't care about this
374            // update.
375            if !is_new_room {
376                context
377                    .room_info_notable_updates
378                    .entry(room_id.to_owned())
379                    .or_default()
380                    .insert(RoomInfoNotableUpdateReasons::RECENCY_STAMP);
381            }
382        }
383    }
384}
385
386impl State {
387    /// Construct a [`State`] from the state changes for a joined or left room
388    /// from a response of the Simplified Sliding Sync endpoint.
389    fn from_msc4186(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
390        Self::After(events)
391    }
392}