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