Compare commits

...

412 Commits

Author SHA1 Message Date
Damir Jelić 18b169ca7e chore: Release matrix-sdk version 0.13.0 2025-07-10 15:15:04 +02:00
Damir Jelić b9ce4059fb refactor(sqlite): Move the transaction in find_event_relations into a function 2025-07-10 14:23:10 +02:00
Damir Jelić e8dcb5d250 chore: Add a changelog entry for the fix for GHSA-275g-g844-73jh
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-07-10 13:55:02 +02:00
Damir Jelić d0c01006e4 fix(sqlite): Fix a SQL injection issue in the find_event_relations function
The SQLite implementation for the
EventCache::find_event_with_relations() the relation type list isn't
inserted using SQL placeholders.

The relation types are inserted manually using a format!() call. The
usage of the format!() call can lead to SQL injection if a
RelationType::Custom variant is used which contains SQL expressions.

This patch modifies the, query logic which retrieves the related events,
to use two separate queries which use SQL placeholders to insert all
the dynamic variables.

Security-Impact: Moderate
CVE: CVE-2025-53549
GitHub-Advisory: GHSA-275g-g844-73jh
2025-07-10 13:55:02 +02:00
Damir Jelić dc98bf7633 test: Add a test for GHSA-275g-g844-73jh 2025-07-10 13:55:02 +02:00
Doug ae57156252 ffi: Add reload_tracing_file_writer() to reconfigure the RollingFileAppender. 2025-07-10 13:21:42 +02:00
Doug 3b09c60e20 ffi: Wrap the tracing text layers inside a reload layer.
The generics have been removed from text_layers() to make this possible.

---------

Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2025-07-10 13:21:42 +02:00
Ivan Enderlin fcdb63dcbe feat(ui): Add RoomListService::new_with_share_pos.
This patch adds the new `RoomListService::new_with_share_pos`
constructor. It decides whether the `share_pos` feature of sliding sync
should be enabled or not.

`SyncServiceBuilder` gains a new `with_share_pos` method to configure
the way the `RoomListService` is built.

The FFI bindings are updated accordingly.
2025-07-10 11:52:31 +02:00
Nico Steinle a095872083 fix(examples): Remove a duplicate comment from the examples
Signed-off-by: Nico Steinle <Nico-Steinle@t-online.de>
2025-07-09 18:42:22 +02:00
Denis Kasak d68895f24a Clarify project structure and status of internal crates (#5377)
Signed-off-by: Denis Kasak <dkasak@termina.org.uk>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2025-07-09 14:47:38 +00:00
Michael Goldenberg ab81388018 test(indexeddb): add event cache store integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg c5436ed73e test(indexeddb): add IndexedDB-specific integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg 2e7721b36c feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::load_all_chunks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg a514019d7c refactor(indexeddb): add helper fns for EventCacheStore::load_all_chunks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg 2f8f39795f feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::handle_linked_chunk_updates
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg b1ef15c346 refactor(indexeddb): add helper fns for EventCacheStore::handle_linked_chunk_updates
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg 829b6a7624 feat(indexeddb): add indexeddb event cache store impl (backed by memory store)
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg 7b2cd8e434 feat(indexeddb): add debug impl to serializers and event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg d567a45bee feat(indexeddb): add error type for communicating errors outside of module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg 9c08cd8973 feat(indexeddb): add type for building IndexeddbEventCacheStore
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Michael Goldenberg e0ceef33f8 feat(indexeddb): add top-level type for implementing EventCacheStore
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-09 15:35:31 +02:00
Ivan Enderlin 72d133260c test(sdk): Test SendQueue::subscribe.
This patch updates the `assert_update` macro in the send queue test
suites to test that `SendQueue::subscribe` provides the same data that
`RoomSendQueue::subscribe`. It ensures `SendQueueUpdate` won't miss
an update.
2025-07-09 14:03:03 +02:00
Ivan Enderlin 91e0c76a2f feat(sdk): Introduce SendQueue::subscribe.
To get updates from the `SendQueue`, one needs to use
`RoomSendQueue::subscribe`, it emits `RoomSendQueueUpdate`s. However, if
one wants to receive updates for all rooms managed by the `SendQueue`,
instead of subscribing to all `RoomSendQueue` individually, this
patch introduces a new `SendQueue::subscribe` method, which emits
`SendQueueUpdate`s. It's basically a wrapper around
`RoomSendQueueUpdate` by adding an `OwnedRoomId`.
2025-07-09 14:03:03 +02:00
Ivan Enderlin 7fac1d246d doc(sdk): Update the CHANGELOG.md. 2025-07-09 12:43:09 +02:00
Ivan Enderlin 6762e70880 chore(sdk): Rename variants of RoomEventCacheGenericUpdate.
This patch renames variants of `RoomEventCacheGenericUpdate`:

- `TimelineUpdated` becomes `UpdateTimeline`,
- `Cleared` becomes `Clear`.

It matches the rest of the codebase where we use _verb_ + _subject_.
2025-07-09 12:43:09 +02:00
Ivan Enderlin 6b93f6698b feat(sdk): Add RoomEventCacheGenericUpdate::Cleared.
This patch adds the `RoomEventCacheGenericUpdate::Cleared` variant. It's
helpful to know when a room has been cleared.
2025-07-09 12:43:09 +02:00
Benjamin Bouvier c92a89d571 chore(sqlite): reorder methods and add doc comment for encode_key
This reorders methods so that they're grouped in "dual" pairs
(encode/decode, serialize/deserialize). Also adds a doc comment to
`encode_key`, as I've wondered in the past what it did.
2025-07-09 12:37:34 +02:00
Benjamin Bouvier 684f228e70 refactor(sqlite): share implementations of the encode/decode/serialize/deserialize sqlite store helpers 2025-07-09 12:37:34 +02:00
Ivan Enderlin 0f9faad48a doc(ffi,ui): Update the CHANGELOG.mds. 2025-07-09 12:31:22 +02:00
Ivan Enderlin d7550ec645 feat(ui): RoomListService::subscribe_to_rooms calls LatestEvents::listen_to_room.
This patch updates `RoomListService::subscribe_to_rooms` to call
`LatestEvents::listen_to_room` automatically. This method becomes async,
which propagates to a couple of callers.

The idea is that when one is interested by a specific room, a
subscription will be applied. This is an opportunity to also “activate”
the computation of the `LatestEvent` for this specific room, so that the
user doesn't have to do that manually (except if room subscription is
never used).
2025-07-09 12:31:22 +02:00
Ivan Enderlin ea5645869e feat(sdk): Add the request_duration log field in HttpClient::send_request.
This patch adds a new log field, named `request_duration` in
`HttpClient::send_request`. It helps to know how much time the request
took to be executed.
2025-07-09 12:19:34 +02:00
Ivan Enderlin 8eed17bbfd doc(sdk): Fix a typo in SlidingSyncInner::network_timeout. 2025-07-09 11:27:53 +02:00
Benjamin Bouvier 40c08335ee feat(multiverse): add support for read receipts 2025-07-09 11:05:09 +02:00
Benjamin Bouvier ae5ec0fa26 test(timeline): add a test for the initial filling of threaded read receipts 2025-07-09 11:05:09 +02:00
Benjamin Bouvier 880f754f32 test(timeline): add test for sending threaded read receipts 2025-07-09 11:05:09 +02:00
Benjamin Bouvier 4d23b6490d test(timeline): add test for read receipts updates received from sync 2025-07-09 11:05:09 +02:00
Benjamin Bouvier e0e531c737 test(timeline): add test for latest_user_read_receipt for a threaded-timeline 2025-07-09 11:05:09 +02:00
Benjamin Bouvier 82926d6f08 test(timeline): allow setting the timeline focus in TestTimeline 2025-07-09 11:05:09 +02:00
Benjamin Bouvier f45c9aa3a7 feat(timeline): support threaded read receipts 2025-07-09 11:05:09 +02:00
Benjamin Bouvier 7966dd0544 fix(timeline): when a threaded timeline has no read receipts, don't unset the unread flag 2025-07-09 11:05:09 +02:00
Kévin Commaille 54e6a7d8d1 Upgrade Ruma
This is a bugfix for the `compat-lax-room-create-deser` and
`compat-lax-room-topic-deser` features, which didn't work with events
in the wild.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-09 08:43:12 +02:00
Jonas Platte 995ec618df chore: Upgrade xtask and uniffi-bindgen to Rust edition 2024 2025-07-09 08:41:41 +02:00
Jonas Platte f9864b7ef4 refactor(ui): Use named return types over RPITs
… in private methods.
2025-07-09 08:38:38 +02:00
Jonas Platte d23eae262e refactor(ui): Inline a variable 2025-07-09 08:38:38 +02:00
Tobias Fella 52090bb199 docs(crypto): Remove wrong statement about encryption keys for OlmMachine::with_store
When using this function, whether encryption keys are dropped depends on the crypto store implementation used.
2025-07-09 08:32:31 +02:00
Valere 40e3cd3c22 changelog: add changelog for sending e2ee to-device with widgets 2025-07-08 15:40:05 +02:00
Ivan Enderlin a58a74eaa7 feat(sdk): LatestEventValue::None overwrites previous value.
This patch removes an optimisation that was considering
`LatestEventValue::None` as a value that should no overwrite the
previous value if any. This is a mistake: if the event cache is cleared,
it won't be possible to calculate a new value, and the previous value
must be overwritten.
2025-07-08 14:47:43 +02:00
Ivan Enderlin 1c549a3ca1 test(sdk): Test LatestEventValue is being updated. 2025-07-08 14:47:43 +02:00
Ivan Enderlin a7bef8870f feat(sdk): LatestEventValue implements Default. 2025-07-08 14:47:43 +02:00
Ivan Enderlin 5e946108fe feat(sdk): LatestEventValue represents all kind of suitable latest events.
This patch adds new variants to `LatestEventValue` that represent
all the possible suitable latest events. It's heavily inspired by
`matrix_sdk_base::latest_event::PossibleLatestEvent`. It uses the
new `find_and_map` method to find a suitable `TimelineEvent` and to
map it into a `LatestEventValue`. This method is heavily inspired by
`matrix_sdk_base::latest_value::is_suitable_for_latest_event`.

To be able to provide the `power_levels` to `find_and_map`, a
`WeakClient` is required by `LatestEvents::new`. It flows up to a
`WeakRoom` in `RoomLatestEvents`, used to create or to update the
`LatestEventValue`.

A particular care is applied to re-compute the `power_levels` only when
necessary and once **per room** (and not per latest event value).
Fetching the `power_levels` requires an access to the storage, it's
not anodyne.
2025-07-08 14:47:43 +02:00
Ivan Enderlin 34ccd26ee6 refactor(sdk): Rename RoomEventCache::rfind_event_in_memory_by to rfind_map_event_in_memory_by.
This patch changes `RoomEventCache::rfind_event_in_memory_by` to `rfind_map…`.
2025-07-08 14:47:43 +02:00
Ivan Enderlin 3eeb046e62 feat(skd): compute_latest_events calls the new LatestEvent::update method. 2025-07-08 14:47:43 +02:00
Ivan Enderlin 6f84a44a1c feat(sdk): Create LatestEventsError. 2025-07-08 14:47:43 +02:00
Ivan Enderlin cf16978b15 feat(sdk): RegisteredRooms gets a clone of EventCache. 2025-07-08 14:47:43 +02:00
Ivan Enderlin 6a30a802bb chore(sdk): Use std::fmt to simplify the code. 2025-07-08 14:47:43 +02:00
Benjamin Bouvier db477a84bf chore(tests): make new threads discovery deterministic 2025-07-08 10:20:30 +02:00
Benjamin Bouvier 96fbbd3cd8 fix(timeline): when reloading a fresh timeline, also reload the related events 2025-07-08 10:20:30 +02:00
Benjamin Bouvier bbbcec5963 chore(tests): make the tests independent to races with respect to initial values 2025-07-08 10:20:30 +02:00
Benjamin Bouvier df98b71836 fix(event cache): don't remove duplicated paginated events, if there's no new events 2025-07-08 10:20:30 +02:00
Benjamin Bouvier 0d901e4a86 fix(timeline): correctly update a thread root after a gappy sync 2025-07-08 10:20:30 +02:00
Benjamin Bouvier 9d7afaaa1c feat(timeline): clear a thread if a user has been ignored/unignored 2025-07-08 10:20:30 +02:00
Benjamin Bouvier 2fc616645f chore(tests): split the event cache integration test module into smaller files 2025-07-08 10:20:30 +02:00
Benjamin Bouvier b8f6ab066d test(timeline): add a test for local echo filtering in a threaded timeline 2025-07-08 10:20:30 +02:00
Benjamin Bouvier bb7e6cb562 fix(timeline): use the timeline handle to send a new reaction
This is more precise than using the event timeline item kind: indeed,
when the event for which we want to add a reaction is a local echo that
has been sent, it is still marked as a local echo, but the send queue
will not know about it, and thus will not be able to add the reaction
immediately, leading to a silent failure.

The `TimelineItemHandle` was made to help with this kind of situation:
in this case, it will return `TimelineItemHandle::Remote`, even though
the item is a local echo that has been sent; that way we can use the
event id, and correctly send a (remote) reaciton event immediately.

The following commit includes a regression test.
2025-07-08 10:20:30 +02:00
Benjamin Bouvier 3e81514d07 test(timeline): add a test that a related event comes to a threaded timeline 2025-07-08 10:20:30 +02:00
Benjamin Bouvier bfcf47743e chore(tests): add comments explaining what the timeline thread tests do 2025-07-08 10:20:30 +02:00
Kévin Commaille 69448cca61 Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-08 09:25:26 +02:00
Kévin Commaille 5daf2922b7 refactor(sdk): Use SupportedVersions to cache the response of the /versions endpoint
Using this type will be mandatory in the next breaking release of Ruma,
that will gain support for recognizing unstable features. Besides, it
allows to cache the supported versions and features in a single
CachedValue, which makes more sense than separately because we always
get both at the same time in production.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-08 09:25:26 +02:00
Benjamin Bouvier c81a56c22b fix(doc): add a missing parameter to BaseClient::new() in a doctest 2025-07-07 16:18:45 +02:00
Benjamin Bouvier f891bd13cb chore(base): add changelog entry for the new parameter in BaseClient::new() 2025-07-07 16:18:45 +02:00
Benjamin Bouvier a50a570fc1 doc(ffi): update an oudated method doc comment 2025-07-07 16:18:45 +02:00
Damir Jelić ec112ca32d test(base): Test that we set the joined at timestamp 2025-07-07 12:36:23 +02:00
Damir Jelić 15fdf1e86e feat(base): Remember when a user explicitly accepted an invite 2025-07-07 12:36:23 +02:00
Damir Jelić 3d6d798ca3 docs(base): Document the room_joined() method a bit better 2025-07-07 12:36:23 +02:00
Benjamin Bouvier 935ffa5aea chore(multiverse): paginate fewer events, to better test the behavior of pagination 2025-07-07 11:40:18 +02:00
Benjamin Bouvier 1c19e7477c fix(event cache): when some thread events came from sync, don't consider we've reached the start if we haven't seen the root yet 2025-07-07 11:40:18 +02:00
Benjamin Bouvier 6a1576a085 refactor(timeline): make use of the threaded event cache in the timeline 2025-07-07 11:40:18 +02:00
Benjamin Bouvier a1f028c54a feat(event cache): add a linked chunk per thread and add some useful primitives 2025-07-07 11:40:18 +02:00
Kévin Commaille f2576e80ec Upgrade Ruma to latest release
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 11:08:44 +02:00
Ivan Enderlin e8f705d76f doc: Fix markup in README.md
This patch fixes the markup in the `README.md` file. Links were broken because of the mix of HTML and Markdown. Plus, the HTML markup was kind of incorrect. Plus, too much `<br />`, most of them were “collapsed” automatically.

Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2025-07-07 10:17:14 +02:00
Kévin Commaille 049993d37e test(sdk): Port sliding-sync tests to MatrixMockServer and MockClientBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Kévin Commaille 14366e85b1 test(sdk): Port sliding-sync tests to MatrixMockServer and MockClientBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Kévin Commaille 849d705cd1 refactor(sdk): Port client tests to MatrixMockServer and MockClientBuilder
Because it's such a nicer API!

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Kévin Commaille d4adc81fe0 test(sdk): Allow to override the server versions and request config of MockClientBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Kévin Commaille 577a8feb12 refactor(sdk): Expose directly the URI of MatrixMockServer
Since there are several places that use it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Kévin Commaille 7fb3d216f6 refactor(sdk): Take an Option<&str> in MockClientBuilder::new()
Usually tests that don't construct it via MockMatrixServer don't care
about the homeserver URL, so this allows to provide a default URL for
them.

Also we don't force to allocate a string when the inner API actually
uses a borrowed string.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-07 09:42:47 +02:00
Jonas Platte 07808b4301 refactor: Enable extra code style clippy lints 2025-07-05 21:20:34 +02:00
Kévin Commaille f9e7d16347 Upgrade Ruma
It seems that some `m.room.topic` events in the wild have the wrong
format for the `m.topic` field. Since this field was stabilized recently
in Ruma, deserializing these events now fails. We added a feature to
ignore this field if its deserialization fails.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-04 20:51:57 +02:00
Kévin Commaille c98b2a1b3f refactor(test): Change the bound on EventBuilder to StaticEventContent
The `EventContent` trait is gone in Ruma so this will ease the upgrade
to the next breaking release.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-04 12:13:22 +03:00
Stefan Ceriu b44a1e46c4 feat(base): ignore threaded messages when computing room read receipts and client wide threads feature flag
With the UI crate now sending threaded read receipts we need to start considering them when computing unread counts. As a first step before the participation model, threaded messages will be ignored when computing room unread counts.
2025-07-04 11:42:35 +03:00
Stefan Ceriu f17c3c5af4 feat(ui): send (un)threaded read receipts based on the current timeline instance's focus kind.
This patch moves away from always sending unthreaded read receipts to checking the timeline's focus and `hide_threaded_events` associated values to see whether `ReceiptThread::Main` or `ReceiptThread::Thread` should be used.
2025-07-04 11:42:35 +03:00
Kévin Commaille 5448192ea4 Upgrade Ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-04 09:53:29 +02:00
Neil Johnson 74800e20b4 Update README to make Element sponsorship explicit 2025-07-03 18:06:18 +02:00
Benjamin Bouvier 8157193aef chore(ffi): adjust to uniffi-bindgen-go's expectations for types 2025-07-03 17:14:56 +02:00
Benjamin Bouvier 679c99aa76 refactor(notification): remove BatchNotificationFetchingResult abstraction
It's a hashmap!
2025-07-03 17:14:56 +02:00
Benjamin Bouvier 6fa76e4b12 refactor(notification): remove generic parameter in BatchNotificationFetchingResult 2025-07-03 17:14:56 +02:00
Benjamin Bouvier 8b33806496 refactor(notification): have NotificationClient::get_notifications also return statuses 2025-07-03 17:14:56 +02:00
Benjamin Bouvier 3d114aea50 refactor(notification): random tweaks here and there 2025-07-03 17:14:56 +02:00
Benjamin Bouvier 72dcf5ed46 refactor(ffi): return some error info when a single notification couldn't be obtained 2025-07-03 17:14:56 +02:00
Benjamin Bouvier 58748bec3a chore(ffi): tweak alias names in the FFI layer
It doesn't make sense to name a thing "Matrix" when it comes from the
core SDK crate.

Also adjust a now slightly-incorrect doc comment for `get_notification`.
2025-07-03 17:14:56 +02:00
Benjamin Bouvier f0b6225e40 feat(notifications): provide the NotificationStatus as the return type when getting notifications
This is more explicit for these API's users, which can decide to do
different things based on whether an event has been ignored, or filtered
out.
2025-07-03 17:14:56 +02:00
Benjamin Bouvier 17a58684f6 refactor(notification client): reduce indent again, and make use of Option::is_some_and 2025-07-03 17:14:56 +02:00
Benjamin Bouvier f8c468d6fa refactor(notification client): reduce indent in get_notifications_in_sliding_sync
No functional changes, only reducing indent.
2025-07-03 17:14:56 +02:00
Benjamin Bouvier 39d1ed9bc6 chore: exclude the room id / event id from the data to be sent to sentry
These are not included in Element's main privacy policy, and may
constitute PII (because the homeserver may include the name of some
user). We keep the information as separate log lines, so that
rageshakes/manual reports still include those.
2025-07-03 16:07:46 +02:00
Benjamin Bouvier 3d1d1c8f6d feat(state store): send deserialization errors to sentry \o/ 2025-07-03 16:07:46 +02:00
Benjamin Bouvier 5ad958722f feat(state store): include serde json error path when failing to deserialize in the state store 2025-07-03 16:07:46 +02:00
Benjamin Bouvier bedcbfd7ff fix(event cache): don't return an error when a linked chunk is empty
The metadata loading shouldn't cause an error to be displayed, when the
linked chunk is empty; this can happen for new rooms we've never
visited.

Spotted while investigating some failures in a rageshake.
2025-07-03 14:34:27 +02:00
Benjamin Bouvier 743dec9a65 chore(docs): fix a doc comment referring to the wrong type 2025-07-02 15:47:59 +02:00
Ivan Enderlin f1a2093cfc fix(ui): Avoid calling TimelineEvent::raw() twice.
This patch replaces two successive calls to `TimelineEvent::raw()` by a
single one.
2025-07-02 15:45:29 +02:00
Ivan Enderlin 8057991aee fix(sdk): Don't call event.raw() twice.
This patch replaces a second call to `event.raw()` by re-using the
`raw_event` variable.
2025-07-02 15:45:29 +02:00
Ivan Enderlin d45ef567d4 test(sdk): Add a new test for RoomEventCache::rfind_event_in_memory_by. 2025-07-02 14:44:21 +02:00
Ivan Enderlin 42ee967b46 feat(sdk): Add RoomEventCache::rfind_event_in_memory_by.
This patch introduces a new method named
`RoomEventCache::rfind_event_in_memory_by` to look for an in-memory
event, starting from the most recent event, by applying a predicate
closure on each event.
2025-07-02 14:44:21 +02:00
Ivan Enderlin 4c7575bc9e refactor(sdk): Rename RoomEventCache::event_with_relations to find_event_with_relations.
This patch renames the `RoomEventCache::event_with_relations` method to
`find_event_with_relations` for the sake of clarity, also to match the
other method names, and to match the Rust standard library namings.
2025-07-02 14:44:21 +02:00
Ivan Enderlin 5e927f8109 refactor(sdk): Rename RoomEventCache::event to RoomEventCache::find_event.
This patch renames the `RoomEventCache::event` method to `find_event` to
clarify what it does, and to match the Rust standard library namings.
2025-07-02 14:44:21 +02:00
Ivan Enderlin f91ee36245 doc(sdk): Fix a typo in the documentation. 2025-07-02 14:44:21 +02:00
Benjamin Bouvier 0f8fc53019 chore(ci): use the dev profile when building the swift bindings (#5328)
The swift bindings aren't getting tested (they don't run) in CI anymore,
so building with the reldbg profile (that's a workaround to make it run
and not crash in production) doesn't provide more value than building in
debug mode, while taking much longer to build.

Let's use the default dev profile for this; we have to specify it
manually, because the default for the xtask command is to use the
`reldbg` profile otherwise.

This requires a fix for the dev profile, that consists in being able to
set the iOS deployment target, and set it to a high value in CI
settings. Production builds *don't* have to set it, though.
2025-07-02 14:20:53 +02:00
Benjamin Bouvier be3af5e0d4 refactor(event cache): push down absence of gap handling in back-paginations
This will be useful to do for threaded paginations too.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 38bbdf0547 doc(event cache): tweak outdated comment 2025-07-02 12:43:42 +02:00
Benjamin Bouvier b188a157af refactor(event cache): inline most uses of EventLinkedChunk::push_events
The only remaining uses outside the `EventLinkedChunk` are in tests of
deduplication, so keep the method as a test-only method for now.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 9ccbac0c0e refactor(event cache): inline and remove from public API remove_empty_chunk_at 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 137fc9cfbb refactor(event cache): remove EventLinkedChunk::push_gap from the public API
it's only used internally (and in tests), and it's not doing much, so we
can inline it as call sites instead.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 40a4c9a7e1 refactor(event cache): move pushing the live events into its own method in EventLinkedChunk 2025-07-02 12:43:42 +02:00
Benjamin Bouvier ad358955fd refactor(linked chunk): simplify remove_item_at 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 0ee89c86ab refactor(event cache): remove methods that are plain private wrappers
These two methods could be only made private now, since they're only
used in `finish_back_pagination` (and in tests). But they only call
inside other methods of the underlying linked chunk without any extra
value, so they can be inlined instead. This reduces the public API and
removes tests for other APIs that were tested some place else.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 216f0df945 refactor(linked chunk): invert the position of insert_items_at parameters
Same logic as `replace_gap_at`.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 129e9e173e refactor(event cache): move finishing a network paginations in a EventLinkedChunk method 2025-07-02 12:43:42 +02:00
Benjamin Bouvier f7df0ebf97 refactor(event cache): change order of parameters in replace_gap
"Replace *this* gap with *these* events" read more natural to me than
"replace with *these* events *this* gap".
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 2f78701374 refactor(event cache): move order of parameters in filter_duplicate_events 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 872bded711 refactor(event cache): rename room_event names to linked_chunk or room_linked_chunk where it makes sense 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 5196298e5f refactor(event cache): rename RoomEvents to EventLinkedChunk 2025-07-02 12:43:42 +02:00
Johannes Marbach 308526a6bc feat(ffi): expose timestamp and identifier on EmbeddedEventDetails
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-02 12:38:53 +02:00
Johannes Marbach 12f94a3fd2 feat(ui): expose timestamp and identifier on EmbeddedEvent
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-02 12:38:53 +02:00
Benjamin Bouvier 3c873262c7 fix(event cache): disable order assertions entirely
Until we figure out some panics in production this has caused.
2025-07-02 12:18:29 +02:00
Benjamin Bouvier 9689c4a40a feat(ffi): add a new log pack for the notification client 2025-07-02 10:52:55 +02:00
Benjamin Bouvier 57e7ae488e chore(ui): tweak the notification client logs 2025-07-02 10:52:55 +02:00
Jade Ellis a9ffe5fd72 chore: Update rusqlite 2025-07-01 21:25:15 +02:00
Damir Jelić 59c29801e5 refactor: Remove some useless cfg guards
The whole module is behind a e2e-encryption feature guard, so those
individual ones in the module are not useful.
2025-07-01 14:32:53 +02:00
Ivan Enderlin 0a822c1a06 fix(sdk): EventCache::for_room returns an error when room isn't found.
This patch fixes `EventCache::for_room` to return an error when the room
isn't found. Additionally, it also returns an error when the `Client`
(actually the `WeakClient`) is dropped.

These errors were “hidden” behind the room version logic because of
the fallback. The code has been rewritten in a way it makes it harder to
do it again.
2025-07-01 14:31:01 +02:00
Valere Fedronic 4b5e1c6676 feat(widget): Add support for the widget to send encrypted to-device messages
This patch adds support for widgets to send encrypted to-device messages as described in MSC3819.

MSC3819: https://github.com/matrix-org/matrix-spec-proposals/pull/3819
2025-07-01 14:13:31 +02:00
Benjamin Bouvier ecb9d4d2e8 refactor(timeline): remove fields from TimelineFocusKind that are used only to init the focus 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 9ade32fcd0 refactor(timeline): remove generic from a few function signatures by using plain bools 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 8b4a01ea54 refactor(timeline): pass TimelineSettings immediately when creating a Timeline 2025-07-01 14:06:57 +02:00
Benjamin Bouvier d5d5b9ee01 refactor(timeline): move the crypto tasks spawning to decryption_retry_task too
This will make it easier to move all the code around, when needs be.
2025-07-01 14:06:57 +02:00
Benjamin Bouvier a3dd594c9e refactor(timeline): move the crypto drop handles to their own data structure
And put it in the `decryption_retry_task`, so it can be moved
altogether, later, to the event cache or some place else.
2025-07-01 14:06:57 +02:00
Benjamin Bouvier 98c331466e doc(timeline): beef up comments for the long-lived tasks 2025-07-01 14:06:57 +02:00
Benjamin Bouvier e8877fd987 refactor(timeline): move long-lived tasks to a new tasks mod
Only code motion.
2025-07-01 14:06:57 +02:00
Benjamin Bouvier babf16f15a refactor(timeline): use the num_events parameter for a thread back-pagination
And not the initial `num_events` parameter used for the initial
pagination.
2025-07-01 14:06:57 +02:00
Benjamin Bouvier 000419cdf3 refactor(timeline): rename TimelineFocusData to TimelineFocusKind 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 89d661ca8c refactor(timeline): get rid of TimelineFocusKind \o/ 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 1624d798ee refactor(timeline): replace remaining uses of TimelineFocusKind with TimelineFocusData 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 726218000a refactor(timeline): add TimelineFocusKind's fields to TimelineFocusData
This will make it possible to get rid of `TimelineFocusKind`!
2025-07-01 14:06:57 +02:00
Benjamin Bouvier 08dcb267b3 refactor(timeline): use the TimelineFocusData when processing relations 2025-07-01 14:06:57 +02:00
Benjamin Bouvier da0a32b088 refactor(timeline): use the TimelineFocusData to update the skip count 2025-07-01 14:06:57 +02:00
Benjamin Bouvier e22a833057 refactor(timeline): add a TimelineFocusData to TimelineState and TimelineStateTransaction 2025-07-01 14:06:57 +02:00
Benjamin Bouvier 8aba664578 refactor(timeline): get rid of internal locking for TimelineFocusData
Turns out it's not needed, because all the internal data structures
already use inner mutability patterns.
2025-07-01 14:06:57 +02:00
Benjamin Bouvier e117a3d22f fix(sdk): use a cfg guard instead of if cfg! to avoid build failures on !test && !debug_assertions builds 2025-07-01 12:11:09 +02:00
Andy Balaam c2f50fd8a5 doc(crypto): Attempt to explain what handle_to_device_event does and make all types explicit 2025-07-01 11:25:03 +02:00
Ivan Enderlin dc90c77c7d feat(sdk): Introduce the LatestEvents API.
This patch is the first part of the new `LatestEvents` API. It contains
the “framework”, i.e. the structure, tasks, logic to make it work, but
no latest events are computed for the moment.

The Latest Events API provides a lazy, reactive and efficient way to
compute the latest event for a room or a thread.

The latest event represents the last displayable and relevant event
a room or a thread has been received. It is usually displayed in a
_summary_, e.g. below the room title in a room list.

The entry point is `LatestEvents`. It is preferable to get a reference
to it from `Client::latest_events`, which already plugs everything to
build it. `LatestEvents` is using the `EventCache` and the `SendQueue`
to respectively get known remote events (i.e. synced from the server),
or local events (i.e. ones being sent).

\## Laziness

`LatestEvents` is lazy, it means that, despites `LatestEvents`
is listening to all `EventCache` or `SendQueue` updates, it will
only do something if one is expected to get the latest event for a
particular room or a particular thread. Concretely, it means that until
`LatestEvents::listen_to_room` is called for a particular room, no
latest event will ever be computed for that room (and similarly with
`LatestEvents::listen_to_thread`).

If one is no longer interested to get the latest event for a
particular room or thread, the `LatestEvents::forget_room` and
`LatestEvents::forget_thread` methods must be used.

\## Reactive

`LatestEvents` is designed to be reactive. Use
`LatestEvents::listen_and_subscribe_to_room` (same for thread) to get
a `Subscriber`.
2025-07-01 10:52:32 +02:00
Ivan Enderlin 6c9038eb4f refactor(sdk,common): Move JoinHandleExt inside matrix-sdk-common.
This patch moves the `JoinHandleExt` trait and the
`AbortOnDrop` type from `matrix_sdk::sliding_sync::utils` into
`matrix_sdk_common::executor`. This is going to be useful for other
modules.
2025-07-01 10:52:32 +02:00
dependabot[bot] 2b9b4cc589 chore(deps): bump crate-ci/typos from 1.33.1 to 1.34.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.33.1 to 1.34.0.
- [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.33.1...v1.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 08:32:45 +03:00
Ivan Enderlin dd0336ee72 test(ui): Update test_sync_indicator and make it non-flaky.
This patch updates `test_sync_indicator` since the `SyncIndicator`
is shown for the `SettingUp` state. Also, this patch makes this
test non-flaky entirely by changing the `sync::mpsc::channel` to a
`sync::Barrier`.
2025-06-30 16:55:17 +02:00
Ivan Enderlin 0095912091 doc(ui): Update the documentation to be in-sync with the code. 2025-06-30 16:55:17 +02:00
Ivan Enderlin e6774a34da feat(ui): Show the sync indicator when RoomListService is in SettingUp state.
Since `RoomListService` uses a persistent `pos` for sliding sync, the
`SyncIndicator` no longer shows its face except if the sliding sync
session doesn't exist or has expired.

This patch changes that by extending the `Show` from `Init` to
`SettingUp`.
2025-06-30 16:55:17 +02:00
Kévin Commaille 8ad52e34ea refactor: Don't use AnyMessageLikeEventContent with the event factory
When we upgrade Ruma, the `EventContent` bound on `EventBuilder` will be
changed to `StaticEventContent`, which is not implemented by the
`Any*EventContent` enums.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-30 16:44:14 +02:00
Ivan Enderlin 60a7bf0c3f doc(sdk): Fix a typo in the documentation of EventCacheDropHandles.
This patch fix a typo in the documentation of `EventCacheDropHandles`:
`EventCache` starts the tasks, not `RoomEventCache`.
2025-06-30 16:41:40 +02:00
Benjamin Bouvier 8b31d8f6a3 test(event cache): add an integration test for the ordering of relations 2025-06-30 16:10:49 +02:00
Benjamin Bouvier e21dd763e8 feat(event cache): return related events sorted by their position in the linked chunk 2025-06-30 16:10:49 +02:00
Benjamin Bouvier 31df84f5a1 feat(event cache): return the event's positions in find_event_relations 2025-06-30 16:10:49 +02:00
Benjamin Bouvier e68bdf8460 refactor(linked chunk): shortcut if there's no events at the top of MemoryStore::filter_duplicate_events
And remove useless comments.
2025-06-30 16:10:49 +02:00
Benjamin Bouvier 6ca1f16f48 refactor(linked chunk): make events iteration order deterministic in the relational linked chunk 2025-06-30 16:10:49 +02:00
Benjamin Bouvier 8c5d878172 refactor(linked chunk): simplify API of RelationalLinkedChunk::items()
One must now specify the target linked chunk; both callers would filter
out items based on this after the fact, which is a bit overkill since
most items would thus be filtered out on the sinking end.
2025-06-30 16:10:49 +02:00
Benjamin Bouvier d6239d614a refactor(test): reflect that unordered events can come, well, unordered 2025-06-30 16:10:49 +02:00
Benjamin Bouvier 5af084c8c9 refactor(test): don't make test_filter_duplicated_event rely on the position in the duplicate set 2025-06-30 16:10:49 +02:00
Benjamin Bouvier dc450ac25a refactor(linked chunk): rejigger the relational linked chunk to include the position of items 2025-06-30 16:10:49 +02:00
Benjamin Bouvier cf375dd753 refactor(linked chunk): don't try to find events or related events in any linked chunk, only the room one 2025-06-30 16:10:49 +02:00
Benjamin Bouvier ff935df136 refactor(linked chunk): don't nest internal methods 2025-06-30 16:10:49 +02:00
Jonas Richard Richter 0d080935cf chore(changelog): add pull request reference for NotificationItem room topic addition 2025-06-30 11:26:17 +01:00
Jonas Richard Richter 4d140d8155 feat(notification): add room topic to NotificationItem and NotificationRoomInfo structs 2025-06-30 11:26:17 +01:00
Benjamin Bouvier cef1f8c5cb chore(event cache): address review comments 2025-06-30 11:09:11 +02:00
Benjamin Bouvier 6a054d6c74 refactor(event cache): use the linked chunk metadata to construct the OrderTracker 2025-06-30 11:09:11 +02:00
Benjamin Bouvier 1f89efb88d feat(event cache store): add a method to return the chunks' metadata 2025-06-30 11:09:11 +02:00
Benjamin Bouvier e83c09e425 refactor(event cache): regroup lazy-loading methods in the same impl block 2025-06-30 11:09:11 +02:00
Benjamin Bouvier a85dac1f52 feat(event cache): introduce an OrderTracker for each room tracked by the event cache
The one hardship is that lazy-loading updates must NOT affect the order
tracker, otherwise its internal state will be incorrect (disynchronized
from the store) and thus return incorrect values upon shrink/lazy-load.

In this specific case, some updates must be ignored, the same way we do
it for the store using `let _ = store_updates().take()` in a few places.

The author considered that a right place where to flush the pending
updates was at the same time we flushed the updates-as-vector-diffs,
since they would be observable at the same time.
2025-06-30 11:09:11 +02:00
Benjamin Bouvier a0bc9aafcf feat(linked chunk): introduce an OrderTracker to keep track of the ordering of the current items
This is a new data structure that will help figuring out a local,
absolute ordering for events in the current linked chunk. It's designed
to work even if the linked chunk is being lazily loaded, and it provides
a few high-level primitives that make it possible to work nicely with
the event cache.
2025-06-30 11:09:11 +02:00
Benjamin Bouvier 8217f967d4 doc(linked chunk): explain briefly what the reattaching/detaching flags are doing 2025-06-30 11:09:11 +02:00
Benjamin Bouvier 47c9585606 refactor(linked chunk): generalize UpdateToVectorDiff to handle different accumulators
In the next patch, we're going to introduce another user of
`UpdatesToVectorDiff` which doesn't require accumulating the
`VectorDiff` updates; so as to make it optional, let's generalize the
algorithm with a trait, that carries the same semantics.

No changes in functionality.
2025-06-30 11:09:11 +02:00
Benjamin Bouvier 20e09531fb refactor(sdk): move the common pagination types to the paginators root mod 2025-06-30 10:48:27 +02:00
Benjamin Bouvier d92b33f959 refactor(sdk): move the ThreadedEventsLoader into the sdk's paginators module
A bit of code motion, sprinkling a new interface here, adjusting
expectations in the timeline, and we're all set 👌
2025-06-30 10:48:27 +02:00
Benjamin Bouvier a74bcfab8f refactor(sdk): move the room Paginator outside of the event cache
It doesn't belong there anymore, because it's not used within the event
cache itself. It's used in the timeline, and I think it's nice to keep
it available for external users as well, so it now lives in its own
`matrix-sdk/src/paginators` module. Next step is to move the thread
loader in there as well.
2025-06-30 10:48:27 +02:00
Benjamin Bouvier 6dcc744b48 refactor(event cache): remove unused impl of PaginableRoom for WeakRoom 2025-06-30 10:48:27 +02:00
Benjamin Bouvier 9d1c296657 doc(ffi): fix incorrect reference to a function that's been renamed 2025-06-30 10:48:27 +02:00
Damir Jelić 3c7683ea53 chore(sdk): Add a missing dot 2025-06-30 09:53:47 +02:00
Jonas Platte cd03a58083 refactor(examples): Use if-let chains in oauth_cli 2025-06-29 20:58:05 +02:00
Jonas Platte 4a1249fa96 chore(examples): Upgrade to Rust edition 2024 2025-06-29 20:58:05 +02:00
Kévin Commaille 06732ca71a refactor(common): Use a constant for the room version to use as a fallback
It avoids using different versions in several places for consistency. It
also allows to be able to change it in a single place when needed.

This also bumps the fallback to v11 everywhere, since it is the default
version for new rooms since Matrix 1.14 and it has the sanest redaction
rules.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-29 16:59:37 +02:00
Jonas Platte 115c7578d4 refactor(multiverse): Rewrite condition for readability 2025-06-29 09:42:27 +02:00
Jonas Platte 9f3e7debb1 refactor(multiverse): Use let chains 2025-06-29 09:42:27 +02:00
Jonas Platte 58d2ae4c39 chore(multiverse): Upgrade to Rust edition 2024 2025-06-29 09:42:27 +02:00
Kévin Commaille 8a847a99d4 ci: Bump the version of Rust nightly
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-28 09:47:28 +02:00
Jonas Platte 3d642356c6 refactor: Clean up formatting in many places
Process:
- set style_edition to 2024 in .rustfmt.toml
- run `cargo fmt`
- undo .rustfmt.toml change
- run `cargo fmt` again
- manually rewrap some strings
2025-06-27 19:54:13 +02:00
Kévin Commaille b4b0f3a203 refactor(sdk): Remove fallback support for the /auth_issuer endpoint
The `/auth_metadata` endpoint has been supported by Synapse for 6 months
now so there shouldn't be any homeserver that still depend exclusively
on it. This endpoint is also part of Matrix 1.15.

Support for this endpoint has been removed from Ruma so this is
necessary before an upgrade of the dependency.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-27 17:24:30 +00:00
Kévin Commaille ca99977207 ui: Inline format! args
Detected by lint clippy::uninlined_format_args.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-27 19:04:00 +02:00
Kévin Commaille dd02274883 base: Avoid clone in test
Detected by lint clippy::cloned_ref_to_slice_refs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-27 19:04:00 +02:00
Kévin Commaille ad2e3a3b8f common: Put UpdatesSubscriber behind test cfg
Since it is only used in tests, it is now detected as dead code.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-27 19:04:00 +02:00
Kévin Commaille 96119f9a30 crypto: Use Ord implementation of SequenceNumber for its PartialOrd implementation
Detected by lint clippy::non_canonical_partial_ord_impl.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-27 19:04:00 +02:00
Damir Jelić 737e06b581 Add a missing changelog entry for PR 5177 2025-06-27 14:54:21 +02:00
Richard van der Hoff 4ecd599c15 fix(sdk): correctly import e2ee history in join_room_by_id (#5284)
It turns out that downstream clients can and do call
`Client::join_room_by_id()` rather than `Room::join`, so we need to do
the room key history import in the lower-level method.

---------

Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-06-27 12:29:01 +01:00
Doug c6521a8aaf ffi: Remove the ElementWellKnown struct and related function. 2025-06-27 12:00:33 +02:00
Benjamin Bouvier 4046a59786 refactor(event cache): make filter_duplicate_events operate on a linked chunk id 2025-06-26 16:23:39 +02:00
Benjamin Bouvier d931cd0ea7 refactor(event cache): include non_empty_all_duplicates in the DeduplicationOutcome 2025-06-26 16:23:39 +02:00
Benjamin Bouvier a14488617e refactor(common): don't parse the full bundled latest thread event, but only its type
The previous code would parse the entire bundled event, only to look at
its type indirectly. We can do better than this, and only look at the
type instead. This brings a few benefits:

- it is faster, as we don't have to deserialize the entire event
- while the spec seems to indicate that the latest thread event has a
  `room_id`, it seems that, under some circumstances, it does not, as
  indicated by some rageshakes. As such, not parsing as `AnyMessageLike`
  (which mandates a room id) makes it more robust to the absence of a
  room id in there, marking more events as latest events.
2025-06-26 15:33:26 +02:00
Jorge Martín 1de51614f1 fix: Add m.room.avatar to the required state
This works around the issue with removed avatars not being explicitly flagged by sliding sync until https://github.com/element-hq/synapse/issues/18598 is fixed
2025-06-26 14:21:00 +02:00
Robin 6cc98ee9f7 feat(widget): Allow Element Call to learn the room name
The latest mobile designs for Element Call have it displaying the room name in an "app bar". So the Element Call widget will soon start requesting the capability to learn the room name, and the SDK should auto-approve this capability.
2025-06-26 13:28:34 +02:00
Damir Jelić 3a98d46bfa feat: Add a stream to listen for historic room key bundles 2025-06-26 13:22:24 +02:00
Damir Jelić 1558858bde chore: Add a TODO item reminding us that we should zeroize room key bundle contents 2025-06-26 13:22:24 +02:00
Damir Jelić e4d2f62d48 docs: Document the store/types module properly 2025-06-26 12:11:16 +02:00
Benjamin Bouvier 70f48be582 refactor(sliding sync): avoid an unwrap by inlining a function into its one caller 2025-06-26 12:08:43 +02:00
Ivan Enderlin 836c643769 doc(indexeddb): Add documentation about IndexedKeyBounds and IndexedKeyComponentBounds. 2025-06-25 16:17:34 +02:00
Michael Goldenberg a48099d5ac refactor(indexeddb): add function to transaction type for clearing all data from an object store in IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 09d8be7b4c doc(indexeddb): add docs to transaction type where they are missing
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 03b8cabc22 refactor(indexeddb): add functions to transaction type for deleting data from IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 07372c475c refactor(indexeddb): add functions to transaction type for adding data to IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg a00e4089e8 refactor(indexeddb): add functions to transaction type for getting data out of IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 1a24b21d42 refactor(indexeddb): add type to represent IndexedDB transactions specific to event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg f51496fa0f refactor(indexeddb): track corresponding index for type in IndexedKey trait
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg c6ce9c560b refactor(indexeddb): track corresponding object store for type in Indexed trait
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 60af16ada8 refactor(indexeddb): add function for encoding key range from key component range
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 159fb73b0a refactor(indexeddb): join key range encoding functions using indexed key range enum
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 0fa0f2329d refactor(indexeddb): add enum for representing ranges over indexed keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 3b64d18c99 refactor(indexeddb): move key component bounds into a separate trait
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg f67fd87e57 refactor(indexeddb): rename IndexedKeyBounds fn's so they are consistent
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg 5e64da660c refactor(indexeddb): add default impls for IndexedKeyBounds::{encode_lower, encode_upper}
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg d152ce13a0 refactor(indexddb): add fn's to IndexedKeyBounds to get bounds of key components
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Michael Goldenberg d021020ee6 refactor(indexeddb): use generic type rather than dynamic type in serializer error
This prevents us from having to add type constraints on the dynamic type
and instead only having to specify the type constraint at the call site.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-25 16:17:34 +02:00
Timo 7e5f22ba9e feat(widgets) Add backwards compatibility hide_header for element-call url query parameter 2025-06-25 16:12:28 +03:00
Timo 6bc6ea4e72 feat(widgets): Update the widget url parameters to use the new header property (was hideHeader which is now deprecated) 2025-06-25 16:12:28 +03:00
Timo 585ae29868 refactor(uniffi): make the sdk widget config struct derive uniffi.
This allows to not duplicate the struct in the uniffi crate.
2025-06-25 16:12:28 +03:00
Benjamin Bouvier 3919c2a89a feat(ffi): also disable the send queues when clearing caches
And beef up the doc comment.
2025-06-25 14:32:25 +02:00
Benjamin Bouvier 7c85e7aa4f feat(room list service): allow to manually expire sessions
This can be used to invalidate the persisted state on disk related to
the sliding sync positions. It's useful to do so when clearing up all
the caches, since the sliding sync `pos`itions are stored in the crypto
store (to benefit from the cross-process lock).
2025-06-25 14:32:25 +02:00
Benjamin Bouvier 4b845e17c8 feat(sliding sync): also empty the list caches when expiring a session 2025-06-25 14:32:25 +02:00
Benjamin Bouvier 394124cda5 refactor(sliding sync): rename invalidate_cached_list to remove_cached_list 2025-06-25 14:32:25 +02:00
Benjamin Bouvier bbf9bf2c0b feat(room list service): skip the initial state when an initial pos is set 2025-06-25 14:32:25 +02:00
Benjamin Bouvier 67327a0365 doc(sliding sync): remove dubious doc comment
It speaks of a time where the `storage_key` was optional, and it's also
a private field, so it's doubly confusing.
2025-06-25 14:32:25 +02:00
Benjamin Bouvier 5e40426b99 refactor(sliding sync): don't eagerly reload sliding sync state if it's unused 2025-06-25 14:32:25 +02:00
Ivan Enderlin 0f264cac6e feat(ui): RoomListService re-enables share_pos. 2025-06-25 14:32:25 +02:00
Andy Balaam 3c1d0b37e5 refactor(crypto): Provide a specific error type for to-device events from dehydrated devices
This will get more usage soon, when we add a variant for events with
unverified senders.
2025-06-25 12:05:14 +01:00
Stefan Ceriu 62231878cc fix(ffi): make RoomInfo power levels optional as they can be missing depending on the required state
The `m.room.power_levels` state event is not part of the room list required state and computing the RoomInfo would fail in that case.
2025-06-25 12:41:33 +03:00
Benjamin Bouvier 22c99f30f3 chore(sdk): fill in the pull request numbers in changelogs
Signed-off-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-06-25 10:32:01 +02:00
Benjamin Bouvier a7efff9849 chore: add changelog entries 😅 2025-06-25 10:32:01 +02:00
Benjamin Bouvier bc9192f818 refactor!(sdk): make the join_rule and related getters optional
The join rule state event can be missing from a room state. In this
case, it's an API footgun to return a default value; instead, we should
return none and let the caller decide what to do with missing
information.
2025-06-25 10:32:01 +02:00
Richard van der Hoff 0722ed9d8f Indexeddb: support for received room key bundles
Add a new store to keep track of the information we have received about room
key bundles.
2025-06-25 10:26:11 +02:00
Valere Fedronic 1aa933cfd6 widgets: Filter out crypto related fields of raw decrypted to-device
Ensure that only sender/type/content are exposed to the widgets, and not the crypto-related fields.
2025-06-24 16:04:52 +02:00
Ivan Enderlin e0ab16f979 chore(sdk): Address feedback. 2025-06-24 15:14:57 +02:00
Ivan Enderlin a9c999af72 doc(sdk): Precise when the side-effect of RoomEventCacheSubscriber happens. 2025-06-24 15:14:57 +02:00
Ivan Enderlin 8156413132 feat(sdk): Introduce RoomEventCacheGenericUpdate, one channel to get update of all rooms.
`RoomEventCache::subscribe` is nice to subscribe to every update
happening inside a room in the event cache. However, the returned
`RoomEventCacheSubscriber` has side-effects when dropped (see
auto-shrink to save memory space). In some situation, this is pretty
annoying. for example, if one wants to listen to multiple room updates,
all the room event cache subscribers must be kept in memory, thus
breaking the side-effects. This isn't always the desired output. In
addition, listening to multiple channels/subscribers at the same is
quite complex, as it implies non-trivial async runtime efforts or
complex future types.

To solve this problem, this patch introduces a new
`EventCache::subscribe_to_room_generic_updates` method, which returns a
single `Receiver<RoomEventCacheGenericUpdate>`.

First off, it hides the details of `RoomEventCacheUpdate` (returned by
`RoomEventCacheSubscriber`), which might be desired, but particularly
lighter because events aren't part of the payload.

Second, one no longer needs to subscribe to all rooms. Only one channel
can be listened to get updates for all rooms. It reduces the complexity
on the caller side, plus `Receiver<RoomEventCacheGenericUpdate>` doesn't
have any side-effect.

This patch tests this feature in 4 situations:

1. when a room is created/loaded empty,
2. when a room is loaded and is not empty because data exists in the storage,
3. when a room receives data from the sync,
4. when a room receives data from the pagination.
2025-06-24 15:14:57 +02:00
Ivan Enderlin e551efec8d doc(sdk): Fix comment of RoomEventCacheState::handle_sync.
This patch fixes a comment about `RoomEventCacheState::handle_sync`
returned values.
2025-06-24 15:14:57 +02:00
Benjamin Bouvier 877a7d678f fix(notification client): request the join rules so as to be able to compute them 2025-06-24 14:34:54 +02:00
Daniel Salinas 457af2a2f8 feat(wasm): Remove network config features from ffi ClientBuilder for Wasm (#5248)
<!-- description of the changes in this PR -->
Features to configure UserAgent, Proxy, Disabling SSL, and additional
certificates are not available on Wasm platforms. We remove these
configuration options from the FFI layer, while preserving them on
non-Wasm platforms.

- [ ] Public API changes documented in changelogs (optional)

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by: Daniel Salinas

---------

Co-authored-by: Daniel Salinas <danielsalinas@Daniels-MacBook-Pro-2.local>
2025-06-24 14:22:43 +02:00
Andy Balaam 7b38c442c7 refactor(crypto): Hold a DecryptionSettings in base Client
Previously, we held a TrustRequirement inside Client, and had to wrap it
in a DecryptionSettings manually a couple of times.

It feels more right to hold on to a DecryptionSettings directly. If
there were ever some additional settings, most likely we would need them
all here.

If I haven't screwed it up, this should not affect behaviour in any way.
2025-06-24 12:36:31 +01:00
Benjamin Bouvier 201b818cc8 fix(timeline): take into account the skip count when computing the pagination status
The pagination status was only mapped onto the "global" state, that is, the state of the event cache pagination.

Now, consider the following case, where two timeline instances are live:

- the first one could have backpaginated all the items back to the start of the room
- the second one is created later. Because of the initial value for the skip count, it will only return a subset of event items (~20).

However, listening to the pagination status for the second timeline would incorrectly state that the timeline was entirely paginated (because it returned the "global" pagination status). As such, an observer might think that there are no more items in the timeline, while a subsequent pagination would adjust the skip count and return more items.

This fixes it by combining the global pagination state with the local timeline state (aka the skip count value). If the skip count is positive (meaning, we could set it to 0 later and thus returning more events), we pretend we haven't reached the start of the timeline. This way, an observer can call pagination later, which may adjust the skip count and "return" more items.
2025-06-24 09:41:04 +00:00
Daniel Salinas 1f98e0cd19 Run cargo fmt 2025-06-23 18:51:51 +03:00
Daniel Salinas 21c59c95c4 Avoid using of tokio::time:sleep which does not work on Wasm
We already have a wrapped sleep in matrix-sdk-base, so use that.
2025-06-23 18:51:51 +03:00
Andy Balaam 4025c11e73 refactor(crypto): Simplify the code that ignores to-device messages from dehydrated devices 2025-06-23 16:24:41 +01:00
Stefan Ceriu dc6130562a feat(ffi): add simpler methods for checking own user permissions
This is so that the final client no longer needs to go through the string to user_id conversion and error handling step before retrieving permissions.
2025-06-23 18:04:59 +03:00
Stefan Ceriu d1a14f895e chore(ffi): move user_power_levels outside the RoomInfo and into the newly introduced RoomPowerLevels.
`RoomPowerLevels` already holds an inner version of the power levels and has access to everything needed to compute the user to level map.
This way the async fetching only happens once and the mapping only on request.
2025-06-23 18:04:59 +03:00
Stefan Ceriu b680705d15 feat(ffi): expose the RoomPowerLevels on the RoomInfo directly.
Currently, clients have to make async requests to `Room::get_power_levels` from multiple places throughout the app in order to correctly configure the various UI components and again, on randomly decided events, to keep them up to date.
This patch starts publishing the power levels directly on the `RoomInfo` and allows them to be handled (and updated!) through the normal `subscribe_to_room_info_updates` mechanism.
2025-06-23 18:04:59 +03:00
Stefan Ceriu 2b1ee853fc chore(ffi): move RoomInfo to the room module 2025-06-23 18:04:59 +03:00
Ivan Enderlin ef137730cb doc(sdk): Add #5269 to the CHANGELOG.md. 2025-06-23 15:45:28 +02:00
Ivan Enderlin 45caaffb26 refactor(sdk): Rename RoomEventCacheListener to RoomEventCacheSubscriber.
This patch removes a name ambiguity around _listener_ vs. _subscriber_.
Both terms are used to talk about `RoomEventCacheListener`. We usually
use the term _subscriber_ for the type being returned by a `subscribe`
method. The code refers to this sometimes as listener, sometimes
as subscriber, sometimes both in the same sentence, which can be very
confusing! This patch solves this by using the _subscriber_ term only.
2025-06-23 15:45:28 +02:00
Ivan Enderlin 50c3217353 doc(sdk): Document the side-effect of RoomEventCacheListener. 2025-06-23 15:45:28 +02:00
Ivan Enderlin b5dafd9798 chore(sdk): Rename variables.
This patch renames `tx` and `rx` to `auto_shrink_sender` and
`auto_shrink_receiver` to clarify the code.
2025-06-23 15:45:28 +02:00
Ivan Enderlin 5a39fd051b chore(sdk): Rename a variable.
This patch renames a variable to match the `EventCacheDropHandles`
field's name.
2025-06-23 15:45:28 +02:00
Andy Balaam ff52cf36dd refactor(crypto): Rename raw_event to processed_event to reflect its changed state 2025-06-23 11:34:59 +02:00
Daniel Salinas 25d217cc6f Address timeline panic to avoid resume_unwind on Wasm 2025-06-23 10:06:04 +02:00
Jonas Platte 2116ad82df refactor: Move comments with visual indentation
Visual indentation was removed from rustfmt's style defaults in the 2024
edition of the style rules.
2025-06-23 09:37:45 +02:00
Jonas Platte fe4109cb9a refactor: Remove {self} imports
They are only semantically different if a macro of the same name exists,
in which case the foo::{self} import only imports foo-the-module, not
foo-the-macro.
2025-06-23 09:37:45 +02:00
Jonas Platte 41f107e5ba refactor: Remove useless comment
This comment was added in the very first commit for matrix-sdk-ffi and I
have no recollection of what was meant there.
2025-06-23 09:37:45 +02:00
Daniel Salinas ab699a90f1 Adjust platform init on Wasm to avoid tokio environment
The multi-threaded tokio environment does not work in Wasm.
console_error_panic_hook turns rust panics into JS console statements
2025-06-23 09:47:08 +03:00
Daniel Salinas ddee7f8ccd feat(wasm): Small fixes for imports in ffi timeline (#5263)
Adjust some imports to use our shims to support Wasm.

Signed-off-by: Daniel Salinas
2025-06-20 22:22:15 +02:00
Daniel Salinas 2ab5ab527b refactor(wasm): Remove special Send/Sync behavior for Wasm on a crypto-store error (#5265)
Not necessary it turns out

Signed-off-by: Daniel Salinas
2025-06-20 22:18:32 +02:00
Daniel Salinas 53e3b90436 feat(wasm): Mark CapabilitiesProvider::acquire_capabilities as SendOutsideWasm (#5262)
Correct an accidental recent addition of a `Send` trait instead of
`SendOutsideWasm`

Signed-off-by: Daniel Salinas
2025-06-20 21:58:11 +02:00
Daniel Salinas 08e1d3876b Reorganize QRCode related functionality into its own file
We have a working implementation of the additional forms of QR Code login
of MSC4108. This commit moves the existing code into its own file
to make future updates easier to follow.
2025-06-20 14:24:28 +01:00
Richard van der Hoff c3179ea5ed Merge pull request #5260 from matrix-org/rav/join_room_logging
sdk: add logging to `Room::join()`
2025-06-20 12:12:05 +01:00
Daniel Salinas 9676daee5a feat(wasm): Remove MediaFileHandle from ffi on Wasm platforms (#5249)
Remove the MediaFileHandle concept from the matrix-sdk-ffi crate on Wasm
platforms. File handles are not supported in the browser.

Signed-off-by: Daniel Salinas
2025-06-19 18:55:23 +02:00
Daniel Salinas 798cece4a2 feat(wasm): Add lib to matrix-sdk-ffi target (#5242)
The uniffi tool for generating JS/Wasm bindings utilizes rust as its
intermediate language.

As a result, the 'target' uniffi annotated library needs to be marked as
a 'lib' so that the generated rust code can utilize it to generate the
Wasm create + typescript bindings.

Signed-off-by: Daniel Salinas
2025-06-19 18:16:16 +02:00
Doug 06b387101b chore: Fix changelogs after rebase 2025-06-19 17:40:04 +02:00
Doug 675963ec4b chore: Make the RTC foci crash fix type-safe. 2025-06-19 17:40:04 +02:00
Doug d30dae3322 fix: Handle a crash accessing the RTC foci when the well-known was None. 2025-06-19 17:40:04 +02:00
Doug bdb640a126 ffi: Expose a check for LiveKit RTC support. 2025-06-19 17:40:04 +02:00
Doug ea28234d95 sdk: Cache the client well-known file and add Client::rtc_foci which uses it. 2025-06-19 17:40:04 +02:00
Doug c74295c604 chore: Refactor ServerCapabilities into ServerInfo.
It has nothing to do with /capabilities so is confusing. We can use this new struct to combine the well-known response into a single cache too.
2025-06-19 17:40:04 +02:00
Michael Goldenberg ec30e7b85c docs(indexeddb): add license to event_cache_store::types file
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg fd17c28ebb refactor(indexeddb): add convenience functions for (de)serializing Indexed types
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg 841131f127 refactor(indexeddb): add indexing trait impls for gap
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg a22d592bf1 refactor(indexeddb): add chunk identifier into gap type
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg 1a32aa59a6 refactor(indexeddb): add indexing trait impls for event
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg a99df7e1d8 refactor(indexeddb): add indexing trait impls for chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg 3e37f9d0ad refactor(indexeddb): expose current version and keys used in event cache store database 2025-06-19 17:26:30 +02:00
Michael Goldenberg 2689e2d25a refactor(indexeddb): add trait for constructing key bounds for indexed types in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg 2cfba4cd9b refactor(indexeddb): add trait for encoding keys for indexed types in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Michael Goldenberg 1bce2af93c refactor(indexeddb): add trait for converting between high-level types and indexed types in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-19 17:26:30 +02:00
Stefan Ceriu 47c8df0ef8 chore(ffi): move the room power levels to their own file and restructure the code 2025-06-19 17:55:48 +03:00
Stefan Ceriu 9f32dfe9a0 change(ffi): expose the full RoomPowerLevels object and move corresponding methods to it.
This patch expands on the already existent `RoomPowerLevels` record (which it renames to `RoomPowerLevelsValues` to work around uniffi not exposing public fields) and nests them inside a new exported object that also provides methods for retrieving the actions that an user can take, methods moved from the room object.
This reduces the amount of async calls the clients need to make, simplifies the API and groups the code better together.
2025-06-19 17:55:48 +03:00
Daniel Salinas 040fd6c736 Run cargo fmt 2025-06-19 16:16:59 +02:00
Daniel Salinas 4dac175db0 Back to other phrasing to make CI run 2025-06-19 16:16:59 +02:00
Daniel Salinas 5faf97cf99 Rework phrasing to make CI run 2025-06-19 16:16:59 +02:00
Daniel Salinas 7236b80b3b Adjust language to make CI do something 2025-06-19 16:16:59 +02:00
Daniel Salinas 79b0941687 Remove lib 2025-06-19 16:16:59 +02:00
Daniel Salinas ad001e475f Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas 5106d55be9 Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas 9771b99395 Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas 2c287e706f Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas fffff783d4 Spell check 2025-06-19 16:16:59 +02:00
Daniel Salinas b047bd0dc6 Add refactor flag 2025-06-19 16:16:59 +02:00
Daniel Salinas 28b3b6aedf Update bindings/matrix-sdk-ffi/CHANGELOG.md
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas 171974a44b Add note about bundled-sqlite 2025-06-19 16:16:59 +02:00
Daniel Salinas f53302a7a0 Code review feedback, improved documentation 2025-06-19 16:16:59 +02:00
Daniel Salinas dd709682d7 Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas 991e0cd395 Update bindings/matrix-sdk-ffi/Cargo.toml
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 16:16:59 +02:00
Daniel Salinas abcc05f889 Change matrix-sdk-ffi to rely on features over platform targets
The system of platform targets was already quite messy, and becoming
even worse as we start preparing for Wasm support. Switch to features
instead to make this easier to work with.
2025-06-19 16:16:59 +02:00
Daniel Salinas f3e636ea42 fix(wasm): Fix unwrap error on Wasm platforms caused by UInt::MAX conversions (#5240)
UInt::MAX.try_into().unwrap() was causing errors on Wasm platforms, due
to the result being unrepresentable.

This `unwrap_or` was also always being calculated regardless, so I think
using `usize::MAX` is preferable on all platforms.

Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
2025-06-19 13:56:50 +00:00
Daniel Salinas 1d47507faa refactor(wasm) Remove some unnecessary wasm32 differentiations (#5256)
Some of these Box<dyn Error + Send + Sync> are okay, but a few are
problematic.

Confirmed this still compiles fine on the fully working wasm tree.

Signed-off-by: Daniel Salinas
2025-06-19 13:28:04 +00:00
Damir Jelić cc7f6243c6 ci: Make the text output for our coverage xtask the default 2025-06-19 13:57:58 +02:00
Jonas Platte e7e9d5b746 refactor: Use native async fn in traits for widget::CapabilitiesProvider (#5135)
The main thing left now are the store traits, unsure how to deal with
those. `dynosaur` + `trait_variant` are kind of the modern replacement
for `async_trait`, but (a) `trait_variant` seemed to generate invalid
code here when I tried it and (b) even with that fixed I think the error
type erasure is going to present some extra problems. Maybe it's fine to
just keep the current solution for the store traits for now.

Signed-off-by: Jonas Platte <jplatte+matrix@posteo.de>
2025-06-19 11:47:36 +00:00
Andy Balaam b4146caac8 refactor(crypto): Extract a method for handling encrypted to-device events 2025-06-19 11:22:11 +01:00
Damir Jelić 1cb51f49be ci: Store successful test results in the JUnit file as well
This is useful to detect which tests might be the slowest.
2025-06-19 11:14:51 +02:00
Damir Jelić fea0e0d373 ci: Use the correct token to upload JUnit reports 2025-06-19 10:26:43 +02:00
Damir Jelić 72911c66ad ci: Upload the JUnit reports to codecov as well 2025-06-19 09:50:16 +02:00
Damir Jelić dc047854d4 ci: Use a separate cache prefix for the coverage workflow 2025-06-19 09:50:16 +02:00
Damir Jelić f51a008921 ci: Attempt to free up some space on the container for the coverage workflow 2025-06-19 09:50:16 +02:00
Damir Jelić 3c5bcce217 ci: Use llvm-cov for coverage reports
This patch switches from tarpaulin to llvm-cov for our coverage reports.
llvm-cov can use cargo-nextest to run the tests which means that we can
tolerate flaky tests for coverage just like do for the rest of our CI
run.

We can also start using JUnit reports to track flaky tests.
2025-06-19 09:50:16 +02:00
Damir Jelić 422fd19d10 ci: Add an xtask subcommand for coverage reports
This command uses llvm-cov which we are planning to switch to.
2025-06-19 09:50:16 +02:00
Damir Jelić 0ea07e11e9 ci: Add a CI specific profile for nextest 2025-06-19 09:50:16 +02:00
Damir Jelić 059a6fa573 test: Make some tests less flaky by increasing timeouts 2025-06-19 09:50:16 +02:00
Daniel Salinas 07656c2e26 Correct use to propagate error with additional text 2025-06-19 09:42:43 +02:00
Daniel Salinas 940325574b Address use of errors and panic::resume_unwind for wasm targets 2025-06-19 09:42:43 +02:00
Damir Jelić 9f8824b9a5 ci: Use a tag for the changed-files github action 2025-06-17 16:13:39 +02:00
Valere Fedronic cd141c5b84 feat(widget): Receive custom to-device messages in widgets in e2ee rooms
Proper support for receiving to-device messages for widgets.

If the widget is in an e2ee room, clear to-device traffic will be excluded. Also filter out internal to-device messages that widgets should not be aware off.
2025-06-17 16:00:44 +02:00
dependabot[bot] 9596aa0830 chore(deps): bump qmaru/wasm-pack-action from 0.5.0 to 0.5.1
Bumps [qmaru/wasm-pack-action](https://github.com/qmaru/wasm-pack-action) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/qmaru/wasm-pack-action/releases)
- [Commits](https://github.com/qmaru/wasm-pack-action/compare/v0.5.0...v0.5.1)

---
updated-dependencies:
- dependency-name: qmaru/wasm-pack-action
  dependency-version: 0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-17 13:56:43 +02:00
Benjamin Bouvier 11424ce443 chore(ffi): add the SDK commit hash to sentry as the release version 2025-06-16 16:44:48 +02:00
Benjamin Bouvier cd4ec90b38 chore(timeline): report day divider invariant violations to sentry
On this date (heh), the date divider reports include the following
information:

- initial timeline items in a shortened format,
- operations to apply,
- final timeline items in the same shortened format,
- errors

The shortened format includes:

- either, for events: "[event id]: server timestamp"
- or for date divider: "--- date divider timestamp"

As such, they don't include any personal information.

The initial timeline items state and set of operations to apply
constitutes a fully enclosed test case, so it's nice to report it to
Sentry, so we can reuse it almost as is (we'd only need to randomize the
event IDs) and fix it in a subsequent commit.
2025-06-16 16:44:48 +02:00
Benjamin Bouvier 4680354abd fix(timeline): remove development tracing log in the pinned events loader 2025-06-16 14:25:30 +02:00
Damir Jelić 145d6c5782 refactor(multiverse): Use a paragraph to render an individual read receipt 2025-06-16 12:54:40 +02:00
Damir Jelić a955af61e1 refactor(multiverse): Simplify the selected read receipt rendering
This patch simplifies the selected read receipt rendering by the fact
that we can simply fetch the selected timeline item instead of the event
ID and then do a search for the selected item.

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-06-16 12:54:40 +02:00
Damir Jelić 2a78b5b67a chore: Fix a clippy lint 2025-06-16 12:54:40 +02:00
Damir Jelić 9d29c36531 feat(multiverse): Only render read receipts for a single event if one is selected 2025-06-16 12:54:40 +02:00
Damir Jelić ed9c7d90b4 feat(multiverse): Allow event selection even if the details view is open 2025-06-16 12:54:40 +02:00
Damir Jelić 2af23d052c feat(multiverse): Show the selected event in the read receipts view 2025-06-16 12:54:40 +02:00
Damir Jelić fb80e06839 feat(multiverse): Show read receipts for individual messages 2025-06-16 12:54:40 +02:00
Daniel Salinas b8f9cba5e7 Wasm corrections for ffi's error file
Add a From to handle RequestVerificationError to avoid anyhow
Stop using SystemTime directly
2025-06-16 09:14:54 +02:00
Daniel Salinas d119b01322 Add AbortHandle as well 2025-06-16 09:09:28 +02:00
Daniel Salinas 85833c74ba Update JoinHandle as well 2025-06-16 09:09:28 +02:00
Daniel Salinas 5b20136a50 Stop using tokio::runtime::Handle directly
Use our platform aware export from matrix-sdk-common instead
2025-06-16 09:09:28 +02:00
Ivan Enderlin 362ca2bd59 doc(base): Remove ambiguities around inline comments. 2025-06-13 14:52:15 +02:00
Ivan Enderlin 0a9a849826 fix(base): Ignore invalid state events instead of throwing an error.
This patch changes the strategy of `check_room_upgrades`.
Instead of checking all state events in `StateChanges` and
throwing an error in case of an invalid state, this patch updates
`state_events::sync::dispatch` to filter out invalid state events
(specifically `m.room.create` and `m.room.tombstone`), and log the
error. That way, the sync doesn't stop and the app can continue working
smoothly.

So `check_room_upgrades` is splited into two functions:
`is_create_event_valid` and `is_tombstone_event_valid`. It's no longer
necessary to detect mergers or splitters because those are _soft
errors_, however loops are still critical errors (hence the fact
the state events aren't stored nor saved nor applied, and that a log
is emitted).

Note: `check_room_upgrades` has been reverted in a previous commit,
that's why it doesn't appear in this diff.
2025-06-13 14:52:15 +02:00
Valere Fedronic 7126fc8a29 feat(crypto): Emmit EncryptionInfo with event handlers for to-device messages as well 2025-06-13 14:31:22 +02:00
Stefan Ceriu f4e612ca9e feat: add thread support to the room message draft facilities
This patch adds optional thread root event id parameters to the drafting functions exposed on the room level
allowing unfinished messages to be managed for the main room as well as any inner thread.

Internally it uses the room id or a tuple of the room id and the thread as keys for the various backing stores.
2025-06-13 14:41:10 +03:00
Richard van der Hoff 6ab11a0323 Merge pull request #5219 from matrix-org/rav/megolm_sender_verification_main
crypto: new `VerificationLevel::MismatchedSender`
2025-06-12 12:44:47 +01:00
Stefan Ceriu 76626db613 chore(ffi): expose ThreadSummary num_replies on the ffi layer. 2025-06-12 14:26:40 +03:00
Benjamin Bouvier bcea1d32e6 refactor(multiverse): use a log line instead of a status message for showing intent to open a thread view 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 346f11319c refactor(multiverse): move the opening of a threaded timeline to its own function 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 937b223627 chore(multiverse): add missing help lines for the new functionalities 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 000d8514f6 refactor(multiverse): store the selected room in the TimelineKind::Room field 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 72692b7b33 feat(multiverse): add basic support for threads 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 0f84d482b9 refactor(multiverse): inline send_message_impl into its own caller
Also don't clear the input if the timeline wasn't found yet.
2025-06-12 13:25:08 +02:00
Benjamin Bouvier c609150a3e refactor(multiverse): introduce RoomView::get_selected_timeline() 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 2f46a6c8a0 refactor(multiverse): use Client to get a Room object by room id 2025-06-12 13:25:08 +02:00
Benjamin Bouvier 7bdddc9d35 refactor(multiverse): misc tiny changes
Notably, avoid holding a lock if it's not going to be used later.
2025-06-12 13:25:08 +02:00
Stefan Ceriu 5113f114a7 fix(ui): forward live events to threaded timelines, the same as live ones
- drop `is_live` and `is_pinned_events` and use the timeline focus directly at the decision point.
2025-06-12 12:57:10 +03:00
Stefan Ceriu 9d96d6ead2 feat(ffi): add support for sending locations as replies or within threads 2025-06-12 12:57:10 +03:00
Daniel Salinas c340a7187a feat(wasm): Fix cargo runtime on Wasm platforms (#5220)
When file was
[moved](https://github.com/matrix-org/matrix-rust-sdk/commit/2a140770a02cbeeaedc8c4dec90de9135aebc679)
it looks like an update was missed, since wasm is not in the CI yet.

Signed-off-by: Daniel Salinas
2025-06-11 22:07:54 +02:00
Richard van der Hoff 0aece695dc crypto: update changelog 2025-06-11 17:15:44 +01:00
Richard van der Hoff b2210292bf crypto: Add a test for spoofed sender, with TrustRequirement::CrossSigned 2025-06-11 17:06:44 +01:00
Richard van der Hoff f0ab6cb1a4 crypto: use a dedicated VerificationLevel if we know the sender of an event is spoofed 2025-06-11 17:06:44 +01:00
Richard van der Hoff c2eeca3f33 crypto: add some instrumentation to get_room_event_encryption_info
It helps to know which event we're getting the encryption info for.
2025-06-11 16:51:51 +01:00
Benjamin Bouvier cc974dd3c9 refactor(event cache): use Event instead of TimelineEvent more evenly 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 8b2a8e7265 refactor(event cache): move the timeline-event-diffs sending back into the callers 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 7cad237dc6 refactor(event cache): reduce indent in maybe_apply_new_redaction 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 72a3972303 refactor(event cache): simplify mutating the RoomEvents in RoomEventCacheStore
No more generic function parameter! Having a separate function for
post-processing is simple enough.
2025-06-11 17:04:15 +02:00
Benjamin Bouvier 2e590e2f67 refactor(event cache): only mark that we've waited for an initial previous-batch token after a sync
It doesn't make sense to do it after a back-pagination, since a
back-pagination does require a previous-batch token in the first place,
meaning that if we did paginate, then we did wait for a previous-batch
token beforehand.
2025-06-11 17:04:15 +02:00
Benjamin Bouvier 224e437a78 refactor(event cache): simplify handling of previous-batch token in handle_backpagination too 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 8a9cae4af3 refactor(event cache): have even fewer methods return timelinediff updates 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 22a15f1342 refactor(event cache): remove code comment that doesn't make sense anymore 2025-06-11 17:04:15 +02:00
Benjamin Bouvier 3ab4584dfe refactor(event cache): have fewer methods return timelinediff updates 2025-06-11 17:04:15 +02:00
Benjamin Bouvier a3238cdadf refactor(event cache): remove indent in RoomEventCacheState::handle_sync 2025-06-11 17:04:15 +02:00
Benjamin Bouvier a884b2c696 refactor(event cache): move handling of a backpagination in RoomEventCacheState 2025-06-11 17:04:15 +02:00
Benjamin Bouvier ec0d7b4311 refactor(event cache): move handling of a sync in RoomEventCacheState 2025-06-11 17:04:15 +02:00
Benjamin Bouvier e8c2d27c9e refactor(event cache): slightly tweak logic around prev-batch token suppression 2025-06-11 17:04:15 +02:00
Benjamin Bouvier bff600a937 refactor(event cache): make deduplication entirely stateless
Having a small data structure to hold the room id and store isn't that
useful, after all.
2025-06-11 17:04:15 +02:00
Michael Goldenberg 404a982503 refactor(indexeddb): support querying by next chunk index, even when next chunk does not exist
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg e904a98735 refactor(indexeddb): re-type IndexedEventPositionIndex as usize as IndexedDB supports numeric keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg b55e79fdac refactor(indexeddb): re-type IndexedChunkId as u64 as IndexedDB supports numeric keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 717116cc05 refactor(indexeddb): re-type IndexedRoomId and IndexedEventId as String for compatibility with SafeEncode
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 0ad4df2031 refactor(indexeddb): remove extraneous room id field from event in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 891e9813b1 refactor(indexeddb): re-type next/previous chunk fields as chunk identifiers rather than entire chunks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 19b21fdd49 fix(indexeddb): enforce type rather than variant distinction between in-band/out-of-band events
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 307fa355ad refactor(indexeddb): add internal types that support encryption and indexing in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 351053fef5 refactor(indexeddb): add internal types that support encryption and indexing in event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 8c735c602a refactor(indexeddb): add internal types for event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 7ffc390cea feat(indexeddb): put event cache store module behind feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Michael Goldenberg 05b67df6e2 feat(indexeddb): add initial database migrations for event cache store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-06-11 16:09:33 +02:00
Denis Kasak f3f3d968b5 Merge pull request #5214 from matrix-org/update-changelog-main
Update changelog main
2025-06-11 12:18:22 +02:00
Denis Kasak bde1d4a353 Merge branch 'release-0.11' 2025-06-11 12:01:48 +02:00
Denis Kasak 4f6ddcd072 Merge pull request #5213 from matrix-org/update-changelog-sec-ref
chore: Add CVE-2025-48937 reference to the CHANGELOG
2025-06-11 11:36:16 +02:00
Denis Kasak b99188dd59 chore: Add CVE-2025-48937 reference to the CHANGELOG
Signed-off-by: Denis Kasak <dkasak@termina.org.uk>
2025-06-11 11:27:36 +02:00
homersimpsons e2fee14ced 📝 Fix changelog links
Signed-off-by: homersimpsons <guillaume.alabre@gmail.com>
2025-06-11 10:32:33 +02:00
Benjamin Bouvier 9fca8f0007 refactor(timeline): include the bundled item owner in edit aggregation metadata 2025-06-10 15:52:00 +02:00
Benjamin Bouvier ca0fc3cf6d fix(timeline): correctly use the bundled event id when handling replacements 2025-06-10 15:52:00 +02:00
Benjamin Bouvier 378f50d8b5 fix(ci): bump the timeout values for test_subscribe_to_knock_requests
They were exceedingly low, especially in the context of code coverage
which can be quite slow.
2025-06-10 14:42:27 +02:00
Damir Jelić 485bb0790e refactor: Move the store caches into the caches module 2025-06-10 13:53:09 +02:00
Damir Jelić 0e9ce0271e refactor: Create a store/types submodule 2025-06-10 13:53:09 +02:00
Damir Jelić c0294d5e33 refactor: Use the EVENT_TYPE constant to deserialize AnyDecryptedToDeviceEvent 2025-06-10 13:53:09 +02:00
365 changed files with 21054 additions and 7247 deletions
+9
View File
@@ -2,3 +2,12 @@
retries = { backoff = "exponential", count = 3, delay = "1s", jitter = true }
# kill the slow tests if they still aren't up after 180s
slow-timeout = { period = "60s", terminate-after = 3 }
[profile.ci]
retries = { backoff = "exponential", count = 4, delay = "1s", jitter = true }
# kill the slow tests if they still aren't up after 180s
slow-timeout = { period = "60s", terminate-after = 3 }
[profile.ci.junit]
path = "junit.xml"
store-success-output = true
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-02-20
toolchain: nightly-2025-06-27
components: rustfmt
- name: Run Benchmarks
+1 -1
View File
@@ -175,7 +175,7 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
complement-crypto:
name: "Run Complement Crypto tests"
+3 -4
View File
@@ -246,7 +246,7 @@ jobs:
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.0
uses: qmaru/wasm-pack-action@v0.5.1
if: '!matrix.check_only'
with:
version: v0.10.3
@@ -290,7 +290,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.33.1
uses: crate-ci/typos@v1.34.0
lint:
name: Lint
@@ -309,7 +309,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-02-20
toolchain: nightly-2025-06-27
components: clippy, rustfmt
- name: Load cache
@@ -397,4 +397,3 @@ jobs:
- name: Compile benchmarks (no run)
run: |
cargo bench --profile dev --no-run
+30 -13
View File
@@ -17,8 +17,12 @@ env:
RUST_LOG: info,matrix_sdk=trace
jobs:
xtask:
uses: ./.github/workflows/xtask.yml
code_coverage:
name: Code Coverage
needs: xtask
runs-on: "ubuntu-latest"
# run several docker containers with the same networking stack so the hostname 'synapse'
@@ -35,6 +39,11 @@ jobs:
- 8008:8008
steps:
# This CI workflow can run into space issue, so we're cleaning up some
# space here.
- name: Create some more space
run: rm -rf /opt/hostedtoolcache
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -56,23 +65,25 @@ jobs:
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install tarpaulin
uses: taiki-e/install-action@v2
with:
tool: cargo-tarpaulin
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
# set up backend for integration tests
- uses: actions/setup-python@v5
with:
python-version: 3.8
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Run tarpaulin
- name: Get xtask
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Create the coverage report
run: |
rustup run stable cargo tarpaulin \
--skip-clean --profile cov --out xml \
--features experimental-widgets,testing
target/debug/xtask ci coverage -o codecov
env:
CARGO_PROFILE_COV_INHERITS: 'dev'
CARGO_PROFILE_COV_DEBUG: 1
@@ -89,6 +100,11 @@ jobs:
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
- name: Move the JUnit file into the root directory
shell: bash
run: |
mv target/nextest/ci/junit.xml ./junit.xml
# This stores the coverage report and metadata in artifacts.
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
@@ -97,7 +113,8 @@ jobs:
with:
name: codecov_report
path: |
cobertura.xml
coverage.xml
junit.xml
pr_number.txt
commit_sha.txt
if-no-files-found: error
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@115870536a85eaf050e369291c7895748ff12aea
uses: tj-actions/changed-files@v46.0.5
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-02-20
toolchain: nightly-2025-06-27
- name: Install Node.js
uses: actions/setup-node@v4
+17
View File
@@ -77,3 +77,20 @@ jobs:
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
Generated
+58 -32
View File
@@ -877,6 +877,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -1173,9 +1183,9 @@ dependencies = [
[[package]]
name = "deadpool-sqlite"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84a12c51972a50e54895427e43743da9737af66395a609283be01ec72efd9fb"
checksum = "9e531d0beb6d12daa84df0482bf89e06c7ed059551ae1d7313dc7531d37778fb"
dependencies = [
"deadpool 0.12.1",
"deadpool-sync",
@@ -2689,9 +2699,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.31.0"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
dependencies = [
"cc",
"pkg-config",
@@ -2864,7 +2874,7 @@ dependencies = [
[[package]]
name = "matrix-sdk"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"anymap2",
@@ -2940,8 +2950,9 @@ dependencies = [
[[package]]
name = "matrix-sdk-base"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"as_variant",
"assert_matches",
"assert_matches2",
@@ -2976,7 +2987,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-common"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"assert_matches",
"assert_matches2",
@@ -3007,7 +3018,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-crypto"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"aes",
"anyhow",
@@ -3089,11 +3100,12 @@ dependencies = [
[[package]]
name = "matrix-sdk-ffi"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"as_variant",
"async-compat",
"console_error_panic_hook",
"extension-trait",
"eyeball-im",
"futures-util",
@@ -3135,7 +3147,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-indexeddb"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"assert_matches",
@@ -3204,7 +3216,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-qrcode"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"byteorder",
"image",
@@ -3216,7 +3228,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-sqlite"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"as_variant",
"assert_matches",
@@ -3236,6 +3248,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"serde_path_to_error",
"similar-asserts",
"tempfile",
"thiserror 2.0.11",
@@ -3246,7 +3259,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-store-encryption"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"base64",
@@ -3266,7 +3279,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-test"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"as_variant",
"ctor",
@@ -3288,7 +3301,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-test-macros"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"quote",
"syn",
@@ -3296,7 +3309,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-ui"
version = "0.12.0"
version = "0.13.0"
dependencies = [
"anyhow",
"as_variant",
@@ -3426,6 +3439,7 @@ dependencies = [
"crossterm",
"futures-util",
"imbl",
"indexmap",
"itertools 0.14.0",
"matrix-sdk",
"matrix-sdk-base",
@@ -4448,9 +4462,9 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.12.3"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d910a9b75cbf0e88f74295997c1a41c3ab7a117879a029c72db815192c167a0d"
checksum = "c1d47e42b7dea75a468dea63a230f51331c58d690ca018ea1c6ac782ea98880c"
dependencies = [
"assign",
"js_int",
@@ -4465,9 +4479,9 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc4ff88a70a3d1e7a2c5b51cca7499cb889b42687608ab664b9a216c49314d"
checksum = "3a9e9c613cfda4923b851c5d8bc442305905bee4f0c2b924564b00e71636c8d4"
dependencies = [
"as_variant",
"assign",
@@ -4489,9 +4503,9 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.15.2"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b75da013b362664c3e161662902e5da3f77e990525681b59c6035bac27e87b4"
checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf"
dependencies = [
"as_variant",
"base64",
@@ -4522,9 +4536,9 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.30.3"
version = "0.30.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ab3d1b54c32a65194ecc44bc7f7575df50ef4255b139547d7dcc1753dc883d"
checksum = "3cdc7abec9bc2a9ca0b4831cc26ce97a6a8c39a0bde44a19281a719e861b4293"
dependencies = [
"as_variant",
"indexmap",
@@ -4548,9 +4562,9 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373bc5a30b84574dfce3e75c33d79d6ba9843bf0eee1bf351f904eef9bea001a"
checksum = "bb2a705c3911870782e036a3a8b676d0166c6c93800b84f6b8b23c981f78ef08"
dependencies = [
"http",
"js_int",
@@ -4586,9 +4600,9 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.15.1"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1182e83ee5cd10121974f163337b16af68a93eedfc7cdbdbd52307ac7e1d743"
checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -4602,9 +4616,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.33.0"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
dependencies = [
"bitflags 2.8.0",
"fallible-iterator",
@@ -4800,6 +4814,7 @@ dependencies = [
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio",
@@ -4846,6 +4861,17 @@ dependencies = [
"serde_json",
]
[[package]]
name = "sentry-debug-images"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60bc2154e6df59beed0ac13d58f8dfaf5ad20a88548a53e29e4d92e8e835c2"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.36.0"
@@ -4952,9 +4978,9 @@ dependencies = [
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
dependencies = [
"itoa",
"serde",
+19 -15
View File
@@ -60,24 +60,26 @@ reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.3", features = [
ruma = { version = "0.12.5", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"compat-encrypted-stickers",
"compat-lax-room-create-deser",
"compat-lax-room-topic-deser",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4143",
"unstable-msc4171",
"unstable-msc4278",
"unstable-msc4286",
] }
ruma-common = "0.15.2"
] }
ruma-common = "0.15.4"
sentry = "0.36.0"
sentry-tracing = "0.36.0"
serde = { version = "1.0.217", features = ["rc"] }
@@ -105,17 +107,17 @@ web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.12.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.12.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.12.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.12.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.13.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.13.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.13.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.13.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.12.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.12.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.12.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.12.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.12.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.12.0", default-features = false }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.13.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.13.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.13.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.13.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.13.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.13.0", default-features = false }
[workspace.lints.rust]
rust_2018_idioms = "warn"
@@ -137,13 +139,15 @@ cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
manual_let_else = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
redundant_clone = "warn"
str_to_string = "warn"
todo = "warn"
unnecessary_semicolon = "warn"
unused_async = "warn"
redundant_clone = "warn"
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
+48 -27
View File
@@ -1,38 +1,55 @@
<h1 align="center">Matrix Rust SDK</h1>
<div align="center">
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
<br/><br/>
<img src="contrib/logo.svg">
<br>
<hr>
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
<a href="https://crates.io/crates/matrix-sdk/">
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
<br>
<a href="https://docs.rs/matrix-sdk/">
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
<br>
<br>
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
<br />
<img src="contrib/logo.svg">
<hr />
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
<a href="https://crates.io/crates/matrix-sdk/">
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
<br />
<a href="https://docs.rs/matrix-sdk/">
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
</div>
<div align="center">
The Matrix Rust SDK is a collection of libraries that make it easier to build
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
<br />
<br />
<picture>
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
<img src="contrib/element-logo-fallback.png" alt="Element logo">
</picture>
<br />
<br />
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
</div>
## Purpose
The SDK takes care of the low-level details like encryption,
syncing, and room state, so you can focus on your app's logic and UI. Whether
you're writing a small bot, a desktop client, or something in between, the SDK
is designed to be flexible, async-friendly, and ready to use out of the box.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
## Project structure
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
The Matrix Rust SDK is made up of several crates that build on top of each
other. The following crates are expected to be usable as direct dependencies:
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) A high-level client library that makes it easy to build
full-featured UI clients with minimal setup. Check out our reference client,
@@ -45,6 +62,9 @@ The Matrix Rust SDK is made up of several crates that build on top of each other
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
for a step-by-step introduction.
All other crates are effectively internal-only and only structured as crates
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
## Status
The library is considered production ready and backs multiple client
@@ -54,9 +74,6 @@ implementations such as Element X
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
confident to build upon it.
Development of the SDK has been primarily sponsored by Element though accepts
contributions from all.
## Bindings
The higher-level crates of the Matrix Rust SDK can be embedded in other
@@ -67,3 +84,7 @@ into your language of choice.
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
+2
View File
@@ -4,6 +4,7 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Through
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
ThreadingSupport,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
@@ -58,6 +59,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
let base_client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
ThreadingSupport::Disabled,
);
runtime
@@ -3,7 +3,7 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
use hmac::Hmac;
use matrix_sdk_crypto::{
backups::DecryptionError,
store::{BackupDecryptionKey, CryptoStoreError as InnerStoreError},
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
};
use pbkdf2::pbkdf2;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
@@ -1,15 +1,15 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
use tokio::runtime::Handle;
use crate::{CryptoStoreError, DehydratedDeviceKey};
+10 -7
View File
@@ -37,8 +37,11 @@ use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState,
use matrix_sdk_crypto::{
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
types::{
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
CryptoStore,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
@@ -221,7 +224,7 @@ async fn migrate_data(
passphrase: Option<String>,
progress_listener: Box<dyn ProgressListener>,
) -> anyhow::Result<()> {
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::BackupDecryptionKey};
use matrix_sdk_crypto::{olm::PrivateCrossSigningIdentity, store::types::BackupDecryptionKey};
use vodozemac::olm::Account;
use zeroize::Zeroize;
@@ -818,10 +821,10 @@ impl BackupKeys {
}
}
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
impl TryFrom<matrix_sdk_crypto::store::types::BackupKeys> for BackupKeys {
type Error = ();
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
fn try_from(keys: matrix_sdk_crypto::store::types::BackupKeys) -> Result<Self, Self::Error> {
Ok(Self {
recovery_key: BackupRecoveryKey {
inner: keys.decryption_key.ok_or(())?,
@@ -866,8 +869,8 @@ impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
}
}
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
impl From<matrix_sdk_crypto::store::types::RoomKeyCounts> for RoomKeyCounts {
fn from(count: matrix_sdk_crypto::store::types::RoomKeyCounts) -> Self {
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
}
}
@@ -16,7 +16,7 @@ use matrix_sdk_crypto::{
},
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
store::types::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
};
@@ -96,8 +96,8 @@ pub struct RoomKeyInfo {
pub session_id: String,
}
impl From<matrix_sdk_crypto::store::RoomKeyInfo> for RoomKeyInfo {
fn from(value: matrix_sdk_crypto::store::RoomKeyInfo) -> Self {
impl From<matrix_sdk_crypto::store::types::RoomKeyInfo> for RoomKeyInfo {
fn from(value: matrix_sdk_crypto::store::types::RoomKeyInfo) -> Self {
Self {
algorithm: value.algorithm.to_string(),
room_id: value.room_id.to_string(),
@@ -1,6 +1,7 @@
use std::sync::Arc;
use futures_util::{Stream, StreamExt};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
@@ -8,7 +9,6 @@ use matrix_sdk_crypto::{
VerificationRequestState as RustVerificationRequestState,
};
use ruma::events::key::verification::VerificationMethod;
use tokio::runtime::Handle;
use vodozemac::{base64_decode, base64_encode};
use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadRequest};
+46 -4
View File
@@ -6,11 +6,53 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.13.0] - 2025-07-10
### Features
- Add `NotificationRoomInfo::topic` to the `NotificationRoomInfo` struct, which
contains the topic of the room. This is useful for displaying the room topic
in notifications. ([#5300](https://github.com/matrix-org/matrix-rust-sdk/pull/5300))
- Add `EmbeddedEventDetails::timestamp` and `EmbeddedEventDetails::event_or_transaction_id`
which are already available in regular timeline items.
([#5331](https://github.com/matrix-org/matrix-rust-sdk/pull/5331))
- `RoomListService::subscribe_to_rooms` becomes `async` and automatically calls
`matrix_sdk::latest_events::LatestEvents::listen_to_room`
([#5369](https://github.com/matrix-org/matrix-rust-sdk/pull/5369))
### Refactor
- Adjust features in the `matrix-sdk-ffi` crate to expose more platform-specific knobs.
Previously the `matrix-sdk-ffi` was configured primarily by target configs, choosing
between the tls flavor (`rustls-tls` or `native-tls`) and features like `sentry` based
purely on the target. As we work to add an additional Wasm target to this crate,
the cross product of target specific features has become somewhat chaotic, and we
have shifted to externalize these choices as feature flags.
To maintain existing compatibility on the major platforms, these features should be used:
Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
Javascript/Wasm: `"unstable-msc4274,native-tls"`
In the future additional choices (such as session storage, `sqlite` and `indexeddb`)
will likely be added as well.
Breaking changes:
- `Client::reset_server_capabilities` has been renamed to `Client::reset_server_info`.
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
- `RoomPreview::join_rule`, `NotificationItem::join_rule`, `RoomInfo::is_public`, and
`Room::is_public()` return values are now optional. They will be set to `None` if the join rule
state event is missing for a given room. `NotificationRoomInfo::is_public` has been removed;
callers can inspect the value of `NotificationItem::join_rule` to determine if the room is public
(i.e. if the join rule is `Public`).
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
## [0.12.0] - 2025-06-10
Breaking changes:
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
the event was sent.
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
config is uploaded.
@@ -25,8 +67,8 @@ Breaking changes:
Additions:
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
we can automatically update the UI.
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
uploads by compressing/transcoding the media.
@@ -46,7 +88,7 @@ Additions:
Breaking changes:
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
## [0.11.0] - 2025-04-11
+43 -58
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-ffi"
version = "0.12.0"
version = "0.13.0"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
@@ -14,99 +14,84 @@ publish = false
release = true
[lib]
crate-type = ["cdylib", "staticlib"]
crate-type = [
# Needed by uniffi for Android bindings
"cdylib",
# Needed by uniffi for iOS bindings
"staticlib",
# Needed by uniffi for JS/Wasm bindings, which use rust as an intermediate language
"lib"
]
[features]
default = ["bundled-sqlite", "unstable-msc4274"]
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
# Use Rustls as the TLS implementation, necessary on Android platforms.
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
sentry = ["dep:sentry", "dep:sentry-tracing"]
[dependencies]
anyhow.workspace = true
as_variant.workspace = true
async-compat = "0.2.4"
extension-trait = "1.0.1"
eyeball-im.workspace = true
futures-util.workspace = true
language-tags = "0.3.2"
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk = { workspace = true, features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
"socks",
"sqlite",
"uniffi",
] }
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.16"
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
sentry-tracing = "0.36.0"
serde.workspace = true
serde_json.workspace = true
sentry = { version = "0.36.0", optional = true, default-features = false, features = [
# Most default features enabled otherwise.
"backtrace",
"contexts",
"panic",
"reqwest",
"sentry-debug-images",
] }
sentry-tracing = { version = "0.36.0", optional = true }
thiserror.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tracing.workspace = true
tracing-appender = { version = "0.2.2" }
tracing-core.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
uniffi = { workspace = true, features = ["tokio"] }
url.workspace = true
uuid = { version = "1.4.1", features = ["v4"] }
zeroize.workspace = true
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
workspace = true
features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
# note: differ from block below
"native-tls",
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(target_family = "wasm")'.dependencies]
console_error_panic_hook = "0.1.7"
tokio = { workspace = true, features = ["sync", "macros"] }
uniffi.workspace = true
[target.'cfg(not(target_os = "android"))'.dependencies.sentry]
version = "0.36.0"
default-features = false
features = [
# TLS lib used on non-Android platforms.
"native-tls",
# Most default features enabled otherwise.
"backtrace",
"contexts",
"panic",
"reqwest",
]
[target.'cfg(not(target_family = "wasm"))'.dependencies]
async-compat.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.1"
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
workspace = true
features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
# note: differ from block above
"rustls-tls",
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(target_os = "android")'.dependencies.sentry]
version = "0.36.0"
default-features = false
features = [
# TLS lib specific for Android.
"rustls",
# Most default features enabled otherwise.
"backtrace",
"contexts",
"panic",
"reqwest",
]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
+20
View File
@@ -2,8 +2,28 @@
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
## Features
Given the number of platforms targeted, we have broken out a number of features
### Platform specific
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
### Functionality
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
### Unstable specs
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
## Platforms
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
### Swift/iOS sync
+91 -45
View File
@@ -2,24 +2,26 @@ use std::{
collections::HashMap,
fmt::Debug,
path::PathBuf,
sync::{Arc, OnceLock, RwLock},
sync::{Arc, OnceLock},
time::Duration,
};
use anyhow::{anyhow, Context as _};
use futures_util::pin_mut;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
use matrix_sdk::{
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
},
event_cache::EventCacheError,
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
MediaRetentionPolicy, MediaThumbnailSettings,
},
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
ruma::{
api::client::{
discovery::get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
discovery::{
discover_homeserver::RtcFocusInfo,
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
},
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
@@ -84,7 +86,10 @@ use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
use super::{room::Room, session_verification::SessionVerificationController};
use super::{
room::{room_info::RoomInfo, Room},
session_verification::SessionVerificationController,
};
use crate::{
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
@@ -93,7 +98,6 @@ use crate::{
notification_settings::NotificationSettings,
room::{RoomHistoryVisibility, RoomInfoListener},
room_directory_search::RoomDirectorySearch,
room_info::RoomInfo,
room_preview::RoomPreview,
ruma::{
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
@@ -490,32 +494,6 @@ impl Client {
Ok(())
}
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
filename: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
) -> Result<Arc<MediaFileHandle>, ClientError> {
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
let handle = self
.inner
.media()
.get_media_file(
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
)
.await?;
Ok(Arc::new(MediaFileHandle::new(handle)))
}
/// Restores the client from a `Session`.
///
/// It reloads the entire set of rooms from the previous session.
@@ -725,11 +703,44 @@ impl Client {
/// Empty the server version and unstable features cache.
///
/// Since the SDK caches server capabilities (versions and unstable
/// features), it's possible to have a stale entry in the cache. This
/// functions makes it possible to force reset it.
pub async fn reset_server_capabilities(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_server_capabilities().await?)
/// Since the SDK caches server info (versions, unstable features,
/// well-known etc), it's possible to have a stale entry in the cache.
/// This functions makes it possible to force reset it.
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_server_info().await?)
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Retrieves a media file from the media source
///
/// Not available on Wasm platforms, due to lack of accessible file system.
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
filename: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
) -> Result<Arc<MediaFileHandle>, ClientError> {
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
let handle = self
.inner
.media()
.get_media_file(
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
)
.await?;
Ok(Arc::new(MediaFileHandle::new(handle)))
}
}
@@ -1419,9 +1430,16 @@ impl Client {
/// Clear all the non-critical caches for this Client instance.
///
/// WARNING: This will clear all the caches, including the base store (state
/// store), so callers must make sure that any sync is inactive before
/// calling this method. In particular, the `SyncService` must not be
/// running. After the method returns, the Client will be in an unstable
/// store), so callers must make sure that the Client is at rest before
/// calling it.
///
/// In particular, if a [`SyncService`] is running, it must be passed here
/// as a parameter, or stopped before calling this method. Ideally, the
/// send queues should have been disabled and must all be inactive (i.e.
/// not sending events); this method will disable them, but it might not
/// be enough if the queues are still processing events.
///
/// After the method returns, the Client will be in an unstable
/// state, and it is required that the caller reinstantiates a new
/// Client instance, be it via dropping the previous and re-creating it,
/// restarting their application, or any other similar means.
@@ -1431,8 +1449,23 @@ impl Client {
/// will start as if they were empty.
/// - This will empty the media cache according to the current media
/// retention policy.
pub async fn clear_caches(&self) -> Result<(), ClientError> {
pub async fn clear_caches(
&self,
sync_service: Option<Arc<SyncService>>,
) -> Result<(), ClientError> {
let closure = async || -> Result<_, ClientError> {
// First, make sure to expire sessions in the sync service.
if let Some(sync_service) = sync_service {
sync_service.inner.expire_sessions().await;
}
// Disable the send queues, as they might read and write to the state store.
// Events being send might still be active, and cause errors if
// processing finishes, so this will only minimize damage. Since
// this method should only be called in exceptional cases, this has
// been deemed acceptable.
self.inner.send_queue().set_enabled(false).await;
// Clean up the media cache according to the current media retention policy.
self.inner
.event_cache_store()
@@ -1489,6 +1522,16 @@ impl Client {
Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
}
/// Checks if the server supports the LiveKit RTC focus for placing calls.
pub async fn is_livekit_rtc_supported(&self) -> Result<bool, ClientError> {
Ok(self
.inner
.rtc_foci()
.await?
.iter()
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
}
/// Subscribe to changes in the media preview configuration.
pub async fn subscribe_to_media_preview_config(
&self,
@@ -2153,17 +2196,20 @@ fn gen_transaction_id() -> String {
/// A file handle that takes ownership of a media file on disk. When the handle
/// is dropped, the file will be removed from the disk.
#[cfg(not(target_family = "wasm"))]
#[derive(uniffi::Object)]
pub struct MediaFileHandle {
inner: RwLock<Option<SdkMediaFileHandle>>,
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
}
#[cfg(not(target_family = "wasm"))]
impl MediaFileHandle {
fn new(handle: SdkMediaFileHandle) -> Self {
Self { inner: RwLock::new(Some(handle)) }
Self { inner: std::sync::RwLock::new(Some(handle)) }
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl MediaFileHandle {
/// Get the media file's path.
+118 -243
View File
@@ -1,32 +1,35 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use futures_util::StreamExt;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::{
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
CollectStrategy, TrustRequirement,
types::qr_login::QrCodeModeData, CollectStrategy, DecryptionSettings, TrustRequirement,
},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
reqwest::Certificate,
ruma::{ServerName, UserId},
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, SqliteStoreConfig,
RumaApiError, SqliteStoreConfig, ThreadingSupport,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use zeroize::Zeroizing;
use super::client::Client;
use crate::{
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
helpers::unwrap_or_clone_arc, runtime::get_runtime_handle, task_handle::TaskHandle,
authentication::OidcConfiguration,
client::ClientSessionDelegate,
error::ClientError,
helpers::unwrap_or_clone_arc,
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
runtime::get_runtime_handle,
task_handle::TaskHandle,
};
/// A list of bytes containing a certificate in DER or PEM form.
@@ -39,164 +42,6 @@ enum HomeserverConfig {
ServerNameOrUrl(String),
}
/// Data for the QR code login mechanism.
///
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
/// decoded from a QR code.
#[derive(Debug, uniffi::Object)]
pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
/// The slice of bytes would generally be returned by a QR code decoder.
#[uniffi::constructor]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
/// The server name contained within the scanned QR code data.
///
/// Note: This value is only present when scanning a QR code the belongs to
/// a logged in client. The mode where the new client shows the QR code
/// will return `None`.
pub fn server_name(&self) -> Option<String> {
match &self.inner.mode_data {
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
QrCodeModeData::Login => None,
}
}
}
/// Error type for the decoding of the [`QrCodeData`].
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum QrCodeDecodeError {
#[error("Error decoding QR code: {error:?}")]
Crypto {
#[from]
error: LoginQrCodeDecodeError,
},
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum HumanQrLoginError {
#[error("Linking with this device is not supported.")]
LinkingNotSupported,
#[error("The sign in was cancelled.")]
Cancelled,
#[error("The sign in was not completed in the required time.")]
Expired,
#[error("A secure connection could not have been established between the two devices.")]
ConnectionInsecure,
#[error("The sign in was declined.")]
Declined,
#[error("An unknown error has happened.")]
Unknown,
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
SlidingSyncNotAvailable,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
OidcMetadataInvalid,
#[error("The other device is not signed in and as such can't sign in other devices.")]
OtherDeviceNotSignedIn,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
fn from(value: qrcode::QRCodeLoginError) -> Self {
use qrcode::{QRCodeLoginError, SecureChannelError};
match value {
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
_ => HumanQrLoginError::Unknown,
},
QRCodeLoginError::OAuth(e) => {
if let Some(e) = e.as_request_token_error() {
match e {
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
_ => HumanQrLoginError::Unknown,
}
} else {
HumanQrLoginError::Unknown
}
}
QRCodeLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
},
QRCodeLoginError::UnexpectedMessage { .. }
| QRCodeLoginError::CrossProcessRefreshLock(_)
| QRCodeLoginError::DeviceKeyUpload(_)
| QRCodeLoginError::SessionTokens(_)
| QRCodeLoginError::UserIdDiscovery(_)
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
}
}
}
/// Enum describing the progress of the QR-code login.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum QrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We established a secure channel with the other device.
EstablishingSecureChannel {
/// The check code that the device should display so the other device
/// can confirm that the channel is secure as well.
check_code: u8,
/// The string representation of the check code, will be guaranteed to
/// be 2 characters long, preserving the leading zero if the
/// first digit is a zero.
check_code_string: String,
},
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::Done => Self::Done,
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum ClientBuildError {
@@ -274,21 +119,29 @@ pub struct ClientBuilder {
system_is_memory_constrained: bool,
username: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
user_agent: Option<String>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
disable_built_in_root_certificates: bool,
encryption_settings: EncryptionSettings,
room_key_recipient_strategy: CollectStrategy,
decryption_trust_requirement: TrustRequirement,
decryption_settings: DecryptionSettings,
enable_share_history_on_invite: bool,
request_config: Option<RequestConfig>,
#[cfg(not(target_family = "wasm"))]
user_agent: Option<String>,
#[cfg(not(target_family = "wasm"))]
proxy: Option<String>,
#[cfg(not(target_family = "wasm"))]
disable_ssl_verification: bool,
#[cfg(not(target_family = "wasm"))]
disable_built_in_root_certificates: bool,
#[cfg(not(target_family = "wasm"))]
additional_root_certificates: Vec<Vec<u8>>,
threads_enabled: bool,
}
#[matrix_sdk_ffi_macros::export]
@@ -321,9 +174,12 @@ impl ClientBuilder {
auto_enable_backups: false,
},
room_key_recipient_strategy: Default::default(),
decryption_trust_requirement: TrustRequirement::Untrusted,
decryption_settings: DecryptionSettings {
sender_device_trust_requirement: TrustRequirement::Untrusted,
},
enable_share_history_on_invite: false,
request_config: Default::default(),
threads_enabled: false,
})
}
@@ -455,12 +311,6 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.user_agent = Some(user_agent);
Arc::new(builder)
}
pub fn sliding_sync_version_builder(
self: Arc<Self>,
version_builder: SlidingSyncVersionBuilder,
@@ -470,43 +320,12 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.proxy = Some(url);
Arc::new(builder)
}
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_ssl_verification = true;
Arc::new(builder)
}
pub fn disable_automatic_token_refresh(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_automatic_token_refresh = true;
Arc::new(builder)
}
pub fn add_root_certificates(
self: Arc<Self>,
certificates: Vec<CertificateBytes>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.additional_root_certificates = certificates;
Arc::new(builder)
}
/// Don't trust any system root certificates, only trust the certificates
/// provided through
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_built_in_root_certificates = true;
Arc::new(builder)
}
pub fn auto_enable_cross_signing(
self: Arc<Self>,
auto_enable_cross_signing: bool,
@@ -545,12 +364,12 @@ impl ClientBuilder {
}
/// Set the trust requirement to be used when decrypting events.
pub fn room_decryption_trust_requirement(
pub fn decryption_settings(
self: Arc<Self>,
trust_requirement: TrustRequirement,
decryption_settings: DecryptionSettings,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.decryption_trust_requirement = trust_requirement;
builder.decryption_settings = decryption_settings;
Arc::new(builder)
}
@@ -574,6 +393,12 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn threads_enabled(self: Arc<Self>, enabled: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.threads_enabled = enabled;
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
@@ -650,52 +475,55 @@ impl ClientBuilder {
}
};
let mut certificates = Vec::new();
#[cfg(not(target_family = "wasm"))]
{
let mut certificates = Vec::new();
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
match Certificate::from_der(&certificate) {
Ok(cert) => {
certificates.push(cert);
}
Err(der_error) => {
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
ClientBuildError::Generic {
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
}
})?;
certificates.push(cert);
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
match Certificate::from_der(&certificate) {
Ok(cert) => {
certificates.push(cert);
}
Err(der_error) => {
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
ClientBuildError::Generic {
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
}
})?;
certificates.push(cert);
}
}
}
}
inner_builder = inner_builder.add_root_certificates(certificates);
inner_builder = inner_builder.add_root_certificates(certificates);
if builder.disable_built_in_root_certificates {
inner_builder = inner_builder.disable_built_in_root_certificates();
}
if builder.disable_built_in_root_certificates {
inner_builder = inner_builder.disable_built_in_root_certificates();
}
if let Some(proxy) = builder.proxy {
inner_builder = inner_builder.proxy(proxy);
}
if let Some(proxy) = builder.proxy {
inner_builder = inner_builder.proxy(proxy);
}
if builder.disable_ssl_verification {
inner_builder = inner_builder.disable_ssl_verification();
if builder.disable_ssl_verification {
inner_builder = inner_builder.disable_ssl_verification();
}
if let Some(user_agent) = builder.user_agent {
inner_builder = inner_builder.user_agent(user_agent);
}
}
if !builder.disable_automatic_token_refresh {
inner_builder = inner_builder.handle_refresh_tokens();
}
if let Some(user_agent) = builder.user_agent {
inner_builder = inner_builder.user_agent(user_agent);
}
inner_builder = inner_builder
.with_encryption_settings(builder.encryption_settings)
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
.with_decryption_trust_requirement(builder.decryption_trust_requirement)
.with_decryption_settings(builder.decryption_settings)
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
match builder.sliding_sync_version_builder {
@@ -736,6 +564,12 @@ impl ClientBuilder {
inner_builder = inner_builder.request_config(updated_config);
}
inner_builder = inner_builder.with_threading_support(if builder.threads_enabled {
ThreadingSupport::Enabled
} else {
ThreadingSupport::Disabled
});
let sdk_client = inner_builder.build().await?;
Ok(Arc::new(
@@ -804,6 +638,47 @@ impl ClientBuilder {
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.proxy = Some(url);
Arc::new(builder)
}
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_ssl_verification = true;
Arc::new(builder)
}
pub fn add_root_certificates(
self: Arc<Self>,
certificates: Vec<CertificateBytes>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.additional_root_certificates = certificates;
Arc::new(builder)
}
/// Don't trust any system root certificates, only trust the certificates
/// provided through
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_built_in_root_certificates = true;
Arc::new(builder)
}
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.user_agent = Some(user_agent);
Arc::new(builder)
}
}
/// The store paths the client will use when built.
#[derive(Clone)]
struct SessionPaths {
-22
View File
@@ -1,22 +0,0 @@
use serde::Deserialize;
use crate::ClientError;
/// Well-known settings specific to ElementCall
#[derive(Deserialize, uniffi::Record)]
pub struct ElementCallWellKnown {
widget_url: String,
}
/// Element specific well-known settings
#[derive(Deserialize, uniffi::Record)]
pub struct ElementWellKnown {
call: Option<ElementCallWellKnown>,
registration_helper_url: Option<String>,
}
/// Helper function to parse a string into a ElementWellKnown struct
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::from_err)
}
+1 -1
View File
@@ -442,7 +442,7 @@ impl Encryption {
Err(error) => {
error!("Failed fetching identity from the store: {error}");
}
};
}
info!("Requesting identity from the server.");
+21 -6
View File
@@ -1,13 +1,20 @@
use std::{collections::HashMap, error::Error, fmt, fmt::Display, time::SystemTime};
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
use matrix_sdk::{
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError,
authentication::oauth::OAuthError,
encryption::{identities::RequestVerificationError, CryptoStoreError},
event_cache::EventCacheError,
reqwest,
room::edit::EditError,
send_queue::RoomSendQueueError,
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
MilliSecondsSinceUnixEpoch,
};
use tracing::warn;
use uniffi::UnexpectedUniFFICallbackError;
@@ -198,6 +205,12 @@ impl From<FocusEventError> for ClientError {
}
}
impl From<RequestVerificationError> for ClientError {
fn from(e: RequestVerificationError) -> Self {
Self::from_err(e)
}
}
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
/// String.
///
@@ -749,7 +762,9 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
let retry_after_ms = match retry_after {
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
Some(RetryAfter::DateTime(system_time)) => {
let duration = system_time.duration_since(SystemTime::now()).ok();
let duration = MilliSecondsSinceUnixEpoch::now()
.to_system_time()
.and_then(|now| system_time.duration_since(now).ok());
duration.map(|duration| duration.as_millis() as u64)
}
None => None,
+3 -6
View File
@@ -1,14 +1,11 @@
// 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.
// Needed because uniffi macros contain empty lines after docs.
#![allow(clippy::empty_line_after_doc_comments)]
mod authentication;
mod chunk_iterator;
mod client;
mod client_builder;
mod element;
mod encryption;
mod error;
mod event;
@@ -18,10 +15,10 @@ mod live_location_share;
mod notification;
mod notification_settings;
mod platform;
mod qr_code;
mod room;
mod room_alias;
mod room_directory_search;
mod room_info;
mod room_list;
mod room_member;
mod room_preview;
+70 -34
View File
@@ -1,10 +1,10 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
};
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
use tracing::error;
use crate::{
client::{Client, JoinRule},
@@ -31,11 +31,11 @@ pub struct NotificationRoomInfo {
pub display_name: String,
pub avatar_url: Option<String>,
pub canonical_alias: Option<String>,
pub topic: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_public: bool,
}
#[derive(uniffi::Record)]
@@ -54,12 +54,12 @@ pub struct NotificationItem {
}
impl NotificationItem {
fn from_inner(item: MatrixNotificationItem) -> Self {
fn from_inner(item: SdkNotificationItem) -> Self {
let event = match item.event {
matrix_sdk_ui::notification_client::NotificationEvent::Timeline(event) => {
SdkNotificationEvent::Timeline(event) => {
NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
}
matrix_sdk_ui::notification_client::NotificationEvent::Invite(event) => {
SdkNotificationEvent::Invite(event) => {
NotificationEvent::Invite { sender: event.sender.to_string() }
}
};
@@ -74,11 +74,11 @@ impl NotificationItem {
display_name: item.room_computed_display_name,
avatar_url: item.room_avatar_url,
canonical_alias: item.room_canonical_alias,
join_rule: item.room_join_rule.try_into().ok(),
topic: item.room_topic,
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: item.joined_members_count,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_public: item.is_room_public,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
@@ -87,9 +87,46 @@ impl NotificationItem {
}
}
#[allow(clippy::large_enum_variant)]
#[derive(uniffi::Enum)]
pub enum NotificationStatus {
/// The event has been found and was not filtered out.
Event { item: NotificationItem },
/// The event couldn't be found in the network queries used to find it.
EventNotFound,
/// The event has been filtered out, either because of the user's push
/// rules, or because the user which triggered it is ignored by the
/// current user.
EventFilteredOut,
}
impl From<SdkNotificationStatus> for NotificationStatus {
fn from(item: SdkNotificationStatus) -> Self {
match item {
SdkNotificationStatus::Event(item) => {
NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
}
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(uniffi::Enum)]
pub enum BatchNotificationResult {
/// We have more detailed information about the notification.
Ok { status: NotificationStatus },
/// An error occurred while trying to fetch the notification.
Error {
/// The error message observed while handling a specific notification.
message: String,
},
}
#[derive(uniffi::Object)]
pub struct NotificationClient {
pub(crate) inner: MatrixNotificationClient,
pub(crate) inner: SdkNotificationClient,
/// A reference to the FFI client.
///
@@ -113,55 +150,54 @@ impl NotificationClient {
Ok(room)
}
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
/// Fetches the content of a notification.
///
/// This will first try to get the notification using a short-lived sliding
/// sync, and if the sliding-sync can't find the event, then it'll use a
/// `/context` query to find the event with associated member information.
///
/// An error result means that we couldn't resolve the notification; in that
/// case, a dummy notification may be displayed instead.
pub async fn get_notification(
&self,
room_id: String,
event_id: String,
) -> Result<Option<NotificationItem>, ClientError> {
) -> Result<NotificationStatus, ClientError> {
let room_id = RoomId::parse(room_id)?;
let event_id = EventId::parse(event_id)?;
let item =
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
if let Some(item) = item {
Ok(Some(NotificationItem::from_inner(item)))
} else {
Ok(None)
}
Ok(item.into())
}
/// Get several notification items in a single batch.
///
/// Returns an error if the flow failed when preparing to fetch the
/// notifications, and a [`HashMap`] containing either a
/// [`NotificationItem`] or no entry for it if it failed to fetch a
/// notification for the provided [`EventId`].
/// [`BatchNotificationResult`], that indicates if the notification was
/// successfully fetched (in which case, it's a [`NotificationStatus`]), or
/// an error message if it couldn't be fetched.
pub async fn get_notifications(
&self,
requests: Vec<NotificationItemsRequest>,
) -> Result<HashMap<String, NotificationItem>, ClientError> {
) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
let requests =
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
let items = self.inner.get_notifications(&requests).await?;
let mut result = HashMap::new();
let mut batch_result = HashMap::new();
for (key, value) in items.into_iter() {
match value {
Ok(item) => {
result.insert(key.to_string(), NotificationItem::from_inner(item));
}
Err(error) => {
// TODO This error should actually be returned so the clients can handle the
// error as they see fit, but it's failing when creating
// bindings for Go, i.e.
// (https://github.com/NordSecurity/uniffi-bindgen-go/issues/62)
error!("Could not fetch notification {key}, an error happened: {error}");
}
}
let result = match value {
Ok(status) => BatchNotificationResult::Ok { status: status.into() },
Err(error) => BatchNotificationResult::Error { message: error.to_string() },
};
batch_result.insert(key.to_string(), result);
}
Ok(result)
Ok(batch_result)
}
}
+346 -233
View File
@@ -1,5 +1,8 @@
use std::sync::{atomic::AtomicBool, Arc, OnceLock};
use std::sync::OnceLock;
#[cfg(feature = "sentry")]
use std::sync::{atomic::AtomicBool, Arc};
#[cfg(feature = "sentry")]
use tracing::warn;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_core::Subscriber;
@@ -11,164 +14,158 @@ use tracing_subscriber::{
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::SubscriberExt as _,
layer::{Layered, SubscriberExt as _},
registry::LookupSpan,
reload::{self, Handle},
util::SubscriberInitExt as _,
Layer,
EnvFilter, Layer, Registry,
};
use crate::{error::ClientError, tracing::LogLevel};
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
display_timestamp: bool,
display_level: bool,
}
impl EventFormatter {
fn new() -> Self {
Self { display_timestamp: true, display_level: true }
}
#[cfg(target_os = "android")]
fn for_logcat() -> Self {
// Level and time are already captured by logcat separately
Self { display_timestamp: false, display_level: false }
}
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
if fmt::time::SystemTime.format_time(writer).is_err() {
writer.write_str("<unknown time>")?;
}
Ok(())
}
fn write_filename(
&self,
writer: &mut fmt::format::Writer<'_>,
filename: &str,
) -> std::fmt::Result {
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
let crates_io_filename = filename
.split_once(CRATES_IO_PATH_MATCHER)
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
if let Some(filename) = crates_io_filename {
writer.write_str("<crates.io>/")?;
writer.write_str(filename)
} else {
writer.write_str(filename)
}
}
}
impl<S, N> FormatEvent<S, N> for EventFormatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
display_timestamp: bool,
display_level: bool,
}
fn format_event(
&self,
ctx: &fmt::FmtContext<'_, S, N>,
mut writer: fmt::format::Writer<'_>,
event: &tracing_core::Event<'_>,
) -> std::fmt::Result {
let meta = event.metadata();
impl EventFormatter {
fn new() -> Self {
Self { display_timestamp: true, display_level: true }
if self.display_timestamp {
self.format_timestamp(&mut writer)?;
writer.write_char(' ')?;
}
#[cfg(target_os = "android")]
fn for_logcat() -> Self {
// Level and time are already captured by logcat separately
Self { display_timestamp: false, display_level: false }
if self.display_level {
// For info and warn, add a padding space to the left
write!(writer, "{:>5} ", meta.level())?;
}
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
if fmt::time::SystemTime.format_time(writer).is_err() {
writer.write_str("<unknown time>")?;
}
Ok(())
}
write!(writer, "{}: ", meta.target())?;
fn write_filename(
&self,
writer: &mut fmt::format::Writer<'_>,
filename: &str,
) -> std::fmt::Result {
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
let crates_io_filename = filename
.split_once(CRATES_IO_PATH_MATCHER)
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
ctx.format_fields(writer.by_ref(), event)?;
if let Some(filename) = crates_io_filename {
writer.write_str("<crates.io>/")?;
writer.write_str(filename)
} else {
writer.write_str(filename)
if let Some(filename) = meta.file() {
writer.write_str(" | ")?;
self.write_filename(&mut writer, filename)?;
if let Some(line_number) = meta.line() {
write!(writer, ":{line_number}")?;
}
}
}
impl<S, N> FormatEvent<S, N> for EventFormatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &fmt::FmtContext<'_, S, N>,
mut writer: fmt::format::Writer<'_>,
event: &tracing_core::Event<'_>,
) -> std::fmt::Result {
let meta = event.metadata();
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
if self.display_timestamp {
self.format_timestamp(&mut writer)?;
writer.write_char(' ')?;
}
let mut first = true;
if self.display_level {
// For info and warn, add a padding space to the left
write!(writer, "{:>5} ", meta.level())?;
}
write!(writer, "{}: ", meta.target())?;
ctx.format_fields(writer.by_ref(), event)?;
if let Some(filename) = meta.file() {
writer.write_str(" | ")?;
self.write_filename(&mut writer, filename)?;
if let Some(line_number) = meta.line() {
write!(writer, ":{line_number}")?;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
}
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
first = false;
let mut first = true;
write!(writer, "{}", span.name())?;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
first = false;
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
}
}
writeln!(writer)
}
writeln!(writer)
}
}
let file_layer = config.write_to_files.map(|c| {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&c.file_prefix);
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterForFiles(DefaultFields);
if let Some(max_files) = c.max_files {
builder = builder.max_log_files(max_files as usize)
};
if let Some(file_suffix) = c.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
type ReloadHandle = Handle<
tracing_subscriber::fmt::Layer<
Layered<EnvFilter, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
>,
Layered<EnvFilter, Registry>,
>;
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterForFiles(DefaultFields);
fn text_layers(
config: TracingConfiguration,
) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
let (file_layer, reload_handle) = config
.write_to_files
.map(|c| {
let layer = make_file_layer(c);
reload::Layer::new(layer)
})
.unzip();
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(writer)
});
Layer::and_then(
let layers = Layer::and_then(
file_layer,
config.write_to_stdout_or_system.then(|| {
// Another fields formatter is necessary because of this bug
@@ -206,7 +203,41 @@ where
"org.matrix.rust.sdk".to_owned(),
));
}),
)
);
(layers, reload_handle)
}
fn make_file_layer(
file_configuration: TracingFileConfiguration,
) -> tracing_subscriber::fmt::Layer<
Layered<EnvFilter, Registry, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
> {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&file_configuration.file_prefix);
if let Some(max_files) = file_configuration.max_files {
builder = builder.max_log_files(max_files as usize)
}
if let Some(file_suffix) = file_configuration.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
let writer =
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(writer)
}
/// Configuration to save logs to (rotated) log-files.
@@ -258,6 +289,7 @@ enum LogTarget {
// SDK UI modules.
MatrixSdkUiTimeline,
MatrixSdkUiNotificationClient,
}
impl LogTarget {
@@ -280,6 +312,7 @@ impl LogTarget {
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
LogTarget::MatrixSdkUiNotificationClient => "matrix_sdk_ui::notification_client",
}
}
}
@@ -302,6 +335,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
];
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
@@ -322,6 +356,8 @@ pub enum TraceLogPacks {
SendQueue,
/// Enables all the logs relevant to the timeline.
Timeline,
/// Enables all the logs relevant to the notification client.
NotificationClient,
}
impl TraceLogPacks {
@@ -336,10 +372,12 @@ impl TraceLogPacks {
],
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
}
}
}
#[cfg(feature = "sentry")]
struct SentryLoggingCtx {
/// The Sentry client guard, which keeps the Sentry context alive.
_guard: sentry::ClientInitGuard,
@@ -349,6 +387,8 @@ struct SentryLoggingCtx {
}
struct LoggingCtx {
reload_handle: Option<ReloadHandle>,
#[cfg(feature = "sentry")]
sentry: Option<SentryLoggingCtx>,
}
@@ -376,12 +416,14 @@ pub struct TracingConfiguration {
write_to_files: Option<TracingFileConfiguration>,
/// If set, the Sentry DSN to use for error reporting.
#[cfg(feature = "sentry")]
sentry_dsn: Option<String>,
}
impl TracingConfiguration {
/// Sets up the tracing configuration and return a [`Logger`] instance
/// holding onto it.
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
fn build(mut self) -> LoggingCtx {
// Show full backtraces, if we run into panics.
std::env::set_var("RUST_BACKTRACE", "1");
@@ -389,73 +431,90 @@ impl TracingConfiguration {
// Log panics.
log_panics::init();
// Prepare the Sentry layer, if a DSN is provided.
let (sentry_layer, sentry_logging_ctx) = if let Some(sentry_dsn) = self.sentry_dsn.take() {
// Initialize the Sentry client with the given options.
let sentry_guard = sentry::init((
sentry_dsn,
sentry::ClientOptions {
traces_sample_rate: 0.0,
attach_stacktrace: true,
..sentry::ClientOptions::default()
},
));
let sentry_enabled = Arc::new(AtomicBool::new(true));
// Add a Sentry layer to the tracing subscriber.
//
// Pass custom event and span filters, which will ignore anything, if the Sentry
// support has been globally disabled, or if the statement doesn't include a
// `sentry` field set to `true`.
let sentry_layer = sentry_tracing::layer()
.event_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst)
&& metadata.fields().field("sentry").is_some()
{
sentry_tracing::default_event_filter(metadata)
} else {
// Ignore the event.
sentry_tracing::EventFilter::Ignore
}
}
})
.span_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
sentry_tracing::default_span_filter(metadata)
} else {
// Ignore, if sentry is globally disabled.
false
}
}
});
(
Some(sentry_layer),
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
)
} else {
(None, None)
};
let env_filter = build_tracing_filter(&self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(crate::platform::text_layers(self))
.with(sentry_layer)
.init();
let logging_ctx;
#[cfg(feature = "sentry")]
{
// Prepare the Sentry layer, if a DSN is provided.
let (sentry_layer, sentry_logging_ctx) =
if let Some(sentry_dsn) = self.sentry_dsn.take() {
// Initialize the Sentry client with the given options.
let sentry_guard = sentry::init((
sentry_dsn,
sentry::ClientOptions {
traces_sample_rate: 0.0,
attach_stacktrace: true,
release: Some(env!("VERGEN_GIT_SHA").into()),
..sentry::ClientOptions::default()
},
));
let sentry_enabled = Arc::new(AtomicBool::new(true));
// Add a Sentry layer to the tracing subscriber.
//
// Pass custom event and span filters, which will ignore anything, if the Sentry
// support has been globally disabled, or if the statement doesn't include a
// `sentry` field set to `true`.
let sentry_layer = sentry_tracing::layer()
.event_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst)
&& metadata.fields().field("sentry").is_some()
{
sentry_tracing::default_event_filter(metadata)
} else {
// Ignore the event.
sentry_tracing::EventFilter::Ignore
}
}
})
.span_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
sentry_tracing::default_span_filter(metadata)
} else {
// Ignore, if sentry is globally disabled.
false
}
}
});
(
Some(sentry_layer),
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
)
} else {
(None, None)
};
let (text_layers, reload_handle) = crate::platform::text_layers(self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(text_layers)
.with(sentry_layer)
.init();
logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
}
#[cfg(not(feature = "sentry"))]
{
let (text_layers, reload_handle) = crate::platform::text_layers(self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(text_layers)
.init();
logging_ctx = LoggingCtx { reload_handle };
}
// Log the log levels 🧠.
tracing::info!(env_filter, "Logging has been set up");
LoggingCtx { sentry: sentry_logging_ctx }
logging_ctx
}
}
@@ -505,15 +564,22 @@ pub fn init_platform(
config: TracingConfiguration,
use_lightweight_tokio_runtime: bool,
) -> Result<(), ClientError> {
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
msg: "logger already initialized".to_owned(),
details: None,
})?;
#[cfg(all(feature = "js", target_family = "wasm"))]
{
console_error_panic_hook::set_once();
}
#[cfg(not(target_family = "wasm"))]
{
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
msg: "logger already initialized".to_owned(),
details: None,
})?;
if use_lightweight_tokio_runtime {
setup_lightweight_tokio_runtime();
} else {
setup_multithreaded_tokio_runtime();
if use_lightweight_tokio_runtime {
setup_lightweight_tokio_runtime();
} else {
setup_multithreaded_tokio_runtime();
}
}
Ok(())
@@ -522,6 +588,7 @@ pub fn init_platform(
/// Set the global enablement level for the Sentry layer (after the logs have
/// been set up).
#[matrix_sdk_ffi_macros::export]
#[cfg(feature = "sentry")]
pub fn enable_sentry_logging(enabled: bool) {
if let Some(ctx) = LOGGING.get() {
if let Some(sentry_ctx) = &ctx.sentry {
@@ -535,6 +602,37 @@ pub fn enable_sentry_logging(enabled: bool) {
};
}
/// Updates the tracing subscriber with a new file writer based on the provided
/// configuration.
///
/// This method will throw if `init_platform` hasn't been called, or if it was
/// called with `write_to_files` set to `None`.
#[matrix_sdk_ffi_macros::export]
pub fn reload_tracing_file_writer(
configuration: TracingFileConfiguration,
) -> Result<(), ClientError> {
let Some(logging_context) = LOGGING.get() else {
return Err(ClientError::Generic {
msg: "Logging hasn't been initialized yet".to_owned(),
details: None,
});
};
let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
return Err(ClientError::Generic {
msg: "Logging wasn't initialized with a file config".to_owned(),
details: None,
});
};
let layer = make_file_layer(configuration);
reload_handle.reload(layer).map_err(|error| ClientError::Generic {
msg: format!("Failed to reload file config: {error}"),
details: None,
})
}
#[cfg(not(target_family = "wasm"))]
fn setup_multithreaded_tokio_runtime() {
async_compat::set_runtime_builder(Box::new(|| {
eprintln!("spawning a multithreaded tokio runtime");
@@ -545,6 +643,7 @@ fn setup_multithreaded_tokio_runtime() {
}));
}
#[cfg(not(target_family = "wasm"))]
fn setup_lightweight_tokio_runtime() {
async_compat::set_runtime_builder(Box::new(|| {
eprintln!("spawning a lightweight tokio runtime");
@@ -587,6 +686,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
};
@@ -594,25 +694,30 @@ mod tests {
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=debug,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=debug,\
matrix_sdk::sliding_sync=info,\
matrix_sdk_base::sliding_sync=info,\
matrix_sdk_ui::timeline=info,\
matrix_sdk::send_queue=info,\
matrix_sdk::event_cache=info,\
matrix_sdk_base::event_cache=info,\
matrix_sdk_sqlite::event_cache_store=info,\
matrix_sdk_common::store_locks=warn,\
matrix_sdk_base::store::ambiguity_map=warn,\
super_duper_app=error"
r#"panic=error,
hyper=warn,
matrix_sdk_ffi=info,
matrix_sdk=info,
matrix_sdk::client=trace,
matrix_sdk_crypto=debug,
matrix_sdk_crypto::olm::account=trace,
matrix_sdk::oidc=trace,
matrix_sdk::http_client=debug,
matrix_sdk::sliding_sync=info,
matrix_sdk_base::sliding_sync=info,
matrix_sdk_ui::timeline=info,
matrix_sdk::send_queue=info,
matrix_sdk::event_cache=info,
matrix_sdk_base::event_cache=info,
matrix_sdk_sqlite::event_cache_store=info,
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
super_duper_app=error"#
.split('\n')
.map(|s| s.trim())
.collect::<Vec<_>>()
.join("")
);
}
@@ -624,6 +729,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
};
@@ -631,26 +737,31 @@ mod tests {
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=trace,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=trace,\
matrix_sdk::sliding_sync=trace,\
matrix_sdk_base::sliding_sync=trace,\
matrix_sdk_ui::timeline=trace,\
matrix_sdk::send_queue=trace,\
matrix_sdk::event_cache=trace,\
matrix_sdk_base::event_cache=trace,\
matrix_sdk_sqlite::event_cache_store=trace,\
matrix_sdk_common::store_locks=warn,\
matrix_sdk_base::store::ambiguity_map=warn,\
super_duper_app=trace,\
some_other_span=trace"
r#"panic=error,
hyper=warn,
matrix_sdk_ffi=info,
matrix_sdk=info,
matrix_sdk::client=trace,
matrix_sdk_crypto=trace,
matrix_sdk_crypto::olm::account=trace,
matrix_sdk::oidc=trace,
matrix_sdk::http_client=trace,
matrix_sdk::sliding_sync=trace,
matrix_sdk_base::sliding_sync=trace,
matrix_sdk_ui::timeline=trace,
matrix_sdk::send_queue=trace,
matrix_sdk::event_cache=trace,
matrix_sdk_base::event_cache=trace,
matrix_sdk_sqlite::event_cache_store=trace,
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=trace,
super_duper_app=trace,
some_other_span=trace"#
.split('\n')
.map(|s| s.trim())
.collect::<Vec<_>>()
.join("")
);
}
@@ -662,6 +773,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
};
@@ -687,6 +799,7 @@ mod tests {
matrix_sdk_sqlite::event_cache_store=trace,
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
super_duper_app=info"#
.split('\n')
.map(|s| s.trim())
+166
View File
@@ -0,0 +1,166 @@
use std::sync::Arc;
use matrix_sdk::{
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use tracing::error;
/// Data for the QR code login mechanism.
///
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
/// decoded from a QR code.
#[derive(Debug, uniffi::Object)]
pub struct QrCodeData {
pub(crate) inner: qrcode::QrCodeData,
}
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
/// The slice of bytes would generally be returned by a QR code decoder.
#[uniffi::constructor]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
/// The server name contained within the scanned QR code data.
///
/// Note: This value is only present when scanning a QR code the belongs to
/// a logged in client. The mode where the new client shows the QR code
/// will return `None`.
pub fn server_name(&self) -> Option<String> {
match &self.inner.mode_data {
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
QrCodeModeData::Login => None,
}
}
}
/// Error type for the decoding of the [`QrCodeData`].
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum QrCodeDecodeError {
#[error("Error decoding QR code: {error:?}")]
Crypto {
#[from]
error: LoginQrCodeDecodeError,
},
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum HumanQrLoginError {
#[error("Linking with this device is not supported.")]
LinkingNotSupported,
#[error("The sign in was cancelled.")]
Cancelled,
#[error("The sign in was not completed in the required time.")]
Expired,
#[error("A secure connection could not have been established between the two devices.")]
ConnectionInsecure,
#[error("The sign in was declined.")]
Declined,
#[error("An unknown error has happened.")]
Unknown,
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
SlidingSyncNotAvailable,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
OidcMetadataInvalid,
#[error("The other device is not signed in and as such can't sign in other devices.")]
OtherDeviceNotSignedIn,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
fn from(value: qrcode::QRCodeLoginError) -> Self {
use qrcode::{QRCodeLoginError, SecureChannelError};
match value {
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
_ => HumanQrLoginError::Unknown,
},
QRCodeLoginError::OAuth(e) => {
if let Some(e) = e.as_request_token_error() {
match e {
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
_ => HumanQrLoginError::Unknown,
}
} else {
HumanQrLoginError::Unknown
}
}
QRCodeLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
},
QRCodeLoginError::UnexpectedMessage { .. }
| QRCodeLoginError::CrossProcessRefreshLock(_)
| QRCodeLoginError::DeviceKeyUpload(_)
| QRCodeLoginError::SessionTokens(_)
| QRCodeLoginError::UserIdDiscovery(_)
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
}
}
}
/// Enum describing the progress of the QR-code login.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum QrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We established a secure channel with the other device.
EstablishingSecureChannel {
/// The check code that the device should display so the other device
/// can confirm that the channel is secure as well.
check_code: u8,
/// The string representation of the check code, will be guaranteed to
/// be 2 characters long, preserving the leading zero if the
/// first digit is a zero.
check_code_string: String,
},
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::Done => Self::Done,
}
}
}
@@ -26,23 +26,22 @@ use ruma::{
avatar::ImageInfo as RumaAvatarImageInfo,
history_visibility::HistoryVisibility as RumaHistoryVisibility,
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
MediaSource,
},
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
AnyMessageLikeEventContent, AnySyncTimelineEvent,
},
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId,
};
use tracing::{error, warn};
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
use crate::{
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
event::{MessageLikeEventType, StateEventType},
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
room_info::RoomInfo,
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
@@ -55,6 +54,9 @@ use crate::{
TaskHandle,
};
mod power_levels;
pub mod room_info;
#[derive(Debug, Clone, uniffi::Enum)]
pub enum Membership {
Invited,
@@ -114,7 +116,10 @@ impl Room {
self.inner.is_direct().await.unwrap_or(false)
}
pub fn is_public(&self) -> bool {
/// Whether the room can be publicly joined or not, based on its join rule.
///
/// Can return `None` if the join rule state event is missing.
pub fn is_public(&self) -> Option<bool> {
self.inner.is_public()
}
@@ -570,21 +575,6 @@ impl Room {
Ok(())
}
pub async fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_redact_own(&user_id).await?)
}
pub async fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_redact_other(&user_id).await?)
}
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_ban(&user_id).await?)
}
pub async fn ban_user(
&self,
user_id: String,
@@ -603,16 +593,6 @@ impl Room {
Ok(self.inner.unban_user(&user_id, reason.as_deref()).await?)
}
pub async fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_invite(&user_id).await?)
}
pub async fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_kick(&user_id).await?)
}
pub async fn kick_user(
&self,
user_id: String,
@@ -622,37 +602,6 @@ impl Room {
Ok(self.inner.kick_user(&user_id, reason.as_deref()).await?)
}
pub async fn can_user_send_state(
&self,
user_id: String,
state_event: StateEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_send_state(&user_id, state_event.into()).await?)
}
pub async fn can_user_send_message(
&self,
user_id: String,
message: MessageLikeEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
}
pub async fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_pin_unpin(&user_id).await?)
}
pub async fn can_user_trigger_room_notification(
&self,
user_id: String,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_trigger_room_notification(&user_id).await?)
}
pub fn own_user_id(&self) -> String {
self.inner.own_user_id().to_string()
}
@@ -717,9 +666,9 @@ impl Room {
Ok(())
}
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
Ok(RoomPowerLevels::from(power_levels))
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
}
pub async fn apply_power_level_changes(
@@ -755,8 +704,11 @@ impl Room {
Ok(self.inner.get_suggested_user_role(&user_id).await?)
}
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
pub async fn reset_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
Ok(Arc::new(RoomPowerLevels::new(
self.inner.reset_power_levels().await?,
self.inner.own_user_id().to_owned(),
)))
}
pub async fn matrix_to_permalink(&self) -> Result<String, ClientError> {
@@ -828,18 +780,31 @@ impl Room {
/// Store the given `ComposerDraft` in the state store using the current
/// room id, as identifier.
pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<(), ClientError> {
Ok(self.inner.save_composer_draft(draft.try_into()?).await?)
pub async fn save_composer_draft(
&self,
draft: ComposerDraft,
thread_root: Option<String>,
) -> Result<(), ClientError> {
let thread_root = thread_root.map(EventId::parse).transpose()?;
Ok(self.inner.save_composer_draft(draft.try_into()?, thread_root.as_deref()).await?)
}
/// Retrieve the `ComposerDraft` stored in the state store for this room.
pub async fn load_composer_draft(&self) -> Result<Option<ComposerDraft>, ClientError> {
Ok(self.inner.load_composer_draft().await?.map(Into::into))
pub async fn load_composer_draft(
&self,
thread_root: Option<String>,
) -> Result<Option<ComposerDraft>, ClientError> {
let thread_root = thread_root.map(EventId::parse).transpose()?;
Ok(self.inner.load_composer_draft(thread_root.as_deref()).await?.map(Into::into))
}
/// Remove the `ComposerDraft` stored in the state store for this room.
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
Ok(self.inner.clear_composer_draft().await?)
pub async fn clear_composer_draft(
&self,
thread_root: Option<String>,
) -> Result<(), ClientError> {
let thread_root = thread_root.map(EventId::parse).transpose()?;
Ok(self.inner.clear_composer_draft(thread_root.as_deref()).await?)
}
/// Edit an event given its event id.
@@ -1271,54 +1236,6 @@ pub fn matrix_to_room_alias_permalink(
Ok(room_alias.matrix_to_uri().to_string())
}
#[derive(uniffi::Record)]
pub struct RoomPowerLevels {
/// The level required to ban a user.
pub ban: i64,
/// The level required to invite a user.
pub invite: i64,
/// The level required to kick a user.
pub kick: i64,
/// The level required to redact an event.
pub redact: i64,
/// The default level required to send message events.
pub events_default: i64,
/// The default level required to send state events.
pub state_default: i64,
/// The default power level for every user in the room.
pub users_default: i64,
/// The level required to change the room's name.
pub room_name: i64,
/// The level required to change the room's avatar.
pub room_avatar: i64,
/// The level required to change the room's topic.
pub room_topic: i64,
}
impl From<RumaPowerLevels> for RoomPowerLevels {
fn from(value: RumaPowerLevels) -> Self {
fn state_event_level_for(
power_levels: &RumaPowerLevels,
event_type: &TimelineEventType,
) -> i64 {
let default_state: i64 = power_levels.state_default.into();
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
}
Self {
ban: value.ban.into(),
invite: value.invite.into(),
kick: value.kick.into(),
redact: value.redact.into(),
events_default: value.events_default.into(),
state_default: value.state_default.into(),
users_default: value.users_default.into(),
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
}
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomInfoListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, room_info: RoomInfo);
@@ -0,0 +1,233 @@
use std::collections::HashMap;
use anyhow::Result;
use ruma::{
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
OwnedUserId, UserId,
};
use crate::{
error::ClientError,
event::{MessageLikeEventType, StateEventType},
};
#[derive(uniffi::Object)]
pub struct RoomPowerLevels {
inner: RumaPowerLevels,
own_user_id: OwnedUserId,
}
impl RoomPowerLevels {
pub fn new(value: RumaPowerLevels, own_user_id: OwnedUserId) -> Self {
Self { inner: value, own_user_id }
}
}
#[matrix_sdk_ffi_macros::export]
impl RoomPowerLevels {
fn values(&self) -> RoomPowerLevelsValues {
self.inner.clone().into()
}
/// Gets a map with the `UserId` of users with power levels other than `0`
/// and their power level.
pub fn user_power_levels(&self) -> HashMap<String, i64> {
let mut user_power_levels = HashMap::<String, i64>::new();
for (id, level) in self.inner.users.iter() {
user_power_levels.insert(id.to_string(), (*level).into());
}
user_power_levels
}
/// Returns true if the current user is able to ban in the room.
pub fn can_own_user_ban(&self) -> bool {
self.inner.user_can_ban(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to ban in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_ban(&user_id))
}
/// Returns true if the current user is able to redact their own messages in
/// the room.
pub fn can_own_user_redact_own(&self) -> bool {
self.inner.user_can_redact_own_event(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to redact
/// their own messages in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_redact_own_event(&user_id))
}
/// Returns true if the current user user is able to redact messages of
/// other users in the room.
pub fn can_own_user_redact_other(&self) -> bool {
self.inner.user_can_redact_event_of_other(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to redact
/// messages of other users in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_redact_event_of_other(&user_id))
}
/// Returns true if the current user is able to invite in the room.
pub fn can_own_user_invite(&self) -> bool {
self.inner.user_can_invite(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to invite in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_invite(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_invite(&user_id))
}
/// Returns true if the current user is able to kick in the room.
pub fn can_own_user_kick(&self) -> bool {
self.inner.user_can_kick(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to kick in the
/// room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_kick(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_kick(&user_id))
}
/// Returns true if the current user is able to send a specific state event
/// type in the room.
pub fn can_own_user_send_state(&self, state_event: StateEventType) -> bool {
self.inner.user_can_send_state(&self.own_user_id, state_event.into())
}
/// Returns true if the user with the given user_id is able to send a
/// specific state event type in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_send_state(
&self,
user_id: String,
state_event: StateEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_state(&user_id, state_event.into()))
}
/// Returns true if the current user is able to send a specific message type
/// in the room.
pub fn can_own_user_send_message(&self, message: MessageLikeEventType) -> bool {
self.inner.user_can_send_message(&self.own_user_id, message.into())
}
/// Returns true if the user with the given user_id is able to send a
/// specific message type in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_send_message(
&self,
user_id: String,
message: MessageLikeEventType,
) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_message(&user_id, message.into()))
}
/// Returns true if the current user is able to pin or unpin events in the
/// room.
pub fn can_own_user_pin_unpin(&self) -> bool {
self.inner.user_can_send_state(&self.own_user_id, StateEventType::RoomPinnedEvents.into())
}
/// Returns true if the user with the given user_id is able to pin or unpin
/// events in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_send_state(&user_id, StateEventType::RoomPinnedEvents.into()))
}
/// Returns true if the current user is able to trigger a notification in
/// the room.
pub fn can_own_user_trigger_room_notification(&self) -> bool {
self.inner.user_can_trigger_room_notification(&self.own_user_id)
}
/// Returns true if the user with the given user_id is able to trigger a
/// notification in the room.
///
/// The call may fail if there is an error in getting the power levels.
pub fn can_user_trigger_room_notification(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.user_can_trigger_room_notification(&user_id))
}
}
/// This intermediary struct is used to expose the power levels values through
/// FFI and work around it not exposing public exported object fields.
#[derive(uniffi::Record)]
pub struct RoomPowerLevelsValues {
/// The level required to ban a user.
pub ban: i64,
/// The level required to invite a user.
pub invite: i64,
/// The level required to kick a user.
pub kick: i64,
/// The level required to redact an event.
pub redact: i64,
/// The default level required to send message events.
pub events_default: i64,
/// The default level required to send state events.
pub state_default: i64,
/// The default power level for every user in the room.
pub users_default: i64,
/// The level required to change the room's name.
pub room_name: i64,
/// The level required to change the room's avatar.
pub room_avatar: i64,
/// The level required to change the room's topic.
pub room_topic: i64,
}
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
fn from(value: RumaPowerLevels) -> Self {
fn state_event_level_for(
power_levels: &RumaPowerLevels,
event_type: &TimelineEventType,
) -> i64 {
let default_state: i64 = power_levels.state_default.into();
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
}
Self {
ban: value.ban.into(),
invite: value.invite.into(),
kick: value.kick.into(),
redact: value.redact.into(),
events_default: value.events_default.into(),
state_default: value.state_default.into(),
users_default: value.users_default.into(),
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
}
}
}
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;
use matrix_sdk::{EncryptionState, RoomState};
use tracing::warn;
@@ -7,7 +7,9 @@ use crate::{
client::JoinRule,
error::ClientError,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom},
room::{
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
},
room_member::RoomMember,
};
@@ -24,7 +26,11 @@ pub struct RoomInfo {
topic: Option<String>,
avatar_url: Option<String>,
is_direct: bool,
is_public: bool,
/// Whether the room is public or not, based on the join rules.
///
/// Can be `None` if the join rules state event is not available for this
/// room.
is_public: Option<bool>,
is_space: bool,
/// If present, it means the room has been archived/upgraded.
successor_room: Option<SuccessorRoom>,
@@ -42,7 +48,6 @@ pub struct RoomInfo {
active_members_count: u64,
invited_members_count: u64,
joined_members_count: u64,
user_power_levels: HashMap<String, i64>,
highlight_count: u64,
notification_count: u64,
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
@@ -65,24 +70,34 @@ pub struct RoomInfo {
join_rule: Option<JoinRule>,
/// The history visibility for this room, if known.
history_visibility: RoomHistoryVisibility,
/// This room's current power levels.
///
/// Can be missing if the room power levels event is missing from the store.
power_levels: Option<Arc<RoomPowerLevels>>,
}
impl RoomInfo {
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
let unread_notification_counts = room.unread_notification_counts();
let power_levels_map = room.users_with_power_levels().await;
let mut user_power_levels = HashMap::<String, i64>::new();
for (id, level) in power_levels_map.iter() {
user_power_levels.insert(id.to_string(), *level);
}
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:?}");
}
let join_rule = room
.join_rule()
.map(TryInto::try_into)
.transpose()
.inspect_err(|err| {
warn!("Failed to parse join rule: {err}");
})
.ok()
.flatten();
let power_levels = room
.power_levels()
.await
.ok()
.map(|p| RoomPowerLevels::new(p, room.own_user_id().to_owned()));
Ok(Self {
id: room.room_id().to_string(),
@@ -116,7 +131,6 @@ impl RoomInfo {
active_members_count: room.active_members_count(),
invited_members_count: room.invited_members_count(),
joined_members_count: room.joined_members_count(),
user_power_levels,
highlight_count: unread_notification_counts.highlight_count,
notification_count: unread_notification_counts.notification_count,
cached_user_defined_notification_mode: room
@@ -133,8 +147,9 @@ impl RoomInfo {
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
join_rule: join_rule.ok(),
join_rule,
history_visibility: room.history_visibility_or_default().try_into()?,
power_levels: power_levels.map(Arc::new),
})
}
}
+8 -3
View File
@@ -42,7 +42,10 @@ pub enum RoomListError {
InvalidRoomId { error: String },
#[error("Event cache ran into an error: {error}")]
EventCache { error: String },
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
#[error(
"The requested room doesn't match the membership requirements {expected:?}, \
observed {actual:?}"
)]
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
}
@@ -118,7 +121,7 @@ impl RoomListService {
})))
}
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
let room_ids = room_ids
.into_iter()
.map(|room_id| {
@@ -126,7 +129,9 @@ impl RoomListService {
})
.collect::<Result<Vec<_>, _>>()?;
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
self.inner
.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
.await;
Ok(())
}
+6 -5
View File
@@ -37,8 +37,9 @@ impl RoomPreview {
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.clone()
.try_into()
.as_ref()
.map(TryInto::try_into)
.transpose()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
heroes: info
@@ -114,17 +115,17 @@ pub struct RoomPreviewInfo {
/// 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,
pub join_rule: Option<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 {
impl TryFrom<&SpaceRoomJoinRule> for JoinRule {
type Error = ();
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
fn try_from(join_rule: &SpaceRoomJoinRule) -> Result<Self, ()> {
Ok(match join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
+1 -1
View File
@@ -39,7 +39,7 @@ mod sys {
mod sys {
use std::future::Future;
use crate::executor::{spawn, JoinHandle};
use matrix_sdk_common::executor::{spawn, JoinHandle};
/// A dummy guard that does nothing when dropped.
/// This is used for the Wasm implementation to match
@@ -116,11 +116,8 @@ impl SessionVerificationController {
/// Request verification for the current device
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
.user_identity
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
let verification_request =
self.user_identity.request_verification_with_methods(methods).await?;
self.set_ongoing_verification_request(verification_request)
}
@@ -141,10 +138,7 @@ impl SessionVerificationController {
let methods = vec![VerificationMethod::SasV1];
let verification_request = user_identity
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
let verification_request = user_identity.request_verification_with_methods(methods).await?;
self.set_ongoing_verification_request(verification_request)
}
@@ -241,7 +235,10 @@ impl SessionVerificationController {
if sender != self.user_identity.user_id() {
if let Some(status) = self.encryption.cross_signing_status().await {
if !status.is_complete() {
warn!("Cannot verify other users until our own device's cross-signing status is complete: {status:?}");
warn!(
"Cannot verify other users until our own device's cross-signing status \
is complete: {status:?}"
);
return;
}
}
@@ -119,6 +119,12 @@ impl SyncServiceBuilder {
Arc::new(Self { builder, ..this })
}
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_share_pos(enable);
Arc::new(Self { builder, ..this })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService {
+1 -1
View File
@@ -1,4 +1,4 @@
use tokio::task::JoinHandle;
use matrix_sdk_common::executor::JoinHandle;
use tracing::debug;
/// A task handle is a way to keep the handle a task running by itself in
@@ -79,7 +79,6 @@ pub enum TimelineFocus {
Thread {
/// The thread root event ID to focus on.
root_event_id: String,
num_events: u16,
},
PinnedEvents {
max_events_to_load: u16,
@@ -108,7 +107,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
hide_threaded_events,
})
}
TimelineFocus::Thread { root_event_id, num_events } => {
TimelineFocus::Thread { root_event_id } => {
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
FocusEventError::InvalidEventId {
event_id: root_event_id.clone(),
@@ -116,7 +115,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
}
})?;
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
Ok(Self::Thread { root_event_id: parsed_root_event_id })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
+37 -24
View File
@@ -17,7 +17,7 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt as _};
use futures_util::pin_mut;
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
@@ -30,6 +30,10 @@ use matrix_sdk::{
reply::{EnforceThread, Reply},
},
};
use matrix_sdk_common::{
executor::{AbortHandle, JoinHandle},
stream::StreamExt,
};
use matrix_sdk_ui::timeline::{
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
TimelineUniqueId as SdkTimelineUniqueId,
@@ -47,7 +51,6 @@ use ruma::{
UnstablePollStartContentBlock,
},
},
receipt::ReceiptThread,
room::message::{
LocationMessageEventContent, MessageType, ReplyWithinThread,
RoomMessageEventContentWithoutRelation,
@@ -56,10 +59,7 @@ use ruma::{
},
EventId, UInt,
};
use tokio::{
sync::Mutex,
task::{AbortHandle, JoinHandle},
};
use tokio::sync::Mutex;
use tracing::{error, warn};
use uuid::Uuid;
@@ -350,9 +350,7 @@ impl Timeline {
event_id: String,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
self.inner
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
.await?;
self.inner.send_single_receipt(receipt_type.into(), event_id).await?;
Ok(())
}
@@ -380,7 +378,7 @@ impl Timeline {
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
Err(err) => {
error!("error when sending a message: {err}");
Err(anyhow::anyhow!(err).into())
Err(err.into())
}
}
}
@@ -533,10 +531,7 @@ impl Timeline {
msg: Arc<RoomMessageEventContentWithoutRelation>,
reply_params: ReplyParameters,
) -> Result<(), ClientError> {
self.inner
.send_reply((*msg).clone(), reply_params.try_into()?)
.await
.map_err(|err| anyhow::anyhow!(err))?;
self.inner.send_reply((*msg).clone(), reply_params.try_into()?).await?;
Ok(())
}
@@ -566,7 +561,10 @@ impl Timeline {
let event_id = match event_or_transaction_id {
EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
EventOrTransactionId::TransactionId { .. } => {
warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting");
warn!(
"trying to apply an edit to a local echo that doesn't exist \
in this timeline, aborting"
);
return Ok(());
}
};
@@ -587,7 +585,8 @@ impl Timeline {
description: Option<String>,
zoom_level: Option<u8>,
asset_type: Option<AssetType>,
) {
reply_params: Option<ReplyParameters>,
) -> Result<(), ClientError> {
let mut location_event_message_content =
LocationMessageEventContent::new(body, geo_uri.clone());
@@ -604,8 +603,13 @@ impl Timeline {
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
MessageType::Location(location_event_message_content),
);
// Errors are logged in `Self::send` already.
let _ = self.send(Arc::new(room_message_event_content)).await;
if let Some(reply_params) = reply_params {
self.send_reply(Arc::new(room_message_event_content), reply_params).await
} else {
self.send(Arc::new(room_message_event_content)).await?;
Ok(())
}
}
/// Toggle a reaction on an event.
@@ -630,7 +634,10 @@ impl Timeline {
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
let event_id = <&EventId>::try_from(event_id.as_str())?;
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
self.inner
.fetch_details_for_event(event_id)
.await
.map_err(|e| ClientError::from_str(e, Some("Fetching event details".to_owned())))?;
Ok(())
}
@@ -694,6 +701,8 @@ impl Timeline {
content: replied_to.content.clone().into(),
sender: replied_to.sender.to_string(),
sender_profile: replied_to.sender_profile.into(),
timestamp: replied_to.timestamp.into(),
event_or_transaction_id: replied_to.identifier.into(),
},
))),
@@ -1229,7 +1238,10 @@ impl SendAttachmentJoinHandle {
return Ok(());
}
error!("task panicked! resuming panic from here.");
#[cfg(not(target_family = "wasm"))]
panic::resume_unwind(err.into_panic());
#[cfg(target_family = "wasm")]
panic!("task panicked! {err}");
}
}
}
@@ -1373,24 +1385,22 @@ impl LazyTimelineItemProvider {
mod galleries {
use std::{panic, sync::Arc};
use async_compat::get_runtime_handle;
use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
utils::formatted_body_from,
};
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use tokio::{
sync::Mutex,
task::{AbortHandle, JoinHandle},
};
use tokio::sync::Mutex;
use tracing::error;
use crate::{
error::RoomError,
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
runtime::get_runtime_handle,
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
};
@@ -1560,7 +1570,10 @@ mod galleries {
return Ok(());
}
error!("task panicked! resuming panic from here.");
#[cfg(not(target_family = "wasm"))]
panic::resume_unwind(err.into_panic());
#[cfg(target_family = "wasm")]
panic!("task panicked! {err}");
}
}
}
@@ -241,7 +241,7 @@ pub struct PollAnswer {
#[derive(Clone, uniffi::Object)]
pub struct ThreadSummary {
pub latest_event: EmbeddedEventDetails,
pub num_replies: usize,
pub num_replies: u32,
}
#[matrix_sdk_ffi_macros::export]
@@ -249,6 +249,10 @@ impl ThreadSummary {
pub fn latest_event(&self) -> EmbeddedEventDetails {
self.latest_event.clone()
}
pub fn num_replies(&self) -> u64 {
self.num_replies as u64
}
}
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
+13 -2
View File
@@ -15,6 +15,7 @@
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
use crate::{event::EventOrTransactionId, utils::Timestamp};
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
@@ -50,8 +51,16 @@ impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
pub enum EmbeddedEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
Ready {
content: TimelineItemContent,
sender: String,
sender_profile: ProfileDetails,
timestamp: Timestamp,
event_or_transaction_id: EventOrTransactionId,
},
Error {
message: String,
},
}
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
@@ -63,6 +72,8 @@ impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
content: event.content.into(),
sender: event.sender.to_string(),
sender_profile: event.sender_profile.into(),
timestamp: event.timestamp.into(),
event_or_transaction_id: event.identifier.into(),
},
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
}
+1 -1
View File
@@ -10,7 +10,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// Log an event.
///
/// The target should be something like a module path, and can be referenced in
/// the filter string given to `setup_tracing`. `level` and `target` for a
/// the filter string given to `init_platform`. `level` and `target` for a
/// callsite are fixed at the first `log_event` call for that callsite and can
/// not be changed afterwards, i.e. the level and target passed for second and
/// following `log_event`s with the same callsite will be ignored.
+15 -179
View File
@@ -1,10 +1,7 @@
use std::sync::{Arc, Mutex};
use language_tags::LanguageTag;
use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter},
};
use matrix_sdk::widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::events::MessageLikeEventType;
use tracing::error;
@@ -113,174 +110,6 @@ pub async fn generate_webview_url(
.map(|url| url.to_string())?)
}
/// Defines if a call is encrypted and which encryption system should be used.
///
/// This controls the url parameters: `perParticipantE2EE`, `password`.
#[derive(uniffi::Enum, Clone)]
pub enum EncryptionSystem {
/// Equivalent to the element call url parameter: `enableE2EE=false`
Unencrypted,
/// Equivalent to the element call url parameter:
/// `perParticipantE2EE=true`
PerParticipantKeys,
/// Equivalent to the element call url parameter:
/// `password={secret}`
SharedSecret {
/// The secret/password which is used in the url.
secret: String,
},
}
impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
fn from(value: EncryptionSystem) -> Self {
match value {
EncryptionSystem::Unencrypted => Self::Unencrypted,
EncryptionSystem::PerParticipantKeys => Self::PerParticipantKeys,
EncryptionSystem::SharedSecret { secret } => Self::SharedSecret { secret },
}
}
}
/// Defines the intent of showing the call.
///
/// This controls whether to show or skip the lobby.
#[derive(uniffi::Enum, Clone)]
pub enum Intent {
/// The user wants to start a call.
StartCall,
/// The user wants to join an existing call.
JoinExisting,
}
impl From<Intent> for matrix_sdk::widget::Intent {
fn from(value: Intent) -> Self {
match value {
Intent::StartCall => Self::StartCall,
Intent::JoinExisting => Self::JoinExisting,
}
}
}
/// Properties to create a new virtual Element Call widget.
#[derive(uniffi::Record, Clone)]
pub struct VirtualElementCallWidgetOptions {
/// The url to the Element Call app including any `/room` path if required.
///
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
pub element_call_url: String,
/// The widget id.
pub widget_id: String,
/// The url that is used as the target for the PostMessages sent
/// by the widget (to the client).
///
/// For a web app client this is the client url. In case of using other
/// platforms the client most likely is setup up to listen to
/// postmessages in the same webview the widget is hosted. In this case
/// the `parent_url` is set to the url of the webview with the widget. Be
/// aware that this means that the widget will receive its own postmessage
/// messages. The `matrix-widget-api` (js) ignores those so this works but
/// it might break custom implementations.
///
/// Defaults to `element_call_url` for the non-iframe (dedicated webview)
/// usecase.
pub parent_url: Option<String>,
/// Whether the branding header of Element call should be hidden.
///
/// Default: `true`
pub hide_header: Option<bool>,
/// If set, the lobby will be skipped and the widget will join the
/// call on the `io.element.join` action.
///
/// Default: `false`
pub preload: Option<bool>,
/// The font scale which will be used inside element call.
///
/// Default: `1`
pub font_scale: Option<f64>,
/// Whether element call should prompt the user to open in the browser or
/// the app.
///
/// Default: `false`
pub app_prompt: Option<bool>,
/// Make it not possible to get to the calls list in the webview.
///
/// Default: `true`
pub confine_to_room: Option<bool>,
/// The font to use, to adapt to the system font.
pub font: Option<String>,
/// The encryption system to use.
///
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
pub encryption: EncryptionSystem,
/// The intent of showing the call.
/// If the user wants to start a call or join an existing one.
/// Controls if the lobby is skipped or not.
pub intent: Option<Intent>,
/// Do not show the screenshare button.
pub hide_screensharing: bool,
/// Can be used to pass a PostHog id to element call.
pub posthog_user_id: Option<String>,
/// The host of the posthog api.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub posthog_api_host: Option<String>,
/// The key for the posthog api.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub posthog_api_key: Option<String>,
/// The url to use for submitting rageshakes.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub rageshake_submit_url: Option<String>,
/// Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub sentry_dsn: Option<String>,
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub sentry_environment: Option<String>,
//// - `true`: The webview should show the list of media devices it detects using
//// `enumerateDevices`.
/// - `false`: the webview shows a a list of devices injected by the
/// client. (used on ios & android)
pub controlled_media_devices: bool,
}
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
fn from(value: VirtualElementCallWidgetOptions) -> Self {
Self {
element_call_url: value.element_call_url,
widget_id: value.widget_id,
parent_url: value.parent_url,
hide_header: value.hide_header,
preload: value.preload,
font_scale: value.font_scale,
app_prompt: value.app_prompt,
confine_to_room: value.confine_to_room,
font: value.font,
posthog_user_id: value.posthog_user_id,
encryption: value.encryption.into(),
intent: value.intent.map(Into::into),
hide_screensharing: value.hide_screensharing,
posthog_api_host: value.posthog_api_host,
posthog_api_key: value.posthog_api_key,
rageshake_submit_url: value.rageshake_submit_url,
sentry_dsn: value.sentry_dsn,
sentry_environment: value.sentry_environment,
controlled_media_devices: value.controlled_media_devices,
}
}
}
/// `WidgetSettings` are usually created from a state event.
/// (currently unimplemented)
///
@@ -296,9 +125,9 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
/// call widget.
#[matrix_sdk_ffi_macros::export]
pub fn new_virtual_element_call_widget(
props: VirtualElementCallWidgetOptions,
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
) -> Result<WidgetSettings, ParseError> {
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props.into())
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
.map(|w| w.into())?)
}
@@ -352,6 +181,8 @@ pub fn get_element_call_required_permissions(
read: vec![
// To compute the current state of the matrixRTC session.
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
// To display the name of the room.
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomName.to_string() },
// To detect leaving/kicked room members during a call.
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
// To decide whether to encrypt the call streams based on the room encryption setting.
@@ -553,8 +384,6 @@ pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
async fn acquire_capabilities(
&self,
@@ -656,14 +485,21 @@ mod tests {
cap_assert("org.matrix.msc4157.update_delayed_event");
cap_assert("org.matrix.msc4157.send.delayed_event");
cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.name");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org",
);
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
);
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
);
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+24
View File
@@ -6,6 +6,30 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.13.0] - 2025-07-10
### Features
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
`BaseClient::room_joined()` method was called. A new getter for this
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
timestamp.
([#5333](https://github.com/matrix-org/matrix-rust-sdk/pull/5333))
- [**breaking**] The `BaseClient::new()` method now takes an additional `ThreadingSupport`
parameter controlling whether the client is supposed to do extra processing for threads. Right
now, it controls whether to exclude in-thread events from the room unread counts, but it may be
expanded in the future to support more threading-related features.
([#5325](https://github.com/matrix-org/matrix-rust-sdk/pull/5325))
### Refactor
- The cached `ServerCapabilities` has been renamed to `ServerInfo` and
additionally contains the well-known response alongside the existing server versions.
Despite the old name, it does not contain the server capabilities.
([#5167](https://github.com/matrix-org/matrix-rust-sdk/pull/5167))
- `Room::join_rule` and `Room::is_public` now return an `Option` to reflect that the join rule
state event might be missing, in which case they will return `None`.
([#5278](https://github.com/matrix-org/matrix-rust-sdk/pull/5278))
## [0.12.0] - 2025-06-10
No notable changes in this release.
+2 -1
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.12.0"
version = "0.13.0"
[package.metadata.docs.rs]
all-features = true
@@ -86,6 +86,7 @@ unicode-normalization.workspace = true
uniffi = { workspace = true, optional = true }
[dev-dependencies]
anyhow.workspace = true
assert_matches.workspace = true
assert_matches2.workspace = true
assign = "1.1.1"
+164 -26
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, types::requests::ToDeviceRequest, CollectStrategy, EncryptionSettings,
OlmError, OlmMachine, TrustRequirement,
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
EncryptionSettings, OlmError, OlmMachine, TrustRequirement,
};
#[cfg(feature = "e2e-encryption")]
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
@@ -76,11 +76,12 @@ use crate::{
/// rather through `matrix_sdk::Client`.
///
/// ```rust
/// use matrix_sdk_base::{store::StoreConfig, BaseClient};
/// use matrix_sdk_base::{store::StoreConfig, BaseClient, ThreadingSupport};
///
/// let client = BaseClient::new(StoreConfig::new(
/// "cross-process-holder-name".to_owned(),
/// ));
/// let client = BaseClient::new(
/// StoreConfig::new("cross-process-holder-name".to_owned()),
/// ThreadingSupport::Disabled,
/// );
/// ```
#[derive(Clone)]
pub struct BaseClient {
@@ -115,13 +116,16 @@ pub struct BaseClient {
#[cfg(feature = "e2e-encryption")]
pub room_key_recipient_strategy: CollectStrategy,
/// The trust requirement to use for decrypting events.
/// The settings to use for decrypting events.
#[cfg(feature = "e2e-encryption")]
pub decryption_trust_requirement: TrustRequirement,
pub decryption_settings: DecryptionSettings,
/// If the client should handle verification events received when syncing.
#[cfg(feature = "e2e-encryption")]
pub handle_verification_events: bool,
/// Whether the client supports threads or not.
pub threading_support: ThreadingSupport,
}
#[cfg(not(tarpaulin_include))]
@@ -134,6 +138,25 @@ impl fmt::Debug for BaseClient {
}
}
/// Whether this client instance supports threading or not. Currently used to
/// determine how the client handles read receipts and unread count computations
/// on the base SDK level.
///
/// Timelines on the other hand have a separate `TimelineFocus`
/// `hide_threaded_events` associated value that can be used to hide threaded
/// events but also to enable threaded read receipt sending. This is because
/// certain timeline instances should ignore threading no matter what's defined
/// at the client level. One such example are media filtered timelines which
/// should contain all the room's media no matter what thread its in (unless
/// explicitly opted into).
#[derive(Clone, Copy, Debug)]
pub enum ThreadingSupport {
/// Threading enabled
Enabled,
/// Threading disabled
Disabled,
}
impl BaseClient {
/// Create a new client.
///
@@ -141,7 +164,7 @@ impl BaseClient {
///
/// * `config` - the configuration for the stores (state store, event cache
/// store and crypto store).
pub fn new(config: StoreConfig) -> Self {
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
let store = BaseStateStore::new(config.state_store);
// Create the channel to receive `RoomInfoNotableUpdate`.
@@ -168,9 +191,12 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
room_key_recipient_strategy: Default::default(),
#[cfg(feature = "e2e-encryption")]
decryption_trust_requirement: TrustRequirement::Untrusted,
decryption_settings: DecryptionSettings {
sender_device_trust_requirement: TrustRequirement::Untrusted,
},
#[cfg(feature = "e2e-encryption")]
handle_verification_events: true,
threading_support,
}
}
@@ -200,8 +226,9 @@ impl BaseClient {
ignore_user_list_changes: Default::default(),
room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
decryption_trust_requirement: self.decryption_trust_requirement,
decryption_settings: self.decryption_settings.clone(),
handle_verification_events,
threading_support: self.threading_support,
};
copy.state_store
@@ -222,7 +249,7 @@ impl BaseClient {
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
.state_store(MemoryStore::new());
Ok(Self::new(config))
Ok(Self::new(config, ThreadingSupport::Disabled))
}
/// Get the session meta information.
@@ -392,9 +419,33 @@ impl BaseClient {
Ok(room)
}
/// User has joined a room.
/// The user has joined a room using this specific client.
///
/// This method should be called if the user accepts an invite or if they
/// join a public room.
///
/// The method will create a [`Room`] object if one does not exist yet and
/// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`]
/// object will be persisted in the cache. Please note that the [`Room`]
/// will be a stub until a sync has been received with the full room
/// state using [`BaseClient::receive_sync_response`].
///
/// Update the internal and cached state accordingly. Return the final Room.
///
/// # Examples
///
/// ```rust
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
/// # use ruma::OwnedRoomId;
/// # async {
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
/// let room_id = send_join_request().await?;
/// let room = client.room_joined(&room_id).await?;
///
/// assert_eq!(room.state(), RoomState::Joined);
/// # anyhow::Ok(()) };
/// ```
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
let room = self.state_store.get_or_create_room(
room_id,
@@ -402,16 +453,36 @@ impl BaseClient {
self.room_info_notable_update_sender.clone(),
);
// If the state isn't `RoomState::Joined` then this means that we knew about
// this room before. Let's modify the existing state now.
if room.state() != RoomState::Joined {
let _sync_lock = self.sync_lock().lock().await;
let mut room_info = room.clone_info();
// If our previous state was an invite and we're now in the joined state, this
// means that the user has explicitly accepted the invite. Let's
// remember when this has happened.
//
// This is somewhat of a workaround for our lack of cryptographic membership.
// Later on we will decide if historic room keys should be accepted
// based on this info. If a user has accepted an invite and we receive a room
// key bundle shortly after, we might accept it. If we don't do
// this, the homeserver could trick us into accepting any historic room key
// bundle.
if room.state() == RoomState::Invited {
room_info.set_invite_accepted_now();
}
room_info.mark_as_joined();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.state_store.save_changes(&changes).await?; // Update the store
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
@@ -496,7 +567,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
let to_device = {
let processors::e2ee::to_device::Output {
decrypted_to_device_events: to_device,
processed_to_device_events: to_device,
room_key_updates,
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
@@ -509,7 +580,7 @@ impl BaseClient {
.collect(),
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
&self.decryption_settings,
self.handle_verification_events,
),
)
@@ -519,7 +590,22 @@ impl BaseClient {
};
#[cfg(not(feature = "e2e-encryption"))]
let to_device = response.to_device.events;
let to_device = response
.to_device
.events
.into_iter()
.map(|raw| {
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
}
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
}
})
.collect();
let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
@@ -553,7 +639,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
&self.decryption_settings,
self.handle_verification_events,
),
)
@@ -580,7 +666,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
&self.decryption_settings,
self.handle_verification_events,
),
)
@@ -1063,6 +1149,7 @@ mod tests {
use super::{BaseClient, RequestedRequiredStates};
use crate::{
client::ThreadingSupport,
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
test_utils::logged_in_base_client,
RoomDisplayName, RoomState, SessionMeta,
@@ -1357,8 +1444,10 @@ mod tests {
let user_id = user_id!("@alice:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1417,8 +1506,10 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1479,8 +1570,10 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1551,8 +1644,10 @@ mod tests {
#[async_test]
async fn test_ignored_user_list_changes() {
let user_id = user_id!("@alice:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
@@ -1642,4 +1737,47 @@ mod tests {
assert!(client.is_user_ignored(ignored_user_id).await);
}
#[async_test]
async fn test_joined_at_timestamp_is_set() {
let client = logged_in_base_client(None).await;
let invited_room_id = room_id!("!invited:localhost");
let unknown_room_id = room_id!("!unknown:localhost");
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
// Let us first check the initial state, we should have a room in the invite
// state.
let invited_room = client
.get_room(invited_room_id)
.expect("The sync should have created a room in the invited state");
assert_eq!(invited_room.state(), RoomState::Invited);
assert!(invited_room.inner.get().invite_accepted_at().is_none());
// Now we join the room.
let joined_room = client
.room_joined(invited_room_id)
.await
.expect("We should be able to mark a room as joined");
// Yup, there's a timestamp now.
assert_eq!(joined_room.state(), RoomState::Joined);
assert!(joined_room.inner.get().invite_accepted_at().is_some());
// If we didn't know about the room before the join, we assume that there wasn't
// an invite and we don't record the timestamp.
assert!(client.get_room(unknown_room_id).is_none());
let unknown_room = client
.room_joined(unknown_room_id)
.await
.expect("We should be able to mark a room as joined");
assert_eq!(unknown_room.state(), RoomState::Joined);
assert!(unknown_room.inner.get().invite_accepted_at().is_none());
}
}
+14
View File
@@ -17,6 +17,7 @@
use std::fmt;
pub use matrix_sdk_common::debug::*;
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use ruma::{
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
serde::Raw,
@@ -35,6 +36,19 @@ impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
}
}
/// A wrapper around a slice of `ProcessedToDeviceEvent` events that implements
/// `Debug` in a way that only prints the event type of each item.
pub struct DebugListOfProcessedToDeviceEvents<'a>(pub &'a [ProcessedToDeviceEvent]);
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for DebugListOfProcessedToDeviceEvents<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(|e| DebugRawEventNoId(e.as_raw())));
list.finish()
}
}
/// A wrapper around an invited room as found in `/sync` responses that
/// implements `Debug` in a way that only prints the event ID and event type for
/// the raw events contained in `invite_state`.
@@ -14,7 +14,7 @@
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use std::sync::Arc;
use std::{collections::BTreeMap, sync::Arc};
use assert_matches::assert_matches;
use matrix_sdk_common::{
@@ -133,6 +133,9 @@ pub trait EventCacheStoreIntegrationTests {
/// anything.
async fn test_rebuild_empty_linked_chunk(&self);
/// Test that loading a linked chunk's metadata works as intended.
async fn test_load_all_chunks_metadata(&self);
/// Test that clear all the rooms' linked chunks works.
async fn test_clear_all_linked_chunks(&self);
@@ -417,6 +420,72 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert!(chunks.next().is_none());
}
async fn test_load_all_chunks_metadata(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
self.handle_linked_chunk_updates(
linked_chunk_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 2
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(room_id, "sup")],
},
// and an empty items chunk to finish
Update::NewItemsChunk { previous: Some(CId::new(2)), new: CId::new(3), next: None },
],
)
.await
.unwrap();
let metas = self.load_all_chunks_metadata(linked_chunk_id).await.unwrap();
assert_eq!(metas.len(), 4);
// The first chunk has two items.
assert_eq!(metas[0].identifier, CId::new(0));
assert_eq!(metas[0].previous, None);
assert_eq!(metas[0].next, Some(CId::new(1)));
assert_eq!(metas[0].num_items, 2);
// The second chunk is a gap, so it has 0 items.
assert_eq!(metas[1].identifier, CId::new(1));
assert_eq!(metas[1].previous, Some(CId::new(0)));
assert_eq!(metas[1].next, Some(CId::new(2)));
assert_eq!(metas[1].num_items, 0);
// The third event chunk has one item.
assert_eq!(metas[2].identifier, CId::new(2));
assert_eq!(metas[2].previous, Some(CId::new(1)));
assert_eq!(metas[2].next, Some(CId::new(3)));
assert_eq!(metas[2].num_items, 1);
// The final event chunk is empty.
assert_eq!(metas[3].identifier, CId::new(3));
assert_eq!(metas[3].previous, Some(CId::new(2)));
assert_eq!(metas[3].next, None);
assert_eq!(metas[3].num_items, 0);
}
async fn test_linked_chunk_incremental_loading(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
@@ -822,8 +891,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.await
.unwrap();
let duplicated_events = self
.filter_duplicated_events(
let duplicated_events = BTreeMap::from_iter(
self.filter_duplicated_events(
linked_chunk_id,
vec![
event_comte.event_id().unwrap().to_owned(),
@@ -835,20 +904,22 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
],
)
.await
.unwrap();
.unwrap(),
);
assert_eq!(duplicated_events.len(), 3);
assert_eq!(
duplicated_events[0],
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
*duplicated_events.get(&event_comte.event_id().unwrap()).unwrap(),
Position::new(CId::new(0), 0)
);
assert_eq!(
duplicated_events[1],
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
*duplicated_events.get(&event_morbier.event_id().unwrap()).unwrap(),
Position::new(CId::new(2), 0)
);
assert_eq!(
duplicated_events[2],
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
*duplicated_events.get(&event_mont_dor.event_id().unwrap()).unwrap(),
Position::new(CId::new(2), 1)
);
}
@@ -949,7 +1020,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Save All The Things!
self.save_event(room_id, e1).await.unwrap();
self.save_event(room_id, edit_e1).await.unwrap();
self.save_event(room_id, reaction_e1).await.unwrap();
self.save_event(room_id, reaction_e1.clone()).await.unwrap();
self.save_event(room_id, e2).await.unwrap();
self.save_event(another_room_id, e3).await.unwrap();
self.save_event(another_room_id, reaction_e3).await.unwrap();
@@ -957,8 +1028,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Finding relations without a filter returns all of them.
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
// The position is `None` for items outside the linked chunk.
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none()));
// Finding relations with a filter only returns a subset.
let relations = self
@@ -966,7 +1042,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.await
.unwrap();
assert_eq!(relations.len(), 1);
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
assert_eq!(relations[0].0.event_id().as_deref(), Some(edit_eid1));
let relations = self
.find_event_relations(
@@ -977,8 +1053,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.await
.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.0.event_id().as_deref() == Some(reaction_eid1)));
// We can't find relations using the wrong room.
let relations = self
@@ -986,6 +1062,35 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.await
.unwrap();
assert!(relations.is_empty());
// But if an event exists in the linked chunk, we may have its position when
// it's found as a relationship.
// Add reaction_e1 to the room's linked chunk.
self.handle_linked_chunk_updates(
LinkedChunkId::Room(room_id),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec![reaction_e1] },
],
)
.await
.unwrap();
// When looking for aggregations to e1, we should have the position for
// reaction_e1.
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
// The position is set for `reaction_eid1` now.
assert!(relations.iter().any(|(ev, pos)| {
ev.event_id().as_deref() == Some(reaction_eid1)
&& *pos == Some(Position::new(CId::new(0), 0))
}));
// But it's still not set for the other related events.
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
}
async fn test_save_event(&self) {
@@ -1105,6 +1210,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_rebuild_empty_linked_chunk().await;
}
#[async_test]
async fn test_load_all_chunks_metadata() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_load_all_chunks_metadata().await;
}
#[async_test]
async fn test_clear_all_linked_chunks() {
let event_cache_store =
@@ -22,7 +22,7 @@ use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
LinkedChunkId, Position, RawChunk, Update,
ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, RawChunk, Update,
},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
@@ -148,6 +148,17 @@ impl EventCacheStore for MemoryStore {
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_all_chunks_metadata(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<ChunkMetadata>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_all_chunks_metadata(linked_chunk_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_last_chunk(
&self,
linked_chunk_id: LinkedChunkId<'_>,
@@ -181,17 +192,17 @@ impl EventCacheStore for MemoryStore {
linked_chunk_id: LinkedChunkId<'_>,
mut events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
// Collect all duplicated events.
if events.is_empty() {
return Ok(Vec::new());
}
let inner = self.inner.read().unwrap();
let mut duplicated_events = Vec::new();
for (event, position) in inner.events.unordered_linked_chunk_items(linked_chunk_id) {
// If `events` is empty, we can short-circuit.
if events.is_empty() {
break;
}
for (event, position) in
inner.events.unordered_linked_chunk_items(&linked_chunk_id.to_owned())
{
if let Some(known_event_id) = event.event_id() {
// This event is a duplicate!
if let Some(index) =
@@ -212,10 +223,12 @@ impl EventCacheStore for MemoryStore {
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event = inner.events.items().find_map(|(event, this_linked_chunk_id)| {
(room_id == this_linked_chunk_id.room_id() && event.event_id()? == event_id)
.then_some(event.clone())
});
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let event = inner
.events
.items(&target_linked_chunk_id)
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
Ok(event)
}
@@ -225,20 +238,17 @@ impl EventCacheStore for MemoryStore {
room_id: &RoomId,
event_id: &EventId,
filters: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error> {
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
let inner = self.inner.read().unwrap();
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let filters = compute_filters_string(filters);
let related_events = inner
.events
.items()
.filter_map(|(event, this_linked_chunk_id)| {
// Must be in the same room.
if room_id != this_linked_chunk_id.room_id() {
return None;
}
.items(&target_linked_chunk_id)
.filter_map(|(event, pos)| {
// Must have a relation.
let (related_to, rel_type) = extract_event_relation(event.raw())?;
@@ -249,9 +259,9 @@ impl EventCacheStore for MemoryStore {
// Must not be filtered out.
if let Some(filters) = &filters {
filters.contains(&rel_type).then_some(event.clone())
filters.contains(&rel_type).then_some((event.clone(), pos))
} else {
Some(event.clone())
Some((event.clone(), pos))
}
})
.collect();
@@ -400,7 +410,7 @@ impl EventCacheStoreMedia for MemoryStore {
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
// Do not store it.
return Ok(());
};
}
// Now, let's add it.
let mut inner = self.inner.write().unwrap();
@@ -126,14 +126,8 @@ impl Deref for EventCacheStoreLockGuard<'_> {
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
#[cfg(not(target_family = "wasm"))]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// An error happened in the underlying database backend.
#[error(transparent)]
#[cfg(target_family = "wasm")]
Backend(Box<dyn std::error::Error>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
#[error("The event cache store failed to be unlocked")]
@@ -175,25 +169,12 @@ impl EventCacheStoreError {
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
#[cfg(not(target_family = "wasm"))]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
#[cfg(target_family = "wasm")]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + 'static,
{
Self::Backend(Box::new(error))
}
}
/// An `EventCacheStore` specific result type.
@@ -17,7 +17,8 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
RawChunk, Update,
},
AsyncTraitDeps,
};
@@ -77,6 +78,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
/// Load all of the chunks' metadata for the given [`LinkedChunkId`].
///
/// Chunks are unordered, and there's no guarantee that the chunks would
/// form a valid linked chunk after reconstruction.
async fn load_all_chunks_metadata(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<ChunkMetadata>, Self::Error>;
/// Load the last chunk of the `LinkedChunk` holding all events of the room
/// identified by `room_id`.
///
@@ -124,7 +134,11 @@ pub trait EventCacheStore: AsyncTraitDeps {
event_id: &EventId,
) -> Result<Option<Event>, Self::Error>;
/// Find all the events that relate to a given event.
/// Find all the events (alongside their position in the room's linked
/// chunk, if available) that relate to a given event.
///
/// The only events which don't have a position are those which have been
/// saved out-of-band using [`Self::save_event`].
///
/// Note: it doesn't process relations recursively: for instance, if
/// requesting only thread events, it will NOT return the aggregated
@@ -138,7 +152,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
room_id: &RoomId,
event_id: &EventId,
filter: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error>;
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
/// Save an event, that might or might not be part of an existing linked
/// chunk.
@@ -313,6 +327,13 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
}
async fn load_all_chunks_metadata(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<ChunkMetadata>, Self::Error> {
self.0.load_all_chunks_metadata(linked_chunk_id).await.map_err(Into::into)
}
async fn load_last_chunk(
&self,
linked_chunk_id: LinkedChunkId<'_>,
@@ -356,7 +377,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
room_id: &RoomId,
event_id: &EventId,
filter: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error> {
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
}
+4 -3
View File
@@ -227,9 +227,10 @@ impl<'de> Deserialize<'de> for LatestEvent {
Err(err) => variant_errors.push(err),
}
Err(serde::de::Error::custom(
format!("data did not match any variant of serialized LatestEvent (using serde_json). Observed errors: {variant_errors:?}")
))
Err(serde::de::Error::custom(format!(
"data did not match any variant of serialized LatestEvent (using serde_json). \
Observed errors: {variant_errors:?}"
)))
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ mod utils;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
pub use client::BaseClient;
pub use client::{BaseClient, ThreadingSupport};
#[cfg(any(test, feature = "testing"))]
pub use http;
#[cfg(feature = "e2e-encryption")]
+120 -16
View File
@@ -122,7 +122,10 @@ use std::{
num::NonZeroUsize,
};
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_common::{
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
serde_helpers::extract_thread_root,
};
use ruma::{
events::{
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
@@ -137,6 +140,8 @@ use ruma::{
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, trace, warn};
use crate::ThreadingSupport;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct LatestReadReceipt {
/// The id of the event the read receipt is referring to. (Not the read
@@ -201,7 +206,18 @@ impl RoomReadReceipts {
///
/// Returns whether a new event triggered a new unread/notification/mention.
#[inline(always)]
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
fn process_event(
&mut self,
event: &TimelineEvent,
user_id: &UserId,
threading_support: ThreadingSupport,
) {
if matches!(threading_support, ThreadingSupport::Enabled)
&& extract_thread_root(event.raw()).is_some()
{
return;
}
if marks_as_unread(event.raw(), user_id) {
self.num_unread += 1;
}
@@ -240,6 +256,7 @@ impl RoomReadReceipts {
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a TimelineEvent>,
threading_support: ThreadingSupport,
) -> bool {
let mut counting_receipts = false;
@@ -259,7 +276,7 @@ impl RoomReadReceipts {
}
if counting_receipts {
self.process_event(event, user_id);
self.process_event(event, user_id, threading_support);
}
}
@@ -443,6 +460,7 @@ pub(crate) fn compute_unread_counts(
mut previous_events: Vec<TimelineEvent>,
new_events: &[TimelineEvent],
read_receipts: &mut RoomReadReceipts,
threading_support: ThreadingSupport,
) {
debug!(?read_receipts, "Starting");
@@ -489,7 +507,12 @@ pub(crate) fn compute_unread_counts(
// The event for the receipt is in `all_events`, so we'll find it and can count
// safely from here.
read_receipts.find_and_process_events(&event_id, user_id, all_events.iter());
read_receipts.find_and_process_events(
&event_id,
user_id,
all_events.iter(),
threading_support,
);
debug!(?read_receipts, "after finding a better receipt");
return;
@@ -503,7 +526,7 @@ pub(crate) fn compute_unread_counts(
// for the next receipt.
for event in new_events {
read_receipts.process_event(event, user_id);
read_receipts.process_event(event, user_id, threading_support);
}
debug!(?read_receipts, "no better receipt, {} new events", new_events.len());
@@ -619,7 +642,10 @@ mod tests {
};
use super::compute_unread_counts;
use crate::read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts};
use crate::{
read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts},
ThreadingSupport,
};
#[test]
fn test_room_message_marks_as_unread() {
@@ -720,7 +746,7 @@ mod tests {
// An interesting event from oneself doesn't count as a new unread message.
let event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
@@ -728,7 +754,7 @@ mod tests {
// An interesting event from someone else does count as a new unread message.
let event = make_event(user_id!("@bob:example.org"), Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
@@ -736,7 +762,7 @@ mod tests {
// Push actions computed beforehand are respected.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
@@ -746,7 +772,7 @@ mod tests {
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 0);
@@ -756,7 +782,7 @@ mod tests {
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 1);
@@ -765,7 +791,7 @@ mod tests {
// make sure to resist against it.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
@@ -779,7 +805,9 @@ mod tests {
// When provided with no events, we report not finding the event to which the
// receipt relates.
let mut receipts = RoomReadReceipts::default();
assert!(receipts.find_and_process_events(ev0, user_id, &[]).not());
assert!(receipts
.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled)
.not());
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
@@ -801,7 +829,12 @@ mod tests {
..Default::default()
};
assert!(receipts
.find_and_process_events(ev0, user_id, &[make_event(event_id!("$1"))],)
.find_and_process_events(
ev0,
user_id,
&[make_event(event_id!("$1"))],
ThreadingSupport::Disabled
)
.not());
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
@@ -816,7 +849,12 @@ mod tests {
num_mentions: 37,
..Default::default()
};
assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)]));
assert!(receipts.find_and_process_events(
ev0,
user_id,
&[make_event(ev0)],
ThreadingSupport::Disabled
),);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
@@ -838,6 +876,7 @@ mod tests {
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
)
.not());
assert_eq!(receipts.num_unread, 42);
@@ -861,6 +900,7 @@ mod tests {
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
@@ -883,6 +923,7 @@ mod tests {
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
@@ -908,7 +949,7 @@ mod tests {
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = Default::default();
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
@@ -916,6 +957,7 @@ mod tests {
previous_events.clone(),
&[ev1.clone(), ev2.clone()],
&mut read_receipts,
ThreadingSupport::Disabled,
);
// It did find the receipt event (ev1).
@@ -934,6 +976,7 @@ mod tests {
previous_events,
&[new_event],
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Only the new event should be added.
@@ -1000,6 +1043,7 @@ mod tests {
all_events.clone(),
&[],
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
@@ -1021,6 +1065,7 @@ mod tests {
head_events.clone(),
&tail_events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
@@ -1065,6 +1110,7 @@ mod tests {
events,
&[], // no new events
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Then there are no unread events,
@@ -1102,6 +1148,7 @@ mod tests {
events,
&[ev0], // duplicate event!
&mut read_receipts,
ThreadingSupport::Disabled,
);
// All events are unread, and there's no pending receipt.
@@ -1536,6 +1583,7 @@ mod tests {
Vec::new(),
&events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Only the last two events sent by Bob count as unread.
@@ -1547,4 +1595,60 @@ mod tests {
// And the active receipt is the implicit one on my event.
assert_eq!(read_receipts.latest_active.unwrap().event_id, event_id!("$6"));
}
#[test]
fn test_compute_unread_counts_with_threading_enabled() {
fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
EventFactory::new()
.text_msg("A")
.sender(user_id)
.event_id(event_id!("$ida"))
.in_thread(thread_root, event_id!("$latest_event"))
.into_event()
}
let mut receipts = RoomReadReceipts::default();
let own_alice = user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
// Threaded messages from myself or other users shouldn't change the
// unread counts.
receipts.process_event(
&make_event(own_alice, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled,
);
receipts.process_event(
&make_event(own_alice, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled,
);
receipts.process_event(
&make_event(bob, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled,
);
receipts.process_event(
&make_event(bob, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled,
);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
// Processing an unthreaded message should still count as unread.
receipts.process_event(
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
own_alice,
ThreadingSupport::Enabled,
);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
}
}
@@ -13,7 +13,7 @@
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::{super::verification, E2EE};
@@ -33,11 +33,11 @@ pub async fn sync_timeline_event(
) -> Result<Option<TimelineEvent>> {
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
Ok(Some(
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
match olm
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
// Note: the push actions are set by the caller.
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
use matrix_sdk_crypto::{DecryptionSettings, OlmMachine};
pub mod decrypt;
pub mod to_device;
@@ -22,16 +22,16 @@ pub mod tracked_users;
#[derive(Clone)]
pub struct E2EE<'a> {
pub olm_machine: Option<&'a OlmMachine>,
pub decryption_trust_requirement: TrustRequirement,
pub decryption_settings: &'a DecryptionSettings,
pub verification_is_allowed: bool,
}
impl<'a> E2EE<'a> {
pub fn new(
olm_machine: Option<&'a OlmMachine>,
decryption_trust_requirement: TrustRequirement,
decryption_settings: &'a DecryptionSettings,
verification_is_allowed: bool,
) -> Self {
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
Self { olm_machine, decryption_settings, verification_is_allowed }
}
}
@@ -14,7 +14,8 @@
use std::collections::BTreeMap;
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
use ruma::{
api::client::sync::sync_events::{v3, v5, DeviceLists},
events::AnyToDeviceEvent,
@@ -93,28 +94,35 @@ async fn process(
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
let events = events
.iter()
// TODO: There is loss of information here, after calling `to_raw` it is not
// possible to make the difference between a successfully decrypted event and a plain
// text event. This information needs to be propagated to top layer at some point if
// clients relies on custom encrypted to device events.
.map(|p| p.to_raw())
.collect();
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
// If we have no `OlmMachine`, just return the events that were passed in.
// If we have no `OlmMachine`, just return the clear events that were passed in.
// The encrypted ones are dropped as they are un-usable.
// This should not happen unless we forget to set things up by calling
// `Self::activate()`.
Output {
decrypted_to_device_events: encryption_sync_changes.to_device_events,
processed_to_device_events: encryption_sync_changes
.to_device_events
.into_iter()
.map(|raw| {
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
ProcessedToDeviceEvent::UnableToDecrypt(raw)
} else {
ProcessedToDeviceEvent::PlainText(raw)
}
} else {
// Exclude events with no type
ProcessedToDeviceEvent::Invalid(raw)
}
})
.collect(),
room_key_updates: None,
}
})
}
pub struct Output {
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
pub processed_to_device_events: Vec<ProcessedToDeviceEvent>,
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
}
@@ -13,7 +13,7 @@
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::{e2ee::E2EE, verification, Context};
@@ -108,13 +108,10 @@ async fn decrypt_sync_room_event(
e2ee: &E2EE<'_>,
room_id: &RoomId,
) -> Result<TimelineEvent> {
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
let event = match e2ee
.olm_machine
.expect("An `OlmMachine` is expected")
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
@@ -184,7 +181,7 @@ mod tests {
vec![room.clone()],
E2EE::new(
client.olm_machine().await.as_ref(),
client.decryption_trust_requirement,
&client.decryption_settings,
client.handle_verification_events,
),
)
@@ -118,6 +118,7 @@ pub async fn update_any_room(
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
)
.await?;
@@ -71,6 +71,7 @@ pub async fn update_joined_room(
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
)
.await?;
@@ -89,6 +90,7 @@ pub async fn update_joined_room(
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
)
.await?;
@@ -185,6 +187,7 @@ pub async fn update_left_room(
&mut room_info,
ambiguity_cache,
&mut (),
state_store,
)
.await?;
@@ -197,6 +200,7 @@ pub async fn update_left_room(
&mut room_info,
ambiguity_cache,
&mut (),
state_store,
)
.await?;
@@ -12,11 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::{events::AnySyncStateEvent, serde::Raw};
use std::collections::BTreeSet;
use ruma::{
events::{
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
AnySyncStateEvent, SyncStateEvent,
},
serde::Raw,
RoomId,
};
use serde::Deserialize;
use tracing::warn;
use super::Context;
use crate::store::BaseStateStore;
/// Collect [`AnySyncStateEvent`].
pub mod sync {
@@ -29,11 +39,11 @@ pub mod sync {
},
OwnedUserId, RoomId, UserId,
};
use tracing::instrument;
use tracing::{error, instrument};
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
use crate::{
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
store::{ambiguity_map::AmbiguityCache, BaseStateStore, Result as StoreResult},
RoomInfo,
};
@@ -76,22 +86,71 @@ pub mod sync {
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
state_store: &BaseStateStore,
) -> StoreResult<()>
where
U: NewUsers,
{
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_state_event(event);
match event {
AnySyncStateEvent::RoomMember(member) => {
room_info.handle_state_event(event);
if let AnySyncStateEvent::RoomMember(member) = event {
dispatch_room_member(
context,
&room_info.room_id,
member,
ambiguity_cache,
new_users,
)
.await?;
dispatch_room_member(
context,
&room_info.room_id,
member,
ambiguity_cache,
new_users,
)
.await?;
}
AnySyncStateEvent::RoomCreate(create) => {
if super::is_create_event_valid(
context,
room_info.room_id(),
create,
state_store,
) {
room_info.handle_state_event(event);
} else {
error!(
room_id = ?room_info.room_id(),
?create,
"`m.create.tombstone` event is invalid, it creates a loop"
);
// Do not add the event to `room_info`.
// Do not add the event to `context.state_changes.state`.
continue;
}
}
AnySyncStateEvent::RoomTombstone(tombstone) => {
if super::is_tombstone_event_valid(
context,
room_info.room_id(),
tombstone,
state_store,
) {
room_info.handle_state_event(event);
} else {
error!(
room_id = ?room_info.room_id(),
?tombstone,
"`m.room.tombstone` event is invalid, it creates a loop"
);
// Do not add the event to `room_info`.
// Do not add the event to `context.state_changes.state`.
continue;
}
}
_ => {
room_info.handle_state_event(event);
}
}
context
@@ -248,16 +307,762 @@ where
.unzip()
}
/// Check if `m.room.create` isn't creating a loop of rooms.
pub fn is_create_event_valid(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomCreateEventContent>,
state_store: &BaseStateStore,
) -> bool {
let mut already_seen = BTreeSet::new();
already_seen.insert(room_id.to_owned());
let Some(mut predecessor_room_id) = event
.as_original()
.and_then(|event| Some(event.content.predecessor.as_ref()?.room_id.clone()))
else {
// `true` means no problem. No predecessor = no problem here.
return true;
};
loop {
// We must check immediately if the `predecessor_room_id` is in `already_seen`
// in case of a room is created and marks itself as its predecessor in a single
// sync.
if already_seen.contains(AsRef::<RoomId>::as_ref(&predecessor_room_id)) {
// Ahhh, there is a loop with `m.room.create` events!
return false;
}
already_seen.insert(predecessor_room_id.clone());
// Where is the predecessor room? Check in `room_infos` and then in
// `state_store`.
let Some(next_predecessor_room_id) = context
.state_changes
.room_infos
.get(&predecessor_room_id)
.and_then(|room_info| Some(room_info.create()?.predecessor.as_ref()?.room_id.clone()))
.or_else(|| {
state_store
.room(&predecessor_room_id)
.and_then(|room| Some(room.predecessor_room()?.room_id))
})
else {
// No more predecessor found. Everything seems alright. No loop.
break;
};
predecessor_room_id = next_predecessor_room_id;
}
true
}
/// Check if `m.room.tombstone` isn't creating a loop of rooms.
pub fn is_tombstone_event_valid(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomTombstoneEventContent>,
state_store: &BaseStateStore,
) -> bool {
let mut already_seen = BTreeSet::new();
already_seen.insert(room_id.to_owned());
let Some(mut successor_room_id) =
event.as_original().map(|event| event.content.replacement_room.clone())
else {
// `true` means no problem. No successor = no problem here.
return true;
};
loop {
// We must check immediately if the `successor_room_id` is in `already_seen` in
// case of a room is created and tombstones itself in a single sync.
if already_seen.contains(AsRef::<RoomId>::as_ref(&successor_room_id)) {
// Ahhh, there is a loop with `m.room.tombstone` events!
return false;
}
already_seen.insert(successor_room_id.clone());
// Where is the successor room? Check in `room_infos` and then in `state_store`.
let Some(next_successor_room_id) = context
.state_changes
.room_infos
.get(&successor_room_id)
.and_then(|room_info| Some(room_info.tombstone()?.replacement_room.clone()))
.or_else(|| {
state_store
.room(&successor_room_id)
.and_then(|room| Some(room.successor_room()?.room_id))
})
else {
// No more successor found. Everything seems alright. No loop.
break;
};
successor_room_id = next_successor_room_id;
}
true
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
};
use ruma::{event_id, user_id};
use ruma::{event_id, room_id, user_id, RoomVersionId};
use crate::test_utils::logged_in_base_client;
#[async_test]
async fn test_not_possible_to_overwrite_m_room_create() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
let client = logged_in_base_client(None).await;
// Create room 0 with 2 `m.room.create` events.
// Create room 1 with 1 `m.room.create` event.
// Create room 2 with 0 `m.room.create` event.
{
let response = response_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("42").unwrap()),
)
.add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("43").unwrap()),
),
)
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("44").unwrap()),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_2))
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
// Room 0
// the second `m.room.create` has been ignored!
assert_eq!(
client.get_room(room_id_0).unwrap().create_content().unwrap().room_version.as_str(),
"42"
);
// Room 1
assert_eq!(
client.get_room(room_id_1).unwrap().create_content().unwrap().room_version.as_str(),
"44"
);
// Room 2
assert!(client.get_room(room_id_2).unwrap().create_content().is_none());
}
// Room 0 receives a new `m.room.create` event.
// Room 1 receives a new `m.room.create` event.
// Room 2 receives its first `m.room.create` event.
{
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("45").unwrap()),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("46").unwrap()),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_2).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("47").unwrap()),
))
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
// Room 0
// the third `m.room.create` has been ignored!
assert_eq!(
client.get_room(room_id_0).unwrap().create_content().unwrap().room_version.as_str(),
"42"
);
// Room 1
// the second `m.room.create` has been ignored!
assert_eq!(
client.get_room(room_id_1).unwrap().create_content().unwrap().room_version.as_str(),
"44"
);
// Room 2
assert_eq!(
client.get_room(room_id_2).unwrap().create_content().unwrap().room_version.as_str(),
"47"
);
}
}
#[async_test]
async fn test_check_room_upgrades_no_newly_tombstoned_rooms() {
let client = logged_in_base_client(None).await;
// Create a new room, no tombstone, no anything.
{
let response = SyncResponseBuilder::new()
.add_joined_room(JoinedRoomBuilder::new(room_id!("!r0")))
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
}
}
#[async_test]
async fn test_check_room_upgrades_no_error() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
let client = logged_in_base_client(None).await;
// Room 0.
{
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
// Room 0 has no predecessor.
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
))
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none());
assert!(room_0.successor_room().is_none());
}
// Room 0 and room 1.
{
let tombstone_event_id = event_id!("$ev0");
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
// Successor of room 0 is room 1.
event_factory.room_tombstone("hello", room_id_1).event_id(tombstone_event_id),
))
.add_joined_room(
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
),
)
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert_eq!(
room_0.successor_room().expect("room 0 must have a successor").room_id,
room_id_1,
"room 0 does not have the expected successor",
);
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert!(room_1.successor_room().is_none(), "room 1 must not have a successor");
}
// Room 1 and room 2.
{
let tombstone_event_id = event_id!("$ev1");
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Successor of room 1 is room 2.
event_factory.room_tombstone("hello", room_id_2).event_id(tombstone_event_id),
))
.add_joined_room(
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, tombstone_event_id),
),
)
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert_eq!(
room_1.successor_room().expect("room 1 must have a successor").room_id,
room_id_2,
"room 1 does not have the expected successor",
);
let room_2 = client.get_room(room_id_2).unwrap();
assert_eq!(
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
room_id_1,
"room 2 does not have the expected predecessor",
);
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
}
}
#[async_test]
async fn test_check_room_upgrades_no_loop_within_misordered_rooms() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
// The room IDs are important because `SyncResponseBuilder` stores them in a
// `HashMap`, so they are going to be “shuffled”.
let room_id_0 = room_id!("!r1");
let room_id_1 = room_id!("!r0");
let room_id_2 = room_id!("!r2");
let client = logged_in_base_client(None).await;
// Create all rooms in a misordered way to see if `check_tombstone` will
// re-order them appropriately.
{
let response = response_builder
// Room 0
.add_joined_room(
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
// No predecessor for room 0.
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
)
.add_timeline_event(
// Successor of room 0 is room 1.
event_factory
.room_tombstone("hello", room_id_1)
.event_id(event_id!("$ev0")),
),
)
// Room 1
.add_joined_room(
JoinedRoomBuilder::new(room_id_1)
.add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, event_id!("$ev0")),
)
.add_timeline_event(
// Successor of room 1 is room 2.
event_factory
.room_tombstone("hello", room_id_2)
.event_id(event_id!("$ev1")),
),
)
// Room 2
.add_joined_room(
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, event_id!("$ev1")),
),
)
.build_sync_response();
// At this point, we can check that `response` contains misordered room updates.
{
let mut rooms = response.rooms.join.keys();
// Room 1 is before room 0!
assert_eq!(rooms.next().unwrap(), room_id_1);
assert_eq!(rooms.next().unwrap(), room_id_0);
assert_eq!(rooms.next().unwrap(), room_id_2);
assert!(rooms.next().is_none());
}
// But the algorithm to detect invalid states works nicely.
assert!(client.receive_sync_response(response).await.is_ok());
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert_eq!(
room_0.successor_room().expect("room 0 must have a successor").room_id,
room_id_1,
"room 0 does not have the expected successor",
);
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert_eq!(
room_1.successor_room().expect("room 1 must have a successor").room_id,
room_id_2,
"room 1 does not have the expected successor",
);
let room_2 = client.get_room(room_id_2).unwrap();
assert_eq!(
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
room_id_1,
"room 2 does not have the expected predecessor",
);
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
}
}
#[async_test]
async fn test_check_room_upgrades_shortest_invalid_successor() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let client = logged_in_base_client(None).await;
// Room 0.
{
let tombstone_event_id = event_id!("$ev0");
let response = response_builder
.add_joined_room(
// Successor of room 0 is room 0.
// No predecessor.
JoinedRoomBuilder::new(room_id_0).add_timeline_event(
event_factory
.room_tombstone("hello", room_id_0)
.event_id(tombstone_event_id),
),
)
.build_sync_response();
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event has not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
}
}
#[async_test]
async fn test_check_room_upgrades_invalid_successor() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
let client = logged_in_base_client(None).await;
// Room 0 and room 1.
{
let tombstone_event_id = event_id!("$ev0");
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
// Successor of room 0 is room 1.
event_factory.room_tombstone("hello", room_id_1).event_id(tombstone_event_id),
))
.add_joined_room(
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
),
)
.build_sync_response();
assert!(client.receive_sync_response(response).await.is_ok());
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert_eq!(
room_0.successor_room().expect("room 0 must have a successor").room_id,
room_id_1,
"room 0 does not have the expected successor",
);
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert!(room_1.successor_room().is_none(), "room 1 must not have a successor");
}
// Room 1, room 2 and room 0.
{
let tombstone_event_id = event_id!("$ev1");
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Successor of room 1 is room 2.
event_factory.room_tombstone("hello", room_id_2).event_id(tombstone_event_id),
))
.add_joined_room(
JoinedRoomBuilder::new(room_id_2)
.add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, tombstone_event_id),
)
.add_timeline_event(
// Successor of room 2 is room 0.
event_factory
.room_tombstone("hehe", room_id_0)
.event_id(event_id!("$ev_foo")),
),
)
.build_sync_response();
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event for `room_id_2` has not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert_eq!(
room_0.successor_room().expect("room 0 must have a successor").room_id,
room_id_1,
"room 0 does not have the expected successor",
);
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert_eq!(
room_1.successor_room().expect("room 1 must have a successor").room_id,
room_id_2,
"room 1 does not have the expected successor",
);
let room_2 = client.get_room(room_id_2).unwrap();
assert_eq!(
room_2.predecessor_room().expect("room 2 must have a predecessor").room_id,
room_id_1,
"room 2 does not have the expected predecessor",
);
// this state event is missing because it creates a loop
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
}
}
#[async_test]
async fn test_check_room_upgrades_shortest_invalid_predecessor() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let client = logged_in_base_client(None).await;
// Room 0.
{
let tombstone_event_id = event_id!("$ev0");
let response = response_builder
.add_joined_room(
// Predecessor of room 0 is room 0.
// No successor.
JoinedRoomBuilder::new(room_id_0).add_timeline_event(
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id)
.event_id(tombstone_event_id),
),
)
.build_sync_response();
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event has not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
}
}
#[async_test]
async fn test_check_room_upgrades_shortest_loop() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let client = logged_in_base_client(None).await;
// Room 0.
{
let tombstone_event_id = event_id!("$ev0");
let response = response_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
// Successor of room 0 is room 0
event_factory
.room_tombstone("hello", room_id_0)
.event_id(tombstone_event_id),
)
.add_timeline_event(
// Predecessor of room 0 is room 0
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
),
)
.build_sync_response();
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event has not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
}
}
#[async_test]
async fn test_check_room_upgrades_loop() {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
let client = logged_in_base_client(None).await;
// Room 0, room 1 and room 2.
//
// Doing that in one sync, it's the only way to create such loop (otherwise it
// implies overwriting the `m.room.create` event, or not setting it first, then
// setting it later… anyway, it works in one sync)
{
let response = response_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
// Predecessor of room 0 is room 2
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_2, event_id!("$ev2")),
)
.add_timeline_event(
// Successor of room 0 is room 1
event_factory
.room_tombstone("hello", room_id_1)
.event_id(event_id!("$ev0")),
),
)
.add_joined_room(
JoinedRoomBuilder::new(room_id_1)
.add_timeline_event(
// Predecessor of room 1 is room 0
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_0, event_id!("$ev0")),
)
.add_timeline_event(
// Successor of room 1 is room 2
event_factory
.room_tombstone("hello", room_id_2)
.event_id(event_id!("$ev1")),
),
)
.add_joined_room(
JoinedRoomBuilder::new(room_id_2)
.add_timeline_event(
// Predecessor of room 2 is room 1
event_factory
.create(sender, RoomVersionId::try_from("44").unwrap())
.predecessor(room_id_1, event_id!("$ev1")),
)
.add_timeline_event(
// Successor of room 2 is room 0
event_factory
.room_tombstone("hello", room_id_0)
.event_id(event_id!("$ev2")),
),
)
.build_sync_response();
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event for room 2 -> room 0 has not been saved, but room 0 <- room
// 2 has been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert_eq!(
room_0.predecessor_room().expect("room 0 must have a predecessor").room_id,
room_id_2,
"room 0 does not have the expected predecessor"
);
assert_eq!(
room_0.successor_room().expect("room 0 must have a successor").room_id,
room_id_1,
"room 0 does not have the expected successor",
);
let room_1 = client.get_room(room_id_1).unwrap();
assert_eq!(
room_1.predecessor_room().expect("room 1 must have a predecessor").room_id,
room_id_0,
"room 1 does not have the expected predecessor",
);
assert_eq!(
room_1.successor_room().expect("room 1 must have a successor").room_id,
room_id_2,
"room 1 does not have the expected successor",
);
let room_2 = client.get_room(room_id_2).unwrap();
// this state event is missing because it creates a loop
assert!(room_2.predecessor_room().is_none(), "room 2 must not have a predecessor");
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
}
}
#[async_test]
async fn test_state_events_after_sync() {
// Given a room
@@ -24,7 +24,7 @@ use ruma::{
StateEventType,
},
push::{Action, PushConditionRoomCtx},
RoomVersionId, UInt, UserId,
UInt, UserId,
};
use tracing::{instrument, trace, warn};
@@ -76,9 +76,9 @@ pub async fn build<'notification, 'e2ee>(
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
redaction_event,
)) => {
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
let room_version = room_info.room_version_or_default();
if let Some(redacts) = redaction_event.redacts(room_version) {
if let Some(redacts) = redaction_event.redacts(&room_version) {
room_info
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
+5 -1
View File
@@ -51,7 +51,11 @@ impl Room {
if event.content.membership == MembershipState::Knock {
event_to_user_ids.push((event.event_id, event.state_key))
} else {
warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
warn!(
"Could not mark knock event as seen: event {} for user {} \
is not in Knock membership state.",
event.event_id, event.state_key
);
}
}
_ => warn!(
@@ -88,6 +88,7 @@ mod tests_with_e2e_encryption {
use serde_json::json;
use crate::{
client::ThreadingSupport,
latest_event::LatestEvent,
response_processors as processors,
store::{MemoryStore, RoomLoadSettings, StoreConfig},
@@ -107,8 +108,10 @@ mod tests_with_e2e_encryption {
#[async_test]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
+8 -6
View File
@@ -53,7 +53,7 @@ use ruma::{
direct::OwnedDirectUserIdentifier,
receipt::{Receipt, ReceiptThread, ReceiptType},
room::{
avatar::{self},
avatar,
guest_access::GuestAccess,
history_visibility::HistoryVisibility,
join_rules::JoinRule,
@@ -340,13 +340,15 @@ impl Room {
}
/// Is the room considered to be public.
pub fn is_public(&self) -> bool {
matches!(self.join_rule(), JoinRule::Public)
///
/// May return `None` if the join rule event is not available.
pub fn is_public(&self) -> Option<bool> {
self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
}
/// Get the join rule policy of this room.
pub fn join_rule(&self) -> JoinRule {
self.inner.read().join_rule().clone()
/// Get the join rule policy of this room, if available.
pub fn join_rule(&self) -> Option<JoinRule> {
self.inner.read().join_rule().cloned()
}
/// Get the maximum power level that this room contains.
+50 -17
View File
@@ -19,7 +19,7 @@ use std::{
use bitflags::bitflags;
use eyeball::Subscriber;
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
use matrix_sdk_common::{deserialized_responses::TimelineEventKind, ROOM_VERSION_FALLBACK};
use ruma::{
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
assign,
@@ -46,8 +46,8 @@ use ruma::{
},
room::RoomType,
serde::Raw,
EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
RoomAliasId, RoomId, RoomVersionId, UserId,
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, field::debug, info, instrument, warn};
@@ -192,6 +192,7 @@ impl BaseRoomInfo {
AnySyncStateEvent::RoomName(n) => {
self.name = Some(n.into());
}
// `m.room.create` can NOT be overwritten.
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
self.create = Some(c.into());
}
@@ -309,7 +310,7 @@ impl BaseRoomInfo {
}
pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
let room_version = self.room_version().unwrap_or(&ROOM_VERSION_FALLBACK).to_owned();
// FIXME: Use let chains once available to get rid of unwrap()s
if self.avatar.has_event_id(redacts) {
@@ -463,6 +464,14 @@ pub struct RoomInfo {
/// more accurate than relying on the latest event.
#[serde(default)]
pub(crate) recency_stamp: Option<u64>,
/// A timestamp remembering when we observed the user accepting an invite on
/// this current device.
///
/// This is useful to remember if the user accepted this a join on this
/// specific client.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) invite_accepted_at: Option<MilliSecondsSinceUnixEpoch>,
}
impl RoomInfo {
@@ -485,6 +494,7 @@ impl RoomInfo {
cached_display_name: None,
cached_user_defined_notification_mode: None,
recency_stamp: None,
invite_accepted_at: None,
}
}
@@ -642,9 +652,9 @@ impl RoomInfo {
event: &SyncRoomRedactionEvent,
_raw: &Raw<SyncRoomRedactionEvent>,
) {
let room_version = self.base_info.room_version().unwrap_or(&RoomVersionId::V1);
let room_version = self.room_version_or_default();
let Some(redacts) = event.redacts(room_version) else {
let Some(redacts) = event.redacts(&room_version) else {
info!("Can't apply redaction, redacts field is missing");
return;
};
@@ -653,7 +663,7 @@ impl RoomInfo {
if let Some(latest_event) = &mut self.latest_event {
tracing::trace!("Checking if redaction applies to latest event");
if latest_event.event_id().as_deref() == Some(redacts) {
match apply_redaction(latest_event.event().raw(), _raw, room_version) {
match apply_redaction(latest_event.event().raw(), _raw, &room_version) {
Some(redacted) => {
// Even if the original event was encrypted, redaction removes all its
// fields so it cannot possibly be successfully decrypted after redaction.
@@ -748,6 +758,22 @@ impl RoomInfo {
self.summary.invited_member_count = count;
}
/// Mark that the user has accepted an invite and remember when this has
/// happened using a timestamp set to [`MilliSecondsSinceUnixEpoch::now()`].
pub(crate) fn set_invite_accepted_now(&mut self) {
self.invite_accepted_at = Some(MilliSecondsSinceUnixEpoch::now());
}
/// Returns the timestamp when an invite to this room has been accepted by
/// this specific client.
///
/// # Returns
/// - `Some` if the invite has been accepted by this specific client.
/// - `None` if the invite has not been accepted
pub fn invite_accepted_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.invite_accepted_at
}
/// Updates the room heroes.
pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
self.summary.room_heroes = heroes;
@@ -813,10 +839,10 @@ impl RoomInfo {
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
warn!("Unknown room version, falling back to v10");
warn!("Unknown room version, falling back to {ROOM_VERSION_FALLBACK}");
}
RoomVersionId::V10
ROOM_VERSION_FALLBACK
})
}
@@ -866,13 +892,12 @@ impl RoomInfo {
}
}
/// Returns the join rule for this room.
///
/// Defaults to `Public`, if missing.
pub fn join_rule(&self) -> &JoinRule {
/// Return the join rule for this room, if the `m.room.join_rules` event is
/// available.
pub fn join_rule(&self) -> Option<&JoinRule> {
match &self.base_info.join_rules {
Some(MinimalStateEvent::Original(ev)) => &ev.content.join_rule,
_ => &JoinRule::Public,
Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
_ => None,
}
}
@@ -882,7 +907,13 @@ impl RoomInfo {
(!name.is_empty()).then_some(name)
}
pub(super) fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
/// Get the content of the `m.room.create` event if any.
pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
Some(&self.base_info.create.as_ref()?.as_original()?.content)
}
/// Get the content of the `m.room.tombstone` event if any.
pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
}
@@ -1167,6 +1198,7 @@ mod tests {
owned_mxc_uri, owned_user_id, room_id, serde::Raw,
};
use serde_json::json;
use similar_asserts::assert_eq;
use super::{BaseRoomInfo, RoomInfo, SyncInfo};
use crate::{
@@ -1215,6 +1247,7 @@ mod tests {
cached_display_name: None,
cached_user_defined_notification_mode: None,
recency_stamp: Some(42),
invite_accepted_at: None,
};
let info_json = json!({
@@ -1268,7 +1301,7 @@ mod tests {
"num_mentions": 0,
"num_notifications": 0,
"latest_active": null,
"pending": []
"pending": [],
},
"recency_stamp": 42,
});
+9 -4
View File
@@ -82,6 +82,7 @@ mod tests {
use super::{super::BaseRoomInfo, RoomNotableTags};
use crate::{
client::ThreadingSupport,
response_processors as processors,
store::{RoomLoadSettings, StoreConfig},
BaseClient, RoomState, SessionMeta,
@@ -90,8 +91,10 @@ mod tests {
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
@@ -186,8 +189,10 @@ mod tests {
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
+13 -11
View File
@@ -14,10 +14,10 @@
//! Extend `BaseClient` with capabilities to handle MSC4186.
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use ruma::{api::client::sync::sync_events::v5 as http, OwnedRoomId};
#[cfg(feature = "e2e-encryption")]
use ruma::{events::AnyToDeviceEvent, serde::Raw};
use tracing::{instrument, trace};
use super::BaseClient;
@@ -44,7 +44,7 @@ impl BaseClient {
&self,
to_device: Option<&http::response::ToDevice>,
e2ee: &http::response::E2EE,
) -> Result<Option<Vec<Raw<AnyToDeviceEvent>>>> {
) -> Result<Option<Vec<ProcessedToDeviceEvent>>> {
if to_device.is_none() && e2ee.is_empty() {
return Ok(None);
}
@@ -62,7 +62,7 @@ impl BaseClient {
let mut context = processors::Context::default();
let processors::e2ee::to_device::Output { decrypted_to_device_events, room_key_updates } =
let processors::e2ee::to_device::Output { processed_to_device_events, room_key_updates } =
processors::e2ee::to_device::from_msc4186(to_device, e2ee, olm_machine.as_ref())
.await?;
@@ -75,7 +75,7 @@ impl BaseClient {
.collect(),
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
&self.decryption_settings,
self.handle_verification_events,
),
)
@@ -89,7 +89,7 @@ impl BaseClient {
)
.await?;
Ok(Some(decrypted_to_device_events))
Ok(Some(processed_to_device_events))
}
/// Process a response from a sliding sync call.
@@ -117,7 +117,7 @@ impl BaseClient {
// we received a room reshuffling event only, there won't be anything for us to
// process. stop early
return Ok(SyncResponse::default());
};
}
let mut context = processors::Context::default();
@@ -152,7 +152,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
self.olm_machine().await.as_ref(),
self.decryption_trust_requirement,
&self.decryption_settings,
self.handle_verification_events,
),
processors::notification::Notification::new(
@@ -284,6 +284,7 @@ impl BaseClient {
room_previous_events,
&joined_room_update.timeline.events,
&mut room_info.read_receipts,
self.threading_support,
);
if prev_read_receipts != room_info.read_receipts {
@@ -348,6 +349,7 @@ mod tests {
#[cfg(feature = "e2e-encryption")]
use super::processors::room::msc4186::cache_latest_events;
use crate::{
client::ThreadingSupport,
room::{RoomHero, RoomInfoNotableUpdateReasons},
store::{RoomLoadSettings, StoreConfig},
test_utils::logged_in_base_client,
@@ -1157,7 +1159,7 @@ mod tests {
let store = StoreConfig::new("cross-process-foo".to_owned());
state_store = store.state_store.clone();
let client = BaseClient::new(store);
let client = BaseClient::new(store, ThreadingSupport::Disabled);
client
.activate(
session_meta.clone(),
@@ -1188,7 +1190,7 @@ mod tests {
let client = {
let mut store = StoreConfig::new("cross-process-foo".to_owned());
store.state_store = state_store;
let client = BaseClient::new(store);
let client = BaseClient::new(store, ThreadingSupport::Disabled);
client
.activate(
session_meta,
@@ -1486,7 +1488,7 @@ mod tests {
#[async_test]
async fn test_when_only_one_event_we_cache_it() {
let event1 = make_event("m.room.message", "$1");
let events = &[event1.clone()];
let events = std::slice::from_ref(&event1);
let chosen = choose_event_to_cache(events).await;
assert_eq!(ev_id(chosen), rawev_id(event1));
}
@@ -7,7 +7,10 @@ use assert_matches2::assert_let;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_test::{event_factory::EventFactory, test_json};
use ruma::{
api::MatrixVersion,
api::{
client::discovery::discover_homeserver::{HomeserverInfo, RtcFocusInfo},
FeatureFlag, MatrixVersion,
},
event_id,
events::{
presence::PresenceEvent,
@@ -34,7 +37,7 @@ use serde_json::{json, value::Value as JsonValue};
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
RoomLoadSettings, ServerCapabilities,
RoomLoadSettings, ServerInfo, WellKnownResponse,
};
use crate::{
deserialized_responses::MemberEvent,
@@ -90,8 +93,8 @@ pub trait StateStoreIntegrationTests {
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);
/// Test saving/restoring server info.
async fn test_server_info_saving(&self);
/// Test fetching room infos based on [`RoomLoadSettings`].
async fn test_get_room_infos(&self);
}
@@ -472,34 +475,41 @@ impl StateStoreIntegrationTests for DynStateStore {
);
}
async fn test_server_capabilities_saving(&self) {
async fn test_server_info_saving(&self) {
let versions = &[MatrixVersion::V1_1, MatrixVersion::V1_2, MatrixVersion::V1_11];
let server_caps = ServerCapabilities::new(
versions,
let server_info = ServerInfo::new(
versions.iter().map(|version| version.to_string()).collect(),
[("org.matrix.experimental".to_owned(), true)].into(),
Some(WellKnownResponse {
homeserver: HomeserverInfo::new("matrix.example.com".to_owned()),
identity_server: None,
tile_server: None,
rtc_foci: vec![RtcFocusInfo::livekit("livekit.example.com".to_owned())],
}),
);
self.set_kv_data(
StateStoreDataKey::ServerCapabilities,
StateStoreDataValue::ServerCapabilities(server_caps.clone()),
StateStoreDataKey::ServerInfo,
StateStoreDataValue::ServerInfo(server_info.clone()),
)
.await
.unwrap();
assert_let!(
Ok(Some(StateStoreDataValue::ServerCapabilities(stored_caps))) =
self.get_kv_data(StateStoreDataKey::ServerCapabilities).await
Ok(Some(StateStoreDataValue::ServerInfo(stored_info))) =
self.get_kv_data(StateStoreDataKey::ServerInfo).await
);
assert_eq!(stored_caps, server_caps);
assert_eq!(stored_info, server_info);
let (stored_versions, stored_features) = stored_caps.maybe_decode().unwrap();
let decoded_server_info = stored_info.maybe_decode().unwrap();
let stored_supported = decoded_server_info.supported_versions();
assert_eq!(stored_versions, versions);
assert_eq!(stored_features.len(), 1);
assert_eq!(stored_features.get("org.matrix.experimental"), Some(&true));
assert_eq!(stored_supported.versions.as_ref(), versions);
assert_eq!(stored_supported.features.len(), 1);
assert!(stored_supported.features.contains(&FeatureFlag::from("org.matrix.experimental")));
self.remove_kv_data(StateStoreDataKey::ServerCapabilities).await.unwrap();
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerCapabilities).await, Ok(None));
self.remove_kv_data(StateStoreDataKey::ServerInfo).await.unwrap();
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerInfo).await, Ok(None));
}
async fn test_sync_token_saving(&self) {
@@ -1807,9 +1817,9 @@ macro_rules! statestore_integration_tests {
}
#[async_test]
async fn test_server_capabilities_saving() {
async fn test_server_info_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_server_capabilities_saving().await
store.test_server_info_saving().await
}
#[async_test]
@@ -19,6 +19,7 @@ use std::{
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::ROOM_VERSION_FALLBACK;
use ruma::{
canonical_json::{redact, RedactedBecause},
events::{
@@ -31,13 +32,13 @@ use ruma::{
serde::Raw,
time::Instant,
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
};
use tracing::{debug, instrument, warn};
use super::{
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
traits::{ComposerDraft, ServerCapabilities},
traits::{ComposerDraft, ServerInfo},
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
RoomLoadSettings, StateChanges, StateStore, StoreError,
};
@@ -51,10 +52,10 @@ use crate::{
#[allow(clippy::type_complexity)]
struct MemoryStoreInner {
recently_visited_rooms: HashMap<OwnedUserId, Vec<OwnedRoomId>>,
composer_drafts: HashMap<OwnedRoomId, ComposerDraft>,
composer_drafts: HashMap<(OwnedRoomId, Option<OwnedEventId>), ComposerDraft>,
user_avatar_url: HashMap<OwnedUserId, OwnedMxcUri>,
sync_token: Option<String>,
server_capabilities: Option<ServerCapabilities>,
server_info: Option<ServerInfo>,
filters: HashMap<String, String>,
utd_hook_manager_data: Option<GrowableBloom>,
account_data: HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
@@ -149,8 +150,8 @@ impl StateStore for MemoryStore {
StateStoreDataKey::SyncToken => {
inner.sync_token.clone().map(StateStoreDataValue::SyncToken)
}
StateStoreDataKey::ServerCapabilities => {
inner.server_capabilities.clone().map(StateStoreDataValue::ServerCapabilities)
StateStoreDataKey::ServerInfo => {
inner.server_info.clone().map(StateStoreDataValue::ServerInfo)
}
StateStoreDataKey::Filter(filter_name) => {
inner.filters.get(filter_name).cloned().map(StateStoreDataValue::Filter)
@@ -166,8 +167,9 @@ impl StateStore for MemoryStore {
StateStoreDataKey::UtdHookManagerData => {
inner.utd_hook_manager_data.clone().map(StateStoreDataValue::UtdHookManagerData)
}
StateStoreDataKey::ComposerDraft(room_id) => {
inner.composer_drafts.get(room_id).cloned().map(StateStoreDataValue::ComposerDraft)
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
let key = (room_id.to_owned(), thread_root.map(ToOwned::to_owned));
inner.composer_drafts.get(&key).cloned().map(StateStoreDataValue::ComposerDraft)
}
StateStoreDataKey::SeenKnockRequests(room_id) => inner
.seen_knock_requests
@@ -215,17 +217,15 @@ impl StateStore for MemoryStore {
.expect("Session data not the hook manager data"),
);
}
StateStoreDataKey::ComposerDraft(room_id) => {
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
inner.composer_drafts.insert(
room_id.to_owned(),
(room_id.to_owned(), thread_root.map(ToOwned::to_owned)),
value.into_composer_draft().expect("Session data not a composer draft"),
);
}
StateStoreDataKey::ServerCapabilities => {
inner.server_capabilities = Some(
value
.into_server_capabilities()
.expect("Session data not containing server capabilities"),
StateStoreDataKey::ServerInfo => {
inner.server_info = Some(
value.into_server_info().expect("Session data not containing server info"),
);
}
StateStoreDataKey::SeenKnockRequests(room_id) => {
@@ -245,7 +245,7 @@ impl StateStore for MemoryStore {
let mut inner = self.inner.write().unwrap();
match key {
StateStoreDataKey::SyncToken => inner.sync_token = None,
StateStoreDataKey::ServerCapabilities => inner.server_capabilities = None,
StateStoreDataKey::ServerInfo => inner.server_info = None,
StateStoreDataKey::Filter(filter_name) => {
inner.filters.remove(filter_name);
}
@@ -256,8 +256,9 @@ impl StateStore for MemoryStore {
inner.recently_visited_rooms.remove(user_id);
}
StateStoreDataKey::UtdHookManagerData => inner.utd_hook_manager_data = None,
StateStoreDataKey::ComposerDraft(room_id) => {
inner.composer_drafts.remove(room_id);
StateStoreDataKey::ComposerDraft(room_id, thread_root) => {
let key = (room_id.to_owned(), thread_root.map(ToOwned::to_owned));
inner.composer_drafts.remove(&key);
}
StateStoreDataKey::SeenKnockRequests(room_id) => {
inner.seen_knock_requests.remove(room_id);
@@ -439,12 +440,13 @@ impl StateStore for MemoryStore {
}
let make_room_version = |room_info: &HashMap<OwnedRoomId, RoomInfo>, room_id| {
room_info.get(room_id).and_then(|info| info.room_version().cloned()).unwrap_or_else(
|| {
warn!(?room_id, "Unable to find the room version, assuming version 9");
RoomVersionId::V9
},
)
room_info.get(room_id).map(|info| info.room_version_or_default()).unwrap_or_else(|| {
warn!(
?room_id,
"Unable to find the room version, assuming {ROOM_VERSION_FALLBACK}"
);
ROOM_VERSION_FALLBACK
})
};
let inner = &mut *inner;
@@ -121,6 +121,7 @@ impl RoomInfoV1 {
cached_display_name: None,
cached_user_defined_notification_mode: None,
recency_stamp: None,
invite_accepted_at: None,
}
}
}
+4 -10
View File
@@ -61,7 +61,7 @@ use crate::{
deserialized_responses::DisplayName,
event_cache::store as event_cache_store,
room::{RoomInfo, RoomInfoNotableUpdate, RoomState},
MinimalRoomMemberEvent, Room, RoomStateFilter, SendOutsideWasm, SessionMeta, SyncOutsideWasm,
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
};
pub(crate) mod ambiguity_map;
@@ -81,8 +81,8 @@ pub use self::{
SentMediaInfo, SentRequestKey, SerializableEventContent,
},
traits::{
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
StateStoreDataKey, StateStoreDataValue, StateStoreExt, WellKnownResponse,
},
};
@@ -90,14 +90,8 @@ pub use self::{
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
/// An error happened in the underlying database backend.
#[cfg(not(target_family = "wasm"))]
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// An error happened in the underlying database backend.
#[cfg(target_family = "wasm")]
#[error(transparent)]
Backend(Box<dyn std::error::Error>),
/// An error happened while serializing or deserializing some data.
#[error(transparent)]
Json(#[from] serde_json::Error),
@@ -140,7 +134,7 @@ impl StoreError {
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
@@ -206,8 +206,7 @@ pub enum QueueWedgeError {
},
}
/// The specific user intent that characterizes a
/// [`DependentQueuedRequestKind`].
/// The specific user intent that characterizes a [`DependentQueuedRequest`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DependentQueuedRequestKind {
/// The event should be edited.
+78 -36
View File
@@ -24,7 +24,12 @@ use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::{
api::MatrixVersion,
api::{
client::discovery::discover_homeserver::{
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
},
SupportedVersions,
},
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptThread, ReceiptType},
@@ -950,48 +955,84 @@ where
}
}
/// Server capabilities returned by the /client/versions endpoint.
/// Useful server info such as data returned by the /client/versions and
/// .well-known/client/matrix endpoints.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServerCapabilities {
pub struct ServerInfo {
/// Versions supported by the remote server.
///
/// This contains [`MatrixVersion`]s converted to strings.
pub versions: Vec<String>,
/// List of unstable features and their enablement status.
pub unstable_features: BTreeMap<String, bool>,
/// Information about the server found in the client well-known file.
#[serde(skip_serializing_if = "Option::is_none")]
pub well_known: Option<WellKnownResponse>,
/// Last time we fetched this data from the server, in milliseconds since
/// epoch.
last_fetch_ts: f64,
}
impl ServerCapabilities {
impl ServerInfo {
/// The number of milliseconds after which the data is considered stale.
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
/// Encode server capabilities into this serializable struct.
pub fn new(versions: &[MatrixVersion], unstable_features: BTreeMap<String, bool>) -> Self {
Self {
versions: versions.iter().map(|item| item.to_string()).collect(),
unstable_features,
last_fetch_ts: now_timestamp_ms(),
}
/// Encode server info into this serializable struct.
pub fn new(
versions: Vec<String>,
unstable_features: BTreeMap<String, bool>,
well_known: Option<WellKnownResponse>,
) -> Self {
Self { versions, unstable_features, well_known, last_fetch_ts: now_timestamp_ms() }
}
/// Decode server capabilities from this serializable struct.
/// Decode server info from this serializable struct.
///
/// May return `None` if the data is considered stale, after
/// [`Self::STALE_THRESHOLD`] milliseconds since the last time we stored
/// it.
pub fn maybe_decode(&self) -> Option<(Vec<MatrixVersion>, BTreeMap<String, bool>)> {
pub fn maybe_decode(&self) -> Option<Self> {
if now_timestamp_ms() - self.last_fetch_ts >= Self::STALE_THRESHOLD {
None
} else {
Some((
self.versions.iter().filter_map(|item| item.parse().ok()).collect(),
self.unstable_features.clone(),
))
Some(self.clone())
}
}
/// Extracts known Matrix versions and features from the un-typed lists of
/// strings.
///
/// Note: Matrix versions that Ruma cannot parse, or does not know about,
/// are discarded.
pub fn supported_versions(&self) -> SupportedVersions {
SupportedVersions::from_parts(&self.versions, &self.unstable_features)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// A serialisable representation of discover_homeserver::Response.
pub struct WellKnownResponse {
/// Information about the homeserver to connect to.
pub homeserver: HomeserverInfo,
/// Information about the identity server to connect to.
pub identity_server: Option<IdentityServerInfo>,
/// Information about the tile server to use to display location data.
pub tile_server: Option<TileServerInfo>,
/// A list of the available MatrixRTC foci, ordered by priority.
pub rtc_foci: Vec<RtcFocusInfo>,
}
impl From<discover_homeserver::Response> for WellKnownResponse {
fn from(response: discover_homeserver::Response) -> Self {
Self {
homeserver: response.homeserver,
identity_server: response.identity_server,
tile_server: response.tile_server,
rtc_foci: response.rtc_foci,
}
}
}
@@ -1011,8 +1052,8 @@ pub enum StateStoreDataValue {
/// The sync token.
SyncToken(String),
/// The server capabilities.
ServerCapabilities(ServerCapabilities),
/// The server info (versions, well-known etc).
ServerInfo(ServerInfo),
/// A filter with the given ID.
Filter(String),
@@ -1097,9 +1138,9 @@ impl StateStoreDataValue {
as_variant!(self, Self::ComposerDraft)
}
/// Get this value if it is the server capabilities metadata.
pub fn into_server_capabilities(self) -> Option<ServerCapabilities> {
as_variant!(self, Self::ServerCapabilities)
/// Get this value if it is the server info metadata.
pub fn into_server_info(self) -> Option<ServerInfo> {
as_variant!(self, Self::ServerInfo)
}
/// Get this value if it is the data for the ignored join requests.
@@ -1114,8 +1155,8 @@ pub enum StateStoreDataKey<'a> {
/// The sync token.
SyncToken,
/// The server capabilities,
ServerCapabilities,
/// The server info,
ServerInfo,
/// A filter with the given name.
Filter(&'a str),
@@ -1134,7 +1175,7 @@ pub enum StateStoreDataKey<'a> {
/// To learn more, see [`ComposerDraft`].
///
/// [`ComposerDraft`]: Self::ComposerDraft
ComposerDraft(&'a RoomId),
ComposerDraft(&'a RoomId, Option<&'a EventId>),
/// A list of knock request ids marked as seen in a room.
SeenKnockRequests(&'a RoomId),
@@ -1143,9 +1184,9 @@ pub enum StateStoreDataKey<'a> {
impl StateStoreDataKey<'_> {
/// Key to use for the [`SyncToken`][Self::SyncToken] variant.
pub const SYNC_TOKEN: &'static str = "sync_token";
/// Key to use for the [`ServerCapabilities`][Self::ServerCapabilities]
/// Key to use for the [`ServerInfo`][Self::ServerInfo]
/// variant.
pub const SERVER_CAPABILITIES: &'static str = "server_capabilities";
pub const SERVER_INFO: &'static str = "server_capabilities"; // Note: this is the old name, kept for backwards compatibility.
/// Key prefix to use for the [`Filter`][Self::Filter] variant.
pub const FILTER: &'static str = "filter";
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
@@ -1171,21 +1212,22 @@ impl StateStoreDataKey<'_> {
#[cfg(test)]
mod tests {
use super::{now_timestamp_ms, ServerCapabilities};
use super::{now_timestamp_ms, ServerInfo};
#[test]
fn test_stale_server_capabilities() {
let mut caps = ServerCapabilities {
fn test_stale_server_info() {
let mut server_info = ServerInfo {
versions: Default::default(),
unstable_features: Default::default(),
last_fetch_ts: now_timestamp_ms() - ServerCapabilities::STALE_THRESHOLD - 1.0,
well_known: Default::default(),
last_fetch_ts: now_timestamp_ms() - ServerInfo::STALE_THRESHOLD - 1.0,
};
// Definitely stale.
assert!(caps.maybe_decode().is_none());
assert!(server_info.maybe_decode().is_none());
// Definitely not stale.
caps.last_fetch_ts = now_timestamp_ms() - 1.0;
assert!(caps.maybe_decode().is_some());
server_info.last_fetch_ts = now_timestamp_ms() - 1.0;
assert!(server_info.maybe_decode().is_some());
}
}
+11 -5
View File
@@ -16,7 +16,10 @@
use std::{collections::BTreeMap, fmt};
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::TimelineEvent};
use matrix_sdk_common::{
debug::DebugRawEvent,
deserialized_responses::{ProcessedToDeviceEvent, TimelineEvent},
};
pub use ruma::api::client::sync::sync_events::v3::{
InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate,
};
@@ -24,7 +27,7 @@ use ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
events::{
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
AnySyncEphemeralRoomEvent, AnySyncStateEvent,
},
push::Action,
serde::Raw,
@@ -33,7 +36,10 @@ use ruma::{
use serde::{Deserialize, Serialize};
use crate::{
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
debug::{
DebugInvitedRoom, DebugKnockedRoom, DebugListOfProcessedToDeviceEvents,
DebugListOfRawEvents, DebugListOfRawEventsNoId,
},
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
};
@@ -50,7 +56,7 @@ pub struct SyncResponse {
/// The global private data created by this user.
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
/// Messages sent directly between devices.
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
pub to_device: Vec<ProcessedToDeviceEvent>,
/// New notifications per room.
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
@@ -61,7 +67,7 @@ impl fmt::Debug for SyncResponse {
f.debug_struct("SyncResponse")
.field("rooms", &self.rooms)
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
.field("to_device", &DebugListOfRawEventsNoId(&self.to_device))
.field("to_device", &DebugListOfProcessedToDeviceEvents(&self.to_device))
.field("notifications", &self.notifications)
.finish_non_exhaustive()
}
+5 -2
View File
@@ -19,6 +19,7 @@
use ruma::{owned_user_id, UserId};
use crate::{
client::ThreadingSupport,
store::{RoomLoadSettings, StoreConfig},
BaseClient, SessionMeta,
};
@@ -26,8 +27,10 @@ use crate::{
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
/// one otherwise.
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
let user_id =
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
client
+8 -2
View File
@@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.13.0] - 2025-07-10
### Features
- Expose the `ROOM_VERSION_FALLBACK` that should be used when the version of a
room is unknown.
([#5306](https://github.com/matrix-org/matrix-rust-sdk/pull/5306))
## [0.12.0] - 2025-06-10
No notable changes in this release.
@@ -53,5 +61,3 @@ No notable changes in this release.
### Refactor
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
+1 -1
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.12.0"
version = "0.13.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
+1 -1
View File
@@ -15,5 +15,5 @@ fn main() {
",
);
process::exit(1);
};
}
}
@@ -17,7 +17,7 @@ use std::{collections::BTreeMap, fmt, sync::Arc};
#[cfg(doc)]
use ruma::events::AnyTimelineEvent;
use ruma::{
events::{AnyMessageLikeEvent, AnySyncTimelineEvent},
events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyToDeviceEvent, MessageLikeEventType},
push::Action,
serde::{
AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw,
@@ -42,6 +42,9 @@ const VERIFICATION_VIOLATION: &str =
"Encrypted by a previously-verified user who is no longer verified.";
const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner.";
const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device.";
const MISMATCHED_SENDER: &str = "\
The sender of the event does not match the owner of the device \
that created the Megolm session.";
pub const SENT_IN_CLEAR: &str = "Not encrypted.";
/// Represents the state of verification for a decrypted message sent by a
@@ -117,6 +120,10 @@ impl VerificationState {
message: AUTHENTICITY_NOT_GUARANTEED,
},
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
@@ -171,6 +178,10 @@ impl VerificationState {
}
}
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
@@ -198,6 +209,10 @@ pub enum VerificationLevel {
/// deleted) or because the key to decrypt the message was obtained from
/// an insecure source.
None(DeviceLinkProblem),
/// The `sender` field on the event does not match the owner of the device
/// that established the Megolm session.
MismatchedSender,
}
impl fmt::Display for VerificationLevel {
@@ -211,6 +226,7 @@ impl fmt::Display for VerificationLevel {
"The sending device was not signed by the user's identity"
}
VerificationLevel::None(..) => "The sending device is not known",
VerificationLevel::MismatchedSender => MISMATCHED_SENDER,
};
write!(f, "{display}")
}
@@ -271,6 +287,9 @@ pub enum ShieldStateCode {
/// The sender was previously verified but changed their identity.
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
/// The `sender` field on the event does not match the owner of the device
/// that established the Megolm session.
MismatchedSender,
}
/// The algorithm specific information of a decrypted event.
@@ -387,7 +406,7 @@ pub struct ThreadSummary {
/// This doesn't include the thread root event itself. It can be zero if no
/// events in the thread are considered to be meaningful (or they've all
/// been redacted).
pub num_replies: usize,
pub num_replies: u32,
}
/// The status of a thread summary.
@@ -567,16 +586,18 @@ impl TimelineEvent {
}
}
let deserialized = match latest_event.deserialize() {
Ok(ev) => ev,
Err(err) => {
warn!("couldn't deserialize bundled latest thread event: {err}");
return None;
match latest_event.get_field::<MessageLikeEventType>("type") {
Ok(None) => {
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
warn!(
?event_id,
"couldn't deserialize bundled latest thread event: missing `type` field \
in bundled latest thread event"
);
None
}
};
match deserialized {
AnyMessageLikeEvent::RoomEncrypted(_) => {
Ok(Some(MessageLikeEventType::RoomEncrypted)) => {
// The bundled latest thread event is encrypted, but we didn't have any
// information about it in the unsigned map. Provide some dummy
// UTD info, since we can't really do much better.
@@ -589,7 +610,13 @@ impl TimelineEvent {
)))
}
_ => Some(Box::new(TimelineEvent::from_plaintext(latest_event.cast()))),
Ok(_) => Some(Box::new(TimelineEvent::from_plaintext(latest_event.cast()))),
Err(err) => {
let event_id = latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
warn!(?event_id, "couldn't deserialize bundled latest thread event's type: {err}");
None
}
}
}
@@ -1147,6 +1174,53 @@ impl From<SyncTimelineEventDeserializationHelperV0> for TimelineEvent {
}
}
/// Represents a to-device event after it has been processed by the Olm machine.
#[derive(Clone, Debug)]
pub enum ProcessedToDeviceEvent {
/// A successfully-decrypted encrypted event.
/// Contains the raw decrypted event and encryption info
Decrypted {
/// The raw decrypted event
raw: Raw<AnyToDeviceEvent>,
/// The Olm encryption info
encryption_info: EncryptionInfo,
},
/// An encrypted event which could not be decrypted.
UnableToDecrypt(Raw<AnyToDeviceEvent>),
/// An unencrypted event.
PlainText(Raw<AnyToDeviceEvent>),
/// An invalid to device event that was ignored because it is missing some
/// required information to be processed (like no event `type` for
/// example)
Invalid(Raw<AnyToDeviceEvent>),
}
impl ProcessedToDeviceEvent {
/// Converts a ProcessedToDeviceEvent to the `Raw<AnyToDeviceEvent>` it
/// encapsulates
pub fn to_raw(&self) -> Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw.clone(),
ProcessedToDeviceEvent::UnableToDecrypt(event) => event.clone(),
ProcessedToDeviceEvent::PlainText(event) => event.clone(),
ProcessedToDeviceEvent::Invalid(event) => event.clone(),
}
}
/// Gets the raw to-device event.
pub fn as_raw(&self) -> &Raw<AnyToDeviceEvent> {
match self {
ProcessedToDeviceEvent::Decrypted { raw, .. } => raw,
ProcessedToDeviceEvent::UnableToDecrypt(event) => event,
ProcessedToDeviceEvent::PlainText(event) => event,
ProcessedToDeviceEvent::Invalid(event) => event,
}
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, sync::Arc};
@@ -1467,6 +1541,7 @@ mod tests {
"origin_server_ts": 42,
"content": {
"body": "Hello to you too!",
"msgtype": "m.text",
}
},
"count": 2,
@@ -1486,6 +1561,8 @@ mod tests {
assert_eq!(latest_reply.as_deref(), Some(event_id!("$latest_event:example.com")));
});
assert!(timeline_event.bundled_latest_thread_event.is_some());
// When deserializing an old serialized timeline event, the thread summary is
// also extracted, if it wasn't serialized.
let serialized_timeline_item = json!({
@@ -1499,6 +1576,10 @@ mod tests {
let timeline_event: TimelineEvent =
serde_json::from_value(serialized_timeline_item).unwrap();
assert_matches!(timeline_event.thread_summary, ThreadSummaryStatus::Unknown);
// The bundled latest thread event is not persisted, so it should be `None` when
// deserialized from a previously serialized `TimelineEvent`.
assert!(timeline_event.bundled_latest_thread_event.is_none());
}
#[test]
+42 -1
View File
@@ -14,11 +14,17 @@
//! Abstraction over an executor so we can spawn tasks under Wasm the same way
//! we do usually.
//!
//! On non Wasm platforms, this re-exports parts of tokio directly. For Wasm,
//! we provide a single-threaded solution that matches the interface that tokio
//! provides as a drop in replacement.
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
#[cfg(not(target_family = "wasm"))]
mod sys {
pub use tokio::{
@@ -141,6 +147,41 @@ mod sys {
pub use sys::*;
/// A type ensuring a task is aborted on drop.
#[derive(Debug)]
pub struct AbortOnDrop<T>(JoinHandle<T>);
impl<T> AbortOnDrop<T> {
pub fn new(join_handle: JoinHandle<T>) -> Self {
Self(join_handle)
}
}
impl<T> Drop for AbortOnDrop<T> {
fn drop(&mut self) {
self.0.abort();
}
}
impl<T: 'static> Future for AbortOnDrop<T> {
type Output = Result<T, JoinError>;
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(context)
}
}
/// Trait to create an [`AbortOnDrop`] from a [`JoinHandle`].
pub trait JoinHandleExt<T> {
fn abort_on_drop(self) -> AbortOnDrop<T>;
}
impl<T> JoinHandleExt<T> for JoinHandle<T> {
fn abort_on_drop(self) -> AbortOnDrop<T> {
AbortOnDrop::new(self)
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
+2 -2
View File
@@ -190,7 +190,7 @@ fn write_message_to_logger(level: Level, message: &JsValue, logger: &JsLogger) {
Level::INFO => logger.info(message),
Level::WARN => logger.warn(message),
Level::ERROR => logger.error(message),
};
}
}
fn write_message_to_console(level: Level, message: &JsValue) {
@@ -199,7 +199,7 @@ fn write_message_to_console(level: Level, message: &JsValue) {
Level::INFO => web_sys::console::info_1(message),
Level::WARN => web_sys::console::warn_1(message),
Level::ERROR => web_sys::console::error_1(message),
};
}
}
/// An implementation of [`FormatEvent`] which formats events in a sensible way
+4
View File
@@ -42,6 +42,7 @@ pub mod ttl_cache;
#[cfg(all(target_family = "wasm", not(tarpaulin_include)))]
pub mod js_tracing;
use ruma::RoomVersionId;
pub use store_locks::LEASE_DURATION_MS;
/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on
@@ -104,3 +105,6 @@ pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
/// The room version to use as a fallback when the version of a room is unknown.
pub const ROOM_VERSION_FALLBACK: RoomVersionId = RoomVersionId::V11;
@@ -15,7 +15,7 @@
use std::{
collections::VecDeque,
iter::repeat_n,
ops::{ControlFlow, Not},
ops::ControlFlow,
sync::{Arc, RwLock},
};
@@ -25,6 +25,7 @@ use super::{
updates::{ReaderToken, Update, UpdatesInner},
ChunkContent, ChunkIdentifier, Iter, Position,
};
use crate::linked_chunk::ChunkMetadata;
/// A type alias to represent a chunk's length. This is purely for commodity.
type ChunkLength = usize;
@@ -43,7 +44,7 @@ pub struct AsVector<Item, Gap> {
token: ReaderToken,
/// Mapper from `Update` to `VectorDiff`.
mapper: UpdateToVectorDiff,
mapper: UpdateToVectorDiff<Item, Vec<VectorDiff<Item>>>,
}
impl<Item, Gap> AsVector<Item, Gap> {
@@ -83,20 +84,38 @@ impl<Item, Gap> AsVector<Item, Gap> {
}
}
/// Internal type that converts [`Update`] into [`VectorDiff`].
#[derive(Debug)]
struct UpdateToVectorDiff {
/// Pairs of all known chunks and their respective length. This is the only
/// required data for this algorithm.
chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
/// Interface for a type accumulating updates from [`UpdateToVectorDiff::map`],
/// and being returned as a result of this.
pub(super) trait UpdatesAccumulator<Item>: Extend<VectorDiff<Item>> {
/// Create a new accumulator with a rough estimation of the number of
/// updates this accumulator is going to receive.
fn new(num_updates_hint: usize) -> Self;
}
impl UpdateToVectorDiff {
// Simple implementation for a `Vec<VectorDiff<Item>>` collection for
// `AsVector<Item, Gap>`.
impl<Item> UpdatesAccumulator<Item> for Vec<VectorDiff<Item>> {
fn new(num_updates_hint: usize) -> Vec<VectorDiff<Item>> {
Vec::with_capacity(num_updates_hint)
}
}
/// Internal type that converts [`Update`] into [`VectorDiff`].
#[derive(Debug)]
pub(super) struct UpdateToVectorDiff<Item, Acc: UpdatesAccumulator<Item>> {
/// Pairs of all known chunks and their respective length. This is the only
/// required data for this algorithm.
pub chunks: VecDeque<(ChunkIdentifier, ChunkLength)>,
_phantom: std::marker::PhantomData<(Item, Acc)>,
}
impl<Item, Acc: UpdatesAccumulator<Item>> UpdateToVectorDiff<Item, Acc> {
/// Construct [`UpdateToVectorDiff`], based on an iterator of
/// [`Chunk`](super::Chunk)s, used to set up its own internal state.
///
/// See [`Self::map`] to learn more about the algorithm.
fn new<const CAP: usize, Item, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
pub fn new<const CAP: usize, Gap>(chunk_iterator: Iter<'_, CAP, Item, Gap>) -> Self {
let mut initial_chunk_lengths = VecDeque::new();
for chunk in chunk_iterator {
@@ -109,7 +128,22 @@ impl UpdateToVectorDiff {
))
}
Self { chunks: initial_chunk_lengths }
Self { chunks: initial_chunk_lengths, _phantom: std::marker::PhantomData }
}
/// Construct [`UpdateToVectorDiff`], based on a linked chunk's full
/// metadata, used to set up its own internal state.
///
/// The vector of [`ChunkMetadata`] must be ordered by their links in the
/// linked chunk. If that precondition doesn't hold, then the mapping will
/// be incorrect over time, and may cause assertions/panics.
///
/// See [`Self::map`] to learn more about the algorithm.
pub fn from_metadata(metas: Vec<ChunkMetadata>) -> Self {
let initial_chunk_lengths =
metas.into_iter().map(|meta| (meta.identifier, meta.num_items)).collect();
Self { chunks: initial_chunk_lengths, _phantom: std::marker::PhantomData }
}
/// Map several [`Update`] into [`VectorDiff`].
@@ -172,13 +206,18 @@ impl UpdateToVectorDiff {
/// [`LinkedChunk`]: super::LinkedChunk
/// [`ChunkContent::Gap`]: super::ChunkContent::Gap
/// [`ChunkContent::Content`]: super::ChunkContent::Content
fn map<Item, Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Vec<VectorDiff<Item>>
pub fn map<Gap>(&mut self, updates: &[Update<Item, Gap>]) -> Acc
where
Item: Clone,
{
let mut diffs = Vec::with_capacity(updates.len());
let mut acc = Acc::new(updates.len());
// A flag specifying when updates are reattaching detached items.
// Flags specifying when updates are reattaching detached items.
//
// TL;DR: This is an optimization to avoid that insertions in the middle of a
// chunk cause a large series of `VectorDiff::Remove` and
// `VectorDiff::Insert` updates for the elements placed after the
// inserted item.
//
// Why is it useful?
//
@@ -329,7 +368,7 @@ impl UpdateToVectorDiff {
.expect("Removing an index out of the bounds");
// Removing at the same index because each `Remove` shifts items to the left.
diffs.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
acc.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
}
Update::PushItems { at: position, items } => {
@@ -348,12 +387,12 @@ impl UpdateToVectorDiff {
}
// Optimisation: we can emit a `VectorDiff::Append` in this particular case.
if is_pushing_back && detaching.not() {
diffs.push(VectorDiff::Append { values: items.into() });
if is_pushing_back && !detaching {
acc.extend([VectorDiff::Append { values: items.into() }]);
}
// No optimisation: let's emit `VectorDiff::Insert`.
else {
diffs.extend(items.iter().enumerate().map(|(nth, item)| {
acc.extend(items.iter().enumerate().map(|(nth, item)| {
VectorDiff::Insert { index: offset + nth, value: item.clone() }
}));
}
@@ -364,7 +403,7 @@ impl UpdateToVectorDiff {
// The chunk length doesn't change.
diffs.push(VectorDiff::Set { index: offset, value: item.clone() });
acc.extend([VectorDiff::Set { index: offset, value: item.clone() }]);
}
Update::RemoveItem { at: position } => {
@@ -379,7 +418,7 @@ impl UpdateToVectorDiff {
}
// Let's emit a `VectorDiff::Remove`.
diffs.push(VectorDiff::Remove { index: offset });
acc.extend([VectorDiff::Remove { index: offset }]);
}
Update::DetachLastItems { at: position } => {
@@ -422,12 +461,12 @@ impl UpdateToVectorDiff {
self.chunks.clear();
// Let's straightforwardly emit a `VectorDiff::Clear`.
diffs.push(VectorDiff::Clear);
acc.extend([VectorDiff::Clear]);
}
}
}
diffs
acc
}
fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
@@ -524,8 +563,8 @@ mod tests {
linked_chunk
.insert_items_at(
['w', 'x', 'y', 'z'],
linked_chunk.item_position(|item| *item == 'b').unwrap(),
['w', 'x', 'y', 'z'],
)
.unwrap();
assert_items_eq!(linked_chunk, ['a', 'w', 'x'] ['y', 'z', 'b'] ['c'] ['d']);
@@ -607,7 +646,7 @@ mod tests {
);
linked_chunk
.insert_items_at(['m'], linked_chunk.item_position(|item| *item == 'a').unwrap())
.insert_items_at(linked_chunk.item_position(|item| *item == 'a').unwrap(), ['m'])
.unwrap();
assert_items_eq!(
linked_chunk,
@@ -670,7 +709,7 @@ mod tests {
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
linked_chunk
.insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
.insert_items_at(linked_chunk.item_position(|item| *item == 'h').unwrap(), ['z'])
.unwrap();
assert_items_eq!(
+109 -64
View File
@@ -93,6 +93,7 @@ macro_rules! assert_items_eq {
mod as_vector;
pub mod lazy_loader;
mod order_tracker;
pub mod relational;
mod updates;
@@ -104,6 +105,7 @@ use std::{
};
pub use as_vector::*;
pub use order_tracker::OrderTracker;
use ruma::{OwnedRoomId, RoomId};
pub use updates::*;
@@ -127,12 +129,6 @@ impl LinkedChunkId<'_> {
LinkedChunkId::Room(room_id) => OwnedLinkedChunkId::Room((*room_id).to_owned()),
}
}
pub fn room_id(&self) -> &RoomId {
match self {
LinkedChunkId::Room(room_id) => room_id,
}
}
}
impl PartialEq<&OwnedLinkedChunkId> for LinkedChunkId<'_> {
@@ -166,6 +162,7 @@ impl Display for OwnedLinkedChunkId {
}
impl OwnedLinkedChunkId {
#[cfg(test)]
fn as_ref(&self) -> LinkedChunkId<'_> {
match self {
OwnedLinkedChunkId::Room(room_id) => LinkedChunkId::Room(room_id.as_ref()),
@@ -297,7 +294,7 @@ impl<const CAP: usize, Item, Gap> Ends<CAP, Item, Gap> {
// Fetch the previous chunk pointer.
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
// Re-box the chunk, and let Rust does its job.
// Re-box the chunk, and let Rust do its job.
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
// Update the `current_chunk_ptr`.
@@ -469,7 +466,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
///
/// Because the `position` can be invalid, this method returns a
/// `Result`.
pub fn insert_items_at<I>(&mut self, items: I, position: Position) -> Result<(), Error>
pub fn insert_items_at<I>(&mut self, position: Position, items: I) -> Result<(), Error>
where
Item: Clone,
Gap: Clone,
@@ -486,7 +483,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
let chunk = match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
}
ChunkContent::Items(current_items) => {
@@ -572,32 +569,24 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
.chunk_mut(chunk_identifier)
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
let can_unlink_chunk = match &mut chunk.content {
let current_items = match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
}
ChunkContent::Items(current_items) => {
let current_items_length = current_items.len();
if item_index > current_items_length {
return Err(Error::InvalidItemIndex { index: item_index });
}
removed_item = current_items.remove(item_index);
if let Some(updates) = self.updates.as_mut() {
updates
.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
}
current_items.is_empty()
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
}
ChunkContent::Items(current_items) => current_items,
};
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
// if the `chunk` is not the first one, we can remove it.
if can_unlink_chunk && !chunk.is_first_chunk() {
if item_index > current_items.len() {
return Err(Error::InvalidItemIndex { index: item_index });
}
removed_item = current_items.remove(item_index);
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
}
// If the chunk is empty and not the first one, we can remove it.
if current_items.is_empty() && !chunk.is_first_chunk() {
// Unlink `chunk`.
chunk.unlink(self.updates.as_mut());
@@ -616,7 +605,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
if let Some(chunk_ptr) = chunk_ptr {
// `chunk` has been unlinked.
// Re-box the chunk, and let Rust does its job.
// Re-box the chunk, and let Rust do its job.
//
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
@@ -644,7 +633,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
return Err(Error::ChunkIsAGap { identifier: chunk_identifier });
}
ChunkContent::Items(current_items) => {
@@ -864,7 +853,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
if chunk.is_items() {
return Err(Error::ChunkIsItems { identifier: chunk_identifier });
};
}
let chunk_was_first = chunk.is_first_chunk();
@@ -908,7 +897,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
// Stop borrowing `chunk`.
}
// Re-box the chunk, and let Rust does its job.
// Re-box the chunk, and let Rust do its job.
//
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
@@ -1085,6 +1074,42 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
Some(AsVector::new(updates, token, chunk_iterator))
}
/// Get an [`OrderTracker`] for the linked chunk, which can be used to
/// compare the relative position of two events in this linked chunk.
///
/// A pre-requisite is that the linked chunk has been constructed with
/// [`Self::new_with_update_history`], and that if the linked chunk is
/// lazily-loaded, an iterator over the fully-loaded linked chunk is
/// passed at construction time here.
pub fn order_tracker(
&mut self,
all_chunks: Option<Vec<ChunkMetadata>>,
) -> Option<OrderTracker<Item, Gap>>
where
Item: Clone,
{
let (updates, token) = self
.updates
.as_mut()
.map(|updates| (updates.inner.clone(), updates.new_reader_token()))?;
Some(OrderTracker::new(
updates,
token,
all_chunks.unwrap_or_else(|| {
// Consider the linked chunk as fully loaded.
self.chunks()
.map(|chunk| ChunkMetadata {
identifier: chunk.identifier(),
num_items: chunk.num_items(),
previous: chunk.previous().map(|prev| prev.identifier()),
next: chunk.next().map(|next| next.identifier()),
})
.collect()
}),
))
}
/// Returns the number of items of the linked chunk.
pub fn num_items(&self) -> usize {
self.items().count()
@@ -1158,7 +1183,10 @@ impl ChunkIdentifierGenerator {
// Check for overflows.
// unlikely — TODO: call `std::intrinsics::unlikely` once it's stable.
if previous == u64::MAX {
panic!("No more chunk identifiers available. Congrats, you did it. 2^64 identifiers have been consumed.")
panic!(
"No more chunk identifiers available. Congrats, you did it. \
2^64 identifiers have been consumed."
)
}
ChunkIdentifier(previous + 1)
@@ -1721,6 +1749,25 @@ pub struct RawChunk<Item, Gap> {
pub next: Option<ChunkIdentifier>,
}
/// A simplified [`RawChunk`] that only contains the number of items in a chunk,
/// instead of its type.
#[derive(Clone, Debug)]
pub struct ChunkMetadata {
/// The number of items in this chunk.
///
/// By convention, a gap chunk contains 0 items.
pub num_items: usize,
/// Link to the previous chunk, via its identifier.
pub previous: Option<ChunkIdentifier>,
/// Current chunk's identifier.
pub identifier: ChunkIdentifier,
/// Link to the next chunk, via its identifier.
pub next: Option<ChunkIdentifier>,
}
#[cfg(test)]
mod tests {
use std::{
@@ -2210,11 +2257,11 @@ mod tests {
// Insert inside the last chunk.
{
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
// Insert 4 elements, so that it overflows the chunk capacity. It's important to
// see whether chunks are correctly updated and linked.
linked_chunk.insert_items_at(['w', 'x', 'y', 'z'], position_of_e)?;
linked_chunk.insert_items_at(pos_e, ['w', 'x', 'y', 'z'])?;
assert_items_eq!(
linked_chunk,
@@ -2247,8 +2294,8 @@ mod tests {
// Insert inside the first chunk.
{
let position_of_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
linked_chunk.insert_items_at(['l', 'm', 'n', 'o'], position_of_a)?;
let pos_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
linked_chunk.insert_items_at(pos_a, ['l', 'm', 'n', 'o'])?;
assert_items_eq!(
linked_chunk,
@@ -2281,8 +2328,8 @@ mod tests {
// Insert inside a middle chunk.
{
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
linked_chunk.insert_items_at(['r', 's'], position_of_c)?;
let pos_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
linked_chunk.insert_items_at(pos_c, ['r', 's'])?;
assert_items_eq!(
linked_chunk,
@@ -2303,11 +2350,10 @@ mod tests {
// Insert at the end of a chunk.
{
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
let position_after_f =
Position(position_of_f.chunk_identifier(), position_of_f.index() + 1);
let pos_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
let pos_f = Position(pos_f.chunk_identifier(), pos_f.index() + 1);
linked_chunk.insert_items_at(['p', 'q'], position_after_f)?;
linked_chunk.insert_items_at(pos_f, ['p', 'q'])?;
assert_items_eq!(
linked_chunk,
['l', 'm', 'n'] ['o', 'a', 'b'] ['r', 's', 'c'] ['d', 'w', 'x'] ['y', 'z', 'e'] ['f', 'p', 'q']
@@ -2322,7 +2368,7 @@ mod tests {
// Insert in a chunk that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -2331,7 +2377,7 @@ mod tests {
// Insert in a chunk that exists, but at an item that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
Err(Error::InvalidItemIndex { index: 128 })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -2356,7 +2402,7 @@ mod tests {
);
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(6), 0)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(6), 0), ['u', 'v'],),
Err(Error::ChunkIsAGap { identifier: ChunkIdentifier(6) })
);
}
@@ -2391,11 +2437,11 @@ mod tests {
);
// Insert inside the last chunk.
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
// Insert 4 elements, so that it overflows the chunk capacity. It's important to
// see whether chunks are correctly updated and linked.
linked_chunk.insert_items_at(['w', 'x', 'y', 'z'], position_of_e)?;
linked_chunk.insert_items_at(pos_e, ['w', 'x', 'y', 'z'])?;
assert_items_eq!(
linked_chunk,
@@ -2453,8 +2499,8 @@ mod tests {
);
// Insert inside the first chunk.
let position_of_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
linked_chunk.insert_items_at(['l', 'm', 'n', 'o'], position_of_a)?;
let pos_a = linked_chunk.item_position(|item| *item == 'a').unwrap();
linked_chunk.insert_items_at(pos_a, ['l', 'm', 'n', 'o'])?;
assert_items_eq!(
linked_chunk,
@@ -2517,8 +2563,8 @@ mod tests {
]
);
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
linked_chunk.insert_items_at(['r', 's'], position_of_d)?;
let pos_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
linked_chunk.insert_items_at(pos_d, ['r', 's'])?;
assert_items_eq!(
linked_chunk,
@@ -2570,11 +2616,10 @@ mod tests {
);
// Insert at the end of a chunk.
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let position_after_e =
Position(position_of_e.chunk_identifier(), position_of_e.index() + 1);
let pos_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let pos_after_e = Position(pos_e.chunk_identifier(), pos_e.index() + 1);
linked_chunk.insert_items_at(['p', 'q'], position_after_e)?;
linked_chunk.insert_items_at(pos_after_e, ['p', 'q'])?;
assert_items_eq!(
linked_chunk,
['a', 'b', 'c'] ['d', 'e', 'p'] ['q']
@@ -2624,7 +2669,7 @@ mod tests {
// Insert in a chunk that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -2633,7 +2678,7 @@ mod tests {
// Insert in a chunk that exists, but at an item that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
Err(Error::InvalidItemIndex { index: 128 })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -2642,7 +2687,7 @@ mod tests {
// Insert in a gap.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(1), 0)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(1), 0), ['u', 'v'],),
Err(Error::ChunkIsAGap { identifier: ChunkIdentifier(1) })
);
}
@@ -2984,7 +3029,7 @@ mod tests {
// Insert in a chunk that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(128), 0)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(128), 0), ['u', 'v'],),
Err(Error::InvalidChunkIdentifier { identifier: ChunkIdentifier(128) })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -2993,7 +3038,7 @@ mod tests {
// Insert in a chunk that exists, but at an item that does not exist.
{
assert_matches!(
linked_chunk.insert_items_at(['u', 'v'], Position(ChunkIdentifier(0), 128)),
linked_chunk.insert_items_at(Position(ChunkIdentifier(0), 128), ['u', 'v'],),
Err(Error::InvalidItemIndex { index: 128 })
);
assert!(linked_chunk.updates().unwrap().take().is_empty());
@@ -0,0 +1,569 @@
// Copyright 2025 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::sync::{Arc, RwLock};
use eyeball_im::VectorDiff;
use super::{
updates::{ReaderToken, Update, UpdatesInner},
Position,
};
use crate::linked_chunk::{ChunkMetadata, UpdateToVectorDiff};
/// A tracker for the order of items in a linked chunk.
///
/// This can be used to determine the absolute ordering of an item, and thus the
/// relative ordering of two items in a linked chunk, in an
/// efficient manner, thanks to [`OrderTracker::ordering`]. Internally, it
/// keeps track of the relative ordering of the chunks themselves; given a
/// [`Position`] in a linked chunk, the item ordering is the lexicographic
/// ordering of the chunk in the linked chunk, and the internal position within
/// the chunk. For the sake of ease, we return the absolute vector index of the
/// item in the linked chunk.
///
/// It requires the full links' metadata to be provided at creation time, so
/// that it can also give an order for an item that's not loaded yet, in the
/// context of lazy-loading.
#[derive(Debug)]
pub struct OrderTracker<Item, Gap> {
/// Strong reference to [`UpdatesInner`].
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
/// The token to read the updates.
token: ReaderToken,
/// Mapper from `Update` to `VectorDiff`.
mapper: UpdateToVectorDiff<Item, NullAccumulator<Item>>,
}
struct NullAccumulator<Item> {
_phantom: std::marker::PhantomData<Item>,
}
#[cfg(not(tarpaulin_include))]
impl<Item> std::fmt::Debug for NullAccumulator<Item> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NullAccumulator")
}
}
impl<Item> super::UpdatesAccumulator<Item> for NullAccumulator<Item> {
fn new(_num_updates_hint: usize) -> Self {
Self { _phantom: std::marker::PhantomData }
}
}
impl<Item> Extend<VectorDiff<Item>> for NullAccumulator<Item> {
fn extend<T: IntoIterator<Item = VectorDiff<Item>>>(&mut self, _iter: T) {
// This is a no-op, as we don't want to accumulate anything.
}
}
impl<Item, Gap> OrderTracker<Item, Gap>
where
Item: Clone,
{
/// Create a new [`OrderTracker`].
///
/// The `all_chunks_metadata` parameter must include the metadata for *all*
/// chunks (the full collection, even if the linked chunk is
/// lazy-loaded).
///
/// They must be ordered by their links in the linked chunk, i.e. the first
/// chunk in the vector is the first chunk in the linked chunk, the
/// second in the vector is the first's next chunk, and so on. If that
/// precondition doesn't hold, then the ordering of items will be undefined.
pub(super) fn new(
updates: Arc<RwLock<UpdatesInner<Item, Gap>>>,
token: ReaderToken,
all_chunks_metadata: Vec<ChunkMetadata>,
) -> Self {
// Drain previous updates so that this type is synced with `Updates`.
{
let mut updates = updates.write().unwrap();
let _ = updates.take_with_token(token);
}
Self { updates, token, mapper: UpdateToVectorDiff::from_metadata(all_chunks_metadata) }
}
/// Force flushing of the updates manually.
///
/// If `inhibit` is `true` (which is useful in the case of lazy-loading
/// related updates, which shouldn't affect the canonical, persisted
/// linked chunk), the updates are ignored; otherwise, they are consumed
/// normally.
pub fn flush_updates(&mut self, inhibit: bool) {
if inhibit {
// Ignore the updates.
let _ = self.updates.write().unwrap().take_with_token(self.token);
} else {
// Consume the updates.
let mut updater = self.updates.write().unwrap();
let updates = updater.take_with_token(self.token);
let _ = self.mapper.map(updates);
}
}
/// Apply some out-of-band updates to the ordering tracker.
///
/// This must only be used when the updates do not affect the observed
/// linked chunk, but would affect the fully-loaded collection.
pub fn map_updates(&mut self, updates: &[Update<Item, Gap>]) {
let _ = self.mapper.map(updates);
}
/// Given an event's position, returns its final ordering in the current
/// state of the linked chunk as a vector.
///
/// Useful to compare the ordering of multiple events.
///
/// Precondition: the reader must be up to date, i.e.
/// [`Self::flush_updates`] must have been called before this method.
///
/// Will return `None` if the position doesn't match a known chunk in the
/// linked chunk, or if the chunk is a gap.
pub fn ordering(&self, event_pos: Position) -> Option<usize> {
// Check the precondition: there must not be any pending updates for this
// reader.
debug_assert!(self.updates.read().unwrap().is_reader_up_to_date(self.token));
// Find the chunk that contained the event.
let mut ordering = 0;
for (chunk_id, chunk_length) in &self.mapper.chunks {
if *chunk_id == event_pos.chunk_identifier() {
let offset_within_chunk = event_pos.index();
if offset_within_chunk >= *chunk_length {
// The event is out of bounds for this chunk, return None.
return None;
}
// The final ordering is the number of items before the event, plus its own
// index within the chunk.
return Some(ordering + offset_within_chunk);
}
// This is not the target chunk yet, so add the size of the current chunk to the
// number of seen items, and continue.
ordering += *chunk_length;
}
None
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test_macros::async_test;
use crate::linked_chunk::{
lazy_loader::from_last_chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator,
ChunkMetadata, LinkedChunk, OrderTracker, Position, RawChunk, Update,
};
#[async_test]
async fn test_linked_chunk_without_update_history_no_tracking() {
let mut linked_chunk = LinkedChunk::<10, char, ()>::new();
assert_matches!(linked_chunk.order_tracker(None), None);
}
/// Given a fully-loaded linked chunk, checks that the ordering of an item
/// is effectively the same as its index in an iteration of items.
fn assert_order_fully_loaded(
linked_chunk: &LinkedChunk<3, char, ()>,
tracker: &OrderTracker<char, ()>,
) {
assert_order(linked_chunk, tracker, 0);
}
/// Given a linked chunk with an offset representing the number of items not
/// loaded yet, checks that the ordering of an item is effectively the
/// same as its index+offset in an iteration of items.
fn assert_order(
linked_chunk: &LinkedChunk<3, char, ()>,
tracker: &OrderTracker<char, ()>,
offset: usize,
) {
for (i, (item_pos, _value)) in linked_chunk.items().enumerate() {
assert_eq!(tracker.ordering(item_pos), Some(i + offset));
}
}
#[async_test]
async fn test_non_lazy_updates() {
// Assume the linked chunk is fully loaded, so we have all the chunks at
// our disposal.
let mut linked_chunk = LinkedChunk::<3, _, _>::new_with_update_history();
let mut tracker = linked_chunk.order_tracker(None).unwrap();
// Let's apply some updates to the live linked chunk.
// Pushing new items.
{
linked_chunk.push_items_back(['a', 'b', 'c']);
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Pushing a gap.
{
linked_chunk.push_gap_back(());
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Inserting items in the middle.
{
let pos_b = linked_chunk.item_position(|c| *c == 'b').unwrap();
linked_chunk.insert_items_at(pos_b, ['d', 'e']).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Inserting a gap in the middle.
{
let c_pos = linked_chunk.item_position(|c| *c == 'c').unwrap();
linked_chunk.insert_gap_at((), c_pos).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Replacing a gap with items.
{
let last_gap =
linked_chunk.rchunks().filter(|c| c.is_gap()).last().unwrap().identifier();
linked_chunk.replace_gap_at(['f', 'g'], last_gap).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Removing an item.
{
let a_pos = linked_chunk.item_position(|c| *c == 'd').unwrap();
linked_chunk.remove_item_at(a_pos).unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Replacing an item.
{
let b_pos = linked_chunk.item_position(|c| *c == 'e').unwrap();
linked_chunk.replace_item_at(b_pos, 'E').unwrap();
tracker.flush_updates(false);
assert_order_fully_loaded(&linked_chunk, &tracker);
}
// Clearing all items.
{
linked_chunk.clear();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
}
}
#[async_test]
async fn test_lazy_loading() {
// Assume that all the chunks haven't been loaded yet, so we have a few of them
// in some memory, and some of them are still in an hypothetical
// database.
let db_metadata = vec![
// Hypothetical non-empty items chunk with items 'a', 'b', 'c'.
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 3,
},
// Hypothetical gap chunk.
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
// Hypothetical non-empty items chunk with items 'g'.
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
// The in-memory linked chunk contains the latest chunk only.
let mut linked_chunk = from_last_chunk::<3, _, ()>(
Some(RawChunk {
content: ChunkContent::Items(vec!['g']),
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
}),
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
)
.expect("could recreate the linked chunk")
.expect("the linked chunk isn't empty");
let tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
// At first, even if the main linked chunk is empty, the order tracker can
// compute the position for unloaded items.
// Order of 'a':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), Some(0));
// Order of 'b':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
// Order of 'c':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 2)), Some(2));
// An invalid position in a known chunk returns no ordering.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 42)), None);
// A gap chunk doesn't have an ordering.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(1), 42)), None);
// Order of 'd':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 0)), Some(3));
// Order of 'e':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(4));
// Order of 'f':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 2)), Some(5));
// No subsequent entry in the same chunk, it's been split when inserting g.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 3)), None);
// Order of 'g':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(6));
// This was the final entry so far.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 1)), None);
}
#[async_test]
async fn test_lazy_updates() {
// Assume that all the chunks haven't been loaded yet, so we have a few of them
// in some memory, and some of them are still in an hypothetical
// database.
let db_metadata = vec![
// Hypothetical non-empty items chunk with items 'a', 'b'.
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 2,
},
// Hypothetical gap chunk.
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
// Hypothetical non-empty items chunk with items 'g'.
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
// The in-memory linked chunk contains the latest chunk only.
let mut linked_chunk = from_last_chunk(
Some(RawChunk {
content: ChunkContent::Items(vec!['g']),
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
}),
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier(3)),
)
.expect("could recreate the linked chunk")
.expect("the linked chunk isn't empty");
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
// Sanity checks on the initial state.
{
// Order of 'b':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
// Order of 'g':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
}
// Let's apply some updates to the live linked chunk.
// Pushing new items.
{
linked_chunk.push_items_back(['h', 'i']);
tracker.flush_updates(false);
// Order of items not loaded:
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
}
// Pushing a gap.
let gap_id = {
linked_chunk.push_gap_back(());
tracker.flush_updates(false);
// The gap doesn't have an ordering.
let last_chunk = linked_chunk.rchunks().next().unwrap();
assert!(last_chunk.is_gap());
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 0)), None);
assert_eq!(tracker.ordering(Position::new(last_chunk.identifier(), 42)), None);
// The previous items are still ordered.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
last_chunk.identifier()
};
// Inserting items in the middle.
{
let pos_h = linked_chunk.item_position(|c| *c == 'h').unwrap();
linked_chunk.insert_items_at(pos_h, ['j', 'k']).unwrap();
tracker.flush_updates(false);
// The previous items are still ordered.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
}
// Replacing a gap with items.
{
linked_chunk.replace_gap_at(['l', 'm'], gap_id).unwrap();
tracker.flush_updates(false);
// The previous items are still ordered.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
}
// Removing an item.
{
let j_pos = linked_chunk.item_position(|c| *c == 'j').unwrap();
linked_chunk.remove_item_at(j_pos).unwrap();
tracker.flush_updates(false);
// The previous items are still ordered.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
}
// Replacing an item.
{
let k_pos = linked_chunk.item_position(|c| *c == 'k').unwrap();
linked_chunk.replace_item_at(k_pos, 'K').unwrap();
tracker.flush_updates(false);
// The previous items are still ordered.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), Some(5));
// The loaded items are off by 5 (the absolute order of g).
assert_order(&linked_chunk, &tracker, 5);
}
// Clearing all items.
{
linked_chunk.clear();
tracker.flush_updates(false);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 0)), None);
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(3), 0)), None);
}
}
#[async_test]
async fn test_out_of_band_updates() {
// Assume that all the chunks haven't been loaded yet, so we have a few of them
// in some memory, and some of them are still in an hypothetical
// database.
let db_metadata = vec![
// Hypothetical non-empty items chunk with items 'a', 'b'.
ChunkMetadata {
previous: None,
identifier: ChunkIdentifier(0),
next: Some(ChunkIdentifier(1)),
num_items: 2,
},
// Hypothetical gap chunk.
ChunkMetadata {
previous: Some(ChunkIdentifier(0)),
identifier: ChunkIdentifier(1),
next: Some(ChunkIdentifier(2)),
num_items: 0,
},
// Hypothetical non-empty items chunk with items 'd', 'e', 'f'.
ChunkMetadata {
previous: Some(ChunkIdentifier(1)),
identifier: ChunkIdentifier(2),
next: Some(ChunkIdentifier(3)),
num_items: 3,
},
// Hypothetical non-empty items chunk with items 'g'.
ChunkMetadata {
previous: Some(ChunkIdentifier(2)),
identifier: ChunkIdentifier(3),
next: None,
num_items: 1,
},
];
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let mut tracker = linked_chunk.order_tracker(Some(db_metadata)).unwrap();
// Sanity checks.
// Order of 'b':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), Some(1));
// Order of 'e':
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(3));
// It's possible to apply updates out of band, i.e. without affecting the
// observed linked chunk. This can be useful when an update only applies
// to a database, but not to the in-memory linked chunk.
tracker.map_updates(&[Update::RemoveChunk(ChunkIdentifier::new(0))]);
// 'b' doesn't exist anymore, so its ordering is now undefined.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(0), 1)), None);
// 'e' has been shifted back by 2 places, aka the number of items in the first
// chunk.
assert_eq!(tracker.ordering(Position::new(ChunkIdentifier::new(2), 1)), Some(1));
}
}
@@ -15,14 +15,19 @@
//! Implementation for a _relational linked chunk_, see
//! [`RelationalLinkedChunk`].
use std::{collections::HashMap, hash::Hash};
use std::{
collections::{BTreeMap, HashMap},
hash::Hash,
};
use ruma::{OwnedEventId, OwnedRoomId};
use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
use crate::{
deserialized_responses::TimelineEvent,
linked_chunk::{ChunkIdentifier, LinkedChunkId, OwnedLinkedChunkId, Position, Update},
linked_chunk::{
ChunkIdentifier, ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, Update,
},
};
/// A row of the [`RelationalLinkedChunk::chunks`].
@@ -79,7 +84,7 @@ pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
items_chunks: Vec<ItemRow<ItemId, Gap>>,
/// The items' content themselves.
items: HashMap<OwnedLinkedChunkId, HashMap<ItemId, Item>>,
items: HashMap<OwnedLinkedChunkId, BTreeMap<ItemId, (Item, Option<Position>)>>,
}
/// The [`IndexableItem`] trait is used to mark items that can be indexed into a
@@ -103,7 +108,7 @@ impl IndexableItem for TimelineEvent {
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone,
ItemId: Hash + PartialEq + Eq + Clone + Ord,
{
/// Create a new relational linked chunk.
pub fn new() -> Self {
@@ -127,11 +132,11 @@ where
for update in updates {
match update {
Update::NewItemsChunk { previous, new, next } => {
insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
}
Update::NewGapChunk { previous, new, next, gap } => {
insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
Self::insert_chunk(&mut self.chunks, linked_chunk_id, previous, new, next);
self.items_chunks.push(ItemRow {
linked_chunk_id: linked_chunk_id.to_owned(),
position: Position::new(new, 0),
@@ -140,7 +145,7 @@ where
}
Update::RemoveChunk(chunk_identifier) => {
remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
Self::remove_chunk(&mut self.chunks, linked_chunk_id, chunk_identifier);
let indices_to_remove = self
.items_chunks
@@ -173,7 +178,7 @@ where
self.items
.entry(linked_chunk_id.to_owned())
.or_default()
.insert(item_id.clone(), item);
.insert(item_id.clone(), (item, Some(at)));
self.items_chunks.push(ItemRow {
linked_chunk_id: linked_chunk_id.to_owned(),
position: at,
@@ -197,7 +202,7 @@ where
self.items
.entry(linked_chunk_id.to_owned())
.or_default()
.insert(item_id.clone(), item);
.insert(item_id.clone(), (item, Some(at)));
existing.item = Either::Item(item_id);
}
@@ -269,101 +274,93 @@ where
}
}
}
}
fn insert_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
},
)
.expect("Previous chunk should be present");
fn insert_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_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 {
linked_chunk_id: linked_chunk_id.to_owned(),
previous_chunk: previous,
chunk: new,
next_chunk: next,
});
// Link the chunk.
entry_for_previous_chunk.next_chunk = Some(new);
}
fn remove_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
chunk_to_remove: ChunkIdentifier,
) {
let entry_nth_to_remove = chunks
.iter()
.enumerate()
.find_map(
|(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
(linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
.then_some(nth)
},
)
.expect("Remove an unknown chunk");
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
linked_chunk_id == linked_chunk_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
chunks.remove(entry_nth_to_remove);
// Link the chunk.
entry_for_next_chunk.previous_chunk = Some(new);
}
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(
|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_id_candidate && *chunk == previous
},
)
.expect("Previous chunk should be present");
// Insert the chunk.
chunks.push(ChunkRow {
linked_chunk_id: linked_chunk_id.to_owned(),
previous_chunk: previous,
chunk: new,
next_chunk: next,
});
}
// Insert the chunk.
entry_for_previous_chunk.next_chunk = next;
}
fn remove_chunk(
chunks: &mut Vec<ChunkRow>,
linked_chunk_id: LinkedChunkId<'_>,
chunk_to_remove: ChunkIdentifier,
) {
let entry_nth_to_remove = chunks
.iter()
.enumerate()
.find_map(
|(nth, ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. })| {
(linked_chunk_id == linked_chunk_id_candidate && *chunk == chunk_to_remove)
.then_some(nth)
},
)
.expect("Remove an unknown chunk");
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(
|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_id_candidate && *chunk == next
},
)
.expect("Next chunk should be present");
let ChunkRow { linked_chunk_id, previous_chunk: previous, next_chunk: next, .. } =
chunks.remove(entry_nth_to_remove);
// Insert the chunk.
entry_for_next_chunk.previous_chunk = previous;
}
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_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 { linked_chunk_id: linked_chunk_id_candidate, chunk, .. }| {
&linked_chunk_id == linked_chunk_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
// Insert the chunk.
entry_for_next_chunk.previous_chunk = previous;
}
}
@@ -371,37 +368,43 @@ where
/// particular order.
pub fn unordered_linked_chunk_items<'a>(
&'a self,
target: LinkedChunkId<'a>,
) -> impl Iterator<Item = (&'a Item, Position)> {
self.items_chunks.iter().filter_map(move |item_row| {
if item_row.linked_chunk_id == target {
match &item_row.item {
Either::Item(item_id) => {
Some((self.items.get(&target.to_owned())?.get(item_id)?, item_row.position))
}
Either::Gap(..) => None,
}
} else {
None
}
target: &OwnedLinkedChunkId,
) -> impl 'a + Iterator<Item = (&'a Item, Position)> {
self.items.get(target).into_iter().flat_map(|items| {
// Only keep items which have a position.
items.values().filter_map(|(item, pos)| pos.map(|pos| (item, pos)))
})
}
/// Return an iterator over all items of all room linked chunks, without
/// their actual positions.
/// Return an iterator over all items of a given linked chunk, along with
/// their positions, if available.
///
/// The only items which will NOT have a position are those saved with
/// [`Self::save_item`].
///
/// This will include out-of-band items.
pub fn items(&self) -> impl Iterator<Item = (&Item, LinkedChunkId<'_>)> {
self.items.iter().flat_map(|(linked_chunk_id, items)| {
items.values().map(|item| (item, linked_chunk_id.as_ref()))
})
pub fn items(
&self,
target: &OwnedLinkedChunkId,
) -> impl Iterator<Item = (&Item, Option<Position>)> {
self.items
.get(target)
.into_iter()
.flat_map(|items| items.values().map(|(item, pos)| (item, *pos)))
}
/// Save a single item "out-of-band" in the relational linked chunk.
pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
let id = item.id();
let linked_chunk_id = OwnedLinkedChunkId::Room(room_id);
self.items.entry(linked_chunk_id).or_default().insert(id, item);
let map = self.items.entry(linked_chunk_id).or_default();
if let Some(prev_value) = map.get_mut(&id) {
// If the item already exists, we keep the position.
prev_value.0 = item;
} else {
map.insert(id, (item, None));
}
}
}
@@ -409,7 +412,7 @@ impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Gap: Clone,
Item: Clone,
ItemId: Hash + PartialEq + Eq,
ItemId: Hash + PartialEq + Eq + Ord,
{
/// Loads all the chunks.
///
@@ -427,6 +430,22 @@ where
.collect::<Result<Vec<_>, String>>()
}
/// Loads all the chunks' metadata.
///
/// Return an error result if the data was malformed in the struct, with a
/// string message explaining details about the error.
#[doc(hidden)]
pub fn load_all_chunks_metadata(
&self,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<ChunkMetadata>, String> {
self.chunks
.iter()
.filter(|chunk| chunk.linked_chunk_id == linked_chunk_id)
.map(|chunk_row| load_raw_chunk_metadata(self, chunk_row, linked_chunk_id))
.collect::<Result<Vec<_>, String>>()
}
pub fn load_last_chunk(
&self,
linked_chunk_id: LinkedChunkId<'_>,
@@ -511,13 +530,17 @@ where
impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone,
ItemId: Hash + PartialEq + Eq + Clone + Ord,
{
fn default() -> Self {
Self::new()
}
}
/// Loads a single chunk along all its items.
///
/// The code of this method must be kept in sync with that of
/// [`load_raw_chunk_metadata`] below.
fn load_raw_chunk<ItemId, Item, Gap>(
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
chunk_row: &ChunkRow,
@@ -526,7 +549,7 @@ fn load_raw_chunk<ItemId, Item, Gap>(
where
Item: Clone,
Gap: Clone,
ItemId: Hash + PartialEq + Eq,
ItemId: Hash + PartialEq + Eq + Ord,
{
// Find all items that correspond to the chunk.
let mut items = relational_linked_chunk
@@ -577,11 +600,14 @@ where
collected_items
.into_iter()
.filter_map(|(item_id, _index)| {
relational_linked_chunk
.items
.get(&linked_chunk_id.to_owned())?
.get(item_id)
.cloned()
Some(
relational_linked_chunk
.items
.get(&linked_chunk_id.to_owned())?
.get(item_id)?
.0
.clone(),
)
})
.collect(),
),
@@ -612,8 +638,93 @@ where
})
}
/// Loads the metadata for a single chunk.
///
/// The code of this method must be kept in sync with that of [`load_raw_chunk`]
/// above.
fn load_raw_chunk_metadata<ItemId, Item, Gap>(
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
chunk_row: &ChunkRow,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<ChunkMetadata, String>
where
Item: Clone,
Gap: Clone,
ItemId: Hash + PartialEq + Eq,
{
// Find all items that correspond to the chunk.
let mut items = relational_linked_chunk
.items_chunks
.iter()
.filter(|item_row| {
item_row.linked_chunk_id == linked_chunk_id
&& item_row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
let Some(first_item) = items.peek() else {
// No item. It means it is a chunk of kind `Items` and that it is empty!
return Ok(ChunkMetadata {
num_items: 0,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
};
Ok(match first_item.item {
// This is a chunk of kind `Items`.
Either::Item(_) => {
// Count all the items. We add an additional filter that will exclude gaps, in
// case the chunk is malformed, but we should not have to, in theory.
let mut num_items = 0;
for item in items {
match &item.item {
Either::Item(_) => num_items += 1,
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
ChunkMetadata {
num_items,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
Either::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()
));
}
ChunkMetadata {
// By convention, a gap has 0 items.
num_items: 0,
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
})
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use ruma::room_id;
@@ -1288,16 +1399,17 @@ mod tests {
],
);
let mut events =
relational_linked_chunk.unordered_linked_chunk_items(linked_chunk_id.as_ref());
let events = BTreeMap::from_iter(
relational_linked_chunk.unordered_linked_chunk_items(&linked_chunk_id),
);
assert_eq!(events.next().unwrap(), (&'a', Position::new(CId::new(0), 0)));
assert_eq!(events.next().unwrap(), (&'b', Position::new(CId::new(0), 1)));
assert_eq!(events.next().unwrap(), (&'c', Position::new(CId::new(0), 2)));
assert_eq!(events.next().unwrap(), (&'d', Position::new(CId::new(1), 0)));
assert_eq!(events.next().unwrap(), (&'e', Position::new(CId::new(1), 1)));
assert_eq!(events.next().unwrap(), (&'f', Position::new(CId::new(1), 2)));
assert!(events.next().is_none());
assert_eq!(events.len(), 6);
assert_eq!(*events.get(&'a').unwrap(), Position::new(CId::new(0), 0));
assert_eq!(*events.get(&'b').unwrap(), Position::new(CId::new(0), 1));
assert_eq!(*events.get(&'c').unwrap(), Position::new(CId::new(0), 2));
assert_eq!(*events.get(&'d').unwrap(), Position::new(CId::new(1), 0));
assert_eq!(*events.get(&'e').unwrap(), Position::new(CId::new(1), 1));
assert_eq!(*events.get(&'f').unwrap(), Position::new(CId::new(1), 2));
}
#[test]
@@ -14,13 +14,10 @@
use std::{
collections::HashMap,
pin::Pin,
sync::{Arc, RwLock, Weak},
task::{Context, Poll, Waker},
sync::{Arc, RwLock},
task::Waker,
};
use futures_core::Stream;
use super::{ChunkIdentifier, Position};
/// Represent the updates that have happened inside a [`LinkedChunk`].
@@ -294,6 +291,12 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
slice
}
/// Has the given reader, identified by its [`ReaderToken`], some pending
/// updates, or has it consumed all the pending updates?
pub(super) fn is_reader_up_to_date(&self, token: ReaderToken) -> bool {
*self.last_index_per_reader.get(&token).expect("unknown reader token") == self.updates.len()
}
/// Return the number of updates in the buffer.
#[cfg(test)]
fn len(&self) -> usize {
@@ -321,36 +324,42 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
/// A subscriber to [`ObservableUpdates`]. It is helpful to receive updates via
/// a [`Stream`].
#[cfg(test)]
pub(super) struct UpdatesSubscriber<Item, Gap> {
/// Weak reference to [`UpdatesInner`].
///
/// Using a weak reference allows [`ObservableUpdates`] to be dropped
/// freely even if a subscriber exists.
updates: Weak<RwLock<UpdatesInner<Item, Gap>>>,
updates: std::sync::Weak<RwLock<UpdatesInner<Item, Gap>>>,
/// The token to read the updates.
token: ReaderToken,
}
#[cfg(test)]
impl<Item, Gap> UpdatesSubscriber<Item, Gap> {
/// Create a new [`Self`].
#[cfg(test)]
fn new(updates: Weak<RwLock<UpdatesInner<Item, Gap>>>, token: ReaderToken) -> Self {
fn new(updates: std::sync::Weak<RwLock<UpdatesInner<Item, Gap>>>, token: ReaderToken) -> Self {
Self { updates, token }
}
}
impl<Item, Gap> Stream for UpdatesSubscriber<Item, Gap>
#[cfg(test)]
impl<Item, Gap> futures_core::Stream for UpdatesSubscriber<Item, Gap>
where
Item: Clone,
Gap: Clone,
{
type Item = Vec<Update<Item, Gap>>;
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
fn poll_next(
self: std::pin::Pin<&mut Self>,
context: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let Some(updates) = self.updates.upgrade() else {
// The `ObservableUpdates` has been dropped. It's time to close this stream.
return Poll::Ready(None);
return std::task::Poll::Ready(None);
};
let mut updates = updates.write().unwrap();
@@ -362,14 +371,15 @@ where
updates.wakers.push(context.waker().clone());
// The stream is pending.
return Poll::Pending;
return std::task::Poll::Pending;
}
// There is updates! Let's forward them in this stream.
Poll::Ready(Some(the_updates.to_owned()))
std::task::Poll::Ready(Some(the_updates.to_owned()))
}
}
#[cfg(test)]
impl<Item, Gap> Drop for UpdatesSubscriber<Item, Gap> {
fn drop(&mut self) {
// Remove `Self::token` from `UpdatesInner::last_index_per_reader`.
@@ -394,9 +404,10 @@ mod tests {
};
use assert_matches::assert_matches;
use futures_core::Stream;
use futures_util::pin_mut;
use super::{super::LinkedChunk, ChunkIdentifier, Position, Stream, UpdatesInner};
use super::{super::LinkedChunk, ChunkIdentifier, Position, UpdatesInner};
#[test]
fn test_updates_take_and_garbage_collector() {
@@ -18,7 +18,7 @@
use ruma::{
events::{relation::BundledThread, AnyMessageLikeEvent, AnySyncTimelineEvent},
serde::Raw,
OwnedEventId, UInt,
OwnedEventId,
};
use serde::Deserialize;
@@ -80,9 +80,9 @@ pub fn extract_bundled_thread_summary(
match event.get_field::<Unsigned>("unsigned") {
Ok(Some(Unsigned { relations: Some(Relations { thread: Some(bundled_thread) }) })) => {
// Take the count from the bundled thread summary, if available. If it can't be
// converted to a `u64`, we use `UInt::MAX` as a fallback, as this is unlikely
// converted to a `u32`, we use `u32::MAX` as a fallback, as this is unlikely
// to happen to have that many events in real-world threads.
let count = bundled_thread.count.try_into().unwrap_or(UInt::MAX.try_into().unwrap());
let count = bundled_thread.count.try_into().unwrap_or(u32::MAX);
let latest_reply =
bundled_thread.latest_event.get_field::<OwnedEventId>("event_id").ok().flatten();
+20 -3
View File
@@ -6,17 +6,34 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.13.0] - 2025-07-10
### Features
- [**breaking**] Add a new `VerificationLevel::MismatchedSender` to indicate that the sender of an event appears to have been tampered with.
([#5219](https://github.com/matrix-org/matrix-rust-sdk/pull/5219))
### Refactor
- [**breaking**] The `PendingChanges`, `Changes`, `StoredRoomKeyBundleData`,
`TrackedUser`, `IdentityChanges`, `DeviceChanges`, `DeviceUpdates`,
`IdentityUpdates`, `BackupDecryptionKey`, `DehydratedDeviceKey`,
`RoomKeyCounts`, `BackupKeys`, `CrossSigningKeyExport`, `UserKeyQueryResult`,
`RoomSettings`, `RoomKeyInfo`, and `RoomKeyWithheldInfo` types have been moved
from the `store` module into a new `store/types` module.
([#5177](https://github.com/matrix-org/matrix-rust-sdk/pull/5177))
## [0.12.0] - 2025-06-10
### Features
- [**breaking**] The `ProcessedToDeviceEvent::Decrypted` variant now also have an `EncryptionInfo` field.
Format changed from `Decrypted(Raw<AnyToDeviceEvent>)` to `Decrypted { raw: Raw<AnyToDeviceEvent>, encryption_info: EncryptionInfo) }`
([5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
([#5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
- [**breaking**] Move `session_id` from `EncryptionInfo` to `AlgorithmInfo` as it is megolm specific.
Use `EncryptionInfo::session_id()` helper for quick access.
([4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
([#4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
- Send stable identifier `sender_device_keys` for MSC4147 (Including device
keys with Olm-encrypted events).
@@ -49,7 +66,7 @@ All notable changes to this project will be documented in this file.
### Security Fixes
- Check the sender of an event matches owner of session, preventing sender
spoofing by homeserver owners.
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [CVE-2025-48937](https://www.cve.org/CVERecord?id=CVE-2025-48937), [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
### Bug Fixes
- Remove a wildcard enum variant import which breaks compilation if used with
+1 -1
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.12.0"
version = "0.13.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]
@@ -28,7 +28,7 @@ use zeroize::{Zeroize, Zeroizing};
use super::MegolmV1BackupKey;
use crate::{
olm::BackedUpRoomKey,
store::BackupDecryptionKey,
store::types::BackupDecryptionKey,
types::{MegolmV1AuthData, RoomKeyBackupInfo},
};
+9 -3
View File
@@ -37,7 +37,10 @@ use tracing::{debug, info, instrument, trace, warn};
use crate::{
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
store::{
types::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts},
Store,
},
types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
CryptoStoreError, Device, RoomKeyImportResult, SignatureError,
};
@@ -510,7 +513,7 @@ impl BackupMachine {
?request_id,
"Tried to mark a pending backup as sent but there isn't a backup pending"
);
};
}
Ok(())
}
@@ -644,7 +647,10 @@ mod tests {
use super::BackupMachine;
use crate::{
olm::BackedUpRoomKey,
store::{BackupDecryptionKey, Changes, CryptoStore, MemoryStore},
store::{
types::{BackupDecryptionKey, Changes},
CryptoStore, MemoryStore,
},
types::RoomKeyBackupInfo,
OlmError, OlmMachine,
};
@@ -55,7 +55,10 @@ use tracing::{instrument, trace};
use vodozemac::{DehydratedDeviceError, LibolmPickleError};
use crate::{
store::{Changes, CryptoStoreWrapper, DehydratedDeviceKey, MemoryStore, RoomKeyInfo, Store},
store::{
types::{Changes, DehydratedDeviceKey, RoomKeyInfo},
CryptoStoreWrapper, MemoryStore, Store,
},
verification::VerificationMachine,
Account, CryptoStoreError, EncryptionSyncChanges, OlmError, OlmMachine, SignatureError,
};
@@ -113,7 +116,9 @@ impl DehydratedDevices {
let store =
Store::new(account.static_data().clone(), user_identity, store, verification_machine);
store.save_pending_changes(crate::store::PendingChanges { account: Some(account) }).await?;
store
.save_pending_changes(crate::store::types::PendingChanges { account: Some(account) })
.await?;
Ok(DehydratedDevice { store })
}
@@ -210,7 +215,7 @@ impl RehydratedDevice {
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{ OlmMachine, store::DehydratedDeviceKey };
/// # use matrix_sdk_crypto::{ OlmMachine, store::types::DehydratedDeviceKey };
/// # use ruma::{api::client::dehydrated_device, DeviceId};
/// # async fn example() -> Result<()> {
/// # let machine: OlmMachine = unimplemented!();
@@ -326,7 +331,7 @@ impl DehydratedDevice {
///
/// ```no_run
/// # use matrix_sdk_crypto::OlmMachine; /// #
/// use matrix_sdk_crypto::store::DehydratedDeviceKey;
/// use matrix_sdk_crypto::store::types::DehydratedDeviceKey;
///
/// async fn example() -> anyhow::Result<()> {
/// # let machine: OlmMachine = unimplemented!();
@@ -415,7 +420,7 @@ mod tests {
tests::to_device_requests_to_content,
},
olm::OutboundGroupSession,
store::DehydratedDeviceKey,
store::types::DehydratedDeviceKey,
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
utilities::json_convert,
EncryptionSettings, OlmMachine,
@@ -47,7 +47,7 @@ use crate::{
identities::IdentityManager,
olm::{InboundGroupSession, Session},
session_manager::GroupSessionCache,
store::{Changes, CryptoStoreError, SecretImportError, Store, StoreCache},
store::{caches::StoreCache, types::Changes, CryptoStoreError, SecretImportError, Store},
types::{
events::{
forwarded_room_key::ForwardedRoomKeyContent,
@@ -1119,7 +1119,7 @@ mod tests {
use crate::{
gossiping::KeyForwardDecision,
olm::OutboundGroupSession,
store::{CryptoStore, DeviceChanges},
store::{types::DeviceChanges, CryptoStore},
types::requests::AnyOutgoingRequest,
types::{
events::{
@@ -1134,7 +1134,10 @@ mod tests {
identities::{DeviceData, IdentityManager, LocalTrust},
olm::{Account, PrivateCrossSigningIdentity},
session_manager::GroupSessionCache,
store::{Changes, CryptoStoreWrapper, MemoryStore, PendingChanges, Store},
store::{
types::{Changes, PendingChanges},
CryptoStoreWrapper, MemoryStore, Store,
},
types::events::room::encrypted::{
EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent,
},
@@ -2018,7 +2021,7 @@ mod tests {
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
let decryption_key = crate::store::BackupDecryptionKey::new().unwrap();
let decryption_key = crate::store::types::BackupDecryptionKey::new().unwrap();
alice_machine
.backup_machine()
.save_decryption_key(Some(decryption_key), None)
@@ -44,7 +44,9 @@ use crate::{
InboundGroupSession, OutboundGroupSession, Session, ShareInfo, SignedJsonObject, VerifyJson,
},
store::{
caches::SequenceNumber, Changes, CryptoStoreWrapper, DeviceChanges, Result as StoreResult,
caches::SequenceNumber,
types::{Changes, DeviceChanges},
CryptoStoreWrapper, Result as StoreResult,
},
types::{
events::{

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