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
21#[cfg(feature = "e2e-encryption")]
22use matrix_sdk_common::deserialized_responses::TimelineEvent;
23use matrix_sdk_common::timer;
24use ruma::{
25    JsOption, OwnedRoomId, RoomId, UserId,
26    api::client::sync::sync_events::{
27        v3::{InviteState, InvitedRoom, KnockState, KnockedRoom},
28        v5 as http,
29    },
30    assign,
31    events::{
32        AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
33        room::member::{MembershipState, RoomMemberEventContent},
34    },
35    serde::Raw,
36};
37use tokio::sync::broadcast::Sender;
38
39#[cfg(feature = "e2e-encryption")]
40use super::super::e2ee;
41use super::{
42    super::{Context, notification, state_events, timeline},
43    RoomCreationData,
44};
45#[cfg(feature = "e2e-encryption")]
46use crate::StateChanges;
47use crate::{
48    Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
49    RoomState,
50    store::BaseStateStore,
51    sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
52};
53
54/// Represent any kind of room updates.
55pub enum RoomUpdateKind {
56    Joined(JoinedRoomUpdate),
57    Left(LeftRoomUpdate),
58    Invited(InvitedRoomUpdate),
59    Knocked(KnockedRoomUpdate),
60}
61
62pub async fn update_any_room(
63    context: &mut Context,
64    user_id: &UserId,
65    room_creation_data: RoomCreationData<'_>,
66    room_response: &http::response::Room,
67    rooms_account_data: &BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
68    #[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
69    notification: notification::Notification<'_>,
70) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
71    let _timer = timer!(tracing::Level::TRACE, "update_any_room");
72
73    let RoomCreationData {
74        room_id,
75        room_info_notable_update_sender,
76        requested_required_states,
77        ambiguity_cache,
78    } = room_creation_data;
79
80    // Read state events from the `required_state` field.
81    //
82    // Don't read state events from the `timeline` field, because they might be
83    // incomplete or staled already. We must only read state events from
84    // `required_state`.
85    let state = State::from_msc4186(room_response.required_state.clone());
86    let (raw_state_events, state_events) = state.collect(&[]);
87
88    let state_store = notification.state_store;
89
90    // Find or create the room in the store
91    let is_new_room = !state_store.room_exists(room_id);
92
93    let invite_state_events =
94        room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
95
96    #[allow(unused_mut)] // Required for some feature flag combinations
97    let (mut room, mut room_info, maybe_room_update_kind) = membership(
98        context,
99        &state_events,
100        &invite_state_events,
101        state_store,
102        user_id,
103        room_id,
104        room_info_notable_update_sender,
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, &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.clone(),
125    )
126    .await?;
127
128    // This will be used for both invited and knocked rooms.
129    if let Some((raw_events, events)) = invite_state_events {
130        state_events::stripped::dispatch_invite_or_knock(
131            context,
132            (&raw_events, &events),
133            &room,
134            &mut room_info,
135            notification::Notification::new(
136                notification.push_rules,
137                notification.notifications,
138                notification.state_store,
139            ),
140        )
141        .await?;
142    }
143
144    properties(context, room_id, room_response, &mut room_info, is_new_room);
145
146    let timeline = timeline::build(
147        context,
148        &room,
149        &mut room_info,
150        timeline::builder::Timeline::from(room_response),
151        notification,
152        #[cfg(feature = "e2e-encryption")]
153        e2ee.clone(),
154    )
155    .await?;
156
157    // Cache the latest decrypted event in room_info, and also keep any later
158    // encrypted events, so we can slot them in when we get the keys.
159    #[cfg(feature = "e2e-encryption")]
160    cache_latest_events(
161        &room,
162        &mut room_info,
163        &timeline.events,
164        Some(&context.state_changes),
165        Some(state_store),
166    )
167    .await;
168
169    #[cfg(feature = "e2e-encryption")]
170    e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
171        e2ee.olm_machine,
172        &new_user_ids,
173        room_info.encryption_state(),
174        room.encryption_state(),
175        room_id,
176        state_store,
177    )
178    .await?;
179
180    let notification_count = room_response.unread_notifications.clone().into();
181    room_info.update_notification_count(notification_count);
182
183    let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
184    let room_account_data = rooms_account_data.get(room_id);
185
186    match (room_info.state(), maybe_room_update_kind) {
187        (RoomState::Joined, None) => {
188            // Ephemeral events are added separately, because we might not
189            // have a room subsection in the response, yet we may have receipts for
190            // that room.
191            let ephemeral = Vec::new();
192
193            Ok(Some((
194                room_info,
195                RoomUpdateKind::Joined(JoinedRoomUpdate::new(
196                    timeline,
197                    state,
198                    room_account_data.cloned().unwrap_or_default(),
199                    ephemeral,
200                    notification_count,
201                    ambiguity_changes,
202                )),
203            )))
204        }
205
206        (RoomState::Left, None) | (RoomState::Banned, None) => Ok(Some((
207            room_info,
208            RoomUpdateKind::Left(LeftRoomUpdate::new(
209                timeline,
210                state,
211                room_account_data.cloned().unwrap_or_default(),
212                ambiguity_changes,
213            )),
214        ))),
215
216        (RoomState::Invited, Some(update @ RoomUpdateKind::Invited(_)))
217        | (RoomState::Knocked, Some(update @ RoomUpdateKind::Knocked(_))) => {
218            Ok(Some((room_info, update)))
219        }
220
221        _ => Ok(None),
222    }
223}
224
225/// Look through the sliding sync data for this room, find/create it in the
226/// store, and process any invite information.
227///
228/// If there is any invite state events, the room can be considered an invited
229/// or knocked room, depending of the membership event (if any).
230fn membership(
231    context: &mut Context,
232    state_events: &[AnySyncStateEvent],
233    invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
234    store: &BaseStateStore,
235    user_id: &UserId,
236    room_id: &RoomId,
237    room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
238) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
239    // There are invite state events. It means the room can be:
240    //
241    // 1. either an invited room,
242    // 2. or a knocked room.
243    //
244    // Let's find out.
245    if let Some(state_events) = invite_state_events {
246        // We need to find the membership event since it could be for either an invited
247        // or knocked room.
248        let membership_event = state_events.1.iter().find_map(|event| {
249            if let AnyStrippedStateEvent::RoomMember(membership_event) = event
250                && membership_event.state_key == user_id
251            {
252                return Some(membership_event.content.clone());
253            }
254            None
255        });
256
257        match membership_event {
258            // There is a membership event indicating it's a knocked room.
259            Some(RoomMemberEventContent { membership: MembershipState::Knock, .. }) => {
260                let room = store.get_or_create_room(
261                    room_id,
262                    RoomState::Knocked,
263                    room_info_notable_update_sender,
264                );
265                let mut room_info = room.clone_info();
266                // Override the room state if the room already exists.
267                room_info.mark_as_knocked();
268
269                let raw_events = state_events.0.clone();
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(
279                    room_id,
280                    RoomState::Invited,
281                    room_info_notable_update_sender,
282                );
283                let mut room_info = room.clone_info();
284                // Override the room state if the room already exists.
285                room_info.mark_as_invited();
286
287                let raw_events = state_events.0.clone();
288                let invited_room = InvitedRoom::from(InviteState::from(raw_events));
289
290                (room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
291            }
292        }
293    }
294    // No invite state events. We assume this is a joined room for the moment. See this block to
295    // learn more.
296    else {
297        let room =
298            store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
299        let mut room_info = room.clone_info();
300
301        // We default to considering this room joined if it's not an invite. If it's
302        // actually left (and we remembered to request membership events in
303        // our sync request), then we can find this out from the events in
304        // required_state by calling handle_own_room_membership.
305        room_info.mark_as_joined();
306
307        // We don't need to do this in a v2 sync, because the membership of a room can
308        // be figured out by whether the room is in the "join", "leave" etc.
309        // property. In sliding sync we only have invite_state,
310        // required_state and timeline, so we must process required_state and timeline
311        // looking for relevant membership events.
312        own_membership(context, user_id, state_events, &mut room_info);
313
314        (room, room_info, None)
315    }
316}
317
318/// Find any `m.room.member` events that refer to the current user, and update
319/// the state in room_info to reflect the "membership" property.
320fn own_membership(
321    context: &mut Context,
322    user_id: &UserId,
323    state_events: &[AnySyncStateEvent],
324    room_info: &mut RoomInfo,
325) {
326    // Start from the last event; the first membership event we see in that order is
327    // the last in the regular order, so that's the only one we need to
328    // consider.
329    for event in state_events.iter().rev() {
330        if let AnySyncStateEvent::RoomMember(member) = &event {
331            // If this event updates the current user's membership, record that in the
332            // room_info.
333            if member.state_key() == user_id.as_str() {
334                let new_state: RoomState = member.membership().into();
335
336                if new_state != room_info.state() {
337                    room_info.set_state(new_state);
338                    // Update an existing notable update entry or create a new one
339                    context
340                        .room_info_notable_updates
341                        .entry(room_info.room_id.to_owned())
342                        .or_default()
343                        .insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
344                }
345
346                break;
347            }
348        }
349    }
350}
351
352fn properties(
353    context: &mut Context,
354    room_id: &RoomId,
355    room_response: &http::response::Room,
356    room_info: &mut RoomInfo,
357    is_new_room: bool,
358) {
359    // Handle the room's avatar.
360    //
361    // It can be updated via the state events, or via the
362    // [`http::ResponseRoom::avatar`] field. This part of the code handles the
363    // latter case. The former case is handled by [`BaseClient::handle_state`].
364    match &room_response.avatar {
365        // A new avatar!
366        JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
367        // Avatar must be removed.
368        JsOption::Null => room_info.update_avatar(None),
369        // Nothing to do.
370        JsOption::Undefined => {}
371    }
372
373    // Sliding sync doesn't have a room summary, nevertheless it contains the joined
374    // and invited member counts, in addition to the heroes.
375    if let Some(count) = room_response.joined_count {
376        room_info.update_joined_member_count(count.into());
377    }
378    if let Some(count) = room_response.invited_count {
379        room_info.update_invited_member_count(count.into());
380    }
381
382    if let Some(heroes) = &room_response.heroes {
383        room_info.update_heroes(
384            heroes
385                .iter()
386                .map(|hero| RoomHero {
387                    user_id: hero.user_id.clone(),
388                    display_name: hero.name.clone(),
389                    avatar_url: hero.avatar.clone(),
390                })
391                .collect(),
392        );
393    }
394
395    room_info.set_prev_batch(room_response.prev_batch.as_deref());
396
397    if room_response.limited {
398        room_info.mark_members_missing();
399    }
400
401    if let Some(recency_stamp) = &room_response.bump_stamp {
402        let recency_stamp = u64::from(*recency_stamp).into();
403
404        if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
405            room_info.update_recency_stamp(recency_stamp);
406
407            // If it's not a new room, let's emit a `RECENCY_STAMP` update.
408            // For a new room, the room will appear as new, so we don't care about this
409            // update.
410            if !is_new_room {
411                context
412                    .room_info_notable_updates
413                    .entry(room_id.to_owned())
414                    .or_default()
415                    .insert(RoomInfoNotableUpdateReasons::RECENCY_STAMP);
416            }
417        }
418    }
419}
420
421/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
422///
423/// If any encrypted events are found after that one, store them in the RoomInfo
424/// too so we can use them when we get the relevant keys.
425///
426/// It is the responsibility of the caller to update the `RoomInfo` instance
427/// stored in the `Room`.
428#[cfg(feature = "e2e-encryption")]
429pub(crate) async fn cache_latest_events(
430    room: &Room,
431    room_info: &mut RoomInfo,
432    events: &[TimelineEvent],
433    changes: Option<&StateChanges>,
434    store: Option<&BaseStateStore>,
435) {
436    use tracing::warn;
437
438    use crate::{
439        deserialized_responses::DisplayName,
440        latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
441        store::ambiguity_map::is_display_name_ambiguous,
442    };
443
444    let _timer = timer!(tracing::Level::TRACE, "cache_latest_events");
445
446    let mut encrypted_events =
447        Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
448
449    // Try to get room power levels from the current changes. If we didn't get any
450    // info, try getting it from local data.
451    let power_levels = match changes.and_then(|changes| changes.power_levels(room_info.room_id())) {
452        Some(power_levels) => Some(power_levels),
453        None => room.power_levels().await.ok(),
454    };
455
456    let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
457
458    for event in events.iter().rev() {
459        if let Ok(timeline_event) = event.raw().deserialize() {
460            match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
461                PossibleLatestEvent::YesRoomMessage(_)
462                | PossibleLatestEvent::YesPoll(_)
463                | PossibleLatestEvent::YesCallInvite(_)
464                | PossibleLatestEvent::YesRtcNotification(_)
465                | PossibleLatestEvent::YesSticker(_)
466                | PossibleLatestEvent::YesKnockedStateEvent(_) => {
467                    // We found a suitable latest event. Store it.
468
469                    // In order to make the latest event fast to read, we want to keep the
470                    // associated sender in cache. This is a best-effort to gather enough
471                    // information for creating a user profile as fast as possible. If information
472                    // are missing, let's go back on the “slow” path.
473
474                    let mut sender_profile = None;
475                    let mut sender_name_is_ambiguous = None;
476
477                    // First off, look up the sender's profile from the `StateChanges`, they are
478                    // likely to be the most recent information.
479                    if let Some(changes) = changes {
480                        sender_profile = changes
481                            .profiles
482                            .get(room.room_id())
483                            .and_then(|profiles_by_user| {
484                                profiles_by_user.get(timeline_event.sender())
485                            })
486                            .cloned();
487
488                        if let Some(sender_profile) = sender_profile.as_ref() {
489                            sender_name_is_ambiguous = sender_profile
490                                .as_original()
491                                .and_then(|profile| profile.content.displayname.as_ref())
492                                .and_then(|display_name| {
493                                    let display_name = DisplayName::new(display_name);
494
495                                    changes.ambiguity_maps.get(room.room_id()).and_then(
496                                        |map_for_room| {
497                                            map_for_room.get(&display_name).map(|users| {
498                                                is_display_name_ambiguous(&display_name, users)
499                                            })
500                                        },
501                                    )
502                                });
503                        }
504                    }
505
506                    // Otherwise, look up the sender's profile from the `Store`.
507                    if sender_profile.is_none()
508                        && let Some(store) = store
509                    {
510                        sender_profile = store
511                            .get_profile(room.room_id(), timeline_event.sender())
512                            .await
513                            .ok()
514                            .flatten();
515
516                        // TODO: need to update `sender_name_is_ambiguous`,
517                        // but how?
518                    }
519
520                    let latest_event = Box::new(LatestEvent::new_with_sender_details(
521                        event.clone(),
522                        sender_profile,
523                        sender_name_is_ambiguous,
524                    ));
525
526                    // Store it in the return RoomInfo (it will be saved for us in the room later).
527                    room_info.latest_event = Some(latest_event);
528                    // We don't need any of the older encrypted events because we have a new
529                    // decrypted one.
530                    room.latest_encrypted_events.write().unwrap().clear();
531                    // We can stop looking through the timeline now because everything else is
532                    // older.
533                    break;
534                }
535                PossibleLatestEvent::NoEncrypted => {
536                    // m.room.encrypted - this might be the latest event later - we can't tell until
537                    // we are able to decrypt it, so store it for now
538                    //
539                    // Check how many encrypted events we have seen. Only store another if we
540                    // haven't already stored the maximum number.
541                    if encrypted_events.len() < encrypted_events.capacity() {
542                        encrypted_events.push(event.raw().clone());
543                    }
544                }
545                _ => {
546                    // Ignore unsuitable events
547                }
548            }
549        } else {
550            warn!(
551                event_id = ?event.event_id(),
552                "Failed to deserialize event as `AnySyncTimelineEvent`",
553            );
554        }
555    }
556
557    // Push the encrypted events we found into the Room, in reverse order, so
558    // the latest is last
559    room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
560}
561
562impl State {
563    /// Construct a [`State`] from the state changes for a joined or left room
564    /// from a response of the Simplified Sliding Sync endpoint.
565    fn from_msc4186(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
566        Self::After(events)
567    }
568}