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