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