Compare commits

...

326 Commits

Author SHA1 Message Date
Jonas Platte 3e17fc2072 Add a description for matrix-sdk-ui 2024-01-05 14:23:41 +01:00
Jonas Platte f08e978540 Upgrade uniffi
… and specify a version such that publishing of the ui crate becomes possible.

We still use a git dependency for FFI crate builds because there were some
likely important breaking changes that haven't been released.

This is required to publish `matrix-sdk-ui`.
2024-01-05 14:03:44 +01:00
Jonas Platte 40b09cda2f Bump all of the versions to 0.7.0 2024-01-05 12:58:54 +01:00
Jonas Platte 2710a85897 Rename matrix-sdk-base/{Changelog => CHANGELOG.md}
… for consistency.
2024-01-05 12:58:54 +01:00
Jonas Platte 315e6c9d85 Use workspace dependencies for matrix-sdk-test 2024-01-05 12:58:54 +01:00
Benjamin Bouvier 2822f2471a Update crates/matrix-sdk/src/room/mod.rs
Signed-off-by: Benjamin Bouvier <public@benj.me>
2024-01-05 12:12:52 +01:00
Kévin Commaille b0530ba3a6 sdk: Create ReportedContentScore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-05 12:12:52 +01:00
Kévin Commaille 844212e965 sdk: Add method to report an event
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-05 12:12:52 +01:00
Jonas Platte eb33c3754a bindings: Move matrix_sdk_ui enum definitions out of UDL 2024-01-04 17:14:44 +01:00
Richard van der Hoff cf57992346 indexeddb: logging for open sequence (#2983)
I was trying to figure out what was taking so long, so added some logging.
2024-01-04 15:10:54 +00:00
Jonas Platte d9f99f84f5 Fix Cargo warning about deafult-features
`default-features = false` was already a no-op prior to workspace dependency change,
as `matrix-sdk-crypto` has no default features.
2024-01-04 14:54:33 +01:00
Benjamin Bouvier afd05a24df Rename BaseClient::update_summary to BaseClient::set_room_info 2024-01-04 11:41:50 +01:00
Benjamin Bouvier 0f91eebc96 BaseClient::apply_changes doesn't need to be async 2024-01-04 11:41:50 +01:00
Benjamin Bouvier 0f8b99b744 Remove spurious testing guards in the read status integration test 2024-01-04 11:41:50 +01:00
Benjamin Bouvier 30714c3f92 Don't cause a spurious room info update when setting the latest event 2024-01-04 11:41:50 +01:00
Benjamin Bouvier 96b427a332 Add test showing the spurious room info update 2024-01-04 11:41:50 +01:00
Jonas Platte 1c7bf820bf Use workspace dependencies for crates/* dependencies
… except from examples (such that they remain copy-pastable).
2024-01-04 10:02:07 +01:00
Jonas Platte 24b879bbc0 Clean up Cargo manifest formattting 2024-01-04 10:02:07 +01:00
Jonas Platte 51a0bb6f3b ci: Upgrade typos action 2024-01-04 09:53:17 +01:00
Jonas Platte 9eca314511 Fix a typo 2024-01-04 09:53:17 +01:00
Jonas Platte c4724c082e Upgrade dependencies 2024-01-02 19:12:42 +01:00
manuroe 3d9d3b7ca6 Merge pull request #2977 from matrix-org/valere/long_mark_as_sent
Remove an expensive database call only used for tracing prupose
2024-01-02 13:35:36 +01:00
Valere 541d9184c6 remove expensive db call for tracing 2023-12-22 16:19:25 +01:00
Benjamin Bouvier 45bdbf5067 Add test to make sure notification count is taken into account when processing sliding sync 2023-12-22 14:45:54 +01:00
Benjamin Bouvier 04ef3d9f95 Fix: revert notification count change from read receipts PR
This was modified and then removed from the PR, and forgot to put back the previous
implementation.
2023-12-22 14:45:54 +01:00
Benjamin Bouvier 75fe874cae read receipts: don't update a RoomInfo if the read receipts haven't changed 2023-12-21 17:41:35 +01:00
Benjamin Bouvier 2a77aaa068 read receipts: update the API shape of compute_notifications 2023-12-21 15:54:19 +01:00
Benjamin Bouvier 4a686229e1 read receipts: make find_and_count_events a method of RoomReadReceipts 2023-12-21 15:54:19 +01:00
Benjamin Bouvier d64fb8241d read receipts: add test for find_and_count_events 2023-12-21 15:54:19 +01:00
Benjamin Bouvier 870d1eafb4 read receipts: add RoomReadReceipts::reset 2023-12-21 15:54:19 +01:00
Benjamin Bouvier a9905eaedd read receipts: don't count the same action multiple time per event 2023-12-21 15:54:19 +01:00
Benjamin Bouvier f1c15da87d read receipts: make count_unread_and_mention a method of RoomReadReceipts and rename it 2023-12-21 15:54:19 +01:00
Benjamin Bouvier f2b53080b6 read receipts: move RoomReadReceipts to the read_receipts.rs file 2023-12-21 15:54:19 +01:00
Benjamin Bouvier 7fe4e22076 read receipts: test count_unread_and_highlights 2023-12-21 15:54:19 +01:00
Benjamin Bouvier b6dab1a3a5 read receipts: compute_notifications doesn't need to be async 2023-12-21 15:54:19 +01:00
Andy Balaam ec833c81e0 Migrate inbound_group_session2 to fix keys incorrectly copied from old store version (#2957)
In migrate_data_for_v6, we incorrectly copied the keys in inbound_group_sessions verbatim into inbound_group_sessions2. What we should have done is re-encrypt them using the new table name, so we fix that up with a new migration here.

This caused the bug because we were looking for sessions to mark as backed up by calculating their key (from room_id and session_id) but that key did not exist, because the old sessions were stored under the incorrect keys. So no sessions were marked as backed up, and we repeatedly tried to re-mark them.
2023-12-21 14:52:02 +00:00
Ivan Enderlin de0574aa14 Merge pull request #2925 from zecakeh/create-dm-encrypted
client: Allow to create encrypted DM
2023-12-21 14:38:32 +01:00
Benjamin Bouvier fb1ff70538 Disable integration test in code coverage build
The test fails only in the codecov build, not in a local build or in the other integration test.

Needs further investigation.
2023-12-21 11:55:29 +01:00
Benjamin Bouvier d6bcbf2281 Address review comments
- copyright notice
- doc comments and better doc in general
- use static dispatch instead of &dyn T
- other misc comments
2023-12-21 11:55:29 +01:00
Benjamin Bouvier cbc832411d read receipts: add an extra num_unread_notifications field
This helps supporting cases where we want to show that a room has some activity (unread messages) but no notifications.
2023-12-21 11:55:29 +01:00
Benjamin Bouvier 843fcac3c1 read receipts: don't clone cached events for computing the read receipt state
Before this patch, we needed to clone the inner `timeline_queue` and turn it into a concrete `Vec<SyncTimelineEvent>`, just to iterate on the elements,
and because returning an iterator from a trait method is impractical. This now changes it to return the actual concrete type of `timeline_queue`, so
we don't need the extra allocations.

Ideally, matrix-sdk and matrix-sdk-base would be merged, so we don't need to use a trait at all here.
2023-12-21 11:55:29 +01:00
Benjamin Bouvier b3a8f34655 read receipts: move the unread messages and mentions counts to separate fields of RoomInfo 2023-12-21 11:55:29 +01:00
Benjamin Bouvier 73af3d9cfa Make it clear that some functions are tests or test helpers only 2023-12-21 11:55:29 +01:00
Benjamin Bouvier 09e355a7bc read receipts: add integration tests for read receipts 2023-12-21 11:55:29 +01:00
Benjamin Bouvier 9ba09b2ae8 sync: process unread notification count client-side 2023-12-21 11:55:29 +01:00
Benjamin Bouvier c9b02ad068 doc: document SlidingSyncRoomInner::timeline_queue a bit better 2023-12-21 11:55:29 +01:00
Benjamin Bouvier 07c428ec56 sdk base: add latest_read_receipt_event_id field to RoomInfo 2023-12-21 11:55:29 +01:00
Valere d4b8f88e10 Fix regenerate_olm_machine losing backup state (#2961)
* Fix regenerate_olm_machine loosing backup state

* use new helper

* clippy fix

* quick review
2023-12-21 09:43:55 +01:00
Stefan Ceriu 28a58479e7 Bring back original (slower) build style under the sequentially fla… (#2960)
* Bring back original (slower) build style under the `sequentially` flag to work around new build style hanging on older machines

* Apply suggestions from code review

Signed-off-by: Benjamin Bouvier <public@benj.me>

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2023-12-21 08:15:21 +00:00
Benjamin Bouvier 320b868694 style: remove spurious fully-qualified path 2023-12-18 15:47:45 +01:00
Benjamin Bouvier 8615b1283a sliding sync: don't cause a spurious RoomInfo update
The room_info variable in this function contains the latest_event, and later it's set as the Room's room info (`inner.inner`) field.
Calling `set_latest_event` manually here will cause an update of the `RoomInfo` subscriber, while we're not done processing the full
room, and a room info update will happen anyways later (when the entire room is processed), so this one is spurious and will only
show a partial update of the fields.
2023-12-18 15:47:45 +01:00
Benjamin Bouvier 9a4d539428 sliding sync: optimize finding the room member state event
- We can look up the session meta only once, it's not useful to do it once per event as it's
loop-invariant.
- We can look at the events backwards and stop after the first room membership event we see
in that order.
2023-12-18 15:47:45 +01:00
Kévin Commaille 4d3fc44425 Put imports behind feature
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-18 14:07:01 +01:00
Kévin Commaille 8b878c6591 Fix test
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-18 14:05:01 +01:00
Kévin Commaille 738f7f0336 client: Create DM as encrypted by default
If `e2e-encryption` feature is enabled.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-18 13:04:39 +01:00
Valere f5fb44bd80 Maybe trigger backup at end of sync (#2939)
* maybe trigger backup at end of sync

* add test for backup upload

* missing e2e cfg

* add sliding sync tests

* fix borrow

* fix indent

* fix naming from copy/paste

* Remove extracted function

* fix clippy

* style: inline a few variables only used once into their use sites

---------

Co-authored-by: Benjamin Bouvier <public@benj.me>
2023-12-18 10:41:44 +00:00
Doug e0ba9f5a22 xtask: Use Uniffi Library mode for Kotlin too. 2023-12-18 10:34:38 +01:00
Doug b5aeea0a3b xtask: Handle multiple headers/modulemaps. 2023-12-18 10:34:38 +01:00
Ivan Enderlin 19526cea6b Merge pull request #2928 from zecakeh/qrcode-changes
verification: Expose the `QrVerificationState` and the stream for its changes
2023-12-15 12:52:50 +01:00
Ivan Enderlin 99d0b37914 Merge pull request #2951 from matrix-org/andybalaam/clarify-meaning-of-pendingbackup-sessions
Clarify the meaning of the sessions field in PendingBackup
2023-12-15 12:45:29 +01:00
Andy Balaam b21a438b7e Merge pull request #2934 from matrix-org/andybalaam/mark_sessions_as_backed_up
Provide `CryptoStore::mark_inbound_group_sessions_as_backed_up` on stores and use it in `BackupMachine::mark_request_as_sent`
2023-12-15 11:40:50 +00:00
Andy Balaam f3101baa08 Merge branch 'main' into andybalaam/mark_sessions_as_backed_up 2023-12-15 10:47:36 +00:00
Andy Balaam 0cf99db001 Delete accidentally-copied repeat_vars function
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-15 10:47:11 +00:00
Andy Balaam a032d33d21 Remove comments in favour of a separate PR with type aliases
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-15 10:40:36 +00:00
Andy Balaam 92212cc328 Clarify the meaning of the sessions field in PendingBackup
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-15 10:38:56 +00:00
Kévin Commaille 6ffb0181e4 Make sure qrcode feature is additive
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-15 11:01:03 +01:00
dependabot[bot] 614bf942c0 build(deps): bump zerocopy from 0.7.26 to 0.7.31
Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.26 to 0.7.31.
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.7.26...v0.7.31)

---
updated-dependencies:
- dependency-name: zerocopy
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 10:39:21 +01:00
Andy Balaam 512509ce8b Switch to using chunk_large_query_over in mark_inbound_group_sessions_as_backed_up
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 16:56:13 +00:00
Andy Balaam dfb33c9534 Move chunk_large_query_over into SqliteObjectExt so it can be reused
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 16:55:36 +00:00
Andy Balaam 03aad4e965 Move repeat_vars into utils so it can be reused
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 16:34:45 +00:00
Andy Balaam 9e67b6fcb1 Merge branch 'main' into andybalaam/mark_sessions_as_backed_up 2023-12-14 15:25:43 +00:00
Ivan Enderlin 9443f455a4 Test Media caching, and re-implement it inside MemoryStore
Test Media caching, and re-implement it inside `MemoryStore`
2023-12-14 15:51:04 +01:00
Ivan Enderlin 384deec1c8 chore(base): Simplify code. 2023-12-14 15:39:24 +01:00
Kévin Commaille 5310f41cda integration-testing: Add test for QR verification
And check room ID in SAS test.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 15:23:16 +01:00
Ivan Enderlin 95a1db8fc7 Merge pull request #2947 from matrix-org/andybalaam/use-correct-limit-in-chunk_large_query_over
Use the limit() method to find the variable limit in chunk_large_query_over
2023-12-14 14:31:40 +01:00
Ivan Enderlin 3d9dffa3b7 chore(base): Remove unused imports. 2023-12-14 14:18:56 +01:00
Andy Balaam 3d7e1d5989 Merge branch 'main' into andybalaam/use-correct-limit-in-chunk_large_query_over 2023-12-14 13:17:06 +00:00
Ivan Enderlin a46bf76d74 test(base): Improve test_media_content.
This patch improves `test_media_content` to ensure that, in case of
multiple medias, only the expected ones are removed. Previously, the
test wasn't testing _other_ medias that should be kept in case of
removals.

This patch continues to improve `test_media_content` to ensure that the
content of the media are the expected ones.

Finally, this patch updates the `MemoryStore` implementation to make
tests happy.
2023-12-14 14:15:01 +01:00
Ivan Enderlin 326935db63 feat(base): Correct implementations for MemoryStore media removal.
This patch rewrites `MemoryStore::add_media_content`,
`::get_media_content`, `::remove_media_content` and
`::remove_media_content_for_uri` to (i) work on `mxc://` URI instead
of “unique key”, and (ii) to handle removal correctly thanks to the new
`RingBuffer::remove` method.
2023-12-14 13:54:12 +01:00
Ivan Enderlin 416bc8b0e4 feat(base): Implement Media::uri.
This new method returns the `MxcUri` associated to the `Media`.
2023-12-14 13:53:23 +01:00
Ivan Enderlin edd113a17c feat(common): Implement RingBuffer::remove. 2023-12-14 13:28:17 +01:00
Ivan Enderlin 66411d7b2e Merge pull request #2946 from matrix-org/andybalaam/tests-for-repeat_vars
Unit tests for the repeat_vars function
2023-12-14 13:10:12 +01:00
Ivan Enderlin 4a53bf1f3d Merge pull request #2945 from matrix-org/andybalaam/limits-access-for-sqlite
Provide a limit() method in sqlite to find limit values
2023-12-14 13:10:02 +01:00
Andy Balaam a3aa55ce91 Use the limit() method to find the variable limit
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 11:31:01 +00:00
Andy Balaam 0066ae6614 Unit tests for the repeat_vars function
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 11:26:43 +00:00
Andy Balaam 17ebc23719 Provide a limit() method in sqlite to find limit values
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-14 11:21:25 +00:00
Ivan Enderlin cdb3449ce2 test(sdk): Test that Media::get_media_content with caching works. 2023-12-14 12:11:24 +01:00
Ivan Enderlin c5b11fc2f8 feat(sdk): Simplify code from Media::get_media_content.
If `use_cache` is true and the cache exists, let's return everything in
one go instead of declaring a `content` variable. It makes the code
easier to read and to understand.
2023-12-14 12:10:08 +01:00
Ivan Enderlin 2ad6acb930 doc(common): Add documentation for RingBuffer::drain. 2023-12-14 12:07:08 +01:00
Ivan Enderlin dac779f4fc test(base): Re-implement a super basic media cache for MemoryStore.
This is a rather over simplistic media cache implementation for the
`MemoryStore`.

It's based on a `RingBuffer` of size 20.

`remove_media_content` pops all medias until the correct one is met (if
it exists). `remove_media_content_for_uri` removes all medias, it
ignores the URI.
2023-12-14 12:04:58 +01:00
Kévin Commaille e2ea19ee77 Add rule kind to error messages
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille cc38768bf4 notification settings: Don't error if poll start rules are not found
These rules are unstable so they might not be
found in every ruleset.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille c87bd4d4ec notification settings: Log errors from requests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille 5700c700f0 error: Use NotificationSettingsError::RuleNotFound's rule ID in display impl
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille fff9882792 notification settings: Rely more on Ruma methods
Simplifies code.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille 45f8ff11c2 notification settings: Derive Copy for enum types
This a good practice for inexpensive types
and avoids to have to call `.clone()` explicitely.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille 7c9d842d05 notification settings: Use private method to get poll start rule ID
A `From` implementation is part of the public API
and this conversion does not make sense outside of the module.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-14 11:39:42 +01:00
Kévin Commaille c3706d7ca0 notification settings: Allow to manage keywords (#2905)
* notification settings: Allow to manage keywords
* Fix wording of docs

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
Signed-off-by: Kévin Commaille <76261501+zecakeh@users.noreply.github.com>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2023-12-14 11:16:40 +01:00
Ivan Enderlin b314da37b8 feat(ffi): Timeline::send_image and send_video takes an optional thumbnail_url
feat(ffi): `Timeline::send_image` and `send_video` takes an optional `thumbnail_url`
2023-12-14 10:48:16 +01:00
Ivan Enderlin 02ba6c0dbe feat(ffi): Client::*media* are now async
feat(ffi): `Client::*media*` are now async
2023-12-14 09:14:39 +01:00
Ivan Enderlin 22d9c62262 feat(ffi): Timeline::send_image and send_video takes an optional thumbnail_url.
This patch updates `Timeline::send_image` and `Timeline::send_video` so
that `thumbnail_url` is now an `Option<String>`.

The idea is to allow sending an image or a video without a thumbnail.
2023-12-13 17:07:03 +01:00
Ivan Enderlin 18b6387a7a Merge pull request #2935 from matrix-org/rav/fix_nonmonotonic_panic
Configure `Instant` wasm polyfill to use monotonic time
2023-12-13 15:57:32 +01:00
Ivan Enderlin c5ddba2e13 feat(ffi): Client::*media* are now async.
This patch removes `RUNTIME.block_on` inside `Client::get_media_file`,
`::upload_media`, `::get_media_content` and `::get_media_thumbnail`, and
makes those methods async.
2023-12-13 15:33:01 +01:00
Kévin Commaille 9756ed28cf verification: Expose the room ID where the verification is happening
Useful if we want to know in what room
we just sent a VerificationRequest,
with UserIdentity::request_verification

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-13 15:22:13 +01:00
Richard van der Hoff 5701cea51e Configure Instant wasm polyfill to use monotonic time
With the `inaccurate` feature, this polyfill uses `Date.now()` to emulate
`Instant`, which is not monotonic, causing problems like
https://github.com/element-hq/element-web/issues/26416.
2023-12-13 12:08:54 +00:00
Andy Balaam 794b7ead7a Clippy fixes 2023-12-13 08:43:59 +00:00
Andy Balaam f879f3d866 Formatting 2023-12-13 08:32:00 +00:00
Andy Balaam 4ebb8e29b4 Provide CryptoStore::mark_sessions_as_backed_up on stores and use it in BackupMachine::mark_request_as_sent
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2023-12-12 16:09:22 +00:00
Doug d764fca7da xtask: Move all the Swift files found
… and fix build swift errors.

- The XCFramework was being built with dylibs which aren't supported on iOS
- The tests build was attempting to generate uniffi from a moved file
2023-12-12 16:51:49 +01:00
Jonas Platte 3b7f9f7361 Reapply "bindings: Use new uniffi-bindgen build mode"
This reverts commit 0d24bcf6e5.
2023-12-12 16:51:49 +01:00
Doug 73770b78bb room: Add test for unban_user. 2023-12-12 16:13:09 +01:00
Doug 7cffc34984 ffi: Support banning, unbanning and kicking users. 2023-12-12 16:13:09 +01:00
Benjamin Bouvier c4ef967523 dx: tweak debugging of user/device pairs missing a session 2023-12-11 17:11:11 +01:00
Kévin Commaille f0b378179e matrix auth: Await save_session_callback
Otherwise the future will not run.
Changes the corresponding test to fail with the old behavior.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-11 16:54:53 +01:00
Kévin Commaille ee344112f3 verification: Expose the QrVerificationState and the stream for its changes
Allows to have a similar API as SasVerification

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-11 16:13:56 +01:00
Benoit Marty 1f52aca210 Fix typo in doc. 2023-12-11 12:13:05 +01:00
Kévin Commaille 74091de8ef sdk: Make sure "testing" feature is enabled for integration tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-11 11:53:25 +01:00
Kévin Commaille 0f6efc391a client: Allow to create encrypted DM
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-11 11:14:19 +01:00
Mauro c707e1f17e feat(bindings): expose a function to send private read receipts (#2906) 2023-12-11 09:29:06 +00:00
Benjamin Bouvier 5ab69f7400 ffi: add emoji indices in the session verification data + use decimals as a fallback 2023-12-08 14:48:48 +01:00
Jonas Platte e652069896 sdk: Use crates.io release of mas-oidc-client 2023-12-07 17:34:36 +01:00
Benjamin Bouvier 5337c9d9ea refactoring: make it clear that notifications aren't stored in the state store
`notifications` were stored in the `StateChanges` struct, which made me think that they're then persisted in the database. It's not the case, they
were just stored there by convenience. This commit changes it to a parameter that's passed, as it's not too invasive and clearer that this is only
transient data.
2023-12-07 16:56:18 +01:00
Benjamin Bouvier 9fd52e5df7 timeline: document event_filter better 2023-12-07 10:35:42 +01:00
Benjamin Bouvier 4ab79a4085 timeline: add the room version in the event_filter parameters 2023-12-07 10:35:42 +01:00
Benjamin Bouvier a132c0a885 read receipts: apply a default event filter only for events that get rendered 2023-12-07 10:35:42 +01:00
Benjamin Bouvier 5081802177 room: try restoring from a key backup during backpagination too 2023-12-05 16:53:06 +01:00
Stefan Ceriu 736188811f ffi: expose the call member state event type for clients to check if users can join calls 2023-12-05 15:51:28 +01:00
Benoit Marty 1337fdf0b8 ffi: Expose is_invite_for_me_enabled and set_invite_for_me_enabled in notification settings 2023-12-04 17:04:19 +01:00
Ivan Enderlin 0573137835 Merge pull request #2902 from matrix-org/rav/outgoing_key_requests_switch
Crypto: add new methods for turning off room key requests
2023-12-04 15:12:33 +01:00
Ivan Enderlin 21ce2b07a7 Merge pull request #2904 from matrix-org/jme/add-typing-notice-bindings
Add FFI bindgings for `Room.typing_notice()`
2023-12-04 15:09:46 +01:00
Richard van der Hoff 6451a9dffe rename toggle -> set 2023-12-04 12:53:02 +00:00
Kévin Commaille a6af31984a Assert that the stream is pending
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Kévin Commaille 2de45bcb51 notification settings: Do not expose NotificationSettings::new in the public API
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Kévin Commaille 7bdc1448bd notification settings: Drop event handler only when last instance is dropped
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Kévin Commaille 511c44c588 Add test for NotificationSettings::subscribe_to_changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Kévin Commaille a915900580 notification settings: Allow to subscribe to changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Kévin Commaille e12f6fcbb7 notification settings: Accept any type that implements AsRef<str> as rule_id
Simplifies the use of this API with the Predefined{*}RuleId Ruma types.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-12-04 13:30:43 +01:00
Jorge Martín 72dfd3d1fd Add FFI bindgings for Room.typing_notice() 2023-12-01 16:19:32 +01:00
Richard van der Hoff a6c206118d changelog 2023-12-01 12:50:43 +00:00
Richard van der Hoff 2536373546 Add new methods for turning off room key requests 2023-12-01 12:47:44 +00:00
Jorge Martín 045d94ab4b Rename message inside an error, since it clashes with Kotlin's default message property in exceptions 2023-12-01 12:07:40 +01:00
Benjamin Bouvier d8c02d7e55 timeline builder: tweak comments 2023-12-01 11:05:53 +01:00
Benjamin Bouvier b20313c492 read receipts: rename ReadReceipts fields
The "read_receipts" suffix isn't useful since we're looking at the fields of `ReadReceipts` already.
2023-12-01 11:05:53 +01:00
Benjamin Bouvier c3ea2c3736 Format with rustfmt nightly. 2023-12-01 11:05:53 +01:00
Benjamin Bouvier 2c1377b2b5 read receipts: a few function renamings and add getter/setter for receipts on events 2023-12-01 11:05:53 +01:00
Benjamin Bouvier bb85af9279 read receipts: add getter/setter for the latest read receipts cache 2023-12-01 11:05:53 +01:00
Benjamin Bouvier 58ab6704ff read receipts: a few function renamings
Add the prefix `load` to make it clear this may load from the storage.
2023-12-01 11:05:53 +01:00
Benjamin Bouvier 94a64296d1 Show the wrapped error when displaying a SlidingSync::JoinError 2023-11-30 19:52:20 +01:00
Benjamin Bouvier 7d6c16956f fix: reify reformat of missing_session_devices_by_user
`itertools::format` is known to cause issues when its display implementation is being
used multiple times, as it consumes the iterator it was given (and that can only happen once,
unless caching it). This is bad, as our production apps may have multiple subscribers they will
run into a panic.

The fix is to reify the debug string before it's logged, so the tracing consumer will only see
a string, and not the display implementation that would panic on the second use.
2023-11-30 19:52:20 +01:00
Damir Jelić 916bf69e5c Enable backups and set the backup download strategy for the bindings 2023-11-30 16:51:06 +01:00
Damir Jelić 915c10e1b4 Test for the one-by-one download of room keys 2023-11-30 16:20:51 +01:00
Damir Jelić 5e122c5c5e Add a background task which downloads room keys from the backup one-by-one 2023-11-30 16:20:51 +01:00
Damir Jelić 49e5461ef7 Add a method to check if we have a room key stored locally 2023-11-30 16:20:51 +01:00
Damir Jelić 27927c676d Allow the to create a FailuresCache with different values for the max timeout 2023-11-30 16:20:51 +01:00
Damir Jelić ac5a9e106b Move the FailuresCache into the common crate 2023-11-30 16:20:51 +01:00
Damir Jelić 94c4e685fc Test for recovery 2023-11-30 15:12:22 +01:00
Damir Jelić f248e272e9 Expose the recovery stuff in the bindings 2023-11-30 15:12:22 +01:00
Damir Jelić 97026fc3a6 Recovery support
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2023-11-30 15:12:22 +01:00
Richard van der Hoff 1fcd5af526 indexeddb: Update storage for inbound_group_sessions (#2885)
Currently, querying for inbound group sessions which need backing up is very
inefficient: we have to search through the whole list.

Here, we change the way they are stored so that we can maintain an index of the
ones that need a backup.

Fixes: https://github.com/vector-im/element-web/issues/26488
Fixes: https://github.com/matrix-org/matrix-rust-sdk/issues/2877

---

* indexeddb: Update storage for inbound_group_sessions

Currently, querying for inbound group sessions which need backing up is very
inefficient: we have to search through the whole list.

Here, we change the way they are stored so that we can maintain an index of the
ones that need a backup.

* Rename functions for clarity

* Remove spurious log line

This was a bit verbose

* Rename constants for i_g_s store names

* improve log messages

* add a warning

* Rename `InboundGroupSessionIndexedDbObject.data`

* formatting
2023-11-30 12:01:20 +01:00
Benjamin Bouvier 8b04db666c Remove room.rs file added by mistake
Thanks @jaller94 for noticing and letting me know :)
2023-11-30 10:27:25 +01:00
Timo 1ff2c5bb3e Add recommended vscode settings in contrib/ide 2023-11-28 13:41:04 +01:00
Jonas Platte 05f0106e06 ui: Improve logging for sync update processing 2023-11-27 18:55:21 +01:00
Jonas Platte fd0f369f75 ui: Add extra tracing spans for reactions 2023-11-27 18:55:21 +01:00
Jonas Platte 39fc283353 ui: Improve logging for redactions 2023-11-27 18:55:21 +01:00
Jonas Platte 3ebd8afa49 ui: Add more logging for Timeline::retry_send 2023-11-27 18:55:21 +01:00
Jonas Platte 2483ba2cc6 ui: Raise log level for local events
Local events don't happen as often, so we can afford a higher log level.
2023-11-27 18:55:21 +01:00
Jonas Platte 2d3a458a08 ui: Improve logging for sending attachments 2023-11-27 18:55:21 +01:00
Jonas Platte 246a128ec3 ui: Add logging for send-event cancellation 2023-11-27 18:55:21 +01:00
Jonas Platte 932f12e76d ui: Improve logging for timeline resets 2023-11-27 18:55:21 +01:00
Richard van der Hoff bfe79468c6 Indexeddb: Groundwork for fixing inbound_group_session lookups (#2884)
A set of non-functional changes which lay some groundwork in preparation for fixing vector-im/element-web#26488.
2023-11-27 15:59:49 +00:00
Timo 9503eb49c7 ffi: Expose power level overwrites on room creation 2023-11-27 14:38:35 +00:00
Marco Romano ded854425a timeline: Add poll history API
Allow to retrieve the Poll history of a Room.
The poll history is a Timeline instance that filters only on poll events.
2023-11-27 14:28:40 +00:00
Jonas Platte 959e90252b ffi: Create separate timeline object, mirroring the Rust API 2023-11-27 11:55:48 +01:00
Jonas Platte e761ad8f97 ffi: Remove remove_timeline method
It was somewhat of a footgun because it affected the cached timeline in
`RoomListItem`s as well and is not used anywhere anymore.
2023-11-27 11:55:48 +01:00
Jonas Platte ceeb5e78b6 ffi: Move more things into ruma module 2023-11-24 19:25:44 +01:00
Jonas Platte 04c4284b33 ffi: Split timeline into smaller modules 2023-11-24 19:25:44 +01:00
Jonas Platte bae191b4ed ffi: Move Ruma wrappers / extension traits to new module 2023-11-24 19:25:44 +01:00
Damir Jelić ea2e85c5f5 feat: Support for server-side key backups #2666 2023-11-24 18:16:42 +01:00
Damir Jelić d6401ef278 When disabling backups first delete it from the server 2023-11-24 18:01:05 +01:00
Damir Jelić 9bba437fdd Simplify the secret inbox handling for backups 2023-11-24 18:01:05 +01:00
Damir Jelić 19e65c05cf Remove the CheckingIfUploadNeeded UploadState variant 2023-11-24 18:01:05 +01:00
Damir Jelić 369ca7024f Apply suggestions from code review
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
2023-11-24 18:01:05 +01:00
Damir Jelić e958b1ce28 Don't run the event handler examples 2023-11-24 18:01:05 +01:00
Damir Jelić 4912cd8a40 Typos please 2023-11-24 18:01:05 +01:00
Damir Jelić c99b0e8344 Fix some clippy warnings 2023-11-24 18:01:04 +01:00
Damir Jelić b38f501902 Add an example for the room key backup support 2023-11-24 17:59:00 +01:00
Damir Jelić f37467f81f Add tests for backups 2023-11-24 17:59:00 +01:00
Damir Jelić 6239231ba0 Try to resume backups if we restore the client
Co-authored-by: Benjamin Bouvier <public@benj.me>
2023-11-24 17:59:00 +01:00
Damir Jelić aa1623b891 Add a hack so the timeline retries to decrypt if we receive room keys from backup 2023-11-24 17:59:00 +01:00
Damir Jelić 99131d0d7a Fetch the backup recovery key when we import all known secrets 2023-11-24 17:59:00 +01:00
Damir Jelić c5c62d8fda Add a client task that will upload room keys to the backup 2023-11-24 17:58:58 +01:00
Damir Jelić b909f4400d Add support for backups 2023-11-24 17:56:09 +01:00
Damir Jelić 18d69f7515 Add a ChannelObservable 2023-11-24 17:53:33 +01:00
Richard van der Hoff 456d8bb4f2 Reduce logspam during encryption (#2859)
A few different changes to reduce the number of lines that get logged during an
encryption operation.
2023-11-24 14:21:01 +00:00
Jonas Platte b277423237 sdk: Upgrade mas-oicd-client 2023-11-24 15:01:17 +01:00
Jonas Platte 4621dd4317 ffi: Upgrade opentelemetry 2023-11-24 15:01:17 +01:00
Jonas Platte 17797da71a indexeddb: Upgrade dependencies 2023-11-24 15:01:17 +01:00
Jonas Platte eec4227dfc sdk: Upgrade async-related dependencies 2023-11-24 15:01:17 +01:00
Jonas Platte 64ddfd872c sqlite: Upgrade rusqlite / deadpool-sqlite 2023-11-24 15:01:17 +01:00
Jonas Platte 19a990ad41 Update Cargo.lock
The reqwest update is held back to avoid linking issues on iOS.
2023-11-24 15:01:17 +01:00
Jonas Platte 53ad6f5fe5 Upgrade itertools 2023-11-24 15:01:17 +01:00
Jonas Platte 8038414de2 Raise minimum version for Ruma
Otherwise the matrix-sdk crate can be used with an older version of
ruma-client-api which uses different types for a sliding sync type.

The sliding sync breaking change was allowed to be part of a patch
release because sliding sync is an unstable feature in Ruma.
2023-11-24 15:01:17 +01:00
Jonas Platte 560d71ceea sdk: Implement a custom event formatter for javascript logging
Somehow `fmt::pretty` manages to be both overcomplicated and not flexible
enough.

The driver here is that I want to put the event "fields" on a separate line to
the message.
2023-11-24 13:48:35 +01:00
Richard van der Hoff 6591a6ef04 Apply suggestions from code review
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2023-11-24 12:35:36 +00:00
Benjamin Bouvier 35bac2a6c3 test: add an integration test for left rooms 2023-11-23 15:17:26 +01:00
Benjamin Bouvier 7455b90d24 Tweak debug message wording for a timeline event
Probably from a bad copy-pasta with a poll event.
2023-11-23 15:11:54 +01:00
Benjamin Bouvier 1e359576ad room list service: add a new filter to get all rooms but the left ones
This allows to get a list of all the rooms except for left ones, since they're still part of a sliding sync server response.
2023-11-23 15:06:00 +01:00
Alfonso Grillo fe02752f29 fix: EventTimelineItem.is_editable() respects poll’s preconditions for editing (#2875)
This PR fixes the `EventTimelineItem.is_editable()` function for polls.

Before this changes it always returned false.
Now it consider poll's preconditions for editing:
- The poll has no votes yet
- The poll hasn't an end event
2023-11-23 14:03:25 +00:00
Jonas Platte aa7d2a21a3 test: Fix new clippy lints 2023-11-23 14:15:21 +01:00
Jonas Platte 337f2ad415 Upgrade nightly toolchain used for ci, xtask 2023-11-23 14:15:21 +01:00
Jonas Platte c5c3850edf Use the same nightly toolchain for all xtask commands 2023-11-23 14:15:21 +01:00
Richard van der Hoff 086e988e68 crypto: Be consistent about /keys/query endpoint name
Sometimes, we called this `keys query`, sometimes `/keys/query`, and sometimes,
just for variety, `keys/query`. Generally in Matrix we talk about `/keys/query`
so let's standardise on that.
2023-11-22 14:02:39 +00:00
Val Lorentz 5c37acb81c sdk: Add method to get the set of parent spaces of a room 2023-11-22 14:31:52 +01:00
Richard van der Hoff 2aaa709b0e Reinstate tracing instrumentation on ReadOnlyDevice::encrypt (#2873)
Until https://github.com/matrix-org/matrix-rust-sdk/pull/2862,
`GroupSessionManager::encrypt_session_for` called `Device::encrypt` (via
`Device::maybe_encrypt_room_key`.

`Device::encrypt` is a thin wrapper for `ReadOnlyDevice::encrypt`, but it also
has an `instrument` annotation.

https://github.com/matrix-org/matrix-rust-sdk/pull/2862 short-cuts
`Device::encrypt` and calls `ReadOnlyDevice::encrypt` directly: that was
functionally fine but of course means that we no longer benefit from the
`instrument` annotation.

This PR rectifies the situation by pushing the annotation down to
`ReadOnlyDevice::encrypt`. It also adds some documentation for that function,
since we are using it in more places now.

(Longer-term, I think we should probably aim to get rid of `Device::encrypt`
altogether, but that's a refactor I don't want to take on today.)
2023-11-22 13:04:36 +00:00
Alfonso Grillo 8fe501c4ed ui: Add poll editing API 2023-11-22 13:46:37 +01:00
Stefan Ceriu 79c020f169 Switch from building each target individually to letting cargo figure out the optimal way 2023-11-22 12:33:26 +01:00
Damir Jelić bb4254b9c9 Prepare our /keys/claim response handling to handle multiple OTKs (#2870)
This is useful if we ever decide to switch to X3DH for the session
establishment. It also refactors a bit the /keys/claim response handling
method.

Co-authored-by: Jonas Platte <jplatte@matrix.org>
2023-11-22 10:41:08 +01:00
Damir Jelić ee0010e831 Make the mocks in some notification tests a bit more specific
The mocks in some notification tests mock any PUT/DELETE request without
the request to hit a certain path. These mocks then might respond to
unintended new requests the client might make.

The tests also assert a bunch of things manually instead of using
expectations when building the mock and calling server.verify().
2023-11-22 10:02:32 +01:00
Richard van der Hoff c9ddf92e25 Disable coverage checking for js_tracing 2023-11-22 07:37:21 +00:00
Jonas Platte c7712b52e6 crypto: Use Option instead of JsOption
We don't need to distinguish null from an absent field in any of the
places we used JsOption, so a regular Option is better.
2023-11-21 16:35:07 +01:00
Richard van der Hoff ead29388f2 crypto: Add instrument annotations to various methods 2023-11-21 14:31:41 +00:00
Benjamin Bouvier 06c9a1a355 Update crates/matrix-sdk/src/sliding_sync/room.rs
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2023-11-21 14:50:04 +01:00
Benjamin Bouvier 0ed4a9950c test: add a unit test for the group avatar being set and unset 2023-11-21 14:50:04 +01:00
Benjamin Bouvier 844340e0f7 sliding sync: fix unsetting the avatar in a sliding sync room
The meaning of "null" and "undefined" were conflated by Ruma, previously. This updates to the latest Ruma, which changed the `avatar` type from
`Option` to `JsOption` and thus allowed us to distinguish both cases: `null` means the avatar has been unset, `undefined` means it's not changed
since the previous request.
2023-11-21 14:50:04 +01:00
Benjamin Bouvier 692be61043 test: add an integration test for avatar in group conversations 2023-11-21 14:50:04 +01:00
Jonas Platte d0b6771251 crypto: Simplify double reference handling 2023-11-21 13:15:27 +01:00
Jonas Platte 47298b9200 test: Fix indentation 2023-11-21 13:15:27 +01:00
Jonas Platte a262cb23d9 Fix rustc, clippy warnings 2023-11-21 13:15:27 +01:00
Richard van der Hoff a881cd0712 crypto: Improve logging in receive_keys_claim_response 2023-11-21 11:40:48 +01:00
Richard van der Hoff c536760fc1 Improve performance of share_room_key (#2862)
Encryption doesn't usually require access to the user identity, so we can skip loading that from the store.

Unfortunately that means a fair bit of refectoring, in the form of replacing Device with ReadOnlyDevice.

This reduces the time taken to encrypt a message in a large room from about 3 seconds to 1 second.
2023-11-20 18:46:58 +00:00
Timo 894f4c218d element call: Remove E2EEenabled flag (#2847)
This is a deprecated flag for element call using livekit. It was used to enable/disable matrix signalling event encryption. This is not relevant for embedded mode at all since there the hosting client is taking care of encryption.

---

* Remove E2EEenabled flag from the rust sdk.
This is a deprecated flag for element call using livekit. It is replaced
by the `password` and the `perParticipantE2EE` flag.
If `perParticipantE2EE` is set to `false`
and there is now password encryption is disabled implicitly.

Signed-off-by: Timo K <toger5@hotmail.de>

* `cargo +nightly fmt`

Signed-off-by: Timo K <toger5@hotmail.de>

* change test based on review

Signed-off-by: Timo K <toger5@hotmail.de>

* Apply suggestions from code review

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2023-11-20 16:43:33 +01:00
Jonas Platte 6472ba5109 Upgrade UniFFI 2023-11-20 11:43:05 +01:00
Benjamin Bouvier bcf33c0bcb Test that a live read receipt update received in sliding sync updates the timeline 2023-11-20 10:53:38 +01:00
Benjamin Bouvier fe541bc601 sliding sync: include read receipts into room updates so they're handled by the timeline too 2023-11-20 10:53:38 +01:00
Richard van der Hoff 2d3ee89477 delint, again 2023-11-19 14:50:02 +00:00
Richard van der Hoff cb1827f151 delint 2023-11-17 18:37:57 +00:00
Richard van der Hoff 4299697935 Implement a custom event formatter for javascript logging
Somehow `fmt::pretty` manages to be both overcomplicated and not flexible
enough.

The driver here is that I want to put the event "fields" on a separate line to
the message.
2023-11-17 18:30:21 +00:00
Richard van der Hoff 7e53c6821b Improve performance of get_missing_sessions (#2845)
Two sets of improvements to `get_missing_sessions` here:

 * Currently, for any users who have an empty device list, we call `KeyQueryManager::wait_if_user_key_query_pending`, which waits up to 5 seconds for a `/keys/query` response. (Arguably, we should be waiting longer: it is not unusual for such a request to take a while.)

   The problem is that that user's server may have been blacklisted, in which case we won't even be trying to do `/keys/query` resquests for that user, so we'll be waiting for something that never happens.

   To fix this, let's check the failures list when deciding if we should wait for a user's devices.

 * Separately, but closely related: for each user thus affected, we do the wait *in series*. This is a bit silly: there is no point waiting 50 times. We can parallelise the work.

Fixes #2793.

---

* Move `get_user_devices` to `IdentityManager`

... since it's going to need to interact with the `FailuresCache` which is
stored there.

* `get_user_devices_for_encryption`: don't wait for failed servers

* `get_missing_sessions`: wait for users in parallel

Rather than waiting for each pending user in series, do all the waits in
parallel.

* Changelog

* Update crates/matrix-sdk-crypto/src/identities/manager.rs

* Address review comments
2023-11-17 12:45:16 +01:00
Benjamin Bouvier f21a9b704b doc: update doc comment for OlmMachine::receive_sync_changes and mention it in the changelog
Fixes #2846.

Update crates/matrix-sdk-crypto/src/machine.rs

Co-authored-by: Jonas Platte <jplatte@matrix.org>
2023-11-16 16:53:21 +01:00
Jonas Platte cb08e5a103 crypto: Fix some typos 2023-11-16 15:58:28 +01:00
Jonas Platte e43a25a703 Avoid verbose string conversions when logging / printing 2023-11-16 15:58:28 +01:00
Jonas Platte ee9af5f501 crypto: Remove redundant tracing event field
Already logged as part of the parent span.
2023-11-16 15:58:28 +01:00
Jonas Platte f9aeb590e0 crypto: Generalize encryption methods 2023-11-16 15:58:28 +01:00
Jonas Platte 2d2eef8b0e crypto: Improve some documentation strings 2023-11-16 15:58:28 +01:00
Jonas Platte bfd2a1c445 Upgrade ruma crates to pull in bugfixes
Includes the following bugfixes:

- Fix the name of the fallback text field for extensible events in
  `RoomMessageEventContentWithoutRelation::make_reply_to_raw()`
- Allow to deserialize `(New)ConditionalPushRule` with a missing `conditions`
  field
- Fix deserialization of `claim_keys` responses without a `failures` field
2023-11-16 11:06:19 +01:00
Timo 6e5682d8d2 ffi: Add get_element_call_required_permissions 2023-11-15 12:00:53 +01:00
Alfonso Grillo 4a428b4731 Update ruma-events 2023-11-15 11:49:42 +01:00
Mauro c24830d794 ffi: Add a method to check if MSC 4028 is enabled on the homeserver 2023-11-15 09:30:23 +00:00
Richard van der Hoff 5786b1a631 Update changelog for matrix-sdk-crypto (#2837)
- Add an entry which I forgot for #2805

 - Reorder a few other items that have been added since
 https://github.com/matrix-org/matrix-rust-sdk/pull/2591.
2023-11-10 12:19:40 +00:00
Val Lorentz 445bf3b02a Add missing "room_id" to test_json::MEMBERS
It doesn't matter at the moment as the only test using `test_json::MEMBERS`
does not rely on the event being valid, but it shows this error
nonetheless:

```
2023-11-10T08:54:29.920782Z DEBUG receive_members{room_id="!hIMjEx205EXNyjVPCV:localhost"}: matrix_sdk_base::client: Failed to deserialize member event: missing field `room_id` at line 1 column 297 event_id="$151800140517rfvjc:localhost"
```

and https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv3roomsroomidmembers
says it is a required key.
2023-11-10 09:35:47 +00:00
Richard van der Hoff 71a2d23bf3 Handle missing devices in /keys/claim responses (#2805)
Keep a record of devices that were included in a /keys/claim request, and then, if they are missing in the response, register them as "failed".
2023-11-09 18:30:10 +00:00
Damir Jelić 955d611aaa Bump our deps so we pull in the new release of ruma-client-api 2023-11-09 17:40:03 +01:00
Ivan Enderlin 0f4a175a99 Merge pull request #2833 from matrix-org/mauroromito/notification_has_mention
feat (bindings): `has_mention` in `NotificationItem`
2023-11-09 11:39:33 +01:00
Mauro b553dbb31c Merge branch 'main' into mauroromito/notification_has_mention 2023-11-09 11:14:56 +01:00
Benjamin Bouvier 1abe039582 style: decrease indent in GroupSessionManager::mark_request_as_sent 2023-11-09 11:01:50 +01:00
Benjamin Bouvier 6c490b7aec crypto: inline GroupSessionCache::get_with_id into its single callsite 2023-11-09 11:01:50 +01:00
Benjamin Bouvier c17acd5fe3 crypto: remove direct usage of GroupSessionCache::sessions_being_shared outside the struct 2023-11-09 11:01:50 +01:00
Benjamin Bouvier 8c05d09e4d crypto: remove usage of GroupSessionCache::sessions fields in external users 2023-11-09 11:01:50 +01:00
Jonas Platte 72254caf08 sdk: Enable indexeddb in docsrs feature
… so `ClientBuilder::indexeddb_store` is visible on docs.rs.
2023-11-09 09:25:39 +01:00
Mauro Romito b232cd37e8 implement has_mentions 2023-11-08 17:53:06 +01:00
Jonas Platte 6baf092bc8 sdk: Exclude query parameters from logging
Unfortunately there are a few requests like
check_registration_token_validity that include secrets in the query
parameters.
2023-11-08 10:58:42 +01:00
Jonas Platte c109119b35 sdk: Clean up http_client logging
- Log URI instead of separate homeserver, path
- Always log query parameters (not just for sliding sync)
- Only log request size for requests that could have a body based on the
  HTTP verb
2023-11-08 09:33:39 +01:00
Benjamin Bouvier 8bce5e2416 oidc: add custom hex display to SessionHash
The most important thing is that it's stable and doesn't miss any byte.
2023-11-07 16:44:27 +01:00
Benjamin Bouvier a03b29ac70 oidc(style): avoid qualifying module name when not needed 2023-11-07 16:44:27 +01:00
Benjamin Bouvier c7f7941734 oidc: use hex logging for the hashed oidc tokens 2023-11-07 16:44:27 +01:00
Kévin Commaille 8895ce40d1 Add test for PaginationOptions::until_num_items
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-11-07 12:36:01 +01:00
Kévin Commaille edf32e8941 Store back-pagination tokens in same order as timeline
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-11-07 12:36:01 +01:00
Kévin Commaille 73ddd34cb1 Make several requests if the back-pagination token is not updated
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-11-07 12:36:01 +01:00
Kévin Commaille 0916c93641 ui: Update back-pagination token with the first or last event added
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-11-07 12:36:01 +01:00
Kévin Commaille 8a65e32e7e ui: Update back-pagination token even if chunk is empty or event fails to deserialize
Otherwise back-pagination goes in a loop because the token is never correct

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-11-07 12:36:01 +01:00
Jonas Platte 91c4bc0b3e Upgrade tracing-opentelemetry 2023-11-06 18:41:48 +01:00
Jonas Platte 6f710063b5 Upgrade mas-oidc-client 2023-11-06 18:41:48 +01:00
Jonas Platte e788a6b099 ffi: Simplify Drop implementation for Client 2023-11-06 16:16:40 +01:00
Jonas Platte 8c1baf6ba8 crypto-ffi: Simplify ManuallyDrop usage 2023-11-06 16:16:40 +01:00
Damir Jelić 99032511aa Order the changelog, new entries at the top 2023-11-06 15:17:39 +01:00
Damir Jelić 645bbd67df Remove a unused error type 2023-11-06 14:19:12 +01:00
Damir Jelić 26ede608b0 Add a method which checks if a backup decryption key matches a backup info 2023-11-06 14:19:12 +01:00
Damir Jelić 554aa0404a Add a higher level method to decrypt backed up room keys 2023-11-06 14:19:12 +01:00
Damir Jelić 3438e7de9b Add a higher level method to sign backup versions (#2819) 2023-11-06 12:50:51 +00:00
Damir Jelić 44bb9a3d8f Add a method to create an ExportedRoomKey from a BackedUpRoomKey 2023-11-06 13:41:57 +01:00
Damir Jelić bc5b190509 Add a method to get the RoomKeyBackupInfo to the BackupDecryptionKey 2023-11-06 12:41:22 +01:00
Damir Jelić 2e20f00d99 Add an example for the secret storage support 2023-11-06 10:41:44 +01:00
Damir Jelić 760053b15d Add a test for the secret storage support in the main crate 2023-11-06 10:41:44 +01:00
Damir Jelić e599fa1ccf Add secret storage support to the main SDK crate 2023-11-06 10:41:44 +01:00
Jonas Platte c7fbfd4db8 ffi: Add MediaFileHandle::persist 2023-11-06 10:32:11 +01:00
Jonas Platte ad392a1977 sdk: Add MediaFileHandle::persist 2023-11-06 10:32:11 +01:00
Jonas Platte 57136247bf ffi: Add use_cache parameter to get_media_file 2023-11-06 10:32:11 +01:00
Damir Jelić e5b06bd6d8 Enable backups in the crypto crate by default 2023-11-03 19:17:10 +01:00
Damir Jelić beb01eacfa fixup! Use the better Signatures type in the MegolmV1BackupKey type 2023-11-03 17:01:47 +01:00
Damir Jelić 1e9fab1e4e Use the better Signatures type in the MegolmV1BackupKey type 2023-11-03 17:01:47 +01:00
Benjamin Bouvier 1be7fab4fd ffi: have the (ffi) NotificationClient keep the (ffi) Client alive
Otherwise, it's possible for the `NotificationClient` to be destroyed *after* the ffi `Client`, and then the hack introduced in the previous commit won't work.
2023-11-03 16:54:54 +01:00
Benjamin Bouvier ad5761bfb5 fix(ffi): don't leak Client instances
A detached task was spawned to react upon session changes, and that task captured a clone of the current `Client`.
This caused a leak of the `Client`, because that task would never get aborted, and would not stop by itself.
The fix here consists in having `Client::set_delegate` return a task handle that needs to be stashed by the FFI
users, and cancelled when the Client gets out of scope. This fixes the leak, by removing the last reference onto
the Client.

Then, when dropping the Client, we have to drop the Stores in it. These stores may be sqlite-based stores, which
make use of deadpool. Deadpool has a sync wrapper that will call `block_on` in a `drop` method, and as such it
requires to be in the scope of a tokio runtime to run properly. To avoid breaking all abstractions and giving
access to the inners of the `Client`, the hack used here to properly be in a runtime when dropping the stores is
to replace the inner sdk `Client` in the FFI `Client::drop` method (and replace it with a dummy client that is
minimally configured and will use in-memory stores).
2023-11-03 16:54:54 +01:00
Damir Jelić efb72063ac Add a backup specific method to import room keys 2023-11-03 15:34:11 +01:00
Jonas Platte 9ef6103912 Insert strategic Box::pin to reduce async stack size 2023-11-03 12:23:59 +01:00
Jonas Platte 71c6b98d6a crypto: Box inner field of Account
This reduces its stack size to less than a third of what it previously
was and thus helps with async fn stack size problems.
2023-11-03 12:23:59 +01:00
Jonas Platte dc86835ae2 base: Box large RoomInfo fields
RoomInfo is often passed around by value, including in futures where
holding types with a large stack size is especially problematic¹.
It might make sense to move the actual data of (Base)RoomInfo into
an inner struct that is always held inside a Box or Arc, but this change
should have most of the benefits of that while being a bit simpler.

¹ https://github.com/rust-lang/rust/issues/69826
2023-11-03 12:23:59 +01:00
Benjamin Bouvier ab4c524212 crypto(perf): don't hold the cache lock while waiting on a user key query (#2806)
* crypto(fix): don't hold the cache lock while waiting on a user key query

Fixes #2802. The lock was only useful to sync the database and the in-memory cache for the users awaiting a key query request.
So it's possible to slightly tweak the API by moving the method from `SyncedKeyQueryManager` to non-synced `KeyQueryManager`, and require a
`StoreCacheGuard` (i.e. the owned lock, so we can manually drop it when we feel like so).

I've looked at all the other methods, and they do require the cache for writing into it and the store.
At the limit we could also move `SyncedKeyQueryManager::users_for_key_query`
into `KeyQueryManager`, but the lock in there is hold for a very short-time, so it shouldn't be an issue.

* Add test for the key query deadlock while waiting for the response.

* Update crates/matrix-sdk-crypto/src/machine.rs

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-11-03 11:36:54 +01:00
Richard van der Hoff c2f422209f Review comment: process failed devices together 2023-11-03 10:43:26 +01:00
Richard van der Hoff d90c623375 Add a test for devices with no key map 2023-11-03 10:43:26 +01:00
Richard van der Hoff abd6779210 Inline OlmMachine::receive_keys_claim_response
it's a bit pointless.
2023-11-03 10:43:26 +01:00
Richard van der Hoff d48c27dc86 Remove redundant key_id arg on create_session test helper 2023-11-03 10:43:26 +01:00
Richard van der Hoff 303417eae2 Factor out SessionManager::create_sessions
... and use it in some tests.

Simplify some of the test code by not building a whole keys/claim response.
2023-11-03 10:43:26 +01:00
Richard van der Hoff 017d72e80f Hoist check for missing OTKs to SessionManager
`Account::create_outbound_session` no longer takes an entire list of keys;
rather it takes a single key and it is up to the caller to pick a key out of
the list.

This in turn means that `SessionCreationError` loses one of its reason codes.
2023-11-03 10:43:26 +01:00
Timo d7f6231acd Add perParticipantE2EE to element call url. (#2807)
* Add `perParticipantE2EE` to element call url
* add ffi
* nightly fmt
* refactor to use enum + test
* rename to PerParticipantKeys
* cleanup (spelling + formatting)

Signed-off-by: Timo K <toger5@hotmail.de>
2023-11-03 10:42:50 +01:00
Marco Romano 6a27fc2e04 Upgrade uniffi
This will include https://github.com/mozilla/uniffi-rs/pull/1781
Which fixes https://github.com/mozilla/uniffi-rs/issues/1760
2023-11-03 10:19:20 +01:00
Jonas Platte 3481cde1dc Fix unquoted strings in tracing fields 2023-11-02 17:46:38 +01:00
Jonas Platte d821d611ba sdk: Clean up logging in sliding_sync
- Don't include pos in event data, it's available in a parent span
- Remove superfluous backtick
- Rewrap an `info!` invocation
2023-11-02 17:46:38 +01:00
Jonas Platte 4485abbfdf Replace all two uses of async-std with equivalent tokio functionality 2023-11-02 17:21:48 +01:00
Jonas Platte 2d19c7ad65 crypto: Use Option::map in UsersForKeyQuery::maybe_register_waiting_task 2023-11-02 17:21:48 +01:00
Jonas Platte 1692460b30 Simplify dependency specifications for tokio
matrix-sdk-common and matrix-sdk-crypto were repeating things that would
be inherited from the workspace dependency specification anyways.
2023-11-02 17:21:48 +01:00
Benjamin Bouvier 250d63c6da fix: change the event_type after encrypting
The event_type passed to `encrypt_room_event_raw` must be the one of the cleartext event, not `m.room.encrypted`. The returned event has the expected type.
2023-11-02 16:56:50 +01:00
Jonas Platte 0a33642851 widget: Change content to RawValue representation 2023-11-02 16:56:50 +01:00
Jonas Platte 64c9fa5542 sdk: Allow different raw JSON types for send_raw, send_state_event_raw 2023-11-02 16:56:50 +01:00
Jonas Platte d042bcce04 crypto: Optimize OutboundGroupSession::encrypt 2023-11-02 16:56:50 +01:00
Jonas Platte 7d52322687 crypto: Update raw encryption methods to take &Raw content 2023-11-02 16:56:50 +01:00
Jonas Platte b797635255 crypto: Restrict visibility of internal types
This makes it more obvious that breaking changes to these types don't
break the public API.
2023-11-02 16:56:50 +01:00
Jonas Platte d9845c1658 crypto: Put event type before content for raw encryption methods 2023-11-02 16:56:50 +01:00
Daniel Abramov 4428b525ea widget: Implement limits for requests 2023-11-02 14:56:51 +00:00
Jonas Platte b67ca2c123 ci: Increase log level for coverage job 2023-11-02 12:58:03 +01:00
Jonas Platte 7be84ca71a test: Deduplicate tracing_subscriber initialization
… and set a sensible default log level.
2023-11-02 12:58:03 +01:00
Jonas Platte 9956b56a2c test: Remove unused helpers feature from integration testing crate 2023-11-02 12:58:03 +01:00
Jonas Platte afcc7022a2 Exclude remaining Debug impls from coverage reporting 2023-11-02 12:58:03 +01:00
Benjamin Bouvier 8de33f68f3 integration tests: randomize user names better
In the previous situation, running the tests with `cargo test` would sometimes fail because despite appending the number of milliseconds since
the start of epoch to the user names, some user names would clash across different tests, leading to unexpected results. This fixes it by using
an actual RNG in there, so the names don't ever clash.
2023-11-01 07:57:46 +01:00
Damir Jelić 2efb09907b Add a minimal integration test that sends a message in an encrypted room (#2799) 2023-10-31 19:45:18 +01:00
Jonas Platte 91e7f2f722 sdk: Add changes to send-event API to changelog 2023-10-31 12:46:10 +01:00
Kévin Commaille 7e1eaddf5d Bump tracing in cargo manifest
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2023-10-31 11:18:44 +00:00
249 changed files with 20971 additions and 6126 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-07-03
toolchain: nightly-2023-11-08
components: rustfmt
- name: Run Benchmarks
+3 -3
View File
@@ -281,7 +281,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-07-03
toolchain: nightly-2023-11-08
components: rustfmt
- name: Cargo fmt
@@ -298,7 +298,7 @@ jobs:
uses: actions/checkout@v3
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.16.17
uses: crate-ci/typos@v1.17.0
clippy:
name: Run clippy
@@ -318,7 +318,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-07-03
toolchain: nightly-2023-11-08
components: clippy
- name: Load cache
+3
View File
@@ -17,6 +17,9 @@ concurrency:
env:
CARGO_TERM_COLOR: always
# without matrix_sdk=trace, expressions in `trace!` fields are not evaluated
# when the `trace!` statement is hit, and thus not covered
RUST_LOG: info,matrix_sdk=trace
jobs:
code_coverage:
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-07-03
toolchain: nightly-2023-11-08
- name: Install Node.js
uses: actions/setup-node@v3
+1
View File
@@ -21,6 +21,7 @@ WeeChat = "WeeChat"
sing = "sign"
singed = "signed"
singing = "signing"
Nd = "Nd"
[files]
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
Generated
+1013 -1083
View File
File diff suppressed because it is too large Load Diff
+19 -7
View File
@@ -27,7 +27,6 @@ async-trait = "0.1.60"
as_variant = "1.2.0"
base64 = "0.21.0"
byteorder = "1.4.3"
ctor = "0.2.0"
eyeball = { version = "0.8.7", features = ["tracing"] }
eyeball-im = { version = "0.4.1", features = ["tracing"] }
eyeball-im-util = "0.5.1"
@@ -35,10 +34,11 @@ futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
http = "0.2.6"
itertools = "0.11.0"
ruma = { version = "0.9.2", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
itertools = "0.12.0"
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
ruma-common = "0.12.0"
once_cell = "1.16.0"
rand = "0.8.5"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
@@ -46,13 +46,25 @@ sha2 = "0.10.8"
stream_assert = "0.1.1"
thiserror = "1.0.38"
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
tracing-core = "0.1.30"
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "48b1f51f6e5406cab20f502f535b30a589a5b140" }
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "48b1f51f6e5406cab20f502f535b30a589a5b140" }
tokio-stream = "0.1.14"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
vodozemac = "0.5.0"
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
lto = true
+6 -6
View File
@@ -9,18 +9,18 @@ publish = false
[dependencies]
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
matrix-sdk-base = { path = "../crates/matrix-sdk-base" }
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.6.0"}
matrix-sdk-sqlite = { path = "../crates/matrix-sdk-sqlite", version = "0.1.0", default-features = false, features = ["crypto-store"] }
matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"}
matrix-sdk = { path = "../crates/matrix-sdk" }
matrix-sdk-base = { workspace = true }
matrix-sdk-crypto = { workspace = true }
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test = { workspace = true }
matrix-sdk = { workspace = true }
ruma = { workspace = true }
serde_json = { workspace = true }
tempfile = "3.3.0"
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.12.0", features = ["flamegraph", "criterion"] }
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
+3 -3
View File
@@ -30,7 +30,7 @@ fn keys_query_response() -> get_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
fn keys_claim_response() -> claim_keys::v3::Response {
@@ -38,7 +38,7 @@ fn keys_claim_response() -> claim_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
claim_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
fn huge_keys_query_response() -> get_keys::v3::Response {
@@ -46,7 +46,7 @@ fn huge_keys_query_response() -> get_keys::v3::Response {
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys query response")
.expect("Can't parse the `/keys/query` response")
}
pub fn keys_query(c: &mut Criterion) {
+5 -12
View File
@@ -21,8 +21,9 @@ anyhow = { workspace = true }
futures-util = "0.3.28"
hmac = "0.12.1"
http = { workspace = true }
matrix-sdk-common = { workspace = true }
pbkdf2 = "0.12.2"
rand = "0.8.5"
rand = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -38,20 +39,12 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
version = "0.2.2"
features = ["lax_deserialize"]
[dependencies.matrix-sdk-common]
path = "../../crates/matrix-sdk-common"
version = "0.6.0"
[dependencies.matrix-sdk-crypto]
path = "../../crates/matrix-sdk-crypto"
version = "0.6.0"
default_features = false
features = ["qrcode", "backups_v1", "automatic-room-key-forwarding"]
workspace = true
features = ["qrcode", "automatic-room-key-forwarding"]
[dependencies.matrix-sdk-sqlite]
path = "../../crates/matrix-sdk-sqlite"
version = "0.1.0"
default_features = false
workspace = true
features = ["crypto-store"]
[dependencies.tokio]
@@ -149,7 +149,22 @@ impl BackupRecoveryKey {
let signatures: HashMap<String, HashMap<String, String>> = public_key
.signatures()
.into_iter()
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))
.map(|(k, v)| {
(
k.to_string(),
v.into_iter()
.map(|(k, v)| {
(
k.to_string(),
match v {
Ok(s) => s.to_base64(),
Err(s) => s.source,
},
)
})
.collect(),
)
})
.collect();
MegolmV1BackupKey {
@@ -46,9 +46,10 @@ pub struct DehydratedDevices {
impl Drop for DehydratedDevices {
fn drop(&mut self) {
// See the drop implementation for the `crate::OlmMachine` for an explanation.
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
let _guard = self.runtime.enter();
drop(inner);
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
@@ -99,9 +100,10 @@ pub struct RehydratedDevice {
impl Drop for RehydratedDevice {
fn drop(&mut self) {
// See the drop implementation for the `crate::OlmMachine` for an explanation.
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
let _guard = self.runtime.enter();
drop(inner);
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
@@ -124,9 +126,10 @@ pub struct DehydratedDevice {
impl Drop for DehydratedDevice {
fn drop(&mut self) {
// See the drop implementation for the `crate::OlmMachine` for an explanation.
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
let _guard = self.runtime.enter();
drop(inner);
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
+1 -1
View File
@@ -460,7 +460,7 @@ fn collect_sessions(
.signing_key
.into_iter()
.map(|(k, v)| {
let algorithm = DeviceKeyAlgorithm::try_from(k)?;
let algorithm = DeviceKeyAlgorithm::from(k);
let key = SigningKey::from_parts(&algorithm, v)?;
Ok((algorithm, key))
+10 -8
View File
@@ -113,14 +113,15 @@ pub struct OlmMachine {
impl Drop for OlmMachine {
fn drop(&mut self) {
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::take to be used safely.
let inner = unsafe { ManuallyDrop::take(&mut self.inner) };
let _guard = self.runtime.enter();
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
drop(inner);
let _guard = self.runtime.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
@@ -566,7 +567,7 @@ impl OlmMachine {
///
/// *Note*: Only users that aren't already tracked will be considered for an
/// update. It's safe to call this with already tracked users, it won't
/// result in excessive keys query requests.
/// result in excessive `/keys/query` requests.
///
/// # Arguments
///
@@ -789,11 +790,11 @@ impl OlmMachine {
content: String,
) -> Result<String, CryptoStoreError> {
let room_id = RoomId::parse(room_id)?;
let content: Value = serde_json::from_str(&content)?;
let content = serde_json::from_str(&content)?;
let encrypted_content = self
.runtime
.block_on(self.inner.encrypt_room_event_raw(&room_id, content, &event_type))
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
.expect("Encrypting an event produced an error");
Ok(serde_json::to_string(&encrypted_content)?)
@@ -1464,6 +1465,7 @@ impl OlmMachine {
progress_listener.on_progress(progress as i32, total as i32)
};
#[allow(deprecated)]
let result =
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
@@ -149,7 +149,7 @@ impl From<OutgoingRequest> for Request {
Request::KeysUpload {
request_id: r.request_id().to_string(),
body: serde_json::to_string(&body)
.expect("Can't serialize keys upload request"),
.expect("Can't serialize `/keys/upload` request"),
}
}
KeysQuery(k) => {
+8 -9
View File
@@ -29,11 +29,12 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-core = { workspace = true }
futures-util = { workspace = true }
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui", default-features = false, features = ["e2e-encryption"] }
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
mime = "0.3.16"
once_cell = { workspace = true }
opentelemetry = { version = "0.20.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.13.0", features = ["tokio", "reqwest-client", "http-proto"] }
opentelemetry = "0.21.0"
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
sanitize-filename-reader-friendly = "2.2.1"
serde = { workspace = true }
@@ -41,11 +42,11 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-core = { workspace = true }
tracing-opentelemetry = { version = "0.20.0" }
tracing-opentelemetry = "0.22.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.8"
tokio-stream = { workspace = true, features = ["time"] }
uniffi = { workspace = true, features = ["tokio"] }
url = "2.2.2"
zeroize = { workspace = true }
@@ -57,8 +58,7 @@ log-panics = { version = "2", features = ["with-backtrace"] }
paranoid-android = "0.2.1"
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
path = "../../crates/matrix-sdk"
default-features = false
workspace = true
features = [
"anyhow",
"e2e-encryption",
@@ -72,8 +72,7 @@ features = [
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
path = "../../crates/matrix-sdk"
default-features = false
workspace = true
features = [
"anyhow",
"e2e-encryption",
-12
View File
@@ -20,15 +20,3 @@ interface MediaSource {
string to_json();
string url();
};
enum EventItemOrigin {
"Local",
"Sync",
"Pagination",
};
enum BackPaginationStatus {
"Idle",
"Paginating",
"TimelineStartReached",
};
+237 -86
View File
@@ -1,4 +1,8 @@
use std::sync::{Arc, RwLock};
use std::{
collections::HashMap,
mem::ManuallyDrop,
sync::{Arc, RwLock},
};
use anyhow::{anyhow, Context as _};
use matrix_sdk::{
@@ -36,6 +40,7 @@ use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotifi
use mime::Mime;
use ruma::{
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
events::room::power_levels::RoomPowerLevelsEventContent,
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
};
use serde::{Deserialize, Serialize};
@@ -47,9 +52,11 @@ use url::Url;
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
use crate::{
client,
encryption::Encryption,
notification::NotificationClientBuilder,
notification_settings::NotificationSettings,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
ClientError,
};
@@ -147,12 +154,26 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: MatrixClient,
pub(crate) inner: ManuallyDrop<MatrixClient>,
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
}
impl Drop for Client {
fn drop(&mut self) {
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl Client {
pub fn new(
sdk_client: MatrixClient,
@@ -173,26 +194,11 @@ impl Client {
});
let client = Arc::new(Client {
inner: sdk_client,
inner: ManuallyDrop::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
});
let mut session_change_receiver = client.inner.subscribe_to_session_changes();
let client_clone = client.clone();
RUNTIME.spawn(async move {
loop {
match session_change_receiver.recv().await {
Ok(session_change) => client_clone.process_session_change(session_change),
Err(receive_error) => {
if let RecvError::Closed = receive_error {
break;
}
}
}
}
});
if let Some(process_id) = cross_process_refresh_lock_id {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
@@ -230,7 +236,7 @@ impl Client {
}
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl Client {
/// Login using a username and password.
pub fn login(
@@ -253,31 +259,30 @@ impl Client {
})
}
pub fn get_media_file(
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
body: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
) -> Result<Arc<MediaFileHandle>, ClientError> {
let client = self.inner.clone();
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
RUNTIME.block_on(async move {
let handle = client
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
body,
&mime_type,
true,
temp_dir,
)
.await?;
let handle = self
.inner
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
body,
&mime_type,
use_cache,
temp_dir,
)
.await?;
Ok(Arc::new(MediaFileHandle { inner: handle }))
})
Ok(Arc::new(MediaFileHandle::new(handle)))
}
/// Restores the client from a `Session`.
@@ -343,14 +348,35 @@ impl Client {
}
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl Client {
pub fn set_delegate(&self, delegate: Option<Box<dyn ClientDelegate>>) {
*self.delegate.write().unwrap() = delegate.map(Arc::from);
pub fn set_delegate(
self: Arc<Self>,
delegate: Option<Box<dyn ClientDelegate>>,
) -> Option<Arc<TaskHandle>> {
delegate.map(|delegate| {
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
let client_clone = self.clone();
let session_change_task = RUNTIME.spawn(async move {
loop {
match session_change_receiver.recv().await {
Ok(session_change) => client_clone.process_session_change(session_change),
Err(receive_error) => {
if let RecvError::Closed = receive_error {
break;
}
}
}
}
});
*self.delegate.write().unwrap() = Some(Arc::from(delegate));
Arc::new(TaskHandle::new(session_change_task))
})
}
pub fn session(&self) -> Result<Session, ClientError> {
RUNTIME.block_on(async move { Self::session_inner(self.inner.clone()).await })
RUNTIME.block_on(async move { Self::session_inner((*self.inner).clone()).await })
}
pub fn account_url(
@@ -460,68 +486,65 @@ impl Client {
})
}
pub fn upload_media(
pub async fn upload_media(
&self,
mime_type: String,
data: Vec<u8>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<String, ClientError> {
let l = self.inner.clone();
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = self.inner.media().upload(&mime_type, data);
RUNTIME.block_on(async move {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = l.media().upload(&mime_type, data);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
});
}
let response = request.await?;
Ok(String::from(response.content_uri))
})
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
});
}
let response = request.await?;
Ok(String::from(response.content_uri))
}
pub fn get_media_content(
pub async fn get_media_content(
&self,
media_source: Arc<MediaSource>,
) -> Result<Vec<u8>, ClientError> {
let l = self.inner.clone();
let source = (*media_source).clone();
RUNTIME.block_on(async move {
Ok(l.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.await?)
})
Ok(self
.inner
.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.await?)
}
pub fn get_media_thumbnail(
pub async fn get_media_thumbnail(
&self,
media_source: Arc<MediaSource>,
width: u64,
height: u64,
) -> Result<Vec<u8>, ClientError> {
let l = self.inner.clone();
let source = (*media_source).clone();
RUNTIME.block_on(async move {
Ok(l.media()
.get_media_content(
&MediaRequest {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Scale,
width: UInt::new(width).unwrap(),
height: UInt::new(height).unwrap(),
}),
},
true,
)
.await?)
})
Ok(self
.inner
.media()
.get_media_content(
&MediaRequest {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Scale,
width: UInt::new(width).unwrap(),
height: UInt::new(height).unwrap(),
}),
},
true,
)
.await?)
}
pub fn get_session_verification_controller(
@@ -665,24 +688,28 @@ impl Client {
}
pub fn notification_client(
&self,
self: Arc<Self>,
process_setup: NotificationProcessSetup,
) -> Result<Arc<NotificationClientBuilder>, ClientError> {
NotificationClientBuilder::new(self.inner.clone(), process_setup.into())
NotificationClientBuilder::new(self.clone(), process_setup.into())
}
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
SyncServiceBuilder::new(self.inner.clone())
SyncServiceBuilder::new((*self.inner).clone())
}
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
RUNTIME.block_on(async move {
Arc::new(NotificationSettings::new(
self.inner.clone(),
(*self.inner).clone(),
self.inner.notification_settings().await,
))
})
}
pub fn encryption(&self) -> Arc<Encryption> {
Arc::new(self.inner.encryption().into())
}
}
#[derive(uniffi::Enum)]
@@ -782,6 +809,86 @@ impl Client {
}
}
#[derive(uniffi::Record)]
pub struct NotificationPowerLevels {
pub room: i32,
}
impl From<NotificationPowerLevels> for ruma::power_levels::NotificationPowerLevels {
fn from(value: NotificationPowerLevels) -> Self {
let mut notification_power_levels = Self::new();
notification_power_levels.room = value.room.into();
notification_power_levels
}
}
#[derive(uniffi::Record)]
pub struct PowerLevels {
pub users_default: Option<i32>,
pub events_default: Option<i32>,
pub state_default: Option<i32>,
pub ban: Option<i32>,
pub kick: Option<i32>,
pub redact: Option<i32>,
pub invite: Option<i32>,
pub notifications: Option<NotificationPowerLevels>,
pub users: HashMap<String, i32>,
pub events: HashMap<String, i32>,
}
impl From<PowerLevels> for RoomPowerLevelsEventContent {
fn from(value: PowerLevels) -> Self {
let mut power_levels = RoomPowerLevelsEventContent::new();
if let Some(users_default) = value.users_default {
power_levels.users_default = users_default.into();
}
if let Some(state_default) = value.state_default {
power_levels.state_default = state_default.into();
}
if let Some(events_default) = value.events_default {
power_levels.events_default = events_default.into();
}
if let Some(ban) = value.ban {
power_levels.ban = ban.into();
}
if let Some(kick) = value.kick {
power_levels.kick = kick.into();
}
if let Some(redact) = value.redact {
power_levels.redact = redact.into();
}
if let Some(invite) = value.invite {
power_levels.invite = invite.into();
}
if let Some(notifications) = value.notifications {
power_levels.notifications = notifications.into()
}
power_levels.users = value
.users
.iter()
.filter_map(|(user_id, power_level)| match UserId::parse(user_id) {
Ok(id) => Some((id, (*power_level).into())),
Err(e) => {
error!(user_id, "Skipping invalid user ID, error: {e}");
None
}
})
.collect();
power_levels.events = value
.events
.iter()
.map(|(event_type, power_level)| {
let event_type: ruma::events::TimelineEventType = event_type.as_str().into();
(event_type, (*power_level).into())
})
.collect();
power_levels
}
}
#[derive(uniffi::Record)]
pub struct CreateRoomParameters {
pub name: Option<String>,
@@ -796,6 +903,8 @@ pub struct CreateRoomParameters {
pub invite: Option<Vec<String>>,
#[uniffi(default = None)]
pub avatar: Option<String>,
#[uniffi(default = None)]
pub power_level_content_override: Option<PowerLevels>,
}
impl From<CreateRoomParameters> for create_room::v3::Request {
@@ -833,9 +942,19 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
if let Some(power_levels) = value.power_level_content_override {
match Raw::new(&power_levels.into()) {
Ok(power_levels) => {
request.power_level_content_override = Some(power_levels);
}
Err(e) => {
error!("Failed to serialize power levels, error: {e}");
}
}
}
request
}
}
@@ -1101,13 +1220,45 @@ fn gen_transaction_id() -> String {
/// is dropped, the file will be removed from the disk.
#[derive(uniffi::Object)]
pub struct MediaFileHandle {
inner: SdkMediaFileHandle,
inner: RwLock<Option<SdkMediaFileHandle>>,
}
impl MediaFileHandle {
fn new(handle: SdkMediaFileHandle) -> Self {
Self { inner: RwLock::new(Some(handle)) }
}
}
#[uniffi::export]
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> String {
self.inner.path().to_str().unwrap().to_owned()
pub fn path(&self) -> Result<String, ClientError> {
Ok(self
.inner
.read()
.unwrap()
.as_ref()
.context("MediaFileHandle must not be used after calling persist")?
.path()
.to_str()
.unwrap()
.to_owned())
}
pub fn persist(&self, path: String) -> Result<bool, ClientError> {
let mut guard = self.inner.write().unwrap();
Ok(
match guard
.take()
.context("MediaFileHandle was already persisted")?
.persist(path.as_ref())
{
Ok(_) => true,
Err(e) => {
*guard = Some(e.file);
false
}
},
)
}
}
@@ -1,6 +1,7 @@
use std::{fs, path::PathBuf, sync::Arc};
use matrix_sdk::{
encryption::{BackupDownloadStrategy, EncryptionSettings},
ruma::{
api::{error::UnknownVersionError, MatrixVersion},
ServerName, UserId,
@@ -249,6 +250,13 @@ impl ClientBuilder {
impl Default for ClientBuilder {
fn default() -> Self {
let encryption_settings = EncryptionSettings {
auto_enable_cross_signing: true,
auto_enable_backups: true,
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
};
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
Self {
base_path: None,
username: None,
@@ -261,7 +269,7 @@ impl Default for ClientBuilder {
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
inner: MatrixClient::builder(),
inner,
cross_process_refresh_lock_id: None,
session_delegate: None,
}
+329
View File
@@ -0,0 +1,329 @@
use std::sync::Arc;
use futures_util::StreamExt;
use matrix_sdk::encryption::{backups, recovery};
use thiserror::Error;
use zeroize::Zeroize;
use super::RUNTIME;
use crate::{error::ClientError, task_handle::TaskHandle};
#[derive(uniffi::Object)]
pub struct Encryption {
inner: matrix_sdk::encryption::Encryption,
}
impl From<matrix_sdk::encryption::Encryption> for Encryption {
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
Self { inner: value }
}
}
#[uniffi::export(callback_interface)]
pub trait BackupStateListener: Sync + Send {
fn on_update(&self, status: BackupState);
}
#[uniffi::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
fn on_update(&self, status: BackupUploadState);
}
#[uniffi::export(callback_interface)]
pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[derive(uniffi::Enum)]
pub enum BackupUploadState {
Waiting,
Uploading { backed_up_count: u32, total_count: u32 },
Error,
Done,
}
#[derive(Debug, Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum SteadyStateError {
#[error("The backup got disabled while waiting for the room keys to be uploaded.")]
BackupDisabled,
#[error("There was a connection error.")]
Connection,
#[error("We couldn't read status updates from the upload task quickly enough.")]
Lagged,
}
#[derive(Debug, Error, uniffi::Error)]
pub enum RecoveryError {
/// A backup already exists on the homeserver, the recovery subsystem does
/// not allow backups to be overwritten, disable recovery first.
#[error(
"A backup already exists on the homeserver and the method does not allow to overwrite it"
)]
BackupExistsOnServer,
/// A typical SDK error.
#[error(transparent)]
Client { source: crate::ClientError },
/// Error in the secret storage subsystem.
#[error("Error in the secret-storage subsystem: {error_message}")]
SecretStorage { error_message: String },
}
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
fn from(value: matrix_sdk::encryption::recovery::RecoveryError) -> Self {
match value {
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
recovery::RecoveryError::SecretStorage(e) => {
Self::SecretStorage { error_message: e.to_string() }
}
}
}
}
pub type Result<A, E = RecoveryError> = std::result::Result<A, E>;
impl From<matrix_sdk::encryption::backups::futures::SteadyStateError> for SteadyStateError {
fn from(value: matrix_sdk::encryption::backups::futures::SteadyStateError) -> Self {
match value {
backups::futures::SteadyStateError::BackupDisabled => Self::BackupDisabled,
backups::futures::SteadyStateError::Connection => Self::Connection,
backups::futures::SteadyStateError::Lagged => Self::Lagged,
}
}
}
#[derive(uniffi::Enum)]
pub enum BackupState {
Unknown,
Creating,
Enabling,
Resuming,
Enabled,
Downloading,
Disabling,
}
impl From<backups::BackupState> for BackupState {
fn from(value: backups::BackupState) -> Self {
match value {
backups::BackupState::Unknown => Self::Unknown,
backups::BackupState::Creating => Self::Creating,
backups::BackupState::Enabling => Self::Enabling,
backups::BackupState::Resuming => Self::Resuming,
backups::BackupState::Enabled => Self::Enabled,
backups::BackupState::Downloading => Self::Downloading,
backups::BackupState::Disabling => Self::Disabling,
}
}
}
impl From<backups::UploadState> for BackupUploadState {
fn from(value: backups::UploadState) -> Self {
match value {
backups::UploadState::Idle => Self::Waiting,
backups::UploadState::Uploading(count) => Self::Uploading {
backed_up_count: count.backed_up.try_into().unwrap_or(u32::MAX),
total_count: count.total.try_into().unwrap_or(u32::MAX),
},
backups::UploadState::Error => Self::Error,
backups::UploadState::Done => Self::Done,
}
}
}
#[derive(uniffi::Enum)]
pub enum RecoveryState {
Unknown,
Enabled,
Disabled,
Incomplete,
}
impl From<recovery::RecoveryState> for RecoveryState {
fn from(value: recovery::RecoveryState) -> Self {
match value {
recovery::RecoveryState::Unknown => Self::Unknown,
recovery::RecoveryState::Enabled => Self::Enabled,
recovery::RecoveryState::Disabled => Self::Disabled,
recovery::RecoveryState::Incomplete => Self::Incomplete,
}
}
}
#[uniffi::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
fn on_update(&self, status: EnableRecoveryProgress);
}
#[derive(uniffi::Enum)]
pub enum EnableRecoveryProgress {
Starting,
CreatingBackup,
CreatingRecoveryKey,
BackingUp { backed_up_count: u32, total_count: u32 },
RoomKeyUploadError,
Done { recovery_key: String },
}
impl From<recovery::EnableProgress> for EnableRecoveryProgress {
fn from(value: recovery::EnableProgress) -> Self {
match &value {
recovery::EnableProgress::Starting => Self::Starting,
recovery::EnableProgress::CreatingBackup => Self::CreatingBackup,
recovery::EnableProgress::CreatingRecoveryKey => Self::CreatingRecoveryKey,
recovery::EnableProgress::BackingUp(counts) => Self::BackingUp {
backed_up_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
total_count: counts.backed_up.try_into().unwrap_or(u32::MAX),
},
recovery::EnableProgress::RoomKeyUploadError => Self::RoomKeyUploadError,
recovery::EnableProgress::Done { recovery_key } => {
Self::Done { recovery_key: recovery_key.to_owned() }
}
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl Encryption {
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
let mut stream = self.inner.backups().state_stream();
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
while let Some(state) = stream.next().await {
let Ok(state) = state else { continue };
listener.on_update(state.into());
}
}));
stream_task.into()
}
pub fn backup_state(&self) -> BackupState {
self.inner.backups().state().into()
}
/// Does a backup exist on the server?
///
/// Because the homeserver doesn't notify us about changes to the backup
/// version, the [`BackupState`] and its listener are a bit crippled.
/// The `BackupState::Unknown` state might mean there is no backup at all or
/// a backup exists but we don't have access to it.
///
/// Therefore it is necessary to poll the server for an answer every time
/// you want to differentiate between those two states.
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
Ok(self.inner.backups().exists_on_server().await?)
}
pub fn recovery_state(&self) -> RecoveryState {
self.inner.recovery().state().into()
}
pub fn recovery_state_listener(
&self,
listener: Box<dyn RecoveryStateListener>,
) -> Arc<TaskHandle> {
let mut stream = self.inner.recovery().state_stream();
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
while let Some(state) = stream.next().await {
listener.on_update(state.into());
}
}));
stream_task.into()
}
pub async fn enable_backups(&self) -> Result<()> {
Ok(self.inner.recovery().enable_backup().await?)
}
pub async fn is_last_device(&self) -> Result<bool> {
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
}
pub async fn wait_for_backup_upload_steady_state(
&self,
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
) -> Result<(), SteadyStateError> {
let backups = self.inner.backups();
let wait_for_steady_state = backups.wait_for_steady_state();
let task = if let Some(listener) = progress_listener {
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
Some(RUNTIME.spawn(async move {
while let Some(progress) = progress_stream.next().await {
let Ok(progress) = progress else { continue };
listener.on_update(progress.into());
}
}))
} else {
None
};
let result = wait_for_steady_state.await;
if let Some(task) = task {
task.abort();
}
Ok(result?)
}
pub async fn enable_recovery(
&self,
wait_for_backups_to_upload: bool,
progress_listener: Box<dyn EnableRecoveryProgressListener>,
) -> Result<String> {
let recovery = self.inner.recovery();
let enable = if wait_for_backups_to_upload {
recovery.enable().wait_for_backups_to_upload()
} else {
recovery.enable()
};
let mut progress_stream = enable.subscribe_to_progress();
let task = RUNTIME.spawn(async move {
while let Some(progress) = progress_stream.next().await {
let Ok(progress) = progress else { continue };
progress_listener.on_update(progress.into());
}
});
let ret = enable.await?;
task.abort();
Ok(ret)
}
pub async fn disable_recovery(&self) -> Result<()> {
Ok(self.inner.recovery().disable().await?)
}
pub async fn reset_recovery_key(&self) -> Result<String> {
Ok(self.inner.recovery().reset_key().await?)
}
pub async fn recover_and_reset(&self, mut old_recovery_key: String) -> Result<String> {
let result = self.inner.recovery().recover_and_reset(&old_recovery_key).await;
old_recovery_key.zeroize();
Ok(result?)
}
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
let result = self.inner.recovery().recover(&recovery_key).await;
recovery_key.zeroize();
Ok(result?)
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ use ruma::events::{
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
};
use crate::{room_member::MembershipState, timeline::MessageType, ClientError};
use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
+4 -2
View File
@@ -24,6 +24,7 @@ mod authentication_service;
mod chunk_iterator;
mod client;
mod client_builder;
mod encryption;
mod error;
mod event;
mod helpers;
@@ -34,23 +35,24 @@ mod room;
mod room_info;
mod room_list;
mod room_member;
mod ruma;
mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod tracing;
mod utils;
mod widget;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::ruma::events::room::{
message::RoomMessageEventContentWithoutRelation, MediaSource,
};
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin};
use self::{
error::ClientError,
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
task_handle::TaskHandle,
timeline::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
};
uniffi::include_scaffolding!("api");
+20 -7
View File
@@ -7,7 +7,9 @@ use matrix_sdk_ui::notification_client::{
};
use ruma::{EventId, RoomId};
use crate::{error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME};
use crate::{
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
};
#[derive(uniffi::Enum)]
pub enum NotificationEvent {
@@ -42,6 +44,7 @@ pub struct NotificationItem {
/// Can be `None` if we couldn't determine this, because we lacked
/// information to create a push context.
pub is_noisy: Option<bool>,
pub has_mention: Option<bool>,
}
impl NotificationItem {
@@ -70,23 +73,26 @@ impl NotificationItem {
is_direct: item.is_direct_message_room,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
}
}
}
#[derive(Clone, uniffi::Object)]
pub struct NotificationClientBuilder {
client: Arc<Client>,
builder: MatrixNotificationClientBuilder,
}
impl NotificationClientBuilder {
pub(crate) fn new(
client: matrix_sdk::Client,
client: Arc<Client>,
process_setup: NotificationProcessSetup,
) -> Result<Arc<Self>, ClientError> {
let builder = RUNTIME
.block_on(async { MatrixNotificationClient::builder(client, process_setup).await })?;
Ok(Arc::new(Self { builder }))
let builder = RUNTIME.block_on(async {
MatrixNotificationClient::builder((*client.inner).clone(), process_setup).await
})?;
Ok(Arc::new(Self { builder, client }))
}
}
@@ -97,18 +103,25 @@ impl NotificationClientBuilder {
pub fn filter_by_push_rules(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.filter_by_push_rules();
Arc::new(Self { builder })
Arc::new(Self { builder, client: this.client })
}
pub fn finish(self: Arc<Self>) -> Arc<NotificationClient> {
let this = unwrap_or_clone_arc(self);
Arc::new(NotificationClient { inner: this.builder.build() })
Arc::new(NotificationClient { inner: this.builder.build(), _client: this.client })
}
}
#[derive(uniffi::Object)]
pub struct NotificationClient {
inner: MatrixNotificationClient,
/// A reference to the FFI client.
///
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
/// would need to repeat the hack done in the FFI `Client::drop` method.
_client: Arc<Client>,
}
#[uniffi::export]
@@ -267,10 +267,7 @@ impl NotificationSettings {
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsRoomMention.as_str(),
)
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
.await?;
Ok(enabled)
}
@@ -284,7 +281,7 @@ impl NotificationSettings {
notification_settings
.set_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsRoomMention.as_str(),
PredefinedOverrideRuleId::IsRoomMention,
enabled,
)
.await?;
@@ -295,14 +292,33 @@ impl NotificationSettings {
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsUserMention.as_str(),
)
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
.await?;
Ok(enabled)
}
/// Check if [MSC 4028 push rule][rule] is enabled.
///
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
let notification_settings = self.sdk_notification_settings.read().await;
// Check stable identifier
if let Ok(enabled) = notification_settings
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
.await
{
enabled
// Check unstable identifier
} else if let Ok(enabled) = notification_settings
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
.await
{
enabled
} else {
false
}
}
/// Set whether user mentions are enabled.
pub async fn set_user_mention_enabled(
&self,
@@ -312,7 +328,7 @@ impl NotificationSettings {
notification_settings
.set_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsUserMention.as_str(),
PredefinedOverrideRuleId::IsUserMention,
enabled,
)
.await?;
@@ -323,18 +339,42 @@ impl NotificationSettings {
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call.as_str())
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
.await?;
Ok(enabled)
}
/// Set whether the `.m.rule.call` push rule is enabled
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
.await?;
Ok(())
}
/// Get whether the `.m.rule.invite_for_me` push rule is enabled
pub async fn is_invite_for_me_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::InviteForMe.as_str(),
)
.await?;
Ok(enabled)
}
/// Set whether the `.m.rule.invite_for_me` push rule is enabled
pub async fn set_invite_for_me_enabled(
&self,
enabled: bool,
) -> Result<(), NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(
RuleKind::Underride,
PredefinedUnderrideRuleId::Call.as_str(),
RuleKind::Override,
PredefinedOverrideRuleId::InviteForMe.as_str(),
enabled,
)
.await?;
+17 -16
View File
@@ -1,12 +1,10 @@
use std::{collections::HashMap, fmt::Debug};
use std::{collections::HashMap, fmt::Debug, pin::Pin};
use base64::{engine::general_purpose::STANDARD, Engine};
use futures_core::future::BoxFuture;
use opentelemetry::{
sdk::{runtime::RuntimeChannel, trace::Tracer, util::tokio_interval_stream, Resource},
KeyValue,
};
use opentelemetry::KeyValue;
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
use tokio::runtime::Handle;
use tracing_core::Subscriber;
use tracing_subscriber::{
@@ -20,17 +18,17 @@ use tracing_subscriber::{
use crate::RUNTIME;
#[derive(Clone, Debug)]
struct TracingRuntime {
struct TokioRuntime {
runtime: Handle,
}
impl opentelemetry::runtime::Runtime for TracingRuntime {
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
type Interval = tokio_stream::wrappers::IntervalStream;
type Delay = ::std::pin::Pin<Box<tokio::time::Sleep>>;
type Delay = Pin<Box<tokio::time::Sleep>>;
fn interval(&self, duration: std::time::Duration) -> Self::Interval {
fn interval(&self, period: std::time::Duration) -> Self::Interval {
let _guard = self.runtime.enter();
tokio_interval_stream(duration)
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
}
fn spawn(&self, future: BoxFuture<'static, ()>) {
@@ -44,11 +42,14 @@ impl opentelemetry::runtime::Runtime for TracingRuntime {
}
}
impl<T: Debug + Send> RuntimeChannel<T> for TracingRuntime {
type Receiver = tokio_stream::wrappers::ReceiverStream<T>;
type Sender = tokio::sync::mpsc::Sender<T>;
impl RuntimeChannel for TokioRuntime {
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
fn batch_message_channel(&self, capacity: usize) -> (Self::Sender, Self::Receiver) {
fn batch_message_channel<T: Debug + Send>(
&self,
capacity: usize,
) -> (Self::Sender<T>, Self::Receiver<T>) {
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
}
@@ -73,14 +74,14 @@ pub fn create_otlp_tracer(
.with_endpoint(otlp_endpoint)
.with_headers(headers);
let tracer_runtime = TracingRuntime { runtime: runtime.to_owned() };
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
let _guard = runtime.enter();
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(exporter)
.with_trace_config(
opentelemetry::sdk::trace::config()
opentelemetry_sdk::trace::config()
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
)
.install_batch(tracer_runtime)?;
+53 -703
View File
@@ -1,66 +1,27 @@
use std::{convert::TryFrom, fs, sync::Arc};
use std::{convert::TryFrom, sync::Arc};
use anyhow::{anyhow, Context, Result};
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
},
room::Room as SdkRoom,
ruma::{
api::client::{receipt::create_receipt::v3::ReceiptType, room::report_content},
events::{
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::unstable_start::{
UnstablePollAnswer, UnstablePollAnswers, UnstablePollStartContentBlock,
},
receipt::ReceiptThread,
relation::Annotation,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
message::{
ForwardThread, LocationMessageEventContent, MessageType,
RoomMessageEventContentWithoutRelation,
},
},
AnyMessageLikeEventContent,
},
EventId, UserId,
},
RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{BackPaginationStatus, RoomExt, Timeline};
use anyhow::{Context, Result};
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
use matrix_sdk_ui::timeline::RoomExt;
use mime::Mime;
use ruma::{
api::client::room::report_content,
assign,
events::{
poll::{
unstable_end::UnstablePollEndEventContent,
unstable_response::UnstablePollResponseEventContent,
unstable_start::NewUnstablePollStartEventContent,
},
room::MediaSource,
},
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
EventId, UserId,
};
use tokio::{
sync::{Mutex, RwLock},
task::{AbortHandle, JoinHandle},
};
use tracing::{error, info};
use uuid::Uuid;
use tokio::sync::RwLock;
use tracing::error;
use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
client::ProgressWatcher,
error::{ClientError, MediaInfoError, RoomError},
room_info::RoomInfo,
room_member::{MessageLikeEventType, RoomMember, StateEventType},
timeline::{
u64_to_uint, AudioInfo, EventTimelineItem, FileInfo, ImageInfo, PollKind, ThumbnailInfo,
TimelineDiff, TimelineItem, TimelineListener, VideoInfo,
},
ruma::ImageInfo,
timeline::{EventTimelineItem, Timeline},
utils::u64_to_uint,
TaskHandle,
};
@@ -178,41 +139,19 @@ impl Room {
}
}
/// Removes the timeline.
///
/// Timeline items cached in memory as well as timeline listeners are
/// dropped.
pub fn remove_timeline(&self) {
RUNTIME.block_on(async {
*self.timeline.write().await = None;
});
pub async fn timeline(&self) -> Arc<Timeline> {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
timeline.clone()
} else {
let timeline = Timeline::new(self.inner.timeline().await);
*write_guard = Some(timeline.clone());
timeline
}
}
pub fn retry_decryption(&self, session_ids: Vec<String>) {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
error!("Timeline not set up, can't retry decryption");
return;
}
};
RUNTIME.spawn(async move {
timeline.retry_decryption(&session_ids).await;
});
}
pub async fn fetch_members(&self) -> Result<(), ClientError> {
let timeline = self
.timeline
.read()
.await
.clone()
.context("Timeline not set up, can't fetch members")?;
timeline.fetch_members().await;
Ok(())
pub async fn poll_history(&self) -> Arc<Timeline> {
Timeline::new(self.inner.poll_history().await)
}
pub fn display_name(&self) -> Result<String, ClientError> {
@@ -258,37 +197,6 @@ impl Room {
})
}
pub async fn add_timeline_listener(
&self,
listener: Box<dyn TimelineListener>,
) -> RoomTimelineListenerResult {
let timeline = {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
timeline.clone()
} else {
let timeline = Arc::new(self.inner.timeline().await);
*write_guard = Some(timeline.clone());
timeline
}
};
let (timeline_items, timeline_stream) = timeline.subscribe_batched().await;
let timeline_stream = TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(timeline_stream);
while let Some(diffs) = timeline_stream.next().await {
listener
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
}
}));
RoomTimelineListenerResult {
items: timeline_items.into_iter().map(TimelineItem::from_arc).collect(),
items_stream: Arc::new(timeline_stream),
}
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
let avatar_url = self.inner.avatar_url();
@@ -297,7 +205,7 @@ impl Room {
// First off, let's see if a `Timeline` exists…
if let Some(timeline) = self.timeline.read().await.clone() {
// If it contains a `latest_event`…
if let Some(timeline_last_event) = timeline.latest_event().await {
if let Some(timeline_last_event) = timeline.inner.latest_event().await {
// If it's a local echo…
if timeline_last_event.is_local_echo() {
return Ok(RoomInfo::new(
@@ -342,200 +250,6 @@ impl Room {
})))
}
pub fn subscribe_to_back_pagination_status(
&self,
listener: Box<dyn BackPaginationStatusListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let mut subscriber = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => t.back_pagination_status(),
None => {
return Err(anyhow!(
"Timeline not set up, can't subscribe to back-pagination status"
)
.into());
}
};
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Send the current state even if it hasn't changed right away.
listener.on_update(subscriber.next_now());
while let Some(status) = subscriber.next().await {
listener.on_update(status);
}
}))))
}
/// Loads older messages into the timeline.
///
/// Raises an exception if there are no timeline listeners.
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let timeline: Arc<_> = self
.timeline
.read()
.await
.clone()
.context("No timeline listeners registered, can't paginate")?;
Ok(timeline.paginate_backwards(opts.into()).await?)
})
}
pub fn send_read_receipt(&self, event_id: String) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async move {
self.timeline
.read()
.await
.clone()
.context("Timeline not set up, can't send read receipt")?
.send_single_receipt(ReceiptType::Read, ReceiptThread::Unthreaded, event_id)
.await?;
Ok(())
})
}
pub fn send(&self, msg: Arc<RoomMessageEventContentWithoutRelation>) {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
error!("Timeline not set up, can't send message");
return;
}
};
RUNTIME.spawn(async move {
timeline.send((*msg).to_owned().with_relation(None).into()).await;
});
}
pub fn create_poll(
&self,
question: String,
answers: Vec<String>,
max_selections: u8,
poll_kind: PollKind,
) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
return Err(anyhow!("Timeline not set up, can't send the poll").into());
}
};
let poll_answers_vec: Vec<UnstablePollAnswer> = answers
.iter()
.map(|answer| UnstablePollAnswer::new(Uuid::new_v4().to_string(), answer))
.collect();
let poll_answers = UnstablePollAnswers::try_from(poll_answers_vec)
.context("Failed to create poll answers")?;
let mut poll_content_block =
UnstablePollStartContentBlock::new(question.clone(), poll_answers);
poll_content_block.kind = poll_kind.into();
poll_content_block.max_selections = max_selections.into();
let fallback_text = answers
.iter()
.enumerate()
.fold(question, |acc, (index, answer)| format!("{acc}\n{}. {answer}", index + 1));
let poll_start_event_content =
NewUnstablePollStartEventContent::plain_text(fallback_text, poll_content_block);
let event_content =
AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
RUNTIME.spawn(async move {
timeline.send(event_content).await;
});
Ok(())
}
pub fn send_poll_response(
&self,
poll_start_id: String,
answers: Vec<String>,
) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
return Err(anyhow!("Timeline not set up, can't send the poll vote").into());
}
};
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
let poll_response_event_content =
UnstablePollResponseEventContent::new(answers, poll_start_event_id);
let event_content =
AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
RUNTIME.spawn(async move {
timeline.send(event_content).await;
});
Ok(())
}
pub fn end_poll(&self, poll_start_id: String, text: String) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
return Err(anyhow!("Timeline not set up, can't end the poll").into());
}
};
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
RUNTIME.spawn(async move {
timeline.send(event_content).await;
});
Ok(())
}
pub fn send_reply(
&self,
msg: Arc<RoomMessageEventContentWithoutRelation>,
reply_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
};
RUNTIME.block_on(async move {
timeline.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
anyhow::Ok(())
})?;
Ok(())
}
pub fn edit(
&self,
new_content: Arc<RoomMessageEventContentWithoutRelation>,
edit_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
};
RUNTIME.block_on(async move {
timeline.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
anyhow::Ok(())
})?;
Ok(())
}
/// Redacts an event from the room.
///
/// # Arguments
@@ -552,19 +266,6 @@ impl Room {
})
}
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => return Err(anyhow!("Timeline not set up, can't send message").into()),
};
RUNTIME.block_on(async move {
let event_id = EventId::parse(event_id)?;
timeline.toggle_reaction(&Annotation::new(event_id, key)).await?;
Ok(())
})
}
pub fn active_members_count(&self) -> u64 {
self.inner.active_members_count()
}
@@ -713,262 +414,6 @@ impl Room {
})
}
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
let timeline = RUNTIME
.block_on(self.timeline.read())
.as_ref()
.context("Timeline not set up, can't fetch event details")?
.clone();
RUNTIME.block_on(async move {
let event_id = <&EventId>::try_from(event_id.as_str())?;
timeline.fetch_details_for_event(event_id).await.context("Fetching event details")?;
Ok(())
})
}
pub fn send_image(
self: Arc<Self>,
url: String,
thumbnail_url: String,
image_info: ImageInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_image_info = BaseImageInfo::try_from(&image_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Image(base_image_info);
let attachment_config = match image_info.thumbnail_info {
Some(thumbnail_image_info) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
None => AttachmentConfig::new().info(attachment_info),
};
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_video(
self: Arc<Self>,
url: String,
thumbnail_url: String,
video_info: VideoInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Video(base_video_info);
let attachment_config = match video_info.thumbnail_info {
Some(thumbnail_image_info) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
None => AttachmentConfig::new().info(attachment_info),
};
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_audio(
self: Arc<Self>,
url: String,
audio_info: AudioInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Audio(base_audio_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_voice_message(
self: Arc<Self>,
url: String,
audio_info: AudioInfo,
waveform: Vec<u16>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info =
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_file(
self: Arc<Self>,
url: String,
file_info: FileInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_file_info: BaseFileInfo =
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::File(base_file_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn retry_send(&self, txn_id: String) {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
error!("Timeline not set up, can't retry sending message");
return;
}
};
RUNTIME.spawn(async move {
if let Err(e) = timeline.retry_send(txn_id.as_str().into()).await {
error!(txn_id, "Failed to retry sending: {e}");
}
});
}
pub fn send_location(
&self,
body: String,
geo_uri: String,
description: Option<String>,
zoom_level: Option<u8>,
asset_type: Option<AssetType>,
) {
let mut location_event_message_content =
LocationMessageEventContent::new(body, geo_uri.clone());
if let Some(asset_type) = asset_type {
location_event_message_content =
location_event_message_content.with_asset_type(RumaAssetType::from(asset_type));
}
let mut location_content = LocationContent::new(geo_uri);
location_content.description = description;
location_content.zoom_level = zoom_level.and_then(ZoomLevel::new);
location_event_message_content.location = Some(location_content);
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
MessageType::Location(location_event_message_content),
);
self.send(Arc::new(room_message_event_content))
}
pub fn cancel_send(&self, txn_id: String) {
let timeline = match &*RUNTIME.block_on(self.timeline.read()) {
Some(t) => Arc::clone(t),
None => {
error!("Timeline not set up, can't retry sending message");
return;
}
};
RUNTIME.spawn(async move {
if !timeline.cancel_send(txn_id.as_str().into()).await {
info!(txn_id, "Failed to discard local echo: Not found");
}
});
}
pub fn get_event_timeline_item_by_event_id(
&self,
event_id: String,
) -> Result<Arc<EventTimelineItem>, ClientError> {
RUNTIME.block_on(async move {
let timeline = self
.timeline
.read()
.await
.clone()
.context("Timeline not set up, can't get event ")?;
let event_id = EventId::parse(event_id)?;
let item = timeline
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
})
}
pub fn get_timeline_event_content_by_event_id(
&self,
event_id: String,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
RUNTIME.block_on(async move {
let timeline = self
.timeline
.read()
.await
.clone()
.context("Timeline not set up, can't get event content")?;
let event_id = EventId::parse(event_id)?;
let item = timeline
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
let msgtype = item
.content()
.as_message()
.context("Item with given event ID is not a message")?
.msgtype()
.to_owned();
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
})
}
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_redact(&user_id).await?)
@@ -979,6 +424,24 @@ impl Room {
Ok(self.inner.can_user_ban(&user_id).await?)
}
pub async fn ban_user(
&self,
user_id: String,
reason: Option<String>,
) -> Result<(), ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.ban_user(&user_id, reason.as_deref()).await?)
}
pub async fn unban_user(
&self,
user_id: String,
reason: Option<String>,
) -> Result<(), ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
}
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_invite(&user_id).await?)
@@ -989,6 +452,15 @@ impl Room {
Ok(self.inner.can_user_kick(&user_id).await?)
}
pub async fn kick_user(
&self,
user_id: String,
reason: Option<String>,
) -> Result<(), ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
}
pub async fn can_user_send_state(
&self,
user_id: String,
@@ -1018,86 +490,10 @@ impl Room {
pub fn own_user_id(&self) -> String {
self.inner.own_user_id().to_string()
}
}
impl Room {
fn build_thumbnail_info(
&self,
thumbnail_url: String,
thumbnail_info: ThumbnailInfo,
) -> Result<Thumbnail, RoomError> {
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
Ok(Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
})
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
Ok(self.inner.typing_notice(is_typing).await?)
}
async fn send_attachment(
&self,
url: String,
mime_type: Mime,
attachment_config: AttachmentConfig,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<(), RoomError> {
let timeline = self.timeline.read().await.clone().ok_or(RoomError::TimelineUnavailable)?;
let request = timeline.send_attachment(url, mime_type, attachment_config);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
});
}
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
Ok(())
}
}
#[derive(uniffi::Object)]
pub struct SendAttachmentJoinHandle {
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
abort_hdl: AbortHandle,
}
impl SendAttachmentJoinHandle {
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
let abort_hdl = join_hdl.abort_handle();
let join_hdl = Arc::new(Mutex::new(join_hdl));
Arc::new(Self { join_hdl, abort_hdl })
}
}
#[uniffi::export(async_runtime = "tokio")]
impl SendAttachmentJoinHandle {
pub async fn join(&self) -> Result<(), RoomError> {
let join_hdl = self.join_hdl.clone();
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
}
pub fn cancel(&self) {
self.abort_hdl.abort();
}
}
#[derive(uniffi::Record)]
pub struct RoomTimelineListenerResult {
pub items: Vec<Arc<TimelineItem>>,
pub items_stream: Arc<TaskHandle>,
}
#[uniffi::export(callback_interface)]
@@ -1105,52 +501,6 @@ pub trait RoomInfoListener: Sync + Send {
fn call(&self, room_info: RoomInfo);
}
#[uniffi::export(callback_interface)]
pub trait BackPaginationStatusListener: Sync + Send {
fn on_update(&self, status: BackPaginationStatus);
}
#[derive(uniffi::Enum)]
pub enum PaginationOptions {
SingleRequest { event_limit: u16, wait_for_token: bool },
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
}
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
fn from(value: PaginationOptions) -> Self {
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
let (wait_for_token, mut opts) = match value {
PaginationOptions::SingleRequest { event_limit, wait_for_token } => {
(wait_for_token, Opts::single_request(event_limit))
}
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
(wait_for_token, Opts::until_num_items(event_limit, items))
}
};
if wait_for_token {
opts = opts.wait_for_token();
}
opts
}
}
#[derive(Clone, uniffi::Enum)]
pub enum AssetType {
Sender,
Pin,
}
impl From<AssetType> for RumaAssetType {
fn from(value: AssetType) -> Self {
match value {
AssetType::Sender => Self::Self_,
AssetType::Pin => Self::Pin,
}
}
}
#[derive(uniffi::Object)]
pub struct RoomMembersIterator {
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
+12
View File
@@ -31,6 +31,15 @@ pub struct RoomInfo {
user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
/// "Interesting" messages received in that room, independently of the
/// notification settings.
num_unread_messages: u64,
/// Events that will notify the user, according to their
/// notification settings.
num_unread_notifications: u64,
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
}
impl RoomInfo {
@@ -75,6 +84,9 @@ impl RoomInfo {
.iter()
.map(|u| u.to_string())
.collect(),
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
})
}
}
+9 -4
View File
@@ -13,14 +13,17 @@ use matrix_sdk::{
RoomListEntry as MatrixRoomListEntry,
};
use matrix_sdk_ui::room_list_service::filters::{
new_filter_all, new_filter_fuzzy_match_room_name, new_filter_none,
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
new_filter_normalized_match_room_name,
};
use tokio::sync::RwLock;
use crate::{
error::ClientError, room::Room, room_info::RoomInfo, timeline::EventTimelineItem, TaskHandle,
RUNTIME,
error::ClientError,
room::Room,
room_info::RoomInfo,
timeline::{EventTimelineItem, Timeline},
TaskHandle, RUNTIME,
};
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -376,6 +379,7 @@ impl RoomListDynamicEntriesController {
match kind {
Kind::All => self.inner.set_filter(new_filter_all()),
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
Kind::None => self.inner.set_filter(new_filter_none()),
Kind::NormalizedMatchRoomName { pattern } => {
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
@@ -398,6 +402,7 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All,
AllNonLeft,
None,
NormalizedMatchRoomName { pattern: String },
FuzzyMatchRoomName { pattern: String },
@@ -443,7 +448,7 @@ impl RoomListItem {
async fn full_room(&self) -> Arc<Room> {
Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
Arc::new(RwLock::new(Some(self.inner.timeline().await))),
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
))
}
@@ -140,6 +140,7 @@ impl RoomMember {
#[derive(Clone, uniffi::Enum)]
pub enum StateEventType {
CallMember,
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
@@ -166,6 +167,7 @@ pub enum StateEventType {
impl From<StateEventType> for ruma::events::StateEventType {
fn from(val: StateEventType) -> Self {
match val {
StateEventType::CallMember => Self::CallMember,
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
+712
View File
@@ -0,0 +1,712 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::BTreeSet, sync::Arc, time::Duration};
use extension_trait::extension_trait;
use matrix_sdk::attachment::{
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
};
use ruma::{
assign,
events::{
location::AssetType as RumaAssetType,
poll::start::PollKind as RumaPollKind,
room::{
message::{
AudioInfo as RumaAudioInfo,
AudioMessageEventContent as RumaAudioMessageEventContent,
EmoteMessageEventContent as RumaEmoteMessageEventContent, FileInfo as RumaFileInfo,
FileMessageEventContent as RumaFileMessageEventContent,
FormattedBody as RumaFormattedBody,
ImageMessageEventContent as RumaImageMessageEventContent,
LocationMessageEventContent as RumaLocationMessageEventContent,
MessageType as RumaMessageType,
NoticeMessageEventContent as RumaNoticeMessageEventContent,
RoomMessageEventContentWithoutRelation,
TextMessageEventContent as RumaTextMessageEventContent,
UnstableAudioDetailsContentBlock as RumaUnstableAudioDetailsContentBlock,
UnstableVoiceContentBlock as RumaUnstableVoiceContentBlock,
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
},
},
serde::JsonObject,
OwnedUserId, UInt, UserId,
};
use tracing::info;
use crate::{
error::{ClientError, MediaInfoError},
helpers::unwrap_or_clone_arc,
utils::u64_to_uint,
};
#[uniffi::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[uniffi::export]
pub fn message_event_content_new(
msgtype: MessageType,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
}
#[uniffi::export]
pub fn message_event_content_from_markdown(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
}
#[uniffi::export]
pub fn message_event_content_from_markdown_as_emote(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
}
#[uniffi::export]
pub fn message_event_content_from_html(
body: String,
html_body: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_html(
body, html_body,
)))
}
#[uniffi::export]
pub fn message_event_content_from_html_as_emote(
body: String,
html_body: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_html(
body, html_body,
)))
}
#[extension_trait]
pub impl MediaSourceExt for MediaSource {
fn from_json(json: String) -> Result<MediaSource, ClientError> {
let res = serde_json::from_str(&json)?;
Ok(res)
}
fn to_json(&self) -> String {
serde_json::to_string(self).expect("Media source should always be serializable ")
}
fn url(&self) -> String {
match self {
MediaSource::Plain(url) => url.to_string(),
MediaSource::Encrypted(file) => file.url.to_string(),
}
}
}
#[extension_trait]
pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWithoutRelation {
fn with_mentions(self: Arc<Self>, mentions: Mentions) -> Arc<Self> {
let mut content = unwrap_or_clone_arc(self);
content.mentions = Some(mentions.into());
Arc::new(content)
}
}
pub struct Mentions {
pub user_ids: Vec<String>,
pub room: bool,
}
impl From<Mentions> for ruma::events::Mentions {
fn from(value: Mentions) -> Self {
let mut user_ids = BTreeSet::<OwnedUserId>::new();
for user_id in value.user_ids {
if let Ok(user_id) = UserId::parse(user_id) {
user_ids.insert(user_id);
}
}
let mut result = Self::default();
result.user_ids = user_ids;
result.room = value.room;
result
}
}
#[derive(Clone, uniffi::Enum)]
pub enum MessageType {
Emote { content: EmoteMessageContent },
Image { content: ImageMessageContent },
Audio { content: AudioMessageContent },
Video { content: VideoMessageContent },
File { content: FileMessageContent },
Notice { content: NoticeMessageContent },
Text { content: TextMessageContent },
Location { content: LocationContent },
Other { msgtype: String, body: String },
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = serde_json::Error;
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
Ok(match value {
MessageType::Emote { content } => {
Self::Emote(assign!(RumaEmoteMessageEventContent::plain(content.body), {
formatted: content.formatted.map(Into::into),
}))
}
MessageType::Image { content } => Self::Image(
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Audio { content } => Self::Audio(
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Video { content } => Self::Video(
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::File { content } => Self::File(
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
.filename(content.filename)
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Notice { content } => {
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
formatted: content.formatted.map(Into::into),
}))
}
MessageType::Text { content } => {
Self::Text(assign!(RumaTextMessageEventContent::plain(content.body), {
formatted: content.formatted.map(Into::into),
}))
}
MessageType::Location { content } => {
Self::Location(RumaLocationMessageEventContent::new(content.body, content.geo_uri))
}
MessageType::Other { msgtype, body } => {
Self::new(&msgtype, body, JsonObject::default())?
}
})
}
}
impl From<RumaMessageType> for MessageType {
fn from(value: RumaMessageType) -> Self {
match value {
RumaMessageType::Emote(c) => MessageType::Emote {
content: EmoteMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
},
},
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
voice: c.voice.map(Into::into),
},
},
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::Notice(c) => MessageType::Notice {
content: NoticeMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
},
},
RumaMessageType::Text(c) => MessageType::Text {
content: TextMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
},
},
RumaMessageType::Location(c) => {
let (description, zoom_level) =
c.location.map(|loc| (loc.description, loc.zoom_level)).unwrap_or((None, None));
MessageType::Location {
content: LocationContent {
body: c.body,
geo_uri: c.geo_uri,
description,
zoom_level: zoom_level.and_then(|z| z.get().try_into().ok()),
asset: c.asset.and_then(|a| match a.type_ {
RumaAssetType::Self_ => Some(AssetType::Sender),
RumaAssetType::Pin => Some(AssetType::Pin),
_ => None,
}),
},
}
}
_ => MessageType::Other {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
},
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct EmoteMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
}
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<ImageInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<AudioInfo>,
pub audio: Option<UnstableAudioDetailsContent>,
pub voice: Option<UnstableVoiceContent>,
}
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<VideoInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
pub body: String,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct ImageInfo {
pub height: Option<u64>,
pub width: Option<u64>,
pub mimetype: Option<String>,
pub size: Option<u64>,
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
pub blurhash: Option<String>,
}
impl From<ImageInfo> for RumaImageInfo {
fn from(value: ImageInfo) -> Self {
assign!(RumaImageInfo::new(), {
height: value.height.map(u64_to_uint),
width: value.width.map(u64_to_uint),
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
blurhash: value.blurhash,
})
}
}
impl TryFrom<&ImageInfo> for BaseImageInfo {
type Error = MediaInfoError;
fn try_from(value: &ImageInfo) -> Result<Self, MediaInfoError> {
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
Ok(BaseImageInfo {
height: Some(height),
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
})
}
}
#[derive(Clone, uniffi::Record)]
pub struct AudioInfo {
pub duration: Option<Duration>,
pub size: Option<u64>,
pub mimetype: Option<String>,
}
impl From<AudioInfo> for RumaAudioInfo {
fn from(value: AudioInfo) -> Self {
assign!(RumaAudioInfo::new(), {
duration: value.duration,
size: value.size.map(u64_to_uint),
mimetype: value.mimetype,
})
}
}
impl TryFrom<&AudioInfo> for BaseAudioInfo {
type Error = MediaInfoError;
fn try_from(value: &AudioInfo) -> Result<Self, MediaInfoError> {
let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
}
}
#[derive(Clone, uniffi::Record)]
pub struct UnstableAudioDetailsContent {
pub duration: Duration,
pub waveform: Vec<u16>,
}
impl From<RumaUnstableAudioDetailsContentBlock> for UnstableAudioDetailsContent {
fn from(details: RumaUnstableAudioDetailsContentBlock) -> Self {
Self {
duration: details.duration,
waveform: details
.waveform
.iter()
.map(|x| u16::try_from(x.get()).unwrap_or(0))
.collect(),
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct UnstableVoiceContent {}
impl From<RumaUnstableVoiceContentBlock> for UnstableVoiceContent {
fn from(_details: RumaUnstableVoiceContentBlock) -> Self {
Self {}
}
}
#[derive(Clone, uniffi::Record)]
pub struct VideoInfo {
pub duration: Option<Duration>,
pub height: Option<u64>,
pub width: Option<u64>,
pub mimetype: Option<String>,
pub size: Option<u64>,
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
pub blurhash: Option<String>,
}
impl From<VideoInfo> for RumaVideoInfo {
fn from(value: VideoInfo) -> Self {
assign!(RumaVideoInfo::new(), {
duration: value.duration,
height: value.height.map(u64_to_uint),
width: value.width.map(u64_to_uint),
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
blurhash: value.blurhash,
})
}
}
impl TryFrom<&VideoInfo> for BaseVideoInfo {
type Error = MediaInfoError;
fn try_from(value: &VideoInfo) -> Result<Self, MediaInfoError> {
let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
Ok(BaseVideoInfo {
duration: Some(duration),
height: Some(height),
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
})
}
}
#[derive(Clone, uniffi::Record)]
pub struct FileInfo {
pub mimetype: Option<String>,
pub size: Option<u64>,
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
}
impl From<FileInfo> for RumaFileInfo {
fn from(value: FileInfo) -> Self {
assign!(RumaFileInfo::new(), {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
})
}
}
impl TryFrom<&FileInfo> for BaseFileInfo {
type Error = MediaInfoError;
fn try_from(value: &FileInfo) -> Result<Self, MediaInfoError> {
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseFileInfo { size: Some(size) })
}
}
#[derive(Clone, uniffi::Record)]
pub struct ThumbnailInfo {
pub height: Option<u64>,
pub width: Option<u64>,
pub mimetype: Option<String>,
pub size: Option<u64>,
}
impl From<ThumbnailInfo> for RumaThumbnailInfo {
fn from(value: ThumbnailInfo) -> Self {
assign!(RumaThumbnailInfo::new(), {
height: value.height.map(u64_to_uint),
width: value.width.map(u64_to_uint),
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
})
}
}
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
type Error = MediaInfoError;
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
}
}
#[derive(Clone, uniffi::Record)]
pub struct NoticeMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
}
#[derive(Clone, uniffi::Record)]
pub struct TextMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
}
#[derive(Clone, uniffi::Record)]
pub struct LocationContent {
pub body: String,
pub geo_uri: String,
pub description: Option<String>,
pub zoom_level: Option<u8>,
pub asset: Option<AssetType>,
}
#[derive(Clone, uniffi::Enum)]
pub enum AssetType {
Sender,
Pin,
}
impl From<AssetType> for RumaAssetType {
fn from(value: AssetType) -> Self {
match value {
AssetType::Sender => Self::Self_,
AssetType::Pin => Self::Pin,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct FormattedBody {
pub format: MessageFormat,
pub body: String,
}
impl From<FormattedBody> for RumaFormattedBody {
fn from(f: FormattedBody) -> Self {
Self {
format: match f.format {
MessageFormat::Html => matrix_sdk::ruma::events::room::message::MessageFormat::Html,
MessageFormat::Unknown { format } => format.into(),
},
body: f.body,
}
}
}
impl From<&RumaFormattedBody> for FormattedBody {
fn from(f: &RumaFormattedBody) -> Self {
Self {
format: match &f.format {
matrix_sdk::ruma::events::room::message::MessageFormat::Html => MessageFormat::Html,
_ => MessageFormat::Unknown { format: f.format.to_string() },
},
body: f.body.clone(),
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum MessageFormat {
Html,
Unknown { format: String },
}
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
});
Self {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
blurhash: info.blurhash.clone(),
}
}
}
impl From<&RumaAudioInfo> for AudioInfo {
fn from(info: &RumaAudioInfo) -> Self {
Self {
duration: info.duration,
size: info.size.map(Into::into),
mimetype: info.mimetype.clone(),
}
}
}
impl From<&RumaVideoInfo> for VideoInfo {
fn from(info: &RumaVideoInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
});
Self {
duration: info.duration,
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
blurhash: info.blurhash.clone(),
}
}
}
impl From<&RumaFileInfo> for FileInfo {
fn from(info: &RumaFileInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
});
Self {
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
}
}
}
#[derive(uniffi::Enum)]
pub enum PollKind {
Disclosed,
Undisclosed,
}
impl From<PollKind> for RumaPollKind {
fn from(value: PollKind) -> Self {
match value {
PollKind::Disclosed => Self::Disclosed,
PollKind::Undisclosed => Self::Undisclosed,
}
}
}
impl From<RumaPollKind> for PollKind {
fn from(value: RumaPollKind) -> Self {
match value {
RumaPollKind::Disclosed => Self::Disclosed,
RumaPollKind::Undisclosed => Self::Undisclosed,
_ => {
info!("Unknown poll kind, defaulting to undisclosed");
Self::Undisclosed
}
}
}
}
@@ -31,11 +31,17 @@ impl SessionVerificationEmoji {
}
}
#[derive(uniffi::Enum)]
pub enum SessionVerificationData {
Emojis { emojis: Vec<Arc<SessionVerificationEmoji>>, indices: Vec<u8> },
Decimals { values: Vec<u16> },
}
#[uniffi::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: Vec<Arc<SessionVerificationEmoji>>);
fn did_receive_verification_data(&self, data: SessionVerificationData);
fn did_fail(&self);
fn did_cancel(&self);
fn did_finish(&self);
@@ -199,25 +205,31 @@ impl SessionVerificationController {
while let Some(state) = stream.next().await {
match state {
SasState::KeysExchanged { emojis, decimals: _ } => {
// TODO: If emojis is None, decimals should be used.
if let Some(emojis) = emojis {
if let Some(delegate) = &*delegate.read().unwrap() {
let emojis = emojis
.emojis
.iter()
.map(|e| {
Arc::new(SessionVerificationEmoji {
symbol: e.symbol.to_owned(),
description: e.description.to_owned(),
})
})
.collect::<Vec<_>>();
delegate.did_receive_verification_data(emojis);
SasState::KeysExchanged { emojis, decimals } => {
if let Some(delegate) = &*delegate.read().unwrap() {
if let Some(emojis) = emojis {
delegate.did_receive_verification_data(
SessionVerificationData::Emojis {
emojis: emojis
.emojis
.into_iter()
.map(|emoji| {
Arc::new(SessionVerificationEmoji {
symbol: emoji.symbol.to_owned(),
description: emoji.description.to_owned(),
})
})
.collect(),
indices: emojis.indices.to_vec(),
},
);
} else {
delegate.did_receive_verification_data(
SessionVerificationData::Decimals {
values: vec![decimals.0, decimals.1, decimals.2],
},
)
}
} else if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_fail()
}
}
SasState::Done { .. } => {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,399 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use tracing::warn;
use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContentKind::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
url: content.url.to_string(),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
user_id: membership.user_id().to_string(),
change: membership.change().map(Into::into),
},
Content::ProfileChange(profile) => {
let (display_name, prev_display_name) = profile
.displayname_change()
.map(|change| (change.new.clone(), change.old.clone()))
.unzip();
let (avatar_url, prev_avatar_url) = profile
.avatar_url_change()
.map(|change| {
(
change.new.as_ref().map(ToString::to_string),
change.old.as_ref().map(ToString::to_string),
)
})
.unzip();
TimelineItemContentKind::ProfileChange {
display_name: display_name.flatten(),
prev_display_name: prev_display_name.flatten(),
avatar_url: avatar_url.flatten(),
prev_avatar_url: prev_avatar_url.flatten(),
}
}
Content::OtherState(state) => TimelineItemContentKind::State {
state_key: state.state_key().to_owned(),
content: state.content().into(),
},
Content::FailedToParseMessageLike { event_type, error } => {
TimelineItemContentKind::FailedToParseMessageLike {
event_type: event_type.to_string(),
error: error.to_string(),
}
}
Content::FailedToParseState { event_type, state_key, error } => {
TimelineItemContentKind::FailedToParseState {
event_type: event_type.to_string(),
state_key: state_key.to_string(),
error: error.to_string(),
}
}
}
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
}
}
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
RedactedMessage,
Sticker {
body: String,
info: ImageInfo,
url: String,
},
Poll {
question: String,
kind: PollKind,
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<u64>,
has_been_edited: bool,
},
UnableToDecrypt {
msg: EncryptedMessage,
},
RoomMembership {
user_id: String,
change: Option<MembershipChange>,
},
ProfileChange {
display_name: Option<String>,
prev_display_name: Option<String>,
avatar_url: Option<String>,
prev_avatar_url: Option<String>,
},
State {
state_key: String,
content: OtherState,
},
FailedToParseMessageLike {
event_type: String,
error: String,
},
FailedToParseState {
event_type: String,
state_key: String,
error: String,
},
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
pub fn msgtype(&self) -> MessageType {
self.0.msgtype().clone().into()
}
pub fn body(&self) -> String {
self.0.msgtype().body().to_owned()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
self.0.in_reply_to().map(InReplyToDetails::from)
}
pub fn is_threaded(&self) -> bool {
self.0.is_threaded()
}
pub fn is_edited(&self) -> bool {
self.0.is_edited()
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(event.content().to_owned())),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
TimelineDetails::Error(err) => {
RepliedToEventDetails::Error { message: err.to_string() }
}
};
Self { event_id, event }
}
}
#[derive(uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
/// The Curve25519 key of the sender.
sender_key: String,
},
// Other fields not included because UniFFI doesn't have the concept of
// deprecated fields right now.
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
},
Unknown,
}
impl EncryptedMessage {
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id }
}
Message::Unknown => Self::Unknown,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct Reaction {
pub key: String,
pub count: u64,
pub senders: Vec<ReactionSenderData>,
}
#[derive(Clone, uniffi::Record)]
pub struct ReactionSenderData {
pub sender_id: String,
pub timestamp: u64,
}
#[derive(Clone, uniffi::Enum)]
pub enum MembershipChange {
None,
Error,
Joined,
Left,
Banned,
Unbanned,
Kicked,
Invited,
KickedAndBanned,
InvitationAccepted,
InvitationRejected,
InvitationRevoked,
Knocked,
KnockAccepted,
KnockRetracted,
KnockDenied,
NotImplemented,
}
impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
fn from(membership_change: matrix_sdk_ui::timeline::MembershipChange) -> Self {
use matrix_sdk_ui::timeline::MembershipChange as Change;
match membership_change {
Change::None => Self::None,
Change::Error => Self::Error,
Change::Joined => Self::Joined,
Change::Left => Self::Left,
Change::Banned => Self::Banned,
Change::Unbanned => Self::Unbanned,
Change::Kicked => Self::Kicked,
Change::Invited => Self::Invited,
Change::KickedAndBanned => Self::KickedAndBanned,
Change::InvitationAccepted => Self::InvitationAccepted,
Change::InvitationRejected => Self::InvitationRejected,
Change::InvitationRevoked => Self::InvitationRevoked,
Change::Knocked => Self::Knocked,
Change::KnockAccepted => Self::KnockAccepted,
Change::KnockRetracted => Self::KnockRetracted,
Change::KnockDenied => Self::KnockDenied,
Change::NotImplemented => Self::NotImplemented,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum OtherState {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar { url: Option<String> },
RoomCanonicalAlias,
RoomCreate,
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomJoinRules,
RoomName { name: Option<String> },
RoomPinnedEvents,
RoomPowerLevels,
RoomServerAcl,
RoomThirdPartyInvite { display_name: Option<String> },
RoomTombstone,
RoomTopic { topic: Option<String> },
SpaceChild,
SpaceParent,
Custom { event_type: String },
}
impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
match content {
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
Content::RoomAliases(_) => Self::RoomAliases,
Content::RoomAvatar(c) => {
let url = match c {
FullContent::Original { content, .. } => {
content.url.as_ref().map(ToString::to_string)
}
FullContent::Redacted(_) => None,
};
Self::RoomAvatar { url }
}
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
Content::RoomCreate(_) => Self::RoomCreate,
Content::RoomEncryption(_) => Self::RoomEncryption,
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
Content::RoomJoinRules(_) => Self::RoomJoinRules,
Content::RoomName(c) => {
let name = match c {
FullContent::Original { content, .. } => Some(content.name.clone()),
FullContent::Redacted(_) => None,
};
Self::RoomName { name }
}
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
let display_name = match c {
FullContent::Original { content, .. } => Some(content.display_name.clone()),
FullContent::Redacted(_) => None,
};
Self::RoomThirdPartyInvite { display_name }
}
Content::RoomTombstone(_) => Self::RoomTombstone,
Content::RoomTopic(c) => {
let topic = match c {
FullContent::Original { content, .. } => Some(content.topic.clone()),
FullContent::Redacted(_) => None,
};
Self::RoomTopic { topic }
}
Content::SpaceChild(_) => Self::SpaceChild,
Content::SpaceParent(_) => Self::SpaceParent,
Content::_Custom { event_type, .. } => Self::Custom { event_type: event_type.clone() },
}
}
}
#[derive(uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContentKind {
fn from(value: PollResult) -> Self {
TimelineItemContentKind::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
answers: value
.answers
.into_iter()
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: value.votes,
end_time: value.end_time,
has_been_edited: value.has_been_edited,
}
}
}
+998
View File
@@ -0,0 +1,998 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
};
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin, Profile, TimelineDetails};
use mime::Mime;
use ruma::{
events::{
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::{
unstable_end::UnstablePollEndEventContent,
unstable_response::UnstablePollResponseEventContent,
unstable_start::{
NewUnstablePollStartEventContent, UnstablePollAnswer, UnstablePollAnswers,
UnstablePollStartContentBlock,
},
},
receipt::ReceiptThread,
relation::Annotation,
room::message::{
ForwardThread, LocationMessageEventContent, MessageType,
RoomMessageEventContentWithoutRelation,
},
AnyMessageLikeEventContent,
},
EventId,
};
use tokio::{
sync::Mutex,
task::{AbortHandle, JoinHandle},
};
use tracing::{error, info, warn};
use uuid::Uuid;
use crate::{
client::ProgressWatcher,
error::{ClientError, RoomError},
helpers::unwrap_or_clone_arc,
ruma::{AssetType, AudioInfo, FileInfo, ImageInfo, PollKind, ThumbnailInfo, VideoInfo},
task_handle::TaskHandle,
RUNTIME,
};
mod content;
pub use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
#[derive(uniffi::Object)]
#[repr(transparent)]
pub struct Timeline {
pub(crate) inner: matrix_sdk_ui::timeline::Timeline,
}
impl Timeline {
pub(crate) fn new(inner: matrix_sdk_ui::timeline::Timeline) -> Arc<Self> {
Arc::new(Self { inner })
}
pub(crate) fn from_arc(inner: Arc<matrix_sdk_ui::timeline::Timeline>) -> Arc<Self> {
// SAFETY: repr(transparent) means transmuting the arc this way is allowed
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
}
fn build_thumbnail_info(
&self,
thumbnail_url: String,
thumbnail_info: ThumbnailInfo,
) -> Result<Thumbnail, RoomError> {
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
Ok(Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
})
}
async fn send_attachment(
&self,
url: String,
mime_type: Mime,
attachment_config: AttachmentConfig,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<(), RoomError> {
let request = self.inner.send_attachment(url, mime_type, attachment_config);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
});
}
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
Ok(())
}
}
#[uniffi::export(async_runtime = "tokio")]
impl Timeline {
pub async fn add_listener(
&self,
listener: Box<dyn TimelineListener>,
) -> RoomTimelineListenerResult {
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
let timeline_stream = TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(timeline_stream);
while let Some(diffs) = timeline_stream.next().await {
listener
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
}
}));
RoomTimelineListenerResult {
items: timeline_items.into_iter().map(TimelineItem::from_arc).collect(),
items_stream: Arc::new(timeline_stream),
}
}
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
RUNTIME.spawn(async move {
self.inner.retry_decryption(&session_ids).await;
});
}
pub async fn fetch_members(&self) {
self.inner.fetch_members().await
}
pub fn subscribe_to_back_pagination_status(
&self,
listener: Box<dyn BackPaginationStatusListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let mut subscriber = self.inner.back_pagination_status();
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Send the current state even if it hasn't changed right away.
listener.on_update(subscriber.next_now());
while let Some(status) = subscriber.next().await {
listener.on_update(status);
}
}))))
}
/// Loads older messages into the timeline.
///
/// Raises an exception if there are no timeline listeners.
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
RUNTIME.block_on(async { Ok(self.inner.paginate_backwards(opts.into()).await?) })
}
pub fn send_read_receipt(
&self,
receipt_type: ReceiptType,
event_id: String,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
self.inner
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
.await?;
Ok(())
})
}
pub fn send(self: Arc<Self>, msg: Arc<RoomMessageEventContentWithoutRelation>) {
RUNTIME.spawn(async move {
self.inner.send((*msg).to_owned().with_relation(None).into()).await;
});
}
pub fn send_image(
self: Arc<Self>,
url: String,
thumbnail_url: Option<String>,
image_info: ImageInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_image_info = BaseImageInfo::try_from(&image_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Image(base_image_info);
let attachment_config = match (thumbnail_url, image_info.thumbnail_info) {
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
};
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_video(
self: Arc<Self>,
url: String,
thumbnail_url: Option<String>,
video_info: VideoInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Video(base_video_info);
let attachment_config = match (thumbnail_url, video_info.thumbnail_info) {
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
};
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_audio(
self: Arc<Self>,
url: String,
audio_info: AudioInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Audio(base_audio_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_voice_message(
self: Arc<Self>,
url: String,
audio_info: AudioInfo,
waveform: Vec<u16>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info =
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn send_file(
self: Arc<Self>,
url: String,
file_info: FileInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_file_info: BaseFileInfo =
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::File(base_file_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
}
pub fn create_poll(
self: Arc<Self>,
question: String,
answers: Vec<String>,
max_selections: u8,
poll_kind: PollKind,
) -> Result<(), ClientError> {
let poll_data = PollData { question, answers, max_selections, poll_kind };
let poll_start_event_content = NewUnstablePollStartEventContent::plain_text(
poll_data.fallback_text(),
poll_data.try_into()?,
);
let event_content =
AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
RUNTIME.spawn(async move {
self.inner.send(event_content).await;
});
Ok(())
}
pub fn send_poll_response(
self: Arc<Self>,
poll_start_id: String,
answers: Vec<String>,
) -> Result<(), ClientError> {
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
let poll_response_event_content =
UnstablePollResponseEventContent::new(answers, poll_start_event_id);
let event_content =
AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
RUNTIME.spawn(async move {
self.inner.send(event_content).await;
});
Ok(())
}
pub fn end_poll(
self: Arc<Self>,
poll_start_id: String,
text: String,
) -> Result<(), ClientError> {
let poll_start_event_id =
EventId::parse(poll_start_id).context("Failed to parse EventId")?;
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
RUNTIME.spawn(async move {
self.inner.send(event_content).await;
});
Ok(())
}
pub fn send_reply(
&self,
msg: Arc<RoomMessageEventContentWithoutRelation>,
reply_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
anyhow::Ok(())
})?;
Ok(())
}
pub fn edit(
&self,
new_content: Arc<RoomMessageEventContentWithoutRelation>,
edit_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
anyhow::Ok(())
})?;
Ok(())
}
pub async fn edit_poll(
&self,
question: String,
answers: Vec<String>,
max_selections: u8,
poll_kind: PollKind,
edit_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
let poll_data = PollData { question, answers, max_selections, poll_kind };
RUNTIME.block_on(async {
self.inner
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
.await?;
anyhow::Ok(())
})?;
Ok(())
}
pub fn send_location(
self: Arc<Self>,
body: String,
geo_uri: String,
description: Option<String>,
zoom_level: Option<u8>,
asset_type: Option<AssetType>,
) {
let mut location_event_message_content =
LocationMessageEventContent::new(body, geo_uri.clone());
if let Some(asset_type) = asset_type {
location_event_message_content =
location_event_message_content.with_asset_type(RumaAssetType::from(asset_type));
}
let mut location_content = LocationContent::new(geo_uri);
location_content.description = description;
location_content.zoom_level = zoom_level.and_then(ZoomLevel::new);
location_event_message_content.location = Some(location_content);
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
MessageType::Location(location_event_message_content),
);
self.send(Arc::new(room_message_event_content))
}
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
Ok(())
})
}
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
let event_id = <&EventId>::try_from(event_id.as_str())?;
RUNTIME.block_on(async {
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
Ok(())
})
}
pub fn retry_send(self: Arc<Self>, txn_id: String) {
RUNTIME.spawn(async move {
if let Err(e) = self.inner.retry_send(txn_id.as_str().into()).await {
error!(txn_id, "Failed to retry sending: {e}");
}
});
}
pub fn cancel_send(self: Arc<Self>, txn_id: String) {
RUNTIME.spawn(async move {
if !self.inner.cancel_send(txn_id.as_str().into()).await {
info!(txn_id, "Failed to discard local echo: Not found");
}
});
}
pub fn get_event_timeline_item_by_event_id(
&self,
event_id: String,
) -> Result<Arc<EventTimelineItem>, ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
})
}
pub fn get_timeline_event_content_by_event_id(
&self,
event_id: String,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
let msgtype = item
.content()
.as_message()
.context("Item with given event ID is not a message")?
.msgtype()
.to_owned();
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
})
}
}
#[derive(uniffi::Record)]
pub struct RoomTimelineListenerResult {
pub items: Vec<Arc<TimelineItem>>,
pub items_stream: Arc<TaskHandle>,
}
#[uniffi::export(callback_interface)]
pub trait TimelineListener: Sync + Send {
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
}
#[uniffi::export(callback_interface)]
pub trait BackPaginationStatusListener: Sync + Send {
fn on_update(&self, status: BackPaginationStatus);
}
#[derive(Clone, uniffi::Object)]
pub enum TimelineDiff {
Append { values: Vec<Arc<TimelineItem>> },
Clear,
PushFront { value: Arc<TimelineItem> },
PushBack { value: Arc<TimelineItem> },
PopFront,
PopBack,
Insert { index: usize, value: Arc<TimelineItem> },
Set { index: usize, value: Arc<TimelineItem> },
Remove { index: usize },
Truncate { length: usize },
Reset { values: Vec<Arc<TimelineItem>> },
}
impl TimelineDiff {
pub(crate) fn new(inner: VectorDiff<Arc<matrix_sdk_ui::timeline::TimelineItem>>) -> Self {
match inner {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::Insert { index, value } => {
Self::Insert { index, value: TimelineItem::from_arc(value) }
}
VectorDiff::Set { index, value } => {
Self::Set { index, value: TimelineItem::from_arc(value) }
}
VectorDiff::Truncate { length } => Self::Truncate { length },
VectorDiff::Remove { index } => Self::Remove { index },
VectorDiff::PushBack { value } => {
Self::PushBack { value: TimelineItem::from_arc(value) }
}
VectorDiff::PushFront { value } => {
Self::PushFront { value: TimelineItem::from_arc(value) }
}
VectorDiff::PopBack => Self::PopBack,
VectorDiff::PopFront => Self::PopFront,
VectorDiff::Reset { values } => {
warn!("Timeline subscriber lagged behind and was reset");
Self::Reset { values: values.into_iter().map(TimelineItem::from_arc).collect() }
}
}
}
}
#[uniffi::export]
impl TimelineDiff {
pub fn change(&self) -> TimelineChange {
match self {
Self::Append { .. } => TimelineChange::Append,
Self::Insert { .. } => TimelineChange::Insert,
Self::Set { .. } => TimelineChange::Set,
Self::Remove { .. } => TimelineChange::Remove,
Self::PushBack { .. } => TimelineChange::PushBack,
Self::PushFront { .. } => TimelineChange::PushFront,
Self::PopBack => TimelineChange::PopBack,
Self::PopFront => TimelineChange::PopFront,
Self::Clear => TimelineChange::Clear,
Self::Truncate { .. } => TimelineChange::Truncate,
Self::Reset { .. } => TimelineChange::Reset,
}
}
pub fn append(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Append { values } => values)
}
pub fn insert(self: Arc<Self>) -> Option<InsertData> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Insert { index, value } => {
InsertData { index: index.try_into().unwrap(), item: value }
})
}
pub fn set(self: Arc<Self>) -> Option<SetData> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Set { index, value } => {
SetData { index: index.try_into().unwrap(), item: value }
})
}
pub fn remove(&self) -> Option<u32> {
as_variant!(self, Self::Remove { index } => (*index).try_into().unwrap())
}
pub fn push_back(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::PushBack { value } => value)
}
pub fn push_front(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::PushFront { value } => value)
}
pub fn reset(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Reset { values } => values)
}
}
#[derive(uniffi::Record)]
pub struct InsertData {
pub index: u32,
pub item: Arc<TimelineItem>,
}
#[derive(uniffi::Record)]
pub struct SetData {
pub index: u32,
pub item: Arc<TimelineItem>,
}
#[derive(Clone, Copy, uniffi::Enum)]
pub enum TimelineChange {
Append,
Clear,
Insert,
Set,
Remove,
PushBack,
PushFront,
PopBack,
PopFront,
Truncate,
Reset,
}
#[repr(transparent)]
#[derive(Clone, uniffi::Object)]
pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
impl TimelineItem {
pub(crate) fn from_arc(arc: Arc<matrix_sdk_ui::timeline::TimelineItem>) -> Arc<Self> {
// SAFETY: This is valid because Self is a repr(transparent) wrapper
// around the other Timeline type.
unsafe { Arc::from_raw(Arc::into_raw(arc) as _) }
}
}
#[uniffi::export]
impl TimelineItem {
pub fn as_event(self: Arc<Self>) -> Option<Arc<EventTimelineItem>> {
let event_item = self.0.as_event()?;
Some(Arc::new(EventTimelineItem(event_item.clone())))
}
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
match self.0.as_virtual()? {
VItem::DayDivider(ts) => Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() }),
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
}
}
pub fn unique_id(&self) -> u64 {
self.0.unique_id()
}
pub fn fmt_debug(&self) -> String {
format!("{:#?}", self.0)
}
}
/// This type represents the “send state” of a local event timeline item.
#[derive(Clone, uniffi::Enum)]
pub enum EventSendState {
/// The local event has not been sent yet.
NotSentYet,
/// The local event has been sent to the server, but unsuccessfully: The
/// sending has failed.
SendingFailed { error: String },
/// Sending has been cancelled because an earlier event in the
/// message-sending queue failed.
Cancelled,
/// The local event has been sent successfully to the server.
Sent { event_id: String },
}
impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
fn from(value: &matrix_sdk_ui::timeline::EventSendState) -> Self {
use matrix_sdk_ui::timeline::EventSendState::*;
match value {
NotSentYet => Self::NotSentYet,
SendingFailed { error } => Self::SendingFailed { error: error.to_string() },
Cancelled => Self::Cancelled,
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
}
}
}
#[derive(uniffi::Object)]
pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem);
#[uniffi::export]
impl EventTimelineItem {
pub fn is_local(&self) -> bool {
self.0.is_local_echo()
}
pub fn is_remote(&self) -> bool {
!self.0.is_local_echo()
}
pub fn transaction_id(&self) -> Option<String> {
self.0.transaction_id().map(ToString::to_string)
}
pub fn event_id(&self) -> Option<String> {
self.0.event_id().map(ToString::to_string)
}
pub fn sender(&self) -> String {
self.0.sender().to_string()
}
pub fn sender_profile(&self) -> ProfileDetails {
self.0.sender_profile().into()
}
pub fn is_own(&self) -> bool {
self.0.is_own()
}
pub fn is_editable(&self) -> bool {
self.0.is_editable()
}
pub fn content(&self) -> Arc<TimelineItemContent> {
Arc::new(TimelineItemContent(self.0.content().clone()))
}
pub fn timestamp(&self) -> u64 {
self.0.timestamp().0.into()
}
pub fn reactions(&self) -> Vec<Reaction> {
self.0
.reactions()
.iter()
.map(|(k, v)| Reaction {
key: k.to_owned(),
count: v.len().try_into().unwrap(),
senders: v
.senders()
.map(|v| ReactionSenderData {
sender_id: v.sender_id.to_string(),
timestamp: v.timestamp.0.into(),
})
.collect(),
})
.collect()
}
pub fn debug_info(&self) -> EventTimelineItemDebugInfo {
EventTimelineItemDebugInfo {
model: format!("{:#?}", self.0),
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
}
}
pub fn local_send_state(&self) -> Option<EventSendState> {
self.0.send_state().map(Into::into)
}
pub fn read_receipts(&self) -> HashMap<String, Receipt> {
self.0.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect()
}
pub fn origin(&self) -> Option<EventItemOrigin> {
self.0.origin()
}
pub fn can_be_replied_to(&self) -> bool {
self.0.can_be_replied_to()
}
}
#[derive(uniffi::Record)]
pub struct Receipt {
pub timestamp: Option<u64>,
}
impl From<ruma::events::receipt::Receipt> for Receipt {
fn from(value: ruma::events::receipt::Receipt) -> Self {
Receipt { timestamp: value.ts.map(|ts| ts.0.into()) }
}
}
#[derive(uniffi::Record)]
pub struct EventTimelineItemDebugInfo {
model: String,
original_json: Option<String>,
latest_edit_json: Option<String>,
}
#[derive(uniffi::Enum)]
pub enum ProfileDetails {
Unavailable,
Pending,
Ready { display_name: Option<String>, display_name_ambiguous: bool, avatar_url: Option<String> },
Error { message: String },
}
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() },
}
}
}
struct PollData {
question: String,
answers: Vec<String>,
max_selections: u8,
poll_kind: PollKind,
}
impl PollData {
fn fallback_text(&self) -> String {
self.answers.iter().enumerate().fold(self.question.clone(), |mut acc, (index, answer)| {
write!(&mut acc, "\n{}. {answer}", index + 1).unwrap();
acc
})
}
}
impl TryFrom<PollData> for UnstablePollStartContentBlock {
type Error = ClientError;
fn try_from(value: PollData) -> Result<Self, Self::Error> {
let poll_answers_vec: Vec<UnstablePollAnswer> = value
.answers
.iter()
.map(|answer| UnstablePollAnswer::new(Uuid::new_v4().to_string(), answer))
.collect();
let poll_answers = UnstablePollAnswers::try_from(poll_answers_vec)
.context("Failed to create poll answers")?;
let mut poll_content_block =
UnstablePollStartContentBlock::new(value.question.clone(), poll_answers);
poll_content_block.kind = value.poll_kind.into();
poll_content_block.max_selections = value.max_selections.into();
Ok(poll_content_block)
}
}
#[derive(uniffi::Object)]
pub struct SendAttachmentJoinHandle {
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
abort_hdl: AbortHandle,
}
impl SendAttachmentJoinHandle {
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
let abort_hdl = join_hdl.abort_handle();
let join_hdl = Arc::new(Mutex::new(join_hdl));
Arc::new(Self { join_hdl, abort_hdl })
}
}
#[uniffi::export(async_runtime = "tokio")]
impl SendAttachmentJoinHandle {
pub async fn join(&self) -> Result<(), RoomError> {
let join_hdl = self.join_hdl.clone();
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
}
pub fn cancel(&self) {
self.abort_hdl.abort();
}
}
#[derive(uniffi::Enum)]
pub enum PaginationOptions {
SimpleRequest { event_limit: u16, wait_for_token: bool },
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
}
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
fn from(value: PaginationOptions) -> Self {
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
let (wait_for_token, mut opts) = match value {
PaginationOptions::SimpleRequest { event_limit, wait_for_token } => {
(wait_for_token, Opts::simple_request(event_limit))
}
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
(wait_for_token, Opts::until_num_items(event_limit, items))
}
};
if wait_for_token {
opts = opts.wait_for_token();
}
opts
}
}
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
#[derive(uniffi::Enum)]
pub enum VirtualTimelineItem {
/// A divider between messages of two days.
DayDivider {
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
ts: u64,
},
/// The user's own read marker.
ReadMarker,
}
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
#[derive(uniffi::Enum)]
pub enum ReceiptType {
Read,
ReadPrivate,
FullyRead,
}
impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::ReceiptType {
fn from(value: ReceiptType) -> Self {
match value {
ReceiptType::Read => Self::Read,
ReceiptType::ReadPrivate => Self::Read,
ReceiptType::FullyRead => Self::FullyRead,
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::UInt;
use tracing::warn;
pub(crate) fn u64_to_uint(u: u64) -> UInt {
UInt::new(u).unwrap_or_else(|| {
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
UInt::MAX
})
}
+74
View File
@@ -111,6 +111,34 @@ pub async fn generate_webview_url(
.map(|url| url.to_string())?)
}
/// Defines if a call is encrypted and which encryption system should be used.
///
/// This controls the url parameters: `perParticipantE2EE`, `password`.
#[derive(uniffi::Enum, Clone)]
pub enum EncryptionSystem {
/// Equivalent to the element call url parameter: `enableE2EE=false`
Unencrypted,
/// Equivalent to the element call url parameter:
/// `perParticipantE2EE=true`
PerParticipantKeys,
/// Equivalent to the element call url parameter:
/// `password={secret}`
SharedSecret {
/// The secret/password which is used in the url.
secret: String,
},
}
impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
fn from(value: EncryptionSystem) -> Self {
match value {
EncryptionSystem::Unencrypted => Self::Unencrypted,
EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
}
}
}
/// Properties to create a new virtual Element Call widget.
#[derive(uniffi::Record, Clone)]
pub struct VirtualElementCallWidgetOptions {
@@ -174,6 +202,11 @@ pub struct VirtualElementCallWidgetOptions {
/// Can be used to pass a PostHog id to element call.
pub analytics_id: Option<String>,
/// The encryption system to use.
///
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
pub encryption: EncryptionSystem,
}
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
@@ -190,6 +223,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
confine_to_room: value.confine_to_room,
font: value.font,
analytics_id: value.analytics_id,
encryption: value.encryption.into(),
}
}
}
@@ -213,6 +247,46 @@ pub fn new_virtual_element_call_widget(
.map(|w| w.into())?)
}
/// The Capabilities required to run a element call widget.
///
/// This is intended to be used in combination with: `acquire_capabilities` of
/// the `CapabilitiesProvider`.
///
/// `acquire_capabilities` can simply return the `WidgetCapabilities` from this
/// function. Even if there are non intersecting permissions to what the widget
/// requested.
///
/// Editing and extending the capabilities from this function is also possible,
/// but should only be done as temporal workarounds until this function is
/// adjusted
#[uniffi::export]
pub fn get_element_call_required_permissions() -> WidgetCapabilities {
use ruma::events::StateEventType;
WidgetCapabilities {
read: vec![
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
send: vec![
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
WidgetEventFilter::StateWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
WidgetEventFilter::StateWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
requires_client: true,
}
}
#[derive(uniffi::Record)]
pub struct ClientProperties {
/// The client_id provides the widget with the option to behave differently
+4
View File
@@ -0,0 +1,4 @@
{
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.rustfmt.extraArgs": ["+nightly"]
}
+1 -1
View File
@@ -36,7 +36,7 @@ FAILURES = {
"Success": (lambda x: None, 50),
"Gateway error":
(lambda _: http.Response.make(500, b"Gateway error"), 20),
"Limit exeeded": (lambda _: http.Response.make(
"Limit exceeded": (lambda _: http.Response.make(
429,
json.dumps({
"errcode": "M_LIMIT_EXCEEDED",
@@ -1,6 +1,4 @@
# Changelog
## unreleased
# 0.7.0
- Rename `RoomType` to `RoomState`
- Add `RoomInfo::state` accessor
@@ -33,9 +31,9 @@
- Move `Session`, `SessionTokens` and associated methods to the `matrix-sdk` crate.
- Add `Room::subscribe_info`
## 0.5.1
# 0.5.1
### Bug Fixes
## Bug Fixes
- #664: Fix regression with push rules being applied to the own user_id only instead of all but the own user_id
## 0.5.0
# 0.5.0
+8 -8
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.6.1"
version = "0.7.0"
[package.metadata.docs.rs]
all-features = true
@@ -40,12 +40,13 @@ assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = "2.1.0"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
http = { workspace = true, optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true, default-features = false }
matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", optional = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
serde = { workspace = true, features = ["rc"] }
@@ -58,11 +59,10 @@ tracing = { workspace = true }
assert_matches = { workspace = true }
assert_matches2 = { workspace = true }
assign = "1.1.1"
ctor = { workspace = true }
futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+27 -17
View File
@@ -46,7 +46,7 @@ use ruma::{
},
push::{Action, PushConditionRoomCtx, Ruleset},
serde::Raw,
MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, RoomVersionId, UInt, UserId,
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, UInt, UserId,
};
use tokio::sync::RwLock;
#[cfg(feature = "e2e-encryption")]
@@ -283,6 +283,7 @@ impl BaseClient {
user_ids: &mut BTreeSet<OwnedUserId>,
room_info: &mut RoomInfo,
changes: &mut StateChanges,
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
ambiguity_cache: &mut AmbiguityCache,
) -> Result<Timeline> {
let mut timeline = Timeline::new(limited, prev_batch);
@@ -397,8 +398,7 @@ impl BaseClient {
let actions = push_rules.get_actions(&event.event, context);
if actions.iter().any(Action::should_notify) {
changes.add_notification(
room.room_id(),
notifications.entry(room.room_id().to_owned()).or_default().push(
Notification::new(
actions.to_owned(),
event.event.clone(),
@@ -541,8 +541,7 @@ impl BaseClient {
for (user_id, rooms) in e.content.iter() {
for room_id in rooms {
trace!(
room_id = room_id.as_str(),
target = user_id.as_str(),
?room_id, target = ?user_id,
"Marking room as direct room"
);
@@ -608,8 +607,7 @@ impl BaseClient {
// event. If we found one, set it as the latest and delete any older
// encrypted events
if let Some((found, found_index)) = self.decrypt_latest_suitable_event(room).await {
room.on_latest_event_decrypted(found, found_index);
changes.room_infos.insert(room.room_id().to_owned(), room.clone_info());
room.on_latest_event_decrypted(found, found_index, changes);
}
}
@@ -620,12 +618,21 @@ impl BaseClient {
/// decrypted event if we found one, along with its index in the
/// latest_encrypted_events list, or None if we didn't find one.
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
async fn decrypt_latest_suitable_event(&self, room: &Room) -> Option<(LatestEvent, usize)> {
async fn decrypt_latest_suitable_event(
&self,
room: &Room,
) -> Option<(Box<LatestEvent>, usize)> {
let enc_events = room.latest_encrypted_events();
// Walk backwards through the encrypted events, looking for one we can decrypt
for (i, event) in enc_events.iter().enumerate().rev() {
if let Ok(Some(decrypted)) = self.decrypt_sync_room_event(event, room.room_id()).await {
// Size of the decrypt_sync_room_event future should not impact this
// async fn since it is likely that there aren't even any encrypted
// events when calling it.
let decrypt_sync_room_event =
Box::pin(self.decrypt_sync_room_event(event, room.room_id()));
if let Ok(Some(decrypted)) = decrypt_sync_room_event.await {
// We found an event we can decrypt
if let Ok(any_sync_event) = decrypted.event.deserialize() {
// We can deserialize it to find its type
@@ -633,7 +640,7 @@ impl BaseClient {
is_suitable_for_latest_event(&any_sync_event)
{
// The event is the right type for us to use as latest_event
return Some((LatestEvent::new(decrypted), i));
return Some((Box::new(LatestEvent::new(decrypted)), i));
}
}
}
@@ -656,7 +663,7 @@ impl BaseClient {
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.store.save_changes(&changes).await?; // Update the store
room.update_summary(room_info); // Update the cached room handle
room.set_room_info(room_info); // Update the cached room handle
}
Ok(room)
@@ -677,7 +684,7 @@ impl BaseClient {
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.store.save_changes(&changes).await?; // Update the store
room.update_summary(room_info); // Update the cached room handle
room.set_room_info(room_info); // Update the cached room handle
}
Ok(())
@@ -733,6 +740,7 @@ impl BaseClient {
let push_rules = self.get_push_rules(&changes).await?;
let mut new_rooms = Rooms::default();
let mut notifications = Default::default();
for (room_id, new_info) in response.rooms.join {
let room = self.store.get_or_create_room(&room_id, RoomState::Joined);
@@ -788,6 +796,7 @@ impl BaseClient {
&mut user_ids,
&mut room_info,
&mut changes,
&mut notifications,
&mut ambiguity_cache,
)
.await?;
@@ -858,6 +867,7 @@ impl BaseClient {
&mut user_ids,
&mut room_info,
&mut changes,
&mut notifications,
&mut ambiguity_cache,
)
.await?;
@@ -906,7 +916,7 @@ impl BaseClient {
let sync_lock = self.sync_lock().write().await;
self.store.save_changes(&changes).await?;
*self.store.sync_token.write().await = Some(response.next_batch.clone());
self.apply_changes(&changes).await;
self.apply_changes(&changes);
drop(sync_lock);
info!("Processed a sync response in {:?}", now.elapsed());
@@ -917,20 +927,20 @@ impl BaseClient {
account_data: response.account_data.events,
to_device,
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
notifications: changes.notifications,
notifications,
};
Ok(response)
}
pub(crate) async fn apply_changes(&self, changes: &StateChanges) {
pub(crate) fn apply_changes(&self, changes: &StateChanges) {
if changes.account_data.contains_key(&GlobalAccountDataEventType::IgnoredUserList) {
self.ignore_user_list_changes.set(());
}
for (room_id, room_info) in &changes.room_infos {
if let Some(room) = self.store.get_room(room_id) {
room.update_summary(room_info.clone())
room.set_room_info(room_info.clone())
}
}
}
@@ -1024,7 +1034,7 @@ impl BaseClient {
changes.add_room(room_info);
self.store.save_changes(&changes).await?;
self.apply_changes(&changes).await;
self.apply_changes(&changes);
}
Ok(MembersResponse {
+1
View File
@@ -27,6 +27,7 @@ use ruma::{
/// that only prints the event type of each item.
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
+6 -9
View File
@@ -30,8 +30,12 @@ mod error;
pub mod latest_event;
pub mod media;
mod rooms;
mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;
pub mod store;
pub mod sync;
mod utils;
@@ -51,15 +55,8 @@ pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
};
#[cfg(all(test, not(target_arch = "wasm32")))]
#[ctor::ctor]
fn init_logging() {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer().with_test_writer())
.init();
}
#[cfg(test)]
matrix_sdk_test::init_tracing_for_tests!();
/// The Matrix user session info.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+56 -1
View File
@@ -12,7 +12,7 @@ use ruma::{
},
sticker::StickerEventContent,
},
UInt,
MxcUri, UInt,
};
const UNIQUE_SEPARATOR: &str = "_";
@@ -83,11 +83,22 @@ pub struct MediaRequest {
pub format: MediaFormat,
}
impl MediaRequest {
/// Get the [`MxcUri`] from `Self`.
pub fn uri(&self) -> &MxcUri {
match &self.source {
MediaSource::Plain(url) => url.as_ref(),
MediaSource::Encrypted(file) => file.url.as_ref(),
}
}
}
impl UniqueKey for MediaRequest {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
}
}
/// Trait for media event content.
pub trait MediaEventContent {
/// Get the source of the file for `Self`.
@@ -166,3 +177,47 @@ impl MediaEventContent for LocationMessageEventContent {
self.info.as_ref()?.thumbnail_source.clone()
}
}
#[cfg(test)]
mod tests {
use ruma::mxc_uri;
use serde_json::json;
use super::*;
#[test]
fn test_media_request_url() {
let mxc_uri = mxc_uri!("mxc://homeserver/media");
let plain = MediaRequest {
source: MediaSource::Plain(mxc_uri.to_owned()),
format: MediaFormat::File,
};
assert_eq!(plain.uri(), mxc_uri);
let file = MediaRequest {
source: MediaSource::Encrypted(Box::new(
serde_json::from_value(json!({
"url": mxc_uri,
"key": {
"kty": "oct",
"key_ops": ["encrypt", "decrypt"],
"alg": "A256CTR",
"k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8",
"ext": true,
},
"iv": "AK1wyzigZtQAAAABAAAAKK",
"hashes": {
"sha256": "foobar",
},
"v": "v2",
}))
.unwrap(),
)),
format: MediaFormat::File,
};
assert_eq!(file.uri(), mxc_uri);
}
}
+658
View File
@@ -0,0 +1,658 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! # Client-side read receipts computation
//!
//! While Matrix servers have the ability to provide basic information about the
//! unread status of rooms, via [`matrix_sdk::ruma::UnreadNotificationCounts`],
//! it's not reliable for encrypted rooms. Indeed, the server doesn't have
//! access to the content of encrypted events, so it can only makes guesses when
//! estimating unread and highlight counts.
//!
//! Instead, this module provides facilities to compute the number of unread
//! messages, unread notifications and unread highlights in a room.
//!
//! Counting unread messages is performed by looking at the latest receipt of
//! the current user, and inferring which events are following it, according to
//! the sync ordering.
//!
//! For notifications and highlights to be precisely accounted for, we also need
//! to pay attention to the user's notification settings. Fortunately, this is
//! also something we need to for notifications, so we can reuse this code.
//!
//! Of course, not all events are created equal, and some are less interesting
//! than others, and shouldn't cause a room to be marked unread. This module's
//! `marks_as_unread` function shows the opiniated set of rules that will filter
//! out uninterested events.
//!
//! The only public method in that module is [`compute_notifications`], which
//! updates the `RoomInfo` in place according to the new counts.
#![allow(dead_code)] // too many different build configurations, I give up
use eyeball_im::Vector;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use ruma::{
events::{
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
room::message::Relation,
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
SyncMessageLikeEvent,
},
serde::Raw,
EventId, OwnedEventId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};
use crate::error::Result;
/// Information about read receipts collected during processing of that room.
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub(crate) struct RoomReadReceipts {
/// Does the room have unread messages?
pub num_unread: u64,
/// Does the room have unread events that should notify?
pub num_notifications: u64,
/// Does the room have messages causing highlights for the users? (aka
/// mentions)
pub num_mentions: u64,
/// The id of the event the last unthreaded (or main-threaded, for better
/// compatibility with clients that have thread support) read receipt is
/// attached to.
latest_read_receipt_event_id: Option<OwnedEventId>,
}
impl RoomReadReceipts {
/// Update the [`RoomReadReceipts`] unread counts according to the new
/// event.
///
/// Returns whether a new event triggered a new unread/notification/mention.
#[inline(always)]
fn update_for_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) -> bool {
let mut has_unread = false;
if marks_as_unread(&event.event, user_id) {
self.num_unread += 1;
has_unread = true
}
let mut has_notify = false;
let mut has_mention = false;
for action in &event.push_actions {
if !has_notify && action.should_notify() {
self.num_notifications += 1;
has_notify = true;
}
if !has_mention && action.is_highlight() {
self.num_mentions += 1;
has_mention = true;
}
}
has_unread || has_notify || has_mention
}
#[inline(always)]
fn reset(&mut self) {
self.num_unread = 0;
self.num_notifications = 0;
self.num_mentions = 0;
}
/// Try to find the event to which the receipt attaches to, and if found,
/// will update the notification count in the room.
fn find_and_count_events<'a>(
&mut self,
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
) -> bool {
let mut counting_receipts = false;
for event in events {
if counting_receipts {
self.update_for_event(event, user_id);
} else if let Ok(Some(event_id)) = event.event.get_field::<OwnedEventId>("event_id") {
if event_id == receipt_event_id {
// Bingo! Switch over to the counting state, after resetting the
// previous counts.
trace!("Found the event the receipt was referring to! Starting to count.");
self.reset();
counting_receipts = true;
}
}
}
counting_receipts
}
}
/// Provider for timeline events prior to the current sync.
pub trait PreviousEventsProvider: Send + Sync {
/// Returns the list of known timeline events, in sync order, for the given
/// room.
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
}
impl PreviousEventsProvider for () {
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
Vector::new()
}
}
/// Given a set of events coming from sync, for a room, update the
/// [`RoomReadReceipts`]'s counts of unread messages, notifications and
/// highlights' in place.
///
/// A provider of previous events may be required to reconcile a read receipt
/// that has been just received for an event that came in a previous sync.
///
/// See this module's documentation for more information.
///
/// Returns a boolean indicating if a field changed value in the read receipts.
#[instrument(skip_all, fields(room_id = %room_id, ?read_receipts))]
pub(crate) fn compute_notifications<PEP: PreviousEventsProvider>(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
previous_events_provider: &PEP,
new_events: &[SyncTimelineEvent],
read_receipts: &mut RoomReadReceipts,
) -> Result<bool> {
let prev_latest_receipt_event_id = read_receipts.latest_read_receipt_event_id.clone();
if let Some(receipt_event) = receipt_event {
trace!("Got a new receipt event!");
// Find a private or public read receipt for the current user.
let mut receipt_event_id = None;
if let Some((event_id, receipt)) = receipt_event
.user_receipt(user_id, ReceiptType::Read)
.or_else(|| receipt_event.user_receipt(user_id, ReceiptType::ReadPrivate))
{
if receipt.thread == ReceiptThread::Unthreaded || receipt.thread == ReceiptThread::Main
{
receipt_event_id = Some(event_id.to_owned());
}
}
if let Some(receipt_event_id) = receipt_event_id {
// We've found the id of an event to which the receipt attaches. The associated
// event may either come from the new batch of events associated to
// this sync, or it may live in the past timeline events we know
// about.
// First, save the event id as the latest one that has a read receipt.
read_receipts.latest_read_receipt_event_id = Some(receipt_event_id.clone());
// Try to find if the read receipt refers to an event from the current sync, to
// avoid searching the cached timeline events.
trace!("We got a new event with a read receipt: {receipt_event_id}. Search in new events...");
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
// It did, so our work here is done.
// Always return true here; we saved at least the latest read receipt.
return Ok(true);
}
// We didn't find the event attached to the receipt in the new batches of
// events. It's possible it's referring to an event we've already
// seen. In that case, try to find it.
let previous_events = previous_events_provider.for_room(room_id);
trace!("Couldn't find the event attached to the receipt in the new events; looking in past events too now...");
if read_receipts.find_and_count_events(
&receipt_event_id,
user_id,
previous_events.iter().chain(new_events.iter()),
) {
// It did refer to an old event, so our work here is done.
// Always return true here; we saved at least the latest read receipt.
return Ok(true);
}
}
}
if let Some(receipt_event_id) = prev_latest_receipt_event_id {
// There's no new read-receipt here. We assume the cached events have been
// properly processed, and we only need to process the new events based
// on the previous receipt.
trace!("No new receipts, or couldn't find attached event; looking if the past latest known receipt refers to a new event...");
if read_receipts.find_and_count_events(&receipt_event_id, user_id, new_events) {
// We found the event to which the previous receipt attached to (so we at least
// reset the counts once), our work is done here.
return Ok(true);
}
}
// If we haven't returned at this point, it means that either we had no previous
// read receipt, or the previous read receipt was not attached to any new
// event.
//
// In that case, accumulate all events as part of the current batch, and wait
// for the next receipt.
trace!("Default path: including all new events for the receipts count.");
let mut new_receipt = false;
for event in new_events {
if read_receipts.update_for_event(event, user_id) {
new_receipt = true;
}
}
Ok(new_receipt)
}
/// Is the event worth marking a room as unread?
fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
let event = match event.deserialize() {
Ok(event) => event,
Err(err) => {
tracing::debug!(
"couldn't deserialize event {:?}: {err}",
event.get_field::<String>("event_id").ok().flatten()
);
return false;
}
};
if event.sender() == user_id {
// Not interested in one's own events.
return false;
}
match event {
ruma::events::AnySyncTimelineEvent::MessageLike(event) => {
// Filter out redactions.
let Some(content) = event.original_content() else {
tracing::trace!("not interesting because redacted");
return false;
};
// Filter out edits.
if matches!(
content.relation(),
Some(ruma::events::room::encrypted::Relation::Replacement(..))
) {
tracing::trace!("not interesting because edited");
return false;
}
match event {
AnySyncMessageLikeEvent::CallAnswer(_)
| AnySyncMessageLikeEvent::CallInvite(_)
| AnySyncMessageLikeEvent::CallHangup(_)
| AnySyncMessageLikeEvent::CallCandidates(_)
| AnySyncMessageLikeEvent::CallNegotiate(_)
| AnySyncMessageLikeEvent::CallReject(_)
| AnySyncMessageLikeEvent::CallSelectAnswer(_)
| AnySyncMessageLikeEvent::PollResponse(_)
| AnySyncMessageLikeEvent::UnstablePollResponse(_)
| AnySyncMessageLikeEvent::Reaction(_)
| AnySyncMessageLikeEvent::RoomRedaction(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_) => false,
// For some reason, Ruma doesn't handle these two in `content.relation()` above.
AnySyncMessageLikeEvent::PollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content:
PollStartEventContent { relates_to: Some(Relation::Replacement(_)), .. },
..
},
))
| AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content: UnstablePollStartEventContent::Replacement(_),
..
},
)) => false,
AnySyncMessageLikeEvent::Message(_)
| AnySyncMessageLikeEvent::PollStart(_)
| AnySyncMessageLikeEvent::UnstablePollStart(_)
| AnySyncMessageLikeEvent::PollEnd(_)
| AnySyncMessageLikeEvent::UnstablePollEnd(_)
| AnySyncMessageLikeEvent::RoomEncrypted(_)
| AnySyncMessageLikeEvent::RoomMessage(_)
| AnySyncMessageLikeEvent::Sticker(_) => true,
_ => {
// What I don't know about, I don't care about.
tracing::debug!("unhandled timeline event type: {}", event.event_type());
false
}
}
}
ruma::events::AnySyncTimelineEvent::State(_) => false,
}
}
#[cfg(test)]
mod tests {
use std::ops::Not as _;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_test::sync_timeline_event;
use ruma::{event_id, push::Action, user_id, EventId, UserId};
use crate::read_receipts::{marks_as_unread, RoomReadReceipts};
#[test]
fn test_room_message_marks_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
// A message from somebody else marks the room as unread...
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
assert!(marks_as_unread(&ev, user_id));
// ... but a message from ourselves doesn't.
let ev = sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_room_edit_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
// An edit to a message from somebody else doesn't mark the room as unread.
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": {
"body": " * edited message",
"m.new_content": {
"body": "edited message",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$someeventid:localhost",
"rel_type": "m.replace"
},
"msgtype": "m.text"
},
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_redaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
// A redact of a message from somebody else doesn't mark the room as unread.
let ev = sync_timeline_event!({
"content": {
"reason": "🛑"
},
"event_id": "$151957878228ssqrJ:localhost",
"origin_server_ts": 151957878000000_u64,
"sender": other_user_id,
"type": "m.room.redaction",
"redacts": "$151957878228ssqrj:localhost",
"unsigned": {
"age": 85
}
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_reaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
// A reaction from somebody else to a message doesn't mark the room as unread.
let ev = sync_timeline_event!({
"content": {
"m.relates_to": {
"event_id": "$15275047031IXQRi:localhost",
"key": "👍",
"rel_type": "m.annotation"
}
},
"event_id": "$15275047031IXQRi:localhost",
"origin_server_ts": 159027581000000_u64,
"sender": other_user_id,
"type": "m.reaction",
"unsigned": {
"age": 85
}
});
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_state_event_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let event_id = event_id!("$1");
let ev = sync_timeline_event!({
"content": {
"displayname": "Alice",
"membership": "join",
},
"event_id": event_id,
"origin_server_ts": 1432135524678u64,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
});
assert!(marks_as_unread(&ev, user_id).not());
let other_user_id = user_id!("@bob:example.org");
assert!(marks_as_unread(&ev, other_user_id).not());
}
#[test]
fn test_count_unread_and_mentions() {
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions,
}
}
let user_id = user_id!("@alice:example.org");
// An interesting event from oneself doesn't count as a new unread message.
let event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
// An interesting event from someone else does count as a new unread message.
let event = make_event(user_id!("@bob:example.org"), Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
// Push actions computed beforehand are respected.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 1);
// Technically this `push_actions` set would be a bug somewhere else, but let's
// make sure to resist against it.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.update_for_event(&event, user_id);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
}
#[test]
fn test_find_and_count_events() {
let ev0 = event_id!("$0");
let user_id = user_id!("@alice:example.org");
// When provided with no events, we report not finding the event to which the
// receipt relates.
let mut receipts = RoomReadReceipts::default();
assert!(receipts.find_and_count_events(ev0, user_id, &[]).not());
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
// When provided with one event, that's not the receipt event, we don't count
// it.
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
"sender": "@bob:example.org",
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions: Vec::new(),
}
}
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts
.find_and_count_events(ev0, user_id, &[make_event(event_id!("$1"))],)
.not());
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
// When provided with one event that's the receipt target, we find it, reset the
// count, and since there's nothing else, we stop there and end up with
// zero counts.
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts.find_and_count_events(ev0, user_id, &[make_event(ev0)]));
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
// When provided with multiple events and not the receipt event, we do not count
// anything..
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts
.find_and_count_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
)
.not());
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
// When provided with multiple events including one that's the receipt event, we
// find it and count from it.
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
latest_read_receipt_event_id: None,
};
assert!(receipts.find_and_count_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(ev0),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
}
}
+1 -1
View File
@@ -100,7 +100,7 @@ pub struct BaseRoomInfo {
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
/// The topic of this room.
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
/// All Minimal state events that containing one or more running matrixRTC
/// All minimal state events that containing one or more running matrixRTC
/// memberships.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
+142 -33
View File
@@ -63,6 +63,7 @@ use super::{
use crate::latest_event::LatestEvent;
use crate::{
deserialized_responses::MemberEvent,
read_receipts::RoomReadReceipts,
store::{DynStateStore, Result as StoreResult, StateStoreExt},
sync::UnreadNotificationsCount,
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
@@ -187,6 +188,31 @@ impl Room {
self.inner.read().notification_counts
}
/// Get the number of unread messages (computed client-side).
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_messages(&self) -> u64 {
self.inner.read().read_receipts.num_unread
}
/// Get the number of unread notifications (computed client-side).
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_notifications(&self) -> u64 {
self.inner.read().read_receipts.num_notifications
}
/// Get the number of unread mentions (computed client-side), that is,
/// messages causing a highlight in a room.
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_mentions(&self) -> u64 {
self.inner.read().read_receipts.num_mentions
}
/// Check if the room has its members fully synced.
///
/// Members might be missing if lazy member loading was enabled for the
@@ -403,13 +429,7 @@ impl Room {
/// sliding sync.
#[cfg(feature = "experimental-sliding-sync")]
pub fn latest_event(&self) -> Option<LatestEvent> {
self.inner.read().latest_event.clone()
}
/// Update the last event in the room
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
pub(crate) fn set_latest_event(&self, latest_event: Option<LatestEvent>) {
self.inner.update(|info| info.latest_event = latest_event);
self.inner.read().latest_event.as_deref().cloned()
}
/// Return the most recent few encrypted events. When the keys come through
@@ -428,10 +448,23 @@ impl Room {
///
/// Panics if index is not a valid index in the latest_encrypted_events
/// list.
///
/// It is the responsibility of the caller to apply the changes into the
/// state store after calling this function.
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
pub(crate) fn on_latest_event_decrypted(&self, latest_event: LatestEvent, index: usize) {
self.set_latest_event(Some(latest_event));
pub(crate) fn on_latest_event_decrypted(
&self,
latest_event: Box<LatestEvent>,
index: usize,
changes: &mut crate::StateChanges,
) {
self.latest_encrypted_events.write().unwrap().drain(0..=index);
let room_info = changes
.room_infos
.entry(self.room_id().to_owned())
.or_insert_with(|| self.clone_info());
room_info.latest_event = Some(latest_event);
}
/// Get the list of users ids that are considered to be joined members of
@@ -577,8 +610,8 @@ impl Room {
};
debug!(
room_id = self.room_id().as_str(),
own_user = self.own_user_id.as_str(),
room_id = ?self.room_id(),
own_user = ?self.own_user_id,
joined, invited,
heroes = ?members,
"Calculating name for a room",
@@ -597,9 +630,10 @@ impl Room {
self.inner.get()
}
/// Update the summary with given RoomInfo
pub fn update_summary(&self, summary: RoomInfo) {
self.inner.set(summary);
/// Update the inner summary with the given RoomInfo, and notify
/// subscribers.
pub fn set_room_info(&self, room_info: RoomInfo) {
self.inner.set(room_info);
}
/// Get the `RoomMember` with the given `user_id`.
@@ -686,7 +720,7 @@ impl Room {
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
/// `receipt_type`, `thread` and `user_id` in this room.
pub async fn user_receipt(
pub async fn load_user_receipt(
&self,
receipt_type: ReceiptType,
thread: ReceiptThread,
@@ -695,9 +729,10 @@ impl Room {
self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
}
/// Get the receipts as a list of `OwnedUserId` and `Receipt` tuples for the
/// given `receipt_type`, `thread` and `event_id` in this room.
pub async fn event_receipts(
/// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
/// tuples for the given `receipt_type`, `thread` and `event_id` in this
/// room.
pub async fn load_event_receipts(
&self,
receipt_type: ReceiptType,
thread: ReceiptThread,
@@ -716,26 +751,42 @@ impl Room {
pub struct RoomInfo {
/// The unique room id of the room.
pub(crate) room_id: OwnedRoomId,
/// The state of the room.
pub(crate) room_state: RoomState,
/// The unread notifications counts.
/// The unread notifications counts, as returned by the server.
///
/// These might be incorrect for encrypted rooms, since the server doesn't
/// have access to the content of the encrypted events.
pub(crate) notification_counts: UnreadNotificationsCount,
/// The summary of this room.
pub(crate) summary: RoomSummary,
/// Flag remembering if the room members are synced.
pub(crate) members_synced: bool,
/// The prev batch of this room we received during the last sync.
pub(crate) last_prev_batch: Option<String>,
/// How much we know about this room.
pub(crate) sync_info: SyncInfo,
/// Whether or not the encryption info was been synced.
pub(crate) encryption_state_synced: bool,
/// The last event send by sliding sync
#[cfg(feature = "experimental-sliding-sync")]
pub(crate) latest_event: Option<LatestEvent>,
pub(crate) latest_event: Option<Box<LatestEvent>>,
/// Information about read receipts for this room.
#[serde(default)]
pub(crate) read_receipts: RoomReadReceipts,
/// Base room info which holds some basic event contents important for the
/// room state.
pub(crate) base_info: BaseRoomInfo,
pub(crate) base_info: Box<BaseRoomInfo>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -769,7 +820,8 @@ impl RoomInfo {
encryption_state_synced: false,
#[cfg(feature = "experimental-sliding-sync")]
latest_event: None,
base_info: BaseRoomInfo::new(),
read_receipts: Default::default(),
base_info: Box::new(BaseRoomInfo::new()),
}
}
@@ -1246,10 +1298,11 @@ mod tests {
last_prev_batch: Some("pb".to_owned()),
sync_info: SyncInfo::FullySynced,
encryption_state_synced: true,
latest_event: Some(LatestEvent::new(
latest_event: Some(Box::new(LatestEvent::new(
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(),
)),
base_info: BaseRoomInfo::new(),
))),
base_info: Box::new(BaseRoomInfo::new()),
read_receipts: Default::default(),
};
let info_json = json!({
@@ -1289,6 +1342,12 @@ mod tests {
"name": null,
"tombstone": null,
"topic": null,
},
"read_receipts": {
"num_unread": 0,
"num_mentions": 0,
"num_notifications": 0,
"latest_read_receipt_event_id": null,
}
});
@@ -1626,6 +1685,50 @@ mod tests {
);
}
#[async_test]
#[cfg(feature = "experimental-sliding-sync")]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_update() {
// Given a room,
let client = crate::BaseClient::new();
client
.set_session_meta(crate::SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
})
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// That has an encrypted event,
add_encrypted_event(&room, "$A");
// Sanity: it has no latest_event
assert!(room.latest_event().is_none());
// When I set up an observer on the latest_event,
let mut room_info_subscriber = room.subscribe_info();
// And I provide a decrypted event to replace the encrypted one,
let event = make_latest_event("$A");
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
// The subscriber isn't notified at this point.
stream_assert::assert_pending!(room_info_subscriber);
// Then updating the room info will store the event,
client.apply_changes(&changes);
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
// And wake up the subscriber.
use futures_util::FutureExt as _;
assert!(room_info_subscriber.next().now_or_never().is_some());
stream_assert::assert_pending!(room_info_subscriber);
}
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
@@ -1637,7 +1740,9 @@ mod tests {
// When I provide a decrypted event to replace the encrypted one
let event = make_latest_event("$A");
room.on_latest_event_decrypted(event.clone(), 0);
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
// Then is it stored
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
@@ -1657,13 +1762,15 @@ mod tests {
// When I provide a latest event
let new_event = make_latest_event("$1");
let new_event_index = 1;
room.on_latest_event_decrypted(new_event.clone(), new_event_index);
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(new_event.clone(), new_event_index, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
// Then the encrypted events list is shortened to only newer events
let enc_evs = room.latest_encrypted_events();
assert_eq!(enc_evs.len(), 2);
assert_eq!(enc_evs.get(0).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$2");
assert_eq!(enc_evs.get(1).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$3");
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
// And the event is stored
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
@@ -1682,7 +1789,9 @@ mod tests {
// When I provide a latest event and say it was the very latest
let new_event = make_latest_event("$3");
let new_event_index = 3;
room.on_latest_event_decrypted(new_event, new_event_index);
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(new_event, new_event_index, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
// Then the encrypted events list ie empty
let enc_evs = room.latest_encrypted_events();
@@ -1698,10 +1807,10 @@ mod tests {
}
#[cfg(feature = "experimental-sliding-sync")]
fn make_latest_event(event_id: &str) -> LatestEvent {
LatestEvent::new(SyncTimelineEvent::new(
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
Box::new(LatestEvent::new(SyncTimelineEvent::new(
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
))
)))
}
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
+180 -69
View File
@@ -12,21 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use std::ops::Deref;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::AnyToDeviceEvent;
use ruma::{
api::client::sync::sync_events::{
v3::{self, InvitedRoom, RoomSummary},
v4::{self, AccountData},
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{
v3::{self, InvitedRoom, RoomSummary},
v4,
},
},
events::{AnySyncStateEvent, AnySyncTimelineEvent},
serde::Raw,
RoomId,
OwnedRoomId, RoomId,
};
use tracing::{instrument, trace, warn};
@@ -38,6 +41,7 @@ use crate::RoomMemberships;
use crate::{
deserialized_responses::AmbiguityChanges,
error::Result,
read_receipts::{compute_notifications, PreviousEventsProvider},
rooms::RoomState,
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse},
@@ -96,7 +100,7 @@ impl BaseClient {
trace!("ready to submit changes to store");
self.store.save_changes(&changes).await?;
self.apply_changes(&changes).await;
self.apply_changes(&changes);
trace!("applied changes");
Ok(to_device)
@@ -109,7 +113,11 @@ impl BaseClient {
/// * `response` - The response that we received after a successful sliding
/// sync.
#[instrument(skip_all, level = "trace")]
pub async fn process_sliding_sync(&self, response: &v4::Response) -> Result<SyncResponse> {
pub async fn process_sliding_sync<PEP: PreviousEventsProvider>(
&self,
response: &v4::Response,
previous_events_provider: &PEP,
) -> Result<SyncResponse> {
let v4::Response {
// FIXME not yet supported by sliding sync. see
// https://github.com/matrix-org/matrix-rust-sdk/issues/1014
@@ -135,32 +143,33 @@ impl BaseClient {
return Ok(SyncResponse::default());
};
let v4::Extensions { account_data, receipts, .. } = extensions;
let mut changes = StateChanges::default();
let store = self.store.clone();
let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone());
let account_data = &extensions.account_data;
if !account_data.is_empty() {
self.handle_account_data(&account_data.global, &mut changes).await;
}
let mut new_rooms = Rooms::default();
let mut notifications = Default::default();
for (room_id, room_data) in rooms {
let (room_to_store, joined_room, left_room, invited_room) = self
for (room_id, response_room_data) in rooms {
let (room_info, joined_room, left_room, invited_room) = self
.process_sliding_sync_room(
room_id,
room_data,
response_room_data,
account_data,
&store,
&mut changes,
&mut notifications,
&mut ambiguity_cache,
account_data,
)
.await?;
changes.add_room(room_to_store);
changes.add_room(room_info);
if let Some(joined_room) = joined_room {
new_rooms.join.insert(room_id.clone(), joined_room);
@@ -175,8 +184,11 @@ impl BaseClient {
}
}
// Process receipts now we have rooms
for (room_id, raw) in &receipts.rooms {
// Handle read receipts and typing notifications independently of the rooms:
// these both live in a different subsection of the server's response,
// so they may exist without any update for the associated room.
for (room_id, raw) in &extensions.receipts.rooms {
match raw.deserialize() {
Ok(event) => {
changes.add_receipts(room_id, event.content);
@@ -186,10 +198,52 @@ impl BaseClient {
#[rustfmt::skip]
warn!(
?room_id, event_id,
"Failed to deserialize ephemeral room event: {e}"
"Failed to deserialize read receipt room event: {e}"
);
}
}
// We assume this can only happen in joined rooms, or something's very wrong.
new_rooms
.join
.entry(room_id.to_owned())
.or_insert_with(JoinedRoom::default)
.ephemeral
.push(raw.clone().cast());
}
for (room_id, raw) in &extensions.typing.rooms {
// We assume this can only happen in joined rooms, or something's very wrong.
new_rooms
.join
.entry(room_id.to_owned())
.or_insert_with(JoinedRoom::default)
.ephemeral
.push(raw.clone().cast());
}
// Rooms in `new_rooms.join` either have a timeline update, or a new read
// receipt. Update the read receipt accordingly.
let user_id = &self.session_meta().expect("logged in user").user_id;
for (room_id, joined_room_update) in &mut new_rooms.join {
if let Some(mut room_info) = changes
.room_infos
.get(room_id)
.cloned()
.or_else(|| self.get_room(room_id).map(|r| r.clone_info()))
{
if compute_notifications(
user_id,
room_id,
changes.receipts.get(room_id),
previous_events_provider,
&joined_room_update.timeline.events,
&mut room_info.read_receipts,
)? {
changes.add_room(room_info);
}
}
}
// TODO remove this, we're processing account data events here again
@@ -214,13 +268,13 @@ impl BaseClient {
trace!("ready to submit changes to store");
store.save_changes(&changes).await?;
self.apply_changes(&changes).await;
self.apply_changes(&changes);
trace!("applied changes");
Ok(SyncResponse {
rooms: new_rooms,
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
notifications: changes.notifications,
notifications,
// FIXME not yet supported by sliding sync.
presence: Default::default(),
account_data: account_data.global.clone(),
@@ -228,14 +282,16 @@ impl BaseClient {
})
}
#[allow(clippy::too_many_arguments)]
async fn process_sliding_sync_room(
&self,
room_id: &RoomId,
room_data: &v4::SlidingSyncRoom,
account_data: &v4::AccountData,
store: &Store,
changes: &mut StateChanges,
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
ambiguity_cache: &mut AmbiguityCache,
account_data: &AccountData,
) -> Result<(RoomInfo, Option<JoinedRoom>, Option<LeftRoom>, Option<InvitedRoom>)> {
let mut state_events = Self::deserialize_state_events(&room_data.required_state);
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
@@ -288,6 +344,7 @@ impl BaseClient {
&mut user_ids,
&mut room_info,
changes,
notifications,
ambiguity_cache,
)
.await?;
@@ -319,18 +376,25 @@ impl BaseClient {
room_info.update_notification_count(notification_count);
match room_info.state() {
RoomState::Joined => Ok((
room_info,
Some(JoinedRoom::new(
timeline,
raw_state_events,
room_account_data.unwrap_or_default(),
Vec::new(),
notification_count,
)),
None,
None,
)),
RoomState::Joined => {
// Ephemeral events are added separately, because we might not
// have a room subsection in the response, yet we may have receipts for
// that room.
let ephemeral = Vec::new();
Ok((
room_info,
Some(JoinedRoom::new(
timeline,
raw_state_events,
room_account_data.unwrap_or_default(),
ephemeral,
notification_count,
)),
None,
None,
))
}
RoomState::Left => Ok((
room_info,
@@ -412,16 +476,20 @@ impl BaseClient {
state_events: &[AnySyncStateEvent],
room_info: &mut RoomInfo,
) {
for event in state_events {
let Some(meta) = self.session_meta() else {
return;
};
// Start from the last event; the first membership event we see in that order is
// the last in the regular order, so that's the only one we need to
// consider.
for event in state_events.iter().rev() {
if let AnySyncStateEvent::RoomMember(member) = &event {
// If this event updates the current user's membership, record that in the
// room_info.
if let Some(meta) = self.session_meta() {
if member.sender() == meta.user_id
&& member.state_key() == meta.user_id.as_str()
{
room_info.set_state(member.membership().into());
}
if member.sender() == meta.user_id && member.state_key() == meta.user_id.as_str() {
room_info.set_state(member.membership().into());
break;
}
}
}
@@ -456,8 +524,12 @@ impl BaseClient {
}
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
///
/// If any encrypted events are found after that one, store them in the RoomInfo
/// too so we can use them when we get the relevant keys.
///
/// It is the responsibility of the caller to update the `RoomInfo` instance
/// stored in the `Room`.
#[cfg(feature = "e2e-encryption")]
async fn cache_latest_events(
room: &Room,
@@ -524,16 +596,14 @@ async fn cache_latest_events(
}
}
let latest_event = LatestEvent::new_with_sender_details(
let latest_event = Box::new(LatestEvent::new_with_sender_details(
event.clone(),
sender_profile,
sender_name_is_ambiguous,
);
));
// Store it in the return RoomInfo, and in the Room, to make sure they are
// consistent
// Store it in the return RoomInfo (it will be saved for us in the room later).
room_info.latest_event = Some(latest_event.clone());
room.set_latest_event(Some(latest_event));
// We don't need any of the older encrypted events because we have a new
// decrypted one.
room.latest_encrypted_events.write().unwrap().clear();
@@ -601,8 +671,8 @@ mod tests {
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::async_test;
use ruma::{
api::client::sync::sync_events::v4,
device_id, event_id,
api::client::sync::sync_events::{v4, UnreadNotificationsCount},
assign, device_id, event_id,
events::{
direct::DirectEventContent,
room::{
@@ -623,11 +693,41 @@ mod tests {
use super::cache_latest_events;
use crate::{store::MemoryStore, BaseClient, Room, RoomState, SessionMeta};
#[async_test]
async fn test_notification_count_set() {
let client = logged_in_client().await;
let mut response = v4::Response::new("42".to_owned());
let room_id = room_id!("!room:example.org");
let count = assign!(UnreadNotificationsCount::default(), {
highlight_count: Some(uint!(13)),
notification_count: Some(uint!(37)),
});
response.rooms.insert(
room_id.to_owned(),
assign!(v4::SlidingSyncRoom::new(), {
unread_notifications: count.clone()
}),
);
let sync_response =
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Check it's present in the response.
let room = sync_response.rooms.join.get(room_id).unwrap();
assert_eq!(room.unread_notifications, count.clone().into());
// Check it's been updated in the store.
let room = client.get_room(room_id).expect("found room");
assert_eq!(room.unread_notification_counts(), count.into());
}
#[async_test]
async fn can_process_empty_sliding_sync_response() {
let client = logged_in_client().await;
let empty_response = v4::Response::new("5".to_owned());
client.process_sliding_sync(&empty_response).await.expect("Failed to process sync");
client.process_sliding_sync(&empty_response, &()).await.expect("Failed to process sync");
}
#[async_test]
@@ -642,7 +742,7 @@ mod tests {
room.joined_count = Some(uint!(41));
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room appears in the client (with the same joined count)
let client_room = client.get_room(room_id).expect("No room found");
@@ -667,7 +767,7 @@ mod tests {
room.name = Some("little room".to_owned());
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room appears in the client with the expected name
let client_room = client.get_room(room_id).expect("No room found");
@@ -693,7 +793,7 @@ mod tests {
room.name = Some("little room".to_owned());
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room appears in the client with the expected name
let client_room = client.get_room(room_id).expect("No room found");
@@ -717,7 +817,7 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
// And then leave with a `required_state` state event…
@@ -725,7 +825,7 @@ mod tests {
set_room_left(&mut room, user_id);
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
@@ -747,14 +847,14 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
// And then leave with a `timeline` state event…
let mut room = v4::SlidingSyncRoom::new();
set_room_left_as_timeline_event(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
@@ -773,7 +873,7 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// (sanity: state is join)
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
@@ -781,7 +881,7 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
set_room_left(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// (sanity: state is left)
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
@@ -789,7 +889,7 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
set_room_invited(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room is in the invite state
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
@@ -902,7 +1002,7 @@ mod tests {
// When I send sliding sync response containing a room with an avatar
let room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has the avatar
let client_room = client.get_room(room_id).expect("No room found");
@@ -924,7 +1024,7 @@ mod tests {
set_room_invited(&mut room, user_id);
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room is added to the client
let client_room = client.get_room(room_id).expect("No room found");
@@ -947,7 +1047,7 @@ mod tests {
let mut room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
set_room_invited(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has the avatar
let client_room = client.get_room(room_id).expect("No room found");
@@ -969,7 +1069,7 @@ mod tests {
let mut room = room_with_canonical_alias(room_alias_id, user_id);
set_room_invited(&mut room, user_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has the avatar
let client_room = client.get_room(room_id).expect("No room found");
@@ -989,7 +1089,7 @@ mod tests {
let mut room = room_with_canonical_alias(room_alias_id, user_id);
room.name = Some("This came from the server".to_owned());
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room's name is just exactly what the server supplied
let client_room = client.get_room(room_id).expect("No room found");
@@ -1023,7 +1123,7 @@ mod tests {
let events = &[event_a, event_b.clone()];
let room = room_with_timeline(events);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room holds the latest event
let client_room = client.get_room(room_id).expect("No room found");
@@ -1049,7 +1149,7 @@ mod tests {
// When the sliding sync response contains a timeline
let room = room_with_timeline(&[event_a]);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room holds the latest event
let client_room = client.get_room(room_id).expect("No room found");
@@ -1070,7 +1170,7 @@ mod tests {
// When a redaction for that event is received
let room = room_with_timeline(&[redaction]);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room still holds the latest event
let client_room = client.get_room(room_id).expect("No room found");
@@ -1145,9 +1245,11 @@ mod tests {
// The latest message is stored
assert_eq!(
ev_id(room_info.latest_event.map(|latest_event| latest_event.event().clone())),
ev_id(room_info.latest_event.as_ref().map(|latest_event| latest_event.event().clone())),
rawev_id(event2.clone())
);
room.set_room_info(room_info);
assert_eq!(
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
rawev_id(event2)
@@ -1169,6 +1271,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
// The latest message is stored
assert_eq!(
@@ -1195,6 +1298,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
// The latest message is stored, ignoring the receipt
assert_eq!(
@@ -1247,6 +1351,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
// The latest message is stored, ignoring encrypted and receipts
assert_eq!(
@@ -1287,6 +1392,8 @@ mod tests {
None,
)
.await;
room.set_room_info(room_info);
// Sanity: room_info has 10 encrypted events inside it
assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10);
@@ -1294,6 +1401,7 @@ mod tests {
let eventa = make_encrypted_event("$a");
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, &[eventa], None, None).await;
room.set_room_info(room_info);
// The oldest event is gone
assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned()));
@@ -1315,11 +1423,13 @@ mod tests {
None,
)
.await;
room.set_room_info(room_info.clone());
// When I ask to cache an unecnrypted event, and some more encrypted events
// When I ask to cache an unencrypted event, and some more encrypted events
let eventa = make_event("m.room.message", "$a");
let eventb = make_encrypted_event("$b");
cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await;
room.set_room_info(room_info);
// The only encrypted events stored are the ones after the decrypted one
assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]);
@@ -1332,6 +1442,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
room.latest_event().map(|latest_event| latest_event.event().clone())
}
@@ -1446,7 +1557,7 @@ mod tests {
let mut response = response_with_room(room_id, room).await;
set_direct_with(&mut response, their_id.to_owned(), vec![room_id.to_owned()]);
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
}
/// Set this user's membership within this room to new_state
@@ -1459,7 +1570,7 @@ mod tests {
let mut room = v4::SlidingSyncRoom::new();
room.required_state.push(make_membership_event(user_id, new_state));
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response).await.expect("Failed to process sync");
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
}
fn set_direct_with(
@@ -200,11 +200,8 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let content: Vec<u8> = "somebinarydata".into();
let request_file =
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
let request_thumbnail = MediaRequest {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSize {
@@ -214,6 +211,17 @@ impl StateStoreIntegrationTests for DynStateStore {
}),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequest {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
@@ -223,35 +231,63 @@ impl StateStoreIntegrationTests for DynStateStore {
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_some(),
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone())
.await
.expect("adding media again failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_some(),
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
self.add_media_content(&request_thumbnail, content.clone())
// Let's add the thumbnail media.
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
.await
.expect("adding thumbnail failed");
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_some(),
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// Let's add another media with a different URI.
self.add_media_content(&request_other_file, other_content.clone())
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
@@ -260,6 +296,10 @@ impl StateStoreIntegrationTests for DynStateStore {
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
}
async fn test_topic_redaction(&self) -> Result<()> {
@@ -18,7 +18,7 @@ use std::{
};
use async_trait::async_trait;
use matrix_sdk_common::instant::Instant;
use matrix_sdk_common::{instant::Instant, ring_buffer::RingBuffer};
use ruma::{
canonical_json::{redact, RedactedBecause},
events::{
@@ -29,15 +29,16 @@ use ruma::{
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
},
serde::Raw,
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId,
RoomVersionId, UserId,
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId,
RoomId, RoomVersionId, UserId,
};
use tracing::{debug, warn};
use super::{Result, RoomInfo, StateChanges, StateStore, StoreError};
use crate::{
deserialized_responses::RawAnySyncOrStrippedState, media::MediaRequest, MinimalRoomMemberEvent,
RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
deserialized_responses::RawAnySyncOrStrippedState,
media::{MediaRequest, UniqueKey as _},
MinimalRoomMemberEvent, RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
};
/// In-Memory, non-persistent implementation of the `StateStore`
@@ -77,13 +78,14 @@ pub struct MemoryStore {
HashMap<(String, Option<String>), HashMap<OwnedEventId, HashMap<OwnedUserId, Receipt>>>,
>,
>,
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
}
impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Default::default()
Self { media: StdRwLock::new(RingBuffer::new(20)), ..Default::default() }
}
fn get_user_room_receipt_event_impl(
@@ -700,17 +702,55 @@ impl StateStore for MemoryStore {
Ok(self.custom.write().unwrap().remove(key))
}
// The in-memory store doesn't cache media
async fn add_media_content(&self, _request: &MediaRequest, _data: Vec<u8>) -> Result<()> {
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
// Now, let's add it.
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
async fn get_media_content(&self, _request: &MediaRequest) -> Result<Option<Vec<u8>>> {
Ok(None)
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
let media = self.media.read().unwrap();
let expected_key = request.unique_key();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
async fn remove_media_content(&self, _request: &MediaRequest) -> Result<()> {
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = request.unique_key();
let Some(index) = media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
Ok(())
}
async fn remove_media_content_for_uri(&self, _uri: &MxcUri) -> Result<()> {
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
(media_uri == &expected_key).then_some(position)
})
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
}
Ok(())
}
@@ -738,5 +778,5 @@ mod tests {
Ok(MemoryStore::new())
}
statestore_integration_tests!();
statestore_integration_tests!(with_media_tests);
}
@@ -117,7 +117,8 @@ impl RoomInfoV1 {
sync_info,
encryption_state_synced,
#[cfg(feature = "experimental-sliding-sync")]
latest_event: latest_event.map(LatestEvent::new),
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
read_receipts: Default::default(),
base_info: base_info.migrate(create),
}
}
@@ -157,7 +158,10 @@ struct BaseRoomInfoV1 {
impl BaseRoomInfoV1 {
/// Migrate this to a [`BaseRoomInfo`].
fn migrate(self, create: Option<&SyncOrStrippedState<RoomCreateEventContent>>) -> BaseRoomInfo {
fn migrate(
self,
create: Option<&SyncOrStrippedState<RoomCreateEventContent>>,
) -> Box<BaseRoomInfo> {
let BaseRoomInfoV1 {
avatar,
canonical_alias,
@@ -186,7 +190,7 @@ impl BaseRoomInfoV1 {
MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev),
});
BaseRoomInfo {
Box::new(BaseRoomInfo {
avatar,
canonical_alias,
create,
@@ -200,7 +204,7 @@ impl BaseRoomInfoV1 {
tombstone,
topic,
rtc_member: BTreeMap::new(),
}
})
}
}
-9
View File
@@ -41,7 +41,6 @@ mod traits;
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use ruma::{
api::client::push::get_notifications::v3::Notification,
events::{
presence::PresenceEvent,
receipt::ReceiptEventContent,
@@ -291,8 +290,6 @@ pub struct StateChanges {
/// A map from room id to a map of a display name and a set of user ids that
/// share that display name in the given room.
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
/// A map of `RoomId` to a vector of `Notification`s
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
impl StateChanges {
@@ -379,12 +376,6 @@ impl StateChanges {
.insert(redacted_event_id.to_owned(), redaction);
}
/// Update the `StateChanges` struct with the given room with a new
/// `Notification`.
pub fn add_notification(&mut self, room_id: &RoomId, notification: Notification) {
self.notifications.entry(room_id.to_owned()).or_default().push(notification);
}
/// Update the `StateChanges` struct with the given room with a new
/// `Receipts`.
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
@@ -371,6 +371,7 @@ pub trait StateStore: AsyncTraitDeps {
#[repr(transparent)]
struct EraseStateStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseStateStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
+4 -2
View File
@@ -96,7 +96,7 @@ impl fmt::Debug for Rooms {
}
/// Updates to joined rooms.
#[derive(Clone)]
#[derive(Clone, Default)]
pub struct JoinedRoom {
/// Counts of unread notifications for this room.
pub unread_notifications: UnreadNotificationsCount,
@@ -114,6 +114,7 @@ pub struct JoinedRoom {
pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for JoinedRoom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JoinedRoom")
@@ -139,7 +140,7 @@ impl JoinedRoom {
}
/// Counts of unread notifications for a room.
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct UnreadNotificationsCount {
/// The number of unread notifications for this room with the highlight flag
/// set.
@@ -182,6 +183,7 @@ impl LeftRoom {
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for LeftRoom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JoinedRoom")
+6 -6
View File
@@ -9,14 +9,14 @@ name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.6.0"
version = "0.7.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[features]
js = ["instant/wasm-bindgen", "instant/inaccurate", "wasm-bindgen-futures"]
js = ["instant/wasm-bindgen", "wasm-bindgen-futures"]
[dependencies]
async-trait = { workspace = true }
@@ -27,21 +27,21 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true, features = ["rt", "time", "sync"] }
tokio = { workspace = true, features = ["rt", "time"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures-util = { workspace = true, features = ["channel"] }
wasm-bindgen-futures = { version = "0.4.33", optional = true }
gloo-timers = { version = "0.3.0", features = ["futures"] }
web-sys = {version = "0.3.60", features = ["console"] }
web-sys = { version = "0.3.60", features = ["console"] }
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["fmt", "ansi"] }
wasm-bindgen = "0.2.84"
[dev-dependencies]
assert_matches = { workspace = true }
matrix-sdk-test = { path = "../../testing/matrix-sdk-test/", version= "0.6.0"}
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
matrix-sdk-test = { workspace = true }
wasm-bindgen-test = "0.3.33"
tracing-subscriber = "0.3.15"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
js-sys = "0.3.64"
@@ -0,0 +1,255 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A TTL cache which can be used to time out repeated operations that might
//! experience intermittent failures.
use std::{
borrow::Borrow,
collections::HashMap,
hash::Hash,
sync::{Arc, RwLock},
time::Duration,
};
use super::instant::Instant;
const MAX_DELAY: u64 = 15 * 60;
const MULTIPLIER: u64 = 15;
/// A TTL cache where items get inactive instead of discarded.
///
/// The items need to be explicitly removed from the cache. This allows us to
/// implement exponential backoff based TTL.
#[derive(Clone, Debug)]
pub struct FailuresCache<T: Eq + Hash> {
inner: Arc<InnerCache<T>>,
}
#[derive(Debug)]
struct InnerCache<T: Eq + Hash> {
max_delay: Duration,
backoff_multiplier: u64,
items: RwLock<HashMap<T, FailuresItem>>,
}
impl<T: Eq + Hash> Default for InnerCache<T> {
fn default() -> Self {
Self {
max_delay: Duration::from_secs(MAX_DELAY),
backoff_multiplier: MULTIPLIER,
items: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct FailuresItem {
insertion_time: Instant,
duration: Duration,
/// Number of times that this item has failed after it was first added to
/// the cache. (In other words, one less than the total number of
/// failures.)
failure_count: u8,
}
impl FailuresItem {
/// Has the item expired.
fn expired(&self) -> bool {
self.insertion_time.elapsed() >= self.duration
}
/// Force the expiry of this item.
///
/// This doesn't reset the failure count, but does mark the item as ready
/// for immediate retry.
fn expire(&mut self) {
self.duration = Duration::from_secs(0);
}
}
impl<T> FailuresCache<T>
where
T: Eq + Hash,
{
pub fn new() -> Self {
Self { inner: Default::default() }
}
pub fn with_settings(max_delay: Duration, multiplier: u8) -> Self {
Self {
inner: InnerCache {
max_delay,
backoff_multiplier: multiplier.into(),
items: Default::default(),
}
.into(),
}
}
/// Is the given key non-expired and part of the cache.
pub fn contains<Q>(&self, key: &Q) -> bool
where
T: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let lock = self.inner.items.read().unwrap();
let contains = if let Some(item) = lock.get(key) { !item.expired() } else { false };
contains
}
/// Get the failure count for a given key.
///
/// # Returns
///
/// * `None` if this key is not in the failure cache. (It has never failed,
/// or it has been [`FailuresCache::remove()`]d since the last failure.)
///
/// * `Some(u8)`: the number of times it has failed since it was first
/// added to the failure cache. (In other words, one less than the total
/// number of failures.)
pub fn failure_count<Q>(&self, key: &Q) -> Option<u8>
where
T: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let lock = self.inner.items.read().unwrap();
lock.get(key).map(|i| i.failure_count)
}
/// This will calculate a duration that determines how long an item is
/// considered to be valid while being in the cache.
///
/// The returned duration will follow this sequence if the default
/// multiplier and `max_delay` values are used, values are in minutes:
/// [0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0]
fn calculate_delay(&self, failure_count: u8) -> Duration {
let exponential_backoff = 2u64.saturating_pow(failure_count.into());
let delay = exponential_backoff.saturating_mul(self.inner.backoff_multiplier);
Duration::from_secs(delay).clamp(Duration::from_secs(1), self.inner.max_delay)
}
/// Add a single item to the cache.
pub fn insert(&self, item: T) {
self.extend([item]);
}
/// Extend the cache with the given iterator of items.
///
/// Items that are already part of the cache, whether they are expired or
/// not, will have their TTL extended using an exponential backoff
/// algorithm.
pub fn extend(&self, iterator: impl IntoIterator<Item = T>) {
let mut lock = self.inner.items.write().unwrap();
let now = Instant::now();
for key in iterator {
let failure_count = if let Some(value) = lock.get(&key) {
value.failure_count.saturating_add(1)
} else {
0
};
let delay = self.calculate_delay(failure_count);
let item = FailuresItem { insertion_time: now, duration: delay, failure_count };
lock.insert(key, item);
}
}
/// Remove the items contained in the iterator from the cache.
pub fn remove<'a, I, Q>(&'a self, iterator: I)
where
I: Iterator<Item = &'a Q>,
T: Borrow<Q>,
Q: Hash + Eq + 'a + ?Sized,
{
let mut lock = self.inner.items.write().unwrap();
for item in iterator {
lock.remove(item);
}
}
/// Force the expiry of the given item, if it is present in the cache.
///
/// This doesn't reset the failure count, but does mark the item as ready
/// for immediate retry.
#[doc(hidden)]
pub fn expire(&self, item: &T) {
let mut lock = self.inner.items.write().unwrap();
lock.get_mut(item).map(FailuresItem::expire);
}
}
impl<T: Eq + Hash> Default for FailuresCache<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use proptest::prelude::*;
use super::FailuresCache;
#[test]
fn failures_cache() {
let cache = FailuresCache::new();
assert!(!cache.contains(&1));
cache.extend([1u8].iter());
assert!(cache.contains(&1));
cache.inner.items.write().unwrap().get_mut(&1).unwrap().duration = Duration::from_secs(0);
assert!(!cache.contains(&1));
cache.remove([1u8].iter());
assert!(cache.inner.items.read().unwrap().get(&1).is_none())
}
#[test]
fn failures_cache_timeout() {
let cache: FailuresCache<u8> = FailuresCache::new();
assert_eq!(cache.calculate_delay(0).as_secs(), 15);
assert_eq!(cache.calculate_delay(1).as_secs(), 30);
assert_eq!(cache.calculate_delay(2).as_secs(), 60);
assert_eq!(cache.calculate_delay(3).as_secs(), 120);
assert_eq!(cache.calculate_delay(4).as_secs(), 240);
assert_eq!(cache.calculate_delay(5).as_secs(), 480);
assert_eq!(cache.calculate_delay(6).as_secs(), 900);
assert_eq!(cache.calculate_delay(7).as_secs(), 900);
}
proptest! {
#[test]
fn failures_cache_proptest_timeout(count in 0..10u8) {
let cache: FailuresCache<u8> = FailuresCache::new();
let delay = cache.calculate_delay(count).as_secs();
assert!(delay <= 900);
assert!(delay >= 15);
}
}
}
+133 -10
View File
@@ -23,10 +23,13 @@ use std::{
sync::atomic::{AtomicU32, Ordering},
};
use tracing::{level_filters::LevelFilter, Level, Metadata};
use tracing_subscriber::fmt::{
format::{DefaultFields, Format, Pretty},
MakeWriter, Subscriber,
use tracing::{field::Field, level_filters::LevelFilter, Event, Level, Metadata};
use tracing_subscriber::{
fmt::{
format::{DefaultFields, Writer},
FmtContext, FormatEvent, FormatFields, FormattedFields, MakeWriter, Subscriber,
},
registry::LookupSpan,
};
use wasm_bindgen::prelude::*;
@@ -199,9 +202,130 @@ fn write_message_to_console(level: Level, message: &JsValue) {
};
}
/// An implementation of [`FormatEvent`] which formats events in a sensible way
/// for sending events to the JS console.
#[derive(Debug, Default)]
pub struct JsEventFormatter {}
impl JsEventFormatter {
pub fn new() -> Self {
Self::default()
}
}
impl<C, N> FormatEvent<C, N> for JsEventFormatter
where
C: tracing::Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, C, N>,
mut writer: Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let meta = event.metadata();
write!(writer, "{} {}: ", meta.level(), meta.target())?;
// write the message
let mut v = FindMessageVisitor::default();
event.record(&mut v);
if let Some(m) = v.message {
writer.write_str(m.as_str())?
}
// write the other fields
let mut v = JsFieldVisitor::new(writer.by_ref());
event.record(&mut v);
if let Some(file) = meta.file() {
write!(writer, "\n at {file}")?;
if let Some(line) = meta.line() {
write!(writer, ":{line}")?;
}
}
let span = event.parent().and_then(|id| ctx.span(id)).or_else(|| ctx.lookup_current());
let scope = span.into_iter().flat_map(|span| span.scope());
for span in scope {
let meta = span.metadata();
write!(writer, "\n in {}::{}", meta.target(), meta.name())?;
let ext = span.extensions();
let fields = &ext
.get::<FormattedFields<N>>()
.expect("Unable to find FormattedFields in extensions; this is a bug");
if !fields.is_empty() {
write!(writer, " with {fields}")?;
}
}
Ok(())
}
}
/// A field visitor which is used by [`JsEventFormatter`] to find the "message"
/// for the event.
#[derive(Debug, Default)]
struct FindMessageVisitor {
message: Option<String>,
}
impl tracing::field::Visit for FindMessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if field.name() == "message" {
self.message = Some(format!("{value:?}"));
}
}
}
/// A field visitor which is used by [`JsEventFormatter`] to print the fields
/// other than `message`.
struct JsFieldVisitor<'a> {
writer: Writer<'a>,
result: fmt::Result,
is_empty: bool,
}
impl<'a> JsFieldVisitor<'a> {
fn new(writer: Writer<'a>) -> Self {
Self { writer, result: Ok(()), is_empty: true }
}
fn pad_and_record(&mut self, name: &str, value: &dyn Debug) -> fmt::Result {
// If this is the first field since the message, make a new line. Otherwise,
// just print a space.
if self.is_empty {
self.is_empty = false;
write!(self.writer, "\n ")?;
} else {
write!(self.writer, " ")?;
}
write!(self.writer, "{name}={value:?}")
}
}
impl<'a> tracing::field::Visit for JsFieldVisitor<'a> {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if self.result.is_err() {
return;
}
let name = field.name();
if name == "message" {
// Already handled by FindMessageVisitor.
return;
}
self.result = self.pad_and_record(name, value);
}
}
/// The type of [`Subscriber`] returned by [`make_tracing_subscriber`]
pub type JsLoggingSubscriber =
Subscriber<DefaultFields, Format<Pretty, ()>, LevelFilter, MakeJsLogWriter>;
Subscriber<DefaultFields, JsEventFormatter, LevelFilter, MakeJsLogWriter>;
/// Construct a [`tracing::Subscriber`] which will format logs and send them to
/// the Javascript console or the given logging object.
@@ -217,13 +341,11 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
None => MakeJsLogWriter::new(),
};
let format = tracing_subscriber::fmt::format().without_time().pretty();
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.with_writer(make_writer)
.with_ansi(false)
.event_format(format)
.event_format(JsEventFormatter::new())
.finish()
}
@@ -250,7 +372,7 @@ pub(crate) mod tests {
// log something to it
with_default(subscriber, || {
debug!("Test message");
debug!(value = 1, "Test message");
});
// inspect the call log
@@ -268,7 +390,8 @@ pub(crate) mod tests {
assert_eq!(call_args.length(), 1, "Expected 1 argument, got {}", call_args.length());
let message_string = call_args.get(0).as_string().unwrap();
let expected_prefix = " DEBUG matrix_sdk_common::js_tracing::tests: Test message";
let expected_prefix =
"DEBUG matrix_sdk_common::js_tracing::tests: Test message\n value=1\n";
assert!(
message_string.starts_with(expected_prefix),
"Expected log message to start with '{}', but was '{}'",
+5 -1
View File
@@ -23,12 +23,16 @@ pub use ruma;
pub mod debug;
pub mod deserialized_responses;
pub mod executor;
pub mod failures_cache;
pub mod ring_buffer;
pub mod store_locks;
pub mod timeout;
pub mod tracing_timer;
#[cfg(target_arch = "wasm32")]
// We cannot currently measure test coverage in the WASM environment, so
// js_tracing is incorrectly flagged as untested. Disable coverage checking for
// it.
#[cfg(all(target_arch = "wasm32", not(tarpaulin_include)))]
pub mod js_tracing;
pub use store_locks::LEASE_DURATION_MS;
+36 -4
View File
@@ -75,12 +75,19 @@ impl<T> RingBuffer<T> {
self.inner.pop_front()
}
/// Removes and returns one specific element at `index` if it exists,
/// otherwise it returns `None`.
pub fn remove(&mut self, index: usize) -> Option<T> {
self.inner.remove(index)
}
/// Returns an iterator that provides elements in front-to-back order, i.e.
/// the same order you would get if you repeatedly called pop().
pub fn iter(&self) -> Iter<'_, T> {
self.inner.iter()
}
/// Returns an iterator that drains its items.
pub fn drain<R>(&mut self, range: R) -> Drain<'_, T>
where
R: RangeBounds<usize>,
@@ -155,7 +162,7 @@ mod tests {
}
#[test]
pub fn test_push_and_pop_and_length() {
pub fn test_push_and_pop_and_remove_and_length() {
let mut ring_buffer = RingBuffer::new(3);
ring_buffer.push(1);
@@ -167,23 +174,48 @@ mod tests {
ring_buffer.push(3);
assert_eq!(ring_buffer.len(), 3);
ring_buffer.pop();
assert_eq!(ring_buffer.pop(), Some(1));
assert_eq!(ring_buffer.len(), 2);
assert_eq!(ring_buffer.get(0), Some(&2));
assert_eq!(ring_buffer.get(1), Some(&3));
assert_eq!(ring_buffer.get(2), None);
ring_buffer.pop();
assert_eq!(ring_buffer.pop(), Some(2));
assert_eq!(ring_buffer.len(), 1);
assert_eq!(ring_buffer.get(0), Some(&3));
assert_eq!(ring_buffer.get(1), None);
assert_eq!(ring_buffer.get(2), None);
ring_buffer.pop();
assert_eq!(ring_buffer.pop(), Some(3));
assert_eq!(ring_buffer.len(), 0);
assert_eq!(ring_buffer.get(0), None);
assert_eq!(ring_buffer.get(1), None);
assert_eq!(ring_buffer.get(2), None);
assert_eq!(ring_buffer.pop(), None);
ring_buffer.push(1);
ring_buffer.push(2);
ring_buffer.push(3);
assert_eq!(ring_buffer.len(), 3);
assert_eq!(ring_buffer.get(0), Some(&1));
assert_eq!(ring_buffer.get(1), Some(&2));
assert_eq!(ring_buffer.get(2), Some(&3));
assert_eq!(ring_buffer.remove(1), Some(2));
assert_eq!(ring_buffer.len(), 2);
assert_eq!(ring_buffer.get(0), Some(&1));
assert_eq!(ring_buffer.get(1), Some(&3));
assert_eq!(ring_buffer.get(2), None);
assert_eq!(ring_buffer.remove(0), Some(1));
assert_eq!(ring_buffer.len(), 1);
assert_eq!(ring_buffer.get(0), Some(&3));
assert_eq!(ring_buffer.get(1), None);
assert_eq!(ring_buffer.get(2), None);
assert_eq!(ring_buffer.remove(1), None);
assert_eq!(ring_buffer.remove(10), None);
}
#[test]
@@ -109,8 +109,6 @@ mod tests {
async fn test_timer_name() {
use tracing::{span, Level};
tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();
tracing::warn!("Starting test...");
mod time123 {
+75 -14
View File
@@ -1,4 +1,78 @@
# unreleased
# 0.7.0
- Add method to mark a list of inbound group sessions as backed up:
`CryptoStore::mark_inbound_group_sessions_as_backed_up`
- `OlmMachine::toggle_room_key_forwarding` is replaced by two separate methods:
* `OlmMachine::set_room_key_requests_enabled`, which controls whether
outgoing room key requests are enabled, and:
* `OlmMachine::set_room_key_forwarding_enabled`, which controls whether we
automatically reply to incoming room key requests.
`OlmMachine::is_room_key_forwarding_enabled` is updated to return the setting
of `OlmMachine::set_room_key_forwarding_enabled`, while
`OlmMachine::are_room_key_requests_enabled` is added to return the setting of
`OlmMachine::set_room_key_requests_enabled`.
([#2902](https://github.com/matrix-org/matrix-rust-sdk/pull/2902))
- Improve performance of `share_room_key`.
([#2862](https://github.com/matrix-org/matrix-rust-sdk/pull/2862))
- `get_missing_sessions`: Don't block waiting for `/keys/query` requests on
blacklisted servers, and improve performance.
([#2845](https://github.com/matrix-org/matrix-rust-sdk/pull/2845))
- Generalize `olm::Session::encrypt` to accept any value implementing
`Serialize` for the `value` parameter, instead of specifically
`serde_json::Value`. Note that references to `Serialize`-implementing types
themselves implement `Serialize`.
- Change the argument to `OlmMachine::receive_sync_changes` to be an
`EncryptionSyncChanges` struct packing all the arguments instead of many
single arguments. The new `next_batch_token` field there should be the
`next_batch` value read from the latest sync response.
- Handle missing devices in `/keys/claim` responses.
([#2805](https://github.com/matrix-org/matrix-rust-sdk/pull/2805))
- Add the higher level decryption method `decrypt_session_data` to the
`BackupDecryptionKey` type.
- Add a higher level method to create signatures for the backup info. The
`OlmMachine::backup_machine()::sign_backup()` method can be used to add
signatures to a `RoomKeyBackupInfo`.
- Remove the `backups_v1` feature, backups support is now enabled by default.
- Use the `Signatures` type as the return value for the
`MegolmV1BackupKey::signatures()` method.
- Add two new methods to import room keys,
`OlmMachine::store()::import_exported_room_keys()` for file exports and
`OlmMachine::backup_machine()::import_backed_up_room_keys()` for backups. The
`OlmMachine::import_room_keys()` method is now deprecated.
- The parameter order of `OlmMachine::encrypt_room_event_raw` and
`OutboundGroupSession::encrypt` has changed, `content` is now last
- The parameter type of `content` has also changed, from `serde_json::Value`
to `&Raw<AnyMessageLikeEventContent>`
- Change the return value of `bootstrap_cross_signing` so it returns an extra
keys upload request. The three requests must be sent in the order they
appear in the return tuple.
- Stop logging large quantities of data about the `Store` during olm
decryption.
- Remove spurious "Unknown outgoing secret request" warning which was logged
for every outgoing secret request.
- Clean up the logging of to-device messages in `share_room_key`.
- Expose new `OlmMachine::get_room_event_encryption_info` method.
- Add support for secret storage.
@@ -63,16 +137,3 @@
- Change the returned success value type of `BackupMachine::backup` from
`OutgoingRequest` to `(OwnedTransactionId, KeysBackupRequest)`.
- Expose new `OlmMachine::get_room_event_encryption_info` method.
- Clean up the logging of to-device messages in `share_room_key`.
- Remove spurious "Unknown outgoing secret request" warning which was logged
for every outgoing secret request.
- Stop logging large quantities of data about the `Store` during olm
decryption.
- Change the return value of `bootstrap_cross_signing` so it returns an extra keys upload request.
The three requests must be sent in the order they appear in the return tuple.
+8 -12
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.6.0"
version = "0.7.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -19,7 +19,6 @@ default = []
automatic-room-key-forwarding = []
js = ["ruma/js", "vodozemac/js"]
qrcode = ["dep:matrix-sdk-qrcode"]
backups_v1 = ["dep:cbc"]
message-ids = ["dep:ulid"]
experimental-algorithms = []
@@ -29,11 +28,10 @@ testing = ["dep:http"]
[dependencies]
aes = "0.8.1"
as_variant = { workspace = true }
async-std = { version = "1.12.0", features = ["unstable"] }
async-trait = { workspace = true }
bs58 = { version = "0.5.0" }
byteorder = { workspace = true }
cbc = { version = "0.1.2", features = ["std"], optional = true }
cbc = { version = "0.1.2", features = ["std"] }
cfg-if = "1.0"
ctr = "0.9.1"
eyeball = { workspace = true }
@@ -43,18 +41,18 @@ hkdf = "0.12.3"
hmac = "0.12.1"
http = { workspace = true, optional = true } # feature = testing only
itertools = { workspace = true }
matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
matrix-sdk-qrcode = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
pbkdf2 = { version = "0.12.2", default-features = false }
rand = "0.8.5"
rand = { workspace = true }
rmp-serde = "1.1.1"
ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc3814"] }
serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
subtle = "2.5.0"
tokio-stream = { version = "0.1.12", features = ["sync"] }
tokio = { workspace = true, default-features = false, features = ["sync"] }
tokio-stream = { workspace = true, features = ["sync"] }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
ulid = { version = "1.0.0", optional = true }
@@ -71,14 +69,12 @@ tokio = { workspace = true, features = ["time"] }
anyhow = { workspace = true }
assert_matches = { workspace = true }
assert_matches2 = { workspace = true }
ctor = { workspace = true }
futures-executor = { workspace = true }
http = { workspace = true }
indoc = "2.0.1"
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
matrix-sdk-test = { workspace = true }
olm-rs = { version = "2.2.0", features = ["serde"] }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
# required for async_test macro
stream_assert = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
@@ -12,26 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use std::sync::{Arc, Mutex};
use ruma::{
api::client::backup::{EncryptedSessionDataInit, KeyBackupData, KeyBackupDataInit},
serde::Base64,
OwnedDeviceKeyId, OwnedUserId,
};
use vodozemac::Curve25519PublicKey;
use zeroize::Zeroizing;
use super::{compat::PkEncryption, decryption::DecodeError};
use crate::olm::InboundGroupSession;
use crate::{olm::InboundGroupSession, types::Signatures};
#[derive(Debug)]
struct InnerBackupKey {
key: Curve25519PublicKey,
signatures: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>>,
signatures: Signatures,
version: Mutex<Option<String>>,
}
@@ -41,6 +37,7 @@ pub struct MegolmV1BackupKey {
inner: Arc<InnerBackupKey>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for MegolmV1BackupKey {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
@@ -69,7 +66,7 @@ impl MegolmV1BackupKey {
}
/// Get all the signatures of this `MegolmV1BackupKey`.
pub fn signatures(&self) -> BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>> {
pub fn signatures(&self) -> Signatures {
self.inner.signatures.to_owned()
}
@@ -207,6 +207,13 @@ pub enum Error {
/// The message failed to be decoded.
#[error("The message could not been decoded: {0}")]
Decoding(#[from] MessageDecodeError),
/// The message's Curve25519 key failed to be decoded.
#[error("The message's ephemeral Curve25519 key could not been decoded: {0}")]
InvalidCurveKey(#[from] KeyError),
/// The decrypted message should contain a backed up room key, but the
/// plaintext isn't valid JSON.
#[error("The decrypted message isn't valid JSON: {0}")]
Json(#[from] serde_json::error::Error),
}
#[cfg(test)]
@@ -18,14 +18,20 @@ use std::{
};
use bs58;
use ruma::api::client::backup::EncryptedSessionData;
use thiserror::Error;
use zeroize::Zeroizing;
use vodozemac::Curve25519PublicKey;
use zeroize::{Zeroize, Zeroizing};
use super::{
compat::{Error as DecryptionError, Message, PkDecryption},
MegolmV1BackupKey,
};
use crate::store::BackupDecryptionKey;
use crate::{
olm::BackedUpRoomKey,
store::BackupDecryptionKey,
types::{MegolmV1AuthData, RoomKeyBackupInfo},
};
/// Error type for the decoding of a [`BackupDecryptionKey`].
#[derive(Debug, Error)]
@@ -53,16 +59,6 @@ pub enum DecodeError {
PublicKey(#[from] vodozemac::KeyError),
}
#[derive(Debug, Error)]
pub enum UnpicklingError {
#[error(transparent)]
Json(#[from] serde_json::Error),
// #[error("Couldn't decrypt the pickle: {0}")]
// Decryption(String),
#[error(transparent)]
Decode(#[from] DecodeError),
}
impl TryFrom<String> for BackupDecryptionKey {
type Error = DecodeError;
@@ -187,6 +183,17 @@ impl BackupDecryptionKey {
MegolmV1BackupKey::new(pk.public_key(), None)
}
/// Get the [`RoomKeyBackupInfo`] for this [`BackupDecryptionKey`].
///
/// The [`RoomKeyBackupInfo`] can be uploaded to the homeserver to activate
/// a new backup version.
pub fn to_backup_info(&self) -> RoomKeyBackupInfo {
let pk = self.get_pk_decryption();
let auth_data = MegolmV1AuthData::new(pk.public_key(), Default::default());
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(auth_data)
}
/// Try to decrypt the given ciphertext using this [`BackupDecryptionKey`].
///
/// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm
@@ -207,15 +214,52 @@ impl BackupDecryptionKey {
Ok(String::from_utf8_lossy(&decrypted).to_string())
}
/// Try to decrypt the given [`EncryptedSessionData`] using this
/// [`BackupDecryptionKey`].
pub fn decrypt_session_data(
&self,
session_data: EncryptedSessionData,
) -> Result<BackedUpRoomKey, DecryptionError> {
let message = Message {
ciphertext: session_data.ciphertext.into_inner(),
mac: session_data.mac.into_inner(),
ephemeral_key: Curve25519PublicKey::from_slice(session_data.ephemeral.as_bytes())?,
};
let pk = self.get_pk_decryption();
let mut decrypted = pk.decrypt(&message)?;
let result = serde_json::from_slice(&decrypted);
decrypted.zeroize();
Ok(result?)
}
/// Check if the given public key from the [`RoomKeyBackupInfo`] matches to
/// this [`BackupDecryptionKey`].
pub fn backup_key_matches(&self, info: &RoomKeyBackupInfo) -> bool {
match info {
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(info) => {
let pk = self.get_pk_decryption();
let public_key = pk.public_key();
info.public_key == public_key
}
RoomKeyBackupInfo::Other { .. } => false,
}
}
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::async_test;
use ruma::api::client::backup::KeyBackupData;
use serde_json::json;
use super::{BackupDecryptionKey, DecodeError};
use crate::olm::BackedUpRoomKey;
use crate::olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession};
const TEST_KEY: [u8; 32] = [
0x77, 0x07, 0x6D, 0x0A, 0x73, 0x18, 0xA5, 0x7D, 0x3C, 0x16, 0xC1, 0x72, 0x51, 0xB2, 0x66,
@@ -223,6 +267,24 @@ mod tests {
0x2C, 0x2A,
];
fn room_key() -> ExportedRoomKey {
let json = json!({
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
"session_id": "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU",
"room_id": "!DovneieKSTkdHKpIXy:morpheus.localhost",
"session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
fHAblQV",
"sender_claimed_keys":{},
"forwarding_curve25519_key_chain":[]
});
serde_json::from_value(json)
.expect("We should be able to deserialize our backed up room key")
}
#[test]
fn base64_decoding() -> Result<(), DecodeError> {
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
@@ -306,5 +368,35 @@ mod tests {
let _: BackedUpRoomKey = serde_json::from_str(&decrypted)
.expect("The decrypted payload should contain valid JSON");
let _ = decryption_key
.decrypt_session_data(key_backup_data.session_data)
.expect("The backed up key should be decrypted successfully");
}
#[async_test]
async fn test_encryption_cycle() {
let session = InboundGroupSession::from_export(&room_key()).unwrap();
let decryption_key = BackupDecryptionKey::new().unwrap();
let encryption_key = decryption_key.megolm_v1_public_key();
let encrypted = encryption_key.encrypt(session).await;
let _ = decryption_key
.decrypt_session_data(encrypted.session_data)
.expect("We should be able to decrypt a just encrypted room key");
}
#[test]
fn key_matches() {
let decryption_key = BackupDecryptionKey::new().unwrap();
let key_info = decryption_key.to_backup_info();
assert!(
decryption_key.backup_key_matches(&key_info),
"The backup info should match the decryption key"
);
}
}
@@ -52,5 +52,5 @@ mod compat;
mod decryption;
pub use backup::MegolmV1BackupKey;
pub use compat::{Error as DecryptionError, MessageDecodeError};
pub use compat::Error as DecryptionError;
pub use decryption::DecodeError;
+174 -41
View File
@@ -30,16 +30,17 @@ use std::{
use ruma::{
api::client::backup::RoomKeyBackup, serde::Raw, DeviceId, DeviceKeyAlgorithm, OwnedDeviceId,
OwnedRoomId, OwnedTransactionId, TransactionId,
OwnedRoomId, OwnedTransactionId, RoomId, TransactionId,
};
use tokio::sync::RwLock;
use tracing::{debug, info, instrument, trace, warn};
use crate::{
olm::{InboundGroupSession, SignedJsonObject},
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest,
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
SignatureError,
};
mod keys;
@@ -59,22 +60,14 @@ pub struct BackupMachine {
pending_backup: Arc<RwLock<Option<PendingBackup>>>,
}
type SenderKey = String;
type SessionId = String;
#[derive(Debug, Clone)]
struct PendingBackup {
request_id: OwnedTransactionId,
request: KeysBackupRequest,
sessions: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>>,
}
impl PendingBackup {
fn session_was_part_of_the_backup(&self, session: &InboundGroupSession) -> bool {
self.sessions
.get(session.room_id())
.and_then(|r| {
r.get(&session.sender_key().to_base64()).map(|s| s.contains(session.session_id()))
})
.unwrap_or(false)
}
sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
}
impl From<PendingBackup> for OutgoingRequest {
@@ -325,7 +318,7 @@ impl BackupMachine {
///
/// # Arguments
///
/// * `backup_version`: The backup version that should be verified. Should
/// * `backup_info`: The backup info that should be verified. Should
/// be fetched from the server using the [`/room_keys/version`] endpoint.
///
/// * `compute_all_signatures`: *Useful for debugging only*. If this
@@ -349,6 +342,46 @@ impl BackupMachine {
}
}
/// Sign a [`RoomKeyBackupInfo`] using the device's identity key and, if
/// available, the cross-signing master key.
///
/// # Arguments
///
/// * `backup_info`: The backup version that should be verified. Should
/// be created from the [`BackupDecryptionKey`] using the
/// [`BackupDecryptionKey::to_backup_info()`] method.
pub async fn sign_backup(
&self,
backup_info: &mut RoomKeyBackupInfo,
) -> Result<(), SignatureError> {
if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
let canonical_json = data.to_canonical_json()?;
let private_identity = self.store.private_identity();
let identity = private_identity.lock().await;
if let Some(key_id) = identity.master_key_id().await {
if let Ok(signature) = identity.sign(&canonical_json).await {
data.signatures.add_signature(
self.store.user_id().to_owned(),
key_id,
signature,
);
}
}
let cache = self.store.cache().await?;
let account = cache.account().await?;
let key_id = account.signing_key_id();
let signature = account.sign(&canonical_json);
data.signatures.add_signature(self.store.user_id().to_owned(), key_id, signature);
Ok(())
} else {
Err(SignatureError::UnsupportedAlgorithm)
}
}
/// Activate the given backup key to be used to encrypt and backup room
/// keys.
///
@@ -436,44 +469,38 @@ impl BackupMachine {
request_id: &TransactionId,
) -> Result<(), CryptoStoreError> {
let mut request = self.pending_backup.write().await;
if let Some(r) = &*request {
if r.request_id == request_id {
let sessions: Vec<_> = self
.store
.get_inbound_group_sessions()
.await?
.into_iter()
.filter(|s| r.session_was_part_of_the_backup(s))
let room_and_session_ids: Vec<(&RoomId, &str)> = r
.sessions
.iter()
.flat_map(|(room_id, sender_key_to_session_ids)| {
std::iter::repeat(room_id).zip(sender_key_to_session_ids.values().flatten())
})
.map(|(room_id, session_id)| (room_id.as_ref(), session_id.as_str()))
.collect();
for session in &sessions {
session.mark_as_backed_up();
}
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
self.store.save_changes(changes).await?;
let counts = self.store.inbound_group_session_counts().await?;
self.store.mark_inbound_group_sessions_as_backed_up(&room_and_session_ids).await?;
trace!(
room_key_counts = ?counts,
request_id = ?r.request_id, keys = ?r.sessions, "Marked room keys as backed up"
request_id = ?r.request_id,
keys = ?r.sessions,
"Marked room keys as backed up"
);
*request = None;
} else {
warn!(
expected = r.request_id.to_string().as_str(),
got = request_id.to_string().as_str(),
expected = ?r.request_id,
got = ?request_id,
"Tried to mark a pending backup as sent but the request id didn't match"
);
}
} else {
warn!(
request_id = request_id.to_string().as_str(),
?request_id,
"Tried to mark a pending backup as sent but there isn't a backup pending"
);
};
@@ -525,10 +552,10 @@ impl BackupMachine {
backup_key: &MegolmV1BackupKey,
) -> (
BTreeMap<OwnedRoomId, RoomKeyBackup>,
BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>>,
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
) {
let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<String>>> =
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
BTreeMap::new();
for session in sessions {
@@ -555,15 +582,69 @@ impl BackupMachine {
(backup, session_record)
}
/// Import the given room keys into our store.
///
/// # Arguments
///
/// * `room_keys` - A list of previously exported keys that should be
/// imported into our store. If we already have a better version of a key
/// the key will *not* be imported.
///
/// Returns a [`RoomKeyImportResult`] containing information about room keys
/// which were imported.
pub async fn import_backed_up_room_keys(
&self,
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
progress_listener: impl Fn(usize, usize),
) -> Result<RoomKeyImportResult, CryptoStoreError> {
let mut decrypted_room_keys = vec![];
for (room_id, room_keys) in room_keys {
for (session_id, room_key) in room_keys {
let room_key = ExportedRoomKey::from_backed_up_room_key(
room_id.to_owned(),
session_id,
room_key,
);
decrypted_room_keys.push(room_key);
}
}
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches2::assert_let;
use matrix_sdk_test::async_test;
use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
use serde_json::json;
use crate::{store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError, OlmMachine};
use crate::{
olm::BackedUpRoomKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError,
OlmMachine,
};
fn room_key() -> BackedUpRoomKey {
let json = json!({
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
"session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
fHAblQV",
"sender_claimed_keys":{},
"forwarding_curve25519_key_chain":[]
});
serde_json::from_value(json)
.expect("We should be able to deserialize our backed up room key")
}
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
@@ -588,8 +669,8 @@ mod tests {
assert_eq!(counts.total, 0, "Initially no keys exist");
assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
machine.create_outbound_group_session_with_defaults(room_id()).await?;
machine.create_outbound_group_session_with_defaults(room_id2()).await?;
machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
let counts = backup_machine.store.inbound_group_session_counts().await?;
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
@@ -717,4 +798,56 @@ mod tests {
Ok(())
}
#[async_test]
async fn import_backed_up_room_keys() {
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
let backup_machine = machine.backup_machine();
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
let room_key = room_key();
let room_keys: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::from([(
room_id.to_owned(),
BTreeMap::from([(session_id.to_owned(), room_key)]),
)]);
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
assert!(session.is_none(), "Initially we should not have the session in the store");
backup_machine
.import_backed_up_room_keys(room_keys, |_, _| {})
.await
.expect("We should be able to import a room key");
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
assert_let!(Some(session) = session);
assert!(
session.backed_up(),
"If a session was imported from a backup, it should be considered to be backed up"
);
assert!(session.has_been_imported());
}
#[async_test]
async fn sign_backup_info() {
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
let backup_machine = machine.backup_machine();
let decryption_key = BackupDecryptionKey::new().unwrap();
let mut backup_info = decryption_key.to_backup_info();
let result = backup_machine.verify_backup(backup_info.to_owned(), false).await.unwrap();
assert!(!result.trusted());
backup_machine.sign_backup(&mut backup_info).await.unwrap();
let result = backup_machine.verify_backup(backup_info, false).await.unwrap();
assert!(result.trusted());
}
}
@@ -46,6 +46,7 @@ pub struct AttachmentDecryptor<'a, R: Read> {
aes: Aes256Ctr,
}
#[cfg(not(tarpaulin_include))]
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttachmentDecryptor")
@@ -165,6 +166,7 @@ pub struct AttachmentEncryptor<'a, R: Read + ?Sized> {
sha: Sha256,
}
#[cfg(not(tarpaulin_include))]
impl<'a, R: 'a + Read + std::fmt::Debug + ?Sized> std::fmt::Debug for AttachmentEncryptor<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttachmentEncryptor")
@@ -292,7 +292,7 @@ mod tests {
let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
let room_id = room_id!("!test:localhost");
machine.create_outbound_group_session_with_defaults(room_id).await.unwrap();
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
let export = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
assert!(!export.is_empty());
@@ -305,7 +305,7 @@ mod tests {
}
assert_eq!(
machine.import_room_keys(decrypted, false, |_, _| {}).await.unwrap(),
machine.store().import_exported_room_keys(decrypted, |_, _| {}).await.unwrap(),
RoomKeyImportResult::new(0, 1, BTreeMap::new())
);
}
@@ -316,7 +316,7 @@ mod tests {
let (machine, _) = get_prepared_machine_test_helper(user_id, false).await;
let room_id = room_id!("!test:localhost");
let session = machine.create_inbound_session(room_id).await?;
let session = machine.create_inbound_session_test_helper(room_id).await?;
let export = vec![session.export_at_index(10).await];
@@ -332,19 +332,22 @@ mod tests {
)]),
);
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
let export = vec![session.export_at_index(10).await];
assert_eq!(
machine.import_room_keys(export, false, |_, _| {}).await?,
machine.store().import_exported_room_keys(export, |_, _| {}).await?,
RoomKeyImportResult::new(0, 1, BTreeMap::new())
);
let better_export = vec![session.export().await];
assert_eq!(machine.import_room_keys(better_export, false, |_, _| {}).await?, keys);
assert_eq!(
machine.store().import_exported_room_keys(better_export, |_, _| {}).await?,
keys
);
let another_session = machine.create_inbound_session(room_id).await?;
let another_session = machine.create_inbound_session_test_helper(room_id).await?;
let export = vec![another_session.export_at_index(10).await];
let keys = RoomKeyImportResult::new(
@@ -359,7 +362,7 @@ mod tests {
)]),
);
assert_eq!(machine.import_room_keys(export, false, |_, _| {}).await?, keys);
assert_eq!(machine.store().import_exported_room_keys(export, |_, _| {}).await?, keys);
Ok(())
}
@@ -73,7 +73,13 @@ pub(crate) struct GossipMachineInner {
incoming_key_requests: StdRwLock<BTreeMap<RequestInfo, RequestEvent>>,
wait_queue: WaitQueue,
users_for_key_claim: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
/// Whether we should respond to incoming `m.room_key_request` messages.
room_key_forwarding_enabled: AtomicBool,
/// Whether we should send out `m.room_key_request` messages.
room_key_requests_enabled: AtomicBool,
identity_manager: IdentityManager,
}
@@ -87,6 +93,9 @@ impl GossipMachine {
let room_key_forwarding_enabled =
AtomicBool::new(cfg!(feature = "automatic-room-key-forwarding"));
let room_key_requests_enabled =
AtomicBool::new(cfg!(feature = "automatic-room-key-forwarding"));
Self {
inner: Arc::new(GossipMachineInner {
store,
@@ -97,6 +106,7 @@ impl GossipMachine {
wait_queue: WaitQueue::new(),
users_for_key_claim,
room_key_forwarding_enabled,
room_key_requests_enabled,
identity_manager,
}),
}
@@ -107,7 +117,7 @@ impl GossipMachine {
}
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn toggle_room_key_forwarding(&self, enabled: bool) {
pub fn set_room_key_forwarding_enabled(&self, enabled: bool) {
self.inner.room_key_forwarding_enabled.store(enabled, Ordering::SeqCst)
}
@@ -115,6 +125,19 @@ impl GossipMachine {
self.inner.room_key_forwarding_enabled.load(Ordering::SeqCst)
}
/// Configure whether we should send outgoing `m.room_key_request`s on
/// decryption failure.
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn set_room_key_requests_enabled(&self, enabled: bool) {
self.inner.room_key_requests_enabled.store(enabled, Ordering::SeqCst)
}
/// Query whether we should send outgoing `m.room_key_request`s on
/// decryption failure.
pub fn are_room_key_requests_enabled(&self) -> bool {
self.inner.room_key_requests_enabled.load(Ordering::SeqCst)
}
/// Load stored outgoing requests that were not yet sent out.
async fn load_outgoing_requests(&self) -> Result<Vec<OutgoingRequest>, CryptoStoreError> {
Ok(self
@@ -294,8 +317,8 @@ impl GossipMachine {
if device.user_id() == self.user_id() {
if device.is_verified() {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
user_id = ?device.user_id(),
device_id = ?device.device_id(),
?secret_name,
"Sharing a secret with a device",
);
@@ -304,9 +327,9 @@ impl GossipMachine {
Ok(s) => Ok(Some(s)),
Err(OlmError::MissingSession) => {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
secret_name = secret_name.as_ref(),
user_id = ?device.user_id(),
device_id = ?device.device_id(),
?secret_name,
"Secret request is missing an Olm session, \
putting the request in the wait queue",
);
@@ -318,8 +341,8 @@ impl GossipMachine {
}?
} else {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
user_id = ?device.user_id(),
device_id = ?device.device_id(),
?secret_name,
"Received a secret request that we won't serve, the device isn't trusted",
);
@@ -328,8 +351,8 @@ impl GossipMachine {
}
} else {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
user_id = ?device.user_id(),
device_id = ?device.device_id(),
?secret_name,
"Received a secret request that we won't serve, the device doesn't belong to us",
);
@@ -338,8 +361,8 @@ impl GossipMachine {
}
} else {
warn!(
user_id = event.sender.as_str(),
device_id = event.content.requesting_device_id.as_str(),
user_id = ?event.sender,
device_id = ?event.content.requesting_device_id,
?secret_name,
"Received a secret request from an unknown device",
);
@@ -445,9 +468,9 @@ impl GossipMachine {
#[tracing::instrument(
skip_all,
fields(
user_id = %event.sender,
device_id = %event.content.requesting_device_id,
%room_id,
user_id = ?event.sender,
device_id = ?event.content.requesting_device_id,
?room_id,
session_id
)
)]
@@ -516,7 +539,6 @@ impl GossipMachine {
content: SecretSendContent,
) -> OlmResult<Session> {
let event_type = content.event_type();
let content = serde_json::to_value(content)?;
let (used_session, content) = device.encrypt(event_type, content).await?;
let request = ToDeviceRequest::new(
@@ -606,8 +628,9 @@ impl GossipMachine {
let outbound_session = self
.inner
.outbound_group_sessions
.get_with_id(session.room_id(), session.session_id())
.await;
.get_or_load(session.room_id())
.await
.filter(|outgoing_session| outgoing_session.session_id() == session.session_id());
// If this is our own, verified device, we share the entire session from the
// earliest known index.
@@ -618,7 +641,7 @@ impl GossipMachine {
// at. For this, we need an outbound session because this
// information is recorded there.
} else if let Some(outbound) = outbound_session {
match outbound.is_shared_with(device) {
match outbound.is_shared_with(&device.inner) {
ShareState::Shared(message_index) => Ok(Some(message_index)),
ShareState::SharedButChangedSenderKey => Err(KeyForwardDecision::ChangedSenderKey),
ShareState::NotShared => Err(KeyForwardDecision::OutboundSessionNotShared),
@@ -641,7 +664,7 @@ impl GossipMachine {
/// the key we wish to request.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_request_key(&self, key_info: &SecretInfo) -> Result<bool, CryptoStoreError> {
if self.inner.room_key_forwarding_enabled.load(Ordering::SeqCst) {
if self.inner.room_key_requests_enabled.load(Ordering::SeqCst) {
let request = self.inner.store.get_secret_request_by_info(key_info).await?;
// Don't send out duplicate requests, users can re-request them if they
@@ -754,7 +777,10 @@ impl GossipMachine {
) -> Result<bool, CryptoStoreError> {
if let Some(info) = event.room_key_info(room_id).map(|i| i.into()) {
if self.should_request_key(&info).await? {
self.request_key_helper(info).await?;
// Size of the request_key_helper future should not impact this
// async fn since it is likely enough that this branch won't be
// entered.
Box::pin(self.request_key_helper(info)).await?;
return Ok(true);
}
}
@@ -785,9 +811,9 @@ impl GossipMachine {
if let Some(mut info) = info {
trace!(
recipient = info.request_recipient.as_str(),
recipient = ?info.request_recipient,
request_type = info.request_type(),
request_id = info.request_id.to_string().as_str(),
request_id = ?info.request_id,
"Marking outgoing secret request as sent"
);
info.sent_out = true;
@@ -804,9 +830,9 @@ impl GossipMachine {
/// This will queue up a request cancellation.
async fn mark_as_done(&self, key_info: &GossipRequest) -> Result<(), CryptoStoreError> {
trace!(
recipient = key_info.request_recipient.as_str(),
recipient = ?key_info.request_recipient,
request_type = key_info.request_type(),
request_id = key_info.request_id.to_string().as_str(),
request_id = ?key_info.request_id,
"Successfully received a secret, removing the request"
);
@@ -835,7 +861,7 @@ impl GossipMachine {
// secret.
Err(e) => {
warn!(
secret_name = %secret.secret_name,
secret_name = ?secret.secret_name,
error = ?e,
"Error while importing a secret"
);
@@ -948,7 +974,7 @@ impl GossipMachine {
info!(
?sender_key,
claimed_sender_key = ?session.sender_key(),
room_id = session.room_id().as_str(),
room_id = ?session.room_id(),
session_id = session.session_id(),
algorithm = ?session.algorithm(),
"Received a forwarded room key",
@@ -1069,7 +1095,7 @@ mod tests {
#[cfg(feature = "automatic-room-key-forwarding")]
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test::{async_test, message_like_event_content};
use ruma::{
device_id, event_id,
events::{
@@ -1080,7 +1106,6 @@ mod tests {
serde::Raw,
user_id, DeviceId, RoomId, UserId,
};
use serde_json::json;
use tokio::sync::Mutex;
use super::GossipMachine;
@@ -1101,12 +1126,12 @@ mod tests {
identities::{IdentityManager, LocalTrust, ReadOnlyDevice},
olm::{Account, PrivateCrossSigningIdentity},
session_manager::GroupSessionCache,
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
types::events::room::encrypted::{EncryptedEvent, RoomEncryptedEventContent},
store::{Changes, CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
types::events::room::encrypted::{
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
},
verification::VerificationMachine,
};
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
use crate::{store::Changes, types::events::room::encrypted::EncryptedToDeviceEvent};
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
@@ -1253,7 +1278,7 @@ mod tests {
.await
.unwrap();
let content = group_session.encrypt(json!({}), "m.dummy").await;
let content = group_session.encrypt("m.dummy", &message_like_event_content!({})).await;
let event = wrap_encrypted_content(bob_machine.user_id(), content);
// Alice wants to request the outbound group session from bob.
@@ -1276,7 +1301,6 @@ mod tests {
(alice_machine, group_session, bob_machine)
}
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
fn extract_content<'a>(
recipient: &UserId,
request: &'a crate::OutgoingRequest,
@@ -1309,7 +1333,6 @@ mod tests {
}
}
#[cfg(any(feature = "automatic-room-key-forwarding", feature = "backups_v1"))]
fn request_to_event<C>(
recipient: &UserId,
sender: &UserId,
@@ -1343,7 +1366,7 @@ mod tests {
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
let content = outbound.encrypt(json!({}), "m.dummy").await;
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
let event = wrap_encrypted_content(machine.user_id(), content);
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
@@ -1371,7 +1394,7 @@ mod tests {
machine.inner.store.save_devices(&[alice_device]).await.unwrap();
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
let content = outbound.encrypt(json!({}), "m.dummy").await;
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
let event = wrap_encrypted_content(machine.user_id(), content);
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
@@ -1384,12 +1407,41 @@ mod tests {
let requests = machine.outgoing_to_device_requests().await.unwrap();
assert_eq!(requests.len(), 1);
let request = requests.get(0).unwrap();
let request = &requests[0];
machine.mark_outgoing_request_as_sent(&request.request_id).await.unwrap();
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
}
/// We should *not* request keys if that has been disabled
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn create_key_request_requests_disabled() {
let machine = get_machine_test_helper().await;
let account = account();
let second_account = alice_2_account();
let alice_device = ReadOnlyDevice::from_account(&second_account);
// We need a trusted device, otherwise we won't request keys
alice_device.set_trust_state(LocalTrust::Verified);
machine.inner.store.save_devices(&[alice_device]).await.unwrap();
// Disable key requests
assert!(machine.are_room_key_requests_enabled());
machine.set_room_key_requests_enabled(false);
assert!(!machine.are_room_key_requests_enabled());
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
let event = wrap_encrypted_content(machine.user_id(), content);
// The outgoing to-device requests should be empty before and after
// `create_outgoing_key_request`.
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
machine.create_outgoing_key_request(session.room_id(), &event).await.unwrap();
assert!(machine.outgoing_to_device_requests().await.unwrap().is_empty());
}
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn receive_forwarded_key() {
@@ -1404,13 +1456,13 @@ mod tests {
machine.inner.store.save_devices(&[alice_device.clone()]).await.unwrap();
let (outbound, session) = account.create_group_session_pair_with_defaults(room_id()).await;
let content = outbound.encrypt(json!({}), "m.dummy").await;
let content = outbound.encrypt("m.dummy", &message_like_event_content!({})).await;
let room_event = wrap_encrypted_content(machine.user_id(), content);
machine.create_outgoing_key_request(session.room_id(), &room_event).await.unwrap();
let requests = machine.outgoing_to_device_requests().await.unwrap();
let request = requests.get(0).unwrap();
let request = &requests[0];
let id = &request.request_id;
machine.mark_outgoing_request_as_sent(id).await.unwrap();
@@ -1501,7 +1553,7 @@ mod tests {
#[async_test]
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_share_key_test() {
async fn test_should_share_key() {
let machine = get_machine_test_helper().await;
let account = account();
@@ -1869,7 +1921,6 @@ mod tests {
}
#[async_test]
#[cfg(feature = "backups_v1")]
async fn test_secret_broadcasting() {
use futures_util::{pin_mut, FutureExt};
use ruma::api::client::to_device::send_event_to_device::v3::Response as ToDeviceResponse;
@@ -1890,7 +1941,7 @@ mod tests {
vec![SecretName::RecoveryKey],
);
let mut changes = Changes::default();
let request_id = key_requests.first().unwrap().request_id.to_owned();
let request_id = key_requests[0].request_id.to_owned();
changes.key_requests = key_requests;
bob_machine.store().save_changes(changes).await.unwrap();
for request in bob_machine.outgoing_requests().await.unwrap() {
@@ -2001,7 +2052,7 @@ mod tests {
// Bob only has a keys claim request, since we're lacking a session
assert_eq!(bob_machine.outgoing_to_device_requests().await.unwrap().len(), 1);
assert_matches!(
bob_machine.outgoing_to_device_requests().await.unwrap().first().unwrap().request(),
bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(),
OutgoingRequests::KeysClaim(_)
);
assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty());
@@ -109,12 +109,9 @@ impl SecretInfo {
/// comparison.
pub fn as_key(&self) -> String {
match &self {
SecretInfo::KeyRequest(info) => format!(
"keyRequest:{}:{}:{}",
info.room_id().as_str(),
info.session_id(),
&info.algorithm(),
),
SecretInfo::KeyRequest(info) => {
format!("keyRequest:{}:{}:{}", info.room_id(), info.session_id(), info.algorithm())
}
SecretInfo::SecretRequest(sname) => format!("secretName:{sname}"),
}
}
@@ -30,7 +30,6 @@ use ruma::{
OwnedDeviceKeyId, UInt, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::Mutex;
use tracing::{instrument, trace, warn};
use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey};
@@ -96,6 +95,7 @@ fn default_timestamp() -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(UInt::default())
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for ReadOnlyDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReadOnlyDevice")
@@ -119,6 +119,7 @@ pub struct Device {
pub(crate) device_owner_identity: Option<ReadOnlyUserIdentities>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Device").field("device", &self.inner).finish()
@@ -411,63 +412,12 @@ impl Device {
/// # Arguments
///
/// * `content` - The content of the event that should be encrypted.
#[instrument(
skip_all,
fields(
recipient = %self.user_id(),
recipient_device = %self.device_id(),
recipient_key = ?self.curve25519_key(),
event_type,
session,
message_id,
))
]
pub(crate) async fn encrypt(
&self,
event_type: &str,
content: Value,
content: impl Serialize,
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
#[cfg(feature = "message-ids")]
let message_id = {
#[cfg(not(target_arch = "wasm32"))]
let id = ulid::Ulid::new().to_string();
#[cfg(target_arch = "wasm32")]
let id = ruma::TransactionId::new().to_string();
tracing::Span::current().record("message_id", &id);
Some(id)
};
#[cfg(not(feature = "message-ids"))]
let message_id = None;
self.inner
.encrypt(self.verification_machine.store.inner(), event_type, content, message_id)
.await
}
pub(crate) async fn maybe_encrypt_room_key(
&self,
session: OutboundGroupSession,
) -> OlmResult<MaybeEncryptedRoomKey> {
let content = session.as_content().await;
let message_index = session.message_index().await;
let event_type = content.event_type();
let content =
serde_json::to_value(content).expect("We can always serialize our own room key");
match self.encrypt(event_type, content).await {
Ok((session, encrypted)) => Ok(MaybeEncryptedRoomKey::Encrypted {
share_info: ShareInfo::new_shared(session.sender_key().to_owned(), message_index),
used_session: session,
message: encrypted.cast(),
}),
Err(OlmError::MissingSession | OlmError::EventError(EventError::MissingSenderKey)) => {
Ok(MaybeEncryptedRoomKey::Withheld { code: WithheldCode::NoOlm })
}
Err(e) => Err(e),
}
self.inner.encrypt(self.verification_machine.store.inner(), event_type, content).await
}
/// Encrypt the given inbound group session as a forwarded room key for this
@@ -485,7 +435,7 @@ impl Device {
};
let content: ForwardedRoomKeyContent = export.try_into()?;
(content.event_type(), serde_json::to_value(content)?)
(content.event_type(), content)
};
self.encrypt(event_type, content).await
@@ -770,28 +720,89 @@ impl ReadOnlyDevice {
)
}
/// Encrypt the given content for this device.
///
/// # Arguments
///
/// * `store` - The crypto store. Used to find an established Olm session
/// for this device.
/// * `event_type` - The type of the event that should be encrypted.
/// * `content` - The content of the event that should be encrypted.
///
/// # Returns
///
/// On success, a tuple `(session, content)`, where `session` is the Olm
/// [`Session`] that was used to encrypt the content, and `content` is
/// the content for the `m.room.encrypted` to-device event.
///
/// If an Olm session has not already been established with this device,
/// returns `Err(OlmError::MissingSession)`.
#[instrument(
skip_all,
fields(
recipient = ?self.user_id(),
recipient_device = ?self.device_id(),
recipient_key = ?self.curve25519_key(),
event_type,
session,
message_id,
))
]
pub(crate) async fn encrypt(
&self,
store: &CryptoStoreWrapper,
event_type: &str,
content: Value,
message_id: Option<String>,
content: impl Serialize,
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
#[cfg(feature = "message-ids")]
let message_id = {
#[cfg(not(target_arch = "wasm32"))]
let id = ulid::Ulid::new().to_string();
#[cfg(target_arch = "wasm32")]
let id = ruma::TransactionId::new().to_string();
tracing::Span::current().record("message_id", &id);
Some(id)
};
#[cfg(not(feature = "message-ids"))]
let message_id = None;
let session = self.get_most_recent_session(store).await?;
if let Some(mut session) = session {
let message = session.encrypt(self, event_type, content, message_id).await?;
trace!("Successfully encrypted an event");
Ok((session, message))
} else {
warn!("Trying to encrypt an event for a device, but no Olm session is found.",);
trace!("Trying to encrypt an event for a device, but no Olm session is found.");
Err(OlmError::MissingSession)
}
}
pub(crate) async fn maybe_encrypt_room_key(
&self,
store: &CryptoStoreWrapper,
session: OutboundGroupSession,
) -> OlmResult<MaybeEncryptedRoomKey> {
let content = session.as_content().await;
let message_index = session.message_index().await;
let event_type = content.event_type();
match self.encrypt(store, event_type, content).await {
Ok((session, encrypted)) => Ok(MaybeEncryptedRoomKey::Encrypted {
share_info: ShareInfo::new_shared(session.sender_key().to_owned(), message_index),
used_session: session,
message: encrypted.cast(),
}),
Err(OlmError::MissingSession | OlmError::EventError(EventError::MissingSenderKey)) => {
Ok(MaybeEncryptedRoomKey::Withheld { code: WithheldCode::NoOlm })
}
Err(e) => Err(e),
}
}
/// Update a device with a new device keys struct.
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) -> Result<(), SignatureError> {
self.verify_device_keys(device_keys)?;
@@ -823,7 +834,6 @@ impl ReadOnlyDevice {
/// **Note**: Use this method with caution, the `canonical_json` needs to be
/// correctly canonicalized and make sure that the object you are checking
/// the signature for is allowed to be signed by a device.
#[cfg(feature = "backups_v1")]
pub(crate) fn has_signed_raw(
&self,
signatures: &Signatures,
@@ -874,14 +884,14 @@ impl ReadOnlyDevice {
/// Create a `ReadOnlyDevice` from an `Account`
///
/// We will have our own device in the store once we receive a keys/query
/// We will have our own device in the store once we receive a `/keys/query`
/// response, but this is useful to create it before we receive such a
/// response.
///
/// It also makes it easier to check that the server doesn't lie about our
/// own device.
///
/// *Don't* use this after we received a keys/query response, other
/// *Don't* use this after we received a `/keys/query` response, other
/// users/devices might add signatures to our own device, which can't be
/// replicated locally.
pub fn from_account(account: &Account) -> ReadOnlyDevice {
@@ -13,14 +13,15 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ops::Deref,
sync::Arc,
time::Duration,
};
use futures_util::future::join_all;
use itertools::Itertools;
use matrix_sdk_common::executor::spawn;
use matrix_sdk_common::{executor::spawn, failures_cache::FailuresCache};
use ruma::{
api::client::keys::get_keys::v3::Response as KeysQueryResponse, serde::Raw, OwnedDeviceId,
OwnedServerName, OwnedTransactionId, OwnedUserId, ServerName, TransactionId, UserId,
@@ -37,11 +38,10 @@ use crate::{
requests::KeysQueryRequest,
store::{
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
Result as StoreResult, Store, StoreCache,
Result as StoreResult, Store, StoreCache, UserKeyQueryResult,
},
types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
utilities::FailuresCache,
LocalTrust, SignatureError,
CryptoStoreError, LocalTrust, SignatureError,
};
enum DeviceChange {
@@ -61,6 +61,10 @@ enum IdentityUpdateResult {
#[derive(Debug, Clone)]
pub(crate) struct IdentityManager {
/// Servers that have previously appeared in the `failures` section of a
/// `/keys/query` response.
///
/// See also [`crate::session_manager::SessionManager::failures`].
failures: FailuresCache<OwnedServerName>,
store: Store,
@@ -101,7 +105,7 @@ impl IdentityManager {
&self.store.static_account().user_id
}
/// Receive a successful keys query response.
/// Receive a successful `/keys/query` response.
///
/// Returns a list of devices newly discovered devices and devices that
/// changed.
@@ -110,7 +114,7 @@ impl IdentityManager {
///
/// * `request_id` - The request_id returned by `users_for_key_query` or
/// `build_key_query_for_users`
/// * `response` - The keys query response of the request that the client
/// * `response` - The response of the `/keys/query` request that the client
/// performed.
pub async fn receive_keys_query_response(
&self,
@@ -121,7 +125,7 @@ impl IdentityManager {
?request_id,
users = ?response.device_keys.keys().collect::<BTreeSet<_>>(),
failures = ?response.failures,
"Handling a keys query response"
"Handling a `/keys/query` response"
);
// Parse the strings into server names and filter out our own server. We should
@@ -209,7 +213,7 @@ impl IdentityManager {
?deleted_devices,
?new_identities,
?changed_identities,
"Finished handling of the keys/query response"
"Finished handling of the `/keys/query` response"
);
Ok((devices, identities))
@@ -225,8 +229,8 @@ impl IdentityManager {
if let Some(mut device) = old_device {
if let Err(e) = device.update_device(&device_keys) {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
user_id = ?device.user_id(),
device_id = ?device.device_id(),
error = ?e,
"Failed to update device keys",
);
@@ -248,8 +252,8 @@ impl IdentityManager {
d.set_trust_state(LocalTrust::Verified);
trace!(
user_id = d.user_id().as_str(),
device_id = d.device_id().as_str(),
user_id = ?d.user_id(),
device_id = ?d.device_id(),
keys = ?d.keys(),
"Adding our own device to the device store, \
marking it as locally verified",
@@ -261,8 +265,8 @@ impl IdentityManager {
}
} else {
trace!(
user_id = d.user_id().as_str(),
device_id = d.device_id().as_str(),
user_id = ?d.user_id(),
device_id = ?d.device_id(),
keys = ?d.keys(),
"Adding a new device to the device store",
);
@@ -272,8 +276,8 @@ impl IdentityManager {
}
Err(e) => {
warn!(
user_id = device_keys.user_id.as_str(),
device_id = device_keys.device_id.as_str(),
user_id = ?device_keys.user_id,
device_id = ?device_keys.device_id,
error = ?e,
"Failed to create a new device",
);
@@ -301,10 +305,10 @@ impl IdentityManager {
Ok(device_keys) => {
if user_id != device_keys.user_id || device_id != device_keys.device_id {
warn!(
user_id = user_id.as_str(),
device_id = device_id.as_str(),
device_key_user = device_keys.user_id.as_str(),
device_key_device_id = device_keys.device_id.as_str(),
?user_id,
?device_id,
device_key_user = ?device_keys.user_id,
device_key_device_id = ?device_keys.device_id,
"Mismatch in the device keys payload",
);
None
@@ -314,9 +318,7 @@ impl IdentityManager {
}
Err(e) => {
warn!(
user_id = user_id.as_str(),
device_id = device_id.as_str(),
error = ?e,
?user_id, ?device_id, error = ?e,
"Device keys failed to deserialize",
);
None
@@ -346,8 +348,8 @@ impl IdentityManager {
let identity_keys = store.static_account().identity_keys();
warn!(
user_id = own_user_id.as_str(),
device_id = own_device_id.as_str(),
user_id = ?own_user_id,
device_id = ?own_device_id,
curve25519_key = ?identity_keys.curve25519,
ed25519_key = ?identity_keys.ed25519,
"Our own device might have been deleted"
@@ -544,7 +546,7 @@ impl IdentityManager {
}
}
/// Try to deserialize the the master key and self-signing key of an
/// Try to deserialize the master key and self-signing key of an
/// identity from a `/keys/query` response.
///
/// Each user identity *must* at least contain a master and self-signing
@@ -878,6 +880,137 @@ impl IdentityManager {
let cache = self.store.cache().await?;
self.key_query_manager.synced(&cache).await?.update_tracked_users(users.into_iter()).await
}
/// Retrieve a list of a user's current devices, so we can encrypt a message
/// to them.
///
/// If we have not yet seen any devices for the user, and their device list
/// has been marked as outdated, then we wait for the `/keys/query` request
/// to complete. This helps ensure that we attempt at least once to fetch a
/// user's devices before encrypting to them.
pub async fn get_user_devices_for_encryption(
&self,
users: impl Iterator<Item = &UserId>,
) -> StoreResult<HashMap<OwnedUserId, HashMap<OwnedDeviceId, ReadOnlyDevice>>> {
// How long we wait for /keys/query to complete.
const KEYS_QUERY_WAIT_TIME: Duration = Duration::from_secs(5);
let mut devices_by_user = HashMap::new();
let mut users_with_no_devices_on_failed_servers = Vec::new();
let mut users_with_no_devices_on_unfailed_servers = Vec::new();
for user_id in users {
// First of all, check the store for this user.
let devices = self.store.get_readonly_devices_filtered(user_id).await?;
// Now, look for users who have no devices at all.
//
// If a user has no devices at all, that implies we have never (successfully)
// done a `/keys/query` for them; we wait for one to complete if it is
// in flight. (Of course, the user might genuinely have no devices, but
// that's fine, it just means we redundantly grab the cache guard and
// check the pending-query flag.)
if !devices.is_empty() {
// This user has at least one known device.
//
// The device list may also be outdated in this case; but in this
// situation, we are racing between sending a message and retrieving their
// device list. That's an inherently racy situation and there is no real
// benefit to waiting for the `/keys/query` request to complete. So we don't
// bother.
//
// We just add their devices to the result and carry on.
devices_by_user.insert(user_id.to_owned(), devices);
continue;
}
// *However*, if the user's server is currently subject to a backoff due to
// previous failures, then `users_for_key_query` won't attempt to query
// for the user's devices, so there's no point waiting.
//
// XXX: this is racy. It's possible that:
// * `failures` included the user's server when `users_for_key_query` was
// called, so the user was not returned in the `KeyQueryRequest`, and:
// * The backoff has now expired.
//
// In that case, we'll end up waiting for the *next* `users_for_key_query` call,
// which might not be for 30 seconds or so. (And by then, it might be `failed`
// again.)
if self.failures.contains(user_id.server_name()) {
users_with_no_devices_on_failed_servers.push(user_id);
continue;
}
users_with_no_devices_on_unfailed_servers.push(user_id);
}
if !users_with_no_devices_on_failed_servers.is_empty() {
info!(
?users_with_no_devices_on_failed_servers,
"Not waiting for `/keys/query` for users whose server has previously failed"
);
}
if !users_with_no_devices_on_unfailed_servers.is_empty() {
// For each user with no devices, fire off a task to wait for a `/keys/query`
// result if one is pending.
//
// We don't actually update the `devices_by_user` map here since that could
// require concurrent access to it. Instead each task returns a
// `(OwnedUserId, HashMap)` pair (or rather, an `Option` of one) so that we can
// add the results to the map.
let results = join_all(
users_with_no_devices_on_unfailed_servers
.into_iter()
.map(|user_id| self.get_updated_keys_for_user(KEYS_QUERY_WAIT_TIME, user_id)),
)
.await;
// Once all the tasks have completed, process the results.
let mut updated_users = Vec::new();
for result in results {
if let Some((user_id, updated_devices)) = result? {
devices_by_user.insert(user_id.to_owned(), updated_devices);
updated_users.push(user_id);
}
}
if !updated_users.is_empty() {
info!(
?updated_users,
"Waited for `/keys/query` to complete for users who have no devices"
);
}
}
Ok(devices_by_user)
}
/// Helper for get_user_devices_for_encryption.
///
/// Waits for any pending `/keys/query` for the given user. If one was
/// pending, reloads the device list and returns `Some(user_id,
/// device_list)`. If no request was pending, returns `None`.
#[allow(clippy::type_complexity)]
#[instrument(skip(self))]
async fn get_updated_keys_for_user<'a>(
&self,
timeout_duration: Duration,
user_id: &'a UserId,
) -> Result<Option<(&'a UserId, HashMap<OwnedDeviceId, ReadOnlyDevice>)>, CryptoStoreError>
{
let cache = self.store.cache().await?;
match self
.key_query_manager
.wait_if_user_key_query_pending(cache, timeout_duration, user_id)
.await?
{
UserKeyQueryResult::WasPending => {
Ok(Some((user_id, self.store.get_readonly_devices_filtered(user_id).await?)))
}
_ => Ok(None),
}
}
}
#[cfg(any(test, feature = "testing"))]
@@ -987,7 +1120,7 @@ pub(crate) mod testing {
"user_signing_keys": {}
}));
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
// An updated version of `other_key_query` featuring an additional signature on
@@ -1053,7 +1186,7 @@ pub(crate) mod testing {
"user_signing_keys": {}
}));
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
/// Mocked response to a /keys/query request.
@@ -1155,7 +1288,7 @@ pub(crate) mod testing {
}
}));
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
pub fn own_key_query() -> KeyQueryResponse {
@@ -1186,7 +1319,7 @@ pub(crate) mod testing {
);
KeyQueryResponse::try_from_http_response(response_from_file(&json))
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
}
@@ -1355,7 +1488,7 @@ pub(crate) mod tests {
});
let response = KeysQueryResponse::try_from_http_response(response_from_file(&response))
.expect("Can't parse the keys query response");
.expect("Can't parse the `/keys/query` response");
manager.receive_keys_query_response(&TransactionId::new(), &response).await.unwrap();
@@ -1405,7 +1538,7 @@ pub(crate) mod tests {
});
let response = KeysQueryResponse::try_from_http_response(response_from_file(&response))
.expect("Can't parse the keys query response");
.expect("Can't parse the `/keys/query` response");
let (_, private_identity) = manager.handle_cross_signing_keys(&response).await.unwrap();
@@ -1589,7 +1722,7 @@ pub(crate) mod tests {
let (new_request_id, _) =
manager.as_ref().unwrap().build_key_query_for_users(vec![user_id()]);
// A second `keys/query` response with the same result shouldn't fire a change
// A second `/keys/query` response with the same result shouldn't fire a change
// notification: the identity should be unchanged.
manager
.as_ref()
+2 -11
View File
@@ -16,7 +16,6 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs, missing_debug_implementations)]
#[cfg(feature = "backups_v1")]
pub mod backups;
mod ciphers;
pub mod dehydrated_devices;
@@ -104,13 +103,5 @@ pub use vodozemac;
/// The version of the matrix-sdk-cypto crate being used
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
// Enable tracing for tests in this crate
#[cfg(all(test, not(target_arch = "wasm32")))]
#[ctor::ctor]
fn init_logging() {
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer().with_test_writer())
.init();
}
#[cfg(test)]
matrix_sdk_test::init_tracing_for_tests!();
+217 -199
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
collections::{BTreeMap, HashSet},
sync::{Arc, RwLock as StdRwLock},
time::Duration,
};
@@ -27,7 +27,7 @@ use ruma::{
api::client::{
dehydrated_device::DehydratedDeviceData,
keys::{
claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse},
claim_keys::v3::Request as KeysClaimRequest,
get_keys::v3::Response as KeysQueryResponse,
upload_keys::v3::{Request as UploadKeysRequest, Response as UploadKeysResponse},
upload_signatures::v3::Request as UploadSignaturesRequest,
@@ -36,13 +36,14 @@ use ruma::{
},
assign,
events::{
secret::request::SecretName, AnyMessageLikeEvent, AnyToDeviceEvent, MessageLikeEventContent,
secret::request::SecretName, AnyMessageLikeEvent, AnyMessageLikeEventContent,
AnyToDeviceEvent, MessageLikeEventContent,
},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId,
RoomId, TransactionId, UInt, UserId,
};
use serde_json::{value::to_raw_value, Value};
use serde_json::value::to_raw_value;
use tokio::sync::Mutex;
use tracing::{
debug, error,
@@ -54,9 +55,8 @@ use vodozemac::{
Curve25519PublicKey, Ed25519Signature,
};
#[cfg(feature = "backups_v1")]
use crate::backups::BackupMachine;
use crate::{
backups::{BackupMachine, MegolmV1BackupKey},
dehydrated_devices::{DehydratedDevices, DehydrationError},
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult},
gossiping::GossipMachine,
@@ -128,7 +128,6 @@ pub struct OlmMachineInner {
/// of when a key query needs to be done and handling one.
identity_manager: IdentityManager,
/// A state machine that handles creating room key backups.
#[cfg(feature = "backups_v1")]
backup_machine: BackupMachine,
}
@@ -174,7 +173,13 @@ impl OlmMachine {
let store = Arc::new(CryptoStoreWrapper::new(self.user_id(), MemoryStore::new()));
store.save_pending_changes(PendingChanges { account: Some(account) }).await?;
Ok(Self::new_helper(device_id, store, static_account, self.store().private_identity()))
Ok(Self::new_helper(
device_id,
store,
static_account,
self.store().private_identity(),
None,
))
}
fn new_helper(
@@ -182,6 +187,7 @@ impl OlmMachine {
store: Arc<CryptoStoreWrapper>,
account: StaticAccountData,
user_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
maybe_backup_key: Option<MegolmV1BackupKey>,
) -> Self {
let verification_machine =
VerificationMachine::new(account.clone(), user_identity.clone(), store.clone());
@@ -202,8 +208,7 @@ impl OlmMachine {
let session_manager =
SessionManager::new(users_for_key_claim, key_request_machine.clone(), store.clone());
#[cfg(feature = "backups_v1")]
let backup_machine = BackupMachine::new(store.clone(), None);
let backup_machine = BackupMachine::new(store.clone(), maybe_backup_key);
let inner = Arc::new(OlmMachineInner {
user_id: store.user_id().to_owned(),
@@ -215,7 +220,6 @@ impl OlmMachine {
verification_machine,
key_request_machine,
identity_manager,
#[cfg(feature = "backups_v1")]
backup_machine,
});
@@ -309,9 +313,21 @@ impl OlmMachine {
}
};
let saved_keys = store.load_backup_keys().await?;
let maybe_backup_key = saved_keys.decryption_key.and_then(|k| {
if let Some(version) = saved_keys.backup_version {
MegolmV1BackupKey::from_base64(&k.to_base64()).ok().map(|k| {
k.set_version(version);
k
})
} else {
None
}
});
let identity = Arc::new(Mutex::new(identity));
let store = Arc::new(CryptoStoreWrapper::new(user_id, store));
Ok(OlmMachine::new_helper(device_id, store, static_account, identity))
Ok(OlmMachine::new_helper(device_id, store, static_account, identity, maybe_backup_key))
}
/// Get the crypto store associated with this `OlmMachine` instance.
@@ -349,17 +365,43 @@ impl OlmMachine {
Ok(self.inner.identity_manager.key_query_manager.synced(&cache).await?.tracked_users())
}
/// Enable or disable room key requests.
///
/// Room key requests allow the device to request room keys that it might
/// have missed in the original share using `m.room_key_request`
/// events.
///
/// See also [`OlmMachine::set_room_key_forwarding_enabled`] and
/// [`OlmMachine::are_room_key_requests_enabled`].
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn set_room_key_requests_enabled(&self, enable: bool) {
self.inner.key_request_machine.set_room_key_requests_enabled(enable)
}
/// Query whether we should send outgoing `m.room_key_request`s on
/// decryption failure.
///
/// See also [`OlmMachine::set_room_key_requests_enabled`].
pub fn are_room_key_requests_enabled(&self) -> bool {
self.inner.key_request_machine.are_room_key_requests_enabled()
}
/// Enable or disable room key forwarding.
///
/// Room key forwarding allows the device to request room keys that it might
/// have missend in the original share using `m.room_key_request`
/// events.
/// If room key forwarding is enabled, we will automatically reply to
/// incoming `m.room_key_request` messages from verified devices by
/// forwarding the requested key (if we have it).
///
/// See also [`OlmMachine::set_room_key_requests_enabled`] and
/// [`OlmMachine::is_room_key_forwarding_enabled`].
#[cfg(feature = "automatic-room-key-forwarding")]
pub fn toggle_room_key_forwarding(&self, enable: bool) {
self.inner.key_request_machine.toggle_room_key_forwarding(enable)
pub fn set_room_key_forwarding_enabled(&self, enable: bool) {
self.inner.key_request_machine.set_room_key_forwarding_enabled(enable)
}
/// Is room key forwarding enabled?
///
/// See also [`OlmMachine::set_room_key_forwarding_enabled`].
pub fn is_room_key_forwarding_enabled(&self) -> bool {
self.inner.key_request_machine.is_room_key_forwarding_enabled()
}
@@ -407,6 +449,9 @@ impl OlmMachine {
/// This can be useful if we need the results from [`get_identity`] or
/// [`get_user_devices`] to be as up-to-date as possible.
///
/// Note that this request won't be awaited by other calls waiting for a
/// user's or device's keys, since this is an out-of-band query.
///
/// # Arguments
///
/// * `users` - list of users whose keys should be queried
@@ -442,19 +487,22 @@ impl OlmMachine {
) -> OlmResult<()> {
match response.into() {
IncomingResponse::KeysUpload(response) => {
self.receive_keys_upload_response(response).await?;
Box::pin(self.receive_keys_upload_response(response)).await?;
}
IncomingResponse::KeysQuery(response) => {
self.receive_keys_query_response(request_id, response).await?;
Box::pin(self.receive_keys_query_response(request_id, response)).await?;
}
IncomingResponse::KeysClaim(response) => {
self.receive_keys_claim_response(response).await?;
Box::pin(
self.inner.session_manager.receive_keys_claim_response(request_id, response),
)
.await?;
}
IncomingResponse::ToDevice(_) => {
self.mark_to_device_request_as_sent(request_id).await?;
Box::pin(self.mark_to_device_request_as_sent(request_id)).await?;
}
IncomingResponse::SigningKeysUpload(_) => {
self.receive_cross_signing_upload_response().await?;
Box::pin(self.receive_cross_signing_upload_response()).await?;
}
IncomingResponse::SignatureUpload(_) => {
self.inner.verification_machine.mark_request_as_sent(request_id);
@@ -463,8 +511,7 @@ impl OlmMachine {
self.inner.verification_machine.mark_request_as_sent(request_id);
}
IncomingResponse::KeysBackup(_) => {
#[cfg(feature = "backups_v1")]
self.inner.backup_machine.mark_request_as_sent(request_id).await?;
Box::pin(self.inner.backup_machine.mark_request_as_sent(request_id)).await?;
}
};
@@ -563,12 +610,12 @@ impl OlmMachine {
})
}
/// Receive a successful keys upload response.
/// Receive a successful `/keys/upload` response.
///
/// # Arguments
///
/// * `response` - The keys upload response of the request that the client
/// performed.
/// * `response` - The response of the `/keys/upload` request that the
/// client performed.
async fn receive_keys_upload_response(&self, response: &UploadKeysResponse) -> OlmResult<()> {
self.inner
.store
@@ -607,6 +654,7 @@ impl OlmMachine {
/// this method between sync requests.
///
/// [`mark_request_as_sent`]: #method.mark_request_as_sent
#[instrument(skip_all)]
pub async fn get_missing_sessions(
&self,
users: impl Iterator<Item = &UserId>,
@@ -614,25 +662,15 @@ impl OlmMachine {
self.inner.session_manager.get_missing_sessions(users).await
}
/// Receive a successful key claim response and create new Olm sessions with
/// the claimed keys.
///
/// # Arguments
///
/// * `response` - The response containing the claimed one-time keys.
async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
self.inner.session_manager.receive_keys_claim_response(response).await
}
/// Receive a successful keys query response.
/// Receive a successful `/keys/query` response.
///
/// Returns a list of devices newly discovered devices and devices that
/// changed.
///
/// # Arguments
///
/// * `response` - The keys query response of the request that the client
/// performed.
/// * `response` - The response of the `/keys/query` request that the client
/// performed.
async fn receive_keys_query_response(
&self,
request_id: &TransactionId,
@@ -772,7 +810,7 @@ impl OlmMachine {
}
#[cfg(test)]
pub(crate) async fn create_outbound_group_session_with_defaults(
pub(crate) async fn create_outbound_group_session_with_defaults_test_helper(
&self,
room_id: &RoomId,
) -> OlmResult<()> {
@@ -789,7 +827,7 @@ impl OlmMachine {
#[cfg(test)]
#[allow(dead_code)]
pub(crate) async fn create_inbound_session(
pub(crate) async fn create_inbound_session_test_helper(
&self,
room_id: &RoomId,
) -> OlmResult<InboundGroupSession> {
@@ -824,11 +862,11 @@ impl OlmMachine {
content: impl MessageLikeEventContent,
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
let event_type = content.event_type().to_string();
let content = serde_json::to_value(&content)?;
self.encrypt_room_event_raw(room_id, content, &event_type).await
let content = Raw::new(&content)?.cast();
self.encrypt_room_event_raw(room_id, &event_type, &content).await
}
/// Encrypt a json [`Value`] content for the given room.
/// Encrypt a raw JSON content for the given room.
///
/// This method is equivalent to the [`OlmMachine::encrypt_room_event()`]
/// method but operates on an arbitrary JSON value instead of strongly-typed
@@ -840,7 +878,7 @@ impl OlmMachine {
/// encrypted.
///
/// * `content` - The plaintext content of the message that should be
/// encrypted as a json [`Value`].
/// encrypted as a raw JSON value.
///
/// * `event_type` - The plaintext type of the event.
///
@@ -850,10 +888,10 @@ impl OlmMachine {
pub async fn encrypt_room_event_raw(
&self,
room_id: &RoomId,
content: Value,
event_type: &str,
content: &Raw<AnyMessageLikeEventContent>,
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
self.inner.group_session_manager.encrypt(room_id, content, event_type).await
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
}
/// Invalidate the currently active outbound group session for the given
@@ -1142,18 +1180,12 @@ impl OlmMachine {
/// This will decrypt and handle to-device events returning the decrypted
/// versions of them.
///
/// To decrypt an event from the room timeline call [`decrypt_room_event`].
/// To decrypt an event from the room timeline, call [`decrypt_room_event`].
///
/// # Arguments
///
/// * `to_device_events` - The to-device events of the current sync
/// response.
///
/// * `changed_devices` - The list of devices that changed in this sync
/// response.
///
/// * `one_time_keys_count` - The current one-time keys counts that the sync
/// response returned.
/// * `sync_changes` - an [`EncryptionSyncChanges`] value, constructed from
/// a sync response.
///
/// [`decrypt_room_event`]: #method.decrypt_room_event
///
@@ -1347,10 +1379,8 @@ impl OlmMachine {
/// ```
pub async fn query_missing_secrets_from_other_sessions(&self) -> StoreResult<bool> {
let identity = self.inner.user_identity.lock().await;
#[allow(unused_mut)]
let mut secrets = identity.get_missing_secrets().await;
#[cfg(feature = "backups_v1")]
if self.store().load_backup_keys().await?.decryption_key.is_none() {
secrets.push(SecretName::RecoveryKey);
}
@@ -1550,6 +1580,16 @@ impl OlmMachine {
result
}
/// Do we have the room key for the given room and with the given session id
/// in the store?
pub async fn is_room_key_available(
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<bool, CryptoStoreError> {
Ok(self.store().get_inbound_group_session(room_id, session_id).await?.is_some())
}
/// Get encryption info for a decrypted timeline event.
///
/// This recalculates the [`EncryptionInfo`] data that is returned by
@@ -1615,10 +1655,8 @@ impl OlmMachine {
self.inner
.identity_manager
.key_query_manager
.synced(&cache)
.await?
.wait_if_user_key_query_pending(timeout, user_id)
.await;
.wait_if_user_key_query_pending(cache, timeout, user_id)
.await?;
}
Ok(())
}
@@ -1652,6 +1690,7 @@ impl OlmMachine {
/// println!("{:?}", device);
/// # });
/// ```
#[instrument(skip(self))]
pub async fn get_device(
&self,
user_id: &UserId,
@@ -1675,6 +1714,7 @@ impl OlmMachine {
///
/// Returns a `UserIdentities` enum if one is found and the crypto store
/// didn't throw an error.
#[instrument(skip(self))]
pub async fn get_identity(
&self,
user_id: &UserId,
@@ -1710,6 +1750,7 @@ impl OlmMachine {
/// }
/// # });
/// ```
#[instrument(skip(self))]
pub async fn get_user_devices(
&self,
user_id: &UserId,
@@ -1749,77 +1790,17 @@ impl OlmMachine {
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
/// # };
/// ```
#[deprecated(
since = "0.7.0",
note = "Use the OlmMachine::store::import_exported_room_keys method instead"
)]
pub async fn import_room_keys(
&self,
exported_keys: Vec<ExportedRoomKey>,
#[allow(unused_variables)] from_backup: bool,
from_backup: bool,
progress_listener: impl Fn(usize, usize),
) -> StoreResult<RoomKeyImportResult> {
let mut sessions = Vec::new();
async fn new_session_better(
session: &InboundGroupSession,
old_session: Option<InboundGroupSession>,
) -> bool {
if let Some(old_session) = &old_session {
session.compare(old_session).await == SessionOrdering::Better
} else {
true
}
}
let total_count = exported_keys.len();
let mut keys = BTreeMap::new();
for (i, key) in exported_keys.into_iter().enumerate() {
match InboundGroupSession::from_export(&key) {
Ok(session) => {
let old_session = self
.inner
.store
.get_inbound_group_session(session.room_id(), session.session_id())
.await?;
// Only import the session if we didn't have this session or
// if it's a better version of the same session.
if new_session_better(&session, old_session).await {
#[cfg(feature = "backups_v1")]
if from_backup {
session.mark_as_backed_up();
}
keys.entry(session.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(session.sender_key().to_base64())
.or_insert_with(BTreeSet::new)
.insert(session.session_id().to_owned());
sessions.push(session);
}
}
Err(e) => {
warn!(
sender_key= key.sender_key.to_base64(),
room_id = ?key.room_id,
session_id = key.session_id,
error = ?e,
"Couldn't import a room key from a file export."
);
}
}
progress_listener(i, total_count);
}
let imported_count = sessions.len();
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
self.store().save_changes(changes).await?;
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
}
/// Export the keys that match the given predicate.
@@ -1950,7 +1931,6 @@ impl OlmMachine {
///
/// This state machine can be used to incrementally backup all room keys to
/// the server.
#[cfg(feature = "backups_v1")]
pub fn backup_machine(&self) -> &BackupMachine {
&self.inner.backup_machine
}
@@ -2116,9 +2096,10 @@ pub struct CrossSigningBootstrapRequests {
pub struct EncryptionSyncChanges<'a> {
/// The list of to-device events received in the sync.
pub to_device_events: Vec<Raw<AnyToDeviceEvent>>,
/// The mapping of changed and left devices, per user.
/// The mapping of changed and left devices, per user, as returned in the
/// sync response.
pub changed_devices: &'a DeviceLists,
/// The number of one time keys.
/// The number of one time keys, as returned in the sync response.
pub one_time_keys_counts: &'a BTreeMap<DeviceKeyAlgorithm, UInt>,
/// An optional list of fallback keys.
pub unused_fallback_keys: Option<&'a [DeviceKeyAlgorithm]>,
@@ -2152,7 +2133,7 @@ pub(crate) mod tests {
use matrix_sdk_common::deserialized_responses::{
DeviceLinkProblem, ShieldState, VerificationLevel, VerificationState,
};
use matrix_sdk_test::{async_test, test_json};
use matrix_sdk_test::{async_test, message_like_event_content, test_json};
use ruma::{
api::{
client::{
@@ -2226,13 +2207,13 @@ pub(crate) mod tests {
fn keys_upload_response() -> upload_keys::v3::Response {
let data = response_from_file(&test_json::KEYS_UPLOAD);
upload_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
fn keys_query_response() -> get_keys::v3::Response {
let data = response_from_file(&test_json::KEYS_QUERY);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
pub fn to_device_requests_to_content(
@@ -2315,21 +2296,20 @@ pub(crate) mod tests {
bob: &UserId,
use_fallback_key: bool,
) -> (OlmMachine, OlmMachine) {
let (alice, bob, one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await;
let (alice, bob, mut one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await;
let mut bob_keys = BTreeMap::new();
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
let (device_key_id, one_time_key) = one_time_keys.iter().next().unwrap();
let mut keys = BTreeMap::new();
keys.insert(device_key_id.clone(), one_time_key.clone());
bob_keys.insert(bob.device_id().into(), keys);
let mut one_time_keys = BTreeMap::new();
one_time_keys.insert(bob.user_id().to_owned(), bob_keys);
let one_time_keys = BTreeMap::from([(
bob.user_id().to_owned(),
BTreeMap::from([(
bob.device_id().to_owned(),
BTreeMap::from([(device_key_id, one_time_key)]),
)]),
)]);
let response = claim_keys::v3::Response::new(one_time_keys);
alice.receive_keys_claim_response(&response).await.unwrap();
alice.inner.session_manager.create_sessions(&response).await.unwrap();
(alice, bob)
}
@@ -2344,10 +2324,8 @@ pub(crate) mod tests {
let bob_device =
alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
let (session, content) = bob_device
.encrypt("m.dummy", serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap())
.await
.unwrap();
let (session, content) =
bob_device.encrypt("m.dummy", ToDeviceDummyEventContent::new()).await.unwrap();
alice.store().save_sessions(&[session]).await.unwrap();
let event =
@@ -2452,11 +2430,11 @@ pub(crate) mod tests {
}
#[async_test]
async fn tests_session_invalidation() {
async fn test_session_invalidation() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let room_id = room_id!("!test:example.org");
machine.create_outbound_group_session_with_defaults(room_id).await.unwrap();
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
assert!(machine.inner.group_session_manager.get_outbound_group_session(room_id).is_some());
machine.invalidate_group_session(room_id).await.unwrap();
@@ -2631,25 +2609,26 @@ pub(crate) mod tests {
key_id: OwnedDeviceKeyId,
one_time_key: Raw<OneTimeKey>,
) {
let keys = BTreeMap::from([(key_id, one_time_key)]);
let keys = BTreeMap::from([(device_id.to_owned(), keys)]);
let one_time_keys = BTreeMap::from([(user_id.to_owned(), keys)]);
let response = claim_keys::v3::Response::new(one_time_keys);
let one_time_keys = BTreeMap::from([(
user_id.to_owned(),
BTreeMap::from([(device_id.to_owned(), BTreeMap::from([(key_id, one_time_key)]))]),
)]);
machine.receive_keys_claim_response(&response).await.unwrap();
let response = claim_keys::v3::Response::new(one_time_keys);
machine.inner.session_manager.create_sessions(&response).await.unwrap();
}
#[async_test]
async fn test_session_creation() {
let (alice_machine, bob_machine, mut one_time_keys) =
get_machine_pair(alice_id(), user_id(), false).await;
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
create_session(
&alice_machine,
bob_machine.user_id(),
bob_machine.device_id(),
device_key_id,
key_id,
one_time_key,
)
.await;
@@ -2670,7 +2649,7 @@ pub(crate) mod tests {
async fn test_getting_most_recent_session() {
let (alice_machine, bob_machine, mut one_time_keys) =
get_machine_pair(alice_id(), user_id(), false).await;
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
let device = alice_machine
.get_device(bob_machine.user_id(), bob_machine.device_id(), None)
@@ -2684,19 +2663,19 @@ pub(crate) mod tests {
&alice_machine,
bob_machine.user_id(),
bob_machine.device_id(),
device_key_id,
key_id,
one_time_key.to_owned(),
)
.await;
for _ in 0..10 {
let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap();
let (key_id, one_time_key) = one_time_keys.pop_first().unwrap();
create_session(
&alice_machine,
bob_machine.user_id(),
bob_machine.device_id(),
device_key_id,
key_id,
one_time_key.to_owned(),
)
.await;
@@ -2749,7 +2728,7 @@ pub(crate) mod tests {
alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
let (_, content) = bob_device
.encrypt("m.dummy", serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap())
.encrypt("m.dummy", ToDeviceDummyEventContent::new())
.await
.expect("We should be able to encrypt a dummy event.");
@@ -2872,11 +2851,7 @@ pub(crate) mod tests {
})
.collect_vec();
if cfg!(feature = "backups_v1") {
assert_eq!(outgoing_to_device.len(), 4);
} else {
assert_eq!(outgoing_to_device.len(), 3);
}
assert_eq!(outgoing_to_device.len(), 4);
// The second time, as there are already in-flight requests, it should have no
// effect.
@@ -2893,25 +2868,21 @@ pub(crate) mod tests {
let should_query_secrets = alice.query_missing_secrets_from_other_sessions().await.unwrap();
if cfg!(feature = "backups_v1") {
assert!(should_query_secrets);
assert!(should_query_secrets);
let outgoing_to_device = alice
.outgoing_requests()
.await
.unwrap()
.into_iter()
.filter(|outgoing| match outgoing.request.as_ref() {
OutgoingRequests::ToDeviceRequest(request) => {
request.event_type.to_string() == "m.secret.request"
}
_ => false,
})
.collect_vec();
assert_eq!(outgoing_to_device.len(), 1);
} else {
assert!(!should_query_secrets);
}
let outgoing_to_device = alice
.outgoing_requests()
.await
.unwrap()
.into_iter()
.filter(|outgoing| match outgoing.request.as_ref() {
OutgoingRequests::ToDeviceRequest(request) => {
request.event_type.to_string() == "m.secret.request"
}
_ => false,
})
.collect_vec();
assert_eq!(outgoing_to_device.len(), 1);
// The second time, as there are already in-flight requests, it should have no
// effect.
@@ -3294,7 +3265,7 @@ pub(crate) mod tests {
);
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
.expect("Can't parse the keys upload response");
.expect("Can't parse the `/keys/upload` response");
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
@@ -3362,7 +3333,7 @@ pub(crate) mod tests {
);
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
.expect("Can't parse the keys upload response");
.expect("Can't parse the `/keys/upload` response");
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
@@ -3427,7 +3398,7 @@ pub(crate) mod tests {
);
let kq_response = KeyQueryResponse::try_from_http_response(response_from_file(&json))
.expect("Can't parse the keys upload response");
.expect("Can't parse the `/keys/upload` response");
alice.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
bob.receive_keys_query_response(&TransactionId::new(), &kq_response).await.unwrap();
@@ -3452,7 +3423,7 @@ pub(crate) mod tests {
let data = response_from_file(&test_json::KEYS_QUERY_TWO_DEVICES_ONE_SIGNED);
let response = get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response");
.expect("Can't parse the `/keys/upload` response");
let (device_change, identity_change) =
bob.receive_keys_query_response(&TransactionId::new(), &response).await.unwrap();
@@ -3533,7 +3504,7 @@ pub(crate) mod tests {
.expect("should exist")
.set_trust_state(crate::LocalTrust::Verified);
alice.create_outbound_group_session_with_defaults(room_id).await.unwrap();
alice.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
let plaintext = "It is a secret to everybody";
@@ -3750,7 +3721,7 @@ pub(crate) mod tests {
// Alice sends a key
let msgs = alice.inner.verification_machine.outgoing_messages();
assert!(msgs.len() == 1);
let msg = msgs.first().unwrap();
let msg = &msgs[0];
let event = outgoing_request_to_event(alice.user_id(), msg);
alice.inner.verification_machine.mark_request_as_sent(&msg.request_id);
@@ -3763,7 +3734,7 @@ pub(crate) mod tests {
// Now bob sends a key
let msgs = bob.inner.verification_machine.outgoing_messages();
assert!(msgs.len() == 1);
let msg = msgs.first().unwrap();
let msg = &msgs[0];
let event = outgoing_request_to_event(bob.user_id(), msg);
bob.inner.verification_machine.mark_request_as_sent(&msg.request_id);
@@ -3876,16 +3847,15 @@ pub(crate) mod tests {
let session_key = group_session.session_key();
let session_id = group_session.session_id();
let content = json!({
let content = message_like_event_content!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": room_id,
"session_id": session_id,
"session_key": session_key.to_base64(),
});
let encrypted_content =
alice.encrypt_room_event_raw(room_id, content, "m.room_key").await.unwrap();
alice.encrypt_room_event_raw(room_id, "m.room_key", &content).await.unwrap();
let event = json!({
"sender": alice.user_id(),
"content": encrypted_content,
@@ -3946,8 +3916,8 @@ pub(crate) mod tests {
let signing_keys = SigningKeys::from([(DeviceKeyAlgorithm::Ed25519, fake_key)]);
inbound.creator_info.signing_keys = signing_keys.into();
let content = json!({});
let content = outbound.encrypt(content, "m.dummy").await;
let content = message_like_event_content!({});
let content = outbound.encrypt("m.dummy", &content).await;
alice.store().save_inbound_group_sessions(&[inbound]).await.unwrap();
let event = json!({
@@ -4053,4 +4023,52 @@ pub(crate) mod tests {
"Our identity should not be verified when there's a mismatch in the cross-signing keys"
);
}
#[async_test]
async fn test_wait_on_key_query_doesnt_block_store() {
// Waiting for a key query shouldn't delay other write attempts to the store.
// This test will end immediately if it works, and times out after a few seconds
// if it failed.
let machine = OlmMachine::new(bob_id(), bob_device_id()).await;
// Mark Alice as a tracked user, so it gets into the groups of users for which
// we need to query keys.
machine.update_tracked_users([alice_id()]).await.unwrap();
// Start a background task that will wait for the key query to finish silently
// in the background.
let machine_cloned = machine.clone();
let wait = tokio::spawn(async move {
let machine = machine_cloned;
let user_devices =
machine.get_user_devices(alice_id(), Some(Duration::from_secs(10))).await.unwrap();
assert!(user_devices.devices().next().is_some());
});
// Let the background task work first.
tokio::task::yield_now().await;
// Create a key upload request and process it back immediately.
let requests = machine.bootstrap_cross_signing(false).await.unwrap();
let req = requests.upload_keys_req.expect("upload keys request should be there");
let response = keys_upload_response();
let mark_request_as_sent = machine.mark_request_as_sent(&req.request_id, &response);
tokio::time::timeout(Duration::from_secs(5), mark_request_as_sent)
.await
.expect("no timeout")
.expect("the underlying request has been marked as sent");
// Answer the key query, so the background task completes immediately?
let response = keys_query_response();
let key_queries = machine.inner.identity_manager.users_for_key_query().await.unwrap();
for (id, _) in key_queries {
machine.mark_request_as_sent(&id, &response).await.unwrap();
}
// The waiting should successfully complete.
wait.await.unwrap();
}
}
+79 -57
View File
@@ -74,6 +74,11 @@ use crate::{
OlmError, SignatureError,
};
#[derive(Debug)]
enum PrekeyBundle {
Olm3DH { key: SignedKey },
}
#[derive(Debug, Clone)]
pub(crate) enum SessionType {
New(Session),
@@ -245,7 +250,6 @@ impl StaticAccountData {
/// **Note**: Use this method with caution, the `canonical_json` needs to be
/// correctly canonicalized and make sure that the object you are checking
/// the signature for is allowed to be signed by our own device.
#[cfg(any(test, feature = "backups_v1"))]
pub fn has_signed_raw(
&self,
signatures: &crate::types::Signatures,
@@ -314,7 +318,7 @@ impl StaticAccountData {
pub struct Account {
pub(crate) static_data: StaticAccountData,
/// `vodozemac` account.
inner: InnerAccount,
inner: Box<InnerAccount>,
/// Is this account ready to encrypt messages? (i.e. has it shared keys with
/// a homeserver)
shared: bool,
@@ -397,7 +401,7 @@ impl Account {
identity_keys: Arc::new(identity_keys),
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
},
inner: account,
inner: Box::new(account),
shared: false,
uploaded_signed_key_count: 0,
}
@@ -601,7 +605,7 @@ impl Account {
.expect("We should be able to convert a freshly created Account into a libolm pickle");
let data = DehydratedDeviceData::V1(DehydratedDeviceV1::new(device_pickle));
Raw::from_json(to_raw_value(&data).expect("Coulnd't our dehydrated device data"))
Raw::from_json(to_raw_value(&data).expect("Couldn't serialize our dehydrated device data"))
}
pub(crate) async fn rehydrate(
@@ -644,7 +648,7 @@ impl Account {
identity_keys: Arc::new(identity_keys),
creation_local_time: pickle.creation_local_time,
},
inner: account,
inner: Box::new(account),
shared: pickle.shared,
uploaded_signed_key_count: pickle.uploaded_signed_key_count,
})
@@ -836,6 +840,43 @@ impl Account {
}
}
#[instrument(
skip_all,
fields(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
algorithms = ?device.algorithms()
)
)]
fn find_pre_key_bundle(
device: &ReadOnlyDevice,
key_map: &BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>>,
) -> Result<PrekeyBundle, SessionCreationError> {
let mut keys = key_map.iter();
let first_key = keys.next().ok_or_else(|| {
SessionCreationError::OneTimeKeyMissing(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
let first_key_id = first_key.0.to_owned();
let first_key = OneTimeKey::deserialize(first_key_id.algorithm(), first_key.1)?;
let result = match first_key {
OneTimeKey::SignedKey(key) => Ok(PrekeyBundle::Olm3DH { key }),
_ => Err(SessionCreationError::OneTimeKeyUnknown(
device.user_id().to_owned(),
device.device_id().into(),
)),
};
trace!(?result, "Finished searching for a valid pre-key bundle");
result
}
/// Create a new session with another account given a one-time key and a
/// device.
///
@@ -853,50 +894,37 @@ impl Account {
device: &ReadOnlyDevice,
key_map: &BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>>,
) -> Result<Session, SessionCreationError> {
let one_time_key = key_map.values().next().ok_or_else(|| {
SessionCreationError::OneTimeKeyMissing(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
let pre_key_bundle = Self::find_pre_key_bundle(device, key_map)?;
let one_time_key: SignedKey = match one_time_key.deserialize_as() {
Ok(OneTimeKey::SignedKey(k)) => k,
Ok(OneTimeKey::Key(_)) => {
return Err(SessionCreationError::OneTimeKeyNotSigned(
device.user_id().to_owned(),
device.device_id().into(),
));
match pre_key_bundle {
PrekeyBundle::Olm3DH { key } => {
device.verify_one_time_key(&key).map_err(|error| {
SessionCreationError::InvalidSignature {
signing_key: device.ed25519_key(),
one_time_key: key.clone(),
error,
}
})?;
let identity_key = device.curve25519_key().ok_or_else(|| {
SessionCreationError::DeviceMissingCurveKey(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
let is_fallback = key.fallback();
let one_time_key = key.key();
let config = device.olm_session_config();
Ok(self.create_outbound_session_helper(
config,
identity_key,
one_time_key,
is_fallback,
))
}
Ok(_) => {
return Err(SessionCreationError::OneTimeKeyUnknown(
device.user_id().to_owned(),
device.device_id().into(),
));
}
Err(e) => return Err(SessionCreationError::InvalidJson(e)),
};
device.verify_one_time_key(&one_time_key).map_err(|error| {
SessionCreationError::InvalidSignature {
signing_key: device.ed25519_key(),
one_time_key: one_time_key.clone(),
error,
}
})?;
let identity_key = device.curve25519_key().ok_or_else(|| {
SessionCreationError::DeviceMissingCurveKey(
device.user_id().to_owned(),
device.device_id().into(),
)
})?;
let is_fallback = one_time_key.fallback();
let one_time_key = one_time_key.key();
let config = device.olm_session_config();
Ok(self.create_outbound_session_helper(config, identity_key, one_time_key, is_fallback))
}
}
/// Create a new session with another account given a pre-key Olm message.
@@ -956,21 +984,15 @@ impl Account {
use ruma::events::dummy::ToDeviceDummyEventContent;
other.generate_one_time_keys_helper(1);
let one_time = other.signed_one_time_keys();
let one_time_map = other.signed_one_time_keys();
let device = ReadOnlyDevice::from_account(other);
let mut our_session = self.create_outbound_session(&device, &one_time).unwrap();
let mut our_session = self.create_outbound_session(&device, &one_time_map).unwrap();
other.mark_keys_as_published();
let message = our_session
.encrypt(
&device,
"m.dummy",
serde_json::to_value(ToDeviceDummyEventContent::new()).unwrap(),
None,
)
.encrypt(&device, "m.dummy", ToDeviceDummyEventContent::new(), None)
.await
.unwrap()
.deserialize()
@@ -1038,7 +1060,7 @@ impl Account {
self.decrypt_olm_helper(store, sender, content.sender_key, &content.ciphertext).await
}
#[instrument(skip_all, fields(sender, sender_key = %content.sender_key))]
#[instrument(skip_all, fields(sender, sender_key = ?content.sender_key))]
async fn decrypt_olm_v1(
&mut self,
store: &Store,
@@ -111,7 +111,7 @@ pub struct InboundGroupSession {
inner: Arc<Mutex<InnerSession>>,
/// A copy of [`InnerSession::session_id`] to avoid having to acquire a lock
/// to get to the sesison ID.
/// to get to the session ID.
session_id: Arc<str>,
/// A copy of [`InnerSession::first_known_index`] to avoid having to acquire
@@ -398,7 +398,6 @@ impl InboundGroupSession {
/// Export the inbound group session into a format that can be uploaded to
/// the server as a backup.
#[cfg(feature = "backups_v1")]
pub async fn to_backup(&self) -> BackedUpRoomKey {
self.export().await.into()
}
@@ -21,7 +21,7 @@ mod outbound;
pub use inbound::{InboundGroupSession, PickledInboundGroupSession};
pub(crate) use outbound::ShareState;
pub use outbound::{
EncryptionSettings, GroupSession, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo,
EncryptionSettings, OutboundGroupSession, PickledOutboundGroupSession, ShareInfo,
};
use thiserror::Error;
pub use vodozemac::megolm::{ExportedSessionKey, SessionKey};
@@ -97,6 +97,24 @@ pub struct ExportedRoomKey {
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
}
impl ExportedRoomKey {
pub(crate) fn from_backed_up_room_key(
room_id: OwnedRoomId,
session_id: String,
room_key: BackedUpRoomKey,
) -> Self {
Self {
algorithm: room_key.algorithm,
room_id,
sender_key: room_key.sender_key,
session_id,
session_key: room_key.session_key,
sender_claimed_keys: room_key.sender_claimed_keys,
forwarding_curve25519_key_chain: room_key.forwarding_curve25519_key_chain,
}
}
}
/// A backed up version of an `InboundGroupSession`
///
/// This can be used to backup the `InboundGroupSession` to the server.
@@ -24,13 +24,15 @@ use std::{
};
use ruma::{
events::room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
events::{
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
AnyMessageLikeEventContent,
},
serde::Raw,
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId,
SecondsSinceUnixEpoch, TransactionId, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::RwLock;
use tracing::{debug, error, info};
use vodozemac::{megolm::SessionConfig, Curve25519PublicKey};
@@ -54,7 +56,7 @@ use crate::{
},
EventEncryptionAlgorithm,
},
Device, ToDeviceRequest,
ReadOnlyDevice, ToDeviceRequest,
};
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
@@ -356,30 +358,37 @@ impl OutboundGroupSession {
///
/// # Arguments
///
/// * `content` - The plaintext content of the message that should be
/// encrypted in raw json [`Value`] form.
///
/// * `event_type` - The plaintext type of the event, the outer type of the
/// event will become `m.room.encrypted`.
///
/// * `content` - The plaintext content of the message that should be
/// encrypted in raw JSON form.
///
/// # Panics
///
/// Panics if the content can't be serialized.
pub async fn encrypt(
&self,
content: Value,
event_type: &str,
content: &Raw<AnyMessageLikeEventContent>,
) -> Raw<RoomEncryptedEventContent> {
let json_content = json!({
"content": content,
"room_id": &*self.room_id,
"type": event_type,
});
#[derive(Serialize)]
struct Payload<'a> {
#[serde(rename = "type")]
event_type: &'a str,
content: &'a Raw<AnyMessageLikeEventContent>,
room_id: &'a RoomId,
}
let plaintext = json_content.to_string();
let relates_to = content.get("m.relates_to").cloned();
let payload = Payload { event_type, content, room_id: &self.room_id };
let payload_json =
serde_json::to_string(&payload).expect("payload serialization never fails");
let ciphertext = self.encrypt_helper(plaintext).await;
let relates_to = content
.get_field::<serde_json::Value>("m.relates_to")
.expect("serde_json::Value deserialization with valid JSON input never fails");
let ciphertext = self.encrypt_helper(payload_json).await;
let scheme: RoomEventEncryptionScheme = match self.settings.algorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2 => MegolmV1AesSha2Content {
ciphertext,
@@ -495,7 +504,7 @@ impl OutboundGroupSession {
}
/// Has or will the session be shared with the given user/device pair.
pub(crate) fn is_shared_with(&self, device: &Device) -> ShareState {
pub(crate) fn is_shared_with(&self, device: &ReadOnlyDevice) -> ShareState {
// Check if we shared the session.
let shared_state =
self.shared_with_set.read().unwrap().get(device.user_id()).and_then(|d| {
@@ -539,7 +548,7 @@ impl OutboundGroupSession {
}
}
pub(crate) fn is_withheld_to(&self, device: &Device, code: &WithheldCode) -> bool {
pub(crate) fn is_withheld_to(&self, device: &ReadOnlyDevice, code: &WithheldCode) -> bool {
self.shared_with_set
.read()
.unwrap()
@@ -762,7 +771,7 @@ mod tests {
#[async_test]
#[cfg(any(target_os = "linux", target_os = "macos", target_arch = "wasm32"))]
async fn test_expiration() -> Result<(), MegolmError> {
use ruma::SecondsSinceUnixEpoch;
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() };
@@ -777,8 +786,8 @@ mod tests {
assert!(!session.expired());
let _ = session
.encrypt(
serde_json::to_value(RoomMessageEventContent::text_plain("Test message"))?,
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
.await;
assert!(session.expired());
@@ -810,8 +819,8 @@ mod tests {
let _ = session
.encrypt(
serde_json::to_value(RoomMessageEventContent::text_plain("Test message"))?,
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
.await;
assert!(session.expired());
+9 -7
View File
@@ -39,7 +39,7 @@ pub use vodozemac::{olm::IdentityKeys, Curve25519PublicKey};
#[cfg(test)]
pub(crate) mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test::{async_test, message_like_event_content};
use ruma::{
device_id, event_id,
events::{
@@ -47,7 +47,9 @@ pub(crate) mod tests {
room::message::{Relation, RoomMessageEventContent},
AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
},
room_id, user_id, DeviceId, UserId,
room_id,
serde::Raw,
user_id, DeviceId, UserId,
};
use serde_json::{json, Value};
use vodozemac::{
@@ -232,7 +234,7 @@ pub(crate) mod tests {
assert_eq!(outbound.session_id(), inbound.session_id());
let encrypted_content =
outbound.encrypt(serde_json::to_value(content).unwrap(), "m.room.message").await;
outbound.encrypt("m.room.message", &Raw::new(&content).unwrap().cast()).await;
let event = json!({
"sender": alice.user_id(),
@@ -271,10 +273,10 @@ pub(crate) mod tests {
// We first test that we're copying the relation from the content that
// will be encrypted to the content that will stay plaintext.
let content = json!({
let content = message_like_event_content!({
"m.relates_to": relation_json,
});
let encrypted = outbound.encrypt(content, "m.dummy").await;
let encrypted = outbound.encrypt("m.dummy", &content).await;
let event = json!({
"sender": alice.user_id(),
@@ -298,8 +300,8 @@ pub(crate) mod tests {
let relation = decrypted.get("content").and_then(|c| c.get("m.relates_to"));
assert_eq!(relation, Some(&relation_json), "The decrypted event should contain a relation");
let content = json!({});
let encrypted = outbound.encrypt(content, "m.dummy").await;
let content = message_like_event_content!({});
let encrypted = outbound.encrypt("m.dummy", &content).await;
let mut encrypted: Value = json_convert(&encrypted).unwrap();
encrypted.as_object_mut().unwrap().insert("m.relates_to".to_owned(), relation_json.clone());
+8 -8
View File
@@ -14,9 +14,9 @@
use std::{fmt, sync::Arc};
use ruma::{serde::Raw, JsOption, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
use ruma::{serde::Raw, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::json;
use tokio::sync::Mutex;
use tracing::{field::debug, instrument, trace, Span};
use vodozemac::{
@@ -73,7 +73,7 @@ impl fmt::Debug for Session {
impl Session {
/// Decrypt the given Olm message.
///
/// Returns the decrypted plaintext or an `DecryptionError` if decryption
/// Returns the decrypted plaintext or a [`DecryptionError`] if decryption
/// failed.
///
/// # Arguments
@@ -99,12 +99,12 @@ impl Session {
self.sender_key
}
/// Get the `SessionConfig` that this session is using.
/// Get the [`SessionConfig`] that this session is using.
pub async fn session_config(&self) -> SessionConfig {
self.inner.lock().await.session_config()
}
/// Get the `EventEncryptionAlgorithm` of t his `Session`.
/// Get the [`EventEncryptionAlgorithm`] of this [`Session`].
pub async fn algorithm(&self) -> EventEncryptionAlgorithm {
#[cfg(feature = "experimental-algorithms")]
if self.session_config().await.version() == 2 {
@@ -150,7 +150,7 @@ impl Session {
&mut self,
recipient_device: &ReadOnlyDevice,
event_type: &str,
content: Value,
content: impl Serialize,
message_id: Option<String>,
) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
let plaintext = {
@@ -181,14 +181,14 @@ impl Session {
ciphertext,
recipient_key: self.sender_key,
sender_key: self.our_identity_keys.curve25519,
message_id: JsOption::from_implicit_option(message_id),
message_id,
}
.into(),
#[cfg(feature = "experimental-algorithms")]
EventEncryptionAlgorithm::OlmV2Curve25519AesSha2 => OlmV2Curve25519AesSha2Content {
ciphertext,
sender_key: self.our_identity_keys.curve25519,
message_id: JsOption::from_implicit_option(message_id),
message_id,
}
.into(),
_ => unreachable!(),
@@ -51,6 +51,7 @@ pub struct Signing {
public_key: Ed25519PublicKey,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for Signing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Signing").field("public_key", &self.public_key.to_base64()).finish()
@@ -192,7 +192,6 @@ impl SignedJsonObject for CrossSigningKey {
}
}
#[cfg(feature = "backups_v1")]
impl SignedJsonObject for crate::types::MegolmV1AuthData {
fn signatures(&self) -> &Signatures {
&self.signatures
+6 -6
View File
@@ -199,9 +199,9 @@ impl KeysQueryRequest {
/// Enum over the different outgoing requests we can have.
#[derive(Debug)]
pub enum OutgoingRequests {
/// The keys upload request, uploading device and one-time keys.
/// The `/keys/upload` request, uploading device and one-time keys.
KeysUpload(KeysUploadRequest),
/// The keys query request, fetching the device and cross signing keys of
/// The `/keys/query` request, fetching the device and cross signing keys of
/// other users.
KeysQuery(KeysQueryRequest),
/// The request to claim one-time keys for a user/device pair from the
@@ -292,18 +292,18 @@ impl From<KeysUploadRequest> for OutgoingRequest {
/// Enum over all the incoming responses we need to receive.
#[derive(Debug)]
pub enum IncomingResponse<'a> {
/// The keys upload response, notifying us about the amount of uploaded
/// The `/keys/upload` response, notifying us about the amount of uploaded
/// one-time keys.
KeysUpload(&'a KeysUploadResponse),
/// The keys query response, giving us the device and cross signing keys of
/// other users.
/// The `/keys/query` response, giving us the device and cross signing keys
/// of other users.
KeysQuery(&'a KeysQueryResponse),
/// The to-device response, an empty response.
ToDevice(&'a ToDeviceResponse),
/// The key claiming requests, giving us new one-time keys of other users so
/// new Olm sessions can be created.
KeysClaim(&'a KeysClaimResponse),
/// The cross signing keys upload response, marking our private cross
/// The cross signing `/keys/upload` response, marking our private cross
/// signing identity as shared.
SigningKeysUpload(&'a SigningKeysUploadResponse),
/// The cross signing signature upload response.
@@ -23,19 +23,21 @@ use futures_util::future::join_all;
use itertools::{Either, Itertools};
use matrix_sdk_common::executor::spawn;
use ruma::{
events::ToDeviceEventType, serde::Raw, to_device::DeviceIdOrAllDevices, DeviceId,
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
events::{AnyMessageLikeEventContent, ToDeviceEventType},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId,
UserId,
};
use serde_json::Value;
use tracing::{debug, error, info, instrument, trace};
use crate::{
error::{EventError, MegolmResult, OlmResult},
identities::device::MaybeEncryptedRoomKey,
olm::{InboundGroupSession, OutboundGroupSession, Session, ShareInfo, ShareState},
store::{Changes, Result as StoreResult, Store},
store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store},
types::events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode},
Device, EncryptionSettings, OlmError, ToDeviceRequest,
EncryptionSettings, OlmError, ReadOnlyDevice, ToDeviceRequest,
};
#[derive(Clone, Debug)]
@@ -100,17 +102,17 @@ impl GroupSessionCache {
self.sessions.read().unwrap().get(room_id).cloned()
}
/// Get or load the session for the given room with the given session id.
///
/// This is the same as [get_or_load()](#method.get_or_load) but it will
/// filter out the session if it doesn't match the given session id.
#[cfg(feature = "automatic-room-key-forwarding")]
pub async fn get_with_id(
&self,
room_id: &RoomId,
session_id: &str,
) -> Option<OutboundGroupSession> {
self.get_or_load(room_id).await.filter(|o| session_id == o.session_id())
/// Returns whether any session is withheld with the given device and code.
fn has_session_withheld_to(&self, device: &ReadOnlyDevice, code: &WithheldCode) -> bool {
self.sessions.read().unwrap().values().any(|s| s.is_withheld_to(device, code))
}
fn remove_from_being_shared(&self, id: &TransactionId) -> Option<OutboundGroupSession> {
self.sessions_being_shared.write().unwrap().remove(id)
}
fn mark_as_being_shared(&self, id: OwnedTransactionId, session: OutboundGroupSession) {
self.sessions_being_shared.write().unwrap().insert(id, session);
}
}
@@ -121,18 +123,18 @@ impl GroupSessionCache {
/// (`devices`) or not the session, including withheld reason
/// `withheld_devices`.
#[derive(Debug)]
pub struct CollectRecipientsResult {
pub(crate) struct CollectRecipientsResult {
/// If true the outbound group session should be rotated
pub should_rotate: bool,
/// The map of user|device that should receive the session
pub devices: BTreeMap<OwnedUserId, Vec<Device>>,
pub devices: BTreeMap<OwnedUserId, Vec<ReadOnlyDevice>>,
/// The map of user|device that won't receive the key with the withheld
/// code.
pub withheld_devices: Vec<(Device, WithheldCode)>,
pub withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)>,
}
#[derive(Debug, Clone)]
pub struct GroupSessionManager {
pub(crate) struct GroupSessionManager {
/// Store for the encryption keys.
/// Persists all the encryption keys so a client can resume the session
/// without the need to create new keys.
@@ -144,7 +146,7 @@ pub struct GroupSessionManager {
impl GroupSessionManager {
const MAX_TO_DEVICE_MESSAGES: usize = 250;
pub(crate) fn new(store: Store) -> Self {
pub fn new(store: Store) -> Self {
Self { store: store.clone(), sessions: GroupSessionCache::new(store) }
}
@@ -163,35 +165,33 @@ impl GroupSessionManager {
}
pub async fn mark_request_as_sent(&self, request_id: &TransactionId) -> StoreResult<()> {
let removed_session =
self.sessions.sessions_being_shared.write().unwrap().remove(request_id);
if let Some(session) = removed_session {
let no_olm = session.mark_request_as_sent(request_id);
let Some(session) = self.sessions.remove_from_being_shared(request_id) else {
return Ok(());
};
let mut changes = Changes::default();
let no_olm = session.mark_request_as_sent(request_id);
for (user_id, devices) in &no_olm {
for device_id in devices {
let device = self.store.get_device(user_id, device_id).await;
let mut changes = Changes::default();
if let Ok(Some(device)) = device {
device.mark_withheld_code_as_sent();
changes.devices.changed.push(device.inner.clone());
} else {
error!(
?request_id,
"Marking to-device no olm as sent but device not found, might \
for (user_id, devices) in &no_olm {
for device_id in devices {
let device = self.store.get_device(user_id, device_id).await;
if let Ok(Some(device)) = device {
device.mark_withheld_code_as_sent();
changes.devices.changed.push(device.inner.clone());
} else {
error!(
?request_id,
"Marking to-device no olm as sent but device not found, might \
have been deleted?"
);
}
);
}
}
changes.outbound_group_sessions.push(session.clone());
self.store.save_changes(changes).await?;
}
Ok(())
changes.outbound_group_sessions.push(session.clone());
self.store.save_changes(changes).await
}
#[cfg(test)]
@@ -202,15 +202,15 @@ impl GroupSessionManager {
pub async fn encrypt(
&self,
room_id: &RoomId,
content: Value,
event_type: &str,
content: &Raw<AnyMessageLikeEventContent>,
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
let session =
self.sessions.get_or_load(room_id).await.expect("Session wasn't created nor shared");
assert!(!session.expired(), "Session expired");
let content = session.encrypt(content, event_type).await;
let content = session.encrypt(event_type, content).await;
let mut changes = Changes::default();
changes.outbound_group_sessions.push(session);
@@ -221,8 +221,7 @@ impl GroupSessionManager {
/// Create a new outbound group session.
///
/// This also creates a matching inbound group session and saves that one in
/// the store.
/// This also creates a matching inbound group session.
pub async fn create_outbound_group_session(
&self,
room_id: &RoomId,
@@ -264,18 +263,19 @@ impl GroupSessionManager {
/// Encrypt the given content for the given devices and create a to-device
/// requests that sends the encrypted content to them.
async fn encrypt_session_for(
store: Arc<CryptoStoreWrapper>,
group_session: OutboundGroupSession,
devices: Vec<Device>,
devices: Vec<ReadOnlyDevice>,
) -> OlmResult<(
OwnedTransactionId,
ToDeviceRequest,
BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, ShareInfo>>,
Vec<Session>,
Vec<(Device, WithheldCode)>,
Vec<(ReadOnlyDevice, WithheldCode)>,
)> {
// Use a named type instead of a tuple with rather long type name
pub struct DeviceResult {
device: Device,
device: ReadOnlyDevice,
maybe_encrypted_room_key: MaybeEncryptedRoomKey,
}
@@ -284,14 +284,20 @@ impl GroupSessionManager {
let mut share_infos = BTreeMap::new();
let mut withheld_devices = Vec::new();
let encrypt = |device: Device, session: OutboundGroupSession| async move {
let encryption_result = device.maybe_encrypt_room_key(session).await?;
// XXX is there a way to do this that doesn't involve cloning the
// `Arc<CryptoStoreWrapper>` for each device?
let encrypt = |store: Arc<CryptoStoreWrapper>,
device: ReadOnlyDevice,
session: OutboundGroupSession| async move {
let encryption_result = device.maybe_encrypt_room_key(store.as_ref(), session).await?;
Ok::<_, OlmError>(DeviceResult { device, maybe_encrypted_room_key: encryption_result })
};
let tasks: Vec<_> =
devices.iter().map(|d| spawn(encrypt(d.clone(), group_session.clone()))).collect();
let tasks: Vec<_> = devices
.iter()
.map(|d| spawn(encrypt(store.clone(), d.clone(), group_session.clone())))
.collect();
let results = join_all(tasks).await;
@@ -344,8 +350,8 @@ impl GroupSessionManager {
outbound: &OutboundGroupSession,
) -> OlmResult<CollectRecipientsResult> {
let users: BTreeSet<&UserId> = users.collect();
let mut devices: BTreeMap<OwnedUserId, Vec<Device>> = Default::default();
let mut withheld_devices: Vec<(Device, WithheldCode)> = Default::default();
let mut devices: BTreeMap<OwnedUserId, Vec<ReadOnlyDevice>> = Default::default();
let mut withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)> = Default::default();
trace!(?users, ?settings, "Calculating group session recipients");
@@ -373,23 +379,37 @@ impl GroupSessionManager {
// This is calculated in the following code and stored in this variable.
let mut should_rotate = user_left || visibility_changed || algorithm_changed;
let own_identity =
self.store.get_user_identity(self.store.user_id()).await?.and_then(|i| i.into_own());
for user_id in users {
let user_devices = self.store.get_user_devices_filtered(user_id).await?;
let user_devices = self.store.get_readonly_devices_filtered(user_id).await?;
// We only need the user identity if settings.only_allow_trusted_devices is set.
let device_owner_identity = if settings.only_allow_trusted_devices {
self.store.get_user_identity(user_id).await?
} else {
None
};
// From all the devices a user has, we're splitting them into two
// buckets, a bucket of devices that should receive the
// room key and a bucket of devices that should receive
// a withheld code.
let (recipients, withheld_recipients): (Vec<Device>, Vec<(Device, WithheldCode)>) =
user_devices.devices().partition_map(|d| {
if d.is_blacklisted() {
Either::Right((d, WithheldCode::Blacklisted))
} else if settings.only_allow_trusted_devices && !d.is_verified() {
Either::Right((d, WithheldCode::Unverified))
} else {
Either::Left(d)
}
});
let (recipients, withheld_recipients): (
Vec<ReadOnlyDevice>,
Vec<(ReadOnlyDevice, WithheldCode)>,
) = user_devices.into_values().partition_map(|d| {
if d.is_blacklisted() {
Either::Right((d, WithheldCode::Blacklisted))
} else if settings.only_allow_trusted_devices
&& !d.is_verified(&own_identity, &device_owner_identity)
{
Either::Right((d, WithheldCode::Unverified))
} else {
Either::Left(d)
}
});
// If we haven't already concluded that the session should be
// rotated for other reasons, we also need to check whether any
@@ -428,13 +448,14 @@ impl GroupSessionManager {
Ok(CollectRecipientsResult { should_rotate, devices, withheld_devices })
}
pub async fn encrypt_request(
chunk: Vec<Device>,
async fn encrypt_request(
store: Arc<CryptoStoreWrapper>,
chunk: Vec<ReadOnlyDevice>,
outbound: OutboundGroupSession,
being_shared: Arc<StdRwLock<BTreeMap<OwnedTransactionId, OutboundGroupSession>>>,
) -> OlmResult<(Vec<Session>, Vec<(Device, WithheldCode)>)> {
sessions: GroupSessionCache,
) -> OlmResult<(Vec<Session>, Vec<(ReadOnlyDevice, WithheldCode)>)> {
let (id, request, share_infos, used_sessions, no_olm) =
Self::encrypt_session_for(outbound.clone(), chunk).await?;
Self::encrypt_session_for(store, outbound.clone(), chunk).await?;
if !request.messages.is_empty() {
trace!(
@@ -444,7 +465,7 @@ impl GroupSessionManager {
);
outbound.add_request(id.clone(), request.into(), share_infos);
being_shared.write().unwrap().insert(id, outbound.clone());
sessions.mark_as_being_shared(id, outbound.clone());
}
Ok((used_sessions, no_olm))
@@ -486,10 +507,10 @@ impl GroupSessionManager {
async fn encrypt_for_devices(
&self,
recipient_devices: Vec<Device>,
recipient_devices: Vec<ReadOnlyDevice>,
group_session: &OutboundGroupSession,
changes: &mut Changes,
) -> OlmResult<Vec<(Device, WithheldCode)>> {
) -> OlmResult<Vec<(ReadOnlyDevice, WithheldCode)>> {
// If we have some recipients, log them here.
if !recipient_devices.is_empty() {
#[allow(unknown_lints, clippy::unwrap_or_default)] // false positive
@@ -507,7 +528,7 @@ impl GroupSessionManager {
info!(
?recipients,
message_index,
room_id = %group_session.room_id(),
room_id = ?group_session.room_id(),
session_id = group_session.session_id(),
"Trying to encrypt a room key",
);
@@ -521,9 +542,10 @@ impl GroupSessionManager {
.chunks(Self::MAX_TO_DEVICE_MESSAGES)
.map(|chunk| {
spawn(Self::encrypt_request(
self.store.crypto_store(),
chunk.to_vec(),
group_session.clone(),
self.sessions.sessions_being_shared.clone(),
self.sessions.clone(),
))
})
.collect();
@@ -549,7 +571,7 @@ impl GroupSessionManager {
fn is_withheld_to(
&self,
group_session: &OutboundGroupSession,
device: &Device,
device: &ReadOnlyDevice,
code: &WithheldCode,
) -> bool {
// The `m.no_olm` withheld code is special because it is supposed to be sent
@@ -570,14 +592,7 @@ impl GroupSessionManager {
// `OutboundGroupSession` and the `Device` both interact with the flag we'll
// leave it be.
if code == &WithheldCode::NoOlm {
device.was_withheld_code_sent()
|| self
.sessions
.sessions
.read()
.unwrap()
.values()
.any(|s| s.is_withheld_to(device, code))
device.was_withheld_code_sent() || self.sessions.has_session_withheld_to(device, code)
} else {
group_session.is_withheld_to(device, code)
}
@@ -586,7 +601,7 @@ impl GroupSessionManager {
async fn handle_withheld_devices(
&self,
group_session: &OutboundGroupSession,
withheld_devices: Vec<(Device, WithheldCode)>,
withheld_devices: Vec<(ReadOnlyDevice, WithheldCode)>,
) -> OlmResult<()> {
// Convert a withheld code for the group session into a to-device event content.
let to_content = |code| {
@@ -601,7 +616,7 @@ impl GroupSessionManager {
let mut share_infos = BTreeMap::new();
for (device, code) in chunk {
let device: Device = device;
let device: ReadOnlyDevice = device;
let code: WithheldCode = code;
let user_id = device.user_id().to_owned();
@@ -640,13 +655,12 @@ impl GroupSessionManager {
.map(chunk_to_request)
.collect();
let mut sessions_being_shared = self.sessions.sessions_being_shared.write().unwrap();
for (request, share_info) in result {
if !request.messages.is_empty() {
let txn_id = request.txn_id.to_owned();
group_session.add_request(txn_id.to_owned(), request.into(), share_info);
sessions_being_shared.insert(txn_id, group_session.clone());
self.sessions.mark_as_being_shared(txn_id, group_session.clone());
}
}
@@ -764,7 +778,7 @@ impl GroupSessionManager {
// Filter out the devices that already received this room key or have a
// to-device message already queued up.
let devices: Vec<Device> = devices
let devices: Vec<_> = devices
.into_iter()
.flat_map(|(_, d)| {
d.into_iter()
@@ -794,10 +808,7 @@ impl GroupSessionManager {
if requests.is_empty() {
if !outbound.shared() {
debug!(
room_id = room_id.as_str(),
"The room key doesn't need to be shared with anyone. Marking as shared."
);
debug!("The room key doesn't need to be shared with anyone. Marking as shared.");
outbound.mark_as_shared();
changes.outbound_group_sessions.push(outbound.clone());
@@ -824,7 +835,7 @@ impl GroupSessionManager {
#[cfg(test)]
mod tests {
use std::{collections::BTreeSet, ops::Deref, sync::Arc};
use std::{collections::BTreeSet, iter, ops::Deref, sync::Arc};
use matrix_sdk_test::{async_test, response_from_file};
use ruma::{
@@ -862,12 +873,13 @@ mod tests {
device_id!("JLAFKJWSCS")
}
/// Returns a /keys/query response for user "@example:localhost"
fn keys_query_response() -> get_keys::v3::Response {
let data = include_bytes!("../../../../benchmarks/benches/crypto_bench/keys_query.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
fn bob_keys_query_response() -> get_keys::v3::Response {
@@ -898,9 +910,11 @@ mod tests {
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
.expect("Can't parse the `/keys/upload` response")
}
/// Returns a keys claim response for device `BOBDEVICE` of user
/// `@bob:localhost`.
fn bob_one_time_key() -> claim_keys::v3::Response {
let data = json!({
"failures": {},
@@ -925,6 +939,8 @@ mod tests {
.expect("Can't parse the keys claim response")
}
/// Returns a key claim response for device `NMMBNBUSNR` of user
/// `@example2:localhost`
fn keys_claim_response() -> claim_keys::v3::Response {
let data = include_bytes!("../../../../benchmarks/benches/crypto_bench/keys_claim.json");
let data: Value = serde_json::from_slice(data).unwrap();
@@ -935,14 +951,27 @@ mod tests {
async fn machine_with_user_test_helper(user_id: &UserId, device_id: &DeviceId) -> OlmMachine {
let keys_query = keys_query_response();
let keys_claim = keys_claim_response();
let txn_id = TransactionId::new();
let machine = OlmMachine::new(user_id, device_id).await;
// complete a /keys/query and /keys/claim for @example:localhost
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let (txn_id, _keys_claim_request) = machine
.get_missing_sessions(iter::once(user_id!("@example:localhost")))
.await
.unwrap()
.unwrap();
let keys_claim = keys_claim_response();
machine.mark_request_as_sent(&txn_id, &keys_claim).await.unwrap();
// complete a /keys/query and /keys/claim for @bob:localhost
machine.mark_request_as_sent(&txn_id, &bob_keys_query_response()).await.unwrap();
let (txn_id, _keys_claim_request) = machine
.get_missing_sessions(iter::once(user_id!("@bob:localhost")))
.await
.unwrap()
.unwrap();
machine.mark_request_as_sent(&txn_id, &bob_one_time_key()).await.unwrap();
machine
@@ -1158,7 +1187,7 @@ mod tests {
}
#[async_test]
async fn key_recipient_collecting() {
async fn test_key_recipient_collecting() {
// The user id comes from the fact that the keys_query.json file uses
// this one.
let user_id = user_id!("@example:localhost");
@@ -13,36 +13,47 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, BTreeSet},
sync::{Arc, RwLock as StdRwLock},
time::Duration,
};
use matrix_sdk_common::failures_cache::FailuresCache;
use ruma::{
api::client::keys::claim_keys::v3::{
Request as KeysClaimRequest, Response as KeysClaimResponse,
},
assign,
events::dummy::ToDeviceDummyEventContent,
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedServerName, OwnedTransactionId, OwnedUserId,
SecondsSinceUnixEpoch, ServerName, TransactionId, UserId,
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedServerName,
OwnedTransactionId, OwnedUserId, SecondsSinceUnixEpoch, ServerName, TransactionId, UserId,
};
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info, instrument, warn};
use vodozemac::Curve25519PublicKey;
use crate::{
error::OlmResult,
gossiping::GossipMachine,
requests::{OutgoingRequest, ToDeviceRequest},
store::{Changes, Result as StoreResult, Store, UserKeyQueryResult},
store::{Changes, Result as StoreResult, Store},
types::{events::EventType, EventEncryptionAlgorithm},
utilities::FailuresCache,
ReadOnlyDevice,
};
#[derive(Debug, Clone)]
pub(crate) struct SessionManager {
store: Store,
/// If there is an active /keys/claim request, its details.
///
/// This is used when processing the response, so that we can spot missing
/// users/devices.
///
/// According to the doc on [`crate::OlmMachine::get_missing_sessions`],
/// there should only be one such request active at a time, so we only need
/// to keep a record of the most recent.
current_key_claim_request: Arc<StdRwLock<Option<(OwnedTransactionId, KeysClaimRequest)>>>,
/// A map of user/devices that we need to automatically claim keys for.
/// Submodules can insert user/device pairs into this map and the
/// user/device paris will be added to the list of users when
@@ -51,14 +62,19 @@ pub(crate) struct SessionManager {
wedged_devices: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
key_request_machine: GossipMachine,
outgoing_to_device_requests: Arc<StdRwLock<BTreeMap<OwnedTransactionId, OutgoingRequest>>>,
/// Servers that have previously appeared in the `failures` section of a
/// `/keys/claim` response.
///
/// See also [`crate::identities::IdentityManager::failures`].
failures: FailuresCache<OwnedServerName>,
failed_devices: Arc<StdRwLock<BTreeMap<OwnedUserId, FailuresCache<OwnedDeviceId>>>>,
}
impl SessionManager {
const KEY_CLAIM_TIMEOUT: Duration = Duration::from_secs(10);
const UNWEDGING_INTERVAL: Duration = Duration::from_secs(60 * 60);
const KEYS_QUERY_WAIT_TIME: Duration = Duration::from_secs(5);
pub fn new(
users_for_key_claim: Arc<StdRwLock<BTreeMap<OwnedUserId, BTreeSet<OwnedDeviceId>>>>,
@@ -67,6 +83,7 @@ impl SessionManager {
) -> Self {
Self {
store,
current_key_claim_request: Default::default(),
key_request_machine,
users_for_key_claim,
wedged_devices: Default::default(),
@@ -93,7 +110,7 @@ impl SessionManager {
let mut sessions = sessions.lock().await;
sessions.sort_by_key(|s| s.creation_time);
let session = sessions.get(0);
let session = sessions.first();
if let Some(session) = session {
info!(sender_key = ?curve_key, "Marking session to be unwedged");
@@ -148,8 +165,8 @@ impl SessionManager {
.is_some_and(|d| d.remove(device_id))
{
if let Some(device) = self.store.get_device(user_id, device_id).await? {
let content = serde_json::to_value(ToDeviceDummyEventContent::new())?;
let (_, content) = device.encrypt("m.dummy", content).await?;
let (_, content) =
device.encrypt("m.dummy", ToDeviceDummyEventContent::new()).await?;
let request = ToDeviceRequest::new(
device.user_id(),
@@ -173,35 +190,6 @@ impl SessionManager {
Ok(())
}
async fn get_user_devices(
&self,
user_id: &UserId,
) -> StoreResult<HashMap<OwnedDeviceId, ReadOnlyDevice>> {
use UserKeyQueryResult::*;
let user_devices = self.store.get_readonly_devices_filtered(user_id).await?;
let user_devices = if user_devices.is_empty() {
let cache = self.store.cache().await?;
match self
.key_request_machine
.identity_manager()
.key_query_manager
.synced(&cache)
.await?
.wait_if_user_key_query_pending(Self::KEYS_QUERY_WAIT_TIME, user_id)
.await
{
WasPending => self.store.get_readonly_devices_filtered(user_id).await?,
_ => user_devices,
}
} else {
user_devices
};
Ok(user_devices)
}
/// Get a key claiming request for the user/device pairs that we are
/// missing Olm sessions for.
///
@@ -233,23 +221,35 @@ impl SessionManager {
&self,
users: impl Iterator<Item = &UserId>,
) -> StoreResult<Option<(OwnedTransactionId, KeysClaimRequest)>> {
let mut missing: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
let mut timed_out: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
let mut missing_session_devices_by_user: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
let mut timed_out_devices_by_user: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
// Add the list of devices that the user wishes to establish sessions
// right now.
for user_id in users.filter(|u| !self.failures.contains(u.server_name())) {
let user_devices = self.get_user_devices(user_id).await?;
let unfailed_users = users.filter(|u| !self.failures.contains(u.server_name()));
// Get the current list of devices for each user.
let devices_by_user = Box::pin(
self.key_request_machine
.identity_manager()
.get_user_devices_for_encryption(unfailed_users),
)
.await?;
#[derive(Debug, Default)]
struct UserFailedDeviceInfo {
non_olm_devices: BTreeMap<OwnedDeviceId, Vec<EventEncryptionAlgorithm>>,
bad_key_devices: BTreeSet<OwnedDeviceId>,
}
let mut failed_devices_by_user: BTreeMap<_, UserFailedDeviceInfo> = BTreeMap::new();
for (user_id, user_devices) in devices_by_user {
for (device_id, device) in user_devices {
if !(device.supports_olm()) {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
algorithms = ?device.algorithms(),
"Device doesn't support any of our 1-to-1 E2EE \
algorithms, can't establish an Olm session"
);
if !device.supports_olm() {
failed_devices_by_user
.entry(user_id.clone())
.or_default()
.non_olm_devices
.insert(device_id, Vec::from(device.algorithms()));
} else if let Some(sender_key) = device.curve25519_key() {
let sessions = self.store.get_sessions(&sender_key.to_base64()).await?;
@@ -259,23 +259,25 @@ impl SessionManager {
true
};
let is_timed_out = self.is_user_timed_out(user_id, &device_id);
let is_timed_out = self.is_user_timed_out(&user_id, &device_id);
if is_missing && is_timed_out {
timed_out.entry(user_id.to_owned()).or_default().insert(device_id);
timed_out_devices_by_user
.entry(user_id.to_owned())
.or_default()
.insert(device_id);
} else if is_missing && !is_timed_out {
missing
missing_session_devices_by_user
.entry(user_id.to_owned())
.or_default()
.insert(device_id, DeviceKeyAlgorithm::SignedCurve25519);
}
} else {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"Device doesn't have a valid Curve25519 key, \
can't establish an Olm session"
);
failed_devices_by_user
.entry(user_id.clone())
.or_default()
.bad_key_devices
.insert(device_id);
}
}
}
@@ -283,43 +285,167 @@ impl SessionManager {
// Add the list of sessions that for some reason automatically need to
// create an Olm session.
for (user, device_ids) in self.users_for_key_claim.read().unwrap().iter() {
for device_id in device_ids {
missing
.entry(user.to_owned())
.or_default()
.insert(device_id.to_owned(), DeviceKeyAlgorithm::SignedCurve25519);
}
missing_session_devices_by_user.entry(user.to_owned()).or_default().extend(
device_ids
.iter()
.map(|device_id| (device_id.clone(), DeviceKeyAlgorithm::SignedCurve25519)),
);
}
if missing.is_empty() {
Ok(None)
} else {
if tracing::level_enabled!(tracing::Level::DEBUG) {
// Reformat the map to skip the encryption algorithm, which isn't very useful.
let missing_session_devices_by_user = missing_session_devices_by_user
.iter()
.map(|(user_id, devices)| (user_id, devices.keys().collect::<BTreeSet<_>>()))
.collect::<BTreeMap<_, _>>();
debug!(
?missing,
?timed_out,
?missing_session_devices_by_user,
?timed_out_devices_by_user,
"Collected user/device pairs that are missing an Olm session"
);
}
Ok(Some((
if !failed_devices_by_user.is_empty() {
warn!(
?failed_devices_by_user,
"Can't establish an Olm session with some devices due to missing Olm support or bad keys",
);
}
let result = if missing_session_devices_by_user.is_empty() {
None
} else {
Some((
TransactionId::new(),
assign!(KeysClaimRequest::new(missing), {
assign!(KeysClaimRequest::new(missing_session_devices_by_user), {
timeout: Some(Self::KEY_CLAIM_TIMEOUT),
}),
)))
}
))
};
// stash the details of the request so that we can refer to it when handling the
// response
*(self.current_key_claim_request.write().unwrap()) = result.clone();
Ok(result)
}
fn is_user_timed_out(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
self.failed_devices.read().unwrap().get(user_id).is_some_and(|d| d.contains(device_id))
}
/// This method will try to figure out for which devices a one-time key was
/// requested but is not present in the response.
///
/// As per [spec], if a user/device pair does not have any one-time keys on
/// the homeserver, the server will just omit the user/device pair from
/// the response:
///
/// > If the homeserver could be reached, but the user or device was
/// > unknown, no failure is recorded. Instead, the corresponding user
/// > or device is missing from the one_time_keys result.
///
/// The user/device pairs which are missing from the response are going to
/// be put in the failures cache so we don't retry to claim a one-time
/// key right away next time the user tries to send a message.
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#post_matrixclientv3keysclaim
fn handle_otk_exhaustion_failure(
&self,
request_id: &TransactionId,
failed_servers: &BTreeSet<OwnedServerName>,
one_time_keys: &BTreeMap<
&OwnedUserId,
BTreeMap<&OwnedDeviceId, BTreeSet<&OwnedDeviceKeyId>>,
>,
) {
// First check that the response is for the request we were expecting.
let request = {
let mut guard = self.current_key_claim_request.write().unwrap();
let expected_request_id = guard.as_ref().map(|e| e.0.as_ref());
if Some(request_id) == expected_request_id {
// We have a confirmed match. Clear the expectation, but hang onto the details
// of the request.
guard.take().map(|(_, request)| request)
} else {
warn!(
?request_id,
?expected_request_id,
"Received a `/keys/claim` response for the wrong request"
);
None
}
};
// If we were able to pair this response with a request, look for devices that
// were present in the request but did not elicit a successful response.
if let Some(request) = request {
let devices_in_response: BTreeSet<_> = one_time_keys
.iter()
.flat_map(|(user_id, device_key_map)| {
device_key_map
.keys()
.map(|device_id| (*user_id, *device_id))
.collect::<BTreeSet<_>>()
})
.collect();
let devices_in_request: BTreeSet<(_, _)> = request
.one_time_keys
.iter()
.flat_map(|(user_id, device_key_map)| {
device_key_map
.keys()
.map(|device_id| (user_id, device_id))
.collect::<BTreeSet<_>>()
})
.collect();
let missing_devices: BTreeSet<_> = devices_in_request
.difference(&devices_in_response)
.filter(|(user_id, _)| {
// Skip over users whose homeservers were in the "failed servers" list: we don't
// want to mark individual devices as broken *as well as* the server.
!failed_servers.contains(user_id.server_name())
})
.collect();
if !missing_devices.is_empty() {
let mut missing_devices_by_user: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
for &(user_id, device_id) in missing_devices {
missing_devices_by_user.entry(user_id).or_default().insert(device_id.clone());
}
warn!(
?missing_devices_by_user,
"Tried to create new Olm sessions, but the signed one-time key was missing for some devices",
);
let mut failed_devices_lock = self.failed_devices.write().unwrap();
for (user_id, device_set) in missing_devices_by_user {
failed_devices_lock.entry(user_id.clone()).or_default().extend(device_set);
}
}
};
}
/// Receive a successful key claim response and create new Olm sessions with
/// the claimed keys.
///
/// # Arguments
///
/// * `request_id` - The unique id of the request that was sent out. This is
/// needed to couple the response with the sent out request.
///
/// * `response` - The response containing the claimed one-time keys.
pub async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
#[instrument(skip(self, response))]
pub async fn receive_keys_claim_response(
&self,
request_id: &TransactionId,
response: &KeysClaimResponse,
) -> OlmResult<()> {
// Collect the (user_id, device_id, device_key_id) triple for logging reasons.
let one_time_keys: BTreeMap<_, BTreeMap<_, BTreeSet<_>>> = response
.one_time_keys
@@ -337,24 +463,43 @@ impl SessionManager {
})
.collect();
debug!(?one_time_keys, failures = ?response.failures, "Received a `/keys/claim` response");
debug!(?request_id, ?one_time_keys, failures = ?response.failures, "Received a `/keys/claim` response");
let failed_servers = response
// Collect all the servers in the `failures` field of the response.
let failed_servers: BTreeSet<_> = response
.failures
.keys()
.filter_map(|s| ServerName::parse(s).ok())
.filter(|s| s != self.store.static_account().user_id.server_name());
.filter(|s| s != self.store.static_account().user_id.server_name())
.collect();
let successful_servers = response.one_time_keys.keys().map(|u| u.server_name());
// Add the user/device pairs that don't have any one-time keys to the failures
// cache.
self.handle_otk_exhaustion_failure(request_id, &failed_servers, &one_time_keys);
// Add the failed servers to the failures cache.
self.failures.extend(failed_servers);
// Remove the servers we successfully contacted from the failures cache.
self.failures.remove(successful_servers);
// Finally, create some 1-to-1 sessions.
self.create_sessions(response).await
}
/// Create new Olm sessions for the requested devices.
///
/// # Arguments
///
/// * `device_map` - a map from (user ID, device ID) pairs to key object,
/// for each device we should create a session for.
pub(crate) async fn create_sessions(&self, response: &KeysClaimResponse) -> OlmResult<()> {
struct SessionInfo {
session_id: String,
algorithm: EventEncryptionAlgorithm,
fallback_key_used: bool,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for SessionInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
@@ -367,26 +512,23 @@ impl SessionManager {
let mut changes = Changes::default();
let mut new_sessions: BTreeMap<&UserId, BTreeMap<&DeviceId, SessionInfo>> = BTreeMap::new();
let mut store_transaction = self.store.transaction().await;
for (user_id, user_devices) in &response.one_time_keys {
for (device_id, key_map) in user_devices {
let device = match self.store.get_readonly_device(user_id, device_id).await {
Ok(Some(d)) => d,
Ok(None) => {
warn!(
user_id = user_id.as_str(),
device_id = device_id.as_str(),
"Tried to create an Olm session but the device is \
unknown",
?user_id,
?device_id,
"Tried to create an Olm session but the device is unknown",
);
continue;
}
Err(e) => {
warn!(
user_id = user_id.as_str(),
device_id = device_id.as_str(),
error = ?e,
?user_id, ?device_id, error = ?e,
"Tried to create an Olm session, but we can't \
fetch the device from the store",
);
@@ -399,10 +541,8 @@ impl SessionManager {
Ok(s) => s,
Err(e) => {
warn!(
user_id = user_id.as_str(),
device_id = device_id.as_str(),
error = ?e,
"Error creating outbound session"
?user_id, ?device_id, error = ?e,
"Error creating Olm session"
);
self.failed_devices
@@ -467,6 +607,7 @@ mod tests {
iter,
ops::Deref,
sync::{Arc, RwLock as StdRwLock},
time::Duration,
};
use matrix_sdk_test::{async_test, response_from_file};
@@ -478,7 +619,7 @@ mod tests {
},
IncomingResponse,
},
device_id, user_id, DeviceId, UserId,
device_id, owned_server_name, user_id, DeviceId, OwnedUserId, UserId,
};
use serde_json::json;
use tokio::sync::Mutex;
@@ -572,7 +713,7 @@ mod tests {
manager.store.save_devices(&[bob_device]).await.unwrap();
let (_, request) =
let (txn_id, request) =
manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().unwrap();
assert!(request.one_time_keys.contains_key(bob.user_id()));
@@ -590,7 +731,7 @@ mod tests {
let response = KeyClaimResponse::new(one_time_keys);
manager.receive_keys_claim_response(&response).await.unwrap();
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
assert!(manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().is_none());
}
@@ -599,8 +740,8 @@ mod tests {
async fn test_session_creation_waits_for_keys_query() {
let (manager, identity_manager) = session_manager_test_helper().await;
// start a keys query request. At this point, we are only interested in our own
// devices.
// start a `/keys/query` request. At this point, we are only interested in our
// own devices.
let (key_query_txn_id, key_query_request) =
identity_manager.users_for_key_query().await.unwrap().pop_first().unwrap();
info!("Initial key query: {:?}", key_query_request);
@@ -632,7 +773,7 @@ mod tests {
})
};
// the initial keys query completes, and we start another
// the initial `/keys/query` completes, and we start another
let response_json =
json!({ "device_keys": { manager.store.static_account().user_id.to_owned(): {}}});
let response =
@@ -659,6 +800,45 @@ mod tests {
assert!(bob_key_claims.contains_key(bob_device.device_id()));
}
#[async_test]
async fn test_session_creation_does_not_wait_for_keys_query_on_failed_server() {
let (manager, identity_manager) = session_manager_test_helper().await;
// We start tracking Bob's devices.
let other_user_id = OwnedUserId::try_from("@bob:example.com").unwrap();
{
let cache = manager.store.cache().await.unwrap();
identity_manager
.key_query_manager
.synced(&cache)
.await
.unwrap()
.update_tracked_users(iter::once(other_user_id.as_ref()))
.await
.unwrap();
}
// Do a `/keys/query` request, in which Bob's server is a failure.
let (key_query_txn_id, _key_query_request) =
identity_manager.users_for_key_query().await.unwrap().pop_first().unwrap();
let response = KeysQueryResponse::try_from_http_response(response_from_file(
&json!({ "device_keys": {}, "failures": { other_user_id.server_name(): "unreachable" }})
)).unwrap();
identity_manager.receive_keys_query_response(&key_query_txn_id, &response).await.unwrap();
// Now, an attempt to get the missing sessions should now *not* block. We use a
// timeout so that we can detect the call blocking.
let result = tokio::time::timeout(
Duration::from_millis(10),
manager.get_missing_sessions(iter::once(other_user_id.as_ref())),
)
.await
.expect("get_missing_sessions blocked rather than completing quickly")
.expect("get_missing_sessions returned an error");
assert!(result.is_none(), "get_missing_sessions returned Some(...)");
}
// This test doesn't run on macos because we're modifying the session
// creation time so we can get around the UNWEDGING_INTERVAL.
#[async_test]
@@ -697,7 +877,7 @@ mod tests {
assert!(manager.is_device_wedged(&bob_device));
assert!(manager.users_for_key_claim.read().unwrap().contains_key(bob.user_id()));
let (_, request) =
let (txn_id, request) =
manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().unwrap();
assert!(request.one_time_keys.contains_key(bob.user_id()));
@@ -717,7 +897,7 @@ mod tests {
assert!(manager.outgoing_to_device_requests.read().unwrap().is_empty());
manager.receive_keys_claim_response(&response).await.unwrap();
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
assert!(!manager.is_device_wedged(&bob_device));
assert!(manager.get_missing_sessions(iter::once(bob.user_id())).await.unwrap().is_none());
@@ -734,20 +914,54 @@ mod tests {
manager.store.save_devices(&[alice_device]).await.unwrap();
let (_, users_for_key_claim) =
let (txn_id, users_for_key_claim) =
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
manager.receive_keys_claim_response(&keys_claim_with_failure()).await.unwrap();
manager.receive_keys_claim_response(&txn_id, &keys_claim_with_failure()).await.unwrap();
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
manager.receive_keys_claim_response(&keys_claim_without_failure()).await.unwrap();
// expire the failure
manager.failures.expire(&owned_server_name!("example.org"));
let (txn_id, users_for_key_claim) =
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
manager.receive_keys_claim_response(&txn_id, &keys_claim_without_failure()).await.unwrap();
}
#[async_test]
async fn failed_devices_handling() {
let response_with_invalid_signature = json!({
// Alice is missing altogether
test_invalid_claim_response(json!({
"one_time_keys": {},
"failures": {},
}))
.await;
// Alice is present but with no devices
test_invalid_claim_response(json!({
"one_time_keys": {
"@alice:example.org": {}
},
"failures": {},
}))
.await;
// Alice's device is present but with no keys
test_invalid_claim_response(json!({
"one_time_keys": {
"@alice:example.org": {
"DEVICEID": {}
}
},
"failures": {},
}))
.await;
// Alice's device is present with a bad signature
test_invalid_claim_response(json!({
"one_time_keys": {
"@alice:example.org": {
"DEVICEID": {
@@ -764,9 +978,16 @@ mod tests {
}
},
"failures": {},
});
})).await;
}
let response = response_from_file(&response_with_invalid_signature);
/// Helper for failed_devices_handling.
///
/// Takes an invalid /keys/claim response for Alice's device DEVICEID and
/// checks that it is handled correctly. (The device should be marked as
/// 'failed'; and once that
async fn test_invalid_claim_response(response_json: serde_json::Value) {
let response = response_from_file(&response_json);
let response = KeyClaimResponse::try_from_http_response(response).unwrap();
let alice = user_id!("@alice:example.org");
@@ -778,13 +999,13 @@ mod tests {
// Since we don't have a session with Alice yet, the machine will try to claim
// some keys for alice.
let (_, users_for_key_claim) =
let (txn_id, users_for_key_claim) =
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
// We receive a response with an invalid one-time key, this will mark Alice as
// timed out.
manager.receive_keys_claim_response(&response).await.unwrap();
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
// Since alice is timed out, we won't claim keys for her.
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
@@ -798,17 +1019,29 @@ mod tests {
.or_insert_with(BTreeMap::new)
.insert(alice_account.device_id().to_owned(), one_time);
// Now we receive a valid one-time key from Alice.
let response = KeyClaimResponse::new(one_time_keys);
manager.receive_keys_claim_response(&response).await.unwrap();
// Alice isn't timed out anymore.
assert!(!manager
// Now we expire Alice's timeout, and receive a valid one-time key for her.
manager
.failed_devices
.write()
.unwrap()
.entry(alice.to_owned())
.or_default()
.contains(alice_account.device_id()));
.get(alice)
.unwrap()
.expire(&alice_account.device_id().to_owned());
let (txn_id, users_for_key_claim) =
manager.get_missing_sessions(iter::once(alice)).await.unwrap().unwrap();
assert!(users_for_key_claim.one_time_keys.contains_key(alice));
let response = KeyClaimResponse::new(one_time_keys);
manager.receive_keys_claim_response(&txn_id, &response).await.unwrap();
// Alice isn't timed out anymore.
assert!(manager
.failed_devices
.read()
.unwrap()
.get(alice)
.unwrap()
.failure_count(alice_account.device_id())
.is_none());
}
}
+10 -13
View File
@@ -359,22 +359,19 @@ impl UsersForKeyQuery {
&mut self,
user: &UserId,
) -> Option<Arc<KeysQueryWaiter>> {
match self.user_map.get(user) {
None => None,
Some(&sequence_number) => {
trace!(?user, %sequence_number, "Registering new waiting task");
self.user_map.get(user).map(|&sequence_number| {
trace!(?user, %sequence_number, "Registering new waiting task");
let waiter = Arc::new(KeysQueryWaiter {
sequence_number,
user: user.to_owned(),
completed: AtomicBool::new(false),
});
let waiter = Arc::new(KeysQueryWaiter {
sequence_number,
user: user.to_owned(),
completed: AtomicBool::new(false),
});
self.tasks_awaiting_key_query.push(Arc::downgrade(&waiter));
self.tasks_awaiting_key_query.push(Arc::downgrade(&waiter));
Some(waiter)
}
}
waiter
})
}
}
@@ -14,7 +14,7 @@ macro_rules! cryptostore_integration_tests {
room_id,
serde::{Base64, Raw},
to_device::DeviceIdOrAllDevices,
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, TransactionId, UserId,
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId
};
use serde_json::value::to_raw_value;
use $crate::{
@@ -292,6 +292,54 @@ macro_rules! cryptostore_integration_tests {
assert_eq!(to_back_up, vec![session])
}
#[async_test]
async fn mark_inbound_group_sessions_as_backed_up() {
// Given a store exists with multiple unbacked-up sessions
let (account, store) =
get_loaded_store("mark_inbound_group_sessions_as_backed_up").await;
let room_id = &room_id!("!test:localhost");
let mut sessions: Vec<InboundGroupSession> = Vec::with_capacity(10);
for i in 0..10 {
sessions.push(account.create_group_session_pair_with_defaults(room_id).await.1);
}
let changes = Changes { inbound_group_sessions: sessions.clone(), ..Default::default() };
store.save_changes(changes).await.expect("Can't save group session");
assert_eq!(store.inbound_group_sessions_for_backup(100).await.unwrap().len(), 10);
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
(&session.room_id(), &session.session_id())
}
// When I mark some as backed up
let x = store.mark_inbound_group_sessions_as_backed_up(&[
session_info(&sessions[1]),
session_info(&sessions[3]),
session_info(&sessions[5]),
session_info(&sessions[7]),
session_info(&sessions[9]),
]).await.expect("Failed to mark sessions as backed up");
// And ask which still need backing up
let to_back_up = store.inbound_group_sessions_for_backup(10).await.unwrap();
let needs_backing_up = |i: usize| to_back_up.iter().any(|s| s.session_id() == sessions[i].session_id());
// Then the sessions we said were backed up no longer need backing up
assert!(!needs_backing_up(1));
assert!(!needs_backing_up(3));
assert!(!needs_backing_up(5));
assert!(!needs_backing_up(7));
assert!(!needs_backing_up(9));
// And the sessions we didn't mention still need backing up
assert!(needs_backing_up(0));
assert!(needs_backing_up(2));
assert!(needs_backing_up(4));
assert!(needs_backing_up(6));
assert!(needs_backing_up(8));
assert_eq!(to_back_up.len(), 5);
}
#[async_test]
async fn reset_inbound_group_session_for_backup() {
let (account, store) =
@@ -265,6 +265,20 @@ impl CryptoStore for MemoryStore {
.collect())
}
async fn mark_inbound_group_sessions_as_backed_up(
&self,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<()> {
for (room_id, session_id) in room_and_session_ids {
let session = self.inbound_group_sessions.get(room_id, session_id);
if let Some(session) = session {
session.mark_as_backed_up();
self.inbound_group_sessions.add(session);
}
}
Ok(())
}
async fn reset_backup_state(&self) -> Result<()> {
for session in self.get_inbound_group_sessions().await? {
session.reset_backup_state();
+178 -65
View File
@@ -42,12 +42,12 @@ use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fmt::Debug,
ops::Deref,
pin::pin,
sync::{atomic::Ordering, Arc, RwLock as StdRwLock},
time::Duration,
};
use as_variant::as_variant;
use async_std::sync::{Condvar, Mutex as AsyncStdMutex};
use futures_core::Stream;
use futures_util::StreamExt;
use ruma::{
@@ -55,7 +55,7 @@ use ruma::{
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{Mutex, MutexGuard, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
use tokio::sync::{Mutex, MutexGuard, Notify, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
use tracing::{info, warn};
use vodozemac::{base64_encode, megolm::SessionOrdering, Curve25519PublicKey};
use zeroize::Zeroize;
@@ -66,12 +66,12 @@ use crate::{
user::UserIdentities, Device, ReadOnlyDevice, ReadOnlyUserIdentities, UserDevices,
},
olm::{
Account, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
Account, ExportedRoomKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
PrivateCrossSigningIdentity, Session, StaticAccountData,
},
types::{events::room_key_withheld::RoomKeyWithheldEvent, EventEncryptionAlgorithm},
verification::VerificationMachine,
CrossSigningStatus, ReadOnlyOwnUserIdentity,
CrossSigningStatus, ReadOnlyOwnUserIdentity, RoomKeyImportResult,
};
pub mod caches;
@@ -108,14 +108,10 @@ pub struct Store {
#[derive(Debug, Default)]
pub(crate) struct KeyQueryManager {
/// Record of the users that are waiting for a /keys/query.
//
// This uses an async_std::sync::Mutex rather than a
// matrix_sdk_common::locks::Mutex because it has to match the Condvar (and tokio lacks a
// working Condvar implementation)
users_for_key_query: AsyncStdMutex<UsersForKeyQuery>,
users_for_key_query: Mutex<UsersForKeyQuery>,
// condition variable that is notified each time an update is received for a user.
users_for_key_query_condvar: Condvar,
/// Notifier that is triggered each time an update is received for a user.
users_for_key_query_notify: Notify,
}
impl KeyQueryManager {
@@ -164,6 +160,66 @@ impl KeyQueryManager {
Ok(())
}
/// Wait for a `/keys/query` response to be received if one is expected for
/// the given user.
///
/// If the given timeout elapses, the method will stop waiting and return
/// `UserKeyQueryResult::TimeoutExpired`.
///
/// Requires a [`StoreCacheGuard`] to make sure the users for which a key
/// query is pending are up to date, but doesn't hold on to it
/// thereafter: the lock is short-lived in this case.
pub async fn wait_if_user_key_query_pending(
&self,
cache: StoreCacheGuard,
timeout_duration: Duration,
user: &UserId,
) -> Result<UserKeyQueryResult> {
{
// Drop the cache early, so we don't keep it while waiting (since writing the
// results requires to write in the cache, thus take another lock).
self.ensure_sync_tracked_users(&cache).await?;
drop(cache);
}
let mut users_for_key_query = self.users_for_key_query.lock().await;
let Some(waiter) = users_for_key_query.maybe_register_waiting_task(user) else {
return Ok(UserKeyQueryResult::WasNotPending);
};
let wait_for_completion = async {
while !waiter.completed.load(Ordering::Relaxed) {
// Register for being notified before releasing the mutex, so
// it's impossible to miss a wakeup between the last check for
// whether we should wait, and starting to wait.
let mut notified = pin!(self.users_for_key_query_notify.notified());
notified.as_mut().enable();
drop(users_for_key_query);
// Wait for a notification
notified.await;
// Reclaim the lock before checking the flag to avoid races
// when two notifications happen right after each other and the
// second one sets the flag we want to wait for.
users_for_key_query = self.users_for_key_query.lock().await;
}
};
match timeout(Box::pin(wait_for_completion), timeout_duration).await {
Err(_) => {
warn!(
user_id = ?user,
"The user has a pending `/key/query` request which did \
not finish yet, some devices might be missing."
);
Ok(UserKeyQueryResult::TimeoutExpired)
}
_ => Ok(UserKeyQueryResult::WasPending),
}
}
}
pub(crate) struct SyncedKeyQueryManager<'a> {
@@ -193,43 +249,6 @@ impl<'a> SyncedKeyQueryManager<'a> {
self.cache.store.save_tracked_users(&store_updates).await
}
/// Wait for a `/keys/query` response to be received if one is expected for
/// the given user.
///
/// If the given timeout elapses, the method will stop waiting and return
/// `UserKeyQueryResult::TimeoutExpired`
pub async fn wait_if_user_key_query_pending(
&self,
timeout_duration: Duration,
user: &UserId,
) -> UserKeyQueryResult {
let mut users_for_key_query = self.manager.users_for_key_query.lock().await;
let Some(waiter) = users_for_key_query.maybe_register_waiting_task(user) else {
return UserKeyQueryResult::WasNotPending;
};
let wait_for_completion = async {
while !waiter.completed.load(Ordering::Relaxed) {
users_for_key_query =
self.manager.users_for_key_query_condvar.wait(users_for_key_query).await;
}
};
match timeout(Box::pin(wait_for_completion), timeout_duration).await {
Err(_) => {
warn!(
user_id = ?user,
"The user has a pending `/key/query` request which did \
not finish yet, some devices might be missing."
);
UserKeyQueryResult::TimeoutExpired
}
_ => UserKeyQueryResult::WasPending,
}
}
/// Process notifications that users have changed devices.
///
/// This is used to handle the list of device-list updates that is received
@@ -281,7 +300,7 @@ impl<'a> SyncedKeyQueryManager<'a> {
self.cache.store.save_tracked_users(&store_updates).await?;
// wake up any tasks that may have been waiting for updates
self.manager.users_for_key_query_condvar.notify_all();
self.manager.users_for_key_query_notify.notify_waiters();
Ok(())
}
@@ -1048,18 +1067,6 @@ impl Store {
.map(|d| d.devices().find(|d| d.curve25519_key() == Some(curve_key)))
}
/// Get all devices associated with the given `user_id`
///
/// *Note*: This doesn't return our own device.
pub(crate) async fn get_user_devices_filtered(&self, user_id: &UserId) -> Result<UserDevices> {
self.get_user_devices(user_id).await.map(|mut d| {
if user_id == self.user_id() {
d.inner.remove(self.device_id());
}
d
})
}
/// Get all devices associated with the given `user_id`
///
/// *Note*: This does also return our own device.
@@ -1142,16 +1149,12 @@ impl Store {
self.inner.identity.lock().await.export_secret(secret_name).await
}
SecretName::RecoveryKey => {
#[cfg(feature = "backups_v1")]
if let Some(key) = self.load_backup_keys().await?.decryption_key {
let exported = key.to_base64();
Some(exported)
} else {
None
}
#[cfg(not(feature = "backups_v1"))]
None
}
name => {
warn!(secret = ?name, "Unknown secret was requested");
@@ -1470,6 +1473,116 @@ impl Store {
pub fn secrets_stream(&self) -> impl Stream<Item = GossippedSecret> {
self.inner.store.secrets_stream()
}
pub(crate) async fn import_room_keys(
&self,
exported_keys: Vec<ExportedRoomKey>,
from_backup: bool,
progress_listener: impl Fn(usize, usize),
) -> Result<RoomKeyImportResult> {
let mut sessions = Vec::new();
async fn new_session_better(
session: &InboundGroupSession,
old_session: Option<InboundGroupSession>,
) -> bool {
if let Some(old_session) = &old_session {
session.compare(old_session).await == SessionOrdering::Better
} else {
true
}
}
let total_count = exported_keys.len();
let mut keys = BTreeMap::new();
for (i, key) in exported_keys.into_iter().enumerate() {
match InboundGroupSession::from_export(&key) {
Ok(session) => {
let old_session = self
.inner
.store
.get_inbound_group_session(session.room_id(), session.session_id())
.await?;
// Only import the session if we didn't have this session or
// if it's a better version of the same session.
if new_session_better(&session, old_session).await {
if from_backup {
session.mark_as_backed_up();
}
keys.entry(session.room_id().to_owned())
.or_insert_with(BTreeMap::new)
.entry(session.sender_key().to_base64())
.or_insert_with(BTreeSet::new)
.insert(session.session_id().to_owned());
sessions.push(session);
}
}
Err(e) => {
warn!(
sender_key= key.sender_key.to_base64(),
room_id = ?key.room_id,
session_id = key.session_id,
error = ?e,
"Couldn't import a room key from a file export."
);
}
}
progress_listener(i, total_count);
}
let imported_count = sessions.len();
let changes = Changes { inbound_group_sessions: sessions, ..Default::default() };
self.save_changes(changes).await?;
info!(total_count, imported_count, room_keys = ?keys, "Successfully imported room keys");
Ok(RoomKeyImportResult::new(imported_count, total_count, keys))
}
/// Import the given room keys into our store.
///
/// # Arguments
///
/// * `exported_keys` - A list of previously exported keys that should be
/// imported into our store. If we already have a better version of a key
/// the key will *not* be imported.
///
/// Returns a tuple of numbers that represent the number of sessions that
/// were imported and the total number of sessions that were found in the
/// key export.
///
/// # Examples
///
/// ```no_run
/// # use std::io::Cursor;
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
/// # use ruma::{device_id, user_id};
/// # let alice = user_id!("@alice:example.org");
/// # async {
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// # let export = Cursor::new("".to_owned());
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
/// # };
/// ```
pub async fn import_exported_room_keys(
&self,
exported_keys: Vec<ExportedRoomKey>,
progress_listener: impl Fn(usize, usize),
) -> Result<RoomKeyImportResult> {
self.import_room_keys(exported_keys, false, progress_listener).await
}
pub(crate) fn crypto_store(&self) -> Arc<CryptoStoreWrapper> {
self.inner.store.clone()
}
}
impl Deref for Store {
@@ -113,6 +113,13 @@ pub trait CryptoStore: AsyncTraitDeps {
limit: usize,
) -> Result<Vec<InboundGroupSession>, Self::Error>;
/// Mark the inbound group sessions with the supplied room and session IDs
/// as backed up
async fn mark_inbound_group_sessions_as_backed_up(
&self,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<(), Self::Error>;
/// Reset the backup state of all the stored inbound group sessions.
async fn reset_backup_state(&self) -> Result<(), Self::Error>;
@@ -277,6 +284,7 @@ pub trait CryptoStore: AsyncTraitDeps {
#[repr(transparent)]
struct EraseCryptoStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseCryptoStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
@@ -331,6 +339,16 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
self.0.inbound_group_sessions_for_backup(limit).await.map_err(Into::into)
}
async fn mark_inbound_group_sessions_as_backed_up(
&self,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<()> {
self.0
.mark_inbound_group_sessions_as_backed_up(room_and_session_ids)
.await
.map_err(Into::into)
}
async fn reset_backup_state(&self) -> Result<()> {
self.0.reset_backup_state().await.map_err(Into::into)
}
@@ -36,6 +36,14 @@ pub struct MegolmV1AuthData {
extra: BTreeMap<String, Value>,
}
impl MegolmV1AuthData {
// Create a new [`MegolmV1AuthData`] from a public Curve25519 key and a
// [`Signatures`] map.
pub(crate) fn new(public_key: Curve25519PublicKey, signatures: Signatures) -> Self {
Self { public_key, signatures, extra: Default::default() }
}
}
/// Information pertaining to a room key backup. Can be used to upload a new
/// backup version as defined in the [spec].
///
@@ -71,7 +71,7 @@ impl CrossSigningKey {
/// Serialize the cross signing key into a Raw version.
pub fn to_raw<T>(&self) -> Raw<T> {
Raw::from_json(to_raw_value(&self).expect("Coulnd't serialize cross signing keys"))
Raw::from_json(to_raw_value(&self).expect("Couldn't serialize cross signing keys"))
}
/// Get the Ed25519 cross-signing key (and its ID).
@@ -80,7 +80,6 @@ impl MasterPubkey {
/// **Note**: Use this method with caution, the `canonical_json` needs to be
/// correctly canonicalized and make sure that the object you are checking
/// the signature for is allowed to be signed by a master key.
#[cfg(any(feature = "backups_v1", test))]
pub(crate) fn has_signed_raw(
&self,
signatures: &Signatures,
@@ -84,7 +84,7 @@ impl DeviceKeys {
/// Serialize the device keys key into a Raw version.
pub fn to_raw<T>(&self) -> Raw<T> {
Raw::from_json(to_raw_value(&self).expect("Coulnd't serialize device keys"))
Raw::from_json(to_raw_value(&self).expect("Couldn't serialize device keys"))
}
/// Get the key of the given key algorithm belonging to this device.
@@ -176,6 +176,7 @@ pub struct UnknownRoomKeyContent {
other: BTreeMap<String, Value>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardedMegolmV1AesSha2Content")
@@ -188,6 +189,7 @@ impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content {
}
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for ForwardedMegolmV2AesSha2Content {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForwardedMegolmV2AesSha2Content")
@@ -16,7 +16,7 @@
use std::collections::BTreeMap;
use ruma::{JsOption, OwnedDeviceId, RoomId};
use ruma::{OwnedDeviceId, RoomId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vodozemac::{megolm::MegolmMessage, olm::OlmMessage, Curve25519PublicKey};
@@ -126,7 +126,7 @@ pub struct OlmV1Curve25519AesSha2Content {
pub sender_key: Curve25519PublicKey,
/// The unique ID of this content.
pub message_id: JsOption<String>,
pub message_id: Option<String>,
}
/// The event content for events encrypted with the m.olm.v2.curve25519-aes-sha2
@@ -142,8 +142,8 @@ pub struct OlmV2Curve25519AesSha2Content {
pub sender_key: Curve25519PublicKey,
/// The unique ID of this content.
#[serde(default, skip_serializing_if = "JsOption::is_undefined", rename = "org.matrix.msgid")]
pub message_id: JsOption<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "org.matrix.msgid")]
pub message_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
@@ -151,8 +151,8 @@ struct OlmHelper {
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
sender_key: Curve25519PublicKey,
ciphertext: BTreeMap<String, OlmMessage>,
#[serde(default, skip_serializing_if = "JsOption::is_undefined", rename = "org.matrix.msgid")]
message_id: JsOption<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "org.matrix.msgid")]
message_id: Option<String>,
}
impl Serialize for OlmV1Curve25519AesSha2Content {
@@ -503,7 +503,7 @@ pub(crate) mod tests {
assert_let!(
ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(content) = &event.content
);
assert!(content.message_id.is_undefined());
assert!(content.message_id.is_none());
let serialized = serde_json::to_value(event)?;
assert_eq!(json, serialized);
@@ -125,6 +125,7 @@ impl MegolmV1AesSha2Content {
}
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for MegolmV1AesSha2Content {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MegolmV1AesSha2Content")
@@ -19,7 +19,7 @@ use std::collections::BTreeMap;
use ruma::{
exports::ruma_macros::AsStrAsRefStr,
serde::{AsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, SerializeAsRefStr},
JsOption, OwnedDeviceId, OwnedRoomId,
OwnedDeviceId, OwnedRoomId,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -107,7 +107,7 @@ impl RoomKeyWithheldContent {
sender_key: Curve25519PublicKey,
from_device: OwnedDeviceId,
) -> Self {
let from_device = JsOption::Some(from_device);
let from_device = Some(from_device);
match algorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2 => {
@@ -222,7 +222,7 @@ impl std::fmt::Display for WithheldCode {
#[derive(Debug, Deserialize, Serialize)]
struct WithheldHelper {
pub algorithm: EventEncryptionAlgorithm,
pub reason: JsOption<String>,
pub reason: Option<String>,
pub code: WithheldCode,
#[serde(flatten)]
other: Value,
@@ -259,8 +259,8 @@ pub struct CommonWithheldCodeContent {
/// The device ID of the device sending the m.room_key.withheld message
/// MSC3735.
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
pub from_device: JsOption<OwnedDeviceId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_device: Option<OwnedDeviceId>,
#[serde(flatten)]
other: BTreeMap<String, Value>,
@@ -278,7 +278,7 @@ impl CommonWithheldCodeContent {
room_id,
session_id,
sender_key,
from_device: JsOption::Some(device_id),
from_device: Some(device_id),
other: Default::default(),
}
}
@@ -318,13 +318,14 @@ pub struct NoOlmWithheldContent {
/// The device ID of the device sending the m.room_key.withheld message
/// MSC3735.
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
pub from_device: JsOption<OwnedDeviceId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_device: Option<OwnedDeviceId>,
#[serde(flatten)]
other: BTreeMap<String, Value>,
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for CommonWithheldCodeContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnyWithheldContent")
@@ -336,6 +337,7 @@ impl std::fmt::Debug for CommonWithheldCodeContent {
}
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for NoOlmWithheldContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NoOlmWithheldContent")
@@ -356,8 +358,8 @@ pub struct UnknownRoomKeyWithHeld {
/// The withheld code
pub code: WithheldCode,
/// A human-readable reason for why the key was not sent.
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
pub reason: JsOption<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
/// The other data of the unknown room key.
#[serde(flatten)]
other: BTreeMap<String, Value>,
@@ -427,7 +429,7 @@ impl Serialize for RoomKeyWithheldContent {
let helper = match self {
Self::MegolmV1AesSha2(r) => {
let code = r.withheld_code();
let reason = JsOption::Some(code.to_string());
let reason = Some(code.to_string());
match r {
MegolmV1AesSha2WithheldContent::BlackListed(content)
@@ -450,7 +452,7 @@ impl Serialize for RoomKeyWithheldContent {
#[cfg(feature = "experimental-algorithms")]
Self::MegolmV2AesSha2(r) => {
let code = r.withheld_code();
let reason = JsOption::Some(code.to_string());
let reason = Some(code.to_string());
match r {
MegolmV1AesSha2WithheldContent::BlackListed(content)
@@ -66,6 +66,7 @@ impl Drop for SecretSendContent {
}
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for SecretSendContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretSendContent")

Some files were not shown because too many files have changed in this diff Show More