-
Notifications
You must be signed in to change notification settings - Fork 303
feat(widget): Send custom to-device messages in widgets in e2ee rooms #5252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(widget): Send custom to-device messages in widgets in e2ee rooms #5252
Conversation
29cabcc
to
0b565d0
Compare
Codecov ReportAttention: Patch coverage is
✅ All tests successful. No failed tests found. Additional details and impacted files@@ Coverage Diff @@
## main #5252 +/- ##
==========================================
- Coverage 90.20% 88.75% -1.45%
==========================================
Files 334 330 -4
Lines 105133 88874 -16259
Branches 105133 88874 -16259
==========================================
- Hits 94831 78884 -15947
+ Misses 6238 6176 -62
+ Partials 4064 3814 -250 ☔ View full report in Codecov by Sentry. |
if room_encrypted { | ||
trace!("Sending to-device message in encrypted room <{}>", self.room.room_id()); | ||
|
||
// Attempt to re-batch these by content to batch them up into a single request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is a bit annoying, and the root cause is I believe a bad design choice on the widget API that wants the same structure than /sendToDevice
HTTP API.
It is annoying because the crypto API allows to encrypt a same content for a list of recipients (not a bag of different contents). I added a comment on the MSC matrix-org/matrix-spec-proposals#3819 (comment)
but anyhow, for now we have to invert this map and re-group per similar content (have to serialise for checking that)
/// It is possible that sending to some device failed (failed to encrypt or | ||
/// failed to send) | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
pub failures: Option<BTreeMap<String, Vec<String>>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now there is no reason/code for the failures. Will be handled in a follow-up PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks mostly good, there is some surprising logic that we could make a bit more obvious and perhaps even more performant.
let mut content_grouped: BTreeMap< | ||
&str, | ||
BTreeMap<OwnedUserId, Vec<DeviceIdOrAllDevices>>, | ||
> = BTreeMap::new(); | ||
|
||
for (user_id, device_map) in messages.iter() { | ||
for (device_id, content) in device_map.iter() { | ||
content_grouped | ||
.entry(content.json().get()) | ||
.or_default() | ||
.entry(user_id.clone()) | ||
.or_default() | ||
.push(device_id.to_owned()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is a bit trippy, you are reversing the mapping from user -> device -> content to content -> user -> device.
As far as I understand this is an optimization so we can use the function that encrypts the same content for multiple devices.
Could you document this fact, it isn't immediately obvious why this is needed.
Renaming the variable to content_to_recipients_map
or some similar more obvious name might help as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
response.map_err(Into::into) | ||
// Encrypt and send |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a perfect place where we could introduce a helper function.
This is getting a bit long I would say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client.encryption().get_user_devices(&user_id).await?; | ||
let devices = user_devices.devices(); | ||
devices.for_each(|device| { | ||
device_set.insert(( | ||
device.user_id().to_owned(), | ||
device.device_id().to_owned(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit weird, you are doing the following:
- Get all the devices of the user
- Record the user/device id
- Throw the devices away
- Iterate through each recorded user/device id and load each device individually from the store.
It seems to me that it would be much better to:
- Get all the devices of a user
- If an alias was used, you have your set.
- If a device IDs were used, filter out any device that isn't part of the recipient set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update here 159a7a1
I haven't found the simple way of finding failure like you suggested let failures = recipient_list - recipient_devices
, I tried something else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still feels pretty suboptimal:
- You fetch all devices for each
DeviceId
entry in the list. - You collect a bunch of duplicates, which wastes memory until...
- You deduplicate the list of recipient devices.
What I had in mind would be something like the following diff:
Commit ID: d205fe05c3577be6ca56d4a994dccbc00be52b7b
Change ID: unuvrlxqtqmnuptxnkxukwuoxmknkktl
Author : Damir Jelić <[email protected]> (2025-06-26 12:22:21)
Committer: Damir Jelić <[email protected]> (2025-06-26 13:05:20)
(no description set)
diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs
index 8fd210baa7..8b1df37c11 100644
--- a/crates/matrix-sdk/src/widget/matrix.rs
+++ b/crates/matrix-sdk/src/widget/matrix.rs
@@ -15,7 +15,7 @@
//! Matrix driver implementation that exposes Matrix functionality
//! that is relevant for the widget API.
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use as_variant::as_variant;
use matrix_sdk_base::deserialized_responses::{EncryptionInfo, RawAnySyncOrStrippedState};
@@ -436,37 +436,48 @@
let client = self.room.client();
let mut recipient_devices = Vec::<Device>::new();
- for (user_id, device_id_or_alias) in user_to_list_of_device_id_or_all {
+ for (user_id, recipient_device_ids) in user_to_list_of_device_id_or_all {
let user_devices = client.encryption().get_user_devices(&user_id).await?;
- if device_id_or_alias.contains(&DeviceIdOrAllDevices::AllDevices) {
- recipient_devices.extend(user_devices.devices());
+ let user_devices: Vec<Device> = if recipient_device_ids
+ .contains(&DeviceIdOrAllDevices::AllDevices)
+ {
+ // If the user wants to send to all devices there's nothing to filter and no
+ // need to inspect other entries in the users device list.
+ user_devices.devices().collect()
} else {
- recipient_devices.extend(user_devices.devices().filter(|d| {
- device_id_or_alias
- .contains(&DeviceIdOrAllDevices::DeviceId(d.device_id().to_owned()))
- }));
-
- let missing_devices = device_id_or_alias
- .iter()
- .filter_map(|d| {
- as_variant!(d, DeviceIdOrAllDevices::DeviceId)
- .map(|device_id| device_id.to_owned())
- })
- .filter(|device_id| user_devices.get(device_id).is_none())
- .collect::<Vec<_>>();
-
- if !missing_devices.is_empty() {
- failures.entry(user_id).or_default().extend(missing_devices);
- }
- }
+ // If the user wants to send to only some devices, filter out any devices that
+ // aren't part of the recipient_device_ids list.
+ let filtered_devices = user_devices
+ .devices()
+ .map(|device| (device.device_id().to_owned(), device))
+ .filter(|(device_id, _)| {
+ recipient_device_ids
+ .contains(&DeviceIdOrAllDevices::DeviceId(device_id.clone()))
+ });
+
+ let (found_device_ids, devices): (BTreeSet<_>, Vec<_>) = filtered_devices.unzip();
+
+ let list_of_devices: BTreeSet<_> = recipient_device_ids
+ .into_iter()
+ .filter_map(|d| as_variant!(d, DeviceIdOrAllDevices::DeviceId))
+ .collect();
+
+ // Let's now find any devices that are part of the recipient_device_ids list but
+ // were not found in our store.
+ // TODO: Do we want to do this for the case where all devices are part of the
+ // recipient list, in case someone specified all devices as well as some devices
+ // explicitly?
+ let missing_devices =
+ list_of_devices.difference(&found_device_ids).map(|d| d.to_owned()).collect();
+ failures.insert(user_id, missing_devices);
+
+ devices
+ };
+
+ recipient_devices.extend(user_devices);
}
- // The user-provided list of devices may contain duplicates, ensure no
- // duplicates are sent.
- recipient_devices
- .dedup_by(|da, db| da.user_id().eq(db.user_id()) && da.device_id().eq(db.device_id()));
-
if !recipient_devices.is_empty() {
// need to group by content
let encrypt_and_send_failures = client
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't found the simple way of finding failure like you suggested
let failures = recipient_list - recipient_devices
, I tried something else
Sorry, I meant BTreeSet::difference() but couldn't remember the name of the method.
for (u, d) in device_set { | ||
let found_device = client.encryption().get_device(&u, &d).await?; | ||
let Some(device) = found_device else { | ||
warn!("Device {} not found for user {}", d, u); | ||
// device not found, skip it and log | ||
failures.entry(u).or_default().push(d); | ||
continue; | ||
}; | ||
recipients.push(device); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once you have all the recipient devices collected, getting the failures would be a simple set operation.
let failures = recipient_list - recipient_devices
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not quite to my liking. I left a diff that I think makes things clearer and doesn't load all devices for each user multiple times.
Hopefully it helps out a bit.
Adds the capability for widgets to send encrytped to-device messages properly.
It is getting rid of the old encrypted boolean that was used by widget to request encryption. As said by the base MSC
for widget send/receive messages: Widgets should not be aware of encryption.
If the widget is in an encrytped room then to-device traffic will be encrypted.
Fixes #4859
Signed-off-by: