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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user