From 7394b34323dc0a80df120ca4574fcb376fec880f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 1 Jul 2025 15:32:06 +0200 Subject: [PATCH 1/3] docs(base): Document the room_joined() method a bit better --- Cargo.lock | 1 + crates/matrix-sdk-base/Cargo.toml | 1 + crates/matrix-sdk-base/src/client.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index eb11057c397..23f509ca40c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2952,6 +2952,7 @@ dependencies = [ name = "matrix-sdk-base" version = "0.12.0" dependencies = [ + "anyhow", "as_variant", "assert_matches", "assert_matches2", diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index e8aa69b1e89..5e030ab2519 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -86,6 +86,7 @@ unicode-normalization.workspace = true uniffi = { workspace = true, optional = true } [dev-dependencies] +anyhow.workspace = true assert_matches.workspace = true assert_matches2.workspace = true assign = "1.1.1" diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index e0861b7d485..acab41fdb0c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -394,9 +394,33 @@ impl BaseClient { Ok(room) } - /// User has joined a room. + /// The user has joined a room using this specific client. + /// + /// This method should be called if the user accepts an invite or if they + /// join a public room. + /// + /// The method will create a [`Room`] object if one does not exist yet and + /// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`] + /// object will be persisted in the cache. Please note that the [`Room`] + /// will be a stub until a sync has been received with the full room + /// state using [`BaseClient::receive_sync_response`]. /// /// Update the internal and cached state accordingly. Return the final Room. + /// + /// # Examples + /// + /// ```rust + /// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState}; + /// # use ruma::OwnedRoomId; + /// # async { + /// # let client = BaseClient::new(StoreConfig::new("example".to_owned())); + /// # async fn send_join_request() -> anyhow::Result { todo!() } + /// let room_id = send_join_request().await?; + /// let room = client.room_joined(&room_id).await?; + /// + /// assert_eq!(room.state(), RoomState::Joined); + /// # anyhow::Ok(()) }; + /// ``` pub async fn room_joined(&self, room_id: &RoomId) -> Result { let room = self.state_store.get_or_create_room( room_id, @@ -404,6 +428,8 @@ impl BaseClient { self.room_info_notable_update_sender.clone(), ); + // If the state isn't `RoomState::Joined` then this means that we knew about + // this room before. Let's modify the existing state now. if room.state() != RoomState::Joined { let _sync_lock = self.sync_lock().lock().await; From 032b8a7cca6d2e7bcc5657b730b863e3e8f63006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 1 Jul 2025 15:32:06 +0200 Subject: [PATCH 2/3] feat(base): Remember when a user explicitly accepted an invite --- crates/matrix-sdk-base/CHANGELOG.md | 7 +++++ crates/matrix-sdk-base/src/client.rs | 18 +++++++++++ crates/matrix-sdk-base/src/room/room_info.rs | 31 +++++++++++++++++-- .../src/store/migration_helpers.rs | 1 + 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 16108fadc46..c1bff808bf6 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +### Features +- The `RoomInfo` now remembers when an invite was explicitly accepted when the + `BaseClient::room_joined()` method was called. A new getter for this + timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this + timestamp. + ([#5333](https://github.com/matrix-org/matrix-rust-sdk/pull/5333)) + ### Refactor - The cached `ServerCapabilities` has been renamed to `ServerInfo` and diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index acab41fdb0c..21b935de7eb 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -434,12 +434,30 @@ impl BaseClient { let _sync_lock = self.sync_lock().lock().await; let mut room_info = room.clone_info(); + + // If our previous state was an invite and we're now in the joined state, this + // means that the user has explicitly accepted the invite. Let's + // remember when this has happened. + // + // This is somewhat of a workaround for our lack of cryptographic membership. + // Later on we will decide if historic room keys should be accepted + // based on this info. If a user has accepted an invite and we receive a room + // key bundle shortly after, we might accept it. If we don't do + // this, the homeserver could trick us into accepting any historic room key + // bundle. + if room.state() == RoomState::Invited { + room_info.set_invite_accepted_now(); + } + room_info.mark_as_joined(); room_info.mark_state_partially_synced(); room_info.mark_members_missing(); // the own member event changed + let mut changes = StateChanges::default(); changes.add_room(room_info.clone()); + self.state_store.save_changes(&changes).await?; // Update the store + room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP); } diff --git a/crates/matrix-sdk-base/src/room/room_info.rs b/crates/matrix-sdk-base/src/room/room_info.rs index 817af246df0..b55f876e581 100644 --- a/crates/matrix-sdk-base/src/room/room_info.rs +++ b/crates/matrix-sdk-base/src/room/room_info.rs @@ -46,8 +46,8 @@ use ruma::{ }, room::RoomType, serde::Raw, - EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, - RoomAliasId, RoomId, RoomVersionId, UserId, + EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, + OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, }; use serde::{Deserialize, Serialize}; use tracing::{debug, field::debug, info, instrument, warn}; @@ -464,6 +464,14 @@ pub struct RoomInfo { /// more accurate than relying on the latest event. #[serde(default)] pub(crate) recency_stamp: Option, + + /// A timestamp remembering when we observed the user accepting an invite on + /// this current device. + /// + /// This is useful to remember if the user accepted this a join on this + /// specific client. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) invite_accepted_at: Option, } impl RoomInfo { @@ -486,6 +494,7 @@ impl RoomInfo { cached_display_name: None, cached_user_defined_notification_mode: None, recency_stamp: None, + invite_accepted_at: None, } } @@ -749,6 +758,22 @@ impl RoomInfo { self.summary.invited_member_count = count; } + /// Mark that the user has accepted an invite and remember when this has + /// happened using a timestamp set to [`MilliSecondsSinceUnixEpoch::now()`]. + pub(crate) fn set_invite_accepted_now(&mut self) { + self.invite_accepted_at = Some(MilliSecondsSinceUnixEpoch::now()); + } + + /// Returns the timestamp when an invite to this room has been accepted by + /// this specific client. + /// + /// # Returns + /// - `Some` if the invite has been accepted by this specific client. + /// - `None` if the invite has not been accepted + pub fn invite_accepted_at(&self) -> Option { + self.invite_accepted_at + } + /// Updates the room heroes. pub(crate) fn update_heroes(&mut self, heroes: Vec) { self.summary.room_heroes = heroes; @@ -1173,6 +1198,7 @@ mod tests { owned_mxc_uri, owned_user_id, room_id, serde::Raw, }; use serde_json::json; + use similar_asserts::assert_eq; use super::{BaseRoomInfo, RoomInfo, SyncInfo}; use crate::{ @@ -1221,6 +1247,7 @@ mod tests { cached_display_name: None, cached_user_defined_notification_mode: None, recency_stamp: Some(42), + invite_accepted_at: None, }; let info_json = json!({ diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index 36a459c6704..0006a390f31 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -121,6 +121,7 @@ impl RoomInfoV1 { cached_display_name: None, cached_user_defined_notification_mode: None, recency_stamp: None, + invite_accepted_at: None, } } } From 69672147fb58ef6361ffbe03e34347138546096a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 2 Jul 2025 14:34:22 +0200 Subject: [PATCH 3/3] test(base): Test that we set the joined at timestamp --- crates/matrix-sdk-base/src/client.rs | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 21b935de7eb..1d3f0f09299 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -1703,4 +1703,47 @@ mod tests { assert!(client.is_user_ignored(ignored_user_id).await); } + + #[async_test] + async fn test_joined_at_timestamp_is_set() { + let client = logged_in_base_client(None).await; + let invited_room_id = room_id!("!invited:localhost"); + let unknown_room_id = room_id!("!unknown:localhost"); + + let mut sync_builder = SyncResponseBuilder::new(); + let response = sync_builder + .add_invited_room(InvitedRoomBuilder::new(invited_room_id)) + .build_sync_response(); + client.receive_sync_response(response).await.unwrap(); + + // Let us first check the initial state, we should have a room in the invite + // state. + let invited_room = client + .get_room(invited_room_id) + .expect("The sync should have created a room in the invited state"); + + assert_eq!(invited_room.state(), RoomState::Invited); + assert!(invited_room.inner.get().invite_accepted_at().is_none()); + + // Now we join the room. + let joined_room = client + .room_joined(invited_room_id) + .await + .expect("We should be able to mark a room as joined"); + + // Yup, there's a timestamp now. + assert_eq!(joined_room.state(), RoomState::Joined); + assert!(joined_room.inner.get().invite_accepted_at().is_some()); + + // If we didn't know about the room before the join, we assume that there wasn't + // an invite and we don't record the timestamp. + assert!(client.get_room(unknown_room_id).is_none()); + let unknown_room = client + .room_joined(unknown_room_id) + .await + .expect("We should be able to mark a room as joined"); + + assert_eq!(unknown_room.state(), RoomState::Joined); + assert!(unknown_room.inner.get().invite_accepted_at().is_none()); + } }