Compare commits

...

267 Commits

Author SHA1 Message Date
Damir Jelić bb573117e1 chore: Release matrix-sdk version 0.9.0 2024-12-18 15:12:23 +01:00
Andy Balaam ff7077b742 doc(crypto): Add changelog entry for #4424 2024-12-18 13:51:00 +00:00
Ivan Enderlin bb70229dd8 chore: Make Clippy happy. 2024-12-18 13:24:55 +01:00
Andy Balaam 03947618ff Merge branch 'release-for-crypto-wasm-11'
This brings in the fix for #4424 that we did on a release branch to
allow a quick release of crypto-wasm
2024-12-18 11:25:53 +00:00
Andy Balaam b18e7d71ed fix(crypto): Fix error when reading VerifiedStateOrBool with old PreviouslyVerifiedButNoLonger value 2024-12-18 11:22:53 +00:00
Andy Balaam 612ba6fa29 task(crypto): Accept old PreviouslyVerified value of ShieldStateCode when deserializing 2024-12-18 11:22:53 +00:00
Andy Balaam db39c6bea6 task(crypto): Accept old PreviouslyVerified value of VerificationLevel when deserializing 2024-12-18 11:22:53 +00:00
Andy Balaam 5f3b56a987 task(crypto): Accept old PreviouslyVerified value of SenderData when deserializing 2024-12-18 11:22:53 +00:00
Benjamin Bouvier 373709fb38 feat(event cache): don't replace a gap chunk by an empty items chunks 2024-12-17 18:30:57 +01:00
Benjamin Bouvier 5d8ad3a4a9 fix(linked chunk): in LinkedChunk::ritems_from, skip as long as we're on the right chunk
The previous code would skip based on the position's index, but not the
position's chunk. It could be that the position's chunk is different
from the first items chunk, as shown in the example, where the linked
chunk ends with a gap; in this case, the position's index would be 0,
while the first chunk found while iterating backwards had 3 items. As a
result, items 'd' and 'e' would be skipped incorrectly.

The fix is to take into account the chunk id when skipping over items.
2024-12-17 17:48:10 +01:00
Damir Jelić 0ca35d6c4a test: Test that room keys received by notification clients trigger redecryptions 2024-12-17 16:07:21 +01:00
Damir Jelić daeffc07b3 feat: Derive PartialEq and Eq for RoomListLoadingState 2024-12-17 16:07:21 +01:00
Damir Jelić bd15f4ecbe feat(timeline): Listen to the room keys stream to retry decryptions 2024-12-17 16:07:21 +01:00
Damir Jelić f17f4e2bf6 feat: Add a stream to listen to room keys being inserted to the store 2024-12-17 16:07:21 +01:00
Damir Jelić 177ec1216f feat(crypto)!: Don't ignore the error if the room_keys_received stream lags 2024-12-17 16:07:21 +01:00
Valere 512a2d2662 Merge pull request #4399 from matrix-org/valere/ffi_save_store_dehydration_pickle_key
feat(crypto-ffi-bindings): Save/Load dehydrated pickle key
2024-12-17 10:07:54 +01:00
Valere 95582a6c3c feat(crypto-bindings): Save/Load dehydrated pickle key
review: better tests
2024-12-17 09:51:28 +01:00
Jorge Martín 866b5fea40 feat(room): Separate RoomState::Banned from RoomState::Left.
This is needed to tell apart rooms in left and banned state in places like `RoomInfo` or `RoomPreview`.

The banned rooms will still count as left rooms in the sync processes.
2024-12-16 19:19:56 +01:00
Benjamin Bouvier 34ea42aec0 feat(ffi): expose the linked chunk debug string function at the FFI layer 2024-12-16 16:41:03 +01:00
Benjamin Bouvier cae7e43b91 feat(multiverse): add linked chunk debug screen in multiverse 2024-12-16 16:41:03 +01:00
Benjamin Bouvier 34d15a4d37 feat(event cache): propose a debug representation for the linked chunk in RoomEvents too 2024-12-16 16:41:03 +01:00
Benjamin Bouvier f6cb8186c6 task(event cache): limit the display of event ids to 8 chars in the raw chunk debug string 2024-12-16 16:41:03 +01:00
dependabot[bot] 47044b1a23 chore(deps): bump crate-ci/typos from 1.28.2 to 1.28.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.2 to 1.28.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.2...v1.28.3)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 17:29:27 +02:00
Jorge Martín 05d46e6027 Rename JoinRequest in the SDK crates to KnockRequest, make Room::mark_knock_requests_as_seen thread safe and pass user_ids instead of event_ids: the user ids will be used to get the related member state events and they'll only be marked as seen if they're in a knock state.
Also, add extra checks to the integration tests.
2024-12-16 14:08:09 +01:00
Jorge Martín 338769508e feat(ffi): add bindings for subscribing to the join requests 2024-12-16 14:08:09 +01:00
Jorge Martín 93ebae6601 feat(room): allow subscribing to requests to join a room
This subscription will combine 3 streams: one notifying the members in the room have changed, another notifying the seen join requests have changed, and finally a third one notifying when the room members are no longer synced.

With this info we can track when we need to generate a new list of join requests to be emitted so the client can always have an up to date list.
2024-12-16 14:08:09 +01:00
Jorge Martín 780c264e59 feat(room): add JoinRequest abstraction
This struct is an abstraction over a room member or state event with knock membership.
2024-12-16 14:08:09 +01:00
Jorge Martín 9a899c1cb1 feat(room): add 'seen request to join ids' to the stores
This will allow us to keep track of which join room requests are marked as 'seen' by the current user and return them as such.

Also, add some methods to `Room` to mark new join requests as seen and to get the current ids for the seen join requests.
2024-12-16 14:08:09 +01:00
Richard van der Hoff 2703f7f7d4 crypto: extra logging in OtherUserIdentity
Add some extra logging in these two methids, to try to narrow down a bug report
we received.
2024-12-16 12:59:16 +00:00
Kévin Commaille 8d2e672996 feat!: Upgrade Ruma to 0.12.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-16 11:56:44 +01:00
Benjamin Bouvier 5a25e65da3 test(event cache): use the MatrixMockServer 2024-12-16 11:56:22 +01:00
Benjamin Bouvier c197808b42 task(event cache): get rid of one level of indent 2024-12-16 11:56:22 +01:00
Benjamin Bouvier ed34719295 task(event cache): simplify handling a back-pagination result 2024-12-16 11:56:22 +01:00
Benjamin Bouvier a052a79aaf fix(event cache): store the gap *before* events, after back-paginating
The conditions required to cause the bug might have been impossible to
reach in the real world, because it assumes a mix of:

- events present in the linked chunk
- no prev-batch token

However: now that we have storage, we could end up in this situation,
when reaching the start of the timeline (since there'll be no previous
gap in that case). We need to handle that better in the linked chunk
representation itself, but in the meanwhile, we should insert the gap
and the events in a relative correct order.
2024-12-16 11:56:22 +01:00
Benjamin Bouvier b6542477bb task(event cache): make the code more concise in back-pagination 2024-12-16 11:56:22 +01:00
Kévin Commaille a573b650c9 chore(sdk): Remove image-rayon cargo feature check from build.rs
The cargo feature was removed, but the build script was forgotten.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-15 18:07:42 +01:00
Valere 789bd317b3 Merge pull request #4383 from matrix-org/valere/cache_dehydration_pickle_key
feat(crypto): Support storing the dehydrated device pickle key
2024-12-13 14:42:44 +01:00
Valere 2b39476d9b feat(crypto): Support storing the dehydrated device pickle key 2024-12-13 13:05:19 +01:00
Andy Balaam 6dcefe49c2 feat(utds): Provide the reason why an event was an expected UTD 2024-12-13 10:46:10 +00:00
Benjamin Bouvier 150d9e4b05 fix(event cache store): always use immediate mode when handling linked chunk updates
If a linked chunk update starts with a RemoveChunk update, then the
transaction may start with a SELECT query and be considered a read
transaction. Soon enough, it will be upgraded into a write transaction,
because of the next UPDATE/DELETE operations that happen thereafter. If
there's another write transaction already happening, this may result in
a SQLITE_BUSY error, according to
https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions

One solution is to always start the transaction in immediate mode. This
may also fail with SQLITE_BUSY according to the documentation, but it's
unclear whether it will happen in general, since we're using WAL mode
too. Let's try it out.
2024-12-12 17:59:42 +01:00
Damir Jelić 54bd1d7931 refactor(base): Move the joined member count logic into its respective sub-functions 2024-12-12 14:44:21 +01:00
Damir Jelić 7ae31d0cb1 fix(base): Subtract the number of service members from the number joined members
This patch fixes an edge case where the member is alone in the room with
a service member. We already subtracted the number of service members
in the case we calculated the room summary ourselves, but we did not do
so when the server provided the room summary.

This lead to the room, instead of being called `Empty`, being called
`Foo and N others`.
2024-12-12 14:44:21 +01:00
Damir Jelić f7f58dfd71 feat(ui): Add the MemberHints state event type to the required state
This state event allows us to correctly calculate the room display name
according to MSC4171.

MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171
2024-12-12 14:44:21 +01:00
Richard van der Hoff 780a4630e4 chore(ffi): avoid hardcoding clang version
Update the workaround for https://github.com/rust-lang/rust/issues/109717 to
avoid hardcoding the clang version; instead, run `clang -dumpversion` to figure
it out.

While we're there, use the `CC_x86_64-linux-android` env var, which should
point to clang, rather than relying on `ANDROID_NDK_HOME` to be set.
2024-12-12 12:54:00 +00:00
Benjamin Bouvier 3356e0cc82 refactor(state store): use a single lock for all memory store accesses
The `MemoryStore` implementation of the `StateStore` has grown into a
monster, with one lock per field. It's probably overkill, as individual
fields don't need fine-grained locks like this; after all, accesses to
the store shouldn't be reentrant in general.

Fixes #3720.
2024-12-12 10:04:09 +01:00
Richard van der Hoff fda374ee81 feat(ffi): Add new properties to UnableToDecryptInfo
Followup to https://github.com/matrix-org/matrix-rust-sdk/pull/4360: expose
the new properties via FFI
2024-12-11 18:42:28 +00:00
Benjamin Bouvier 0264e49968 task(event cache): rename a few things
- rename RawLinkedChunk -> RawChunk
- rename RawChunk::id -> RawChunk::identifier
- precise that a `RawChunk` is mostly a `Chunk` with different
previous/next links.
2024-12-11 12:10:24 +01:00
Benjamin Bouvier d42c449612 refactor(event cache): only store a prev-batch token if the timeline was limited 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 925d10f2ff task(event cache store): include the number of added items in one log 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 4402f59e74 refactor(event cache): spawn a task to handle updates to the event cache store 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 20184552a8 feat(event cache): start with an empty linked chunk if reloading failed 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 832fedb05e feat(event cache): display raw linked chunks from storage when they fail to be rebuilt 2024-12-11 12:10:24 +01:00
Benjamin Bouvier eeb14f6cbe refactor!(event cache store): have the event cache store return raw linked chunks, not the full linked chunk
And let the caller rebuild the linked chunk. This is slightly nicer in
that it allows us to display the raw representation of a reloaded linked
chunk, before checking its internal state is consistent; this will allow
for better debug of issues related to the linked chunk internal state.

No functional changes.
2024-12-11 12:10:24 +01:00
Kévin Commaille a562f73b1e doc(timeline): Document media caching of send_attachment
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Kévin Commaille 7295f29055 doc(send_queue): Document media caching of send_attachment
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Kévin Commaille 723d7973d5 fix(send_queue): Use MediaFormat::File when caching attachment thumbnail
The `MediaFormat` reflects only the request that would be made to the homeserver.
There is no link between the format of the files stored in the media cache and their purpose in an event.

`MediaFormat::Tumbnail` means that we request a server-generated thumbnail of a file in the media repository.

Since the thumbnail is its own file in the media repository, it makes more sense to use `MediaFormat::File`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Stefan Ceriu d5e7a9c949 chore(ui): rename date divider is_same_date_as to is_same_date_divider_group_as 2024-12-10 14:11:48 +02:00
Stefan Ceriu 8f064581d6 chore(ui): rename the day_dividers module to date_dividers 2024-12-10 14:11:48 +02:00
Stefan Ceriu 634edf2b65 chore(ui): rename all timeline "day dividers" to "date deviders" following the introduction of montly divider mode 2024-12-10 14:11:48 +02:00
Stefan Ceriu 935e4df927 feat(ui): make the timeline date separators configurable; have them appear either when the day changes or when the month changes. 2024-12-10 14:11:48 +02:00
Richard van der Hoff 1d72d2774f feat(ui): Add more properties to UnableToDecryptInfo 2024-12-10 11:36:04 +00:00
Richard van der Hoff 1e72131e7f feat(ui) Add UnableToDecryptInfo::user_trusts_own_identity 2024-12-10 11:36:04 +00:00
Richard van der Hoff e8b3949db3 feat(ui): Add UnableToDecryptInfo::event_local_age_millis 2024-12-10 11:36:04 +00:00
Richard van der Hoff c501a39ad4 refactor(sdk): Add Encryption::device_creation_timestamp
... so that we can use it in more places
2024-12-10 11:36:04 +00:00
Richard van der Hoff a04f9187f8 refactor(ui): store UTD info within PendingUtdReport
... making it easier to report late decryptions.
2024-12-10 11:36:04 +00:00
Benjamin Bouvier 32e2070f56 refactor(ffi): use a bool instead of an option to make the API less awkward
By default, the event cache store will be disabled. If disabled, it will
clean all the events in the cache store; most of the time this will do
nothing, since the store will not even be filled with any event data, so
it would be cheap to do. If some data was filled in the cache store
before, then it would be cleared after the cache store has been
disabled.

This makes a less awkward API than the previous one, where `None` and
`Some(false)` carried different semantics.
2024-12-10 12:05:29 +01:00
Benjamin Bouvier 4ee96aaffc feat(event cache): add a way to clear a single room's persistent storage 2024-12-10 12:05:29 +01:00
Benjamin Bouvier 0783cf89ba feat(ffi): add a feature flag to enable persistent storage for the event cache 2024-12-10 12:05:29 +01:00
Benjamin Bouvier cf02e694f2 feat(event cache store): add a method to clear all rooms' linked chunks 2024-12-10 12:05:29 +01:00
Ivan Enderlin cf178d603c doc(ui): Fix typos. 2024-12-10 11:36:05 +01:00
Ivan Enderlin ee94c86164 doc(ui): Fix a typo. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 3526761580 doc(ui): Unfold a Self in the doc. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 9a08975c8e doc(ui): Explain why ObservableItemsEntries does not implement Iterator. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 6b56c9efd8 doc(ui): Explain why Deref is fine, but DerefMut is not. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0f2ada0958 refactor(ui): Rename ObservableItems::clone to clone_items. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0d17ea353f refactor(ui): Replace a panic by a sensible value + error!. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 13e26b13e7 doc(ui): Rephrase the documentation of ObservableItems::all_remote_events. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 72f1bd6180 doc(ui): Document ObservableItems::items more and TimelineItemKind. 2024-12-10 11:36:05 +01:00
Ivan Enderlin e32ea1627e doc(ui): Fix typos. 2024-12-10 11:36:05 +01:00
Ivan Enderlin ed1f2e29ed refactor(ui): ObservableItemsTransactionEntry::remove is no longer unsafe 2024-12-10 11:36:05 +01:00
Ivan Enderlin 92cb18207e test(ui): Write test suite for ObservableItems. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 80f6b8d2cd test(ui): Write test suite for AllRemoteEvents.
This patch adds test suite for `AllRemoteEvents`.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 05969fefde chore: Make Clippy happy. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 81c962238a doc(ui): Add more documentation for AllRemoteEvents. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 56218ee5d7 refactor(ui): Create ObservableItemsTransactionEntry.
This patch creates `ObservableItemsTransactionEntry` that mimics
`ObservableVectorTransactionEntry`. The differences are `set` is
renamed `replace`, and `remove` is unsafe (because I failed to update
`AllRemoteEvents` in this method due to the borrow checker).
2024-12-10 11:36:05 +01:00
Ivan Enderlin aa9138b281 doc(ui): Add more documentation for ObservableItemsTransaction. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 6f231523b3 refactor(ui): Create ObservableItemsEntries and ObservableItemsEntry.
This patch creates `ObservableItemsEntries` and particularly
`ObservableItemsEntry` that wraps the equivalent
`ObservableVectorEntries` and `ObservableVectorEntry` with the
noticeable difference that `ObservableItemsEntry` does **not** expose
the `remove` method. It only exposes `replace` (which is a renaming
of `set`).
2024-12-10 11:36:05 +01:00
Ivan Enderlin 943b3fbd91 refactor(ui): Rename ObservableItems::set to replace.
This patch renames `ObservableItems(Transaction)::set` to `replace`, it
conveys the semantics a bit better for new comers.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 40ff880597 doc(ui): Add more documentation. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0647be1bc3 refactor(ui): Move AllRemoteEvents inside observable_items.
This patch moves `AllRemoteEvents` inside `observable_items` so that
more methods can be made private, which reduces the risk of misuses
of this API. In particular,  the following methods are now strictly
private:

- `clear`
- `push_front`
- `push_back`
- `remove`
- `timeline_item_has_been_inserted_at`
- `timeline_item_has_been_removed_at`

In fact, now, all `&mut self` method (except `get_by_event_id_mut`) are
now strictly private!
2024-12-10 11:36:05 +01:00
Ivan Enderlin b069b20e18 refactor(ui): Create ObservableItems(Transaction). 2024-12-10 11:36:05 +01:00
Ivan Enderlin 91b73a2b16 refactor(ui): Maintain timeline_item_index when timeline items are inserted or removed.
This patch maintains the `timeline_item_index` when timeline items are
inserted or removed.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 14d0f6877a refactor(ui): Maintain timeline_item_index when remote events are manipulated.
This patch maintains the `timeline_item_index` when a new remote events
is added or removed.
2024-12-10 11:36:05 +01:00
Ivan Enderlin a2210bce48 refactor(ui): Add EventMeta::timeline_item_index.
This is the foundation for the mapping between remote events and
timeline items.
2024-12-10 11:36:05 +01:00
Benjamin Bouvier 68cb85a2b2 refactor(event cache store): use a single transaction to handle all linked chunk updates at once
Instead of one transaction per update. This ensures that if a single
update fails, then none is taken into account.
2024-12-10 11:32:30 +01:00
Jonas Richard Richter 72fcc50f80 feat(ffi): Expose the method to send custom events with JSON content (#4390)
This patch adds the Room::send_raw method to the bindings, making it usable from
e.g. Swift.

Signed-off-by: Jonas Richard Richter <jonas-richard.richter@telekom.de>
2024-12-10 11:03:31 +01:00
Andy Balaam 5721c3622d refactor(key_backups): Rename fast_exists_on_server to exists_on_server 2024-12-09 16:33:56 +00:00
Andy Balaam 50eb46dc82 refactor(key_backups): Rename exists_on_server to fetch_exists_on_server 2024-12-09 16:33:56 +00:00
Andy Balaam 8aae16ffd7 feat(crypto) Provide a method to check whether server backup exists without hitting the server every time 2024-12-09 16:33:56 +00:00
Benjamin Bouvier e402ed4ce8 refactor(event cache): get the *most recent* pagination token, not the *oldest* one
Whenever it needs to back-paginate, the event cache should start with
the *most recent* backpagination token, not the oldest one.

This isn't a functional change, until the persistent storage is enabled.
The reason is that, currently, there is one previous-batch token alive;
after it's used, it's replaced with another gap and the events it served
to request from the server.

When persistent storage will be enabled, we'll have situations like the
one shown in the test code, where we can have multiple previous-batch
token alive at the same time. In that case, we'll need to back-paginate
from the most recent events to the least recent events, and not the
other way around, or we'll have holes in the timeline that won't be
filled until we got to the start of the timeline.
2024-12-09 15:57:14 +01:00
Kévin Commaille a1a04ee513 chore: Remove MSRV from READMEs
It can be found in Cargo.toml.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-09 16:22:00 +02:00
Benjamin Bouvier affdc25256 refactor(linked chunk): rename len() to num_items()
This makes it clearer that it's only concerned about the number of
items, not the number of chunks.
2024-12-09 14:46:21 +01:00
Benjamin Bouvier 8db78efbbc fix(event cache): use a correcter heuristic to decide whether to add initial events or not
Thanks Hywan for spotting the issue.
2024-12-09 14:46:21 +01:00
Kévin Commaille d8184e72eb fix(media): Make sure that local MXC URIs only try to get media from the cache and ignore requested dimensions (#4387)
Extracted from #4329. This does not change the `MediaFormat` of the
request used in the media cache by the send queue.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-09 13:43:49 +01:00
torrybr 3bd57d4307 feat(sdk): support for observing m.beacon events 2024-12-09 10:33:37 +01:00
Kévin Commaille 42193f1b06 chore(xtask): Remove unnecessary lifetime
`const` variables are always `'static`. Detected by clippy.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-08 16:57:22 +01:00
Kévin Commaille a277e6d37f chore(xtask): Disable unexpected_cfgs lint
It is triggered by the `xshell::cmd!` macro, and is fixed in xshell 0.2.7, which we cannot upgrade to.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-08 16:57:22 +01:00
Benjamin Bouvier bf6fa4cd55 fix(event cache): don't fill initial items if the room already had events (#4381)
The test requires subtle conditions to trigger:

- initialize a timeline from a room-list-service's room
- start a backpagination with that timeline (so the room event cache's
paginator is busy)
- try to initialize another timeline with the same room-list-service's
room (e.g. because the first room has been closed, and the app using it
doesn't have a room cache)

This would fail, because initializing a timeline calls
`EventCache::add_initial_events()` all the time, which tries to reset
the paginator's state, which assumes the paginator's not paginating at
this point. In a soon future, we'll get rid of the
`add_initial_events()` function because the event cache will handle its
own persistent storage; in the meantime, a correct fix is to skip
`add_initial_events()` if there was already something in the linked
chunk. After all, we're likely to fill the initial events with the same
events all the time, or a subset of more recent events. By doing that,
we're likely keeping *more* events in the linked chunk, instead.

Thanks to @stefanceriu for reporting the issue and confirming the fix
works!
2024-12-06 12:37:34 +01:00
Damir Jelić 6501a44e6a feat: Add support for MSC4171
Introduce support for MSC4171, enabling the designation of certain users
as service members. These flagged users are excluded from the room
display name calculation.

MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171
2024-12-05 14:23:36 +01:00
Mathieu Velten ee30008f38 feat: Accept any string as a key for m.direct account data (#4228)
This is the follow up of this [Ruma
PR](https://github.com/ruma/ruma/pull/1946) for the SDK.

---------

Signed-off-by: Mathieu Velten <mathieu@velten.xyz>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-05 12:37:16 +00:00
Damir Jelić 22cb8a1878 chore: Bump the pprof version to fix a security issue 2024-12-05 12:08:25 +01:00
Timo 111f916a78 feat(WidgetDriver): Pass Matrix API errors to the widget (#4241)
Currently the WidgetDriver just returns unspecified error strings to the
widget that can be used to display an issue description to the user. It
is not helpful to run code like a retry or other error mitigation logic.

Here it is proposed to add standardized errors for issues that every
widget driver implementation can run into (all matrix cs api errors):
https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1838804895

This PR forwards the errors that occur during the widget processing to
the widget in the correct format.

NOTE:
It does not include request Url and http Headers. See also:
https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1839802292

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-04 16:52:02 +00:00
Benjamin Bouvier a6e1f05957 test(event cache): use the MatrixMockServer for integration testing 2024-12-04 17:18:45 +01:00
Benjamin Bouvier 0b64c68191 test(event cache): make use of macros to avoid manual timeouts 2024-12-04 17:18:45 +01:00
Benjamin Bouvier 713039279c Enable persistent storage in multiverse
And fix an issue that would cause a crash because a timeline wasn't
initialized and we tried to unwrap it later.
2024-12-04 17:18:45 +01:00
Benjamin Bouvier d317e5d73c feat(event cache): don't react specifically to limited timelines, when storage's enabled 2024-12-04 17:18:45 +01:00
Damir Jelić ee93c278df chore: Update the hashbrown version we're using 2024-12-04 16:31:15 +01:00
Valere 1009ea86ae Merge pull request #4305 from matrix-org/valere/support_for_withheld_reason
feat(crypto): Add optional withheld reason to `UnableToDecryptReason`
2024-12-04 15:57:22 +01:00
Damir Jelić 7d8e7af308 Revert "chore(ui): Unify the logic for timeline item insertions"
This reverts commit d2ecd745f6.
2024-12-04 15:55:49 +01:00
Damir Jelić 136522c694 Revert "doc(timeline): tweak comments when inserting a new item"
This reverts commit 197da2c585.
2024-12-04 15:55:49 +01:00
Valere 6801811226 feat(crypto): Supports new UtdCause variants for withheld keys
Adds new UtdCause variants for withheld keys, enabling applications to display customised messages when an Unable-To-Decrypt message is expected.

refactor(crypto): Move WithheldCode from crypto to common crate
2024-12-04 15:33:23 +01:00
Benjamin Bouvier a4434d79c9 feat(event cache): strip bundled relations before persisting events 2024-12-04 12:35:58 +01:00
Benjamin Bouvier e0b1b5dc05 test: don't use the event cache storage but regular syncs instead 2024-12-04 12:35:46 +01:00
Benjamin Bouvier 1a63d8f0b7 task(event cache): ignore add_initial_events() when the event cache storage has been enabled
Not worth testing IMO, since this is about the "temporary" API we're
going to remove in subsequent patches.
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5bf3b11edf test: rewrite test_send_edit_when_timeline_is_clear to not use add_initial_items
Moar MatrixMockServer \o/
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 8f1722f2a8 test: have the PendingEdit test helper use the matrix mock server and event cache storage 2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5d95387935 test: remove unused adding of initial events in sliding sync test helper 2024-12-04 12:35:46 +01:00
Benjamin Bouvier bd93a9a40e test: remvoe use of add_initial_events in test_add_initial_events
And make it a smoke test that the event cache correctly gets events it
retrieves from sync.
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5cde4a6630 test: remove use of add_initial_events in test_ignored_unignored 2024-12-04 12:35:46 +01:00
Benjamin Bouvier de5511f009 test: rewrite test_ignored_unignored so it makes use of the MatrixMockServer 2024-12-04 12:35:46 +01:00
Damir Jelić 9bdd9fa831 chore: Update the RELEASING file so it doesn't mention git-cliff anymore 2024-12-04 11:25:00 +01:00
Damir Jelić 48bb3dbbe7 chore: Update our contributing guide for the manual changelog entries 2024-12-04 11:25:00 +01:00
Damir Jelić b8bf847fc1 chore: Set up cargo-release to update the versions in the changelog 2024-12-04 11:25:00 +01:00
Damir Jelić 17812b6949 chore: Remove our cliff config and don't use cliff to generate changelogs 2024-12-04 11:25:00 +01:00
Damir Jelić bab979aaf4 chore: Update our changelogs 2024-12-04 11:25:00 +01:00
Damir Jelić 42778dc79d chore: Replace git-cliff in the weekly-report command 2024-12-04 11:25:00 +01:00
Damir Jelić a948be9c85 chore: Downgrade xshell due to broken stdin interactions
Since xshell 0.2.3 the behavior of the run() function has changed in a
incompatible manner. Namely the stdin for the run() function no longer
inherits stdin from the shell. This makes it impossible for commands
that are executed by the run() function to accept input from the shell.

We don't use this functionality in many places but the `xtask release
prepare` command is now broken.

Let's just pin xshell to a working version while we wait for this to be
resolved upstream.

Upstream-issue: https://github.com/matklad/xshell/issues/63
2024-12-04 11:22:43 +01:00
Stefan Ceriu 9c381c1022 feat(ffi): expose a generic message_filtered_timeline that can be configured to only include RoomMessage type events and filter those further based on their message type.
Virtual timeline items will still be provided and the `default_event_filter` will be applied before everything else.
Instances of these timelines will be used to power the 2 different tabs shown on the new media browser. The client will be responsible for interacting with it similar to a normal timeline and transforming its data into something renderable e.g. section by date separators (which will be made configurable in a follow up PR)
2024-12-04 11:41:25 +02:00
Andy Balaam 9002f82659 task(backup_tests): Move mock helpers into MatrixMockServer 2024-12-03 14:55:41 +00:00
Andy Balaam 5f7fb4699a task(backup_tests): Split exists_on_server test into separate tests 2024-12-03 14:55:41 +00:00
Andy Balaam 5907104e0e task(backup_tests): Use helper functions to shorten exists_on_server tests 2024-12-03 14:55:41 +00:00
Ivan Enderlin d7dff5b026 refactor(ui): Add AllRemoteEvents::get_by_event_id_mut.
Having a mutable iterator can be dangerous and is probably too generic
regarding the safety we want to add around the `AllRemoteEvents` type.

This patch removes `iter_mut` and replaces it by its only use case:
`get_by_event_id_mut`.
2024-12-03 13:52:42 +01:00
Ivan Enderlin cabde8ed11 refactor(ui): AllRemoteEvents::back becomes last to add semantics.
This patch renames `AllRemoteEvents::back` to `last` so that it now gets
a specific semantics instead of being generic.
2024-12-03 13:52:42 +01:00
Ivan Enderlin b02fd92ad0 refactor(ui): Introduce the AllRemoteEvents type.
This patch replaces `VecDeque<EventMeta>` by `AllRemoteEvents` which
is a wrapper type around `VecDeque<EventMeta>`, but this new type aims
at adding semantics API rather than a generic API. It also helps to
isolate the use of these values and to know precisely when and how they
are used.

As a first step, `AllRemoteEvents` implements a generic API to not break
the existing code. Next patches will revisit that a little bit step
by step.
2024-12-03 13:52:42 +01:00
Ivan Enderlin 9be8578aff doc(ui): Explain why all_remote_events is necessary. 2024-12-03 13:26:26 +01:00
Ivan Enderlin 4f28dd85bf refactor: TimelineStateTransaction::add_or_update_remote_event always return true.
The `add_or_update_remote_event` method always returns `true`. This
patch updates the method to return nothing, and cleans up the call sites
accordingly. This patch also adds comments to clarify the code flow.

The bool value returned by `add_or_update_remote_event` was supposed
to be `false` if the event was duplicated. First off, as soon as the
`Timeline` receives its events from the `EventCache` via `VectorDiff`,
the `event_cache::Deduplicator` will take care of deduplication,
so the `Timeline` won't have to handle that itself. Second,
`add_or_update_remote_event` was sometimes removing an event, but it
was re-inserting a new one immediately without returning `false`: it was
never returning `false` because a new event was always added.
2024-12-03 13:26:26 +01:00
Damir Jelić 74119e8861 Revert "chore: Remove Ruma from the cargo-deny git dep allow list"
This reverts commit f256fe4b24.

As discussed, we want to prioritize the testing of new Ruma features
over stability.
2024-12-03 12:58:37 +01:00
Timo e76b8f7e15 tests: Refactor the widget tests to use MockMatrixServer (#4236)
This is a follow up PR on https://github.com/matrix-org/matrix-rust-sdk/pull/3987.

And tries to use the `MockMatrixServer` wherever reasonable in the
widget integration tests.

---------

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-03 08:36:24 +00:00
Integral 31bd5c6790 refactor: replace static with const for global constants
Signed-off-by: Integral <integral@member.fsf.org>
2024-12-03 09:13:57 +01:00
Andy Balaam 50f036d283 task(crypto_tests): Fix test that will fail when we handle backups correctly
This test was checking that a new device which has access to backups
returned an unknown UTD when it was given empty JSON for the event. It
was only passing because we currently have incorrect behaviour when
backups are enabled.

The fix is to make the device old and without access to backups, so that
we still consider this UTD unexpected.
2024-12-02 17:34:44 +00:00
Andy Balaam 8c73f0c655 task(crypto_tests): Remove duplicate test 2024-12-02 17:34:44 +00:00
Andy Balaam 8de76deb1b task(crypto_tests): Re-arrange and reword utd_cause tests 2024-12-02 17:34:44 +00:00
Andy Balaam b65728d46f task(crypto_tests): Shorten utd_cause tests with helper functions for devices 2024-12-02 17:34:44 +00:00
Andy Balaam 0b4b4ea791 task(crypto_tests): Shorten utd_cause tests with utility functions for UnableToDecryptInfos 2024-12-02 17:34:44 +00:00
Andy Balaam 552ab81739 task(crypto_tests): Add comments and clarify tests for determining UTD causes 2024-12-02 17:34:44 +00:00
dependabot[bot] d49d12249a build(deps): bump crate-ci/typos from 1.27.3 to 1.28.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.3 to 1.28.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.27.3...v1.28.2)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 17:19:21 +01:00
Gabriel Féron ed1d406b72 fix(store): fix indexing issue in ObservableMap (#4346)
Any `.remove` operation called on a `ObservableMap` did not re-index
`values` _after_ the removed position, which means any later operation
on elements inserted after the previously removed key would either be
fetched wrongly, or panic when using `.get_or_create`.

This PR fixes these two related bugs and adds extra test cases.

---------

Signed-off-by: g@leirbag.net
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-02 16:00:19 +00:00
Ivan Enderlin 80a48f53ad refactor: Rename add_or_update_event to add_or_update_remote_event. 2024-12-02 16:45:35 +01:00
Ivan Enderlin 51cfaaacee refactor: Rename TimelineMetadata::all_events to all_remote_events.
This patch renames the `all_events` field to `all_remote_events` in
`TimelineMetada` for the sake of clarity.
2024-12-02 16:45:35 +01:00
Benjamin Bouvier 2f9866cf04 test(event cache): test that the event cache correctly reads updates 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 7de74e2c04 feat(event cache): reload the linked chunk from the store, if storage's enabled 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 019de4ffa0 test(event cache): add a test that the event cache correctly stores updates 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 9f1e3c179b feat(event cache): propagate linked chunk in-memory updates to storage (conditionally) 2024-12-02 16:27:05 +01:00
Damir Jelić 17e17f0b9c ci: Build the Mac framework using the reldbg profile 2024-12-02 15:54:47 +01:00
Benjamin Bouvier 5da36d13c8 fix(event cache): consider empty items chunks in the memory store 2024-12-02 14:09:42 +01:00
Benjamin Bouvier cce322f9c8 test(event cache): add integration test for handling updates and reloading a linked chunk 2024-12-02 14:09:42 +01:00
Benjamin Bouvier ed3b03f454 feat(event cache): implement reloading a linked chunk from the memory store too 2024-12-02 14:09:42 +01:00
Benjamin Bouvier 27e1cded2e feat(event cache): reload a linked chunk from a sqlite store 2024-12-02 14:09:42 +01:00
Ivan Enderlin ad3d1fb6b3 refactor(ui): Use an iterator instead of Vec to represent events.
This patch changes `TimelineStateTransaction::add_remote_events_at`
to take an `IntoIterator<Item = Into<SyncTimelineEvent>>`
for `events`. In the current code, it saves one
`iter().map(Into::into).collect::<Vec<_>>()`, but it will save another
one when we will support `VectorDiff`s coming from the `EventCache`.

It also avoids to allocate a vector to pass new events (this mostly
happens in the test, but it can happen in real life).
2024-12-02 13:09:07 +01:00
Jorge Martín d2fecb6701 fix(room_preview): When requesting a room summary, use fallback server names
If no server names are provided for the room summary request and the
room's server name doesn't match the current user's server name, add the
room alias/id server name as a fallback value. This seems to fix room
preview through federation.

Also, when getting a summary for a room list item, if it's an invite
one, add the server name of the inviter's user id as another possible
fallback.

Changelog: Use the inviter's server name and the server name from the
room alias as fallback values for the via parameter when requesting the
room summary from the homeserver.
2024-12-02 12:11:47 +01:00
Kévin Commaille 685386df13 chore(xtask): Fix scope of push_dir
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 18:03:54 +01:00
Kévin Commaille f94b202341 chore(xtask): Upgrade xshell
Gets rid of an unexpected_cfgs warning.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 18:03:54 +01:00
Kévin Commaille d1a6956e77 chore(sdk): Disable unused_async clippy lint for unimplemented method
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 09:21:23 +01:00
Damir Jelić 2d2215edbe chore: Make use of the member event builder in the EventFactory 2024-11-30 09:20:49 +01:00
Damir Jelić bcd0d20e2f test: Add a method to build m.room.member events in the EventFactory 2024-11-30 09:20:49 +01:00
Kévin Commaille ba5881355d chore(test): Upgrade ctor
Fixes the `unexpected_cfgs` warning so it doesn't need to be disabled anymore.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-29 19:59:24 +01:00
Andy Balaam 1072d0a019 chore(js_tracing): Elide explicit lifetime to satisfy clippy 2024-11-29 18:45:45 +01:00
Damir Jelić 783c86aa78 ci: Build the Mac framework in release mode
The dev profile fails with a linker issue about not finding the
__chkstk_darwin symbol.
2024-11-29 18:45:45 +01:00
Damir Jelić 5564fe8852 ci: Bump the mac OS runner to 15 2024-11-29 18:45:45 +01:00
Damir Jelić e1f0037fd5 chore: Define the lifetime of some const strings explicitly 2024-11-28 11:53:35 +01:00
Benjamin Bouvier daa984f7de feat(event cache store): enable foreign keys pragma \o/ 2024-11-28 11:48:46 +01:00
Benjamin Bouvier aa0eb760de test(event cache): add a test for reading events from multiple rooms
This was to make sure that we can search by blob.
2024-11-28 11:48:46 +01:00
Benjamin Bouvier 9ed65bc321 task(event cache): address review points 2024-11-28 11:48:46 +01:00
Benjamin Bouvier ce95b6089f doc(event cache): add the copyright notice and basic module doc comment 2024-11-28 11:48:46 +01:00
Benjamin Bouvier c6ba71ae33 feat(event cache): allow reloading from the store, and test functionalities
This required adding support for *reading* out of the event cache, for
the sqlite backend. This paves the way for the next PR (reload from the
cache), and it should also help with testing at the `EventCacheStore`
trait layer some day.
2024-11-28 11:48:46 +01:00
Benjamin Bouvier e57d38cf57 doc(common): add a note that a decrypted raw event always has a room id 2024-11-28 11:48:46 +01:00
Benjamin Bouvier 9bea0cff24 feat(event cache): implement the sqlite backend for events 2024-11-28 11:48:46 +01:00
Benjamin Bouvier 197da2c585 doc(timeline): tweak comments when inserting a new item
A comment was duplicating (the first trace! that's removed here), and
the second block comment only applied to new items, and was not as
concise as it could be.
2024-11-28 10:09:50 +01:00
Ivan Enderlin d2ecd745f6 chore(ui): Unify the logic for timeline item insertions
This patch unifies the logic for inserting timeline items at `Start`
and `End` positions. Both `TimelineItemPositions` can share the same
implementation, making separate logic unnecessary. Previously, `End`
included a duplicated events check as well, while `Start` did not, leading
to inconsistency.

The changes strictly involve moving and refactoring, with no functional
modifications.
2024-11-28 08:25:16 +01:00
Damir Jelić e99939db85 refactor(crypto): Rename the IncomingResponse enum to AnyIncomingResponse 2024-11-27 19:55:27 +01:00
Damir Jelić 600a708e7b refactor!(crypto): Rename the OutgoingRequests enum to AnyOutgoingRequest 2024-11-27 19:55:27 +01:00
Damir Jelić a94a5f1716 chore(crypto): Split out the requests module 2024-11-27 19:55:27 +01:00
Damir Jelić 46064680ce refactor!(crypto): Don't re-export the request types from the request module 2024-11-27 19:55:27 +01:00
Damir Jelić 6fe5acfc97 refactor(crypto): Move the requests module under the types module 2024-11-27 19:55:27 +01:00
Valere 3369903766 Merge pull request #4275 from matrix-org/valere/utd_hook_historical_message
feat(utd_hook): Report device-historical expected UTD with new reason
2024-11-27 18:23:53 +01:00
Valere a0c86d9645 feat(utd_hook): Report historical expected UTD with new reason
This PR introduces a new variant to `UtdCause` specifically for device-historical messages (`HistoricalMessage`). These messages cannot be decrypted if key storage is inaccessible. Applications can leverage this new variant to provide more informative error messages to users.
2024-11-27 18:09:06 +01:00
Damir Jelić 7a454888a3 chore: Bump the deps and move some of them to the workspace 2024-11-27 17:03:50 +01:00
Ivan Enderlin 37f52e1c6c fix(common): LinkedChunk emits an Update::NewItemsChunk when constructed.
This patch updates `LinkedChunk::new_with_update_history` to emit an
`Update::NewItemsChunk` because the first chunk is created and it must
emit an update accordingly.
2024-11-27 14:40:26 +01:00
Ivan Enderlin 185423539e test(ui): Fix the test_echo test.
This patch fixes the `test_echo` test. It was doing the following:

* client sends an event to the server,
* servers acknowledges with the ID `$wWgymRfo7ri1uQx0NXO40vLJ`,
* client syncs and the server returns one event with ID `$7at8sd:localhost`,
* the test expects those 2 events to be the same!, which is incorrect.

The test was working because the transaction IDs are the same, but
that's an abuse of the existing code (the code will change soon, another
patch is coming). Whatever the code does: the connection must be based
on the event ID, not the transaction ID.
2024-11-27 14:29:24 +01:00
Stefan Ceriu 9e20659d5d chore: bring back MediaSource JSON serialization methods 2024-11-27 15:13:24 +02:00
Damir Jelić 7783188769 chore: Box the OidcSession so the AuthSession enum isn't unnecessarily big 2024-11-27 13:23:34 +01:00
Damir Jelić 514af54c4c chore: Fix some clippy warnings about our docs 2024-11-27 13:23:34 +01:00
Damir Jelić ad615b7612 chore: Fix some clippy lint warnings around the usage of map_or 2024-11-27 13:23:34 +01:00
Damir Jelić a1b7906a7d chore: Fix some clippy lints around lifetimes 2024-11-27 13:23:34 +01:00
Damir Jelić 79c8d2c345 chore: Don't build the docs for xtask
Building the docs for xtask spews a bunch of unexpected cfg warnings. As
these warnings come from a macro in a dependency and the docs for xtask
don't exist nor will, let's just not build them with the rest of the
docs.
2024-11-27 13:23:34 +01:00
Damir Jelić dcf6af405d chore: Silence unexpected cfg warnings
These are all coming from macro invocations of macros that are defined
in other crates. It's likely a clippy issue. We should try to revert
this the next time we bump the nightly version we're using.
2024-11-27 13:23:34 +01:00
Damir Jelić bb598b61a5 chore: Bump the nightly version we use for the CI 2024-11-27 13:23:34 +01:00
Ivan Enderlin 1c554c4912 chore(ui): Clarifies what TimelineItemPosition::UpdateDecrypted holds.
This patch tries to clear confusion around
`TimelineItemPosition::UpdateDecrypted(usize)`: it does contains
a timeline item index. This patch changes to
`TimelineItemPosition::UpdateDecrypted { timeline_item_index: usize }`
2024-11-27 12:04:59 +01:00
Benjamin Bouvier 21f8b7ed31 refactor(linked chunk): simplify further impl of the LinkedChunkRebuilder 2024-11-27 11:01:44 +01:00
Benjamin Bouvier 23ee8e25dd feat(linked chunk): add a way to reconstruct a linked chunk from its raw representation 2024-11-27 11:01:44 +01:00
Benjamin Bouvier 1098095846 refactor(linked chunk): replace LinkedChunk::len() with a simpler implementation
It's unused so it's mostly cosmetic, and it's trivial to reimplement
using `linked_chunk.items().count()`; let's do that instead of keeping
the perfect exact count synchronized with the chunks, which pollutes the
code in a few places.
2024-11-27 10:16:12 +01:00
Ivan Enderlin 3e7d7e8a31 chore(ui): Rename TimelineEnd to TimelineNewItemPosition.
This patch renames `TimelineEnd` into `TimelineNewItemPosition` for
2 reasons:

1. In the following patches, we will introduce a new variant to insert
   at a specific index, so the suffix `End` would no longer make sense.

2. It's exactly like `TimelineItemPosition` except that it's used
   only and strictly only to add **new** items, which is why we can't use
   `TimelineItemPosition` because it contains the `UpdateDecrypted`
   variant. This renaming reflects it's only about **new** items.

This patch takes the opportunity to move the `RemoteEventOrigin` inside
`TimelineNewItemPosition` to simplify method signatures. They always
go together.
2024-11-26 20:29:31 +01:00
Benjamin Bouvier 2c45316bcb fixup! fix(room): make Room::history_visibility() return an Option 2024-11-26 19:02:46 +01:00
Benjamin Bouvier 8dc7c1f876 fix(ui): have the room list service require the create and history visibility events
These two are required to properly compute the room preview of a joined
room:

- m.room.create ends up filling the `room_type` (space or not)
- m.room.history_visibility ends up filling the `is_world_readable`
  field.
2024-11-26 19:02:46 +01:00
Benjamin Bouvier db84936dcd fix(room): make Room::history_visibility() return an Option
And introduce `Room::history_visibility_or_default()` to return a better
sensible default, according to the spec.
2024-11-26 19:02:46 +01:00
Kévin Commaille 75d7d07013 chore(ffi): Fix thumbnail size info
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:45:54 +01:00
Kévin Commaille d4d5f45edc feat(media)!: Make all fields of Thumbnail required
It seems sensible to assume that if a client is able to generate a thumbnail,
it should be able to get all this information for it too.
A thumbnail with no information is not really useful, as we don't know when it could be used instead of the original image.

Removes `BaseThumbnailInfo`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Kévin Commaille d0257d1cb2 refactor(media): Add method to split Thumbnail into parts
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Kévin Commaille ecf44348cf fix(client): Do not use the encrypted original file's content type as the encrypted thumbnail's content type
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Ivan Enderlin cc8bc05537 refactor: RoomEvents::reset really clear the linked chunk.
This patch updates `RoomEvents::reset` to not drop the `LinkedChunk` to
clear it.
2024-11-26 15:19:24 +01:00
Ivan Enderlin 728d646ce2 fix(common): AsVector clears its internal state on Update::Clear.
This patch fixes a bug in `AsVector`: when an `Update::Clear` value
is received, `AsVector`'s internal state must be cleared too, i.e. the
`UpdateToVectorDiff::chunks` field should be reset to an initial value!

This patch adds a test to ensure this works as expected.
2024-11-26 15:19:24 +01:00
Stefan Ceriu ca397dca0f feat(ffi): wrap Ruma MediaSources and run validations before passing them over FFI
Ruma doesn't currently validate mxuri's and as such `MediaSource`s passed over FFI can contain invalid/empty URLs. This change introduces a wrapper type around Ruma's and failable transformations so that appropiate actions can be taken beforehand e.g. returning a `TimelineItemContent::FailedToParseMessageLike` or nil-ing out the thumbnail info.
2024-11-26 15:40:24 +02:00
Benjamin Bouvier 1fbe6815c3 task(event cache): log whenever we receive an ignore user list change 2024-11-26 12:29:34 +01:00
Ivan Enderlin c61f70727f fix: RelationalLinkedChunk handles Update::Clear.
What the title says.
2024-11-25 17:45:01 +01:00
Ivan Enderlin 2abbf58825 feat(common): Implement LinkedChunk::clear.
This patch implements `LinkedChunk::clear`. The code from `impl Drop
for LinkedChunk` has been moved inside `Ends::clear`, and replaced by
a simple `self.links.clear()`. In addition, `LinkedChunk::clear` indeed
calls `self.links.clear()` but also resets all fields.

This patch adds the `Clear` variant to `Update`.

This patch updates `AsVector` to emit a `VectorDiff::Clear` on
`Update::Clear`.

Finally, this patch adds the necessary tests.
2024-11-25 17:08:43 +01:00
Ivan Enderlin b979b2ea1e doc(common): Fix typos. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 24b968ad39 refactor: EventCacheStore::handle_linked_chunk_updates takes a Vec<Update>.
This patch updates `EventCacheStore::handle_linked_chunk_updates` to
take a `Vec<Update<Item, Gap>>` instead of `&[Update<Item, Gap>]`.
In fact, `linked_chunk::ObservableUpdates::take()` already returns a
`Vec<Update<Item, Gap>>`; we can simply forward this `Vec` up to here
without any further clones.
2024-11-25 17:08:27 +01:00
Ivan Enderlin faa8aa2b9c fix(base): Move all fields of MemoryStore inside a StdRwLock<_>.
This patch creates a new `MemoryStoreInner` and moves all fields from
`MemoryStore` into this new type. All locks are removed, but a new lock
is added around `MemoryStoreInner`. That way we have a single lock.
2024-11-25 17:08:27 +01:00
Ivan Enderlin db9ee9d87b refactor: Add constructors for Position and ChunkIdentifier.
This patch adds constructors for `Position` and `ChunkIdentifier` so
that we keep their inner values private.
2024-11-25 17:08:27 +01:00
Ivan Enderlin 1dbb494b94 feat(common): RelationalLinkedChunk stores the RoomId. 2024-11-25 17:08:27 +01:00
Ivan Enderlin fe52b4cb78 feat(common): EventCacheStore::handle_linked_chunk_updates takes a &RoomId. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 5519442ad8 doc(common): Fix a typo. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 88363d8033 feat(base): MemoryStore uses RelationalLinkedChunk to store events.
That's it.
2024-11-25 17:08:27 +01:00
Ivan Enderlin fb5d8f29ac feat(common): Implement RelationalLinkedChunk.
A `RelationalLinkedChunk` is like a `LinkedChunk` but with a relational
layout, similar to what we would have in a database.

This is used by memory stores. The idea is to have a data layout that
is similar for memory stores and for relational database stores, to
represent a `LinkedChunk`.

This type is also designed to receive `Update`. Applying `Update`s
directly on a `LinkedChunk` is not ideal and particularly not trivial
as the `Update`s do _not_ match the internal data layout of the
`LinkedChunk`, they have been designed for storages, like a relational
database for example.

This type is not as performant as `LinkedChunk` (in terms of memory
layout, CPU caches etc.). It is only designed to be used in memory
stores, which are mostly used for test purposes or light usages of the
SDK.
2024-11-25 17:08:27 +01:00
Benjamin Bouvier 912b121d27 feat(timeline): make more errors transparent 2024-11-25 15:11:02 +01:00
Benjamin Bouvier 2e975d9b19 fix(base): all EventCacheStoreLock must refer to the same underlying cross-process lock
And not duplicate it once per `EventCacheStoreLock`.
2024-11-25 15:11:02 +01:00
Benjamin Bouvier edc93e62b4 task(sdk): expose the SqliteEventCacheStore from the SDK crate
And use it in multiverse.
2024-11-25 15:11:02 +01:00
Ivan Enderlin 9d6ffa951f doc(sdk): Specify how the Client::observe_events works. 2024-11-25 11:49:36 +01:00
Benjamin Bouvier 079ec023b7 task(oidc): add logs when refreshing an OIDC token 2024-11-25 10:58:50 +01:00
Damir Jelić e55a1c7e00 chore: Rework the crypto crate README 2024-11-22 18:20:38 +01:00
Damir Jelić ddd737e4d8 docs: Add a tutorial to the crypto crate
Changelog: Add a tutorial describing how to add end-to-end encryption
support to an existing library.
2024-11-22 18:20:38 +01:00
Mauro Romito 38a15afc9c build (apple): add dynamic type to debug package 2024-11-22 18:44:48 +02:00
Jorge Martín fa93daabd2 feat(ffi): Add RoomInfo::join_rule field to bindings
Breaking-Change: Add `RoomInfo::join_rule` field, remove `RoomInfo::is_public` in the FFI crate, as they contain the same info.
2024-11-22 16:09:55 +01:00
Jorge Martín 6b0987385e refactor(room_preview): make RoomPreview use the local known data only for joined rooms
When instantiating a room preview, previously it would try to just check if the room exists locally either as joined, invited, knocked, left, etc., and then retrieve the info we cached about it.

While this seems fine for most cases, it turns out for non-joined rooms, the info we have locally will **always** be the one we received when the invite/knock/leave action took place and it'll never be updated,
so we may have the case where we knock into a room, never receive a response, someone changes the join rule of the room to something else and we'll think about this room as a 'request to join' room until we clear the local cache.

To prevent that, we can only use the local data for joined rooms, which are constantly updated, and try to use the room summary API and other fallbacks for the rest, even if they're rooms known to us.
2024-11-22 14:20:50 +01:00
Benjamin Bouvier 48fbda844f fix(oidc): make sure we keep track of an ongoing OIDC refresh up to the end
There's a lock making sure we're not doing multiple refreshes of an OIDC
token at the same time. Unfortunately, this lock could be dropped, if
the task spawned by the inner function was detached.

The lock must be held throughout the entire detached task's lifetime,
which this refactoring ensures, by setting the lock's result after
calling the inner function.
2024-11-21 18:36:11 +01:00
Damir Jelić bc70f3c051 refactor: Clean up the Room::compute_display_name() method 2024-11-21 14:34:38 +01:00
Benjamin Bouvier d2f255d613 feat(ffi): add a new function helper to create a caption edit
It has the same semantics used when creating a caption (if no formatted
caption is provided, assume a provided caption is markdown and use that
as the formatted caption).
2024-11-21 10:40:39 +01:00
Doug bf86b168d7 feat(timeline): mark media events as editable in the timeline (#4303)
This PR makes audio, file, image and video messages be editable so that
the timeline signals when it is possible to use #4277/#4300 for editing
captions.
2024-11-21 10:25:32 +01:00
Ivan Enderlin e5ca44bb04 feat(base): Add EventCacheStore::handle_linked_chunk_updates.
This patch adds the `handle_linked_chunk_updates` method on the
`EventCacheStore` trait. Part of
https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
2024-11-20 16:39:49 +01:00
Benjamin Bouvier 1f563c964c task: add manual Sync impl for VerificationCache to avoid overflowing evaluation requirements 2024-11-20 16:33:39 +01:00
Benjamin Bouvier 9a9730d59e task: move the EventFactory to the matrix-sdk-test crate
This makes it available to the crypto crate, by lowering it into the
local dependency tree.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier af3ce4b32b task: remove the dependency from common to test
The (matrix-sdk-)common crate used the (matrix-sdk-)test crate only to
benefit from the `async_test` proc macro, which is conveniently defined
in another crate.

My goal is to make `EventFactory`, at this point in the commit history,
defined in the main SDK crate, available in the test crate.
`EventFactory` makes use of some types defined in common, so there's a
circular dependency at the moment.

To split this circular dependency, I've changed the common crate to
depend on the test-macro crate directly; now the test crate can depend
on the common crate, and everybody's happy.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier 03f0c3a001 task: move the MockClientBuilder to its own mock file 2024-11-20 16:33:39 +01:00
Benjamin Bouvier 639833acf1 task: move test_utils.rs to test_utils/mod.rs
This is more in line with what we're doing in the SDK in general.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier 60893d2797 test(send_queue): add tests for editing a caption while media not sent yet
test(timeline): add an integration test for sending an attachment

test(timeline): add tests for multiple caption edits and local reaction to a media upload
2024-11-20 10:11:56 +01:00
Benjamin Bouvier 9e45111d8b feat(send queue): allow updating caption while the media is being sent 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 0080f17c1f feat(base): add a way to update a dependent send queue request 2024-11-20 10:11:56 +01:00
Benjamin Bouvier fa47af3dd6 refactor!(base): rename StateStore::update_dependent_queued_request to mark_dependent_queued_requests_as_ready 2024-11-20 10:11:56 +01:00
Benjamin Bouvier c4ff07124b feat(ffi): allow editing a media caption from the FFI layer 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 900cf5d071 room: create edits to add a caption to a media event 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 8a6ced0e8f fix(send queue): when adding a local reaction, look for media events in dependent requests too 2024-11-19 17:22:06 +01:00
Benjamin Bouvier f20401c657 test(timeline): add an integration test for sending an attachment in the timeline
Also includes a caption for a file media event, which acts as a
regression test for the previous commit.
2024-11-19 16:59:31 +01:00
Benjamin Bouvier b987fc1de2 fix(media): include the formatted caption and filename for audio and file attachments too 2024-11-19 16:59:31 +01:00
Benjamin Bouvier efeac2ef39 fix(base): clear a room's send queue and dependent event queue after removing it from the state store 2024-11-19 16:50:35 +01:00
Valere 6b80055bd2 fix(utd_hook): Fix regression causing retry to report false late decrypt (#4252)
There has been a recent change on `Decryptor::decrypt_event_impl` causing
the function to return an TimelineEvent of kind unable to decrypt
instead of failing with an error.

The `late_decrypt` detection code was not changed, causing any retry to
mark UTDs as late decrypt.
2024-11-19 16:40:18 +01:00
Jorge Martín 0af53e99ee feat(room_preview): Compute display name for RoomPreview when possible 2024-11-19 16:11:09 +01:00
Jorge Martín bc0c2a6be2 feat(room_preview): Add RoomPreview::heroes field for known rooms 2024-11-19 16:11:09 +01:00
265 changed files with 19419 additions and 5099 deletions
+3
View File
@@ -24,6 +24,7 @@ allow = [
"ISC",
"MIT",
"MPL-2.0",
"Unicode-3.0",
"Zlib",
]
exceptions = [
@@ -54,6 +55,8 @@ allow-git = [
"https://github.com/element-hq/tracing.git",
# Sam as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: rustfmt
- name: Run Benchmarks
+3 -3
View File
@@ -131,7 +131,7 @@ jobs:
test-apple:
name: matrix-rust-components-swift
needs: xtask
runs-on: macos-14
runs-on: macos-15
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
@@ -175,7 +175,7 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
complement-crypto:
name: "Run Complement Crypto tests"
@@ -186,7 +186,7 @@ jobs:
test-crypto-apple-framework-generation:
name: Generate Crypto FFI Apple XCFramework
runs-on: macos-14
runs-on: macos-15
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
+3 -3
View File
@@ -288,7 +288,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: rustfmt
- name: Cargo fmt
@@ -304,7 +304,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.27.3
uses: crate-ci/typos@v1.28.3
clippy:
name: Run clippy
@@ -323,7 +323,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: clippy
- name: Load cache
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
- name: Install Node.js
uses: actions/setup-node@v4
@@ -53,7 +53,7 @@ jobs:
env:
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
run:
cargo doc --no-deps --workspace --features docsrs
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
os-name: 🐧
cachekey-id: linux
- os: macos-14
- os: macos-15
os-name: 🍏
cachekey-id: macos
+45 -36
View File
@@ -45,9 +45,46 @@ that is, just the branch name.)
# Writing changelog entries
We aim to maintain clear and informative changelogs that accurately reflect the
changes in our project. This guide will help you write useful changelog entries
using git-cliff, which fetches changelog entries from commit messages.
Our goal is to maintain clear, concise, and informative changelogs that
accurately document changes in the project. Changelog entries should be written
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
Be sure to include a link to the pull request for additional context. A
well-written changelog entry should be understandable even to those who may not
be deeply familiar with the project. Provide enough context to ensure clarity
and ease of understanding.
A couple of examples of bad changelog entry would look like:
```markdown
- Fixed a panic.
```
```markdown
- Added the Bar function to Foo.
```
A good example of a changelog entry could look like the following:
```markdown
- Use the inviter's server name and the server name from the room alias as
fallback values for the via parameter when requesting the room summary from
the homeserver. This ensures requests succeed even when the room being
previewed is hosted on a federated server.
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
```
For security-related changelog entries, please include the following additional
details alongside the pull request number:
* Impact: Clearly describe the issue's potential impact on users or systems.
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
```markdown
- Use a constant-time Base64 encoder for secret key material to mitigate
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
```
## Commit message format
@@ -74,45 +111,20 @@ The type of changes which will be included in changelogs is one of the following
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
### Changelog trailer
In addition to the Conventional Commit format, you can use the `Changelog` git
trailer to specify the changelog message explicitly. When that trailer is
present, its value will be used as the changelog entry instead of the commit's
leading line. The `Breaking-Change` git trailer can be used in a similar manner
if the changelog entry should be marked as a breaking change.
#### Example commit message
```
feat: Add a method to encode Ed25519 public keys to Base64
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
stringify Ed25519 and thus present them to users. It's also commonly used when
Ed25519 keys need to be inserted into JSON.
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
stringify the Ed25519 public key.
```
In this commit message, the content specified in the `Changelog` trailer will be
used for the changelog entry.
Be careful to add at least one whitespace after new lines to create a paragraph.
### Security fixes
Commits addressing security vulnerabilities must include specific trailers for
vulnerability metadata. These commits are required to include at least the
`Security-Impact` trailer to indicate that the commit is a security fix.
vulnerability metadata, which should also be reflected in the corresponding
changelog entry.
Security issues have some additional git-trailers:
The metadata must be included in the following git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
Please include all of the fields that are available.
Example:
```
@@ -131,9 +143,6 @@ material.
Security-Impact: Low
CVE: CVE-2024-40640
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
Changelog: Use a constant-time Base64 encoder for secret key material
to mitigate side-channel attacks leaking secret key material.
```
## Review process
Generated
+515 -231
View File
File diff suppressed because it is too large Load Diff
+54 -33
View File
@@ -18,34 +18,45 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.76"
rust-version = "1.82"
[workspace.dependencies]
anyhow = "1.0.68"
assert-json-diff = "2"
anyhow = "1.0.93"
aquamarine = "0.6.0"
assert-json-diff = "2.0.2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.1"
assert_matches2 = "0.1.2"
async-rx = "0.1.3"
async-stream = "0.3.3"
async-trait = "0.1.60"
async-stream = "0.3.5"
async-trait = "0.1.83"
as_variant = "1.2.0"
base64 = "0.22.0"
byteorder = "1.4.3"
base64 = "0.22.1"
byteorder = "1.5.0"
chrono = "0.4.38"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.5.1", features = ["tracing"] }
eyeball-im-util = "0.7.0"
futures-core = "0.3.28"
futures-core = "0.3.31"
futures-executor = "0.3.21"
futures-util = "0.3.26"
growable-bloom-filter = "2.1.0"
futures-util = "0.3.31"
gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.1.0"
imbl = "3.0.0"
itertools = "0.12.0"
once_cell = "1.16.0"
pin-project-lite = "0.2.9"
indexmap = "2.6.0"
itertools = "0.13.0"
js-sys = "0.3.69"
mime = "0.3.17"
once_cell = "1.20.2"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.15"
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
ruma = { version = "0.11.1", features = [
rmp-serde = "1.3.0"
ruma = { version = "0.12.0", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -58,38 +69,45 @@ ruma = { version = "0.11.1", features = [
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4171",
] }
ruma-common = "0.14.1"
ruma-common = "0.15.0"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
sha2 = "0.10.8"
similar-asserts = "1.5.0"
similar-asserts = "1.6.0"
stream_assert = "0.1.1"
thiserror = "1.0.38"
tokio = { version = "1.39.1", default-features = false, features = ["sync"] }
tempfile = "3.9.0"
thiserror = "2.0.3"
tokio = { version = "1.41.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
tracing-subscriber = "0.3.18"
unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.0"
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
wiremock = "0.6.0"
zeroize = "1.6.0"
url = "2.5.4"
uuid = "1.11.0"
vodozemac = { version = "0.8.1", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.33"
web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.9.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
@@ -131,7 +149,10 @@ paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git",
[workspace.lints.rust]
rust_2018_idioms = "warn"
semicolon_in_expressions_from_macros = "warn"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
] }
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
-4
View File
@@ -24,10 +24,6 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
used to add Matrix E2EE support to your client or client library.
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
## Status
The library is in an alpha state, things that are implemented generally work but
+2 -2
View File
@@ -17,8 +17,8 @@ The procedure is as follows:
git switch -c release-x.y.z
  ```
2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md`
file using `git cliff`, and bump the version in the `Cargo.toml` file.
2. Prepare the release. This will update the `README.md`, set the versions in
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
```bash
cargo xtask release prepare --execute minor|patch|rc
+1 -1
View File
@@ -23,7 +23,7 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre
wiremock = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
+5 -4
View File
@@ -2,15 +2,16 @@ use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
utils::IntoRawStateEventContent,
config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent,
};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
use matrix_sdk_test::{
event_factory::EventFactory, EventBuilder, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder,
};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
api::client::membership::get_member_events,
+1
View File
@@ -13,6 +13,7 @@ let package = Package(
],
products: [
.library(name: "MatrixRustSDK",
type: .dynamic,
targets: ["MatrixRustSDK"]),
],
targets: [
-4
View File
@@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
```
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
+36 -17
View File
@@ -1,10 +1,10 @@
use std::{env, error::Error};
use std::{env, error::Error, path::PathBuf, process::Command};
use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-ffi/build.rs] too!
@@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
if target_arch == "x86_64" && target_os == "android" {
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
let build_os = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "windows",
_ => panic!(
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
// Configure rust to statically link against the `libclang_rt.builtins` supplied
// with clang.
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
// Android NDK.
let clang_path = PathBuf::from(
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
// clang_path should now look something like
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
// We strip `/bin/clang` from the end to get the toolchain path.
let toolchain_path = clang_path
.ancestors()
.nth(2)
.expect("could not find NDK toolchain path")
.to_str()
.expect("NDK toolchain path is not valid UTF-8");
let clang_version = get_clang_major_version(&clang_path);
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
}
}
/// Run the clang binary at `clang_path`, and return its major version number
fn get_clang_major_version(clang_path: &PathBuf) -> String {
let clang_output =
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
if !clang_output.status.success() {
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
}
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
clang_version.split('.').next().expect("could not parse clang output").to_owned()
}
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
@@ -1,13 +1,17 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_crypto::dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
use matrix_sdk_crypto::{
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
use tokio::runtime::Handle;
use zeroize::Zeroize;
use crate::{CryptoStoreError, DehydratedDeviceKey};
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
@@ -22,6 +26,8 @@ pub enum DehydrationError {
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
#[error(transparent)]
Rand(#[from] rand::Error),
}
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
@@ -33,6 +39,9 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
Self::MissingSigningKey(e)
}
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
Self::PickleKeyLength(l)
}
}
}
}
@@ -66,14 +75,14 @@ impl DehydratedDevices {
pub fn rehydrate(
&self,
pickle_key: Vec<u8>,
pickle_key: &DehydratedDeviceKey,
device_id: String,
device_data: String,
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
let device_id: OwnedDeviceId = device_id.into();
let mut key = get_pickle_key(&pickle_key)?;
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
let ret = RehydratedDevice {
runtime: self.runtime.to_owned(),
@@ -85,10 +94,41 @@ impl DehydratedDevices {
}
.into();
key.zeroize();
Ok(ret)
}
/// Get the cached dehydrated device pickle key if any.
///
/// None if the key was not previously cached (via
/// [`Self::save_dehydrated_device_pickle_key`]).
///
/// Should be used to periodically rotate the dehydrated device to avoid
/// OTK exhaustion and accumulation of to_device messages.
pub fn get_dehydrated_device_key(
&self,
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
Ok(self
.runtime
.block_on(self.inner.get_dehydrated_device_pickle_key())?
.map(crate::DehydratedDeviceKey::from))
}
/// Store the dehydrated device pickle key in the crypto store.
///
/// This is useful if the client wants to periodically rotate dehydrated
/// devices to avoid OTK exhaustion and accumulated to_device problems.
pub fn save_dehydrated_device_key(
&self,
pickle_key: &crate::DehydratedDeviceKey,
) -> Result<(), CryptoStoreError> {
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
}
/// Deletes the previously stored dehydrated device pickle key.
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
}
}
#[derive(uniffi::Object)]
@@ -138,15 +178,13 @@ impl DehydratedDevice {
pub fn keys_for_upload(
&self,
device_display_name: String,
pickle_key: Vec<u8>,
pickle_key: &DehydratedDeviceKey,
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
let mut key = get_pickle_key(&pickle_key)?;
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
let request =
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
key.zeroize();
Ok(request.into())
}
}
@@ -177,15 +215,36 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
}
}
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
let pickle_key_length = pickle_key.len();
#[cfg(test)]
mod tests {
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
if pickle_key_length == 32 {
let mut key = Box::new([0u8; 32]);
key.copy_from_slice(pickle_key);
#[test]
fn test_creating_dehydrated_key() {
let result = DehydratedDeviceKey::new();
assert!(result.is_ok());
let dehydrated_device_key = result.unwrap();
let base_64 = dehydrated_device_key.to_base64();
let inner_bytes = dehydrated_device_key.inner;
Ok(key)
} else {
Err(DehydrationError::PickleKeyLength(pickle_key_length))
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
assert_eq!(base_64, copy.to_base64());
}
#[test]
fn test_creating_dehydrated_key_failure() {
let bytes = [0u8; 24];
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
assert!(pickle_key.is_err());
match pickle_key {
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
assert_eq!(bytes.len(), pickle_key_length);
}
_ => panic!("Should have failed!"),
}
}
}
+6 -3
View File
@@ -1,8 +1,9 @@
#![allow(missing_docs)]
use matrix_sdk_crypto::{
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
SignatureError as InnerSignatureError,
};
use matrix_sdk_sqlite::OpenStoreError;
use ruma::{IdParseError, OwnedUserId};
@@ -57,6 +58,8 @@ pub enum CryptoStoreError {
InvalidUserId(String, IdParseError),
#[error(transparent)]
Identifier(#[from] IdParseError),
#[error(transparent)]
DehydrationError(#[from] InnerDehydrationError),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -112,7 +115,7 @@ mod tests {
#[test]
fn test_withheld_error_mapping() {
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
use matrix_sdk_common::deserialized_responses::WithheldCode;
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
+39 -1
View File
@@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
use matrix_sdk_crypto::{
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
store::{
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
@@ -62,6 +65,8 @@ pub use verification::{
};
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
use crate::dehydrated_devices::DehydrationError;
/// Struct collecting data that is important to migrate to the rust-sdk
#[derive(Deserialize, Serialize, uniffi::Record)]
pub struct MigrationData {
@@ -822,6 +827,39 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
}
}
/// Dehydrated device key
#[derive(uniffi::Record, Clone)]
pub struct DehydratedDeviceKey {
pub(crate) inner: Vec<u8>,
}
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
}
/// Creates a new dehydration pickle key from the given slice.
///
/// Fail if the slice length is not 32.
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
Ok(inner.into())
}
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
pub fn to_base64(&self) -> String {
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
inner.to_base64()
}
}
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
DehydratedDeviceKey { inner: pickle_key.into() }
}
}
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
@@ -17,8 +17,8 @@ use matrix_sdk_crypto::{
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
UserIdentity as SdkUserIdentity,
types::requests::ToDeviceRequest,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
};
use ruma::{
api::{
+15 -12
View File
@@ -4,9 +4,12 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
types::requests::{
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
},
CrossSigningBootstrapRequests,
};
use ruma::{
api::client::{
@@ -136,7 +139,7 @@ pub enum Request {
impl From<OutgoingRequest> for Request {
fn from(r: OutgoingRequest) -> Self {
use matrix_sdk_crypto::OutgoingRequests::*;
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
match r.request() {
KeysUpload(u) => {
@@ -338,16 +341,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
}
}
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
fn from(r: &'a OwnedResponse) -> Self {
match r {
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
}
}
}
+1
View File
@@ -33,3 +33,4 @@ Additions:
- Add `Encryption::get_user_identity` which returns `UserIdentity`
- Add `ClientBuilder::room_key_recipient_strategy`
- Add `Room::send_raw`
+36 -17
View File
@@ -1,10 +1,10 @@
use std::{env, error::Error};
use std::{env, error::Error, path::PathBuf, process::Command};
use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-crypto-ffi/build.rs] too!
@@ -12,26 +12,45 @@ fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
if target_arch == "x86_64" && target_os == "android" {
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
let build_os = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "windows",
_ => panic!(
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
// Configure rust to statically link against the `libclang_rt.builtins` supplied
// with clang.
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
// Android NDK.
let clang_path = PathBuf::from(
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
// clang_path should now look something like
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
// We strip `/bin/clang` from the end to get the toolchain path.
let toolchain_path = clang_path
.ancestors()
.nth(2)
.expect("could not find NDK toolchain path")
.to_str()
.expect("NDK toolchain path is not valid UTF-8");
let clang_version = get_clang_major_version(&clang_path);
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
}
}
/// Run the clang binary at `clang_path`, and return its major version number
fn get_clang_major_version(clang_path: &PathBuf) -> String {
let clang_output =
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
if !clang_output.status.success() {
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
}
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
clang_version.split('.').next().expect("could not parse clang output").to_owned()
}
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
-7
View File
@@ -13,10 +13,3 @@ interface RoomMessageEventContentWithoutRelation {
interface ClientError {
Generic(string msg);
};
interface MediaSource {
[Name=from_json, Throws=ClientError]
constructor(string json);
string to_json();
string url();
};
+72 -19
View File
@@ -32,9 +32,7 @@ use matrix_sdk::{
user_directory::search_users,
},
events::{
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
},
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
@@ -55,7 +53,12 @@ use ruma::{
},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
room::{
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
@@ -76,7 +79,7 @@ use crate::{
notification_settings::NotificationSettings,
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
ruma::AuthData,
ruma::{AuthData, MediaSource},
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utils::AsyncRuntimeDropped,
@@ -117,7 +120,7 @@ impl TryFrom<PusherKind> for RumaPusherKind {
let mut ruma_data = RumaHttpPusherData::new(data.url);
if let Some(payload) = data.default_payload {
let json: Value = serde_json::from_str(&payload)?;
ruma_data.default_payload = json;
ruma_data.data.insert("default_payload".to_owned(), json);
}
ruma_data.format = data.format.map(Into::into);
Ok(Self::Http(ruma_data))
@@ -450,7 +453,7 @@ impl Client {
.inner
.media()
.get_media_file(
&MediaRequestParameters { source, format: MediaFormat::File },
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
@@ -723,7 +726,7 @@ impl Client {
&self,
media_source: Arc<MediaSource>,
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
let source = (*media_source).clone().media_source;
debug!(?source, "requesting media file");
Ok(self
@@ -739,9 +742,9 @@ impl Client {
width: u64,
height: u64,
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
let source = (*media_source).clone().media_source;
debug!(source = ?media_source, width, height, "requesting media thumbnail");
debug!(?source, width, height, "requesting media thumbnail");
Ok(self
.inner
.media()
@@ -1630,7 +1633,7 @@ impl TryFrom<Session> for AuthSession {
user: user_session,
};
Ok(AuthSession::Oidc(session))
Ok(AuthSession::Oidc(session.into()))
} else {
// Create a regular Matrix Session.
let session = matrix_sdk::matrix_auth::MatrixSession {
@@ -1917,9 +1920,13 @@ pub enum AllowRule {
/// Only a member of the `room_id` Room can join the one this rule is used
/// in.
RoomMembership { room_id: String },
/// A custom allow rule implementation, containing its JSON representation
/// as a `String`.
Custom { json: String },
}
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
impl TryFrom<JoinRule> for RumaJoinRule {
type Error = ClientError;
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
@@ -1929,11 +1936,11 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
JoinRule::Knock => Ok(Self::Knock),
JoinRule::Private => Ok(Self::Private),
JoinRule::Restricted { rules } => {
let rules = allow_rules_from(rules)?;
let rules = ruma_allow_rules_from_ffi(rules)?;
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::KnockRestricted { rules } => {
let rules = allow_rules_from(rules)?;
let rules = ruma_allow_rules_from_ffi(rules)?;
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
@@ -1941,12 +1948,10 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
}
}
fn allow_rules_from(
value: Vec<AllowRule>,
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, ClientError> {
let mut ret = Vec::with_capacity(value.len());
for rule in value {
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
match rule {
Ok(rule) => ret.push(rule),
Err(error) => return Err(error),
@@ -1955,7 +1960,7 @@ fn allow_rules_from(
Ok(ret)
}
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
impl TryFrom<AllowRule> for RumaAllowRule {
type Error = ClientError;
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
@@ -1966,6 +1971,54 @@ impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
room_id,
)))
}
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
}
}
}
impl TryFrom<RumaJoinRule> for JoinRule {
type Error = String;
fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
match value {
RumaJoinRule::Knock => Ok(JoinRule::Knock),
RumaJoinRule::Public => Ok(JoinRule::Public),
RumaJoinRule::Private => Ok(JoinRule::Private),
RumaJoinRule::KnockRestricted(restricted) => {
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
Vec<_>,
Self::Error,
>>(
)?;
Ok(JoinRule::KnockRestricted { rules })
}
RumaJoinRule::Restricted(restricted) => {
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
Vec<_>,
Self::Error,
>>(
)?;
Ok(JoinRule::Restricted { rules })
}
RumaJoinRule::Invite => Ok(JoinRule::Invite),
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
_ => Err(format!("Unknown JoinRule: {:?}", value)),
}
}
}
impl TryFrom<RumaAllowRule> for AllowRule {
type Error = String;
fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
match value {
RumaAllowRule::RoomMembership(membership) => {
Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
}
RumaAllowRule::_Custom(repr) => {
let json = serde_json::to_string(&repr)
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
Ok(Self::Custom { json })
}
_ => Err(format!("Invalid AllowRule: {:?}", value)),
}
}
}
@@ -8,6 +8,7 @@ use matrix_sdk::{
CollectStrategy, TrustRequirement,
},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
reqwest::Certificate,
ruma::{ServerName, UserId},
sliding_sync::{
@@ -202,6 +203,8 @@ pub enum ClientBuildError {
SlidingSyncVersion(VersionBuilderError),
#[error(transparent)]
Sdk(MatrixClientBuildError),
#[error(transparent)]
EventCache(#[from] EventCacheError),
#[error("Failed to build the client: {message}")]
Generic { message: String },
}
@@ -269,6 +272,10 @@ pub struct ClientBuilder {
room_key_recipient_strategy: CollectStrategy,
decryption_trust_requirement: TrustRequirement,
request_config: Option<RequestConfig>,
/// Whether to enable use of the event cache store, for reloading events
/// when building timelines et al.
use_event_cache_persistent_storage: bool,
}
#[matrix_sdk_ffi_macros::export]
@@ -299,9 +306,27 @@ impl ClientBuilder {
room_key_recipient_strategy: Default::default(),
decryption_trust_requirement: TrustRequirement::Untrusted,
request_config: Default::default(),
use_event_cache_persistent_storage: false,
})
}
/// Whether to use the event cache persistent storage or not.
///
/// This is a temporary feature flag, for testing the event cache's
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
///
/// This is disabled by default. When disabled, a one-time cleanup is
/// performed when creating the client, and it will clear all the events
/// previously stored in the event cache.
///
/// When enabled, it will attempt to store events in the event cache as
/// they're received, and reuse them when reconstructing timelines.
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.use_event_cache_persistent_storage = value;
Arc::new(builder)
}
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
holder_name: String,
@@ -624,6 +649,19 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
if builder.use_event_cache_persistent_storage {
// Enable the persistent storage \o/
sdk_client.event_cache().enable_storage()?;
} else {
// Get rid of all the previous events, if any.
let store = sdk_client
.event_cache_store()
.lock()
.await
.map_err(EventCacheError::LockingStorage)?;
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
}
Ok(Arc::new(
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
+1 -1
View File
@@ -254,7 +254,7 @@ impl Encryption {
/// 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?)
Ok(self.inner.backups().fetch_exists_on_server().await?)
}
pub fn recovery_state(&self) -> RecoveryState {
+38 -2
View File
@@ -3,7 +3,10 @@ use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
events::{
room::{message::Relation, redaction::SyncRoomRedactionEvent},
room::{
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
@@ -202,7 +205,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
_ => None,
});
MessageLikeEventContent::RoomMessage {
message_type: original_content.msgtype.into(),
message_type: original_content.msgtype.try_into()?,
in_reply_to_event_id,
}
}
@@ -356,6 +359,39 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
}
}
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
pub enum RoomMessageEventMessageType {
Audio,
Emote,
File,
Image,
Location,
Notice,
ServerNotice,
Text,
Video,
VerificationRequest,
Other,
}
impl From<RumaMessageType> for RoomMessageEventMessageType {
fn from(val: ruma::events::room::message::MessageType) -> Self {
match val {
RumaMessageType::Audio { .. } => Self::Audio,
RumaMessageType::Emote { .. } => Self::Emote,
RumaMessageType::File { .. } => Self::File,
RumaMessageType::Image { .. } => Self::Image,
RumaMessageType::Location { .. } => Self::Location,
RumaMessageType::Notice { .. } => Self::Notice,
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
RumaMessageType::Text { .. } => Self::Text,
RumaMessageType::Video { .. } => Self::Video,
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
_ => Self::Other,
}
}
}
/// Contains the 2 possible identifiers of an event, either it has a remote
/// event id or a local transaction id, never both or none.
#[derive(Clone, uniffi::Enum)]
+4 -4
View File
@@ -1,6 +1,8 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
// lines after docs.
mod authentication;
mod chunk_iterator;
@@ -33,13 +35,11 @@ mod utils;
mod widget;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::ruma::events::room::{
message::RoomMessageEventContentWithoutRelation, MediaSource,
};
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
use self::{
error::ClientError,
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
task_handle::TaskHandle,
};
+188 -6
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::StreamExt;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
event_cache::paginator::PaginatorError,
@@ -11,7 +11,7 @@ use matrix_sdk::{
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
use matrix_sdk_ui::timeline::{default_event_filter, PaginationError, RoomExt, TimelineFocus};
use mime::Mime;
use ruma::{
api::client::room::report_content,
@@ -23,7 +23,7 @@ use ruma::{
message::RoomMessageEventContentWithoutRelation,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
},
TimelineEventType,
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
},
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
};
@@ -34,12 +34,12 @@ use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
error::{ClientError, MediaInfoError, RoomError},
event::{MessageLikeEventType, StateEventType},
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
identity_status_change::IdentityStatusChange,
room_info::RoomInfo,
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline},
utils::u64_to_uint,
TaskHandle,
};
@@ -50,6 +50,7 @@ pub enum Membership {
Joined,
Left,
Knocked,
Banned,
}
impl From<RoomState> for Membership {
@@ -59,6 +60,7 @@ impl From<RoomState> for Membership {
RoomState::Joined => Membership::Joined,
RoomState::Left => Membership::Left,
RoomState::Knocked => Membership::Knocked,
RoomState::Banned => Membership::Banned,
}
}
}
@@ -260,6 +262,51 @@ impl Room {
Ok(Timeline::new(timeline))
}
/// A timeline instance that can be configured to only include RoomMessage
/// type events and filter those further based on their message type.
///
/// Virtual timeline items will still be provided and the
/// `default_event_filter` will be applied before everything else.
///
/// # Arguments
///
/// * `internal_id_prefix` - An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
/// distinguish different timeline instances from each other.
///
/// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that
/// will be allowed to appear in the timeline
pub async fn message_filtered_timeline(
&self,
internal_id_prefix: Option<String>,
allowed_message_types: Vec<RoomMessageEventMessageType>,
date_divider_mode: DateDividerMode,
) -> Result<Arc<Timeline>, ClientError> {
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
if let Some(internal_id_prefix) = internal_id_prefix {
builder = builder.with_internal_id_prefix(internal_id_prefix);
}
builder = builder.with_date_divider_mode(date_divider_mode.into());
builder = builder.event_filter(move |event, room_version_id| {
default_event_filter(event, room_version_id)
&& match event {
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
allowed_message_types.contains(&content.msgtype.into())
}
_ => false,
},
_ => false,
}
});
let timeline = builder.build().await?;
Ok(Timeline::new(timeline))
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
}
@@ -336,6 +383,22 @@ impl Room {
Ok(())
}
/// Send a raw event to the room.
///
/// # Arguments
///
/// * `event_type` - The type of the event to send.
///
/// * `content` - The content of the event to send encoded as JSON string.
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
let content_json: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
self.inner.send_raw(&event_type, content_json).await?;
Ok(())
}
/// Redacts an event from the room.
///
/// # Arguments
@@ -840,6 +903,125 @@ impl Room {
Ok(())
}
/// Clear the event cache storage for the current room.
///
/// This will remove all the information related to the event cache, in
/// memory and in the persisted storage, if enabled.
pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> {
let (room_event_cache, _drop_handles) = self.inner.event_cache().await?;
room_event_cache.clear().await?;
Ok(())
}
/// Subscribes to requests to join this room (knock member events), using a
/// `listener` to be notified of the changes.
///
/// The current requests to join the room will be emitted immediately
/// when subscribing, along with a [`TaskHandle`] to cancel the
/// subscription.
pub async fn subscribe_to_knock_requests(
self: Arc<Self>,
listener: Box<dyn KnockRequestsListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let stream = self.inner.subscribe_to_knock_requests().await?;
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(stream);
while let Some(requests) = stream.next().await {
listener.call(requests.into_iter().map(Into::into).collect());
}
})));
Ok(handle)
}
/// Return a debug representation for the internal room events data
/// structure, one line per entry in the resulting vector.
pub async fn room_events_debug_string(&self) -> Result<Vec<String>, ClientError> {
let (cache, _drop_guards) = self.inner.event_cache().await?;
Ok(cache.debug_string().await)
}
}
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
Self {
event_id: request.event_id.to_string(),
user_id: request.member_info.user_id.to_string(),
room_id: request.room_id().to_string(),
display_name: request.member_info.display_name.clone(),
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
reason: request.member_info.reason.clone(),
timestamp: request.timestamp.map(|ts| ts.into()),
is_seen: request.is_seen,
actions: Arc::new(KnockRequestActions { inner: request }),
}
}
}
/// A listener for receiving new requests to a join a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait KnockRequestsListener: Send + Sync {
fn call(&self, join_requests: Vec<KnockRequest>);
}
/// An FFI representation of a request to join a room.
#[derive(Debug, Clone, uniffi::Record)]
pub struct KnockRequest {
/// The event id of the event that contains the `knock` membership change.
pub event_id: String,
/// The user id of the user who's requesting to join the room.
pub user_id: String,
/// The room id of the room whose access was requested.
pub room_id: String,
/// The optional display name of the user who's requesting to join the room.
pub display_name: Option<String>,
/// The optional avatar url of the user who's requesting to join the room.
pub avatar_url: Option<String>,
/// An optional reason why the user wants join the room.
pub reason: Option<String>,
/// The timestamp when this request was created.
pub timestamp: Option<u64>,
/// Whether the knock request has been marked as `seen` so it can be
/// filtered by the client.
pub is_seen: bool,
/// A set of actions to perform for this knock request.
pub actions: Arc<KnockRequestActions>,
}
/// A set of actions to perform for a knock request.
#[derive(Debug, Clone, uniffi::Object)]
pub struct KnockRequestActions {
inner: matrix_sdk::room::knock_requests::KnockRequest,
}
#[matrix_sdk_ffi_macros::export]
impl KnockRequestActions {
/// Accepts the knock request by inviting the user to the room.
pub async fn accept(&self) -> Result<(), ClientError> {
self.inner.accept().await.map_err(Into::into)
}
/// Declines the knock request by kicking the user from the room with an
/// optional reason.
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
}
/// Declines the knock request by banning the user from the room with an
/// optional reason.
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
}
/// Marks the knock request as 'seen'.
///
/// **IMPORTANT**: this won't update the current reference to this request,
/// a new one with the updated value should be emitted instead.
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
self.inner.mark_as_seen().await.map_err(Into::into)
}
}
/// Generates a `matrix.to` permalink to the given room alias.
@@ -973,7 +1155,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
match media_source.as_ref() {
match &media_source.as_ref().media_source {
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
}
+11 -1
View File
@@ -1,8 +1,10 @@
use std::collections::HashMap;
use matrix_sdk::RoomState;
use tracing::warn;
use crate::{
client::JoinRule,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero},
room_member::RoomMember,
@@ -54,8 +56,10 @@ pub struct RoomInfo {
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
/// The currently pinned event ids
/// The currently pinned event ids.
pinned_event_ids: Vec<String>,
/// The join rule for this room, if known.
join_rule: Option<JoinRule>,
}
impl RoomInfo {
@@ -70,6 +74,11 @@ impl RoomInfo {
let pinned_event_ids =
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
let join_rule = room.join_rule().try_into();
if let Err(e) = &join_rule {
warn!("Failed to parse join rule: {:?}", e);
}
Ok(Self {
id: room.room_id().to_string(),
creator: room.creator().as_ref().map(ToString::to_string),
@@ -118,6 +127,7 @@ impl RoomInfo {
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
join_rule: join_rule.ok(),
})
}
}
+12 -1
View File
@@ -616,7 +616,8 @@ impl RoomListItem {
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() {
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
{
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
@@ -624,6 +625,16 @@ impl RoomListItem {
(room_or_alias_id, server_names)
};
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
}
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
+11 -2
View File
@@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_member::RoomMember,
utils::AsyncRuntimeDropped,
};
@@ -38,6 +41,10 @@ impl RoomPreview {
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
heroes: info
.heroes
.as_ref()
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
})
}
@@ -85,13 +92,15 @@ pub struct RoomPreviewInfo {
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: RoomType,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
pub is_history_world_readable: Option<bool>,
/// The membership state for the current user, if known.
pub membership: Option<Membership>,
/// The join rule for this room (private, public, knock, etc.).
pub join_rule: JoinRule,
/// Whether the room is direct or not, if known.
pub is_direct: Option<bool>,
/// Room heroes.
pub heroes: Option<Vec<RoomHero>>,
}
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
+133 -67
View File
@@ -15,9 +15,7 @@
use std::{collections::BTreeSet, sync::Arc, time::Duration};
use extension_trait::extension_trait;
use matrix_sdk::attachment::{
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
};
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
use ruma::{
assign,
events::{
@@ -42,7 +40,8 @@ use ruma::{
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
},
},
matrix_uri::MatrixId as RumaMatrixId,
@@ -154,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId {
}
}
#[matrix_sdk_ffi_macros::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_new(
msgtype: MessageType,
@@ -200,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
)))
}
#[extension_trait]
pub impl MediaSourceExt for MediaSource {
fn from_json(json: String) -> Result<MediaSource, ClientError> {
let res = serde_json::from_str(&json)?;
Ok(res)
#[derive(Clone, uniffi::Object)]
pub struct MediaSource {
pub(crate) media_source: RumaMediaSource,
}
#[matrix_sdk_ffi_macros::export]
impl MediaSource {
#[uniffi::constructor]
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
let media_source = RumaMediaSource::Plain(url.into());
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
fn to_json(&self) -> String {
serde_json::to_string(self).expect("Media source should always be serializable ")
pub fn url(&self) -> String {
self.media_source.url()
}
// Used on Element X Android
#[uniffi::constructor]
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
// Used on Element X Android
pub fn to_json(&self) -> String {
serde_json::to_string(&self.media_source)
.expect("Media source should always be serializable ")
}
}
impl TryFrom<RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value })
}
}
impl TryFrom<&RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value.clone() })
}
}
impl From<MediaSource> for RumaMediaSource {
fn from(value: MediaSource) -> Self {
value.media_source
}
}
#[extension_trait]
pub(crate) impl MediaSourceExt for RumaMediaSource {
fn verify(&self) -> Result<(), ClientError> {
match self {
RumaMediaSource::Plain(url) => {
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
RumaMediaSource::Encrypted(file) => {
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
}
Ok(())
}
fn url(&self) -> String {
match self {
MediaSource::Plain(url) => url.to_string(),
MediaSource::Encrypted(file) => file.url.to_string(),
RumaMediaSource::Plain(url) => url.to_string(),
RumaMediaSource::Encrypted(file) => file.url.to_string(),
}
}
}
@@ -280,7 +337,7 @@ fn get_body_and_filename(filename: String, caption: Option<String>) -> (String,
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = serde_json::Error;
type Error = ClientError;
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
Ok(match value {
@@ -292,7 +349,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Image { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaImageMessageEventContent::new(body, (*content.source).clone())
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
@@ -301,7 +358,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(body, (*content.source).clone())
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
@@ -310,7 +367,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(body, (*content.source).clone())
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
@@ -319,7 +376,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(body, (*content.source).clone())
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
@@ -345,9 +402,11 @@ impl TryFrom<MessageType> for RumaMessageType {
}
}
impl From<RumaMessageType> for MessageType {
fn from(value: RumaMessageType) -> Self {
match value {
impl TryFrom<RumaMessageType> for MessageType {
type Error = ClientError;
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
Ok(match value {
RumaMessageType::Emote(c) => MessageType::Emote {
content: EmoteMessageContent {
body: c.body.clone(),
@@ -359,16 +418,17 @@ impl From<RumaMessageType> for MessageType {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
voice: c.voice.map(Into::into),
@@ -379,8 +439,8 @@ impl From<RumaMessageType> for MessageType {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
RumaMessageType::File(c) => MessageType::File {
@@ -388,8 +448,8 @@ impl From<RumaMessageType> for MessageType {
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
RumaMessageType::Notice(c) => MessageType::Notice {
@@ -425,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
},
}
})
}
}
@@ -520,7 +580,7 @@ impl From<ImageInfo> for RumaImageInfo {
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()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
blurhash: value.blurhash,
})
}
@@ -625,7 +685,7 @@ impl From<VideoInfo> for RumaVideoInfo {
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()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
blurhash: value.blurhash,
})
}
@@ -668,7 +728,7 @@ impl From<FileInfo> for RumaFileInfo {
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()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
})
}
}
@@ -703,21 +763,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
}
}
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,
@@ -790,8 +835,10 @@ pub enum MessageFormat {
Unknown { format: String },
}
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
type Error = ClientError;
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -799,15 +846,20 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
size: info.size.map(Into::into),
});
Self {
Ok(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),
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
blurhash: info.blurhash.clone(),
}
})
}
}
@@ -821,8 +873,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
}
}
impl From<&RumaVideoInfo> for VideoInfo {
fn from(info: &RumaVideoInfo) -> Self {
impl TryFrom<&RumaVideoInfo> for VideoInfo {
type Error = ClientError;
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -830,21 +884,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
size: info.size.map(Into::into),
});
Self {
Ok(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),
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
blurhash: info.blurhash.clone(),
}
})
}
}
impl From<&RumaFileInfo> for FileInfo {
fn from(info: &RumaFileInfo) -> Self {
impl TryFrom<&RumaFileInfo> for FileInfo {
type Error = ClientError;
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -852,12 +913,17 @@ impl From<&RumaFileInfo> for FileInfo {
size: info.size.map(Into::into),
});
Self {
Ok(Self {
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
}
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
})
}
}
@@ -201,6 +201,22 @@ pub struct UnableToDecryptInfo {
/// What we know about what caused this UTD. E.g. was this event sent when
/// we were not a member of this room?
pub cause: UtdCause,
/// The difference between the event creation time (`origin_server_ts`) and
/// the time our device was created. If negative, this event was sent
/// *before* our device was created.
pub event_local_age_millis: i64,
/// Whether the user had verified their own identity at the point they
/// received the UTD event.
pub user_trusts_own_identity: bool,
/// The homeserver of the user that sent the undecryptable event.
pub sender_homeserver: String,
/// Our local user's own homeserver, or `None` if the client is not logged
/// in.
pub own_homeserver: Option<String>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
@@ -209,6 +225,10 @@ impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
cause: value.cause,
event_local_age_millis: value.event_local_age_millis,
user_trusts_own_identity: value.user_trusts_own_identity,
sender_homeserver: value.sender_homeserver.to_string(),
own_homeserver: value.own_homeserver.map(String::from),
}
}
}
+43 -12
View File
@@ -16,26 +16,55 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::{room::MediaSource, FullStateEventContent};
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match value {
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
Content::Message(message) => {
let msgtype = message.msgtype().msgtype().to_owned();
match TryInto::<MessageContent>::try_into(message) {
Ok(message) => TimelineItemContent::Message { content: message },
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: msgtype,
error: error.to_string(),
},
}
}
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContent::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
source: Arc::new(MediaSource::from(content.source.clone())),
let media_source = RumaMediaSource::from(content.source.clone());
if let Err(error) = media_source.verify() {
return TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
};
}
match TryInto::<ImageInfo>::try_into(&content.info) {
Ok(info) => TimelineItemContent::Sticker {
body: content.body.clone(),
info,
source: Arc::new(MediaSource { media_source }),
},
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
},
}
}
@@ -117,16 +146,18 @@ pub struct MessageContent {
pub mentions: Option<Mentions>,
}
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
Self {
msg_type: value.msgtype().clone().into(),
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
type Error = ClientError;
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
Ok(Self {
msg_type: value.msgtype().clone().try_into()?,
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
}
})
}
}
+64 -13
View File
@@ -24,7 +24,7 @@ use matrix_sdk::crypto::CollectStrategy;
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
room::edit::EditedContent as SdkEditedContent,
@@ -53,7 +53,7 @@ use ruma::{
},
AnyMessageLikeEventContent,
},
EventId,
EventId, UInt,
};
use tokio::{
sync::Mutex,
@@ -144,19 +144,26 @@ fn build_thumbnail_info(
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 height = thumbnail_info
.height
.and_then(|u| UInt::try_from(u).ok())
.ok_or(RoomError::InvalidAttachmentData)?;
let width = thumbnail_info
.width
.and_then(|u| UInt::try_from(u).ok())
.ok_or(RoomError::InvalidAttachmentData)?;
let size = thumbnail_info
.size
.and_then(|u| UInt::try_from(u).ok())
.ok_or(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)?;
let thumbnail = Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
};
let thumbnail =
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
Ok(AttachmentConfig::with_thumbnail(thumbnail))
}
@@ -545,6 +552,7 @@ impl Timeline {
.await
{
Ok(()) => Ok(()),
Err(timeline::Error::EventNotInTimeline(_)) => {
// If we couldn't edit, assume it was an (remote) event that wasn't in the
// timeline, and try to edit it via the room itself.
@@ -560,7 +568,8 @@ impl Timeline {
room.send_queue().send(edit_event).await?;
Ok(())
}
Err(err) => Err(err)?,
Err(err) => Err(err.into()),
}
}
@@ -977,7 +986,7 @@ impl TimelineItem {
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::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }),
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
}
}
@@ -1246,8 +1255,9 @@ impl SendAttachmentJoinHandle {
/// 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 divider between messages of different day or month depending on
/// timeline settings.
DateDivider {
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
ts: u64,
@@ -1278,6 +1288,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
#[derive(Clone, uniffi::Enum)]
pub enum EditedContent {
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
PollStart { poll_data: PollData },
}
@@ -1288,6 +1299,12 @@ impl TryFrom<EditedContent> for SdkEditedContent {
EditedContent::RoomMessage { content } => {
Ok(SdkEditedContent::RoomMessage((*content).clone()))
}
EditedContent::MediaCaption { caption, formatted_caption } => {
Ok(SdkEditedContent::MediaCaption {
caption,
formatted_caption: formatted_caption.map(Into::into),
})
}
EditedContent::PollStart { poll_data } => {
let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
Ok(SdkEditedContent::PollStart {
@@ -1299,6 +1316,23 @@ impl TryFrom<EditedContent> for SdkEditedContent {
}
}
/// Create a caption edit.
///
/// If no `formatted_caption` is provided, then it's assumed the `caption`
/// represents valid Markdown that can be used as the formatted caption.
#[matrix_sdk_ffi_macros::export]
fn create_caption_edit(
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
) -> EditedContent {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
EditedContent::MediaCaption {
caption,
formatted_caption: formatted_caption.as_ref().map(Into::into),
}
}
/// Wrapper to retrieve some timeline item info lazily.
#[derive(Clone, uniffi::Object)]
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
@@ -1325,3 +1359,20 @@ impl LazyTimelineItemProvider {
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
}
}
/// Changes how date dividers get inserted, either in between each day or in
/// between each month
#[derive(Debug, Clone, uniffi::Enum)]
pub enum DateDividerMode {
Daily,
Monthly,
}
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
fn from(value: DateDividerMode) -> Self {
match value {
DateDividerMode::Daily => Self::Daily,
DateDividerMode::Monthly => Self::Monthly,
}
}
}
-47
View File
@@ -1,47 +0,0 @@
# This git-cliff configuration file is used to generate weekly reports for This
# Week in Matrix amongst others.
[changelog]
header = """
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
"""
body = """
{% for commit in commits %}
{% set_global commit_message = commit.message -%}
{% for footer in commit.footers -%}
{% if footer.token | lower == "changelog" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "breaking-change" -%}
{% set_global commit_message = footer.value -%}
{% endif -%}
{% endfor -%}
- {{ commit_message | upper_first }}
{% endfor %}
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = true
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
]
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore", skip = true },
{ message = "^style", group = "Styling", skip = true },
{ message = "^test", skip = true },
{ message = "^ci", skip = true },
]
filter_commits = true
tag_pattern = "[0-9]*"
skip_tags = ""
ignore_tags = ""
date_order = false
sort_commits = "newest"
-91
View File
@@ -1,91 +0,0 @@
# This git-cliff configuration file is used to generate release reports.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{% set_global commit_message = commit.message -%}
{% set_global breaking = commit.breaking -%}
{% for footer in commit.footers -%}
{% if footer.token | lower == "changelog" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "breaking-change" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "security-impact" -%}
{% set_global security_impact = footer.value -%}
{% elif footer.token | lower == "cve" -%}
{% set_global cve = footer.value -%}
{% elif footer.token | lower == "github-advisory" -%}
{% set_global github_advisory = footer.value -%}
{% endif -%}
{% endfor -%}
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
{% if security_impact -%}
(\
*{{ security_impact | upper_first }}*\
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
)
{% endif -%}
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ footer = "Security-Impact:", group = "Security" },
{ footer = "CVE:", group = "Security" },
{ footer = "GitHub-Advisory:", group = "Security" },
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore", skip = true },
{ message = "^style", group = "Styling", skip = true },
{ message = "^test", skip = true },
{ message = "^ci", skip = true },
]
# forbid parsers from skipping breaking changes
protect_breaking_commits = true
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "[0-9]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
+21
View File
@@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.9.0] - 2024-12-18
### Features
- Introduced support for
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
the designation of certain users as service members. These flagged users are
excluded from the room display name calculation.
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
### Bug Fixes
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
called. Previously, items following the removed item were not shifted left by
one position, leaving them at incorrect indices.
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
## [0.8.0] - 2024-11-19
### Bug Fixes
+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.8.0"
version = "0.9.0"
[package.metadata.docs.rs]
all-features = true
@@ -30,7 +30,7 @@ uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
@@ -49,9 +49,9 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.4.0", features = ["serde"] }
decancer = "3.2.4"
eyeball = { workspace = true }
bitflags = { version = "2.6.0", features = ["serde"] }
decancer = "3.2.8"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
growable-bloom-filter = { workspace = true }
@@ -61,9 +61,9 @@ matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
regex = "1.11.0"
regex = "1.11.1"
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
unicode-normalization = "0.1.24"
unicode-normalization = { workspace = true }
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
@@ -85,7 +85,7 @@ similar-asserts = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.33"
wasm-bindgen-test = { workspace = true }
[lints]
workspace = true
+11 -8
View File
@@ -26,8 +26,8 @@ use eyeball_im::{Vector, VectorDiff};
use futures_util::Stream;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings,
EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest,
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
EncryptionSettings, EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult,
TrustRequirement,
};
#[cfg(feature = "e2e-encryption")]
@@ -1459,10 +1459,14 @@ impl BaseClient {
pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
match self.olm_machine().await.as_ref() {
Some(o) => {
let (history_visibility, settings) = self
.get_room(room_id)
.map(|r| (r.history_visibility(), r.encryption_settings()))
.unwrap_or((HistoryVisibility::Joined, None));
let Some(room) = self.get_room(room_id) else {
return Err(Error::InsufficientData);
};
let history_visibility = room.history_visibility_or_default();
let Some(room_encryption_event) = room.encryption_settings() else {
return Err(Error::EncryptionNotEnabled);
};
// Don't share the group session with members that are invited
// if the history visibility is set to `Joined`
@@ -1474,9 +1478,8 @@ impl BaseClient {
let members = self.store.get_user_ids(room_id, filter).await?;
let settings = settings.ok_or(Error::EncryptionNotEnabled)?;
let settings = EncryptionSettings::new(
settings,
room_encryption_event,
history_visibility,
self.room_key_recipient_strategy.clone(),
);
+4 -4
View File
@@ -27,7 +27,7 @@ use ruma::{
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(DebugRawEventNoId));
@@ -41,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
impl fmt::Debug for DebugInvitedRoom<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvitedRoom")
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
@@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
impl fmt::Debug for DebugKnockedRoom<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KnockedRoom")
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
@@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(DebugRawEvent));
@@ -30,7 +30,7 @@ use ruma::{
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
@@ -160,12 +160,12 @@ impl PartialEq for DisplayName {
impl DisplayName {
/// Regex pattern matching an MXID.
const MXID_PATTERN: &str = "@.+[:.].+";
const MXID_PATTERN: &'static str = "@.+[:.].+";
/// Regex pattern matching some left-to-right formatting marks:
/// * LTR and RTL marks U+200E and U+200F
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
/// Regex pattern matching bunch of unicode control characters and otherwise
/// misleading/invisible characters.
@@ -176,7 +176,7 @@ impl DisplayName {
/// * Blank/invisible characters (U2800, U2062-U2063)
/// * Arabic Letter RTL mark U+061C
/// * Zero width no-break space (BOM) U+FEFF
const HIDDEN_CHARACTERS_PATTERN: &str =
const HIDDEN_CHARACTERS_PATTERN: &'static str =
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
/// Creates a new [`DisplayName`] from the given raw string.
@@ -476,6 +476,23 @@ impl MemberEvent {
.unwrap_or_else(|| self.user_id().localpart()),
)
}
/// The optional reason why the membership changed.
pub fn reason(&self) -> Option<&str> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
_ => None,
}
}
/// The optional timestamp for this member event.
pub fn timestamp(&self) -> Option<UInt> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
_ => None,
}
}
}
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
@@ -14,13 +14,85 @@
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use assert_matches::assert_matches;
use async_trait::async_trait;
use matrix_sdk_common::{
deserialized_responses::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind,
VerificationState,
},
linked_chunk::{ChunkContent, LinkedChunk, LinkedChunkBuilder, Position, RawChunk, Update},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
push::Action, room_id, uint, RoomId,
};
use super::DynEventCacheStore;
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
use crate::{
event_cache::{Event, Gap},
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
};
/// Create a test event with all data filled, for testing that linked chunk
/// correctly stores event data.
///
/// Keep in sync with [`check_test_event`].
pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
let encryption_info = EncryptionInfo {
sender: (*ALICE).into(),
sender_device: None,
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "1337".to_owned(),
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
};
let event = EventFactory::new()
.text_msg(content)
.room(room_id)
.sender(*ALICE)
.into_raw_timeline()
.cast();
SyncTimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event,
encryption_info,
unsigned_encryption_info: None,
}),
push_actions: vec![Action::Notify],
}
}
/// Check that an event created with [`make_test_event`] contains the expected
/// data.
///
/// Keep in sync with [`make_test_event`].
#[track_caller]
pub fn check_test_event(event: &SyncTimelineEvent, text: &str) {
// Check push actions.
let actions = &event.push_actions;
assert_eq!(actions.len(), 1);
assert_matches!(&actions[0], Action::Notify);
// Check content.
assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => {
// Check encryption fields.
assert_eq!(d.encryption_info.sender, *ALICE);
assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => {
assert_eq!(curve25519_key, "1337");
});
// Check event.
let deserialized = d.event.deserialize().unwrap();
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
assert_eq!(msg.as_original().unwrap().content.body(), text);
});
});
}
/// `EventCacheStore` integration tests.
///
@@ -34,6 +106,21 @@ pub trait EventCacheStoreIntegrationTests {
/// Test replacing a MXID.
async fn test_replace_media_key(&self);
/// Test handling updates to a linked chunk and reloading these updates from
/// the store.
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
/// Test that rebuilding a linked chunk from an empty store doesn't return
/// anything.
async fn test_rebuild_empty_linked_chunk(&self);
/// Test that clear all the rooms' linked chunks works.
async fn test_clear_all_rooms_chunks(&self);
}
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -83,6 +170,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
Some(&content),
"media not found though added"
);
assert_eq!(
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
Some(&content),
"media not found by URI though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
@@ -92,6 +184,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media still found by URI after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone())
@@ -116,6 +212,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
"thumbnail not found"
);
// We get a file with the URI, we don't know which one.
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
"media not found by URI though two where added"
);
// Let's add another media with a different URI.
self.add_media_content(&request_other_file, other_content.clone())
.await
@@ -127,6 +229,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
Some(&other_content),
"other file not found"
);
assert_eq!(
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
Some(&other_content),
"other file not found by URI"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
@@ -143,6 +250,14 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media found by URI wasn't removed"
);
assert!(
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
"other media found by URI was removed"
);
}
async fn test_replace_media_key(&self) {
@@ -182,6 +297,149 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Finding with the new request does work.
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
}
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
let room_id = room_id!("!r0:matrix.org");
self.handle_linked_chunk_updates(
room_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![
make_test_event(room_id, "hello"),
make_test_event(room_id, "world"),
],
},
// a gap chunk
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "parmesan".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(room_id, "sup")],
},
],
)
.await
.unwrap();
// The linked chunk is correctly reloaded.
let raws = self.reload_linked_chunk(room_id).await.unwrap();
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
let mut chunks = lc.chunks();
{
let first = chunks.next().unwrap();
// Note: we can't assert the previous/next chunks, as these fields and their
// getters are private.
assert_eq!(first.identifier(), CId::new(0));
assert_matches!(first.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 2);
check_test_event(&events[0], "hello");
check_test_event(&events[1], "world");
});
}
{
let second = chunks.next().unwrap();
assert_eq!(second.identifier(), CId::new(1));
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "parmesan");
});
}
{
let third = chunks.next().unwrap();
assert_eq!(third.identifier(), CId::new(2));
assert_matches!(third.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
check_test_event(&events[0], "sup");
});
}
assert!(chunks.next().is_none());
}
async fn test_rebuild_empty_linked_chunk(&self) {
// When I rebuild a linked chunk from an empty store, it's empty.
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
assert!(rebuild_linked_chunk(raw_parts).is_none());
}
async fn test_clear_all_rooms_chunks(&self) {
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
let r0 = room_id!("!r0:matrix.org");
let r1 = room_id!("!r1:matrix.org");
// Add updates for the first room.
self.handle_linked_chunk_updates(
r0,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
},
],
)
.await
.unwrap();
// Add updates for the second room.
self.handle_linked_chunk_updates(
r1,
vec![
// Empty items chunk.
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// a gap chunk
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(r0, "yummy")],
},
],
)
.await
.unwrap();
// Sanity check: both linked chunks can be reloaded.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
// Clear the chunks.
self.clear_all_rooms_chunks().await.unwrap();
// Both rooms now have no linked chunk.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
}
}
/// Macro building to allow your `EventCacheStore` implementation to run the
@@ -236,6 +494,27 @@ macro_rules! event_cache_store_integration_tests {
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_replace_media_key().await;
}
#[async_test]
async fn test_handle_updates_and_rebuild_linked_chunk() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
}
#[async_test]
async fn test_rebuild_empty_linked_chunk() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_rebuild_empty_linked_chunk().await;
}
#[async_test]
async fn test_clear_all_rooms_chunks() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_clear_all_rooms_chunks().await;
}
}
};
}
@@ -16,12 +16,17 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti
use async_trait::async_trait;
use matrix_sdk_common::{
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{MxcUri, OwnedMxcUri};
use ruma::{MxcUri, OwnedMxcUri, RoomId};
use super::{EventCacheStore, EventCacheStoreError, Result};
use crate::media::{MediaRequestParameters, UniqueKey as _};
use crate::{
event_cache::{Event, Gap},
media::{MediaRequestParameters, UniqueKey as _},
};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
@@ -29,8 +34,14 @@ use crate::media::{MediaRequestParameters, UniqueKey as _};
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct MemoryStore {
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
leases: StdRwLock<HashMap<String, (String, Instant)>>,
inner: StdRwLock<MemoryStoreInner>,
}
#[derive(Debug)]
struct MemoryStoreInner {
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
leases: HashMap<String, (String, Instant)>,
events: RelationalLinkedChunk<Event, Gap>,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
@@ -39,8 +50,11 @@ const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20)
impl Default for MemoryStore {
fn default() -> Self {
Self {
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
leases: Default::default(),
inner: StdRwLock::new(MemoryStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
events: RelationalLinkedChunk::new(),
}),
}
}
}
@@ -63,7 +77,36 @@ impl EventCacheStore for MemoryStore {
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
let mut inner = self.inner.write().unwrap();
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
}
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
inner.events.apply_updates(room_id, updates);
Ok(())
}
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.reload_chunks(room_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.inner.write().unwrap().events.clear();
Ok(())
}
async fn add_media_content(
@@ -73,8 +116,10 @@ impl EventCacheStore for MemoryStore {
) -> 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));
let mut inner = self.inner.write().unwrap();
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
@@ -86,8 +131,10 @@ impl EventCacheStore for MemoryStore {
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut medias = self.media.write().unwrap();
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
let mut inner = self.inner.write().unwrap();
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
{
*mxc = to.uri().to_owned();
*key = to.unique_key();
}
@@ -98,8 +145,9 @@ impl EventCacheStore for MemoryStore {
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
let expected_key = request.unique_key();
let media = self.media.read().unwrap();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
let inner = self.inner.read().unwrap();
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
@@ -107,23 +155,38 @@ impl EventCacheStore for MemoryStore {
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
let expected_key = request.unique_key();
let mut media = self.media.write().unwrap();
let Some(index) = media
let mut inner = self.inner.write().unwrap();
let Some(index) = inner
.media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
inner.media.remove(index);
Ok(())
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
let inner = self.inner.read().unwrap();
Ok(inner.media.iter().find_map(|(media_uri, _media_key, media_content)| {
(media_uri == uri).then(|| media_content.to_owned())
}))
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let mut inner = self.inner.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
let positions = inner
.media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
@@ -133,7 +196,7 @@ impl EventCacheStore for MemoryStore {
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
inner.media.remove(position);
}
Ok(())
@@ -36,14 +36,14 @@ pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
pub use self::integration_tests::EventCacheStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
};
/// The high-level public type to represent an `EventCacheStore` lock.
#[derive(Clone)]
pub struct EventCacheStoreLock {
/// The inner cross process lock that is used to lock the `EventCacheStore`.
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
/// The store itself.
///
@@ -70,11 +70,11 @@ impl EventCacheStoreLock {
let store = store.into_event_cache_store();
Self {
cross_process_lock: CrossProcessStoreLock::new(
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
),
)),
store,
}
}
@@ -100,13 +100,13 @@ pub struct EventCacheStoreLockGuard<'a> {
}
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
}
}
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
impl Deref for EventCacheStoreLockGuard<'_> {
type Target = DynEventCacheStore;
fn deref(&self) -> &Self::Target {
@@ -138,12 +138,23 @@ pub enum EventCacheStoreError {
#[error("Error encoding or decoding data from the event cache store: {0}")]
Codec(#[from] Utf8Error),
/// The store failed to serialize or deserialize some data.
#[error("Error serializing or deserializing data from the event cache store: {0}")]
Serialization(#[from] serde_json::Error),
/// The database format has changed in a backwards incompatible way.
#[error(
"The database format of the event cache store changed in an incompatible way, \
current version: {0}, latest version: {1}"
)]
UnsupportedDatabaseVersion(usize, usize),
/// The store contains invalid data.
#[error("The store contains invalid data: {details}")]
InvalidData {
/// Details why the data contained in the store was invalid.
details: String,
},
}
impl EventCacheStoreError {
@@ -15,11 +15,22 @@
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::MxcUri;
use matrix_sdk_common::{
linked_chunk::{RawChunk, Update},
AsyncTraitDeps,
};
use ruma::{MxcUri, RoomId};
use super::EventCacheStoreError;
use crate::media::MediaRequestParameters;
use crate::{
event_cache::{Event, Gap},
media::MediaRequestParameters,
};
/// A default capacity for linked chunks, when manipulating in conjunction with
/// an `EventCacheStore` implementation.
// TODO: move back?
pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
@@ -37,6 +48,28 @@ pub trait EventCacheStore: AsyncTraitDeps {
holder: &str,
) -> Result<bool, Self::Error>;
/// An [`Update`] reflects an operation that has happened inside a linked
/// chunk. The linked chunk is used by the event cache to store the events
/// in-memory. This method aims at forwarding this update inside this store.
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error>;
/// Return all the raw components of a linked chunk, so the caller may
/// reconstruct the linked chunk later.
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
/// Clear persisted events for all the rooms.
///
/// This will empty and remove all the linked chunks stored previously,
/// using the above [`Self::handle_linked_chunk_updates`] methods.
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
@@ -95,6 +128,23 @@ pub trait EventCacheStore: AsyncTraitDeps {
request: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// In theory, there could be several files stored using the same URI and a
/// different `MediaFormat`. This API is meant to be used with a media file
/// that has only been stored with a single format.
///
/// If there are several media files for a given URI in different formats,
/// this API will only return one of them. Which one is left as an
/// implementation detail.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
async fn get_media_content_for_uri(&self, uri: &MxcUri)
-> Result<Option<Vec<u8>>, Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
@@ -131,6 +181,25 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
}
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
}
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
@@ -161,6 +230,13 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
+6 -5
View File
@@ -74,7 +74,7 @@ pub fn is_suitable_for_latest_event<'a>(
// Check if this is a replacement for another message. If it is, ignore it
if let Some(original_message) = message.as_original() {
let is_replacement =
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
relation_type == RelationType::Replacement
} else {
@@ -83,12 +83,13 @@ pub fn is_suitable_for_latest_event<'a>(
});
if is_replacement {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
PossibleLatestEvent::NoUnsupportedMessageLikeType
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
return PossibleLatestEvent::YesRoomMessage(message);
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
return PossibleLatestEvent::YesRoomMessage(message);
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
+1 -1
View File
@@ -438,7 +438,7 @@ fn events_intersects<'a>(
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
new_events
.iter()
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
}
/// Given a set of events coming from sync, for a room, update the
@@ -18,9 +18,11 @@ use std::{
};
use ruma::{
events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType},
events::{
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
},
serde::Raw,
OwnedUserId, RoomId,
RoomId,
};
use tracing::{debug, instrument, trace, warn};
@@ -94,10 +96,10 @@ impl AccountDataProcessor {
for event in events {
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedUserId>>::new();
for (user_id, rooms) in direct_event.content.iter() {
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
for (user_identifier, rooms) in direct_event.content.iter() {
for room_id in rooms {
new_dms.entry(room_id).or_default().insert(user_id.clone());
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
}
}
+3 -2
View File
@@ -1,4 +1,4 @@
#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
mod members;
pub(crate) mod normal;
@@ -21,6 +21,7 @@ use ruma::{
events::{
beacon_info::BeaconInfoEventContent,
call::member::{CallMemberEventContent, CallMemberStateKey},
direct::OwnedDirectUserIdentifier,
macros::EventContent,
room::{
avatar::RoomAvatarEventContent,
@@ -127,7 +128,7 @@ pub struct BaseRoomInfo {
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
/// A list of user ids this room is considered as direct message, if this
/// room is a DM.
pub(crate) dm_targets: HashSet<OwnedUserId>,
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
/// The `m.room.encryption` event content that enabled E2EE in this room.
pub(crate) encryption: Option<RoomEncryptionEventContent>,
/// The guest access policy of this room.
File diff suppressed because it is too large Load Diff
+30 -19
View File
@@ -269,7 +269,7 @@ impl BaseClient {
.or_insert_with(JoinedRoomUpdate::default)
.account_data
.append(&mut raw.to_vec()),
RoomState::Left => new_rooms
RoomState::Left | RoomState::Banned => new_rooms
.leave
.entry(room_id.to_owned())
.or_insert_with(LeftRoomUpdate::default)
@@ -546,7 +546,7 @@ impl BaseClient {
))
}
RoomState::Left => Ok((
RoomState::Left | RoomState::Banned => Ok((
room_info,
None,
Some(LeftRoomUpdate::new(
@@ -911,7 +911,7 @@ mod tests {
api::client::sync::sync_events::UnreadNotificationsCount,
assign, event_id,
events::{
direct::DirectEventContent,
direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier},
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
@@ -1247,7 +1247,7 @@ mod tests {
room.required_state.push(make_state_event(
user_b_id,
user_a_id.as_str(),
RoomMemberEventContent::new(membership),
RoomMemberEventContent::new(membership.clone()),
None,
));
let response = response_with_room(room_id, room);
@@ -1256,8 +1256,17 @@ mod tests {
.await
.expect("Failed to process sync");
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
match membership {
MembershipState::Leave => {
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
}
MembershipState::Ban => {
// The room is banned.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Banned);
}
_ => panic!("Unexpected membership state found: {membership}"),
}
// And it is added to the list of left rooms only.
assert!(!sync_resp.rooms.join.contains_key(room_id));
@@ -1337,7 +1346,7 @@ mod tests {
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
// (Sanity: B is a direct target, and is in Join state)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
// When B leaves
@@ -1346,7 +1355,7 @@ mod tests {
// Then B is still a direct target, and is in Leave state (B is a direct target
// because we want to return to our old DM in the UI even if the other
// user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
}
@@ -1362,7 +1371,7 @@ mod tests {
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
// (Sanity: B is a direct target, and is in Invite state)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
// When B declines the invitation (i.e. leaves)
@@ -1371,7 +1380,7 @@ mod tests {
// Then B is still a direct target, and is in Leave state (B is a direct target
// because we want to return to our old DM in the UI even if the other
// user left, so we can reinvite them. See https://github.com/matrix-org/matrix-rust-sdk/issues/2017)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
}
@@ -1389,7 +1398,7 @@ mod tests {
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
// (Sanity: B is a direct target, and is in Join state)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
let room = client.get_room(room_id).unwrap();
@@ -1413,7 +1422,7 @@ mod tests {
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
// (Sanity: B is a direct target, and is in Join state)
assert!(direct_targets(&client, room_id).contains(user_b_id));
assert!(direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id)));
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
let room = client.get_room(room_id).unwrap();
@@ -2558,9 +2567,10 @@ mod tests {
let mut room_response = http::response::Room::new();
set_room_joined(&mut room_response, user_a_id);
let mut response = response_with_room(room_id_1, room_response);
let mut direct_content = BTreeMap::new();
direct_content.insert(user_a_id.to_owned(), vec![room_id_1.to_owned()]);
direct_content.insert(user_b_id.to_owned(), vec![room_id_2.to_owned()]);
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
BTreeMap::new();
direct_content.insert(user_a_id.into(), vec![room_id_1.to_owned()]);
direct_content.insert(user_b_id.into(), vec![room_id_2.to_owned()]);
response
.extensions
.account_data
@@ -2656,7 +2666,7 @@ mod tests {
.unwrap(),
UnableToDecryptInfo {
session_id: Some("".to_owned()),
reason: UnableToDecryptReason::MissingMegolmSession,
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
},
)
}
@@ -2671,7 +2681,7 @@ mod tests {
member.membership().clone()
}
fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedUserId> {
fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedDirectUserIdentifier> {
let room = client.get_room(room_id).expect("Room not found!");
room.direct_targets()
}
@@ -2730,8 +2740,9 @@ mod tests {
user_id: OwnedUserId,
room_ids: Vec<OwnedRoomId>,
) {
let mut direct_content = BTreeMap::new();
direct_content.insert(user_id, room_ids);
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
BTreeMap::new();
direct_content.insert(user_id.into(), room_ids);
response
.extensions
.account_data
@@ -90,6 +90,8 @@ pub trait StateStoreIntegrationTests {
async fn test_send_queue_priority(&self);
/// Test operations related to send queue dependents.
async fn test_send_queue_dependents(&self);
/// Test an update to a send queue dependent request.
async fn test_update_send_queue_dependent(&self);
/// Test saving/restoring server capabilities.
async fn test_server_capabilities_saving(&self);
}
@@ -972,6 +974,24 @@ impl StateStoreIntegrationTests for DynStateStore {
self.populate().await?;
{
// Add a send queue request in that room.
let txn = TransactionId::new();
let ev =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
.unwrap();
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
// Add a single dependent queue request.
self.save_dependent_queued_request(
room_id,
&txn,
ChildTransactionId::new(),
DependentQueuedRequestKind::RedactEvent,
)
.await?;
}
self.remove_room(room_id).await?;
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
@@ -1023,6 +1043,8 @@ impl StateStoreIntegrationTests for DynStateStore {
.is_empty(),
"still event recepts in the store"
);
assert!(self.load_send_queue_requests(room_id).await?.is_empty());
assert!(self.load_dependent_queued_requests(room_id).await?.is_empty());
self.remove_room(stripped_room_id).await?;
@@ -1458,7 +1480,7 @@ impl StateStoreIntegrationTests for DynStateStore {
// Update the event id.
let event_id = owned_event_id!("$1");
let num_updated = self
.update_dependent_queued_request(
.mark_dependent_queued_requests_as_ready(
room_id,
&txn0,
SentRequestKey::Event(event_id.clone()),
@@ -1528,6 +1550,54 @@ impl StateStoreIntegrationTests for DynStateStore {
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 2);
}
async fn test_update_send_queue_dependent(&self) {
let room_id = room_id!("!test_send_queue_dependents:localhost");
let txn = TransactionId::new();
// Save a dependent redaction for an event.
let child_txn = ChildTransactionId::new();
self.save_dependent_queued_request(
room_id,
&txn,
child_txn.clone(),
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].parent_key.is_none());
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Make it a reaction, instead of a redaction.
self.update_dependent_queued_request(
room_id,
&child_txn,
DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() },
)
.await
.unwrap();
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].parent_key.is_none());
assert_matches!(
&dependents[0].kind,
DependentQueuedRequestKind::ReactEvent { key } => {
assert_eq!(key, "👍");
}
);
}
}
/// Macro building to allow your StateStore implementation to run the entire
@@ -1686,6 +1756,12 @@ macro_rules! statestore_integration_tests {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_dependents().await;
}
#[async_test]
async fn test_update_send_queue_dependent() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_update_send_queue_dependent().await;
}
}
};
}
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,7 @@ use std::{
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use ruma::{
events::{
direct::OwnedDirectUserIdentifier,
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
@@ -200,12 +201,17 @@ impl BaseRoomInfoV1 {
MinimalStateEvent::Redacted(ev) => MinimalStateEvent::Redacted(ev),
});
let mut converted_dm_targets = HashSet::new();
for dm_target in dm_targets {
converted_dm_targets.insert(OwnedDirectUserIdentifier::from(dm_target));
}
Box::new(BaseRoomInfo {
avatar,
beacons: BTreeMap::new(),
canonical_alias,
create,
dm_targets,
dm_targets: converted_dm_targets,
encryption,
guest_access,
history_visibility,
@@ -138,6 +138,13 @@ where
L: Hash + Eq + ?Sized,
{
let position = self.mapping.remove(key)?;
// Reindex every mapped entry that is after the position we're looking to
// remove.
for mapped_pos in self.mapping.values_mut().filter(|pos| **pos > position) {
*mapped_pos = mapped_pos.saturating_sub(1);
}
Some(self.values.remove(position))
}
}
@@ -195,6 +202,12 @@ mod tests {
assert_eq!(map.get(&'a'), Some(&'E'));
assert_eq!(map.get(&'b'), Some(&'f'));
assert_eq!(map.get(&'c'), Some(&'G'));
// remove non-last item
assert_eq!(map.remove(&'b'), Some('f'));
// get_or_create item after the removed one
assert_eq!(map.get_or_create(&'c', || 'G'), &'G');
}
#[test]
@@ -208,20 +221,26 @@ mod tests {
// new items
map.insert('a', 'e');
map.insert('b', 'f');
map.insert('c', 'g');
assert_eq!(map.get(&'a'), Some(&'e'));
assert_eq!(map.get(&'b'), Some(&'f'));
assert!(map.get(&'c').is_none());
assert_eq!(map.get(&'c'), Some(&'g'));
assert!(map.get(&'d').is_none());
// remove one item
assert_eq!(map.remove(&'b'), Some('f'));
// remove last item
assert_eq!(map.remove(&'c'), Some('g'));
assert_eq!(map.get(&'a'), Some(&'e'));
assert_eq!(map.get(&'b'), None);
assert_eq!(map.get(&'b'), Some(&'f'));
assert_eq!(map.get(&'c'), None);
// remove a non-existent item
assert_eq!(map.remove(&'c'), None);
// remove a non-last item
assert_eq!(map.remove(&'a'), Some('e'));
assert_eq!(map.get(&'b'), Some(&'f'));
}
#[test]
+29 -2
View File
@@ -248,9 +248,15 @@ pub struct FinishUploadThumbnailInfo {
/// Transaction id for the thumbnail upload.
pub txn: OwnedTransactionId,
/// Thumbnail's width.
pub width: UInt,
///
/// Used previously, kept for backwards compatibility.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
/// Thumbnail's height.
pub height: UInt,
///
/// Used previously, kept for backwards compatibility.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
}
/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
@@ -367,6 +373,27 @@ pub struct DependentQueuedRequest {
pub parent_key: Option<SentRequestKey>,
}
impl DependentQueuedRequest {
/// Does the dependent request represent a new event that is *not*
/// aggregated, aka it is going to be its own item in a timeline?
pub fn is_own_event(&self) -> bool {
match self.kind {
DependentQueuedRequestKind::EditEvent { .. }
| DependentQueuedRequestKind::RedactEvent
| DependentQueuedRequestKind::ReactEvent { .. }
| DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => {
// These are all aggregated events, or non-visible items (file upload producing
// a new MXC ID).
false
}
DependentQueuedRequestKind::FinishUpload { .. } => {
// This one graduates into a new media event.
true
}
}
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for QueuedRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+42 -5
View File
@@ -424,21 +424,31 @@ pub trait StateStore: AsyncTraitDeps {
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error>;
/// Update a set of dependent send queue requests with a key identifying the
/// homeserver's response, effectively marking them as ready.
/// Mark a set of dependent send queue requests as ready, using a key
/// identifying the homeserver's response.
///
/// ⚠ Beware! There's no verification applied that the parent key type is
/// compatible with the dependent event type. The invalid state may be
/// lazily filtered out in `load_dependent_queued_requests`.
///
/// Returns the number of updated requests.
async fn update_dependent_queued_request(
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error>;
/// Update a dependent send queue request with the new content.
///
/// Returns true if the request was found and could be updated.
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool, Self::Error>;
/// Remove a specific dependent send queue request by id.
///
/// Returns true if the dependent send queue request has been indeed
@@ -709,14 +719,14 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
.map_err(Into::into)
}
async fn update_dependent_queued_request(
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error> {
self.0
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
.mark_dependent_queued_requests_as_ready(room_id, parent_txn_id, sent_parent_key)
.await
.map_err(Into::into)
}
@@ -735,6 +745,18 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
}
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool, Self::Error> {
self.0
.update_dependent_queued_request(room_id, own_transaction_id, new_content)
.await
.map_err(Into::into)
}
}
/// Convenience functionality for state stores.
@@ -1000,6 +1022,9 @@ pub enum StateStoreDataValue {
///
/// [`ComposerDraft`]: Self::ComposerDraft
ComposerDraft(ComposerDraft),
/// A list of knock request ids marked as seen in a room.
SeenKnockRequests(BTreeMap<OwnedEventId, OwnedUserId>),
}
/// Current draft of the composer for the room.
@@ -1066,6 +1091,11 @@ impl StateStoreDataValue {
pub fn into_server_capabilities(self) -> Option<ServerCapabilities> {
as_variant!(self, Self::ServerCapabilities)
}
/// Get this value if it is the data for the ignored join requests.
pub fn into_seen_knock_requests(self) -> Option<BTreeMap<OwnedEventId, OwnedUserId>> {
as_variant!(self, Self::SeenKnockRequests)
}
}
/// A key for key-value data.
@@ -1095,6 +1125,9 @@ pub enum StateStoreDataKey<'a> {
///
/// [`ComposerDraft`]: Self::ComposerDraft
ComposerDraft(&'a RoomId),
/// A list of knock request ids marked as seen in a room.
SeenKnockRequests(&'a RoomId),
}
impl StateStoreDataKey<'_> {
@@ -1120,6 +1153,10 @@ impl StateStoreDataKey<'_> {
/// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft]
/// variant.
pub const COMPOSER_DRAFT: &'static str = "composer_draft";
/// Key prefix to use for the
/// [`SeenKnockRequests`][Self::SeenKnockRequests] variant.
pub const SEEN_KNOCK_REQUESTS: &'static str = "seen_knock_requests";
}
#[cfg(test)]
+2 -2
View File
@@ -248,7 +248,7 @@ impl Timeline {
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
}
@@ -257,7 +257,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
}
+26
View File
@@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.9.0] - 2024-12-18
### Bug Fixes
- Change the behavior of `LinkedChunk::new_with_update_history()` to emit an
`Update::NewItemsChunk` when a new, initial empty, chunk is created.
([#4327](https://github.com/matrix-org/matrix-rust-sdk/pull/4321))
- [**breaking**] Make `Room::history_visibility()` return an Option, and
introduce `Room::history_visibility_or_default()` to return a better
sensible default, according to the spec.
([#4325](https://github.com/matrix-org/matrix-rust-sdk/pull/4325))
- Clear the internal state of the `AsVector` struct if an `Update::Clear`
state has been received.
([#4321](https://github.com/matrix-org/matrix-rust-sdk/pull/4321))
### Documentation
- Document that a decrypted raw event always has a room id.
([#728e1fd](https://github.com/matrix-org/matrix-rust-sdk/commit/728e1fda2ae9f1bfa87df162aa553040be705223))
## [0.8.0] - 2024-11-19
### Refactor
+14 -8
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.8.0"
version = "0.9.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -36,19 +36,25 @@ uniffi = { workspace = true, optional = true }
[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"] }
gloo-timers = { workspace = true, features = ["futures"] }
web-sys = { workspace = true, features = ["console"] }
tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] }
wasm-bindgen = "0.2.84"
wasm-bindgen = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
matrix-sdk-test = { workspace = true }
wasm-bindgen-test = "0.3.33"
proptest = { workspace = true }
matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" }
wasm-bindgen-test = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Enable the test macro.
tokio = { workspace = true, features = ["rt", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
js-sys = "0.3.64"
# Enable the JS feature for getrandom.
getrandom = { version = "0.2.6", default-features = false, features = ["js"] }
js-sys = { workspace = true }
[lints]
workspace = true
@@ -17,7 +17,10 @@ use std::{collections::BTreeMap, fmt};
use ruma::{
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent},
push::Action,
serde::{JsonObject, Raw},
serde::{
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
SerializeAsRefStr,
},
DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId,
};
use serde::{Deserialize, Serialize};
@@ -176,6 +179,7 @@ pub enum VerificationLevel {
/// The message was sent by a user identity we have not verified, but the
/// user was previously verified.
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
/// The message was sent by a device not linked to (signed by) any user
@@ -259,6 +263,7 @@ pub enum ShieldStateCode {
/// An unencrypted event in an encrypted room.
SentInClear,
/// The sender was previously verified but changed their identity.
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
}
@@ -587,6 +592,11 @@ impl fmt::Debug for TimelineEventKind {
/// A successfully-decrypted encrypted event.
pub struct DecryptedRoomEvent {
/// The decrypted event.
///
/// Note: it's not an error that this contains an `AnyMessageLikeEvent`: an
/// encrypted payload *always contains* a room id, by the [spec].
///
/// [spec]: https://spec.matrix.org/v1.12/client-server-api/#mmegolmv1aes-sha2
pub event: Raw<AnyMessageLikeEvent>,
/// The encryption info about the event.
@@ -661,7 +671,7 @@ pub struct UnableToDecryptInfo {
pub session_id: Option<String>,
/// Reason code for the decryption failure
#[serde(default = "unknown_utd_reason")]
#[serde(default = "unknown_utd_reason", deserialize_with = "deserialize_utd_reason")]
pub reason: UnableToDecryptReason,
}
@@ -669,6 +679,24 @@ fn unknown_utd_reason() -> UnableToDecryptReason {
UnableToDecryptReason::Unknown
}
/// Provides basic backward compatibility for deserializing older serialized
/// `UnableToDecryptReason` values.
pub fn deserialize_utd_reason<'de, D>(d: D) -> Result<UnableToDecryptReason, D::Error>
where
D: serde::Deserializer<'de>,
{
// Start by deserializing as to an untyped JSON value.
let v: serde_json::Value = Deserialize::deserialize(d)?;
// Backwards compatibility: `MissingMegolmSession` used to be stored without the
// withheld code.
if v.as_str().is_some_and(|s| s == "MissingMegolmSession") {
return Ok(UnableToDecryptReason::MissingMegolmSession { withheld_code: None });
}
// Otherwise, use the derived deserialize impl to turn the JSON into a
// UnableToDecryptReason
serde_json::from_value::<UnableToDecryptReason>(v).map_err(serde::de::Error::custom)
}
/// Reason code for a decryption failure
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UnableToDecryptReason {
@@ -684,9 +712,11 @@ pub enum UnableToDecryptReason {
/// Decryption failed because we're missing the megolm session that was used
/// to encrypt the event.
///
/// TODO: support withheld codes?
MissingMegolmSession,
MissingMegolmSession {
/// If the key was withheld on purpose, the associated code. `None`
/// means no withheld code was received.
withheld_code: Option<WithheldCode>,
},
/// Decryption failed because, while we have the megolm session that was
/// used to encrypt the message, it is ratcheted too far forward.
@@ -718,7 +748,86 @@ impl UnableToDecryptReason {
/// Returns true if this UTD is due to a missing room key (and hence might
/// resolve itself if we wait a bit.)
pub fn is_missing_room_key(&self) -> bool {
matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex)
// In case of MissingMegolmSession with a withheld code we return false here
// given that this API is used to decide if waiting a bit will help.
matches!(
self,
Self::MissingMegolmSession { withheld_code: None } | Self::UnknownMegolmMessageIndex
)
}
}
/// A machine-readable code for why a Megolm key was not sent.
///
/// Normally sent as the payload of an [`m.room_key.withheld`](https://spec.matrix.org/v1.12/client-server-api/#mroom_keywithheld) to-device message.
#[derive(
Clone,
PartialEq,
Eq,
Hash,
AsStrAsRefStr,
AsRefStr,
FromString,
DebugAsRefStr,
SerializeAsRefStr,
DeserializeFromCowStr,
)]
pub enum WithheldCode {
/// the user/device was blacklisted.
#[ruma_enum(rename = "m.blacklisted")]
Blacklisted,
/// the user/devices is unverified.
#[ruma_enum(rename = "m.unverified")]
Unverified,
/// The user/device is not allowed have the key. For example, this would
/// usually be sent in response to a key request if the user was not in
/// the room when the message was sent.
#[ruma_enum(rename = "m.unauthorised")]
Unauthorised,
/// Sent in reply to a key request if the device that the key is requested
/// from does not have the requested key.
#[ruma_enum(rename = "m.unavailable")]
Unavailable,
/// An olm session could not be established.
/// This may happen, for example, if the sender was unable to obtain a
/// one-time key from the recipient.
#[ruma_enum(rename = "m.no_olm")]
NoOlm,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl fmt::Display for WithheldCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let string = match self {
WithheldCode::Blacklisted => "The sender has blocked you.",
WithheldCode::Unverified => "The sender has disabled encrypting to unverified devices.",
WithheldCode::Unauthorised => "You are not authorised to read the message.",
WithheldCode::Unavailable => "The requested key was not found.",
WithheldCode::NoOlm => "Unable to establish a secure channel.",
_ => self.as_str(),
};
f.write_str(string)
}
}
// The Ruma macro expects the type to have this name.
// The payload is counter intuitively made public in order to avoid having
// multiple copies of this struct.
#[doc(hidden)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrivOwnedStr(pub Box<str>);
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for PrivOwnedStr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
@@ -812,9 +921,9 @@ mod tests {
use super::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
UnsignedEventLocation, VerificationState,
UnsignedEventLocation, VerificationState, WithheldCode,
};
use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel};
use crate::deserialized_responses::{DeviceLinkProblem, ShieldStateCode, VerificationLevel};
fn example_event() -> serde_json::Value {
json!({
@@ -889,6 +998,74 @@ mod tests {
);
}
#[test]
fn test_verification_level_deserializes() {
// Given a JSON VerificationLevel
#[derive(Deserialize)]
struct Container {
verification_level: VerificationLevel,
}
let container = json!({ "verification_level": "VerificationViolation" });
// When we deserialize it
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
// Then it is populated correctly
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
}
#[test]
fn test_verification_level_deserializes_from_old_previously_verified_value() {
// Given a JSON VerificationLevel with the old value PreviouslyVerified
#[derive(Deserialize)]
struct Container {
verification_level: VerificationLevel,
}
let container = json!({ "verification_level": "PreviouslyVerified" });
// When we deserialize it
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
// Then it is migrated to the new value
assert_eq!(deserialized.verification_level, VerificationLevel::VerificationViolation);
}
#[test]
fn test_shield_state_code_deserializes() {
// Given a JSON ShieldStateCode with value VerificationViolation
#[derive(Deserialize)]
struct Container {
shield_state_code: ShieldStateCode,
}
let container = json!({ "shield_state_code": "VerificationViolation" });
// When we deserialize it
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
// Then it is populated correctly
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
}
#[test]
fn test_shield_state_code_deserializes_from_old_previously_verified_value() {
// Given a JSON ShieldStateCode with the old value PreviouslyVerified
#[derive(Deserialize)]
struct Container {
shield_state_code: ShieldStateCode,
}
let container = json!({ "shield_state_code": "PreviouslyVerified" });
// When we deserialize it
let deserialized: Container = serde_json::from_value(container)
.expect("We can deserialize the old PreviouslyVerified value");
// Then it is migrated to the new value
assert_eq!(deserialized.shield_state_code, ShieldStateCode::VerificationViolation);
}
#[test]
fn sync_timeline_event_serialisation() {
let room_event = SyncTimelineEvent {
@@ -1033,4 +1210,111 @@ mod tests {
});
});
}
#[test]
fn sync_timeline_event_deserialisation_migration_for_withheld() {
// Old serialized version was
// "utd_info": {
// "reason": "MissingMegolmSession",
// "session_id": "session000"
// }
// The new version would be
// "utd_info": {
// "reason": {
// "MissingMegolmSession": {
// "withheld_code": null
// }
// },
// "session_id": "session000"
// }
let serialized = json!({
"kind": {
"UnableToDecrypt": {
"event": {
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "AwgAEoABzL1JYhqhjW9jXrlT3M6H8mJ4qffYtOQOnPuAPNxsuG20oiD/Fnpv6jnQGhU6YbV9pNM+1mRnTvxW3CbWOPjLKqCWTJTc7Q0vDEVtYePg38ncXNcwMmfhgnNAoW9S7vNs8C003x3yUl6NeZ8bH+ci870BZL+kWM/lMl10tn6U7snNmSjnE3ckvRdO+11/R4//5VzFQpZdf4j036lNSls/WIiI67Fk9iFpinz9xdRVWJFVdrAiPFwb8L5xRZ8aX+e2JDMlc1eW8gk",
"device_id": "SKCGPNUWAU",
"sender_key": "Gim/c7uQdSXyrrUbmUOrBT6sMC0gO7QSLmOK6B7NOm0",
"session_id": "hgLyeSqXfb8vc5AjQLsg6TSHVu0HJ7HZ4B6jgMvxkrs"
},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message"
},
"utd_info": {
"reason": "MissingMegolmSession",
"session_id": "session000"
}
}
}
});
let result = serde_json::from_value(serialized);
assert!(result.is_ok());
// should have migrated to the new format
let event: SyncTimelineEvent = result.unwrap();
assert_matches!(
event.kind,
TimelineEventKind::UnableToDecrypt { utd_info, .. }=> {
assert_matches!(
utd_info.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None }
);
}
)
}
#[test]
fn unable_to_decrypt_info_migration_for_withheld() {
let old_format = json!({
"reason": "MissingMegolmSession",
"session_id": "session000"
});
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(old_format).unwrap();
let session_id = Some("session000".to_owned());
assert_eq!(deserialized.session_id, session_id);
assert_eq!(
deserialized.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
);
let new_format = json!({
"session_id": "session000",
"reason": {
"MissingMegolmSession": {
"withheld_code": null
}
}
});
let deserialized = serde_json::from_value::<UnableToDecryptInfo>(new_format).unwrap();
assert_eq!(
deserialized.reason,
UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
);
assert_eq!(deserialized.session_id, session_id);
}
#[test]
fn unable_to_decrypt_reason_is_missing_room_key() {
let reason = UnableToDecryptReason::MissingMegolmSession { withheld_code: None };
assert!(reason.is_missing_room_key());
let reason = UnableToDecryptReason::MissingMegolmSession {
withheld_code: Some(WithheldCode::Blacklisted),
};
assert!(!reason.is_missing_room_key());
let reason = UnableToDecryptReason::UnknownMegolmMessageIndex;
assert!(reason.is_missing_room_key());
}
}
+1 -1
View File
@@ -81,7 +81,7 @@ impl<T: 'static> Future for JoinHandle<T> {
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use super::spawn;
+2 -2
View File
@@ -306,7 +306,7 @@ impl<'a> JsFieldVisitor<'a> {
}
}
impl<'a> tracing::field::Visit for JsFieldVisitor<'a> {
impl tracing::field::Visit for JsFieldVisitor<'_> {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if self.result.is_err() {
return;
@@ -351,7 +351,7 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
#[cfg(test)]
pub(crate) mod tests {
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use tracing::{debug, subscriber::with_default};
use wasm_bindgen::{JsCast, JsValue};
@@ -302,6 +302,12 @@ impl UpdateToVectorDiff {
self.chunks.insert(next_chunk_index, (*new, 0));
}
// First chunk!
(None, None) if self.chunks.is_empty() => {
self.chunks.push_back((*new, 0));
}
// Impossible state.
(None, None) => {
unreachable!(
"Inserting new chunk with no previous nor next chunk identifiers \
@@ -405,6 +411,14 @@ impl UpdateToVectorDiff {
// Exiting the _detaching_ mode.
detaching = false;
}
Update::Clear => {
// Clean `self.chunks`.
self.chunks.clear();
// Let's straightforwardly emit a `VectorDiff::Clear`.
diffs.push(VectorDiff::Clear);
}
}
}
@@ -450,10 +464,11 @@ impl UpdateToVectorDiff {
mod tests {
use std::fmt::Debug;
use assert_matches::assert_matches;
use imbl::{vector, Vector};
use super::{
super::{EmptyChunk, LinkedChunk},
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
VectorDiff,
};
@@ -473,6 +488,7 @@ mod tests {
VectorDiff::Remove { index } => {
accumulator.remove(index);
}
VectorDiff::Clear => accumulator.clear(),
diff => unimplemented!("{diff:?}"),
}
}
@@ -686,14 +702,72 @@ mod tests {
&[VectorDiff::Insert { index: 14, value: 'z' }],
);
drop(linked_chunk);
assert!(as_vector.take().is_empty());
// Finally, ensure the “reconstitued” vector is the one expected.
// Ensure the “reconstitued” vector is the one expected.
assert_eq!(
accumulator,
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
);
// Let's try to clear the linked chunk now.
linked_chunk.clear();
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
assert!(accumulator.is_empty());
drop(linked_chunk);
assert!(as_vector.take().is_empty());
}
#[test]
fn test_as_vector_with_update_clear() {
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let mut as_vector = linked_chunk.as_vector().unwrap();
{
// 1 initial chunk in the `UpdateToVectorDiff` mapper.
let chunks = &as_vector.mapper.chunks;
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
assert_eq!(chunks[0].1, 0);
assert!(as_vector.take().is_empty());
}
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Append { .. });
assert_matches!(&diffs[1], VectorDiff::Append { .. });
// 2 chunks in the `UpdateToVectorDiff` mapper.
assert_eq!(as_vector.mapper.chunks.len(), 2);
}
linked_chunk.clear();
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 1);
assert_matches!(&diffs[0], VectorDiff::Clear);
// 1 chunk in the `UpdateToVectorDiff` mapper.
let chunks = &as_vector.mapper.chunks;
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
assert_eq!(chunks[0].1, 0);
}
// And we can push again.
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Append { .. });
assert_matches!(&diffs[1], VectorDiff::Append { .. });
}
}
#[test]
@@ -0,0 +1,481 @@
// Copyright 2024 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::{BTreeMap, HashSet},
marker::PhantomData,
};
use tracing::error;
use super::{
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
ObservableUpdates, RawChunk,
};
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
///
/// Instead of using linking the chunks with pointers, this uses
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
/// which will get resolved later when re-building the full data structure. This
/// allows using chunks that references other chunks that aren't known yet.
struct TemporaryChunk<Item, Gap> {
id: ChunkIdentifier,
previous: Option<ChunkIdentifier>,
next: Option<ChunkIdentifier>,
content: ChunkContent<Item, Gap>,
}
/// A data structure to rebuild a linked chunk from its raw representation.
///
/// A linked chunk can be rebuilt incrementally from its internal
/// representation, with the chunks being added *in any order*, as long as they
/// form a single connected component eventually (viz., there's no
/// subgraphs/sublists isolated from the one final linked list). If they don't,
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
/// error).
#[allow(missing_debug_implementations)]
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
/// Work-in-progress chunks.
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
/// Is the final `LinkedChunk` expected to include an update history, as if
/// it were created with [`LinkedChunk::new_with_update_history`]?
build_with_update_history: bool,
}
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
fn default() -> Self {
Self::new()
}
}
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
/// Create an empty [`LinkedChunkBuilder`] with no update history.
pub fn new() -> Self {
Self { chunks: Default::default(), build_with_update_history: false }
}
/// Stash a gap chunk with its content.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_gap(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
content: Gap,
) {
let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) };
self.chunks.insert(id, chunk);
}
/// Stash an item chunk with its contents.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_items(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
items: impl IntoIterator<Item = Item>,
) {
let chunk = TemporaryChunk {
id,
previous,
next,
content: ChunkContent::Items(items.into_iter().collect()),
};
self.chunks.insert(id, chunk);
}
/// Request that the resulting linked chunk will have an update history, as
/// if it were created with [`LinkedChunk::new_with_update_history`].
pub fn with_update_history(&mut self) {
self.build_with_update_history = true;
}
/// Run all error checks before reconstructing the full linked chunk.
///
/// Must be called after checking `self.chunks` isn't empty in
/// [`Self::build`].
///
/// Returns the identifier of the first chunk.
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
// Look for the first id.
let first_id =
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
// it's a malformed list.
let Some(first_id) = first_id else {
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
// We're going to iterate from the first to the last chunk.
// Keep track of chunks we've already visited.
let mut visited = HashSet::new();
// Start from the first chunk.
let mut maybe_cur = Some(first_id);
while let Some(cur) = maybe_cur {
// The chunk must be referenced in `self.chunks`.
let Some(chunk) = self.chunks.get(&cur) else {
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
};
if let ChunkContent::Items(items) = &chunk.content {
if items.len() > CAP {
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
}
}
// If it's not the first chunk,
if cur != first_id {
// It must have a previous link.
let Some(prev) = chunk.previous else {
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
first_candidate: first_id,
second_candidate: cur,
});
};
// And we must have visited its predecessor at this point, since we've
// iterated from the first chunk.
if !visited.contains(&prev) {
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
}
}
// Add the current chunk to the list of seen chunks.
if !visited.insert(cur) {
// If we didn't insert, then it was already visited: there's a cycle!
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
}
// Move on to the next chunk. If it's none, we'll quit the loop.
maybe_cur = chunk.next;
}
// If there are more chunks than those we've visited: some of them were not
// linked to the "main" branch of the linked list, so we had multiple connected
// components.
if visited.len() != self.chunks.len() {
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
}
Ok(first_id)
}
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
if self.chunks.is_empty() {
return Ok(None);
}
// Run checks.
let first_id = self.check_consistency()?;
// We're now going to iterate from the first to the last chunk. As we're doing
// this, we're also doing a few other things:
//
// - rebuilding the final `Chunk`s one by one, that will be linked using
// pointers,
// - counting items from the item chunks we'll encounter,
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
let mut max_chunk_id = first_id.index();
// Small helper to graduate a temporary chunk into a final one. As we're doing
// this, we're also updating the maximum chunk id (that will be used to
// set up the id generator), and the number of items in this chunk.
let mut graduate_chunk = |id: ChunkIdentifier| {
let temp = self.chunks.remove(&id)?;
// Update the maximum chunk identifier, while we're around.
max_chunk_id = max_chunk_id.max(id.index());
// Graduate the current temporary chunk into a final chunk.
let chunk_ptr = Chunk::new_leaked(id, temp.content);
Some((temp.next, chunk_ptr))
};
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
let mut prev_chunk_ptr = first_chunk_ptr;
while let Some(id) = next_chunk_id {
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingChunk { id });
};
let chunk = unsafe { chunk_ptr.as_mut() };
// Link the current chunk to its previous one.
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
prev_chunk.next = Some(chunk_ptr);
chunk.previous = Some(prev_chunk_ptr);
// Prepare for the next iteration.
prev_chunk_ptr = chunk_ptr;
next_chunk_id = new_next;
}
debug_assert!(self.chunks.is_empty());
// Maintain the convention that `Ends::last` may be unset.
let last_chunk_ptr = prev_chunk_ptr;
let last_chunk_ptr =
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
let chunk_identifier_generator =
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
max_chunk_id,
));
let updates =
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
}
/// Fills a linked chunk builder from all the given raw parts.
pub fn from_raw_parts(raws: Vec<RawChunk<Item, Gap>>) -> Self {
let mut this = Self::new();
for raw in raws {
match raw.content {
ChunkContent::Gap(gap) => {
this.push_gap(raw.previous, raw.identifier, raw.next, gap);
}
ChunkContent::Items(vec) => {
this.push_items(raw.previous, raw.identifier, raw.next, vec);
}
}
}
this
}
}
#[derive(thiserror::Error, Debug)]
pub enum LinkedChunkBuilderError {
#[error("chunk with id {} is too large", id.index())]
ChunkTooLarge { id: ChunkIdentifier },
#[error("there's no first chunk")]
MissingFirstChunk,
#[error("there are multiple first chunks")]
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
#[error("unable to resolve chunk with id {}", id.index())]
MissingChunk { id: ChunkIdentifier },
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
Cycle { repeated: ChunkIdentifier },
#[error("multiple connected components")]
MultipleConnectedComponents,
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::LinkedChunkBuilder;
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
#[test]
fn test_empty() {
let lcb = LinkedChunkBuilder::<3, char, char>::new();
// Building an empty linked chunk works, and returns `None`.
let lc = lcb.build().unwrap();
assert!(lc.is_none());
}
#[test]
fn test_success() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
// the chunk id space.
let cid3 = ChunkIdentifier::new(3);
// Check that we can successfully create a linked chunk, independently of the
// order in which chunks are added.
//
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
// Adding chunk cid0.
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
// Adding chunk cid3.
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
// Adding chunk cid1.
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
let mut lc =
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
// Check the entire content first.
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
// Run checks on the first chunk.
let mut chunks = lc.chunks();
let first_chunk = chunks.next().unwrap();
{
assert!(first_chunk.previous().is_none());
assert_eq!(first_chunk.identifier(), cid0);
}
// Run checks on the second chunk.
let second_chunk = chunks.next().unwrap();
{
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
assert_eq!(second_chunk.identifier(), cid1);
}
// Run checks on the third chunk.
let third_chunk = chunks.next().unwrap();
{
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
assert!(third_chunk.next().is_none());
assert_eq!(third_chunk.identifier(), cid3);
}
// There's no more chunk.
assert!(chunks.next().is_none());
// The linked chunk had 5 items.
assert_eq!(lc.num_items(), 5);
// Now, if we add a new chunk, its identifier should be the previous one we used
// + 1.
lc.push_gap_back('h');
let last_chunk = lc.chunks().last().unwrap();
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
}
#[test]
fn test_chunk_too_large() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
// Adding a chunk with 4 items will fail, because the max capacity specified in
// the builder generics is 3.
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
assert_eq!(id, cid0);
});
}
#[test]
fn test_missing_first_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
}
#[test]
fn test_multiple_first_chunks() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
// Second chunk lies and pretends to be the first too.
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
assert_eq!(first_candidate, cid0);
assert_eq!(second_candidate, cid1);
});
}
#[test]
fn test_missing_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
assert_eq!(id, cid1);
});
}
#[test]
fn test_cycle() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
assert_eq!(repeated, cid0);
});
}
#[test]
fn test_multiple_connected_components() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
// cid0 and cid1 are linked to each other.
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
// cid2 stands on its own.
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,898 @@
// Copyright 2024 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.
//! Implementation for a _relational linked chunk_, see
//! [`RelationalLinkedChunk`].
use ruma::{OwnedRoomId, RoomId};
use super::{ChunkContent, RawChunk};
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
/// A row of the [`RelationalLinkedChunk::chunks`].
#[derive(Debug, PartialEq)]
struct ChunkRow {
room_id: OwnedRoomId,
previous_chunk: Option<ChunkIdentifier>,
chunk: ChunkIdentifier,
next_chunk: Option<ChunkIdentifier>,
}
/// A row of the [`RelationalLinkedChunk::items`].
#[derive(Debug, PartialEq)]
struct ItemRow<Item, Gap> {
room_id: OwnedRoomId,
position: Position,
item: Either<Item, Gap>,
}
/// Kind of item.
#[derive(Debug, PartialEq)]
enum Either<Item, Gap> {
/// The content is an item.
Item(Item),
/// The content is a gap.
Gap(Gap),
}
/// A [`LinkedChunk`] but with a relational layout, similar to what we
/// would have in a database.
///
/// This is used by memory stores. The idea is to have a data layout that is
/// similar for memory stores and for relational database stores, to represent a
/// [`LinkedChunk`].
///
/// This type is also designed to receive [`Update`]. Applying `Update`s
/// directly on a [`LinkedChunk`] is not ideal and particularly not trivial as
/// the `Update`s do _not_ match the internal data layout of the `LinkedChunk`,
/// they have been designed for storages, like a relational database for
/// example.
///
/// This type is not as performant as [`LinkedChunk`] (in terms of memory
/// layout, CPU caches etc.). It is only designed to be used in memory stores,
/// which are mostly used for test purposes or light usage of the SDK.
///
/// [`LinkedChunk`]: super::LinkedChunk
#[derive(Debug)]
pub struct RelationalLinkedChunk<Item, Gap> {
/// Chunks.
chunks: Vec<ChunkRow>,
/// Items.
items: Vec<ItemRow<Item, Gap>>,
}
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
/// Create a new relational linked chunk.
pub fn new() -> Self {
Self { chunks: Vec::new(), items: Vec::new() }
}
/// Removes all the chunks and items from this relational linked chunk.
pub fn clear(&mut self) {
self.chunks.clear();
self.items.clear();
}
/// Apply [`Update`]s. That's the only way to write data inside this
/// relational linked chunk.
pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec<Update<Item, Gap>>) {
for update in updates {
match update {
Update::NewItemsChunk { previous, new, next } => {
insert_chunk(&mut self.chunks, room_id, previous, new, next);
}
Update::NewGapChunk { previous, new, next, gap } => {
insert_chunk(&mut self.chunks, room_id, previous, new, next);
self.items.push(ItemRow {
room_id: room_id.to_owned(),
position: Position::new(new, 0),
item: Either::Gap(gap),
});
}
Update::RemoveChunk(chunk_identifier) => {
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
let indices_to_remove = self
.items
.iter()
.enumerate()
.filter_map(
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
(room_id == room_id_candidate
&& position.chunk_identifier() == chunk_identifier)
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
}
}
Update::PushItems { mut at, items } => {
for item in items {
self.items.push(ItemRow {
room_id: room_id.to_owned(),
position: at,
item: Either::Item(item),
});
at.increment_index();
}
}
Update::RemoveItem { at } => {
let mut entry_to_remove = None;
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
self.items.iter_mut().enumerate()
{
// Filter by room ID.
if room_id != room_id_candidate {
continue;
}
// Find the item to remove.
if *position == at {
debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
entry_to_remove = Some(nth);
}
// Update all items that come _after_ `at` to shift their index.
if position.chunk_identifier() == at.chunk_identifier()
&& position.index() > at.index()
{
position.decrement_index();
}
}
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
}
Update::DetachLastItems { at } => {
let indices_to_remove = self
.items
.iter()
.enumerate()
.filter_map(
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
(room_id == room_id_candidate
&& position.chunk_identifier() == at.chunk_identifier()
&& position.index() >= at.index())
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
}
}
Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
Update::Clear => {
self.chunks.clear();
self.items.clear();
}
}
}
fn insert_chunk(
chunks: &mut Vec<ChunkRow>,
room_id: &RoomId,
previous: Option<ChunkIdentifier>,
new: ChunkIdentifier,
next: Option<ChunkIdentifier>,
) {
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
room_id == room_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
// Link the chunk.
entry_for_previous_chunk.next_chunk = Some(new);
}
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
room_id == room_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
// Link the chunk.
entry_for_next_chunk.previous_chunk = Some(new);
}
// Insert the chunk.
chunks.push(ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: previous,
chunk: new,
next_chunk: next,
});
}
fn remove_chunk(
chunks: &mut Vec<ChunkRow>,
room_id: &RoomId,
chunk_to_remove: ChunkIdentifier,
) {
let entry_nth_to_remove = chunks
.iter()
.enumerate()
.find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| {
(room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth)
})
.expect("Remove an unknown chunk");
let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } =
chunks.remove(entry_nth_to_remove);
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
&room_id == room_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
// Insert the chunk.
entry_for_previous_chunk.next_chunk = next;
}
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
&room_id == room_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
// Insert the chunk.
entry_for_next_chunk.previous_chunk = previous;
}
}
}
}
impl<Item, Gap> RelationalLinkedChunk<Item, Gap>
where
Gap: Clone,
Item: Clone,
{
/// Reloads the chunks.
///
/// Return an error result if the data was malformed in the struct, with a
/// string message explaining details about the error.
pub fn reload_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
let mut result = Vec::new();
for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) {
// Find all items that correspond to the chunk.
let mut items = self
.items
.iter()
.filter(|row| {
row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
// Look at the first chunk item type, to reconstruct the chunk at hand.
let Some(first) = items.peek() else {
// The only possibility is that we created an empty items chunk; mark it as
// such, and continue.
result.push(RawChunk {
content: ChunkContent::Items(Vec::new()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
continue;
};
match &first.item {
Either::Item(_) => {
// Collect all the related items.
let mut collected_items = Vec::new();
for row in items {
match &row.item {
Either::Item(item) => {
collected_items.push((item.clone(), row.position.index()))
}
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
// Sort them by their position.
collected_items.sort_unstable_by_key(|(_item, index)| *index);
result.push(RawChunk {
content: ChunkContent::Items(
collected_items.into_iter().map(|(item, _index)| item).collect(),
),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
}
Either::Gap(gap) => {
assert!(items.next().is_some(), "we just peeked the gap");
// We shouldn't have more than one item row for this chunk.
if items.next().is_some() {
return Err(format!(
"there shouldn't be more than one item row attached in gap chunk {}",
chunk_row.chunk.index()
));
}
result.push(RawChunk {
content: ChunkContent::Gap(gap.clone()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
}
}
}
Ok(result)
}
}
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use ruma::room_id;
use super::{ChunkIdentifier as CId, *};
use crate::linked_chunk::LinkedChunkBuilder;
#[test]
fn test_new_items_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// 2 before 0
Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
// 3 between 2 and 0
Update::NewItemsChunk {
previous: Some(CId::new(2)),
new: CId::new(3),
next: Some(CId::new(0)),
},
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(3)),
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(2),
next_chunk: Some(CId::new(3))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(2)),
chunk: CId::new(3),
next_chunk: Some(CId::new(0))
},
],
);
// Items have not been modified.
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_new_gap_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
// 2 after 1
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: Some(CId::new(2))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(1)),
chunk: CId::new(2),
next_chunk: None
},
],
);
// Items contains the gap.
assert_eq!(
relational_linked_chunk.items,
&[ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Gap(())
}],
);
}
#[test]
fn test_remove_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
// 2 after 1
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// remove 1
Update::RemoveChunk(CId::new(1)),
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(2))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(2),
next_chunk: None
},
],
);
// Items no longer contains the gap.
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_push_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
// new chunk (to test new items are pushed in the correct chunk)
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// new items on 1
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
// new items on 0 again
Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 3),
item: Either::Item('d')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 4),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_remove_item() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec!['a', 'b', 'c', 'd', 'e'],
},
// remove an item: 'a'
Update::RemoveItem { at: Position::new(CId::new(0), 0) },
// remove an item: 'd'
Update::RemoveItem { at: Position::new(CId::new(0), 2) },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None
}],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('c')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_detach_last_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new chunk
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec!['a', 'b', 'c', 'd', 'e'],
},
// new items on 1
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
// detach last items on 0
Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
],
);
}
#[test]
fn test_start_and_end_reattach_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
// Nothing happened.
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_clear() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
}],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
],
);
// Now, time for a clean up.
relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]);
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_reload_empty_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
// When I reload the linked chunk components from an empty store,
let relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
let result = relational_linked_chunk.reload_chunks(room_id).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_reload_linked_chunk_with_empty_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
// When I store an empty items chunks,
relational_linked_chunk.apply_updates(
room_id,
vec![Update::NewItemsChunk { previous: None, new: CId::new(0), next: None }],
);
// It correctly gets reloaded as such.
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
.build()
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
assert_items_eq!(lc, []);
}
#[test]
fn test_rebuild_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
// a gap chunk
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: 'g',
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
Update::PushItems { at: Position::new(CId::new(2), 0), items: vec!['d', 'e', 'f'] },
],
);
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
.build()
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
// The linked chunk is correctly reloaded.
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
}
}
@@ -29,6 +29,9 @@ use super::{ChunkIdentifier, Position};
///
/// These updates are useful to store a `LinkedChunk` in another form of
/// storage, like a database or something similar.
///
/// [`LinkedChunk`]: super::LinkedChunk
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
#[derive(Debug, Clone, PartialEq)]
pub enum Update<Item, Gap> {
/// A new chunk of kind Items has been created.
@@ -96,11 +99,17 @@ pub enum Update<Item, Gap> {
/// Reattaching items (see [`Self::StartReattachItems`]) is finished.
EndReattachItems,
/// All chunks have been cleared, i.e. all items and all gaps have been
/// dropped.
Clear,
}
/// A collection of [`Update`]s that can be observed.
///
/// Get a value for this type with [`LinkedChunk::updates`].
///
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
#[derive(Debug)]
pub struct ObservableUpdates<Item, Gap> {
pub(super) inner: Arc<RwLock<UpdatesInner<Item, Gap>>>,
@@ -120,7 +129,7 @@ impl<Item, Gap> ObservableUpdates<Item, Gap> {
/// Take new updates.
///
/// Updates that have been taken will not be read again.
pub(super) fn take(&mut self) -> Vec<Update<Item, Gap>>
pub fn take(&mut self) -> Vec<Update<Item, Gap>>
where
Item: Clone,
Gap: Clone,
@@ -375,7 +384,21 @@ mod tests {
other_token
};
// There is no new update yet.
// There is an initial update.
{
let updates = linked_chunk.updates().unwrap();
assert_eq!(
updates.take(),
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }],
);
assert_eq!(
updates.inner.write().unwrap().take_with_token(other_token),
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }],
);
}
// No new update.
{
let updates = linked_chunk.updates().unwrap();
@@ -608,7 +631,16 @@ mod tests {
let updates_subscriber = linked_chunk.updates().unwrap().subscribe();
pin_mut!(updates_subscriber);
// No update, stream is pending.
// Initial update, stream is ready.
assert_matches!(
updates_subscriber.as_mut().poll_next(&mut context),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
);
}
);
assert_matches!(updates_subscriber.as_mut().poll_next(&mut context), Poll::Pending);
assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 0);
@@ -642,6 +674,7 @@ mod tests {
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None },
PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] },
PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] },
PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] },
@@ -692,9 +725,28 @@ mod tests {
let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe();
pin_mut!(updates_subscriber2);
// No update, streams are pending.
// Initial updates, streams are ready.
assert_matches!(
updates_subscriber1.as_mut().poll_next(&mut context1),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
);
}
);
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 0);
assert_matches!(
updates_subscriber2.as_mut().poll_next(&mut context2),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[NewItemsChunk { previous: None, new: ChunkIdentifier(0), next: None }]
);
}
);
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0);
+4 -5
View File
@@ -343,7 +343,7 @@ mod tests {
};
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use tokio::{
spawn,
time::{sleep, Duration},
@@ -361,7 +361,7 @@ mod tests {
impl TestStore {
fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool {
try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)
try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
}
}
@@ -502,12 +502,11 @@ mod tests {
pub mod memory_store_helper {
use std::{
collections::{hash_map::Entry, HashMap},
sync::RwLock,
time::{Duration, Instant},
};
pub fn try_take_leased_lock(
leases: &RwLock<HashMap<String, (String, Instant)>>,
leases: &mut HashMap<String, (String, Instant)>,
lease_duration_ms: u32,
key: &str,
holder: &str,
@@ -515,7 +514,7 @@ pub mod memory_store_helper {
let now = Instant::now();
let expiration = now + Duration::from_millis(lease_duration_ms.into());
match leases.write().unwrap().entry(key.to_owned()) {
match leases.entry(key.to_owned()) {
// There is an existing holder.
Entry::Occupied(mut entry) => {
let (current_holder, current_expiration) = entry.get_mut();
+1 -1
View File
@@ -62,7 +62,7 @@ where
pub(crate) mod tests {
use std::{future, time::Duration};
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use super::timeout;
@@ -105,7 +105,7 @@ macro_rules! timer {
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
#[matrix_sdk_test::async_test]
#[matrix_sdk_test_macros::async_test]
async fn test_timer_name() {
use tracing::{span, Level};
+35
View File
@@ -2,6 +2,35 @@
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.9.0] - 2024-12-18
- Expose new API `DehydratedDevices::get_dehydrated_device_pickle_key`, `DehydratedDevices::save_dehydrated_device_pickle_key`
and `DehydratedDevices::delete_dehydrated_device_pickle_key` to store/load the dehydrated device pickle key.
This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and to_device accumulation.
[**breaking**] `DehydratedDevices::keys_for_upload` and `DehydratedDevices::rehydrate` now use the `DehydratedDeviceKey`
as parameter instead of a raw byte array. Use `DehydratedDeviceKey::from_bytes` to migrate.
([#4383](https://github.com/matrix-org/matrix-rust-sdk/pull/4383))
- Add extra logging in `OtherUserIdentity::pin_current_master_key` and
`OtherUserIdentity::withdraw_verification`.
([#4415](https://github.com/matrix-org/matrix-rust-sdk/pull/4415))
- Added new `UtdCause` variants `WithheldForUnverifiedOrInsecureDevice` and `WithheldBySender`.
These variants provide clearer categorization for expected Unable-To-Decrypt (UTD) errors
when the sender either did not wish to share or was unable to share the room_key.
([#4305](https://github.com/matrix-org/matrix-rust-sdk/pull/4305))
- `UtdCause` has two new variants that replace the existing `HistoricalMessage`:
`HistoricalMessageAndBackupIsDisabled` and `HistoricalMessageAndDeviceIsUnverified`.
These give more detail about what went wrong and allow us to suggest to users
what actions they can take to fix the problem. See the doc comments on these
variants for suggested wording.
([#4384](https://github.com/matrix-org/matrix-rust-sdk/pull/4384))
## [0.8.0] - 2024-11-19
### Features
@@ -66,6 +95,12 @@ All notable changes to this project will be documented in this file.
### Refactor
- Fix [#4424](https://github.com/matrix-org/matrix-rust-sdk/issues/4424) Failed
storage upgrade for "PreviouslyVerifiedButNoLonger". This bug caused errors to
occur when loading crypto information from storage, which typically prevented
apps from starting correctly.
([#4430](https://github.com/matrix-org/matrix-rust-sdk/pull/4430))
- Add new method `OlmMachine::try_decrypt_room_event`.
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
+20 -14
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.8.0"
version = "0.9.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -23,6 +23,11 @@ experimental-algorithms = []
uniffi = ["dep:uniffi"]
_disable-minimum-rotation-period-ms = []
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
@@ -30,38 +35,39 @@ message-ids = []
testing = ["matrix-sdk-test"]
[dependencies]
aes = "0.8.1"
aes = "0.8.4"
aquamarine = { workspace = true }
as_variant = { workspace = true }
async-trait = { workspace = true }
bs58 = { version = "0.5.0" }
bs58 = { version = "0.5.1" }
byteorder = { workspace = true }
cfg-if = "1.0"
ctr = "0.9.1"
ctr = "0.9.2"
eyeball = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
hkdf = "0.12.3"
hmac = "0.12.1"
hkdf = { workspace = true }
hmac = { workspace = true }
itertools = { workspace = true }
js_option = "0.1.1"
matrix-sdk-qrcode = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true } # feature = testing only
pbkdf2 = { version = "0.12.2", default-features = false }
pbkdf2 = { workspace = true }
rand = { workspace = true }
rmp-serde = "1.1.1"
rmp-serde = { workspace = true }
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"
time = { version = "0.3.34", features = ["formatting"] }
subtle = "2.6.1"
time = { version = "0.3.36", features = ["formatting"] }
tokio-stream = { workspace = true, features = ["sync"] }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
url = { workspace = true }
ulid = { version = "1.0.0" }
ulid = { version = "1.1.3" }
uniffi = { workspace = true, optional = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -78,10 +84,10 @@ assert_matches = { workspace = true }
assert_matches2 = { workspace = true }
futures-executor = { workspace = true }
http = { workspace = true }
indoc = "2.0.1"
indoc = "2.0.5"
matrix-sdk-test = { workspace = true }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
similar-asserts = "1.5.0"
proptest = { workspace = true }
similar-asserts = { workspace = true }
# required for async_test macro
stream_assert = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
+31 -19
View File
@@ -1,13 +1,11 @@
A no-network-IO implementation of a state machine that handles E2EE for
[Matrix] clients.
# Usage
A no-network-IO implementation of a state machine that handles end-to-end
encryption for [Matrix] clients.
If you're just trying to write a Matrix client or bot in Rust, you're probably
looking for [matrix-sdk] instead.
However, if you're looking to add E2EE to an existing Matrix client or library,
read on.
However, if you're looking to add end-to-end encryption to an existing Matrix
client or library, read on.
The state machine works in a push/pull manner:
@@ -52,28 +50,42 @@ async fn main() -> Result<(), OlmError> {
Ok(())
}
```
It is recommended to use the [tutorial] to understand how end-to-end encryption
works in Matrix and how to add end-to-end encryption support in your Matrix
client library.
[Matrix]: https://matrix.org/
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
# Room key forwarding algorithm
The decision tree below visualizes the way this crate decides whether a message
key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a
key request, provided the `automatic-room-key-forwarding` feature is enabled.
Key forwarding is sometimes also referred to as key *gossiping*.
[forwarded_room_key]: <https://spec.matrix.org/v1.10/client-server-api/#mforwarded_room_key>
![](https://raw.githubusercontent.com/matrix-org/matrix-rust-sdk/main/contrib/key-sharing-algorithm/model.png)
# Crate Feature Flags
The following crate feature flags are available:
* `qrcode`: Enbles QRcode generation and reading code
| Feature | Default | Description |
| ------------------- | :-----: | -------------------------------------------------------------------------------------------------------------------------- |
| `qrcode` | No | Enables QR code based interactive verification |
| `js` | No | Enables JavaScript API usage for things like the current system time on WASM (does nothing on other targets) |
| `testing` | No | Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. |
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
# Enabling logging
Users of the `matrix-sdk-crypto` crate can enable log output by depending on the
`tracing-subscriber` crate and including the following line in their
application (e.g. at the start of `main`):
```no_compile
tracing_subscriber::fmt::init();
```
The log output is controlled via the `RUST_LOG` environment variable by
setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
The output is printed to stdout.
The `RUST_LOG` variable also supports a more advanced syntax for filtering
log output more precisely, for instance with crate-level granularity. For
more information on this, check out the [tracing-subscriber documentation].
[tracing-subscriber documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/
+2 -2
View File
@@ -38,8 +38,8 @@ use tracing::{debug, info, instrument, trace, warn};
use crate::{
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError,
types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
CryptoStoreError, Device, RoomKeyImportResult, SignatureError,
};
mod keys;
@@ -57,7 +57,7 @@ use tracing::{instrument, trace};
use vodozemac::LibolmPickleError;
use crate::{
store::{CryptoStoreWrapper, MemoryStore, RoomKeyInfo, Store},
store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store},
verification::VerificationMachine,
Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError,
};
@@ -69,6 +69,10 @@ pub enum DehydrationError {
#[error(transparent)]
Pickle(#[from] LibolmPickleError),
/// The pickle key has an invalid length
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
/// The dehydrated device could not be signed by our user identity,
/// we're missing the self-signing key.
#[error("The self-signing key is missing, can't create a dehydrated device")]
@@ -132,15 +136,49 @@ impl DehydratedDevices {
/// private keys of the device.
pub async fn rehydrate(
&self,
pickle_key: &[u8; 32],
pickle_key: &DehydratedDeviceKey,
device_id: &DeviceId,
device_data: Raw<DehydratedDeviceData>,
) -> Result<RehydratedDevice, DehydrationError> {
let pickle_key = expand_pickle_key(pickle_key, device_id);
let pickle_key = expand_pickle_key(pickle_key.inner.as_ref(), device_id);
let rehydrated = self.inner.rehydrate(&pickle_key, device_id, device_data).await?;
Ok(RehydratedDevice { rehydrated, original: self.inner.to_owned() })
}
/// Get the cached dehydrated device pickle key if any.
///
/// None if the key was not previously cached (via
/// [`DehydratedDevices::save_dehydrated_device_pickle_key`]).
///
/// Should be used to periodically rotate the dehydrated device to avoid
/// one-time keys exhaustion and accumulation of to_device messages.
pub async fn get_dehydrated_device_pickle_key(
&self,
) -> Result<Option<DehydratedDeviceKey>, DehydrationError> {
Ok(self.inner.store().load_dehydrated_device_pickle_key().await?)
}
/// Store the dehydrated device pickle key in the crypto store.
///
/// This is useful if the client wants to periodically rotate dehydrated
/// devices to avoid one-time keys exhaustion and accumulated to_device
/// problems.
pub async fn save_dehydrated_device_pickle_key(
&self,
dehydrated_device_pickle_key: &DehydratedDeviceKey,
) -> Result<(), DehydrationError> {
let changes = Changes {
dehydrated_device_pickle_key: Some(dehydrated_device_pickle_key.clone()),
..Default::default()
};
Ok(self.inner.store().save_changes(changes).await?)
}
/// Deletes the previously stored dehydrated device pickle key.
pub async fn delete_dehydrated_device_pickle_key(&self) -> Result<(), DehydrationError> {
Ok(self.inner.store().delete_dehydrated_device_pickle_key().await?)
}
}
/// A rehydraded device.
@@ -170,7 +208,7 @@ impl RehydratedDevice {
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::OlmMachine;
/// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey };
/// # use ruma::{api::client::dehydrated_device, DeviceId};
/// # async fn example() -> Result<()> {
/// # let machine: OlmMachine = unimplemented!();
@@ -184,9 +222,9 @@ impl RehydratedDevice {
/// ) -> Result<dehydrated_device::get_events::unstable::Response> {
/// todo!("Download the to-device events of the dehydrated device");
/// }
///
/// // Don't use a zero key for real.
/// let pickle_key = [0u8; 32];
/// // Get the cached dehydrated key (got it after verification/recovery)
/// let pickle_key = machine
/// .dehydrated_devices().get_dehydrated_device_pickle_key().await?.unwrap();
///
/// // Fetch the dehydrated device from the server.
/// let response = get_dehydrated_device().await?;
@@ -285,11 +323,13 @@ impl DehydratedDevice {
/// # Examples
///
/// ```no_run
/// # use matrix_sdk_crypto::OlmMachine;
/// # async fn example() -> anyhow::Result<()> {
/// # use matrix_sdk_crypto::OlmMachine; /// #
/// use matrix_sdk_crypto::store::DehydratedDeviceKey;
///
/// async fn example() -> anyhow::Result<()> {
/// # let machine: OlmMachine = unimplemented!();
/// // Don't use a zero key for real.
/// let pickle_key = [0u8; 32];
/// // Create a new random key
/// let pickle_key = DehydratedDeviceKey::new()?;
///
/// // Create the dehydrated device.
/// let device = machine.dehydrated_devices().create().await?;
@@ -299,6 +339,9 @@ impl DehydratedDevice {
/// .keys_for_upload("Dehydrated device".to_owned(), &pickle_key)
/// .await?;
///
/// // Save the key if you want to later one rotate the dehydrated device
/// machine.dehydrated_devices().save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
///
/// // Send the request out using your HTTP client.
/// // client.send(request).await?;
/// # Ok(())
@@ -314,7 +357,7 @@ impl DehydratedDevice {
pub async fn keys_for_upload(
&self,
initial_device_display_name: String,
pickle_key: &[u8; 32],
pickle_key: &DehydratedDeviceKey,
) -> Result<put_dehydrated_device::unstable::Request, DehydrationError> {
let mut transaction = self.store.transaction().await;
@@ -330,7 +373,8 @@ impl DehydratedDevice {
trace!("Creating an upload request for a dehydrated device");
let pickle_key = expand_pickle_key(pickle_key, &self.store.static_account().device_id);
let pickle_key =
expand_pickle_key(pickle_key.inner.as_ref(), &self.store.static_account().device_id);
let device_id = self.store.static_account().device_id.clone();
let device_data = account.dehydrate(&pickle_key);
let initial_device_display_name = Some(initial_device_display_name);
@@ -393,12 +437,15 @@ mod tests {
tests::to_device_requests_to_content,
},
olm::OutboundGroupSession,
store::DehydratedDeviceKey,
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
utilities::json_convert,
EncryptionSettings, OlmMachine,
};
const PICKLE_KEY: &[u8; 32] = &[0u8; 32];
fn pickle_key() -> DehydratedDeviceKey {
DehydratedDeviceKey::from_bytes(&[0u8; 32])
}
fn user_id() -> &'static UserId {
user_id!("@alice:localhost")
@@ -467,7 +514,7 @@ mod tests {
let dehydrated_device = olm_machine.dehydrated_devices().create().await.unwrap();
let request = dehydrated_device
.keys_for_upload("Foo".to_owned(), PICKLE_KEY)
.keys_for_upload("Foo".to_owned(), &pickle_key())
.await
.expect("We should be able to create a request to upload a dehydrated device");
@@ -497,7 +544,7 @@ mod tests {
let dehydrated_device = alice.dehydrated_devices().create().await.unwrap();
let mut request = dehydrated_device
.keys_for_upload("Foo".to_owned(), PICKLE_KEY)
.keys_for_upload("Foo".to_owned(), &pickle_key())
.await
.expect("We should be able to create a request to upload a dehydrated device");
@@ -531,7 +578,7 @@ mod tests {
// Rehydrate the device.
let rehydrated = bob
.dehydrated_devices()
.rehydrate(PICKLE_KEY, &request.device_id, request.device_data)
.rehydrate(&pickle_key(), &request.device_id, request.device_data)
.await
.expect("We should be able to rehydrate the device");
@@ -561,4 +608,43 @@ mod tests {
"The session ids of the imported room key and the outbound group session should match"
);
}
#[async_test]
async fn test_dehydrated_device_pickle_key_cache() {
let alice = get_olm_machine().await;
let dehydrated_manager = alice.dehydrated_devices();
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
assert!(stored_key.is_none());
let pickle_key = DehydratedDeviceKey::new().unwrap();
dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
let stored_key =
dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap().unwrap();
assert_eq!(stored_key.to_base64(), pickle_key.to_base64());
let dehydrated_device = dehydrated_manager.create().await.unwrap();
let request = dehydrated_device
.keys_for_upload("Foo".to_owned(), &stored_key)
.await
.expect("We should be able to create a request to upload a dehydrated device");
// Rehydrate the device.
dehydrated_manager
.rehydrate(&stored_key, &request.device_id, request.device_data)
.await
.expect("We should be able to rehydrate the device");
dehydrated_manager
.delete_dehydrated_device_pickle_key()
.await
.expect("Should be able to delete the dehydrated device key");
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
assert!(stored_key.is_none());
}
}
+2 -5
View File
@@ -14,7 +14,7 @@
use std::collections::BTreeMap;
use matrix_sdk_common::deserialized_responses::VerificationLevel;
use matrix_sdk_common::deserialized_responses::{VerificationLevel, WithheldCode};
use ruma::{CanonicalJsonError, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedUserId};
use serde::{ser::SerializeMap, Serializer};
use serde_json::Error as SerdeError;
@@ -22,10 +22,7 @@ use thiserror::Error;
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
use super::store::CryptoStoreError;
use crate::{
olm::SessionExportError,
types::{events::room_key_withheld::WithheldCode, SignedKey},
};
use crate::{olm::SessionExportError, types::SignedKey};
#[cfg(doc)]
use crate::{CollectStrategy, Device, LocalTrust, OtherUserIdentity};
@@ -56,7 +56,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor
}
}
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
impl<R: Read> Read for AttachmentDecryptor<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner.read(buf)?;
@@ -45,16 +45,18 @@ use crate::{
error::{EventError, OlmError, OlmResult},
identities::IdentityManager,
olm::{InboundGroupSession, Session},
requests::{OutgoingRequest, ToDeviceRequest},
session_manager::GroupSessionCache,
store::{Changes, CryptoStoreError, SecretImportError, Store, StoreCache},
types::events::{
forwarded_room_key::ForwardedRoomKeyContent,
olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent},
room::encrypted::EncryptedEvent,
room_key_request::RoomKeyRequestEvent,
secret_send::SecretSendContent,
EventType,
types::{
events::{
forwarded_room_key::ForwardedRoomKeyContent,
olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent},
room::encrypted::EncryptedEvent,
room_key_request::RoomKeyRequestEvent,
secret_send::SecretSendContent,
EventType,
},
requests::{OutgoingRequest, ToDeviceRequest},
},
Device, MegolmError,
};
@@ -616,7 +618,6 @@ impl GossipMachine {
/// i.
/// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for
/// the refusal.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_share_key(
&self,
@@ -1116,6 +1117,7 @@ mod tests {
use crate::{
gossiping::KeyForwardDecision,
olm::OutboundGroupSession,
types::requests::AnyOutgoingRequest,
types::{
events::{
forwarded_room_key::ForwardedRoomKeyContent, olm_v1::AnyDecryptedOlmEvent,
@@ -1123,7 +1125,7 @@ mod tests {
},
EventEncryptionAlgorithm,
},
EncryptionSettings, OutgoingRequests,
EncryptionSettings,
};
use crate::{
identities::{DeviceData, IdentityManager, LocalTrust},
@@ -1310,7 +1312,7 @@ mod tests {
fn extract_content<'a>(
recipient: &UserId,
request: &'a crate::OutgoingRequest,
request: &'a crate::types::requests::OutgoingRequest,
) -> &'a Raw<ruma::events::AnyToDeviceEventContent> {
request
.request()
@@ -1343,7 +1345,7 @@ mod tests {
fn request_to_event<C>(
recipient: &UserId,
sender: &UserId,
request: &crate::OutgoingRequest,
request: &crate::types::requests::OutgoingRequest,
) -> crate::types::events::ToDeviceEvent<C>
where
C: crate::types::events::EventType
@@ -2064,7 +2066,7 @@ mod tests {
assert_eq!(bob_machine.outgoing_to_device_requests().await.unwrap().len(), 1);
assert_matches!(
bob_machine.outgoing_to_device_requests().await.unwrap()[0].request(),
OutgoingRequests::KeysClaim(_)
AnyOutgoingRequest::KeysClaim(_)
);
assert!(!bob_machine.inner.users_for_key_claim.read().unwrap().is_empty());
assert!(!bob_machine.inner.wait_queue.is_empty());
@@ -36,10 +36,12 @@ use ruma::{
use serde::{Deserialize, Serialize};
use crate::{
requests::{OutgoingRequest, ToDeviceRequest},
types::events::{
olm_v1::DecryptedSecretSendEvent,
room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo},
types::{
events::{
olm_v1::DecryptedSecretSendEvent,
room_key_request::{RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo},
},
requests::{OutgoingRequest, ToDeviceRequest},
},
Device,
};
@@ -21,6 +21,7 @@ use std::{
},
};
use matrix_sdk_common::deserialized_responses::WithheldCode;
use ruma::{
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
events::{key::verification::VerificationMethod, AnyToDeviceEventContent},
@@ -48,13 +49,13 @@ use crate::{
types::{
events::{
forwarded_room_key::ForwardedRoomKeyContent,
room::encrypted::ToDeviceEncryptedEventContent, room_key_withheld::WithheldCode,
EventType,
room::encrypted::ToDeviceEncryptedEventContent, EventType,
},
requests::{OutgoingVerificationRequest, ToDeviceRequest},
DeviceKey, DeviceKeys, EventEncryptionAlgorithm, Signatures, SignedKey,
},
verification::VerificationMachine,
Account, OutgoingVerificationRequest, Sas, ToDeviceRequest, VerificationRequest,
Account, Sas, VerificationRequest,
};
pub enum MaybeEncryptedRoomKey {
@@ -33,12 +33,14 @@ use crate::{
error::OlmResult,
identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData},
olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType},
requests::KeysQueryRequest,
store::{
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult,
},
types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
types::{
requests::KeysQueryRequest, CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey,
UserSigningPubkey,
},
CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentity,
};
@@ -548,7 +550,7 @@ impl IdentityManager {
// First time seen, create the identity. The current MSK will be pinned.
let identity = OtherUserIdentityData::new(master_key, self_signing)?;
let is_verified = maybe_verified_own_identity
.map_or(false, |own_user_identity| own_user_identity.is_identity_signed(&identity));
.is_some_and(|own_user_identity| own_user_identity.is_identity_signed(&identity));
if is_verified {
identity.mark_as_previously_verified();
}
@@ -1228,9 +1230,8 @@ pub(crate) mod testing {
identities::IdentityManager,
olm::{Account, PrivateCrossSigningIdentity},
store::{CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
types::DeviceKeys,
types::{requests::UploadSigningKeysRequest, DeviceKeys},
verification::VerificationMachine,
UploadSigningKeysRequest,
};
pub fn user_id() -> &'static UserId {
+72 -25
View File
@@ -31,14 +31,16 @@ use ruma::{
};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use tracing::error;
use tracing::{error, info};
use crate::{
error::SignatureError,
store::{Changes, IdentityChanges, Store},
types::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
types::{
requests::OutgoingVerificationRequest, MasterPubkey, SelfSigningPubkey, UserSigningPubkey,
},
verification::VerificationMachine,
CryptoStoreError, DeviceData, OutgoingVerificationRequest, VerificationRequest,
CryptoStoreError, DeviceData, VerificationRequest,
};
/// Enum over the different user identity types we can have.
@@ -390,6 +392,7 @@ impl OtherUserIdentity {
/// Pin the current identity (public part of the master signing key).
pub async fn pin_current_master_key(&self) -> Result<(), CryptoStoreError> {
info!(master_key = ?self.master_key.get_first_key(), "Pinning current identity for user '{}'", self.user_id());
self.inner.pin();
let to_save = UserIdentityData::Other(self.inner.clone());
let changes = Changes {
@@ -425,6 +428,7 @@ impl OtherUserIdentity {
/// Remove the requirement for this identity to be verified.
pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> {
info!(master_key = ?self.master_key.get_first_key(), "Withdrawing verification status and pinning current identity for user '{}'", self.user_id());
self.inner.withdraw_verification();
let to_save = UserIdentityData::Other(self.inner.clone());
let changes = Changes {
@@ -435,16 +439,20 @@ impl OtherUserIdentity {
Ok(())
}
// Test helper
/// Test helper that marks that an identity has been previously verified and
/// persist the change in the store.
#[cfg(test)]
pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> {
self.inner.mark_as_previously_verified();
let to_save = UserIdentityData::Other(self.inner.clone());
let changes = Changes {
identities: IdentityChanges { changed: vec![to_save], ..Default::default() },
..Default::default()
};
self.verification_machine.store.inner().save_changes(changes).await?;
Ok(())
}
@@ -854,8 +862,8 @@ impl OtherUserIdentityData {
// Check if the new master_key is signed by our own **verified**
// user_signing_key. If the identity was verified we remember it.
let updated_is_verified = maybe_verified_own_user_signing_key
.map_or(false, |own_user_signing_key| {
let updated_is_verified =
maybe_verified_own_user_signing_key.is_some_and(|own_user_signing_key| {
own_user_signing_key.verify_master_key(&master_key).is_ok()
});
@@ -914,6 +922,7 @@ enum OwnUserIdentityVerifiedState {
NeverVerified,
/// We previously verified this identity, but it has changed.
#[serde(alias = "PreviouslyVerifiedButNoLonger")]
VerificationViolation,
/// We have verified the current identity.
@@ -1533,26 +1542,10 @@ pub(crate) mod tests {
/// that we can deserialize boolean values.
#[test]
fn test_deserialize_own_user_identity_bool_verified() {
let mut json = json!({
"user_id": "@example:localhost",
"master_key": {
"user_id":"@example:localhost",
"usage":["master"],
"keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"},
},
"self_signing_key": {
"user_id":"@example:localhost",
"usage":["self_signing"],
"keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"}
},
"user_signing_key": {
"user_id":"@example:localhost",
"usage":["user_signing"],
"keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"}
},
"verified": false
});
let mut json = own_user_identity_data();
// Set `"verified": false`
*json.get_mut("verified").unwrap() = false.into();
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::NeverVerified);
@@ -1562,6 +1555,38 @@ pub(crate) mod tests {
assert_eq!(*id.verified.read().unwrap(), OwnUserIdentityVerifiedState::Verified);
}
#[test]
fn test_own_user_identity_verified_state_verification_violation_deserializes() {
// Given data containing verified: VerificationViolation
let mut json = own_user_identity_data();
*json.get_mut("verified").unwrap() = "VerificationViolation".into();
// When we deserialize
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
// Then the value is correctly populated
assert_eq!(
*id.verified.read().unwrap(),
OwnUserIdentityVerifiedState::VerificationViolation
);
}
#[test]
fn test_own_user_identity_verified_state_previously_verified_deserializes() {
// Given data containing verified: PreviouslyVerifiedButNoLonger
let mut json = own_user_identity_data();
*json.get_mut("verified").unwrap() = "PreviouslyVerifiedButNoLonger".into();
// When we deserialize
let id: OwnUserIdentityData = serde_json::from_value(json.clone()).unwrap();
// Then the old value is re-interpreted as VerificationViolation
assert_eq!(
*id.verified.read().unwrap(),
OwnUserIdentityVerifiedState::VerificationViolation
);
}
#[test]
fn own_identity_check_signatures() {
let response = own_key_query();
@@ -1937,4 +1962,26 @@ pub(crate) mod tests {
assert!(!own_identity.was_previously_verified());
assert!(!own_identity.has_verification_violation());
}
fn own_user_identity_data() -> Value {
json!({
"user_id": "@example:localhost",
"master_key": {
"user_id":"@example:localhost",
"usage":["master"],
"keys":{"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0":"rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"},
},
"self_signing_key": {
"user_id":"@example:localhost",
"usage":["self_signing"],
"keys":{"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210":"0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"}
},
"user_signing_key": {
"user_id":"@example:localhost",
"usage":["user_signing"],
"keys":{"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo":"DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"}
},
"verified": false
})
}
}
+943 -6
View File
@@ -1,4 +1,5 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
// Copyright 2024 Damir Jelić
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -26,7 +27,6 @@ mod gossiping;
mod identities;
mod machine;
pub mod olm;
pub mod requests;
pub mod secret_storage;
mod session_manager;
pub mod store;
@@ -95,10 +95,6 @@ use matrix_sdk_common::deserialized_responses::{DecryptedRoomEvent, UnableToDecr
#[cfg(feature = "qrcode")]
pub use matrix_sdk_qrcode;
pub use olm::{Account, CrossSigningStatus, EncryptionSettings, Session};
pub use requests::{
IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests,
OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest,
};
use serde::{Deserialize, Serialize};
pub use session_manager::CollectStrategy;
pub use store::{
@@ -114,7 +110,7 @@ pub use verification::{QrVerification, QrVerificationState, ScanError};
pub use vodozemac;
/// The version of the matrix-sdk-cypto crate being used
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
matrix_sdk_test::init_tracing_for_tests!();
@@ -156,3 +152,944 @@ pub enum RoomEventDecryptionResult {
/// We were unable to decrypt the event
UnableToDecrypt(UnableToDecryptInfo),
}
#[cfg_attr(doc, aquamarine::aquamarine)]
/// A step by step guide that explains how to include [end-to-end-encryption]
/// support in a [Matrix] client library.
///
/// This crate implements a [sans-network-io](https://sans-io.readthedocs.io/)
/// state machine that allows you to add [end-to-end-encryption] support to a
/// [Matrix] client library.
///
/// This guide aims to provide a comprehensive understanding of end-to-end
/// encryption in Matrix without any prior knowledge requirements. However, it
/// is recommended that the reader has a basic understanding of Matrix and its
/// [client-server specification] for a more informed and efficient learning
/// experience.
///
/// The [introductory](#introduction) section provides a simplified explanation
/// of end-to-end encryption and its implementation in Matrix for those who may
/// not have prior knowledge. If you already have a solid understanding of
/// end-to-end encryption, including the [Olm] and [Megolm] protocols, you may
/// choose to skip directly to the [Getting Started](#getting-started) section.
///
/// # Table of Contents
/// 1. [Introduction](#introduction)
/// 2. [Getting started](#getting-started)
/// 3. [Decrypting room events](#decryption)
/// 4. [Encrypting room events](#encryption)
/// 5. [Interactively verifying devices and user identities](#verification)
///
/// # Introduction
///
/// Welcome to the first part of this guide, where we will introduce the
/// fundamental concepts of end-to-end encryption and its implementation in
/// Matrix.
///
/// This section will provide a clear and concise overview of what
/// end-to-end encryption is and why it is important for secure communication.
/// You will also learn about how Matrix uses end-to-end encryption to protect
/// the privacy and security of its users' communications. Whether you are new
/// to the topic or simply want to improve your understanding, this section will
/// serve as a solid foundation for the rest of the guide.
///
/// Let's dive in!
///
/// ## Notation
///
/// ## End-to-end-encryption
///
/// End-to-end encryption (E2EE) is a method of secure communication where only
/// the communicating devices, also known as "the ends," can read the data being
/// transmitted. This means that the data is encrypted on one device, and can
/// only be decrypted on the other device. The server is used only as a
/// transport mechanism to deliver messages between devices.
///
/// The following chart displays how communication between two clients using a
/// server in the middle usually works.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// bob[Bob]
/// subgraph Homeserver
/// direction LR
/// outbox[Alice outbox]
/// inbox[Bob inbox]
/// outbox -. unencrypted .-> inbox
/// end
///
/// alice -- encrypted --> outbox
/// inbox -- encrypted --> bob
/// ```
///
/// The next chart, instead, displays how the same flow is happening in a
/// end-to-end-encrypted world.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// bob[Bob]
/// subgraph Homeserver
/// direction LR
/// outbox[Alice outbox]
/// inbox[Bob inbox]
/// outbox == encrypted ==> inbox
/// end
///
/// alice == encrypted ==> outbox
/// inbox == encrypted ==> bob
/// ```
///
/// Note that the path from the outbox to the inbox is now encrypted as well.
///
/// Alice and Bob have created a secure communication channel
/// through which they can exchange messages confidentially, without the risk of
/// the server accessing the contents of their messages.
///
/// ## Publishing cryptographic identities of devices
///
/// If Alice and Bob want to establish a secure channel over which they can
/// exchange messages, they first need learn about each others cryptographic
/// identities. This is achieved by using the homeserver as a public key
/// directory.
///
/// A public key directory is used to store and distribute public keys of users
/// in an end-to-end encrypted system. The basic idea behind a public key
/// directory is that it allows users to easily discover and download the public
/// keys of other users with whom they wish to establish an end-to-end encrypted
/// communication.
///
/// Each user generates a pair of public and private keys. The user then uploads
/// their public key to the public key directory. Other users can then search
/// the directory to find the public key of the user they wish to communicate
/// with, and download it to their own device.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// subgraph homeserver[Homeserver]
/// direction LR
/// directory[(Public key directory)]
/// end
/// bob[Bob]
///
/// alice -- upload keys --> directory
/// directory -- download keys --> bob
/// ```
///
/// Once a user has the other user's public key, they can use it to establish an
/// end-to-end encrypted channel using a [key-agreement protocol].
///
/// ## Using the Triple Diffie-Hellman key-agreement protocol
///
/// In the triple Diffie-Hellman key agreement protocol (3DH in short), each
/// user generates a long-term identity key pair and a set of one-time prekeys.
/// When two users want to establish a shared secret key, they exchange their
/// public identity keys and one of their prekeys. These public keys are then
/// used in a [Diffie-Hellman] key exchange to compute a shared secret key.
///
/// The use of one-time prekeys ensures that the shared secret key is different
/// for each session, even if the same identity keys are used.
///
/// ```mermaid
/// flowchart LR
/// subgraph alice_keys[Alice Keys]
/// direction TB
/// alice_key[Alice's identity key]
/// alice_base_key[Alice's one-time key]
/// end
///
/// subgraph bob_keys[Bob Keys]
/// direction TB
/// bob_key[Bob's identity key]
/// bob_one_time[Bob's one-time key]
/// end
///
/// alice_key <--> bob_one_time
/// alice_base_key <--> bob_one_time
/// alice_base_key <--> bob_key
/// ```
///
/// Similar to [X3DH] (Extended Triple Diffie-Hellman) key agreement protocol
///
/// ## Speeding up encryption for large groups
///
/// In the previous section we learned how to utilize a key agreement protocol
/// to establish secure 1-to-1 encrypted communication channels. These channels
/// allow us to encrypt a message for each device separately.
///
/// One critical property of these channels is that, if you want to send a
/// message to a group of devices, we'll need to encrypt the message for each
/// device individually.
///
/// TODO Explain how megolm fits into this
///
/// # Getting started
///
/// Before we start writing any code, let us get familiar with the basic
/// principle upon which this library is built.
///
/// The central piece of the library is the [`OlmMachine`] which acts as a state
/// machine which consumes data that gets received from the homeserver and
/// outputs data which should be sent to the homeserver.
///
/// ## Push/pull mechanism
///
/// The [`OlmMachine`] at the heart of it acts as a state machine that operates
/// in a push/pull manner. HTTP responses which were received from the
/// homeserver get forwarded into the [`OlmMachine`] and in turn the internal
/// state gets updated which produces HTTP requests that need to be sent to the
/// homeserver.
///
/// In a manner, we're pulling data from the server, we update our internal
/// state based on the data and in turn push data back to the server.
///
/// ```mermaid
/// flowchart LR
/// homeserver[Homeserver]
/// client[OlmMachine]
///
/// homeserver -- pull --> client
/// client -- push --> homeserver
/// ```
///
/// ## Initializing the state machine
///
/// ```
/// use anyhow::Result;
/// use matrix_sdk_crypto::OlmMachine;
/// use ruma::user_id;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let user_id = user_id!("@alice:localhost");
/// let device_id = "DEVICEID".into();
///
/// let machine = OlmMachine::new(user_id, device_id).await;
/// # Ok(())
/// # }
/// ```
///
/// This will create a [`OlmMachine`] that does not persist any data TODO
/// ```ignore
/// use anyhow::Result;
/// use matrix_sdk_crypto::OlmMachine;
/// use matrix_sdk_sqlite::SqliteCryptoStore;
/// use ruma::user_id;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let user_id = user_id!("@alice:localhost");
/// let device_id = "DEVICEID".into();
///
/// let store = SqliteCryptoStore::open("/home/example/matrix-client/", None).await?;
///
/// let machine = OlmMachine::with_store(user_id, device_id, store).await;
/// # Ok(())
/// # }
/// ```
///
/// # Decryption
///
/// In the world of encrypted communication, it is common to start with the
/// encryption step when implementing a protocol. However, in the case of adding
/// end-to-end encryption support to a Matrix client library, a simpler approach
/// is to first focus on the decryption process. This is because there are
/// already Matrix clients in existence that support encryption, which means
/// that our client library can simply receive encrypted messages and then
/// decrypt them.
///
/// In this section, we will guide you through the minimal steps
/// necessary to get the decryption process up and running using the
/// matrix-sdk-crypto Rust crate. By the end of this section you should have a
/// Matrix client that is able to decrypt room events that other clients have
/// sent.
///
/// To enable decryption the following three steps are needed:
///
/// 1. [The cryptographic identity of your device needs to be published to the
/// homeserver](#uploading-identity-and-one-time-keys).
/// 2. [Decryption keys coming in from other devices need to be processed and
/// stored](#receiving-room-keys-and-related-changes).
/// 3. [Individual messages need to be decrypted](#decrypting-room-events).
///
/// The simplified flowchart
/// ```mermaid
/// graph TD
/// sync[Sync with the homeserver]
/// receive_changes[Push E2EE related changes into the state machine]
/// send_outgoing_requests[Send all outgoing requests to the homeserver]
/// decrypt[Process the rest of the sync]
///
/// sync --> receive_changes;
/// receive_changes --> send_outgoing_requests;
/// send_outgoing_requests --> decrypt;
/// decrypt -- repeat --> sync;
/// ```
///
/// ## Uploading identity and one-time keys.
///
/// To enable end-to-end encryption in a Matrix client, the first step is to
/// announce the support for it to other users in the network. This is done by
/// publishing the client's long-term device keys and a set of one-time prekeys
/// to the Matrix homeserver. The homeserver then makes this information
/// available to other devices in the network.
///
/// The long-term device keys and one-time prekeys allow other devices to
/// encrypt messages specifically for your device.
///
/// To achieve this, you will need to extract any requests that need to be sent
/// to the homeserver from the [`OlmMachine`] and send them to the homeserver.
/// The following snippet showcases how to achieve this using the
/// [`OlmMachine::outgoing_requests()`] method:
///
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use ruma::api::client::keys::upload_keys::v3::Response;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest};
/// # async fn send_request(request: OutgoingRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let machine: OlmMachine = unimplemented!();
/// // Get all the outgoing requests.
/// let outgoing_requests = machine.outgoing_requests().await?;
///
/// // Send each request to the server and push the response into the state machine.
/// // You can safely send these requests out in parallel.
/// for request in outgoing_requests {
/// let request_id = request.request_id();
/// // Send the request to the server and await a response.
/// let response = send_request(request).await?;
/// // Push the response into the state machine.
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// #### 🔒 Locking rule
///
/// It's important to note that the outgoing requests method in the
/// [`OlmMachine`], while thread-safe, may return the same request multiple
/// times if it is called multiple times before the request has been marked as
/// sent. To prevent this issue, it is advisable to encapsulate the outgoing
/// request handling logic into a separate helper method and protect it from
/// being called multiple times concurrently using a lock.
///
/// This helps to ensure that the request is only handled once and prevents
/// multiple identical requests from being sent.
///
/// Additionally, if an error occurs while sending a request using the
/// [`OlmMachine::outgoing_requests()`] method, the request will be
/// naturally retried the next time the method is called.
///
/// A more complete example, which uses a helper method, might look like this:
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use ruma::api::client::keys::upload_keys::v3::Response;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::OutgoingRequest};
/// # async fn send_request(request: &OutgoingRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// struct Client {
/// outgoing_requests_lock: tokio::sync::Mutex<()>,
/// olm_machine: OlmMachine,
/// }
///
/// async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// // Let's acquire a lock so we know that we don't send out the same request out multiple
/// // times.
/// let guard = client.outgoing_requests_lock.lock().await;
///
/// for request in client.olm_machine.outgoing_requests().await? {
/// let request_id = request.request_id();
///
/// match send_request(&request).await {
/// Ok(response) => {
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// Err(error) => {
/// // It's OK to ignore transient HTTP errors since requests will be retried.
/// eprintln!(
/// "Error while sending out a end-to-end encryption \
/// related request: {error:?}"
/// );
/// }
/// }
/// }
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// Once we have the helper method that processes our outgoing requests we can
/// structure our sync method as follows:
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::OlmMachine;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// // This is happening at the top of the method so we advertise our
/// // end-to-end encryption capabilities as soon as possible.
/// process_outgoing_requests(client).await?;
///
/// // We can sync with the homeserver now.
/// let response = send_out_sync_request(client).await?;
///
/// // Process the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// ## Receiving room keys and related changes
///
/// The next step in our implementation is to forward messages that were sent
/// directly to the client's device, and state updates about the one-time
/// prekeys, to the [`OlmMachine`]. This is achieved using
/// the [`OlmMachine::receive_sync_changes()`] method.
///
/// The method performs two tasks:
///
/// 1. It processes and, if necessary, decrypts each [to-device] event that was
/// pushed into it, and returns the decrypted events. The original events are
/// replaced with their decrypted versions.
///
/// 2. It produces internal state changes that may trigger the creation of new
/// outgoing requests. For example, if the server informs the client that its
/// one-time prekeys have been depleted, the OlmMachine will create an
/// outgoing request to replenish them.
///
/// Our updated sync method now looks like this:
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
/// # use ruma::api::client::sync::sync_events::v3::Response;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
/// # unimplemented!();
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// process_outgoing_requests(client).await?;
///
/// let response = send_out_sync_request(client).await?;
///
/// let sync_changes = EncryptionSyncChanges {
/// to_device_events: response.to_device.events,
/// changed_devices: &response.device_lists,
/// one_time_keys_counts: &response.device_one_time_keys_count,
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
/// next_batch_token: Some(response.next_batch),
/// };
///
/// // Push the sync changes into the OlmMachine, make sure that this is
/// // happening before the `next_batch` token of the sync is persisted.
/// let to_device_events = client
/// .olm_machine
/// .receive_sync_changes(sync_changes)
/// .await?;
///
/// // Send the outgoing requests out that the sync changes produced.
/// process_outgoing_requests(client).await?;
///
/// // Process the rest of the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// It is important to note that the names of the fields in the response shown
/// in the example match the names of the fields specified in the [sync]
/// response specification.
///
/// It is critical to note that due to the ephemeral nature of to-device
/// events[[1]], it is important to process these events before persisting the
/// `next_batch` sync token. This is because if the `next_batch` sync token is
/// persisted before processing the to-device events, some messages might be
/// lost, leading to decryption failures.
///
/// ## Decrypting room events
///
/// The final step in the decryption process is to decrypt the room events that
/// are received from the server. To do this, the encrypted events must be
/// passed to the [`OlmMachine`], which will use the keys that were previously
/// exchanged between devices to decrypt the events. The decrypted events can
/// then be processed and displayed to the user in the Matrix client.
///
/// Room message [events] can be decrypted using the
/// [`OlmMachine::decrypt_room_event()`] method:
///
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, DecryptionSettings, TrustRequirement};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let encrypted = unimplemented!();
/// # let room_id = unimplemented!();
/// # let machine: OlmMachine = unimplemented!();
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
/// // Decrypt your room events now.
/// let decrypted = machine
/// .decrypt_room_event(encrypted, room_id, &settings)
/// .await?;
/// # Ok(())
/// # }
/// ```
/// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method
/// is designed to be thread-safe and can be safely called concurrently. This
/// means that room message [events] can be processed in parallel, improving the
/// overall efficiency of the end-to-end encryption implementation.
///
/// By allowing room message [events] to be processed concurrently, the client's
/// implementation can take full advantage of the capabilities of modern
/// hardware and achieve better performance, especially when dealing with a
/// large number of messages at once.
///
/// # Encryption
///
/// In this section of the guide, we will focus on enabling the encryption of
/// messages in our Matrix client library. Up until this point, we have been
/// discussing the process of decrypting messages that have been encrypted by
/// other devices. Now, we will shift our focus to the process of encrypting
/// messages on the client side, so that they can be securely transmitted over
/// the Matrix network to other devices.
///
/// This section will guide you through the steps required to set up the
/// encryption process, including establishing the necessary sessions and
/// encrypting messages using the Megolm group session. The specific steps are
/// outlined bellow:
///
/// 1. [Cryptographic devices of other users need to be
/// discovered](#tracking-users)
///
/// 2. [Secure channels between the devices need to be
/// established](#establishing-end-to-end-encrypted-channels)
///
/// 3. [A room key needs to be exchanged with the group](#exchanging-room-keys)
///
/// 4. [Individual messages need to be encrypted using the room
/// key](#encrypting-room-events)
///
/// The process for enabling encryption in a two-device scenario is also
/// depicted in the following sequence diagram:
///
/// ```mermaid
/// sequenceDiagram
/// actor Alice
/// participant Homeserver
/// actor Bob
///
/// Alice->>Homeserver: Download Bob's one-time prekey
/// Homeserver->>Alice: Bob's one-time prekey
/// Alice->>Alice: Encrypt the room key
/// Alice->>Homeserver: Send the room key to each of Bob's devices
/// Homeserver->>Bob: Deliver the room key
/// Alice->>Alice: Encrypt the message
/// Alice->>Homeserver: Send the encrypted message
/// Homeserver->>Bob: Deliver the encrypted message
/// ```
///
/// In the following subsections, we will provide a step-by-step guide on how to
/// enable the encryption of messages using the OlmMachine. We will outline the
/// specific method calls and usage patterns that are required to establish the
/// necessary sessions, encrypt messages, and send them over the Matrix network.
///
/// ## Tracking users
///
/// The first step in the process of encrypting a message and sending it to a
/// device is to discover the devices that the recipient user has. This can be
/// achieved by sending a request to the homeserver to retrieve a list of the
/// recipient's device keys. The response to this request will include the
/// device keys for all of the devices that belong to the recipient, as well as
/// information about their current status and whether or not they support
/// end-to-end encryption.
///
/// The process for discovering and keeping track of devices for a user is
/// outlined in the Matrix specification in the "[Tracking the device list for a
/// user]" section.
///
/// A simplified sequence diagram of the process can also be found bellow.
///
/// ```mermaid
/// sequenceDiagram
/// actor Alice
/// participant Homeserver
///
/// Alice->>Homeserver: Sync with the homeserver
/// Homeserver->>Alice: Users whose device list has changed
/// Alice->>Alice: Mark user's devicel list as outdated
/// Alice->>Homeserver: Ask the server for the new device list of all the outdated users
/// Alice->>Alice: Update the local device list and mark the users as up-to-date
/// ```
///
/// The OlmMachine refers to users whose devices we are tracking as "tracked
/// users" and utilizes the [`OlmMachine::update_tracked_users()`] method to
/// start considering users to be tracked. Keeping the above diagram in mind, we
/// can now update our sync method as follows:
///
/// ```no_run
/// # use anyhow::Result;
/// # use std::ops::Deref;
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
/// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom};
/// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
/// # unimplemented!();
/// # }
/// # fn is_member_event_of_a_joined_user(event: &Raw<AnySyncStateEvent>) -> bool {
/// # true
/// # }
/// # fn get_user_id(event: &Raw<AnySyncStateEvent>) -> OwnedUserId {
/// # unimplemented!();
/// # }
/// # fn is_room_encrypted(room: &JoinedRoom) -> bool {
/// # true
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// process_outgoing_requests(client).await?;
///
/// let response = send_out_sync_request(client).await?;
///
/// let sync_changes = EncryptionSyncChanges {
/// to_device_events: response.to_device.events,
/// changed_devices: &response.device_lists,
/// one_time_keys_counts: &response.device_one_time_keys_count,
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
/// next_batch_token: Some(response.next_batch),
/// };
///
/// // Push the sync changes into the OlmMachine, make sure that this is
/// // happening before the `next_batch` token of the sync is persisted.
/// let to_device_events = client
/// .olm_machine
/// .receive_sync_changes(sync_changes)
/// .await?;
///
/// // Send the outgoing requests out that the sync changes produced.
/// process_outgoing_requests(client).await?;
///
/// // Collect all the joined and invited users of our end-to-end encrypted rooms here.
/// let mut users = Vec::new();
///
/// for (_, room) in &response.rooms.join {
/// // For simplicity reasons we're only looking at the state field of a joined room, but
/// // the events in the timeline are important as well.
/// for event in &room.state.events {
/// if is_member_event_of_a_joined_user(event) && is_room_encrypted(room) {
/// let user_id = get_user_id(event);
/// users.push(user_id);
/// }
/// }
/// }
///
/// // Mark all the users that we consider to be in a end-to-end encrypted room with us to be
/// // tracked. We need to know about all the devices each user has so we can later encrypt
/// // messages for each of their devices.
/// client.olm_machine.update_tracked_users(users.iter().map(Deref::deref)).await?;
///
/// // Process the rest of the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// Now that we have discovered the devices of the users we'd like to
/// communicate with in an end-to-end encrypted manner, we can start considering
/// encrypting messages for those devices. This concludes the sync processing
/// method, we are now ready to move on to the next section, which will explain
/// how to begin the encryption process.
///
/// ## Establishing end-to-end encrypted channels
///
/// In the [Triple
/// Diffie-Hellman](#using-the-triple-diffie-hellman-key-agreement-protocol)
/// section, we described the need for two Curve25519 keys from the recipient
/// device to establish a 1-to-1 secure channel: the long-term identity key of a
/// device and a one-time prekey. In the previous section, we started tracking
/// the device keys, including the long-term identity key that we need. The next
/// step is to download the one-time prekey on an on-demand basis and establish
/// the 1-to-1 secure channel.
///
/// To accomplish this, we can use the [`OlmMachine::get_missing_sessions()`]
/// method in bulk, which will claim the one-time prekey for all the devices of
/// a user that we're not already sharing a 1-to-1 encrypted channel with.
///
/// #### 🔒 Locking rule
///
/// As with the [`OlmMachine::outgoing_requests()`] method, it is necessary to
/// protect this method with a lock, otherwise we will be creating more 1-to-1
/// encrypted channels than necessary.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use ruma::UserId;
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::OlmMachine;
/// # async fn send_request(request: &Request) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let machine: OlmMachine = unimplemented!();
/// // Mark all the users that are part of an encrypted room as tracked
/// if let Some((request_id, request)) =
/// machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
/// {
/// let response = send_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// With the ability to exchange messages directly with devices, we can now
/// start sharing room keys over the 1-to-1 encrypted channel.
///
/// ## Exchanging room keys
///
/// To exchange a room key with our group, we will once again take a bulk
/// approach. The [`OlmMachine::share_room_key()`] method is used to accomplish
/// this step. This method will create a new room key, if necessary, and encrypt
/// it for each device belonging to the users provided as an argument. It will
/// then output an array of sendToDevice requests that we must send to the
/// server, and mark the requests as sent.
///
/// #### 🔒 Locking rule
///
/// Like some of the previous methods, OlmMachine::share_room_key() needs to be
/// protected by a lock to prevent the possibility of creating and sending
/// multiple room keys simultaneously for the same group. The lock can be
/// implemented on a per-room basis, which allows for parallel room key
/// exchanges across different rooms.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use ruma::UserId;
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::{OlmMachine, types::requests::ToDeviceRequest, EncryptionSettings};
/// # async fn send_request(request: &ToDeviceRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let room_id = unimplemented!();
/// # let settings = EncryptionSettings::default();
/// # let machine: OlmMachine = unimplemented!();
/// // Let's share a room key with our group.
/// let requests = machine.share_room_key(
/// room_id,
/// users.iter().map(Deref::deref),
/// EncryptionSettings::default(),
/// ).await?;
///
/// // Make sure each request is sent out
/// for request in requests {
/// let request_id = &request.txn_id;
/// let response = send_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// In order to ensure that room keys are rotated and exchanged when needed, the
/// [`OlmMachine::share_room_key()`] method should be called before sending
/// each room message in an end-to-end encrypted room. If a room key has
/// already been exchanged, the method becomes a no-op.
///
/// ## Encrypting room events
///
/// After the room key has been successfully shared, a plaintext can be
/// encrypted.
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{DecryptionSettings, OlmMachine, TrustRequirement};
/// # use ruma::events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let room_id = unimplemented!();
/// # let event = unimplemented!();
/// # let machine: OlmMachine = unimplemented!();
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
/// let content = AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain("It's a secret to everybody."));
/// let encrypted_content = machine.encrypt_room_event(room_id, content).await?;
/// # Ok(())
/// # }
/// ```
///
/// ## Appendix: Combining the session creation and room key exchange
///
/// The steps from the previous three sections should combined into a single
/// method that is used to send messages.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use serde_json::json;
/// # use ruma::{UserId, RoomId, serde::Raw};
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, types::requests::ToDeviceRequest};
/// # use tokio::sync::MutexGuard;
/// # async fn send_request(request: &Request) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # async fn send_to_device_request(request: &ToDeviceRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # async fn acquire_per_room_lock(room_id: &RoomId) -> MutexGuard<()> {
/// # unimplemented!();
/// # }
/// # async fn get_joined_members(room_id: &RoomId) -> Vec<&UserId> {
/// # unimplemented!();
/// # }
/// # fn is_room_encrypted(room_id: &RoomId) -> bool {
/// # true
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let machine: OlmMachine = unimplemented!();
/// struct Client {
/// session_establishment_lock: tokio::sync::Mutex<()>,
/// olm_machine: OlmMachine,
/// }
///
/// async fn establish_sessions(client: &Client, users: &[&UserId]) -> Result<()> {
/// if let Some((request_id, request)) =
/// client.olm_machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
/// {
/// let response = send_request(&request).await?;
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
/// }
///
/// Ok(())
/// }
///
/// async fn share_room_key(machine: &OlmMachine, room_id: &RoomId, users: &[&UserId]) -> Result<()> {
/// let _lock = acquire_per_room_lock(room_id).await;
///
/// let requests = machine.share_room_key(
/// room_id,
/// users.iter().map(Deref::deref),
/// EncryptionSettings::default(),
/// ).await?;
///
/// // Make sure each request is sent out
/// for request in requests {
/// let request_id = &request.txn_id;
/// let response = send_to_device_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
///
/// Ok(())
/// }
///
/// async fn send_message(client: &Client, room_id: &RoomId, message: &str) -> Result<()> {
/// let mut content = json!({
/// "body": message,
/// "msgtype": "m.text",
/// });
///
/// if is_room_encrypted(room_id) {
/// let content = Raw::new(&json!({
/// "body": message,
/// "msgtype": "m.text",
/// }))?.cast();
///
/// let users = get_joined_members(room_id).await;
///
/// establish_sessions(client, &users).await?;
/// share_room_key(&client.olm_machine, room_id, &users).await?;
///
/// let encrypted = client
/// .olm_machine
/// .encrypt_room_event_raw(room_id, "m.room.message", &content)
/// .await?;
/// }
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// TODO
///
/// [Matrix]: https://matrix.org/
/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md
/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
/// [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md
/// [end-to-end-encryption]: https://en.wikipedia.org/wiki/End-to-end_encryption
/// [homeserver]: https://spec.matrix.org/unstable/#architecture
/// [key-agreement protocol]: https://en.wikipedia.org/wiki/Key-agreement_protocol
/// [client-server specification]: https://matrix.org/docs/spec/client_server/
/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy
/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
/// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user
/// [X3DH]: https://signal.org/docs/specifications/x3dh/
/// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging
/// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync
/// [events]: https://spec.matrix.org/unstable/client-server-api/#events
///
/// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4
pub mod tutorial {}
+20 -15
View File
@@ -70,7 +70,6 @@ use crate::{
KnownSenderData, OlmDecryptionInfo, PrivateCrossSigningIdentity, SenderData,
SenderDataFinder, SessionType, StaticAccountData,
},
requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest},
session_manager::{GroupSessionManager, SessionManager},
store::{
Changes, CryptoStoreWrapper, DeviceChanges, IdentityChanges, IntoCryptoStore, MemoryStore,
@@ -90,12 +89,16 @@ use crate::{
},
ToDeviceEvents,
},
requests::{
AnyIncomingResponse, KeysQueryRequest, OutgoingRequest, ToDeviceRequest,
UploadSigningKeysRequest,
},
EventEncryptionAlgorithm, Signatures,
},
utilities::timestamp_to_iso8601,
verification::{Verification, VerificationMachine, VerificationRequest},
CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, KeysQueryRequest,
LocalTrust, RoomEventDecryptionResult, SignatureError, ToDeviceRequest, TrustRequirement,
CrossSigningKeyExport, CryptoStoreError, DecryptionSettings, DeviceData, LocalTrust,
RoomEventDecryptionResult, SignatureError, TrustRequirement,
};
/// State machine implementation of the Olm/Megolm encryption protocol used for
@@ -576,34 +579,34 @@ impl OlmMachine {
pub async fn mark_request_as_sent<'a>(
&self,
request_id: &TransactionId,
response: impl Into<IncomingResponse<'a>>,
response: impl Into<AnyIncomingResponse<'a>>,
) -> OlmResult<()> {
match response.into() {
IncomingResponse::KeysUpload(response) => {
AnyIncomingResponse::KeysUpload(response) => {
Box::pin(self.receive_keys_upload_response(response)).await?;
}
IncomingResponse::KeysQuery(response) => {
AnyIncomingResponse::KeysQuery(response) => {
Box::pin(self.receive_keys_query_response(request_id, response)).await?;
}
IncomingResponse::KeysClaim(response) => {
AnyIncomingResponse::KeysClaim(response) => {
Box::pin(
self.inner.session_manager.receive_keys_claim_response(request_id, response),
)
.await?;
}
IncomingResponse::ToDevice(_) => {
AnyIncomingResponse::ToDevice(_) => {
Box::pin(self.mark_to_device_request_as_sent(request_id)).await?;
}
IncomingResponse::SigningKeysUpload(_) => {
AnyIncomingResponse::SigningKeysUpload(_) => {
Box::pin(self.receive_cross_signing_upload_response()).await?;
}
IncomingResponse::SignatureUpload(_) => {
AnyIncomingResponse::SignatureUpload(_) => {
self.inner.verification_machine.mark_request_as_sent(request_id);
}
IncomingResponse::RoomMessage(_) => {
AnyIncomingResponse::RoomMessage(_) => {
self.inner.verification_machine.mark_request_as_sent(request_id);
}
IncomingResponse::KeysBackup(_) => {
AnyIncomingResponse::KeysBackup(_) => {
Box::pin(self.inner.backup_machine.mark_request_as_sent(request_id)).await?;
}
};
@@ -2311,8 +2314,8 @@ impl OlmMachine {
/// incremented and updated it in the database. Otherwise, `false`.
///
/// * The (possibly updated) generation counter.
pub async fn maintain_crypto_store_generation<'a>(
&'a self,
pub async fn maintain_crypto_store_generation(
&'_ self,
generation: &Mutex<Option<u64>>,
) -> StoreResult<(bool, u64)> {
let mut gen_guard = generation.lock().await;
@@ -2579,7 +2582,9 @@ fn megolm_error_to_utd_info(
let reason = match error {
EventError(_) => UnableToDecryptReason::MalformedEncryptedEvent,
Decode(_) => UnableToDecryptReason::MalformedEncryptedEvent,
MissingRoomKey(_) => UnableToDecryptReason::MissingMegolmSession,
MissingRoomKey(maybe_withheld) => {
UnableToDecryptReason::MissingMegolmSession { withheld_code: maybe_withheld }
}
Decryption(DecryptionError::UnknownMessageIndex(_, _)) => {
UnableToDecryptReason::UnknownMegolmMessageIndex
}
@@ -34,8 +34,9 @@ use ruma::{
use serde_json::json;
use crate::{
store::Changes, types::events::ToDeviceEvent, CrossSigningBootstrapRequests, DeviceData,
OlmMachine, OutgoingRequests,
store::Changes,
types::{events::ToDeviceEvent, requests::AnyOutgoingRequest},
CrossSigningBootstrapRequests, DeviceData, OlmMachine,
};
/// These keys need to be periodically uploaded to the server.
@@ -214,7 +215,7 @@ pub fn bootstrap_requests_to_keys_query_response(
// And if we have a device, add that
if let Some(dk) = bootstrap_requests
.upload_keys_req
.and_then(|req| as_variant!(req.request.as_ref(), OutgoingRequests::KeysUpload).cloned())
.and_then(|req| as_variant!(req.request.as_ref(), AnyOutgoingRequest::KeysUpload).cloned())
.and_then(|keys_upload_request| keys_upload_request.device_keys)
{
let user_id: String = dk.get_field("user_id").unwrap().unwrap();
@@ -42,11 +42,12 @@ use crate::{
room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
ToDeviceEvent,
},
requests::AnyOutgoingRequest,
CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey,
},
utilities::json_convert,
CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine,
OtherUserIdentityData, OutgoingRequests, TrustRequirement, UserIdentity,
OtherUserIdentityData, TrustRequirement, UserIdentity,
};
#[async_test]
@@ -497,7 +498,7 @@ async fn set_up_alice_cross_signing(alice: &OlmMachine, bob: &OlmMachine) {
upload_signing_keys_req.self_signing_key.unwrap().try_into().unwrap();
let upload_keys_req = cross_signing_requests.upload_keys_req.unwrap().clone();
assert_let!(
OutgoingRequests::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref()
AnyOutgoingRequest::KeysUpload(device_upload_request) = upload_keys_req.request.as_ref()
);
bob.store()
.save_device_data(&[DeviceData::try_from(
@@ -1,18 +1,16 @@
/*
Copyright 2024 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.
*/
// Copyright 2024 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::{fmt::Debug, iter, pin::Pin};
@@ -23,6 +21,7 @@ use matrix_sdk_test::async_test;
use ruma::{room_id, user_id, RoomId, TransactionId, UserId};
use serde::Serialize;
use serde_json::json;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use crate::{
machine::{
@@ -305,13 +304,16 @@ where
/// Given the `room_keys_received_stream`, check that there is a pending update,
/// and pop it.
fn get_room_key_received_update(
room_keys_received_stream: &mut Pin<Box<impl Stream<Item = Vec<RoomKeyInfo>>>>,
room_keys_received_stream: &mut Pin<
Box<impl Stream<Item = Result<Vec<RoomKeyInfo>, BroadcastStreamRecvError>>>,
>,
) -> RoomKeyInfo {
room_keys_received_stream
.next()
.now_or_never()
.flatten()
.expect("We should have received an update of room key infos")
.unwrap()
.pop()
.expect("Received an empty room key info update")
}
@@ -19,6 +19,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt};
use itertools::Itertools;
use matrix_sdk_common::deserialized_responses::{
UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation,
WithheldCode,
};
use matrix_sdk_test::{async_test, message_like_event_content, ruma_response_from_json, test_json};
use ruma::{
@@ -61,17 +62,16 @@ use crate::{
types::{
events::{
room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent},
room_key_withheld::{
MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent, WithheldCode,
},
room_key_withheld::{MegolmV1AesSha2WithheldContent, RoomKeyWithheldContent},
ToDeviceEvent,
},
requests::{AnyOutgoingRequest, ToDeviceRequest},
DeviceKeys, SignedKey, SigningKeys,
},
utilities::json_convert,
verification::tests::bob_id,
Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError,
OutgoingRequests, RoomEventDecryptionResult, ToDeviceRequest, TrustRequirement,
RoomEventDecryptionResult, TrustRequirement,
};
mod decryption_verification_state;
@@ -448,7 +448,7 @@ async fn test_request_missing_secrets() {
.unwrap()
.into_iter()
.filter(|outgoing| match outgoing.request.as_ref() {
OutgoingRequests::ToDeviceRequest(request) => {
AnyOutgoingRequest::ToDeviceRequest(request) => {
request.event_type.to_string() == "m.secret.request"
}
_ => false,
@@ -479,7 +479,7 @@ async fn test_request_missing_secrets_cross_signed() {
.unwrap()
.into_iter()
.filter(|outgoing| match outgoing.request.as_ref() {
OutgoingRequests::ToDeviceRequest(request) => {
AnyOutgoingRequest::ToDeviceRequest(request) => {
request.event_type.to_string() == "m.secret.request"
}
_ => false,
@@ -530,7 +530,8 @@ async fn test_megolm_encryption() {
.next()
.now_or_never()
.flatten()
.expect("We should have received an update of room key infos");
.expect("We should have received an update of room key infos")
.unwrap();
assert_eq!(room_keys.len(), 1);
assert_eq!(room_keys[0].session_id, group_session.session_id());
@@ -682,7 +683,12 @@ async fn test_withheld_unverified() {
bob.try_decrypt_room_event(&room_event, room_id, &decryption_settings).await.unwrap();
assert_let!(RoomEventDecryptionResult::UnableToDecrypt(utd_info) = decrypt_result);
assert!(utd_info.session_id.is_some());
assert_eq!(utd_info.reason, UnableToDecryptReason::MissingMegolmSession);
assert_eq!(
utd_info.reason,
UnableToDecryptReason::MissingMegolmSession {
withheld_code: Some(WithheldCode::Unverified)
}
);
}
/// Test what happens when we feed an unencrypted event into the decryption
@@ -1361,7 +1367,7 @@ async fn test_unsigned_decryption() {
replace_encryption_result,
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
session_id: Some(second_room_key_session_id),
reason: UnableToDecryptReason::MissingMegolmSession,
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
})
);
@@ -1467,7 +1473,7 @@ async fn test_unsigned_decryption() {
thread_encryption_result,
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
session_id: Some(third_room_key_session_id),
reason: UnableToDecryptReason::MissingMegolmSession,
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
})
);
@@ -22,9 +22,9 @@ use crate::{
test_helpers::{get_machine_pair, get_machine_pair_with_session},
tests,
},
types::events::ToDeviceEvent,
types::{events::ToDeviceEvent, requests::ToDeviceRequest},
utilities::json_convert,
EncryptionSyncChanges, OlmError, ToDeviceRequest,
EncryptionSyncChanges, OlmError,
};
#[async_test]
+1 -1
View File
@@ -63,7 +63,6 @@ use crate::{
error::{EventError, OlmResult, SessionCreationError},
identities::DeviceData,
olm::SenderData,
requests::UploadSigningKeysRequest,
store::{Changes, DeviceChanges, Store},
types::{
events::{
@@ -73,6 +72,7 @@ use crate::{
ToDeviceEncryptedEventContent,
},
},
requests::UploadSigningKeysRequest,
CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, OneTimeKey, SignedKey,
},
OlmError, SignatureError,
@@ -23,6 +23,7 @@ use std::{
time::Duration,
};
use matrix_sdk_common::deserialized_responses::WithheldCode;
use ruma::{
events::{
room::{encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility},
@@ -54,11 +55,12 @@ use crate::{
MegolmV1AesSha2Content, RoomEncryptedEventContent, RoomEventEncryptionScheme,
},
room_key::{MegolmV1AesSha2Content as MegolmV1AesSha2RoomKeyContent, RoomKeyContent},
room_key_withheld::{RoomKeyWithheldContent, WithheldCode},
room_key_withheld::RoomKeyWithheldContent,
},
requests::ToDeviceRequest,
EventEncryptionAlgorithm,
},
DeviceData, ToDeviceRequest,
DeviceData,
};
const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
@@ -215,6 +215,7 @@ enum SenderDataReader {
legacy_session: bool,
},
#[serde(alias = "SenderUnverifiedButPreviouslyVerified")]
VerificationViolation(KnownSenderData),
SenderUnverified(KnownSenderData),
@@ -286,7 +287,10 @@ mod tests {
use vodozemac::Ed25519PublicKey;
use super::SenderData;
use crate::types::{DeviceKeys, Signatures};
use crate::{
olm::KnownSenderData,
types::{DeviceKeys, Signatures},
};
#[test]
fn serializing_unknown_device_correctly_preserves_owner_check_failed_if_true() {
@@ -360,6 +364,47 @@ mod tests {
assert_let!(SenderData::SenderVerified { .. } = end);
}
#[test]
fn deserializing_sender_unverified_but_previously_verified_migrates_to_verification_violation()
{
let json = r#"
{
"SenderUnverifiedButPreviouslyVerified":{
"user_id":"@u:s.co",
"master_key":[
150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255,
26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92
],
"master_key_verified":true
}
}
"#;
let end: SenderData = serde_json::from_str(json).expect("Failed to parse!");
assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end);
assert_eq!(user_id, owned_user_id!("@u:s.co"));
}
#[test]
fn deserializing_verification_violation() {
let json = r#"
{
"VerificationViolation":{
"user_id":"@u:s.co",
"master_key":[
150,140,249,139,141,29,63,230,179,14,213,175,176,61,11,255,
26,103,10,51,100,154,183,47,181,117,87,204,33,215,241,92
],
"master_key_verified":true
}
}
"#;
let end: SenderData = serde_json::from_str(json).expect("Failed to parse!");
assert_let!(SenderData::VerificationViolation(KnownSenderData { user_id, .. }) = end);
assert_eq!(user_id, owned_user_id!("@u:s.co"));
}
#[test]
fn equal_sessions_have_same_trust_level() {
let unknown = SenderData::unknown();
@@ -32,9 +32,11 @@ use vodozemac::Ed25519Signature;
use super::StaticAccountData;
use crate::{
error::SignatureError,
requests::UploadSigningKeysRequest,
store::SecretImportError,
types::{DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
types::{
requests::UploadSigningKeysRequest, DeviceKeys, MasterPubkey, SelfSigningPubkey,
UserSigningPubkey,
},
Account, DeviceData, OtherUserIdentityData, OwnUserIdentity, OwnUserIdentityData,
};
-439
View File
@@ -1,439 +0,0 @@
// Copyright 2020 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.
//! Modules containing customized request types.
use std::{collections::BTreeMap, iter, sync::Arc, time::Duration};
#[cfg(test)]
use as_variant::as_variant;
use ruma::{
api::client::{
backup::{add_backup_keys::v3::Response as KeysBackupResponse, RoomKeyBackup},
keys::{
claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse},
get_keys::v3::Response as KeysQueryResponse,
upload_keys::v3::{Request as KeysUploadRequest, Response as KeysUploadResponse},
upload_signatures::v3::{
Request as SignatureUploadRequest, Response as SignatureUploadResponse,
},
upload_signing_keys::v3::Response as SigningKeysUploadResponse,
},
message::send_message_event::v3::Response as RoomMessageResponse,
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
events::{
AnyMessageLikeEventContent, AnyToDeviceEventContent, EventContent, ToDeviceEventType,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId, TransactionId, UserId,
};
use serde::{Deserialize, Serialize};
use crate::types::CrossSigningKey;
/// Customized version of
/// `ruma_client_api::to_device::send_event_to_device::v3::Request`
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ToDeviceRequest {
/// Type of event being sent to each device.
pub event_type: ToDeviceEventType,
/// A request identifier unique to the access token used to send the
/// request.
pub txn_id: OwnedTransactionId,
/// A map of users to devices to a content for a message event to be
/// sent to the user's device. Individual message events can be sent
/// to devices, but all events must be of the same type.
/// The content's type for this field will be updated in a future
/// release, until then you can create a value using
/// `serde_json::value::to_raw_value`.
pub messages:
BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>,
}
impl ToDeviceRequest {
/// Create a new owned to-device request
///
/// # Arguments
///
/// * `recipient` - The ID of the user that should receive this to-device
/// event.
///
/// * `recipient_device` - The device that should receive this to-device
/// event, or all devices.
///
/// * `event_type` - The type of the event content that is getting sent out.
///
/// * `content` - The content of the to-device event.
pub fn new(
recipient: &UserId,
recipient_device: impl Into<DeviceIdOrAllDevices>,
event_type: &str,
content: Raw<AnyToDeviceEventContent>,
) -> Self {
let event_type = ToDeviceEventType::from(event_type);
let user_messages = iter::once((recipient_device.into(), content)).collect();
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
ToDeviceRequest { event_type, txn_id: TransactionId::new(), messages }
}
pub(crate) fn for_recipients(
recipient: &UserId,
recipient_devices: Vec<OwnedDeviceId>,
content: &AnyToDeviceEventContent,
txn_id: OwnedTransactionId,
) -> Self {
let event_type = content.event_type();
let raw_content = Raw::new(content).expect("Failed to serialize to-device event");
if recipient_devices.is_empty() {
Self::new(
recipient,
DeviceIdOrAllDevices::AllDevices,
&event_type.to_string(),
raw_content,
)
} else {
let device_messages = recipient_devices
.into_iter()
.map(|d| (DeviceIdOrAllDevices::DeviceId(d), raw_content.clone()))
.collect();
let messages = iter::once((recipient.to_owned(), device_messages)).collect();
ToDeviceRequest { event_type, txn_id, messages }
}
}
pub(crate) fn with_id_raw(
recipient: &UserId,
recipient_device: impl Into<DeviceIdOrAllDevices>,
content: Raw<AnyToDeviceEventContent>,
event_type: ToDeviceEventType,
txn_id: OwnedTransactionId,
) -> Self {
let user_messages = iter::once((recipient_device.into(), content)).collect();
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
ToDeviceRequest { event_type, txn_id, messages }
}
pub(crate) fn with_id(
recipient: &UserId,
recipient_device: impl Into<DeviceIdOrAllDevices>,
content: &AnyToDeviceEventContent,
txn_id: OwnedTransactionId,
) -> Self {
let event_type = content.event_type();
let raw_content = Raw::new(content).expect("Failed to serialize to-device event");
let user_messages = iter::once((recipient_device.into(), raw_content)).collect();
let messages = iter::once((recipient.to_owned(), user_messages)).collect();
ToDeviceRequest { event_type, txn_id, messages }
}
/// Get the number of unique messages this request contains.
///
/// *Note*: A single message may be sent to multiple devices, so this may or
/// may not be the number of devices that will receive the messages as well.
pub fn message_count(&self) -> usize {
self.messages.values().map(|d| d.len()).sum()
}
}
/// Request that will publish a cross signing identity.
///
/// This uploads the public cross signing key triplet.
#[derive(Debug, Clone)]
pub struct UploadSigningKeysRequest {
/// The user's master key.
pub master_key: Option<CrossSigningKey>,
/// The user's self-signing key. Must be signed with the accompanied master,
/// or by the user's most recently uploaded master key if no master key
/// is included in the request.
pub self_signing_key: Option<CrossSigningKey>,
/// The user's user-signing key. Must be signed with the accompanied master,
/// or by the user's most recently uploaded master key if no master key
/// is included in the request.
pub user_signing_key: Option<CrossSigningKey>,
}
/// Customized version of
/// `ruma_client_api::keys::get_keys::v3::Request`, without any
/// references.
#[derive(Clone, Debug)]
pub struct KeysQueryRequest {
/// The time (in milliseconds) to wait when downloading keys from remote
/// servers. 10 seconds is the recommended default.
pub timeout: Option<Duration>,
/// The keys to be downloaded. An empty list indicates all devices for
/// the corresponding user.
pub device_keys: BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
}
impl KeysQueryRequest {
pub(crate) fn new(users: impl Iterator<Item = OwnedUserId>) -> Self {
let device_keys = users.map(|u| (u, Vec::new())).collect();
Self { timeout: None, device_keys }
}
}
/// Enum over the different outgoing requests we can have.
#[derive(Debug)]
pub enum OutgoingRequests {
/// The `/keys/upload` request, uploading device and one-time keys.
KeysUpload(KeysUploadRequest),
/// 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
/// server, after the response is received an 1-to-1 Olm session will be
/// established with the user/device pair.
KeysClaim(KeysClaimRequest),
/// The to-device requests, this request is used for a couple of different
/// things, the main use is key requests/forwards and interactive device
/// verification.
ToDeviceRequest(ToDeviceRequest),
/// Signature upload request, this request is used after a successful device
/// or user verification is done.
SignatureUpload(SignatureUploadRequest),
/// A room message request, usually for sending in-room interactive
/// verification events.
RoomMessage(RoomMessageRequest),
}
#[cfg(test)]
impl OutgoingRequests {
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
as_variant!(self, Self::ToDeviceRequest)
}
}
impl From<KeysQueryRequest> for OutgoingRequests {
fn from(request: KeysQueryRequest) -> Self {
Self::KeysQuery(request)
}
}
impl From<KeysClaimRequest> for OutgoingRequests {
fn from(r: KeysClaimRequest) -> Self {
Self::KeysClaim(r)
}
}
impl From<KeysUploadRequest> for OutgoingRequests {
fn from(request: KeysUploadRequest) -> Self {
Self::KeysUpload(request)
}
}
impl From<ToDeviceRequest> for OutgoingRequests {
fn from(request: ToDeviceRequest) -> Self {
Self::ToDeviceRequest(request)
}
}
impl From<RoomMessageRequest> for OutgoingRequests {
fn from(request: RoomMessageRequest) -> Self {
Self::RoomMessage(request)
}
}
impl From<SignatureUploadRequest> for OutgoingRequests {
fn from(request: SignatureUploadRequest) -> Self {
Self::SignatureUpload(request)
}
}
impl From<OutgoingVerificationRequest> for OutgoingRequest {
fn from(r: OutgoingVerificationRequest) -> Self {
Self { request_id: r.request_id().to_owned(), request: Arc::new(r.into()) }
}
}
impl From<SignatureUploadRequest> for OutgoingRequest {
fn from(r: SignatureUploadRequest) -> Self {
Self { request_id: TransactionId::new(), request: Arc::new(r.into()) }
}
}
impl From<KeysUploadRequest> for OutgoingRequest {
fn from(r: KeysUploadRequest) -> Self {
Self { request_id: TransactionId::new(), request: Arc::new(r.into()) }
}
}
/// 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
/// one-time keys.
KeysUpload(&'a KeysUploadResponse),
/// 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
/// signing identity as shared.
SigningKeysUpload(&'a SigningKeysUploadResponse),
/// The cross signing signature upload response.
SignatureUpload(&'a SignatureUploadResponse),
/// A room message response, usually for interactive verifications.
RoomMessage(&'a RoomMessageResponse),
/// Response for the server-side room key backup request.
KeysBackup(&'a KeysBackupResponse),
}
impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysUploadResponse) -> Self {
IncomingResponse::KeysUpload(response)
}
}
impl<'a> From<&'a KeysBackupResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysBackupResponse) -> Self {
IncomingResponse::KeysBackup(response)
}
}
impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysQueryResponse) -> Self {
IncomingResponse::KeysQuery(response)
}
}
impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> {
fn from(response: &'a ToDeviceResponse) -> Self {
IncomingResponse::ToDevice(response)
}
}
impl<'a> From<&'a RoomMessageResponse> for IncomingResponse<'a> {
fn from(response: &'a RoomMessageResponse) -> Self {
IncomingResponse::RoomMessage(response)
}
}
impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> {
fn from(response: &'a KeysClaimResponse) -> Self {
IncomingResponse::KeysClaim(response)
}
}
impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> {
fn from(response: &'a SignatureUploadResponse) -> Self {
IncomingResponse::SignatureUpload(response)
}
}
/// Outgoing request type, holds the unique ID of the request and the actual
/// request.
#[derive(Debug, Clone)]
pub struct OutgoingRequest {
/// The unique id of a request, needs to be passed when receiving a
/// response.
pub(crate) request_id: OwnedTransactionId,
/// The underlying outgoing request.
pub(crate) request: Arc<OutgoingRequests>,
}
impl OutgoingRequest {
/// Get the unique id of this request.
pub fn request_id(&self) -> &TransactionId {
&self.request_id
}
/// Get the underlying outgoing request.
pub fn request(&self) -> &OutgoingRequests {
&self.request
}
}
/// Customized owned request type for sending out room messages.
#[derive(Clone, Debug)]
pub struct RoomMessageRequest {
/// The room to send the event to.
pub room_id: OwnedRoomId,
/// The transaction ID for this event.
///
/// Clients should generate an ID unique across requests with the
/// same access token; it will be used by the server to ensure
/// idempotency of requests.
pub txn_id: OwnedTransactionId,
/// The event content to send.
pub content: AnyMessageLikeEventContent,
}
/// A request that will back up a batch of room keys to the server.
#[derive(Clone, Debug)]
pub struct KeysBackupRequest {
/// The backup version that these room keys should be part of.
pub version: String,
/// The map from room id to a backed up room key that we're going to upload
/// to the server.
pub rooms: BTreeMap<OwnedRoomId, RoomKeyBackup>,
}
/// An enum over the different outgoing verification based requests.
#[derive(Clone, Debug)]
pub enum OutgoingVerificationRequest {
/// The to-device verification request variant.
ToDevice(ToDeviceRequest),
/// The in-room verification request variant.
InRoom(RoomMessageRequest),
}
impl OutgoingVerificationRequest {
/// Get the unique id of this request.
pub fn request_id(&self) -> &TransactionId {
match self {
OutgoingVerificationRequest::ToDevice(t) => &t.txn_id,
OutgoingVerificationRequest::InRoom(r) => &r.txn_id,
}
}
}
impl From<ToDeviceRequest> for OutgoingVerificationRequest {
fn from(r: ToDeviceRequest) -> Self {
OutgoingVerificationRequest::ToDevice(r)
}
}
impl From<RoomMessageRequest> for OutgoingVerificationRequest {
fn from(r: RoomMessageRequest) -> Self {
OutgoingVerificationRequest::InRoom(r)
}
}
impl From<OutgoingVerificationRequest> for OutgoingRequests {
fn from(request: OutgoingVerificationRequest) -> Self {
match request {
OutgoingVerificationRequest::ToDevice(r) => OutgoingRequests::ToDeviceRequest(r),
OutgoingVerificationRequest::InRoom(r) => OutgoingRequests::RoomMessage(r),
}
}
}
@@ -22,7 +22,7 @@ use std::{
use futures_util::future::join_all;
use itertools::Itertools;
use matrix_sdk_common::executor::spawn;
use matrix_sdk_common::{deserialized_responses::WithheldCode, executor::spawn};
use ruma::{
events::{AnyMessageLikeEventContent, ToDeviceEventType},
serde::Raw,
@@ -41,8 +41,8 @@ use crate::{
ShareInfo, ShareState,
},
store::{Changes, CryptoStoreWrapper, Result as StoreResult, Store},
types::events::{room::encrypted::RoomEncryptedEventContent, room_key_withheld::WithheldCode},
Device, DeviceData, EncryptionSettings, OlmError, ToDeviceRequest,
types::{events::room::encrypted::RoomEncryptedEventContent, requests::ToDeviceRequest},
Device, DeviceData, EncryptionSettings, OlmError,
};
#[derive(Clone, Debug)]
@@ -779,6 +779,7 @@ mod tests {
};
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::WithheldCode;
use matrix_sdk_test::{async_test, ruma_response_from_json};
use ruma::{
api::client::{
@@ -801,14 +802,12 @@ mod tests {
types::{
events::{
room::encrypted::EncryptedToDeviceEvent,
room_key_withheld::{
RoomKeyWithheldContent::{self, MegolmV1AesSha2},
WithheldCode,
},
room_key_withheld::RoomKeyWithheldContent::{self, MegolmV1AesSha2},
},
requests::ToDeviceRequest,
DeviceKeys, EventEncryptionAlgorithm,
},
EncryptionSettings, LocalTrust, OlmMachine, ToDeviceRequest,
EncryptionSettings, LocalTrust, OlmMachine,
};
fn alice_id() -> &'static UserId {
@@ -19,6 +19,7 @@ use std::{
};
use itertools::{Either, Itertools};
use matrix_sdk_common::deserialized_responses::WithheldCode;
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, trace};
@@ -27,7 +28,6 @@ use super::OutboundGroupSession;
use crate::{
error::{OlmResult, SessionRecipientCollectionError},
store::Store,
types::events::room_key_withheld::WithheldCode,
DeviceData, EncryptionSettings, LocalTrust, OlmError, OwnUserIdentityData, UserIdentityData,
};
#[cfg(doc)]
@@ -517,6 +517,7 @@ mod tests {
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::WithheldCode;
use matrix_sdk_test::{
async_test, test_json,
test_json::keys_query_sets::{
@@ -536,7 +537,6 @@ mod tests {
group_sessions::share_strategy::collect_session_recipients, CollectStrategy,
},
testing::simulate_key_query_response_for_verification,
types::events::room_key_withheld::WithheldCode,
CrossSigningKeyExport, EncryptionSettings, LocalTrust, OlmError, OlmMachine,
};
@@ -1368,7 +1368,7 @@ mod tests {
machine
.mark_request_as_sent(
&TransactionId::new(),
crate::IncomingResponse::KeysQuery(&kq_response),
crate::types::requests::AnyIncomingResponse::KeysQuery(&kq_response),
)
.await
.unwrap();

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