Skip to content

Commit 6de4032

Browse files
committed
feat(base): Remember the inviter if we accept an invite
1 parent 6209bc9 commit 6de4032

File tree

7 files changed

+122
-38
lines changed

7 files changed

+122
-38
lines changed

crates/matrix-sdk-base/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file.
66

77
## [Unreleased] - ReleaseDate
88

9+
### Features
10+
- [**breaking**] The `RoomInfo` method now remembers the inviter at the time
11+
when the `BaseClient::room_joined()` method was called. The caller is
12+
responsible to remember the inviter before a server request to join the room
13+
is made. The `RoomInfo::invite_accepted_at` method was removed, the
14+
`RoomInfo::invite_details` method returns both the timestamp and the
15+
inviter.
16+
([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390))
17+
918
## [0.13.0] - 2025-07-10
1019

1120
### Features

crates/matrix-sdk-base/src/client.rs

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use ruma::{
4343
},
4444
push::Ruleset,
4545
time::Instant,
46-
OwnedRoomId, OwnedUserId, RoomId, UserId,
46+
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
4747
};
4848
use tokio::sync::{broadcast, Mutex};
4949
#[cfg(feature = "e2e-encryption")]
@@ -66,7 +66,7 @@ use crate::{
6666
StateStoreDataValue, StateStoreExt, StoreConfig,
6767
},
6868
sync::{RoomUpdates, SyncResponse},
69-
RoomStateFilter, SessionMeta,
69+
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
7070
};
7171

7272
/// A no (network) IO client implementation.
@@ -432,21 +432,36 @@ impl BaseClient {
432432
///
433433
/// Update the internal and cached state accordingly. Return the final Room.
434434
///
435+
/// # Arguments
436+
///
437+
/// * `room_id` - The unique ID identifying the joined room.
438+
/// * `inviter` - When joining this room in response to an invitation, the
439+
/// inviter should be recorded before sending the join request to the
440+
/// server. Providing the inviter here ensures that the
441+
/// [`InviteAcceptanceDetails`] are stored for this room.
442+
///
435443
/// # Examples
436444
///
437445
/// ```rust
438446
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
439-
/// # use ruma::OwnedRoomId;
447+
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
440448
/// # async {
441449
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
442450
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
451+
/// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
452+
/// # let room_id: &RoomId = todo!();
453+
/// let maybe_inviter = maybe_get_inviter(room_id).await?;
443454
/// let room_id = send_join_request().await?;
444-
/// let room = client.room_joined(&room_id).await?;
455+
/// let room = client.room_joined(&room_id, maybe_inviter).await?;
445456
///
446457
/// assert_eq!(room.state(), RoomState::Joined);
447458
/// # anyhow::Ok(()) };
448459
/// ```
449-
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
460+
pub async fn room_joined(
461+
&self,
462+
room_id: &RoomId,
463+
inviter: Option<OwnedUserId>,
464+
) -> Result<Room> {
450465
let room = self.state_store.get_or_create_room(
451466
room_id,
452467
RoomState::Joined,
@@ -459,25 +474,32 @@ impl BaseClient {
459474
let _sync_lock = self.sync_lock().lock().await;
460475

461476
let mut room_info = room.clone_info();
477+
let previous_state = room.state();
478+
479+
room_info.mark_as_joined();
480+
room_info.mark_state_partially_synced();
481+
room_info.mark_members_missing(); // the own member event changed
462482

463483
// If our previous state was an invite and we're now in the joined state, this
464-
// means that the user has explicitly accepted the invite. Let's
465-
// remember when this has happened.
484+
// means that the user has explicitly accepted an invite. Let's
485+
// remember some details about the invite.
466486
//
467487
// This is somewhat of a workaround for our lack of cryptographic membership.
468488
// Later on we will decide if historic room keys should be accepted
469489
// based on this info. If a user has accepted an invite and we receive a room
470490
// key bundle shortly after, we might accept it. If we don't do
471491
// this, the homeserver could trick us into accepting any historic room key
472492
// bundle.
473-
if room.state() == RoomState::Invited {
474-
room_info.set_invite_accepted_now();
493+
if previous_state == RoomState::Invited {
494+
if let Some(inviter) = inviter {
495+
let details = InviteAcceptanceDetails {
496+
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
497+
inviter,
498+
};
499+
room_info.set_invite_acceptance_details(details);
500+
}
475501
}
476502

477-
room_info.mark_as_joined();
478-
room_info.mark_state_partially_synced();
479-
room_info.mark_members_missing(); // the own member event changed
480-
481503
let mut changes = StateChanges::default();
482504
changes.add_room(room_info.clone());
483505

@@ -1131,7 +1153,7 @@ impl From<&v5::Request> for RequestedRequiredStates {
11311153
mod tests {
11321154
use std::collections::HashMap;
11331155

1134-
use assert_matches2::assert_let;
1156+
use assert_matches2::{assert_let, assert_matches};
11351157
use futures_util::FutureExt as _;
11361158
use matrix_sdk_test::{
11371159
async_test, event_factory::EventFactory, ruma_response_from_json, InvitedRoomBuilder,
@@ -1739,8 +1761,9 @@ mod tests {
17391761
}
17401762

17411763
#[async_test]
1742-
async fn test_joined_at_timestamp_is_set() {
1743-
let client = logged_in_base_client(None).await;
1764+
async fn test_invite_details_are_set() {
1765+
let user_id = user_id!("@alice:localhost");
1766+
let client = logged_in_base_client(Some(user_id)).await;
17441767
let invited_room_id = room_id!("!invited:localhost");
17451768
let unknown_room_id = room_id!("!unknown:localhost");
17461769

@@ -1757,27 +1780,41 @@ mod tests {
17571780
.expect("The sync should have created a room in the invited state");
17581781

17591782
assert_eq!(invited_room.state(), RoomState::Invited);
1760-
assert!(invited_room.inner.get().invite_accepted_at().is_none());
1783+
assert!(invited_room.invite_acceptance_details().is_none());
17611784

17621785
// Now we join the room.
17631786
let joined_room = client
1764-
.room_joined(invited_room_id)
1787+
.room_joined(invited_room_id, Some(user_id.to_owned()))
17651788
.await
17661789
.expect("We should be able to mark a room as joined");
17671790

1768-
// Yup, there's a timestamp now.
1791+
// Yup, we now have some invite details.
17691792
assert_eq!(joined_room.state(), RoomState::Joined);
1770-
assert!(joined_room.inner.get().invite_accepted_at().is_some());
1793+
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
1794+
assert_eq!(details.inviter, user_id);
17711795

17721796
// If we didn't know about the room before the join, we assume that there wasn't
17731797
// an invite and we don't record the timestamp.
17741798
assert!(client.get_room(unknown_room_id).is_none());
17751799
let unknown_room = client
1776-
.room_joined(unknown_room_id)
1800+
.room_joined(unknown_room_id, Some(user_id.to_owned()))
17771801
.await
17781802
.expect("We should be able to mark a room as joined");
17791803

17801804
assert_eq!(unknown_room.state(), RoomState::Joined);
1781-
assert!(unknown_room.inner.get().invite_accepted_at().is_none());
1805+
assert!(unknown_room.invite_acceptance_details().is_none());
1806+
1807+
sync_builder.clear();
1808+
let response =
1809+
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
1810+
client.receive_sync_response(response).await.unwrap();
1811+
1812+
// Now that we left the room, we shouldn't have any details anymore.
1813+
let left_room = client
1814+
.get_room(invited_room_id)
1815+
.expect("The sync should have created a room in the invited state");
1816+
1817+
assert_eq!(left_room.state(), RoomState::Left);
1818+
assert!(left_room.invite_acceptance_details().is_none());
17821819
}
17831820
}

crates/matrix-sdk-base/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ pub use http;
5555
pub use matrix_sdk_crypto as crypto;
5656
pub use once_cell;
5757
pub use room::{
58-
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
59-
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
60-
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
58+
apply_redaction, EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
59+
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
60+
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
61+
RoomStateFilter, SuccessorRoom,
6162
};
6263
pub use store::{
6364
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,

crates/matrix-sdk-base/src/room/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ use matrix_sdk_common::ring_buffer::RingBuffer;
4444
pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
4545
pub(crate) use room_info::SyncInfo;
4646
pub use room_info::{
47-
apply_redaction, BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
47+
apply_redaction, BaseRoomInfo, InviteAcceptanceDetails, RoomInfo, RoomInfoNotableUpdate,
48+
RoomInfoNotableUpdateReasons,
4849
};
4950
#[cfg(feature = "e2e-encryption")]
5051
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
@@ -457,6 +458,17 @@ impl Room {
457458
self.inner.read().recency_stamp
458459
}
459460

461+
/// Returns the details about an invite to this room if the invite has been
462+
/// accepted by this specific client.
463+
///
464+
/// # Returns
465+
/// - `Some` if an invite has been accepted by this specific client.
466+
/// - `None` if we didn't join this room using an invite or the invite
467+
/// wasn't accepted by this client.
468+
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
469+
self.inner.read().invite_acceptance_details.clone()
470+
}
471+
460472
/// Get a `Stream` of loaded pinned events for this room.
461473
/// If no pinned events are found a single empty `Vec` will be returned.
462474
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> {

crates/matrix-sdk-base/src/room/room_info.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use ruma::{
5050
OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
5151
};
5252
use serde::{Deserialize, Serialize};
53-
use tracing::{debug, field::debug, info, instrument, warn};
53+
use tracing::{debug, error, field::debug, info, instrument, warn};
5454

5555
use super::{
5656
AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
@@ -66,6 +66,18 @@ use crate::{
6666
MinimalStateEvent, OriginalMinimalStateEvent,
6767
};
6868

69+
/// A struct remembering details of an invite and if the invite has been
70+
/// accepted on this particular client.
71+
#[derive(Debug, Clone, Serialize, Deserialize)]
72+
pub struct InviteAcceptanceDetails {
73+
/// A timestamp remembering when we observed the user accepting an invite
74+
/// using this client.
75+
pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
76+
77+
/// The user ID of the person that invited us.
78+
pub inviter: OwnedUserId,
79+
}
80+
6981
impl Room {
7082
/// Subscribe to the inner `RoomInfo`.
7183
pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
@@ -471,7 +483,7 @@ pub struct RoomInfo {
471483
/// This is useful to remember if the user accepted this a join on this
472484
/// specific client.
473485
#[serde(default, skip_serializing_if = "Option::is_none")]
474-
pub(crate) invite_accepted_at: Option<MilliSecondsSinceUnixEpoch>,
486+
pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
475487
}
476488

477489
impl RoomInfo {
@@ -494,7 +506,7 @@ impl RoomInfo {
494506
cached_display_name: None,
495507
cached_user_defined_notification_mode: None,
496508
recency_stamp: None,
497-
invite_accepted_at: None,
509+
invite_acceptance_details: None,
498510
}
499511
}
500512

@@ -525,6 +537,12 @@ impl RoomInfo {
525537

526538
/// Set the membership RoomState of this Room
527539
pub fn set_state(&mut self, room_state: RoomState) {
540+
if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
541+
error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
542+
}
543+
// Changing our state removes the invite details since we can't know that they
544+
// are relevant anymore.
545+
self.invite_acceptance_details = None;
528546
self.room_state = room_state;
529547
}
530548

@@ -758,10 +776,8 @@ impl RoomInfo {
758776
self.summary.invited_member_count = count;
759777
}
760778

761-
/// Mark that the user has accepted an invite and remember when this has
762-
/// happened using a timestamp set to [`MilliSecondsSinceUnixEpoch::now()`].
763-
pub(crate) fn set_invite_accepted_now(&mut self) {
764-
self.invite_accepted_at = Some(MilliSecondsSinceUnixEpoch::now());
779+
pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
780+
self.invite_acceptance_details = Some(details);
765781
}
766782

767783
/// Returns the timestamp when an invite to this room has been accepted by
@@ -770,8 +786,8 @@ impl RoomInfo {
770786
/// # Returns
771787
/// - `Some` if the invite has been accepted by this specific client.
772788
/// - `None` if the invite has not been accepted
773-
pub fn invite_accepted_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
774-
self.invite_accepted_at
789+
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
790+
self.invite_acceptance_details.clone()
775791
}
776792

777793
/// Updates the room heroes.
@@ -1247,7 +1263,7 @@ mod tests {
12471263
cached_display_name: None,
12481264
cached_user_defined_notification_mode: None,
12491265
recency_stamp: Some(42),
1250-
invite_accepted_at: None,
1266+
invite_acceptance_details: None,
12511267
};
12521268

12531269
let info_json = json!({

crates/matrix-sdk-base/src/store/migration_helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl RoomInfoV1 {
121121
cached_display_name: None,
122122
cached_user_defined_notification_mode: None,
123123
recency_stamp: None,
124-
invite_accepted_at: None,
124+
invite_acceptance_details: None,
125125
}
126126
}
127127
}

crates/matrix-sdk/src/client/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1480,7 +1480,16 @@ impl Client {
14801480
false
14811481
};
14821482

1483-
let base_room = self.base_client().room_joined(room_id).await?;
1483+
let base_room = self
1484+
.base_client()
1485+
.room_joined(
1486+
room_id,
1487+
pre_join_room_info
1488+
.as_ref()
1489+
.and_then(|info| info.inviter.as_ref())
1490+
.map(|i| i.user_id().to_owned()),
1491+
)
1492+
.await?;
14841493
let room = Room::new(self.clone(), base_room);
14851494

14861495
if mark_as_dm {

0 commit comments

Comments
 (0)