Merge pull request #6000 from matrix-org/kaylendog/history-sharing/ui

Expose information about room key bundle forwarder in `matrix-sdk-ui` and `matrix-sdk-ffi`.
This commit is contained in:
Skye Elliot
2026-01-06 16:40:10 +00:00
committed by GitHub
11 changed files with 383 additions and 48 deletions
+4
View File
@@ -30,6 +30,10 @@ All notable changes to this project will be documented in this file.
[#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)
- Created `RoomPowerLevels::events` function which returns a `HashMap<TimelineEventType, i64>` with all the power
levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of
the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
key bundle.
([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000))
### Refactor
@@ -1007,6 +1007,8 @@ pub struct EventTimelineItem {
event_or_transaction_id: EventOrTransactionId,
sender: String,
sender_profile: ProfileDetails,
forwarder: Option<String>,
forwarder_profile: Option<ProfileDetails>,
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
@@ -1030,6 +1032,8 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
event_or_transaction_id: item.identifier().into(),
sender: item.sender().to_string(),
sender_profile: item.sender_profile().clone().into(),
forwarder: item.forwarder().map(ToString::to_string),
forwarder_profile: item.forwarder_profile().map(Into::into),
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
@@ -1085,6 +1089,21 @@ impl From<TimelineDetails<Profile>> for ProfileDetails {
}
}
impl From<&TimelineDetails<Profile>> for ProfileDetails {
fn from(details: &TimelineDetails<Profile>) -> Self {
match details {
TimelineDetails::Unavailable => Self::Unavailable,
TimelineDetails::Pending => Self::Pending,
TimelineDetails::Ready(profile) => Self::Ready {
display_name: profile.display_name.clone(),
display_name_ambiguous: profile.display_name_ambiguous,
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
},
TimelineDetails::Error(e) => Self::Error { message: e.to_string() },
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct PollData {
question: String,
+5 -1
View File
@@ -32,7 +32,11 @@ All notable changes to this project will be documented in this file.
([#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/))
- `Room::load_event_with_relations` now also calls `/relations` to fetch related events when falling back
to network mode after a cache miss.
([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930))
([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930))
- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of
the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
key bundle.
([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000))
### Refactor
@@ -236,6 +236,8 @@ mod tests {
TimelineItemKind::Event(EventTimelineItem::new(
owned_user_id!("@u:s.to"),
TimelineDetails::Pending,
None,
None,
timestamp(),
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
event_kind,
@@ -262,6 +264,8 @@ mod tests {
TimelineItemKind::Event(EventTimelineItem::new(
owned_user_id!("@u:s.to"),
TimelineDetails::Pending,
None,
None,
timestamp(),
TimelineItemContent::MsgLike(MsgLikeContent::unable_to_decrypt(
EncryptedMessage::from_content(
@@ -315,6 +319,8 @@ mod tests {
TimelineItemKind::Event(EventTimelineItem::new(
owned_user_id!("@u:s.to"),
TimelineDetails::Pending,
None,
None,
timestamp(),
TimelineItemContent::message(
content.msgtype,
@@ -734,6 +734,8 @@ mod observable_items_tests {
EventTimelineItem::new(
owned_user_id!("@ivan:mnt.io"),
TimelineDetails::Unavailable,
None,
None,
MilliSecondsSinceUnixEpoch(0u32.into()),
TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Message(Message {
@@ -768,6 +770,8 @@ mod observable_items_tests {
EventTimelineItem::new(
owned_user_id!("@ivan:mnt.io"),
TimelineDetails::Unavailable,
None,
None,
MilliSecondsSinceUnixEpoch(0u32.into()),
TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Message(Message {
@@ -180,6 +180,8 @@ impl<P: RoomDataProvider> TimelineState<P> {
let ctx = TimelineEventContext {
sender: own_user_id,
sender_profile: own_profile,
forwarder: None,
forwarder_profile: None,
timestamp: MilliSecondsSinceUnixEpoch::now(),
read_receipts: Default::default(),
// An event sent by ourselves is never matched against push rules.
@@ -226,9 +226,15 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
| Some(action @ TimelineAction::HandleAggregation { .. }) => {
let encryption_info = event.kind.encryption_info().cloned();
let sender_profile = room_data_provider.profile_from_user_id(&sender).await;
let (forwarder, forwarder_profile) =
get_forwarder_info(&event, room_data_provider).await;
let mut ctx = TimelineEventContext {
sender,
sender_profile,
forwarder,
forwarder_profile,
timestamp,
// These are not used when handling an aggregation.
read_receipts: Default::default(),
@@ -680,9 +686,9 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
let is_highlighted =
event.push_actions().is_some_and(|actions| actions.iter().any(Action::is_highlight));
let thread_summary = if let ThreadSummaryStatus::Some(summary) = event.thread_summary {
let latest_reply_item = if let Some(latest_reply) = summary.latest_reply {
self.fetch_latest_thread_reply(&latest_reply, room_data_provider).await
let thread_summary = if let ThreadSummaryStatus::Some(ref summary) = event.thread_summary {
let latest_reply_item = if let Some(ref latest_reply) = summary.latest_reply {
self.fetch_latest_thread_reply(latest_reply, room_data_provider).await
} else {
None
};
@@ -700,6 +706,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
map.get(&UnsignedEventLocation::RelationsReplace)?.encryption_info().cloned()
});
let (forwarder, forwarder_profile) = get_forwarder_info(&event, room_data_provider).await;
let (raw, utd_info) = match event.kind {
TimelineEventKind::UnableToDecrypt { utd_info, event } => (event, Some(utd_info)),
_ => (event.kind.into_raw(), None),
@@ -794,6 +802,8 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
let ctx = TimelineEventContext {
sender,
sender_profile,
forwarder,
forwarder_profile,
timestamp,
read_receipts: if settings.track_read_receipts.is_enabled()
&& should_add
@@ -1029,3 +1039,34 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
}
}
}
/// Retrieves the forwarder information for a given timeline event.
///
/// # Parameters
///
/// - `event`: The timeline event to extract forwarder information from.
/// - `room_data_provider`: A reference to the room data provider.
///
/// # Returns
///
/// A tuple containing:
/// - `Option<OwnedUserId>`: The user ID of the forwarder, if available.
/// - `Option<Profile>`: The profile of the forwarder, if available.
async fn get_forwarder_info<P: RoomDataProvider>(
event: &TimelineEvent,
room_data_provider: &P,
) -> (Option<OwnedUserId>, Option<Profile>) {
let forwarder = event
.kind
.encryption_info()
.and_then(|info| info.forwarder.as_ref())
.map(|info| info.user_id.clone());
let forwarder_profile = if let Some(ref forwarder_id) = forwarder {
Some(room_data_provider.profile_from_user_id(forwarder_id).await)
} else {
None
};
(forwarder, forwarder_profile.flatten())
}
@@ -683,6 +683,8 @@ mod tests {
EventTimelineItem::new(
owned_user_id!("@alice:example.org"),
crate::timeline::TimelineDetails::Pending,
None,
None,
timestamp,
TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
event_kind,
@@ -108,6 +108,16 @@ impl Flow {
pub(super) struct TimelineEventContext {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: Option<Profile>,
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, the user ID of the forwarder.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub(super) forwarder: Option<OwnedUserId>,
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, the forwarder's profile.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub(super) forwarder_profile: Option<Profile>,
/// The event's `origin_server_ts` field (or creation time for local echo).
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
pub(super) read_receipts: IndexMap<OwnedUserId, Receipt>,
@@ -762,6 +772,14 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
fn add_item(&mut self, content: TimelineItemContent) {
let sender = self.ctx.sender.to_owned();
let sender_profile = TimelineDetails::from_initial_value(self.ctx.sender_profile.clone());
let forwarder = self.ctx.forwarder.to_owned();
let forwarder_profile = self
.ctx
.forwarder
.as_ref()
.map(|_| TimelineDetails::from_initial_value(self.ctx.forwarder_profile.clone()));
let timestamp = self.ctx.timestamp;
let kind: EventTimelineItemKind = match &self.ctx.flow {
@@ -808,6 +826,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
let item = EventTimelineItem::new(
sender,
sender_profile,
forwarder,
forwarder_profile,
timestamp,
content,
kind,
@@ -67,6 +67,16 @@ pub struct EventTimelineItem {
pub(super) sender: OwnedUserId,
/// The sender's profile of the event.
pub(super) sender_profile: TimelineDetails<Profile>,
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, the user ID of the forwarder.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub(super) forwarder: Option<OwnedUserId>,
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, the forwarder's profile, if present.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
/// The timestamp of the event.
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
/// The content of the event.
@@ -108,15 +118,27 @@ pub(crate) enum TimelineItemHandle<'a> {
}
impl EventTimelineItem {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
sender: OwnedUserId,
sender_profile: TimelineDetails<Profile>,
forwarder: Option<OwnedUserId>,
forwarder_profile: Option<TimelineDetails<Profile>>,
timestamp: MilliSecondsSinceUnixEpoch,
content: TimelineItemContent,
kind: EventTimelineItemKind,
is_room_encrypted: bool,
) -> Self {
Self { sender, sender_profile, timestamp, content, kind, is_room_encrypted }
Self {
sender,
sender_profile,
forwarder,
forwarder_profile,
timestamp,
content,
kind,
is_room_encrypted,
}
}
/// Check whether this item is a local echo.
@@ -216,6 +238,22 @@ impl EventTimelineItem {
&self.sender_profile
}
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, returns the user ID of the forwarder.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub fn forwarder(&self) -> Option<&UserId> {
self.forwarder.as_deref()
}
/// If the keys used to decrypt this event were shared-on-invite as part of
/// an [MSC4268] key bundle, returns the profile of the forwarder.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
self.forwarder_profile.as_ref()
}
/// Get the content of this item.
pub fn content(&self) -> &TimelineItemContent {
&self.content
@@ -449,6 +487,8 @@ impl EventTimelineItem {
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
forwarder: self.forwarder.clone(),
forwarder_profile: self.forwarder_profile.clone(),
timestamp: self.timestamp,
content,
kind,
@@ -6,7 +6,7 @@ use assign::assign;
use eyeball_im::VectorDiff;
use futures::{FutureExt, StreamExt, future, pin_mut};
use matrix_sdk::{
assert_decrypted_message_eq, assert_next_with_timeout,
Client, assert_decrypted_message_eq, assert_next_with_timeout,
deserialized_responses::TimelineEventKind,
encryption::EncryptionSettings,
room::power_levels::RoomPowerLevelChanges,
@@ -32,7 +32,8 @@ use matrix_sdk_ui::{
Timeline,
sync_service::SyncService,
timeline::{
EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineItem, TimelineItemContent,
EncryptedMessage, MsgLikeContent, MsgLikeKind, RoomExt, TimelineDetails, TimelineItem,
TimelineItemContent,
},
};
use similar_asserts::assert_eq;
@@ -65,35 +66,15 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) ->
let alice_span = tracing::info_span!("alice");
let bob_span = tracing::info_span!("bob");
let encryption_settings =
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
let alice = TestClientBuilder::new("alice")
.use_sqlite()
.encryption_settings(encryption_settings)
.enable_share_history_on_invite(true)
.exclude_insecure_devices(exclude_insecure_devices)
.build()
let alice = create_encryption_enabled_client("alice", exclude_insecure_devices)
.instrument(alice_span.clone())
.await?;
let sync_service_span = tracing::info_span!(parent: &alice_span, "sync_service");
let alice_sync_service = SyncService::builder(alice.clone())
.with_parent_span(sync_service_span)
.build()
.await
.expect("Could not build alice sync service");
let alice_sync_service = start_client_sync_service(&alice_span, &alice).await;
alice.encryption().wait_for_e2ee_initialization_tasks().await;
alice_sync_service.start().await;
let bob = SyncTokenAwareClient::new(
TestClientBuilder::new("bob")
.encryption_settings(encryption_settings)
.enable_share_history_on_invite(true)
.exclude_insecure_devices(exclude_insecure_devices)
.build()
.await?,
);
let bob = create_encryption_enabled_client("bob", exclude_insecure_devices)
.instrument(bob_span.clone())
.await?;
// Alice creates a room ...
let alice_room = alice
@@ -139,13 +120,7 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) ->
let bob_response = bob.sync_once().instrument(bob_span.clone()).await?;
// Bob should have received a to-device event with the payload
assert_eq!(bob_response.to_device.len(), 1);
let to_device_event = &bob_response.to_device[0];
assert_let!(ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event);
assert_eq!(
raw.get_field::<String>("type").unwrap().unwrap(),
"io.element.msc4268.room_key_bundle"
);
assert_received_room_key_bundle(bob_response);
bob.get_room(alice_room.room_id()).expect("Bob should have received the invite");
@@ -178,6 +153,35 @@ async fn test_history_share_on_invite_helper(exclude_insecure_devices: bool) ->
"The decrypted event should match the message Alice has sent"
);
// We should be able to find the event using the high level timeline API, and
// inspect who forwarded us the keys to decrypt.
let alice_id = alice.user_id().unwrap();
let alice_display_name =
alice.account().get_display_name().await?.expect("Alice should have a display name");
let bob_timeline = bob_room.timeline().await?;
bob.sync_once().instrument(bob_span.clone()).await?;
let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await;
let event = item.as_event().expect("The timeline item should be an event");
assert_eq!(
event.forwarder().expect("We should be able to access the forwarder's ID"),
alice_id.as_str()
);
assert_let!(
Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(),
"We should be able to access the forwarder's profile"
);
assert_eq!(
profile
.display_name
.as_ref()
.expect("We should be able to access the forwarder's display name"),
&alice_display_name
);
Ok(())
}
@@ -407,11 +411,13 @@ async fn test_transitive_history_share_with_withhelds() -> Result<()> {
let charlie_span = tracing::info_span!("charlie");
let derek_span = tracing::info_span!("derek");
let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?;
let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?;
let alice =
create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?;
let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?;
let charlie =
create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?;
let derek = create_encryption_enabled_client("derek").instrument(derek_span.clone()).await?;
create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?;
let derek =
create_encryption_enabled_client("derek", false).instrument(derek_span.clone()).await?;
// 1. Alice creates a room, and enables encryption
let alice_room = alice
@@ -572,10 +578,11 @@ async fn test_history_sharing_session_merging() -> Result<()> {
let bob_span = tracing::info_span!("bob");
let charlie_span = tracing::info_span!("charlie");
let alice = create_encryption_enabled_client("alice").instrument(alice_span.clone()).await?;
let bob = create_encryption_enabled_client("bob").instrument(bob_span.clone()).await?;
let alice =
create_encryption_enabled_client("alice", false).instrument(alice_span.clone()).await?;
let bob = create_encryption_enabled_client("bob", false).instrument(bob_span.clone()).await?;
let charlie =
create_encryption_enabled_client("charlie").instrument(charlie_span.clone()).await?;
create_encryption_enabled_client("charlie", false).instrument(charlie_span.clone()).await?;
// 1. Alice creates a room, and enables encryption
let alice_room = alice
@@ -700,7 +707,159 @@ async fn test_history_sharing_session_merging() -> Result<()> {
Ok(())
}
async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwareClient> {
/// This is a very similar test to [`test_history_share_on_invite`], but we send
/// a second message once Bob has fully joined.
///
/// We can't combine this with the above since:
///
/// - We want to test that history sharing works when Alice's device is deleted,
/// which prevents Alice from sending;
/// - Sending a message after we invite Bob but before they join causes the
/// sessions to be merged, so we lose the forwarder info on the first event as
/// intended.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_history_share_on_invite_no_forwarder_info_for_normal_events() -> Result<()> {
let alice_span = tracing::info_span!("alice");
let bob_span = tracing::info_span!("bob");
let alice = create_encryption_enabled_client("alice", false).await?;
let alice_sync_service = start_client_sync_service(&alice_span, &alice).await;
alice.encryption().wait_for_e2ee_initialization_tasks().await;
alice_sync_service.start().await;
let bob = create_encryption_enabled_client("bob", false).await?;
// Alice creates a room ...
let alice_room = alice
.create_room(assign!(CreateRoomRequest::new(), {
preset: Some(RoomPreset::PublicChat),
}))
.await?;
alice_room.enable_encryption().await?;
info!(room_id = ?alice_room.room_id(), "Alice has created and enabled encryption in the room");
// ... and sends a message
let event_id = alice_room
.send(RoomMessageEventContent::text_plain("Hello Bob"))
.await
.expect("We should be able to send a message to the room")
.response
.event_id;
let bundle_stream = bob
.encryption()
.historic_room_key_stream()
.await
.expect("We should be able to get the bundle stream");
// Alice invites Bob to the room
alice_room.invite_user_by_id(bob.user_id().unwrap()).await?;
// Workaround for https://github.com/matrix-org/matrix-rust-sdk/issues/5770: Bob needs a copy of
// Alice's identity.
bob.encryption()
.request_user_identity(alice.user_id().unwrap())
.instrument(bob_span.clone())
.await?;
// Bob should have received a to-device event with the payload
assert_received_room_key_bundle(bob.sync_once().instrument(bob_span.clone()).await?);
bob.get_room(alice_room.room_id()).expect("Bob should have received the invite");
pin_mut!(bundle_stream);
let info = bundle_stream
.next()
.now_or_never()
.flatten()
.expect("We should be notified about the received bundle");
assert_eq!(Some(info.sender.deref()), alice.user_id());
assert_eq!(info.room_id, alice_room.room_id());
let bob_room = bob
.join_room_by_id(alice_room.room_id())
.instrument(bob_span.clone())
.await
.expect("Bob should be able to accept the invitation from Alice");
let event = bob_room
.event(&event_id, None)
.instrument(bob_span.clone())
.await
.expect("Bob should be able to fetch the historic event");
assert_decrypted_message_eq!(
event,
"Hello Bob",
"The decrypted event should match the message Alice has sent"
);
// We should be able to find the event using the high level timeline API, and
// inspect who forwarded us the keys to decrypt.
let alice_id = alice.user_id().unwrap();
let alice_display_name =
alice.account().get_display_name().await?.expect("Alice should have a display name");
let bob_timeline = bob_room.timeline().await?;
bob.sync_once().instrument(bob_span.clone()).await?;
let item = assert_event_received(&bob_timeline, &event_id, "Hello Bob").await;
let event = item.as_event().expect("The timeline item should be an event");
assert_eq!(
event.forwarder().expect("We should be able to access the forwarder's ID"),
alice_id.as_str()
);
assert_let!(
Some(TimelineDetails::Ready(profile)) = event.forwarder_profile(),
"We should be able to access the forwarder's profile"
);
assert_eq!(
profile
.display_name
.as_ref()
.expect("We should be able to access the forwarder's display name"),
&alice_display_name
);
// Alice sends a second message, which Bob should receive, but have no forwarder
// info for as it was sent as part of a session they already have.
let event_id = alice_room
.send(RoomMessageEventContent::text_plain("I said Hello, Bob"))
.await
.expect("We should be able to send a message to the room")
.response
.event_id;
bob.sync_once().instrument(bob_span.clone()).await?;
let item = assert_event_received(&bob_timeline, &event_id, "I said Hello, Bob").await;
assert!(
item.as_event().expect("The timeline item should be an event").forwarder().is_none(),
"There should be no forwarder for the second message"
);
Ok(())
}
/// Creates a new encryption-enabled client with the given username and
/// settings.
///
/// # Arguments
///
/// * `username` - The username for the client.
/// * `exclude_insecure_devices` - A boolean indicating whether to exclude
/// insecure devices.
async fn create_encryption_enabled_client(
username: &str,
exclude_insecure_devices: bool,
) -> Result<SyncTokenAwareClient> {
let encryption_settings =
EncryptionSettings { auto_enable_cross_signing: true, ..Default::default() };
@@ -709,6 +868,7 @@ async fn create_encryption_enabled_client(username: &str) -> Result<SyncTokenAwa
.use_sqlite()
.encryption_settings(encryption_settings)
.enable_share_history_on_invite(true)
.exclude_insecure_devices(exclude_insecure_devices)
.build()
.await?,
);
@@ -825,3 +985,36 @@ async fn assert_utd_history_not_shared(timeline: &Timeline, event_id: &EventId)
MissingMegolmSession { withheld_code: Some(WithheldCode::HistoryNotShared) }
);
}
/// Asserts that the given `sync_response` contains exactly one to-device event
/// and that the event is a decrypted room key bundle.
fn assert_received_room_key_bundle(sync_response: matrix_sdk::sync::SyncResponse) {
assert_eq!(sync_response.to_device.len(), 1, "Expected exactly one to-device event");
let to_device_event = &sync_response.to_device[0];
assert_let!(
ProcessedToDeviceEvent::Decrypted { raw, .. } = to_device_event,
"Expected the to-device event to be decrypted"
);
assert_eq!(
raw.get_field::<String>("type").unwrap().unwrap(),
"io.element.msc4268.room_key_bundle",
"Expected the event type to be 'io.element.msc4268.room_key_bundle'"
);
}
/// Start the given client's sync service and attach a new span to track logs.
async fn start_client_sync_service(
span: &tracing::Span,
client: &impl Deref<Target = Client>,
) -> SyncService {
let sync_service_span = tracing::info_span!(parent: span, "sync_service");
let sync_service = SyncService::builder(client.deref().clone())
.with_parent_span(sync_service_span)
.build()
.await
.expect("Could not build sync service");
client.encryption().wait_for_e2ee_initialization_tasks().await;
sync_service.start().await;
sync_service
}