Compare commits

...

1124 Commits

Author SHA1 Message Date
Damir Jelić 0b16d488ad chore: Release matrix-sdk version 0.8.0 (#4291)
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-11-19 14:11:19 +01:00
Damir Jelić d40aac89cb fix: Use the DisplayName struct to protect against homoglyph attacks 2024-11-19 11:54:01 +01:00
Damir Jelić e4ebeb8a42 feat(base): Introduce a DisplayName struct
This patch introduces a struct that normalizes and sanitizes display
names. Display names can be a source of abuse and can contain characters
which might make it hard to distinguish one display name from the other.

This struct attempts to make it easier to protect against such abuse.

Changelog: Introduce a DisplayName struct which normalizes and sanitizes
display names.

Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
2024-11-19 11:54:01 +01:00
Erik Johnston 22bbe0c32e Add 'conn_id' field to sync_once span
This is to make it easier to see which sync requests are for which
connection when debugging.
2024-11-19 11:45:32 +01:00
dependabot[bot] 05505a5a48 chore(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 08:32:34 +01:00
Timo 21bb85ac21 feat(Room): Check if the user is allowed to do a room mention before trying to send a call notify event. (#4271) 2024-11-18 16:15:28 +02:00
Benjamin Bouvier f1a442bad0 refactor(send queue): use a specialized mutex for locking access to the state store and being_sent
There was an implicit relationship that the `being_sent` lock needed to
be taken in order to do non-atomic state store operations. With the
change from this commit, the relationship is now more explicit: to get a
handle to the state store, or being_sent, you have to obtain a
`StoreLockGuard` by locking against the store itself. The `WeakClient`
isn't stored in the QueueStorage data structure itself, so it's the only
way to get a `dyn StateStore` from the `QueueStorage`.
2024-11-18 14:55:31 +01:00
Jorge Martín a8a83c3b45 feat(room_preview): Use room directory search as another data source 2024-11-18 13:22:41 +01:00
Tobias Fella 47246483fa doc(crypto): Fix typo
Signed-off-by: Tobias Fella <fella@posteo.de>
2024-11-16 17:24:17 +01:00
Hubert Chathi 31006ab3bf feat(crypto): pin identity when we withdraw verification 2024-11-16 10:26:58 +01:00
Doug 3ed5d34f49 feat(ffi): Add support for including captions with file uploads. 2024-11-15 20:10:50 +01:00
Benjamin Bouvier 232391c6b2 task(send queue): move some assertions back to logged errors
Better safe than panicky.
2024-11-15 10:35:31 +01:00
Jorge Martín cefd5a27f5 feat(ffi): make RoomPreviewInfo::room_type an enum, not an optional String 2024-11-14 16:41:55 +01:00
Jorge Martín 97952902a3 feat(ffi): add RoomPreviewInfo::num_active_members 2024-11-14 16:41:55 +01:00
Jorge Martín bf4a2ed297 feat(ffi): add is_direct and fn inviter to RoomPreview 2024-11-14 16:41:55 +01:00
Benjamin Bouvier a499988621 task(CI): rename the upload code coverage task to make its name clearer 2024-11-14 16:39:28 +01:00
Benjamin Bouvier 0d01cabb8d refactor(widget): get rid of unused limits parameter when constructing a WidgetMachine 2024-11-14 16:23:52 +01:00
Benjamin Bouvier f3c0309fbc refactor(widget): get rid of ProcessingContext and inline it in its callers 2024-11-14 16:23:52 +01:00
Benjamin Bouvier 8070e3c165 refactor(widget): tidy up and start commenting the widget code 2024-11-14 16:23:52 +01:00
Benjamin Bouvier 02c7c2cdfc test(send queue): caching a thumbnail of unknown dimensions removes it from cache after upload 2024-11-14 16:22:07 +01:00
Benjamin Bouvier 9b6de4e436 test(send queue): add more tests for cancellation 2024-11-14 15:33:59 +01:00
Benjamin Bouvier b7d4be9b65 test(send queue): add a test for cancelling an upload while the thumbnail upload is active 2024-11-14 15:33:59 +01:00
Benjamin Bouvier bc86027853 test(send queue): add a test for cancelling a media upload before it's active 2024-11-14 15:33:59 +01:00
Benjamin Bouvier 50db563363 feat(send queue): allow aborting media uploads 2024-11-14 15:33:59 +01:00
Benjamin Bouvier 8fa07ec22d task(send queue): being_sent is an Option, not a set anymore
There can be at most one thing being sent by the send queue, so make
this super explicit.
2024-11-14 15:33:59 +01:00
Timo 7aa930b81c feat(WidgetDriver): Send state from state sync and not from timeline to widget (#4254) 2024-11-14 15:55:25 +02:00
Benjamin Bouvier c02d8cee77 feat!(send queue): add a priority field to maintain ordering of sending
Prior to this patch, the send queue would not maintain the ordering of
sending a media *then* a text, because it would push back a dependent
request graduating into a queued request.

The solution implemented here consists in adding a new priority column
to the send queue, defaulting to 0 for existing events, and use higher
priorities for the media uploads, so they're considered before other
requests.

A high priority is also used for aggregation events that are sent late,
so they're sent as soon as possible, before other subsequent events.
2024-11-14 12:00:08 +01:00
Benjamin Bouvier 2872af234b test(send queue): add a test for the ordering of media vs other events 2024-11-14 12:00:08 +01:00
Jorge Martín d614878436 refactor(sdk): move formatted_caption_from to the SDK, rename it
Add the `markdown` feature to the SDK crate, otherwise we can't use `FormattedBody::markdown`.

Refactor the pattern matching into an if, add tests to check its behaviour.
2024-11-14 10:38:44 +01:00
Jorge Martín afaecdc457 feat(ffi): generate formatted captions for send_* media fns
Changelog: For `Timeline::send_*` fns, treat the passed `caption` parameter as markdown and use the HTML generated from it as the `formatted_caption` if there is none.
2024-11-14 10:38:44 +01:00
Ivan Enderlin aca83fb4ed refactor: Move Event and Gap into matrix_sdk_base::event_cache. 2024-11-13 15:25:58 +01:00
Ivan Enderlin c3e28f7e33 refactor: Move linked_chunk from matrix-sdk to matrix-sdk-common. 2024-11-13 15:25:58 +01:00
Ivan Enderlin 949cd78d94 refactor: Move event_cache_store/ to event_cache/store/ in matrix-sdk-base. 2024-11-13 15:25:58 +01:00
Benjamin Bouvier 99b9c50548 feat(send queue): implement unwedging for media uploads 2024-11-13 14:32:53 +01:00
Benjamin Bouvier 371e7bc052 task(tests): move error_too_large to the generic endpoint
So it can be reused in more contexts than just the sending of an event,
but also for uploads.
2024-11-13 14:32:53 +01:00
Benjamin Bouvier 0541ec7e3f refactor(send queue): use SendHandle for media uploads too 2024-11-13 14:32:53 +01:00
Ivan Enderlin 0509236cf8 doc(sdk): Improve documentation of Client::observe_events. 2024-11-13 11:24:08 +01:00
Ivan Enderlin 6cef7f20c5 feat(sdk): Implement Client::observe_events and Client::observe_room_events.
Changelog: This patch introduces a mechanism similar to
 `Client::add_event_handler` and `Client::add_room_event_handler`
 but with a reactive programming pattern. This patch adds
 `Client::observe_events` and `Client::observe_room_events`.

 ```rust
 // Get an observer.
 let observer =
     client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();

 // Subscribe to the observer.
 let mut subscriber = observer.subscribe();

 // Use the subscriber as a `Stream`.
 let (message_event, (room, push_actions)) = subscriber.next().await.unwrap();
 ```

 When calling `observe_events`, one has to specify the type of event
 (in the example, `SyncRoomMessageEvent`) and a context (in the example,
 `(Room, Vec<Action>)`, respectively for the room and the push actions).
2024-11-13 11:24:08 +01:00
Ivan Enderlin e798a51709 feat(sdk): Implement EventHandlerContext for tuples.
This patch implements `EventHandlerContext` for tuples where each part
implements `EventHandlerContext` itself.
2024-11-13 11:24:08 +01:00
Ivan Enderlin 8f8aad6f4d chore(cargo): Update eyeball-im-util to 0.7.0. 2024-11-13 11:16:30 +01:00
Ivan Enderlin af84c79e69 feat(base): Make ObservableMap::stream works on wasm32-unknown-unknown.
This patch updates `eyeball-im` and `eyeball-im-util` to integrate
https://github.com/jplatte/eyeball/pull/63/. With this new feature, we
can have a single implementation of `ObservableMap` (instead of 2: one
for all targets, one for `wasm32-u-u`). It makes it possible to get
`Client::rooms_stream` available on all targets now.
2024-11-13 11:16:30 +01:00
Ivan Enderlin a920c3fdec fix(ui): Disable share_pos() inside RoomListService.
This patch disables the call to `share_pos()` inside the
`RoomListService` because it creates slowness we need to investigate.
2024-11-13 09:33:04 +01:00
Benjamin Bouvier 9dd2d5ee3c task(architecture): address typo in architecture.md file about EncryptionSyncService 2024-11-12 16:24:07 +01:00
Benjamin Bouvier f341dc4131 refactor(ffi): remove duplicated fields in media event contents
The caption and filenames were weirdly duplicated in each media content,
when the expected behavior is well defined:

- if there's both a caption and a filename, body := caption, filename is
its own field.
- if there's only a filename, body := filename.

We can remove all duplicated fields, knowing this, and reconstruct the
body based on that information. This should make it clearer to FFI users
which is what, and provide a clearer API when creating the caption and
so on.
2024-11-12 16:22:09 +01:00
Damir Jelić d446eb933e test: Add a bunch examples to the MatrixMockServer docs 2024-11-12 14:51:31 +01:00
Damir Jelić 8f0f0fa4d4 chore(test): Don't require two room IDs in the mock_sync_room method 2024-11-12 14:51:31 +01:00
Benjamin Bouvier 36b96ccef2 task(tests): have the test clients use Matrix v1.12 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 5957232e54 task(tests): add a MockClientBuilder to help with creating Clients connected to a MatrixMockServer 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 6f60eea9ce task(tests): refactor mock system to use generic endpoints and avoid code duplication 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 982c6eab54 feat(send queue): retry uploads if they've failed with transient errors 2024-11-12 11:19:05 +01:00
Benjamin Bouvier cfd0c5ce0c feat(media): allow passing a custom RequestConfig to an upload request 2024-11-12 11:19:05 +01:00
Benjamin Bouvier 8e2939bd91 refactor!(send queue): move RoomSendQueue::unwedge to the SendHandle type 2024-11-12 11:06:19 +01:00
Ivan Enderlin 66a79729ed test(ci): Re-enable Complement Crypto. 2024-11-12 10:46:59 +01:00
dependabot[bot] bd5f5f3fe0 chore(deps): bump crate-ci/typos from 1.27.0 to 1.27.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.0 to 1.27.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.27.0...v1.27.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 15:43:34 +01:00
Ivan Enderlin 403be3dea0 test(ci): Disable Complement Crypto for a short period of time. 2024-11-11 13:28:39 +01:00
Ivan Enderlin 4d39d176d9 fix(ffi): Simplify Client::new constructor.
This patch continues to simplification of the `matrix_sdk_ffi::Client`.
The constructor can receive a `enable_oidc_refresh_lock: bool` instead
of `cross_process_refresh_lock_id: Option<String>`, which was a copy of
`matrix_sdk::Client::cross_process_store_locks_holder_name`.

Now there is a single boolean to indicate whether
`Oidc::enable_cross_process_refresh_lock` should be called
or not. If it has to be called, it is possible to re-use
`matrix_sdk::Client::cross_process_store_locks_holder_name`. Once
again, there is a single place to read this data, it's not copied over
different semantics.
2024-11-11 13:28:39 +01:00
Ivan Enderlin d3a232607a fix(ffi): Replace enable_cross_process_refresh_lock by enable_oidc_refresh_crypto_lock.
This patch simplifies a little the `ClientBuilder` API:

* `enable_cross_process_refresh_lock` is removed
* `enable_oidc_refresh_crypto_lock` + `set_session_delegate` must be
  used instead.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 3070154a57 feat(ffi): Add ClientBuilder::cross_process_store_locks_holder_name. 2024-11-11 13:28:39 +01:00
Ivan Enderlin 563c3aae31 feat(ui): EncryptionSyncService and Notification are using Client::cross_process_store_locks_holder_name.
This patch removes the `process_id` argument from
`EncryptionSyncService::new()` and replaces it by
`Client::cross_process_store_locks_holder_name`. The “process ID” is
set when the `Client` is converted into another `Client` tailore for
notification in `NotificationClient` with `Client::notification_client`
which now has a new `cross_process_store_locks_holder_name` argument.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 90b8ba3c2e feat: Client::cross_process_store_locks_holder_name is used everywhere.
See the Changelog Section to get the details.

Changelog: `Client::cross_process_store_locks_holder_name` is used everywhere:
 - `StoreConfig::new()` now takes a
   `cross_process_store_locks_holder_name` argument.
 - `StoreConfig` no longer implements `Default`.
 - `BaseClient::new()` has been removed.
 - `BaseClient::clone_with_in_memory_state_store()` now takes a
   `cross_process_store_locks_holder_name` argument.
 - `BaseClient` no longer implements `Default`.
 - `EventCacheStoreLock::new()` no longer takes a `key` argument.
 - `BuilderStoreConfig` no longer has
   `cross_process_store_locks_holder_name` field for `Sqlite` and
   `IndexedDb`.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 031a96200b feat(sdk): Add Client::cross_proces_store_locks_holder_name().
This patch adds `ClientInner::cross_process_store_locks_holder_name` and
its public method `Client::cross_process_store_locks_holder_name`. This
patch also adds `ClientBuilder::cross_process_store_locks_holider_name`
to configure this value.
2024-11-11 13:28:39 +01:00
Jorge Martín 57e78dd22b feat(ffi): Add Client::create_room_alias function 2024-11-11 13:26:15 +01:00
Jorge Martín 53900294d0 feat(room_alias): Add create_room_alias function
This associates a room alias with an existing room through its room id.
2024-11-11 13:26:15 +01:00
Damir Jelić f483f35573 chore: Allow backoff to be used in the cargo-deny config
Backoff seems to be unmaintained, there's no drop-in replacement so
let's silence the warning for now.
2024-11-11 13:16:02 +01:00
Jorge Martín 204e6e4ca0 feat(sliding_sync): Add m.room.join_rules to the required state
We need the join rules state event to prevent the SDK from assuming a room with an unknown (as in, not loaded) join rule is public.
2024-11-08 17:05:37 +01:00
Jorge Martín ca8c635f62 feat(ffi): add reason field to TimelineItemContent::RoomMembership 2024-11-08 16:40:50 +01:00
Timo b8a61cfc17 feat(WidgetDriver): Support widget redacts (#3987)
Changelog: Implement proper redact handling in the widget driver.
 This allows the Rust SDK widget driver to support widgets that
 rely on redacting.
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-11-08 14:21:35 +01:00
Jorge Martín ab61077a8b fix(ffi): match the right status code in Client::is_room_alias_available 2024-11-08 12:36:51 +01:00
Benjamin Bouvier 26bee1cc38 doc: start an architecture document with a high-level description of the crates 2024-11-08 13:11:19 +02:00
Jorge Martín 46232ee2c1 fix(sdk): add more invalid characters for room aliases 2024-11-08 11:55:26 +01:00
Jorge Martín 7c600fddf0 refactor(ffi): Improve is_room_alias_format_valid so it's more strict.
Previously this only used the Ruma checks, which only handled the initial `#` char and the domain part. With these changes, the name part is also validated, checking it's lowercase, with no whitespaces and containing only allowed chars, similar to what `DisplayName::to_room_alias_name` does.

Moved the code to the SDK crate so it can be properly tested.
2024-11-08 11:55:26 +01:00
Benjamin Bouvier 965a59d5b8 task(tests): create the client with MatrixMockServer::make_client() instead of embedding one into the struct 2024-11-07 17:37:58 +01:00
Benjamin Bouvier f032d16d20 task(tests): mock upload too 2024-11-07 17:37:58 +01:00
Benjamin Bouvier 57137cdd5b task(tests): introduce prebuilt mocks and mocking helpers 2024-11-07 17:37:58 +01:00
Damir Jelić 5d83808143 feat(base): Consider knocked members to be part of the room for display name disambiguation 2024-11-07 16:41:16 +01:00
Damir Jelić df465a0420 chore(base): Improve the docs for the AmbiguityCache 2024-11-07 16:41:16 +01:00
Damir Jelić 4ca69da93c chore(base): Improve the docs for the DisplayNameUsers struct 2024-11-07 16:41:16 +01:00
Damir Jelić 4039359512 chore(base): Clean up the display name ambiguity calculation logic 2024-11-07 16:41:16 +01:00
Damir Jelić 1304902cb4 refactor(base): Rename AmbiguityMap to DisplayNameUsers
The ambiguity map tracks the users which are using a single display
name, so let's reflect that in the name.
2024-11-07 16:41:16 +01:00
Damir Jelić 219be9b731 refactor(base)!: Rename DisplayName to RoomDisplayName 2024-11-07 16:41:16 +01:00
Benjamin Bouvier 237419c740 feat(media): introduce a stripped down MediaThumbnailSettings::new only taking a width and height
This one is used when caching a thumbnail everywhere, and when
attempting to retrieve it; it gives us a single place where to
coordinate the default `MediaThumbnailSettings` parameters.
2024-11-07 13:04:10 +01:00
Benjamin Bouvier 1658397139 refactor!(media): rename MediaThumbnailsSetting::new to with_method() 2024-11-07 13:04:10 +01:00
Benjamin Bouvier bab6761388 fix(event cache): give looser parameters for the deduplicator's bloom filters
The previous values would lead to super large memory allocations, as
observed with `valgrind --tool=massive` on the tiny test added in this
commit:

- for 400 rooms each having 100 events, this led to 540MB of
allocations.
- for 1000 rooms each having 100 events, this led to 1.5GB of
allocations.

This is not acceptable for any kind of devices, especially for mobile
devices which may be more constrained on memory. The bloom filter is an
optimisation to avoid going through events in the room's event list, so
it shouldn't cause a big toll like that; instead, we can reduce the
parameters values given when creating the filters.

With the given parameters, 1000 rooms each having 100 events leads to
1.2MB of allocations.
2024-11-07 12:59:16 +01:00
Timo 5193c2033f feat(ffi): Auto approve the required widget capabilities for element call raise hand and reaction feature. 2024-11-07 12:42:17 +01:00
Jorge Martín 0f9bc20bb8 fix(ffi): use subscribe_reset for verification_state instead, add a regression test 2024-11-07 12:36:07 +01:00
Jorge Martín d54f2a8b04 fix(encryption): emit an updated current verification state before any network request happens
This way we don't get stuck with an outdated value if there is no network connection.
2024-11-07 12:36:07 +01:00
Jorge Martín 00c4071fe1 feat(ffi): allow VerificationStateListener to emit the current state
With this, we get notified of the current verification state almost immediately.

Without it, you may either call it too soon and receive an `Unknown` state or you might have to call `Encryption::wait_for_e2ee_initialization_tasks()` and wait until it's finished to request a valid state value.
2024-11-07 12:36:07 +01:00
Damir Jelić 65287178d1 chore: Bump futures-util in the lock file
We were locked onto a yanked version of futures-util.
2024-11-07 11:10:56 +01:00
Damir Jelić 90b8015d71 chore: Don't ignore the aquamarine RUST-SEC issue, we bumped aquamarine 2024-11-07 11:10:56 +01:00
Damir Jelić f256fe4b24 chore: Remove Ruma from the cargo-deny git dep allow list 2024-11-07 11:10:56 +01:00
Benjamin Bouvier 8d07f36247 chore(send queue): adapt to new locks around the event cache store 😎 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 77ee02f529 refactor!(media): rename MediaRequest to MediaRequestParameters
Because it's not a request we send to the server; it's some of the
request parameters.
2024-11-06 15:33:51 +01:00
Benjamin Bouvier 566a13b16e refactor!(media): inline MediaThumbnailSize into MediaThumbnailSettings
Changelog: all the fields of `MediaThumbnailSize` have been inlined into
 `MediaThumbnailSettings`, and the former type has been removed.
2024-11-06 15:33:51 +01:00
Benjamin Bouvier 4bbe620d0f feat(timeline): use the send queue for media uploads behind a feature toggle 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 9178e4ce33 chore(send queue): review 2024-11-06 15:33:51 +01:00
Benjamin Bouvier c04a73c28d chore(send queue): move code for media upload to its own file 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 13244d808b chore(send queue): move more code around to split work into smaller functions 2024-11-06 15:33:51 +01:00
Benjamin Bouvier e9d5aa1221 chore(send queue): move code around to avoid an enormous send_attachment method 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 57ad256fe1 doc(send queue): beef up the send queue module comment and describe uploads 2024-11-06 15:33:51 +01:00
Benjamin Bouvier c196a9754b feat(timeline): send medias via the send queue 2024-11-06 15:33:51 +01:00
Benjamin Bouvier a8992f37d7 test(send queue): add a smoke test for sending an attachment with the send queue 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 9483703e35 feat(send queue): allow sending attachments with the send queue 2024-11-06 15:33:51 +01:00
Ivan Enderlin 0942dab2fd doc(base): Document Client::event_cache_store a bit more. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 94bd421a8d refactor: Use a common code for try_take_leased_lock.
This code is shared by all `MemoryStore` implementations.
2024-11-06 15:03:50 +01:00
Ivan Enderlin 7b3eb0b6f1 feat(base,sdk): Client now uses EventCacheStoreLock. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 8b85ff2434 feat(base): Create EventCacheStoreLock. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 94c507dd38 test: Testing the cross-process event cache store. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 37304c8cdc refactor: Implement try_take_leased_lock on SqliteEventCacheStore 2024-11-06 15:03:50 +01:00
Ivan Enderlin 16a86587ea refactor: Implement try_take_leased_lock on MemoryStore. 2024-11-06 15:03:50 +01:00
Ivan Enderlin e24d9b3ce3 feat(base): Create LockableEventCacheStore. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 9f11bced10 chore: Rename BackingStore::Error to BackingStore::LockingError.
The idea is to avoid name conflicts when implementing other traits
that use the `Error` associated type.
2024-11-06 15:03:50 +01:00
Ivan Enderlin e5d4ea5964 chore(base): Simplify &* with .as_ref() or .deref().
This patch replaces a `&*` by a `.as_ref()` and a `.deref()`. The result
is the same but it's just simpler for newcomers to understand what
happens.
2024-11-06 15:03:50 +01:00
Jorge Martín fbc914f586 feat(ffi): add room display name to room alias transformation 2024-11-06 09:22:50 +01:00
Jorge Martín bb2d19a1d8 feat(ffi): add room alias format validation 2024-11-06 09:22:50 +01:00
Ivan Enderlin 933033cc25 fix(sdk): Do not always remove empty chunks from LinkedChunk.
This patch introduces `EmptyChunk`, a new enum used to represent whether
empty chunks must be removed/unlink or kept from the `LinkedChunk`. It
is used by `LinkedChunk::remove_item_at`.

Why is it important? For example, imagine the following situation:

- one inserts a single event in a new chunk (possible if a (sliding)
  sync is done with `timeline_limit=1`),
- one inserts many events at the position of the previous event,
  with one of the new events being a duplicate of the first event
  (possible if a (sliding) sync is done with `timeline_limit=10` this
  time),
- prior to this patch, the older event was removed, resulting in an
  empty chunk, which was removed from the `LinkedChunk`, invalidating
  the insertion position!

So, with this patch:

- `RoomEvents::remove_events` does remove empty chunks, but
- `RoomEvents::remove_events_and_update_insert_position` does NOT remove
  empty chunks, they are kept in case the position wants to insert in this
  same chunk.
2024-11-05 16:56:42 +01:00
Benjamin Bouvier b233aa64d2 chore(timeline): rename TimelineItemPosition::Update to UpdateDecrypted 2024-11-05 16:38:13 +01:00
Damir Jelić ace96e372f chore: Fix a warning from an invalid Cargo.toml config for the OIDC example 2024-11-05 16:30:25 +01:00
Mathieu Velten 8865e2ff74 RoomListLoadingState now yields immediately with current value
This fixes a problem when doing an incremental sync at launch,
where `NotLoaded` event	would not be dispatched	until data became
available or timeout is	reached, leading to app waiting for it.
2024-11-05 12:15:16 +01:00
Mathieu Velten 2fa54e5cfa Activate share_pos on the room-list sliding sync instance 2024-11-05 12:15:16 +01:00
Benjamin Bouvier 04275d7c27 refactor!(room list): remove unneeded argument from RoomList::entries_with_dynamic_adapters
Changelog: the parameter `room_info_notable_update_receiver` was removed
 from `RoomList::entries_with_dynamic_adapters`, since it could be
 inferred internally instead.
2024-11-05 10:02:49 +01:00
Benjamin Bouvier 90d6a37b31 refactor(timeline): factor out in-reply-to updates 2024-11-04 17:50:55 +01:00
Benjamin Bouvier 590c2dd9fd fix(timeline): update responses after a successful decryption
Fixes #4196.
2024-11-04 17:50:55 +01:00
Benjamin Bouvier 478dc0ea90 chore(base): refactor internal helpers related to media
Notably, make it super clear what parameters are required to create the
attachment type, since the function doesn't consume the whole
`AttachmentConfig` for realz.
2024-11-04 17:44:30 +01:00
Benjamin Bouvier 7a422fe126 chore(send queue): rename de to dependent_request 2024-11-04 17:42:47 +01:00
Benjamin Bouvier a739ddfc84 chore(event cache store): update test to reflect that previous events and dependent events are cleared
Because the latest migration would clear events to-be-sent from the send
queue, we need to reflect this in this test.
2024-11-04 17:42:47 +01:00
Benjamin Bouvier 1f2e8c5007 refactor!(event cache store): store the serialized QueuedRequestKind, not a raw event
Changelog: The send queue will now store a serialized
 `QueuedRequestKind` instead of a raw event, which breaks the format.
 As a result, all send queues have been emptied.
2024-11-04 17:42:47 +01:00
Benjamin Bouvier c2a921cb58 chore(send queue): move sending of an event to an helper function 2024-11-04 17:42:47 +01:00
Benjamin Bouvier 06e6cba156 chore(event cache store): Support multiple parent key types for dependent requests
This makes it possible to have different kinds of *parent key*, to
update a dependent request. A dependent request waits for the parent key
to be set, before it can be acted upon; before, it could only be an
event id, because a dependent request would only wait for an event to be
sent. In a soon future, we're going to support uploading medias as
requests, and some subsequent requests will depend on this, but won't be
able to rely on an event id (since an upload doesn't return an
event/event id).

Since this changes the format of `DependentQueuedRequest`, which is
directly serialized into the state stores, I've also cleared the table,
to not have to migrate the data in there. Dependent requests are
supposed to be transient anyways, so it would be a bug if they were many
of them in the queue.

Since a migration was needed anyways, I've also removed the `rename`
annotations (that supported a previous format) for the
`DependentQueuedRequestKind` enum.
2024-11-04 17:42:47 +01:00
Jorge Martín 6828f93720 feat(ffi): add Client::is_room_alias_available function 2024-11-04 16:38:44 +01:00
Jorge Martín 7f7b996d24 refactor(ffi): modify Client::resolve_room_alias function
Breaking: `ffi::Client::resolve_room_alias` now returns `Result<Option<ResolvedRoomAlias>, ClientError>` instead of `Result<ResolvedRoomAlias, ClientError>`. This allows the client to match the 3 possible cases:

- The room alias exists.
- The room alias does not exist.
- The function failed internally.
2024-11-04 16:38:44 +01:00
Jorge Martín ee252437d1 fix(pinned_events): get pinned event ids from the HS if the sync doesn't contain it
This should take care of a bug that caused pinned events to be incorrectly removed when the new pinned event ids list was based on an empty one if the required state of the room didn't contain any pinned events info
2024-11-04 16:36:31 +01:00
dependabot[bot] 494532d579 chore(deps): bump crate-ci/typos from 1.26.8 to 1.27.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.8 to 1.27.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.26.8...v1.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 15:56:26 +01:00
Ivan Enderlin 4002136cfb feat(ui): Remove RoomListService::new_with_encryption.
This patch removes `RoomListService::new_with_encryption`. This feature
is not used, not useful since it's best to use `EncryptionSyncService`,
and it can be racy depending on how it's used. To avoid potential errors
and bugs, it's preferable to remove this code.
2024-11-04 15:17:19 +01:00
Ivan Enderlin 5717eb1722 chore(ui): Display the real error of Error::EventCache (#4207)
This patch displays the wrapped error.
2024-11-04 14:12:18 +00:00
Benjamin Bouvier c08194aa44 chore(ffi): introduce AsyncRuntimeDropped helper
This avoids proliferation of `ManuallyDrop` in the code base, by having
a single type that's used for dropping under an async runtime.
2024-11-04 14:37:50 +01:00
Jorge Martín 5d141fce13 task(room_directory_search): add 'server' parameter to the room directory search
Changelog: a new optional `via_server` parameter was added to `sdk::RoomDirectorySearch::search`, to specify which homeserver to use for searching rooms. In the FFI layer, this parameter is called `via_server_name`.
2024-11-04 09:55:11 +01:00
Richard van der Hoff 70bcddfba5 fix(crypto): Fix spelling error in a warning message. 2024-11-01 12:17:13 +00:00
Kévin Commaille 3c48459768 fix: Upgrade Ruma to 0.11.1
Brings in a fix for KeyId::key_name.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-01 12:37:11 +01:00
Benjamin Bouvier 5107f5f23a chore(ffi): in Client::account_url return early when we're not an oidc session
This avoids one spammy log for sessions not using oidc.
2024-11-01 12:07:40 +01:00
Valere d4b9145bc2 Merge pull request #4105 from matrix-org/valere/crypto_ffi_expose_verification_violation
crypto-ffi: Expose `has_verification_violation` for `UserIdentity`
2024-10-31 11:32:46 +01:00
Valere 49f7fe90a9 crypto-ffi: Expose has_verification_violation for UserIdentity 2024-10-31 11:04:42 +01:00
Kévin Commaille 75683d268f refactor(crypto)!: Remove unused OneTimeKey::Key and SessionCreationError::OneTimeKeyUnknown variants
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-30 17:13:47 +01:00
Ivan Enderlin 71abbeb1f1 test(sdk): Use EventFactory to simplify the test cases. 2024-10-30 15:28:38 +01:00
Ivan Enderlin fe79826c7a feat(sdk): Find and remove duplicated events in RoomEvents.
This patch uses the new `Deduplicator` type, along with
`LinkeChunk::remove_item_at` to remove duplicated events. When a new
event is received, the older one is removed.
2024-10-30 15:28:38 +01:00
Ivan Enderlin 7d64ea1bbc feat(sdk): Introduce event_cache::Deduplicator.
This patch introduces `Deduplicator`, an efficient type to detect
duplicated events in the event cache. It uses a bloom filter, and
decorates a collection of events with `Decoration`, which an enum that
marks whether an event is unique, duplicated or invalid.
2024-10-30 15:28:38 +01:00
Kévin Commaille 5158b39277 refactor!: Upgrade Ruma to 0.11.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-30 09:42:19 +01:00
Benjamin Bouvier be88e0ad69 feat(event cache store): Implement renaming media keys 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 50473ba1a8 chore(ring buffer): prefix all tests with test_ in this file 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 5d828d234e feat(ring buffer): implement RingBuffer::iter_mut() 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 9c858c1208 refactor(base): rename all send-queue related "events" to "requests"
Changelog: Renamed all the send-queue related "events" to "requests", so
  as to generalize usage of the send queue to not-events (e.g. medias,
  redactions, etc.).
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 58d46f015b refactor(base): add a QueuedRequestKind enum
In a next commit, the `QueuedEvent` will be renamed to `QueuedRequest`.
This specifies which kind of request we want to send with the send
queue; for now, it can only be an event.
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 4cbd18cb37 refactor(base): move all send-queue related types to a new store::send_queue module
No changes in functionality, only code motion.
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 888f992df0 refactor(base): Renamed StateStore::list_dependend_send_queue_events to load_dependent_send_queue_events 2024-10-29 18:15:10 +01:00
Kévin Commaille ee80291c41 chore: Never skip breaking changes with git-cliff
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-29 17:33:01 +01:00
Ivan Enderlin de3a667eb9 chore: Add an empty line between struct fields. 2024-10-29 17:21:34 +01:00
Andy Balaam ce9dc73376 doc(crypto) Crypto changelog documenting VerificationRequestState::Transitioned interface change 2024-10-29 12:08:46 +00:00
Jorge Martín 03535832ec refactor(room): remove sdk::Room::room_power_levels function
This has been replaced by `sdk_base::Room::power_levels`, which can also be used from `sdk::Room`
2024-10-29 12:55:39 +01:00
Jorge Martín c143f981bd refactor(room_list): only display the knock state events if the current user can act on them
That is, if their power level allows them to either invite or kick users.
2024-10-29 12:55:39 +01:00
Jorge Martín f4a18989fb feat(room_list): allow knock state event as latest_event
This allows clients to display pending knocking requests in the room list items.
2024-10-29 12:55:39 +01:00
Ivan Enderlin 6752cf73df test(sdk): Move tests into their correct module. 2024-10-29 10:52:14 +01:00
Ivan Enderlin e87bed8ef4 chore(sdk): Move all RoomEventCache types from mod.rs to room/mod.rs. 2024-10-29 10:52:14 +01:00
Ivan Enderlin 2f19e2b762 chore(sdk): Rename event_cache/store.rs to event_cache/room/events.rs.
This patch renames the `store.rs` file to `room/events.rs`.
2024-10-29 10:52:14 +01:00
Ivan Enderlin b66024c386 test: Update Synapse from 1.115 to 1.117.
This patch updates Synapse in our CI infrastructure and in the
`matrix-sdk-integration-testing` crate.
2024-10-29 10:50:26 +01:00
Ivan Enderlin c48bb13159 doc: Deal with paragraphes in trailers (#4179)
Git trailers have a funny format.

---------

Signed-off-by: Ivan Enderlin <ivan@mnt.io>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-10-29 09:39:42 +00:00
Andy Balaam 5f0ba1e7df refactor(crypto) Avoid msk and ssk abbreviations in test data 2024-10-28 16:35:39 +00:00
Andy Balaam 91fa1669be refactor(crypto) Rename device methods in IdentityChangeDataSet to match identity names 2024-10-28 16:35:39 +00:00
Andy Balaam a1a4ce0a95 refactor(crypto) Tidy IdentityChangeDataSet test data 2024-10-28 16:35:39 +00:00
Andy Balaam 131921c045 fix(tests) Increase a test timeout to fix occasional flakes I saw locally 2024-10-28 16:35:39 +00:00
Ivan Enderlin b62661bc70 feat(sdk): Map Update::RemoveItem into VectorDiff::Remove in UpdateToVectorDiff.
This patch implements the support of `Update::RemoveItem` inside
`UpdateToVectorDiff` to emit a `VectorDiff::Remove`.
2024-10-28 17:17:01 +01:00
Ivan Enderlin e0be1e8e32 fix(sdk): Fix a bug in an optimisation of UpdatetoVectorDiff.
This patch fixes a bug in an optimisation inside `UpdateToVectorDiff`
when an `Update::PushItems` is handled. It can sometimes create
`VectorDiff::Append` instead of a `VectorDiff::Insert`. The tests will
be part of the next patch.
2024-10-28 17:17:01 +01:00
Ivan Enderlin c23c3b9558 chore(sdk): Rename a couple of variables.
This is another clean up patch.
2024-10-28 17:17:01 +01:00
Ivan Enderlin ca3d5693b4 chore(sdk): Rename a couple of variables.
This is a clean up patch, nothing fancy.
2024-10-28 17:17:01 +01:00
Ivan Enderlin 135c448f2d chore(sdk): Extract code into a map_to_offset method.
This is only code move, nothing has changed.
2024-10-28 17:17:01 +01:00
Ivan Enderlin 01cbce907c feat(sdk): Add LinkedChunk::remove_item_at.
This patch adds the `LinkedChunk::remove_item_at` method, along with
`Update::RemoveItem` variant.
2024-10-28 17:17:01 +01:00
Stefan Ceriu df4a5c36fc Pass the DeviceData in between the Ready and Transitioned states instead of fetching it from the store. 2024-10-28 17:04:50 +02:00
Stefan Ceriu 8492968792 Pass a copy of the other DeviceData in between the (Requested, Ready) and (Created, Ready) states 2024-10-28 17:04:50 +02:00
Stefan Ceriu d31f5b2a72 chore(tests): fix verification integration tests following changes to the data associated with VerificationRequestState::Requested 2024-10-28 17:04:50 +02:00
Stefan Ceriu 8469cb1146 fix(crypto): fix incorrect VerificationMachine tests
- the tests used to incorrectly wrap the to-device content into an event as if it was sent by alice instead of bob
2024-10-28 17:04:50 +02:00
Stefan Ceriu bb8b0cf6b9 Expose requesting device details to the final client 2024-10-28 17:04:50 +02:00
Stefan Ceriu 35dabf7346 feat(crypto): store a copy of the requesting DeviceData within VerificationRequestStates 2024-10-28 17:04:50 +02:00
Stefan Ceriu f771eec3c5 Fix a clippy warning re single matching 2024-10-28 17:04:50 +02:00
Stefan Ceriu 6455585f1e Documentation + cleanup 2024-10-28 17:04:50 +02:00
Stefan Ceriu 3a34b03726 Expose mechanism for registering to verification updates before actually accepting one
- allows handling remote cancellations on verification requests that have not yet been accepted
2024-10-28 17:04:50 +02:00
Stefan Ceriu 8cf0716db2 refactor(ffi): switch to using VerificationRequest::changes instead of direct to_device events. 2024-10-28 17:04:50 +02:00
Stefan Ceriu 660a305cfa feat(ffi): add support for receiving and working with session verification requests
fixup! feat(ffi): add support for receiving and working with session verification requests
2024-10-28 17:04:50 +02:00
dependabot[bot] b2af1eeb20 chore(deps): bump crate-ci/typos from 1.26.0 to 1.26.8
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.0 to 1.26.8.
- [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.26.0...v1.26.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 15:36:15 +01:00
Ivan Enderlin 77b3aa8124 test(sdk): Test the RoomEvents' methods.
This patch adds unit tests for the `RoomEvents`' methods.
2024-10-28 13:30:35 +01:00
Ivan Enderlin cf7cb5c350 doc(sdk): Add more documentation for RoomEvents. 2024-10-28 13:30:35 +01:00
Ivan Enderlin ac7bc6461f chore(sdk): Add an Event type alias for the sake of convenience.
This patch adds an `Event` type alias to `SyncTimelineEvent` to (i) make
the code shorter, (ii) remove some cognitive effort, (iii) make things
more convenient.
2024-10-28 13:30:35 +01:00
Doug 7c39fd6ae5 chore(ffi): Expose supported OIDC prompts in the login details. 2024-10-28 13:26:14 +01:00
Jorge Martin Espinosa 40f4fc138b chore(room_preview): add RoomListItem::preview_room (#4152)
This method will return a `RoomPreview` for the provided room id. 

Also added `fn RoomPreview::leave()` action to be able to decline
invites or cancel knocks, since there wasn't a
`Client::leave_room_by_id` counterpart as there is for join.

The PR also deprecates `RoomListItem::invited_room`, since we have a
better alternative now.

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-10-25 14:33:16 +02:00
Andy Balaam d3d7c03892 doc(crypto) Update a doc comment on update_user_state_to 2024-10-25 14:27:06 +02:00
Andy Balaam 3558886b98 feat(crypto) Support Verified and VerificationViolation updates in IdentityStatusChanges streams 2024-10-25 10:32:14 +01:00
Andy Balaam 47361b93e9 refactor(crypto) Test RoomIdentityState by hard-coding identity states 2024-10-25 10:32:14 +01:00
Andy Balaam f5cdbd8e41 refactor(crypto) Rename test functions to reflect wider name change
and simplify them slightly by combining the wrapper with the main
function. The separation used to be needed, but is not any more.
2024-10-25 10:32:14 +01:00
Benjamin Bouvier f8c23d8aa0 feat(media): add support for async uploads
Changelog: Support for preallocated media content URI has been added in
`Media::create_content_uri()`, and uploading the content for such a
preallocated URI is possible with `Media::upload_preallocated()`.
2024-10-24 16:49:05 +02:00
Benjamin Bouvier 1a3c5045dd chore(room): add copyright notice to sdk/room/mod 2024-10-24 16:49:05 +02:00
Stefan Ceriu ca1d829788 sliding_sync(state): use * for m.call.member when requesting state through sliding sync
- introduced in https://github.com/matrix-org/matrix-rust-sdk/pull/4159 with an empty string
- call members use custom `state_key`s and as such not specifying the sentinel won't match them and state won't be returned
2024-10-24 11:35:41 +03:00
Valere 31e9600078 feat(send_queue): Persist failed to send errors (#4137)
Modify the SendQueue in order to persist the error that cause the event
to fail to send as a `QueueWedgeError`. The `QueueWedgeError` is not a
1:1 mapping for all kinds of errors, but holds variant and information
that the client can react to in order to propose "quick fixes"/solution
before retrying to send.

Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3973 
Also fixes https://github.com/element-hq/element-x-ios/issues/3287
because when a timeline reset occurs the fail to send reason is also
lost.

This PR starts with a refactoring commit
https://github.com/matrix-org/matrix-rust-sdk/commit/e7696003e846b64c41761109f326fd37c4506040
to introduce the new `QueueWedgedError` and move the logic that was in
the ffi layer to convert api errors to SendState error variant. This
`QueueWedgedError` can be directly use in the `SendingFailed` variant
and expose to ffi.

Second commit
https://github.com/matrix-org/matrix-rust-sdk/commit/109c1337465ba7965825fec858fd2a4b8b954611
adds the persistence, `QueuedEvent` now have an optional error field
instead of a `is_weged` boolean. Same for LocalEchoContent::Event.
Adds also Migration for sqlite and indexeddb

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>

Changelog:  We now persist the error that caused an event to fail to send. The error `QueueWedgeError` contains info that client can use to try to resolve the problem when the error is not automatically retry-able. Some breaking changes occurred in the FFI layer for `timeline::EventSendState`, `SendingFailed` now directly contains the wedge reason enum; use it in place of the removed variant of `EventSendState`.
2024-10-23 12:48:55 +02:00
Richard van der Hoff 3291a426d8 test(crypto): rename UtdCause tests with test_ prefix 2024-10-23 09:44:07 +01:00
Richard van der Hoff 1368a8534c feat(crypto): Add more reason codes to UtdCause 2024-10-23 09:44:07 +01:00
Richard van der Hoff 7cfcc8ecc1 refactor(crypto): pass utd info into UtdCause::determine
We'll need this for future changes
2024-10-23 09:44:07 +01:00
Richard van der Hoff c4f9c20115 feat(crypto): rename UtdCause::Membership
Before we do any more work here, give this variant a better name

Breaking-Change: `matrix_sdk_crypto::type::events::UtdCause::Membership` has
been renamed to `...::SentBeforeWeJoined`.
2024-10-23 09:44:07 +01:00
Richard van der Hoff 0c81206653 refactor(timeline): retry_event_decryption: re-use utd cause
Rather than calling `UtdCause::determine` again when an event is successfully
decrypted on retry, re-use the cause we already determined.
2024-10-23 09:44:07 +01:00
Richard van der Hoff a61bc3cbbd refactor(ui): add UTD info to TimelineEventKind::UnableToDecrypt
Stash the reason for the decryption failure in
`matrix-sdk-ui::event_handler::TimelineEventKind::UnableToDecrypt`.

It's not yet used.
2024-10-23 09:44:07 +01:00
Richard van der Hoff 74de617d76 refactor(ui): add TimelineEventKind::UnableToDecrypt
Give `matrix-sdk-ui::event_handler::TimelineEventKind` a new variant which
specifically represents events that could not be decrypted.
2024-10-23 09:44:07 +01:00
Jorge Martín 3f5d54c494 chore(knocking): Add optional reason and server_names parameters to Client::knock 2024-10-22 18:33:18 +02:00
Stefan Ceriu f2f99fb207 chore(ffi): move the store_in_cache timeline media upload parameter before the progress_watcher closure for aesthetic reasons 2024-10-22 18:18:32 +03:00
Benjamin Bouvier 6196ebaba6 chore(media): use the same media method when caching a thumbnail as the default one used in the FFI
The FFI will request a scaled version of the thumbnail by default; let's
use the same cache key when caching the thumbnail after an upload.

Thanks @zecakeh for flagging the issue.
2024-10-22 15:47:58 +02:00
Ivan Enderlin 65bb373379 chore(ui): Add the DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE constant.
This patch refactors 2 `chain(once(…))` with a 1 `chain`. It
also clarifies the extra `required_state` that are added for room
subscriptions.
2024-10-22 14:55:10 +02:00
Ivan Enderlin e62c47132e feat(ui): RoomListService::subscribe_to_rooms no longer has a settings argument.
This patch removes the `settings` argument of
`RoomListService::subscribe_to_rooms`. The settings were mostly composed
of:

* `required_state`: now shared with `all_rooms`, so that we are
  sure they are synced; except that `m.room.create` is added for
  subscriptions.
* `timeline_limit`: now defaults to 20.

This patch thus creates the `DEFAULT_REQUIRED_STATE` and
`DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT` constants.

Finally, this patch updates the tests, and updates all usages of
`subscribe_to_rooms`.
2024-10-22 14:55:10 +02:00
Ivan Enderlin 996b391506 feat(ui): Add m.room.topic and m.room.pinned_events in all_rooms.
This patch adds the `m.room.topic` and `m.room.pinned_events` state
events in the `required_state` of the `all_rooms` sliding sync list of
`RoomListService`.
2024-10-22 14:55:10 +02:00
Ivan Enderlin 74722f48aa fix(ui): Add the m.call.member state event in the required state.
This patch adds the `m.call.member` state event in the `required_state`
for `all_rooms` of the `RoomListService`.
2024-10-22 14:55:10 +02:00
Andy Balaam e3180cdbc5 fix(crypto): Don't warn about verified users when subscribing to identity updates 2024-10-22 12:40:31 +01:00
Benjamin Bouvier 9c03c5dd7e feat(media): cache thumbnails too with a sensible media request key
We can't know which key is going to be used precisely for the thumbnail,
so assume non-animated cropped same-size thumbnail media request.

Changelog: when `SendAttachment::store_in_cache()` is set, the thumbnail
is also cached with a sensible default media request (not animated,
cropped, same dimensions as the uploaded thumbnail).
2024-10-22 12:06:15 +02:00
Benjamin Bouvier b46ebbf34e feat(media): don't clone the data when uploading an encrypted media 2024-10-22 12:06:15 +02:00
Benjamin Bouvier d3bfdb9563 feat(media)!: optionally cache a media after upload
Changelog: Uploaded medias can now be cached in multiple
attachment-related methods like `Room::send_attachment`.
2024-10-22 12:06:15 +02:00
Richard van der Hoff 3887c10444 test: Update tests to use new UTD TimelineEventKind variant
Make the tests behave the same way as the network code, by returning UTDs
as `TimelineEventKind::UnableToDecrypt` instead of `TimelineEventKind::PlainText`.
2024-10-21 17:26:34 +01:00
Richard van der Hoff b69575d5ff refactor(timeline): store UTDs in decrypt_room_event
When `decrypt_room_event` fails to decrypt an event, return the UTD as a
`TimelineEvent` instead of an Error.
2024-10-21 17:26:34 +01:00
Richard van der Hoff 543152d914 refactor(timeline): store UTDs in decrypt_sync_room_event
When `decrypt_sync_room_event` fails to decrypt an event, return the UTD as a
`SyncTimelineEvent` instead of an Error.
2024-10-21 17:26:34 +01:00
Richard van der Hoff c8b38257f1 refactor(common): add TimelineEventKind::UnableToDecrypt 2024-10-21 17:26:34 +01:00
Andy Balaam 2df359d316 fix(experimental-algorithms) Add missing argument to handle_supported_key_request 2024-10-21 16:56:31 +01:00
Benjamin Bouvier 951a4354c6 refactor(timeline): get rid of local_item_by_transaction_id
There's no need for this API anymore.

Changelog: `Timeline::get_event_timeline_item_by_transaction_id` has
been removed. There's no API that makes use of an `EventTimelineItem`
now, those APIs are using a `TimelineEventItemId` instead.
2024-10-21 17:25:23 +02:00
Doug befcd069c3 FFI: Expose UserIdentity::is_verified and add a new Encryption::user_identity method. (#4142) 2024-10-21 13:36:34 +00:00
Richard van der Hoff 0c26988cf5 refactor(base): Remove impl From for SyncTimelineEvent
I feel like the ability to convert straight from a `Raw<AnySyncTimelineEvent>>`
into a `SyncTimelineEvent` is somewhat over-simplified: the two are only
occasionally equivalent, and it's better to be explicit.

Changelog: `SyncTimelineEvent` no longer implements `From<Raw<AnySyncTimelineEvent>>`.
2024-10-21 12:48:14 +01:00
Ivan Enderlin a7f69973c2 feat(sdk): Dropping a UpdatesSubscriber release the reader token for the GC.
The event cache stores its events in a linked chunk. The linked chunk
supports updates (`ObservableUpdates`) via `LinkedChunk::updates()`.
This `ObservableUpdates` receives all updates that are happening inside
the `LinkedChunk`. An `ObservableUpdates` wraps `UpdatesInner`, which
is the real logic to handle multiple update readers. Each reader has a
unique `ReaderToken`. `UpdatesInner` has a garbage collector that drops
all updates that are read by all readers. And here comes the problem.

A category of readers are `UpdatesSubscriber`, returned by
`ObservableUpdates::subscribe()`. When an `UpdatesSubscriber` is
dropped, its reader token was still alive, thus preventing the garbage
collector to clear all its pending updates: they were kept in memory
for the eternity.

This patch implements `Drop` for `UpdatesSubscriber` to correctly remove
its `ReaderToken` from `UpdatesInner`. This patch also adds a test that
runs multiple subscribers, and when one is dropped, its pending updates
are collected by the garbage collector.
2024-10-21 11:17:09 +02:00
Ivan Enderlin 1750bf597f test(sdk): Fix a comment. 2024-10-21 11:17:09 +02:00
Jorge Martín ad677cb6f2 chore(ffi): Add optional canonical_alias field to CreateRoomParameters 2024-10-18 13:21:04 +02:00
Andy Balaam 350a26cee9 refactor(crypto): Extract a test helper function for simulating verification 2024-10-18 11:37:37 +01:00
Benjamin Bouvier 08152bd9fc refactor(sdk)!: rename PrepareEncryptedFile et al. to UploadEncryptedFile
Changelog: Renamed `PrepareEncryptedFile` and
`Client::prepare_encrypted_file` to `UploadEncryptedFile` and
`Client::upload_encrypted_file`.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 89183a3d4b doc(timeline): rejigger a doc comment around sending attachments 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 65ed4f3f22 refactor(ffi): push further and inline parse_mime into the same caller 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 41d392f899 refactor(ffi): commonize creation of the attachment with a thumbnail 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 1ce5160846 refactor(ffi): introduce a parse_mime function to avoid code repetition 2024-10-17 16:54:50 +02:00
Benjamin Bouvier dc4c6b4d73 refactor(media): inline update_audio_message_event into its unique caller
The name wasn't very descriptive, and it's tweaking the content, so
let's do that in place, instead of deferring to another method somewhere
else in the codebase.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 3b33f3779f chore(media): rename upload methods to make their intents clearer 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 7089ff51c4 refactor(room): take the transaction id by ownership in with_transaction_id
This allows letting the caller whether they need to clone it or not, and
avoids a spurious clone in one call site.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier d8de12561b refactor(media): regroup preparation of the media message after uploading the content
The tails of the prepare_attachment_message and
prepare_encrypted_attachment_message were almost the same, with the one
different that they were using different ctors for the `EventContent`
types. In fact, all these `EventContent` types also expose a plain `new`
function that can take in either an encrypted or a plain media source,
so we can commonize the code there.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 56edc9d00f chore(media): rename all event content values to content 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 2ea114d988 chore(media): reduce indent level of upload_thumbnail by one with let-else 2024-10-17 16:54:50 +02:00
Jorge Martín c7708d6154 feature(ffi): Add optional CreateRoomParameters::join_rule_override
This allows clients to set custom join rules for a room, as would be needed for the knock-only rooms, or restricted rooms (those that can only be joined if the user is part of some other room or space).
2024-10-17 16:05:07 +02:00
Benjamin Bouvier bdfe64179b feat(ffi): support custom membership state value in MembershipState 2024-10-17 15:30:34 +02:00
Benjamin Bouvier 59c47fb22d fix(ffi): don't panic when running into an unknown membership state
Fixes #1254.
2024-10-17 15:30:34 +02:00
Benjamin Bouvier 821fa8fa99 refactor(timeline): don't return a bool in Timeline::edit
See previous commit for explanations. This makes for a simpler API
anyways.

Changelog: `Timeline::edit` doesn't return a bool anymore to indicate it
couldn't manage the edit in some cases, but will return errors
indicating what the cause of the error is.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier a5f1769e28 refactor(timeline): return an invalid local echo state error if a local echo disappeared
I think this can't happen, but the send queue can return an error if a
local echo identified by a transaction id doesn't exist anymore in the
database. The only reason the latter could happen is because the local
echo has been sent, in which case an update to the timeline would be
dispatched, and the timeline item would have morphed into a remote echo
in the meantime. So it's really rare that this would happen, and the
`Timeline::redact()` method doesn't have to return a boolean to indicate
success in general.

Changelog: `Timeline::redact()` doesn't return a boolean; previously, it
would only return false if the internal state was invalid, so a new
error `RedactError::InvalidLocalEchoState` has been introduced to
represent that.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier 59fce90943 chore(ffi): revert to using a room method to edit if a remote event couldn't be found in the timeline
This maintains functionality we had prior to the previous commit: if an
event's missing from the timeline (e.g. timeline's been cleared after a
gappy sync response), then still allow editing it.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier 81bebcf692 refactor(timeline): fuse edit_by_id() within edit()
In particular, this means that trying to edit an event that's not
present anymore in a timeline (e.g. after a timeline reset) will fail,
while it worked before.

Changelog: `Timeline::edit_by_id` has been fused into `Timeline::edit`,
which now takes a `TimelineEventItemId` as the identifier for the local
or remote item to edit. This also means that editing an event that's not
in the timeline anymore will now fail. Callers should manually create
the edit event's content, and then send it via the send queue; which the
FFI function `Room::edit` does.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier c4dd2d192e refactor(timeline): fuse redact_by_id() within redact()
Changelog: `Timeline::redact_by_id` has been fused into
`Timeline::redact`, which now takes a `TimelineEventItemId` as an
identifier of the item (local or remote) to redact.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier a901506a53 fix(ffi): don't panic when joining after having cancelled a media upload
Fixes #3573.
2024-10-16 18:52:06 +02:00
Richard van der Hoff 87f89ec561 crypto: update changelog 2024-10-16 16:44:48 +01:00
Richard van der Hoff 2820f5f3b4 crypto: new method OlmMachine::try_decrypt_room_event 2024-10-16 16:44:48 +01:00
Richard van der Hoff 427c59e266 crypto: add UnableToDecryptReason to UnableToDecryptInfo
Add a field to store the reason that the decryption failed
2024-10-16 16:44:48 +01:00
Richard van der Hoff 4a7f924161 timeline: tests for deserializing SyncTimelineEvent with unsigned events 2024-10-16 16:44:48 +01:00
Richard van der Hoff 2829b07305 doc: fix typo in contributing guide
The convention is for changelog entries to be in the imperative, not the past
tense.
2024-10-16 17:06:10 +02:00
Benjamin Bouvier 4f49b23751 refactor(timeline): introduce TimelineUniqueId as an opaque type for the unique identifier
We can now use this type instead of passing a string, which means
there's no way to confuse oneself in methods like
`toggle_reaction_local`.

Changelog: Introduced `TimelineUniqueId`, returned by
`TimelineItem::unique_id()` and serving as an opaque identifier to use
in other methods modifying the timeline item (e.g. `toggle_reaction`).
2024-10-16 16:21:34 +02:00
Benjamin Bouvier 962a78ab13 chore(timeline): always increment the unique id to avoid issues with stall IDs across timeline clears 2024-10-16 16:21:34 +02:00
Jorge Martín 664f6d5f5a feat(knocking): add code to process knocked rooms separately during sync 2024-10-16 16:12:39 +02:00
Benjamin Bouvier 30f3a3c2e4 chore(timeline): fix instrumentation of update_event_send_state
This would not report the `txn_id` field because of the `skip_all`. It's
actually interesting to also get the error, so I'm only skipping self
from now on.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 8df5d655c0 feat(multiverse): add support to toggle a reaction on the last message of a room 2024-10-16 15:03:36 +02:00
Benjamin Bouvier 1552426961 timeline: get rid of conversions from string to TimelineEventItemId
I suppose these were useful at the FFI layer at some point, but they
aren't anymore, so they could be removed.

Changelog: Got rid of `From<String/&str>` for `TimelineEventItemId`.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 8b7494d17b timeline: use a TimelineItemId to react to a timeline item
Changelog: `Timeline::toggle_reaction` now identifies the item that's
reacted to with a `TimelineEventItemId`.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 77e5281781 chore(timeline): instrument timeline tasks with their focus and internal prefix
This would have avoided a few hours of debugging where we thought there
was an issue with multiple timelines spawned at the same time, and then
realized it was expected because of the existence of the pinned timeline
in EX apps.
2024-10-16 14:38:21 +02:00
Andy Balaam efc2e2c4c8 doc(contributing): Recommend --interactive for git rebase --autosquash
Older versions of Git require --interactive when we supply --autosquash,
and it's also probably a good idea generally.

See https://stackoverflow.com/a/77663575/22610 for more info.
2024-10-16 10:47:15 +01:00
Jorge Martín 79798a9de9 refactor(oidc): allow passing a Prompt to get an OIDC url
Changelog: `Client::url_for_oidc_login` is now `Client::url_for_oidc` with an additional `OidcPrompt` parameter. `abort_oidc_login` has been renamed to `abort_oidc_auth`.

This allows clients to directly open the web page they want: the login one, the registration one, consent, etc. It should improve the UX in the registration flow since we can now skip the login one.
2024-10-16 11:28:34 +02:00
Kévin Commaille 3d0423447c fix(qrcode): Do not enable default features of image crate
Gets rid of dependencies for the different image formats.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-16 10:55:11 +02:00
Kévin Commaille 9999d3ba96 chore(sdk)!: Remove image-proc feature and functions to generate a thumbnail
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-16 10:55:11 +02:00
Kévin Commaille ee4ef2eb53 sdk: Remove room from in-memory list when calling Room::forget
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-15 17:04:47 +02:00
Kévin Commaille 0b57ef4bf6 sdk: Remove room from m.direct account data in Room::forget
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-15 17:04:47 +02:00
Benjamin Bouvier 6dd2e3becf refactor(event cache): use a single mutex for the prev and next batch pagination tokens 2024-10-15 16:15:40 +02:00
Benjamin Bouvier 1018d71bb7 refactor(event cache): get rid of the RoomPaginationData data structure
It only contained two fields, and it avoids one extra level of cognitive
overhead and makes the type hierarchy flatter.
2024-10-15 16:15:40 +02:00
Benjamin Bouvier 87472e7679 refactor(event cache): introduce RoomEventCacheState for inner mutable state
This limits the possibility of race conditions in users of this API.
2024-10-15 16:15:40 +02:00
Benjamin Bouvier cdbfae2aee doc(event cache): simplify module comment, as a source file isn't a good todo list
All the items have their equivalent sub item in the issue anyways.
2024-10-15 16:15:40 +02:00
dependabot[bot] 2eca7271ea chore(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 09:27:23 +02:00
dependabot[bot] 92a02a51c4 chore(deps): bump crate-ci/typos from 1.25.0 to 1.26.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.25.0 to 1.26.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.25.0...v1.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 08:04:16 +02:00
Andy Balaam 019d198af8 crypto: Sort IdentityStatusChanges when providing them via subscribe_to_identity_status_changes
Fixes https://github.com/element-hq/element-meta/issues/2566
2024-10-14 10:46:58 +01:00
Damir Jelić e6db85b7d4 chore: Enable the default features for futures-util (#4120)
We depend on the `futures_util::steam_select` macro since 9b36a04b. This
macro requires the async-await-macros and std feature of futures-util.

These features are the default features so let's just stop disabling the
default features for futures-util.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2024-10-13 19:41:11 +00:00
Jonas Platte a4bda1ac66 chore: Move lint configuration out of .cargo/config.toml
This allows removing a lot of hacks to avoid spurious rebuilds.
2024-10-11 12:40:18 +02:00
Jonas Platte e46e63771b chore(ffi): Merge export and export_async attribute macros 2024-10-11 09:57:48 +02:00
Damir Jelić 41a2ad09cf chore: Move the ffi macros into the bindings folder 2024-10-10 19:58:50 +02:00
Benjamin Bouvier 32919405d6 refactor(ffi): use a single provider for lazily computed info 2024-10-10 15:39:55 +02:00
Benjamin Bouvier b002a8da52 refactor(ffi): Don't repeat information in EventTimelineItem about local vs remote echoes 2024-10-10 15:39:55 +02:00
Benjamin Bouvier 85682ac37f chore(timeline): add extra logs to investigate edit issues 2024-10-10 15:17:18 +02:00
Ivan Enderlin 22c765b9ab fix(ui): all_rooms in RoomListService requires the m.room.avatar state.
This patch updates the `required_state` of `all_rooms` inside the
`RoomListService` to add `m.room.name`. Apparently, Synapse doesn't
always update the `response.rooms.*.avatar` field when the avatar is
updated. It's being investigated, but it doesn't hurt to ensure we get
it from the state events.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 3ad8f1d607 test(integration): Fix one test by adding required_state.
To fix the `test_room_avatar_group_conversation`, we need to ask for the
`m.room.avatar` state event from `required_state`. The rest of the patch
rewrites the test a little bit to make it more Rust idiomatic.

The `response.rooms.*.avatar` field from sliding sync should contain the
new avatar, but for the moment, it doesn't. It seems to be a bug.
2024-10-10 15:07:57 +02:00
Ivan Enderlin a4782939b3 test(integration): Fix one test by adding required_state.
To fix the `test_left_room`, we need to ask for the `m.room.member`
state event from `required_state`. The rest of the patch rewrites the
test a little bit to make it more Rust idiomatic.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 72dc307400 fix(base): Add a way for handle_timeline to ignore state events.
Sliding sync expects all state events to be in `required_state`. State
events in `timeline` **must be ignored**. However, in sync v2, state
events in `timeline` **must be handled**.

In the sync response flow, both sliding sync and sync v2 uses the same
`handle_timeline` method. This patch adds an argument to ignore state
events. This is not ideal, but it's a temporary solution as a first
step. The next step is to refactor this code, but let's start easy.

The rest of the patch updates the tests accordingly.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 248cf55272 fix(base): Don't use state events from timeline with sliding sync.
With sliding sync, we must handle state events from `required_state`
only, not from `timeline`, this is a mistake as they might be incomplete
or _staled_.
2024-10-10 15:07:57 +02:00
Damir Jelić 1260e740ba Update the contributing guide with our new git-cliff setup
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-10-10 14:32:46 +02:00
Damir Jelić 9a4a67d488 Document the new release process 2024-10-10 14:32:46 +02:00
Damir Jelić ab0871f299 Call git-cliff as a pre-release hook 2024-10-10 14:32:46 +02:00
Damir Jelić 86d9fe59d2 Create an xtask for the release handling 2024-10-10 14:32:46 +02:00
Damir Jelić 1945b508c3 Create some missing changelog files 2024-10-10 14:32:46 +02:00
Damir Jelić 4c7461357c Add a git-cliff configuration file 2024-10-10 14:32:46 +02:00
Damir Jelić ca7f2ad3d0 Add a cargo-release config 2024-10-10 14:32:46 +02:00
Benjamin Bouvier 711f4cb868 ci: detect unused dependencies with cargo-machete 2024-10-10 14:18:36 +02:00
Benjamin Bouvier cb51a3155a chore: get rid of unused dependencies 2024-10-10 14:18:36 +02:00
Damir Jelić 81119a66d8 ci: Install libsqlite, it does not seem to be part of the latest ubuntu image (#4108) 2024-10-10 13:43:58 +02:00
Richard van der Hoff 9c6413551c Inline SyncTimelineEvent::set_raw
This is only used in one place, and is much better inlined anyway.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 42f0d83b53 timeline: remove redundant Debug implementations
These are no longer required now that the event itself lives in an inner class.
2024-10-09 15:19:26 +01:00
Richard van der Hoff d9167f208a timeline: Extract inner parts of [Sync]TimelineEvent
Pull out the bits of these classes which are dependent on success or otherwise
of decrypting an event to a new enum.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 7f0a3f0e47 timeline: make TimelineEvent::into_raw return a Raw<AnySyncTimelineEvent>
Give `Timeline::into_raw()` the same treatmeant we just gave `Timeline::ra()`.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 8fe61e1fb3 timeline: make TimelineEvent::raw return a Raw<AnySyncTimelineEvent>
I'm going to be replacing the inner structure of `TimelineEvent` with an
implementation that holds a `Raw<AnySyncTimelineEvent>`, rather than a
`Raw<AnyTimelineEvent>`. Prepare for that by changing the accessors to return
`Raw<AnySyncTimelineEvent>`.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 07cfe3da94 timeline: make TimelineEvent fields private
... and add accessors instead.

Give `TimelineEvent` the same treatment we just gave `SyncTimelineEvent`: make
the fields private, and use accessors where we previously used direct access.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 4d472f6aed timeline: make SyncTimelineEvent fields private
... and add accessors instead.

I'm going to change the inner structure of `SyncTimelineEvent`, meaning that
access will have to be via an accessor in future. Let's start by making the
fields private, and use accessors where we previously used direct access.
2024-10-09 15:19:26 +01:00
Richard van der Hoff ce231e6c2b timeline: test for SyncTimelineEvent serialization
I'm going to change the internal structure of `SyncTimelineEvent`, and since
it implements `Deserialize`, we need to not break it. Let's add a test for the
current format.
2024-10-09 15:19:26 +01:00
Richard van der Hoff b36a9ad781 timeline: Add documentation to [Sync]TimelineEvent
I found it hard to understand what these two structs were for, so let's start
by giving them some documentation.
2024-10-09 15:19:26 +01:00
Doug 95ae5d1938 ffi: Rename get_media_file body parameter to filename. 2024-10-09 10:52:35 +02:00
Doug 9d976d0bcf sdk: Update get_media_file to take a filename instead of the body. 2024-10-09 10:52:35 +02:00
Kévin Commaille 17370a5702 sdk: Upgrade aquamarine
Finally get rid of syn 1!

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-08 19:05:35 +02:00
Jorge Martín b793acd2b1 sdk-ui: allow already sent local events to be redacted using redact_by_id
Test this use case.
2024-10-08 17:50:05 +02:00
Mathieu Velten 752706c51d Get back to Recovering syncing when we haven't sync for a while 2024-10-08 17:17:42 +02:00
Benjamin Bouvier 736aa0351c ffi: add our own macro for processing exports
Including one that will always warn if used with async functions, and
the other one always setting the tokio runtime if used for async stuff.
2024-10-08 17:11:39 +02:00
boxdot 4bcb9b7d9f fix: Fix a deadlock between bootstrap_cross_signing and sync (#4060)
`bootstrap_cross_signing` holds a lock on the private identity. In case
a new identity is created, it will try to acquire a lock on `account`.
The latter is locked by `sync`, which tries to acquire a lock on the private identity.

Note that the `bootstrap_cross_signing` call is executed in a separate
task e.g. in `restore_session`. In particular, this task and `sync` both
race to acquire locks described above.

Signed-off-by: boxdot <d@zerovolt.org>
2024-10-08 15:12:40 +02:00
Jorge Martín 867d9c71fd sdk-base: add prev_room_state to RoomInfo
This is useful for the knocking feature since we'll be able to differentiate between rooms that you were just invited from rooms that you knocked and then were granted access, or rooms that you left and rooms where your knocking attempt was rejected.

The `mark_room_as_*` functions have been updated so they reuse the same `set_state` function underneath, which only updates the previous state if the new one doesn't match.
2024-10-08 13:16:40 +02:00
Kévin Commaille 2dcf06fad2 sdk: Add support for authenticated media stable feature
Was added post-merge to the MSC for servers that support
authenticated media but do not support all of Matrix 1.11 yet.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-08 12:21:32 +02:00
Jorge Martín 1fc3450eac ffi & sdk: add room knocking to Client 2024-10-08 12:07:01 +02:00
Benjamin Bouvier 19b9a73ecc ffi: add async_runtime annotation for impl block with async fun 2024-10-08 11:12:01 +02:00
Ivan Enderlin 4d45b02e91 fix(ui): Consider timeline_limit in sliding sync as non-sticky.
This patch changes the behaviour of `timeline_limit` in sliding sync
requests. It previously was sticky, but since it's now mandatory
with MSC4186, it's preferable it to be non-sticky, otherwise in
some scenarios it might default to 0 (its default value). How?
If the server doesn't reply with our `txn_id` (because it doesn't
support sticky parameters or because it misses a `txn_id`), the
next request will be built with a default `timeline_limit` value,
which is zero, and won't get updated to the `timeline_limit` value
from `SlidingSyncListStickyParameters`. This is not good. Instead,
we must consider `timeline_limit` as non-sticky, and moves it from
`SlidingSyncListStickyParameters` to `SlidingSyncListInner`. This is
what this patch does.
2024-10-07 16:38:07 +02:00
Ivan Enderlin a9cfba2c03 chore(cargo): Update ruma. 2024-10-07 16:38:07 +02:00
Benjamin Bouvier ff7e8c75ee ci: try using macos-14 runners for swift-related tasks 2024-10-07 16:14:18 +02:00
Stefan Ceriu 2967b73aff ci: speed up iOS bindings tests by building them on the dev profile
- speed regression introduced when switching the default bindings profile to `reldbg` in #4020
2024-10-07 16:07:38 +02:00
Ivan Enderlin 6f0fbf92e4 Revert "Revert "chore(ui,ffi): Remove the RoomList::entries method.""
This reverts commit af390328b5.
2024-10-07 15:58:38 +02:00
Benjamin Bouvier 4cbc162964 timeline: update replies when a message has been edited 2024-10-07 15:11:09 +02:00
Andy Balaam 6c7acf6faa ffi: Expose the master_key method on UserIdentity 2024-10-07 13:31:38 +01:00
Andy Balaam 181ee643b1 crypto: Expose a way to pin a user's identity 2024-10-07 13:31:38 +01:00
Doug a12a46b777 ffi: Add caption/formatted_caption to media timeline items.
Also includes the computed filename too.
2024-10-07 14:11:19 +02:00
Doug 93fce02606 chore: Update Ruma to add media caption methods.
fixup

fixup
2024-10-07 14:11:19 +02:00
Benjamin Bouvier 351fbf60c1 tests: serialize Unsigned with serde 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5c353923cd timeline: add test for poll edit in relations overriding pending poll edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier dc4cc02926 timeline: add helpers for Flow to avoid redundant code 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 6b543d105f timeline: avoid passing the raw event in two places 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5a1728a468 timeline: rename find_and_remove_pending to maybe_unstash_pending_edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5c8d1d816e timeline: move adding a new msg to its own function 2024-10-07 07:47:05 +02:00
Benjamin Bouvier bd7f0d695b timeline: add TimelineItemContent::as_poll
That's more aligned with `as_message()`, and allows getting rid of one
custom test helper.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier ccf8bf8652 timeline(test): add test for latest poll with a bundled edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier f21de25da0 timeline: apply bundled edits for polls too 2024-10-07 07:47:05 +02:00
Benjamin Bouvier d789983eff timeline: rename extract_edit_content to extract_room_msg_edit_content 2024-10-07 07:47:05 +02:00
Benjamin Bouvier f8e65f53cd timeline: provide the edit JSON for edits either pending or bundled 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 05cbb9e290 timeline(refactor): get rid of the stored event id in the pending_edits array
Since it's implied from the `Replacement` data structure.

Also reuse `find_and_remove_pending` in more places.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier d403bf3431 timeline(code motion): move the poll code to other files
The content of a poll timeline item goes to the content directory. The
data structure handling pending poll events goes into state.

No functional changes, only code motion.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier 56ccda4ded timeline: apply Message edits in a single place 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 157499955a tests: allow passing an u64 to EventBuilder::server_ts 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 8a71ac622d tests: allow bundled relations in EventBuidler 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 45968b2a2b timeline: prefer a bundled edit to a pending edit when adding a new message 2024-10-07 07:47:05 +02:00
Benjamin Bouvier efbf9472f2 latest event: consider bundled edits when constructing an event item from a latest event 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 1434285a1b timeline: extract edits from bundled relations and pass an optional edited content to Message::from_event
No changes in functionality.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier 46856f54af timeline: get rid of one indent level thanks to a let else 2024-10-07 07:47:05 +02:00
Kévin Commaille 3ce2f16d55 base: Do not warn when room in m.direct account data is missing
It can occur that the data contains rooms that were forgotten.
Knowing that we now update that data after every sync, that creates
a lot of noise in the logs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-07 06:51:19 +02:00
Kévin Commaille abf3c6e7b7 ui: Do not warn when no reaction to redact was found
The `handle_reaction_redaction` method is called by `handle_redaction`
for every single redaction event that we receive as a first step to
check if the redaction matches a reaction.
It means that not finding a reaction to redact is perfectly fine and is not worthy of a warning.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 18:00:30 +03:00
Damir Jelić a3a0125421 chore: Set up cargo-deny 2024-10-04 16:53:17 +02:00
Damir Jelić 657c72904a chore: Define our license in every crate we have 2024-10-04 16:53:17 +02:00
Damir Jelić de752eb089 chore: Use a released version of the qrcode crate for the qr-login example 2024-10-04 16:53:17 +02:00
Damir Jelić a4415c9fa5 chore: Use a released version of vodozemac 2024-10-04 16:53:17 +02:00
Andy Balaam 5d46b35d95 crypto: Rename some straggling 'Identities' to 'Identity'
The main enum was renamed to `UserIdentity` and some aliases and
comments had not kept up.
2024-10-04 14:37:12 +01:00
Kévin Commaille 65b422312c chore: Enable the proper feature of tower
We only use `service_fn` which is behind the `util` feature.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 12:51:38 +02:00
Kévin Commaille 7bac0340d6 base: Apply RoomInfo migrations for notable_tags and pinned_events
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 11:58:10 +02:00
Richard van der Hoff 1d1863d323 crypto: Give decrypt_room_event a new return type
I want to do a bit of a refactoring on `TimelineEvent`, so let's start by
giving `decrypt_room_event` its own return type.
2024-10-03 16:23:45 +01:00
Andy Balaam a695e291fa crypto: Rename PreviouslyVerified to VerificationViolation
For consistency with other places, we have now settled on
`VerificationViolation` as the best way to express this situation.
2024-10-03 15:41:55 +01:00
Andy Balaam c5f5bc8496 crypto: FFI bindings for subscribe_to_identity_status_changes 2024-10-03 13:24:24 +01:00
Andy Balaam cd072e6dff crypto: Provide a way to subscribe to identity status changes 2024-10-03 13:24:24 +01:00
Andy Balaam 9b36a04bb9 crypto: Provide the core logic about how identities change in a room when changes occur 2024-10-03 13:24:24 +01:00
Andy Balaam 6b357de947 crypto: Allow accessing the underlying identity on a UserIdentity 2024-10-03 13:24:24 +01:00
Andy Balaam e85e50b185 crypto: Provide DerefMut on OwnUserIdentity and UserIdentity 2024-10-03 13:24:24 +01:00
Erik Johnston c9fd5a0787 Do not log full keys query response which can be very large (#4065)
Signed-off-by: Erik Johnston <erikj@jki.re>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-10-03 10:42:43 +00:00
Valere 6de676f491 Merge pull request #4046 from matrix-org/valere/trust_decoration_decryption_trust_req
crypto: Expose with_decryption_trust_requirement for ClientBuilder
2024-10-02 12:13:15 +02:00
Benjamin Bouvier 06f60e3b62 base: rename account_data to account_data_processor and other review comments 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 96765cad28 base: add helper to process data on a room info from state changes or store 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 4f265ccd22 base: move processing of the direct room inside the AccountDataProcessor 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 759a9b0e18 base: make the dependency to push rules explicit when processing a room's subpart of a response 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 0a854cdbf7 base: get rid of StateChanges::add_account_data 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 40e6e9f028 base: avoid double-contains check in apply_changes
Calling `contains_key` and then doing `if let Some() = .get` have the
same effect.
2024-10-02 11:58:09 +02:00
Benjamin Bouvier 8f4fcf6299 base: experiment with handling global account data as a separate processor 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 2283c28503 base: tidy up sliding sync code around e2ee 2024-10-02 11:58:09 +02:00
dependabot[bot] 59d3608c32 chore(deps): bump actions/checkout from 2.0.0 to 4.2.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.0.0 to 4.2.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4.2.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 14:37:36 +02:00
Benjamin Bouvier 06e9f01a4a chore: fix new typos 2024-10-01 14:07:14 +02:00
dependabot[bot] 0ff63d3008 chore(deps): bump crate-ci/typos from 1.20.10 to 1.25.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.25.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.20.10...v1.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 14:07:14 +02:00
Damir Jelić 5cc7730dd9 chore: Configure dependabot to notify us about outdated github actions 2024-10-01 13:23:08 +02:00
Damir Jelić e5bd7602e4 refactor: Use a match arm when evaluating session comparison results 2024-10-01 13:00:51 +02:00
Damir Jelić 2fc4aacdd0 feat: Prefer room keys with better SenderData when comparing duplicate room keys 2024-10-01 13:00:51 +02:00
Pratik Deshpande f7d99cc506 Added a binding for custom login using JWT 2024-10-01 12:33:06 +03:00
Valere 60319914e1 code review | quick doc and test cleaning 2024-10-01 10:19:25 +02:00
Valere 740356a350 test: ClientBuilder test for decryption trust requirement 2024-10-01 10:19:25 +02:00
Valere 806ee13aa0 ffi: Expose room_decryption_trust_requirement for ClientBuilder 2024-10-01 10:19:25 +02:00
Valere 3fd2f5794e crypto: Expose with_decryption_trust_requirement for ClientBuilder 2024-10-01 10:15:17 +02:00
Damir Jelić dc055c632c chore: Update the changelog to mention the UserIdentity renames 2024-09-30 18:04:04 +02:00
Damir Jelić c2ab795122 doc: Fix some doc links for the user identities 2024-09-30 18:04:04 +02:00
Damir Jelić e7bc510313 refactor: Rename the UserIdentities enum into UserIdentity 2024-09-30 18:04:04 +02:00
Damir Jelić 7efee5a5af refactor: Rename UserIdentity to OtherUserIdentity in the crypto crate 2024-09-30 18:04:04 +02:00
reivilibre 866b6e5f2d client builder: return a ClientBuildError when failing to build, instead of filtering out unexpected errors (#4016)
This old method was checking invariants that were
spooky-action-at-a-distance: these invariants have changed since then,
so this would panic instead of returning a proper error to the caller.

Signed-off-by: oliverw@element.io

---------

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-09-30 13:43:25 +02:00
Kévin Commaille 1e0e815fab base: Remove deprecated StateStore APIs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:18:10 +02:00
Kévin Commaille 99fe49bbac sdk: Remove deprecated Room APIs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:18:10 +02:00
Kévin Commaille 68bc14e567 base: Expose room avatar info from RoomInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:55:39 +03:00
Benjamin Bouvier fe648d9cb5 event cache(refactoring): don't have related event rely on ordering
This was because we used a `BTreeSet`, which doesn't make sense anymore
since the data part of the key got mangled with some value unrelated to
the key itself.
2024-09-30 11:50:43 +02:00
Jorge Martín 743799fbd2 ffi: move the dependency override from ffi/Cargo.toml to the root one
This seems to be the only way to make the log rotation fix work and avoid build warnings like:

```
warning: patch for the non root package will be ignored, specify patch at the workspace root:
package:   matrix-rust-sdk/bindings/matrix-sdk-ffi/Cargo.toml
workspace: = matrix-rust-sdk/Cargo.toml
    Finished `dev` profile [unoptimized] target(s) in 0.30s
```
2024-09-30 10:22:41 +02:00
Jorge Martín ac61fc8830 ffi: create EventShieldsProvider to load shields on demand in the clients 2024-09-27 17:22:55 +02:00
Jorge Martín 52898fa526 ffi: create EventOrTransactionId enum for functions that can receive both 2024-09-27 17:22:55 +02:00
Jorge Martín 263386ea53 ffi: use event_or_transaction_id parameter name for Timeline functions that can take both 2024-09-27 17:22:55 +02:00
Jorge Martín 0082fbc0b4 ffi: add EventTimelineItemDebugInfoProvider to lazily retrieve an event's debug info 2024-09-27 17:22:55 +02:00
Jorge Martín 281a79ffc6 ffi: make From implementations for some event types use values, not references 2024-09-27 17:22:55 +02:00
Jorge Martín bdf303aa57 ffi: remove unused unwrap_or_clone_arc_into_variant macro 2024-09-27 17:22:55 +02:00
Jorge Martín 7dcf45562c ffi: fix bindings not using Arc wrappers 2024-09-27 17:22:55 +02:00
Jorge Martín 548c66750f sdk-ui: Move the event-fetching logic for edit and redact functions to the sdk-ui crate where they can be tested, to the edit_by_id and redact_by_id functions.
Added some tests for those, based on the existing ones.
2024-09-27 17:22:55 +02:00
Jorge Martín 67df36f733 ffi: Turn EventTimelineItem into a record type
This improves parsing times in mobile Clients. On Android, this means a 5-10x faster parsing of timeline events.

To do that I had to:

- Make functions like `edit/redact/forward` take an identifier (EventId/TransactionId) instead of the actual event. This id will be used to look for the actual SDK timeline event in the timeline. This change will make these functions a bit less performant.
- Make `InReplyToDetails` an object instead since a record can't recursively contain itself.
- Turn `EventTimelineItem` into a record type. Do the same with `Message`, which is now `MessageContent`.
2024-09-27 17:22:55 +02:00
Damir Jelić e61fb45504 ffi: Allow recovery to be enabled using a passphrase 2024-09-27 17:04:00 +02:00
Jorge Martín 9b7f89c183 ffi: use fork of tracing crate with a fix for Android logs rotation 2024-09-27 10:37:21 +02:00
Damir Jelić 322c5b3f83 refactor: Fold the private UserIdentities struct into UserIdentity
It doesn't serve any purpose and only confuses people since we have many
similarly named types.
2024-09-26 12:20:36 +02:00
Valere 14ec35e67f Merge pull request #3985 from matrix-org/valere/invisible_crypto/identity_based_withheld_code
crypto: change withheld code for IdentityBased share strategy
2024-09-25 17:24:43 +02:00
Valere 2bb0c50266 crypto: change withheld code for IdentityBased share strategy 2024-09-25 16:57:52 +02:00
Ivan Enderlin d254217217 test(integration): Enable test_room_preview and test_room_avatar_group_conversation.
These tests were failing since the migration from the sliding sync proxy
to Synapse.

Since the previous fixes to re-enable other tests, these 2 passes for
free.
2024-09-25 16:40:49 +02:00
Ivan Enderlin 83ce4c7ca2 test(integration): Fix test_room_notification_count.
This test was failing since the migration from the sliding sync proxy
to Synapse.

This patch fixes the test. The failing parts were:

1. The `timeline_limit` wasn't set, so Synapse was returning an error,
2. The `unread_notifications` was set to 0 and could not be set to 1
   because that's an encrypted room.

The fact `timeline_limit` is now mandatory has been mentioned in the MSC:
https://github.com/matrix-org/matrix-spec-proposals/pull/4186/files#r1775138458
A patch in Ruma has been created. The previous patch in this repository
also contains the fix for the SDK side.

The assertions around `unread_notifications` have been removed. We no
longer use this API anymore (and it should be deprecated by the way).
2024-09-25 16:13:08 +02:00
Ivan Enderlin 2562aa3fee chore(test): Clean a test by rewriting the code a little bit. 2024-09-25 16:13:08 +02:00
Ivan Enderlin fcb1c96869 feat(sdk): Sliding Sync has a required timeline_limit now.
Since MSC4186, the `timeline_limit` value is required.

This patch uses 1 as the default value for `timeline_limit`, and forces
the `timeline_limit` to be defined everywhere.
2024-09-25 16:13:08 +02:00
Ivan Enderlin 88ceeb3513 testing again 2024-09-25 16:12:57 +02:00
Ivan Enderlin a5dc8ff871 test(integration): Remove what seems to be a bug but it's not.
When Bob receives the invite, the room has the correct name. Bob to sync
more to receive the new name. This is not a bug.

This patch updates the `CreateRoomRequest` to set the correct name
immediately.
2024-09-25 16:12:57 +02:00
Ivan Enderlin c3caf6cbca test(integration): Enable test_notification.
This test was failing since the migration from the sliding sync proxy
to Synapse.

This patch fixes the test. The failing part was:

```rust
assert_eq!(notification.joined_members_count, 1);
```

This patch changes the value from 1 to 0. Indeed, Synapse doesn't share
this data for the sake of privacy because the room is not joined.

A comment has been made on MSC4186 to precise this behaviour:
https://github.com/matrix-org/matrix-spec-proposals/pull/4186#discussion_r1774775560.

Moreover, this test was asserting a bug (which is alright), but now
a bug report has been made. The patch contains the link to this bug
report.

The code has been a bit rewritten to make it simpler, and more comments
have been added.
2024-09-25 16:12:57 +02:00
Ivan Enderlin 8469c6465e test(integration): Update Synapse to 1.115. 2024-09-25 14:55:16 +02:00
Jorge Martín 03f7806000 sdk-ui & sdk: add a relationship type filter to load_event_with_relations
We need this for pinned events, where we want reactions, edits and redactions, but we don't want replies or threaded replies.
2024-09-25 12:09:14 +02:00
Kévin Commaille 5ba90611b4 ui: Allow to subscribe to read receipt changes in timeline metadata
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-24 15:48:07 +02:00
Benjamin Bouvier bda2acf5f6 use precise Dockerfile version 2024-09-24 15:09:05 +02:00
Ivan Enderlin 40f1ce80ea test: Bye bye SS proxy, hello Synapse \o/.
This patch removes the sliding sync proxy, and makes the
`matrix-sdk-integration-testing` tests to run against Synapse with
MSC4186 enabled.
2024-09-24 15:09:05 +02:00
Andy Balaam 3492bd6929 doc: Fix a typo in an error message 2024-09-24 13:13:39 +01:00
Jorge Martín 5e9f629edb sdk-ui: make room encryption optional to create a timeline
Instead of forcing the room encryption to be known when the timeline is created and failing if it's not known, take the latest room encryption info as a base value and update it when processing timeline events.

At the time of writing this commit, the encryption info is only used to decide whether shields should be calculated for timeline items or not.
2024-09-24 08:22:51 +02:00
Richard van der Hoff 794dbb36dc crypto: minor fixes to documentation on UserIdentity 2024-09-23 11:25:26 +01:00
Johannes Marbach 2a03de3bd5 Reformat again... 2024-09-19 08:03:36 +02:00
Johannes Marbach 79d8738ff5 Reformat 2024-09-19 08:03:36 +02:00
Johannes Marbach e16ca9d8ba ffi: default to reldbg when building iOS bindings
Relates to: #4009
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-19 08:03:36 +02:00
Benjamin Bouvier 746d7e13ab tests: change strategy for test_ensure_max_concurrency_is_observed 2024-09-18 17:34:24 +02:00
Benjamin Bouvier 1b4f665d99 integration tests: update instructions (#4017)
- explain what these tests are
- mention that it's sometimes needed to rebuild the synapse image

---------

Signed-off-by: Benjamin Bouvier <benjamin@bouvier.cc>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-09-18 14:45:33 +00:00
Jorge Martín 7d1bbfaa32 sdk-base: split handle_account_data and process_direct_rooms
This removes a couple of TODOs in the codebase.
2024-09-18 08:16:56 +02:00
Timo 8119697ef0 MatrixRTC: Fix different devices from the same user overwriting the room info state event. 2024-09-17 17:02:06 +02:00
Jorge Martín decdd6f47e crypto-ffi: update the x86-64 Android workaround to match matrix-sdk-ffi
This workaround was applied to `matrix-sdk-ffi` and it should be used here too
2024-09-17 16:44:52 +02:00
Ivan Enderlin 72febaee57 feat(base): Increase the room_info_notable_update_sender capacity.
This broadcast channel can easily be overflowed if more than 100 updates
arrive at the time. This patch extends the capacity to 2^16 - 1.
2024-09-17 16:37:23 +02:00
Ivan Enderlin 7a2728f8b5 feat(ui): Add logs when RoomList entries receive a lag.
This patch updates `merge_stream_and_receiver` to display an `error!`
when the room info receiver reads an error, like `Closed` or `Lagged`.
This is helpful when debugging.
2024-09-17 16:37:23 +02:00
Damir Jelić f576c72ef8 crypto: Avoid deep copying the OlmMachine when creating a NotificationClient
The NotificationClient, responsible for handling, fetching, and
potentially decrypting events received via push notifications, creates a
copy of the main Client object.

During this process, the Client object is adjusted to use an in-memory
state store to prevent concurrency issues from multiple sync loops
attempting to write to the same database.

This copying unintentionally recreated the OlmMachine with fresh data
loaded from the database. If both Client instances were used for syncing
without proper cross-process locking, forks of the vodozemac Account and
Olm Sessions could be created and later persisted to the database.

This behavior can lead to the duplication of one-time keys, cause
sessions to lose their ability to decrypt messages, and result in the
generation of undecryptable messages on the recipient’s side.
2024-09-16 18:27:31 +02:00
Damir Jelić 1429c1a06a test: Confirm that the notification client doesn't create duplicate one-time keys 2024-09-16 18:27:31 +02:00
Damir Jelić b7ce2dc7e6 chore: Fix a clippy warning 2024-09-16 17:58:29 +02:00
Benjamin Bouvier ea9da3bdad tests: increase timeout duration when awaiting a timeline update 2024-09-16 17:35:42 +02:00
Stefan Ceriu a1bb7c0acc sdk: add account deactivation tests 2024-09-16 17:52:39 +03:00
Stefan Ceriu ea4b9635c9 ffi: add another method that tells the client if account deactivation is supported
- i.e. only works for `m.login.password`
2024-09-16 17:52:39 +03:00
Stefan Ceriu 5b79a9843e ffi: expose method to allow user account deactivate for m.login.password based accounts 2024-09-16 17:52:39 +03:00
Benjamin Bouvier abbe2ec523 tests: increase timeout duration for await_room_remote_echo
Fixes #4003, or so I suspect. The integration tests run in code coverage
can be quite slow, so we can't put timeouts this low.
2024-09-16 14:29:57 +02:00
Benjamin Bouvier 965390cbdc notification client: use the membership state to match an invite 2024-09-16 14:02:07 +02:00
Ivan Enderlin 119bee66ce feat(sdk,ui): SlidingSync::subscribe_to_rooms has a new cancel_in_flight_request argument.
This patch adds a new `cancel_in_flight_request` argument to
`SlidingSync::subscribe_to_rooms`, which tells the method cancel the
in-flight request if any.

This patch also updates `RoomListService::subscribe_to_rooms` to turn
this new argument to `true` if the state machine isn't in a “starting”
state.

The problem it's solving is the following:

* some apps starts the room list service
* a first request is sent with `pos = None`
* the server calculates a new session (which can be expensive)
* the app subscribes to a set of rooms
* a second request is immediately sent with `pos = None` again
* the server does possibly NOT cancel its previous calculations, but
  starts a new session and its calculations

This is pretty expensive for the server. This patch makes so that the
immediate room subscriptions will be part of the second request, with
the first request not being cancelled.
2024-09-16 12:18:07 +02:00
Ivan Enderlin 4fd4410f4a chore: Update to ruma/ruma. 2024-09-16 12:08:15 +02:00
Ivan Enderlin af390328b5 Revert "chore(ui,ffi): Remove the RoomList::entries method."
This reverts commit 98a3a0b3c4.
2024-09-16 12:02:03 +02:00
Ivan Enderlin 98a3a0b3c4 chore(ui,ffi): Remove the RoomList::entries method.
This method is now private inside `matrix_sdk_ui` and removed
from `matrix_sdk_ffi`. This method is returning a stream of rooms,
but updates on rooms won't update the stream (only new rooms
will be seen on the stream). Nobody uses it as far as I know, and
`entries_with_dynamic_adapters` is the real only API we want people
to use.
2024-09-16 11:50:11 +02:00
Jorge Martín aa92e26342 sdk-base: fix handle_account_data behaviour
Handle the account data in the response if not empty, otherwise use the cached one.
2024-09-16 11:20:47 +02:00
Jorge Martín dd13fe6b4e sdk-base: use updated account data for processing direct rooms 2024-09-16 11:20:47 +02:00
Doug a9ed62284e ffi: Expose the server URL to the app too. 2024-09-13 19:32:08 +03:00
Doug 2532c5227f ffi: Add the registration helper URL to the ElementWellKnown file. 2024-09-13 18:37:45 +03:00
Richard van der Hoff 72cc2bd60c crypto: Include megolm ratchet index in logging span fields
This field is helpful as it tells us the sequence number of the message in the
megolm session, which gives us a clue about how long it will have been since
the session should have been shared with us.
2024-09-13 12:18:52 +01:00
Benjamin Bouvier 2408df8bf5 multiverse: highlight which rooms are DMs in the list 2024-09-12 14:58:59 +02:00
Jorge Martín 5827bb7ab3 sdk-ui: fix typo in TimelineState::replace_with_remove_events 2024-09-12 13:21:39 +02:00
Jorge Martín 25111ac9eb sdk-ui: make SlidingSyncRoom not needed in RoomListItem::default_room_timeline_builder.
Having initial items shouldn't be mandatory to create a timeline, the timeline can also be empty.
2024-09-12 13:21:39 +02:00
Jorge Martín 6ae7d3c017 ffi: add FFI fn for Client::await_room_remote_echo(&room_id) 2024-09-12 13:21:39 +02:00
Jorge Martín bbe16db94c sdk: add Client::await_room_remote_echo(&room_id)
This fn will loop until it finds an at least partially synced room with the given id. It uses the `ClientInner::sync_beat` listener to wait until the next check is needed.
2024-09-12 13:21:39 +02:00
Jorge Martín f8961a4382 sdk-base: add Room::is_state_partially_or_fully_synced()
This new fn is used to check when a room is at least partially synced, which seems to be the case with SSS.
2024-09-12 13:21:39 +02:00
Damir Jelić 9e7ab635c6 bindings: Expose the PkEncryption stuff in the crypto crate bindings (#3971) 2024-09-12 09:54:46 +00:00
Damir Jelić a024c010ce chore: Remove the olm-rs dep now that PkEncryption stuff has moved to vodozemac 2024-09-12 10:35:28 +02:00
Damir Jelić 3555474cad crypto: Bump the vodozemac version and remove the PkEncryption compat module
The PkEncryption support now lives inside of vodozemac so no need to
keep our own copy around.
2024-09-11 17:03:50 +02:00
Ivan Enderlin 2b3ad86869 feat(ui): all_rooms in RoomListService requires m.room.canonical_name.
This patch adds `m.room.canonical_name` in the `required_state` of the
`all_rooms` list defined by `RoomListService`.

This is useful to better compute the room name in a more robust way.
2024-09-11 16:06:03 +02:00
Ivan Enderlin 075f3fa9d2 feat(ffi): Add RoomInfo::creator.
This patch adds the `matrix_sdk_ffi::RoomInfo::creator` field that
simply copies the value from `matrix_sdk_base::Room::creator()`.
2024-09-11 15:19:41 +02:00
Ivan Enderlin 47c7d05499 feat(base): Add Room::creator().
This patch adds `Room::creator()` to expose the value from
`RoomInfo::creator()`.
2024-09-11 15:19:41 +02:00
Jorge Martín 4b970e879f sdk: ensure sync_beat is only notified with a successful sync response 2024-09-11 12:46:32 +02:00
Jorge Martín cc0dfd62e7 sdk: notify when a sync response is received by SlidingSync
Previously this was only done in `Client::sync_once`, which made `ClientInner::sync_beat` not that useful.
2024-09-11 12:46:32 +02:00
Jorge Martin Espinosa 31e6df7234 timeline: unify edit and edit_poll functions (#3951)
## Changes

Takes care of [this
TODO](https://github.com/matrix-org/matrix-rust-sdk/blob/9df1c480795c42afcee39e4c7e553d5a927a2680/crates/matrix-sdk-ui/src/timeline/mod.rs#L520).

- sdk & sdk-ui: unify `Timeline::edit` and `Timeline::edit_polls`, the
new fn takes an `EditedContent` parameter now, which includes a
`PollStart` case too.
- ffi: also unify the FFI fns there, using `PollStart` and a new
`EditContent` enum that must be passed from the clients like:

```kotlin
val messageContent = MessageEventContent.from(...)
timeline.edit(event, EditedContent.RoomMessage(messageContent))
```

Since the is mainly about changing the fns signatures I've reused the
existing tests, including one that used `edit_poll` that now uses the
new fn.

---------

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-09-11 08:50:51 +00:00
Ivan Enderlin 2576042194 chore(sdk): Add an info log in sliding sync. 2024-09-11 10:40:59 +02:00
Ivan Enderlin a3ae9dca75 doc(crypto): Improve documentation. 2024-09-11 10:40:59 +02:00
Ivan Enderlin 6e36111462 fix(sdk): Mark tracked users as dirty when the SS connection is reset.
There is a non-negligible difference MSC3575 and MSC4186 in how the
`e2ee` extension works. When the client sends a request with no `pos`:

* MSC3575 returns all device lists updates since the last request
  from the device that asked for device lists (this works similarly to
  to-device message handling),

* MSC4186 returns no device lists updates, as it only returns changes
  since the provided `pos` (which is `null` in this case); this is in
  line with sync v2.

Therefore, with MSC4186, the device list cache must be marked as to be
re-downloaded if the `since` token is `None`, otherwise it's easy to
miss device lists updates that happened between the previous request and
the new “initial” request.
2024-09-11 10:40:59 +02:00
Ivan Enderlin b7bde3cabe feat(crypto): Implement OldMachine::mark_all_tracked_users_as_dirty.
This patch adds the `OldMachine::mark_all_tracked_users_as_dirty`.

This patch rewrites a bit `OlmMachine::new_helper` by extracting some
piece of it inside `OlmMachine::new_helper_prelude`. With that, we
can rewrite `OlmMachine::migration_post_verified_latch_support` to use
`IdentityManager::mark_all_tracked_users_as_dirty`.
This latter is the shared implementation with
`OlmMachine::mark_all_tracked_users_as_dirty`.

This patch adds a test for `OlmMachine:mark_all_tracked_users_as_dirty`.
2024-09-11 10:40:59 +02:00
Benjamin Bouvier cb825864b9 event cache: reset paginator state when receiving a limited timeline 2024-09-11 09:40:30 +02:00
Jorge Martín 08df153ed9 sdk-ui: create TimelineState::replace_all which combines clear and add_remote_events_at in the same transaction 2024-09-10 17:23:15 +02:00
Ivan Enderlin a6f84d8513 feat(sdk): Use the user ID to discover the sliding sync proxy.
This patch improves `Client::available_sliding_sync_versions` when
trying to detect the sliding sync proxy. Previously, we were relying
on the `Client::server` to send the `discover_homeserver::Request`.
Sadly, this value is an `Option<_>`, meaning it's not always defined
(it depends how the `Client` has been built with `HomeserverConfig`:
sometimes the homeserver URL is passed directly, so the server cannot
be known).

This patch tries to find to discover the homeserver by using
`Client::server` if it exists, like before, but it also tries by using
`Client::user_id`. Another problem arises then: the user ID indeed
contains a server name, but we don't know whether it's behind HTTPS or
HTTP. Thus, this patch tries both: it starts by testing with `https://`
and then fallbacks to `http://`.

A test has been added accordingly.
2024-09-10 14:09:50 +02:00
Benjamin Bouvier 729ba3e22b ffi: rename RustShieldState to SdkShieldState
This is all Rust code, after all :)
2024-09-10 11:20:52 +02:00
Doug 83cc0acf7b ffi: Expose RoomSendQueue::unwedge to allow resending. 2024-09-09 17:47:06 +02:00
Benjamin Bouvier ef6237045e timeline: use a RingBuffer instead of a hashmap to put an upper bound on the number of pending edits 2024-09-09 17:13:11 +02:00
Benjamin Bouvier d005311235 timeline: add comments for each item in TimelineMetadata 2024-09-09 17:13:11 +02:00
Benjamin Bouvier c66ea8162c timeline: use fewer early returns in code around pending edits
😥
2024-09-09 17:13:11 +02:00
Benjamin Bouvier 40c1e8a2da timeline: add more tests for pending edits 2024-09-09 17:13:11 +02:00
Benjamin Bouvier d2709c0679 timeline: handle pending poll edits too 2024-09-09 17:13:11 +02:00
Benjamin Bouvier c9a46173b9 timeline: some renamings around poll edits 2024-09-09 17:13:11 +02:00
Benjamin Bouvier 8a2929fb51 timeline: apply pending edits when adding the new item, not as a separate update 2024-09-09 17:13:11 +02:00
Benjamin Bouvier 79f412790f timeline: stash edits around in case they arrive before the related event 2024-09-09 17:13:11 +02:00
Benjamin Bouvier 5abff2970c room: mark encryption state as missing if a room thinks it's not encrypted after requesting it 2024-09-09 17:04:01 +02:00
Damir Jelić dcc20b6c96 backups: Rename the term session to room key
The term session is usually only used in the crypto crate to reference a
Megolm session, the rest of the SDK uses the name from the event and the
Matrix spec, this should lower the amount of confusion since the main
crate has already a session concept and its unrelated to end-to-end
encryption.
2024-09-09 16:51:15 +02:00
Damir Jelić 4e541ad825 backups: Expire downloaded room keys so they get retried if a better one is found 2024-09-09 16:51:15 +02:00
Damir Jelić 67a4a322f5 backups: Don't queue up room keys to be downloaded if backups aren't enabled 2024-09-09 16:51:15 +02:00
Damir Jelić 26e6a038a1 backups: Don't mark a room key as downloaded if we did not attempt to download it 2024-09-09 16:51:15 +02:00
Damir Jelić e8a920118f timeline: Retry decryption if a room key backup gets enabled 2024-09-09 16:51:15 +02:00
Damir Jelić 626b3d152c test: Check that we don't mark keys as downloaded before backups were enabled 2024-09-09 16:51:15 +02:00
Damir Jelić 38ed66c1b1 test: Test that a timeline decrypts an event if a backup got enabled 2024-09-09 16:51:15 +02:00
Benjamin Bouvier 19e89bbd6a tests: make test_incremental_upload_of_keys_sliding_sync less dependent on timing 2024-09-09 14:21:51 +02:00
Benjamin Bouvier a07be884b7 tests: try to address intermittent failure of test_incremental_upload_of_keys
My theory is that the intermittent failure depends on the ordering of
the requests, and if the /keys/upload request happened before the key
backup request, then after failing the next key backup request wouldn't
run.

This is likely a small typo that the key upload returns a 404 error
instead of a 200, let's see if this improves the situation.
2024-09-09 14:21:51 +02:00
Valere 24d4e60c2b crypto: Bugfix - UTD messages showing unexpected red padlock warning 2024-09-09 14:59:20 +03:00
Benjamin Bouvier 7d7142add3 timeline: check that unique IDs are indeed unique
And log an error in production builds if that's not the case.
2024-09-09 12:10:41 +02:00
Ivan Enderlin 57352f0154 chore(sdk): Rename a variable from_… to with_….
This patch renames the variable `from_msc4186` to `with_msc4186` for
better clarity.
2024-09-09 12:01:33 +02:00
Ivan Enderlin 7eea5628d3 chore: Update Ruma feat-sss to its latest commit.
The new commits from `feat-sss` are about migrating
`unstable-simplified-msc3575` to `unstable-msc4186`.
2024-09-09 12:01:33 +02:00
Ivan Enderlin ea794bb9f2 chore(sdk): Replace “simplified sliding sync” by “MSC4186”.
Simplified sliding sync finally has an MSC number: 4186. Let's use this
name when possible to clarify the code.
2024-09-09 12:01:33 +02:00
Jorge Martín 10a0d59012 sdk-ui: fix max concurrent requests for pinned events timeline. 2024-09-09 09:37:50 +02:00
Ivan Enderlin 16fd88c419 chore(sdk): Improve a doc and format code. 2024-09-09 09:20:01 +02:00
Damir Jelić 1eecb2d603 ui: Remove the e2e-encryption feature from the matrix-sdk-ui crate
It does not make much sense to create an UI client that does not support
end-to-end encryption, besides disabling the feature was broken for
quite some time.
2024-09-06 15:37:18 +02:00
Damir Jelić 98ba714b20 sdk: Fix a clippy warning 2024-09-06 13:51:04 +02:00
Andy Balaam 07aa6d7bc7 doc: Fix missing 'o' in the doc comment for the recovery module 2024-09-05 17:12:15 +01:00
Benjamin Bouvier 9df1c48079 timeline(tests): ASCII art 2024-09-05 16:46:25 +02:00
Benjamin Bouvier 977a9995fe timeline(tests): simplify matching a day divider or a read marker using public APIs 2024-09-05 16:46:25 +02:00
Benjamin Bouvier f978960d30 timeline: don't insert a read marker when all subsequent events have been inserted by ourselves 2024-09-05 16:46:25 +02:00
Benjamin Bouvier 3f93324a85 timeline(style): gather common code under the same arm branches 2024-09-05 16:46:25 +02:00
Richard van der Hoff 3204953738 crypto: update changelog 2024-09-05 13:22:10 +01:00
Richard van der Hoff 88b005ace3 crypto: clarify logging on conclusion of verification requests
* Not verifying the remote device/user is normal: log it at debug rather than
  info.
* On the other hand, if we do verify something, let's log that at info rather
  than trace.

Also fix a comment, while we're here.
2024-09-05 13:22:10 +01:00
Richard van der Hoff c761a84acd crypto: logging during QR code verifications
* Upgrade the log when we get the "reciprocate" message (which tells us the
   other side has scanned our QR code) to debug, instead of trace.
 * Warn if we get a reciprocate we don't understand
 * Log when the user confirms that the other side has scanned successfully.
2024-09-05 13:22:10 +01:00
Richard van der Hoff a2bfc07ecc crypto: log the method on an m.verification.start message
This is the message that tells us whether the other side wants to do QR code or
SAS (emoji) verification. Knowing which they have chosen is really helpful for
following the flow!
2024-09-05 13:22:10 +01:00
Richard van der Hoff b1a533a071 crypto: log flow_id when processing verification requests
Attach the flow_id (the transaction ID or message ID from the `request`
message) to the span, so that it is displayed alongside loglines that happen
when processing the request.
2024-09-05 13:22:10 +01:00
Richard van der Hoff fed418d9a8 crypto: log when we show a QR code
Take the logging that happens when a QR code verification is added to the
`verification cache`, and push it down to the `VerificationCache` itself. Doing
so means that we will log when we *show* a QR code as well as when we scan it.

I would have found this helpful when trying to debug a verification flow this
week.
2024-09-05 13:22:10 +01:00
Richard van der Hoff 2c2d8e9ff0 crypto: log details of our public identity when we update it
For debugging, it's useful to have a record of what we believe our own public
cross-signing keys to be. Currently, we log the keys at startup if we restore
them from the database, but if we subsequently create, or download, a set of
keys, they aren't logged.
2024-09-05 13:22:10 +01:00
Andy Balaam b9b8de7ff1 crypto: Mark all new SenderData info as non-legacy
Since we now have a clear idea of the structure, and anything we create
now should be usable in future.
2024-09-05 13:54:01 +02:00
Benjamin Bouvier 552df0e4c6 timeline(tests): use the event factory in a few more places 2024-09-05 10:01:37 +02:00
Benjamin Bouvier 12f36d5972 timeline: document and rename some concepts around pending poll events 2024-09-05 10:01:37 +02:00
Richard van der Hoff f7ee643475 crypto: update changelog 2024-09-04 16:07:03 +01:00
Richard van der Hoff 73486b2b7b crypto: update senderdata integration tests
Extend the integration tests for megolm sender data to check that we update
existing inbound group sessions when we get a `/keys/query` response.
2024-09-04 16:07:03 +01:00
Richard van der Hoff 3c27f83857 crypto: update sender data on /keys/query responses
When we receive an `/keys/query` response, look for existing
inboundgroupsessions created by updated devices, and see if we can update any
of their senderdata settings.
2024-09-04 16:07:03 +01:00
Richard van der Hoff 385c2b8e71 crypto: Expose sender_data_finder module as pub(crate)
This module has a number of useful types (in particular, error types). Rather
than addding even more types to the top level module, let's export the
`sender_data_finder` module as a whole.
2024-09-04 16:07:03 +01:00
Richard van der Hoff 6bc9887314 crypto: fix memorystore groupsession batch query
If the previous session is removed from the list, we should still be able to
continue iterating through the *rest* of the list.
2024-09-04 16:07:03 +01:00
Richard van der Hoff 30d3d9d26c crypto: expose InboundGroupSession.sender_data
We need write access to this in the integration tests
2024-09-04 16:07:03 +01:00
Hubert Chathi dfb67c88e6 crypto: add changelog 2024-09-04 14:59:21 +01:00
Hubert Chathi 98a79de811 crypto: check trust requirement when decrypting 2024-09-04 14:59:21 +01:00
Hubert Chathi 62d4abd454 crypto: add DecryptionSettings parameter to functions 2024-09-04 14:59:21 +01:00
Hubert Chathi 7b71d3ca1b crypto: add error code for sender device not sufficiently trusted on decryption 2024-09-04 14:59:21 +01:00
Hubert Chathi 31b4d0a2d1 crypto: add setting for checking sender device trust on decryption 2024-09-04 14:59:21 +01:00
Stefan Ceriu 14ee78c54d ffi: expose methods for manually withdrawing certain users' verification or trusting their devices and resending failed messages 2024-09-04 15:39:35 +03:00
Stefan Ceriu f3d3924bb6 send_queue: publish retry updates when unwedging an event; have the timeline update the corresponding item in response. 2024-09-04 15:39:35 +03:00
Stefan Ceriu 6c704352a9 send_queue: add mechanism for unwedging and resending a request based on its transaction identifier 2024-09-04 15:39:35 +03:00
Damir Jelić 0db0ea0977 docs: Add PR review guidelines. 2024-09-04 13:42:16 +02:00
Benjamin Bouvier 3f7909641f client builder(nit): avoid unnecessary clone 2024-09-04 12:41:17 +02:00
Benjamin Bouvier 5b0ad01bab event cache: don't return a useless Option 2024-09-04 12:41:17 +02:00
Kévin Commaille b0e8121347 sqlite: Bump sqlite crates
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-04 10:55:38 +02:00
dependabot[bot] aa94ad846b build(deps): bump quinn-proto from 0.11.3 to 0.11.8
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.3 to 0.11.8.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.3...quinn-proto-0.11.8)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 10:50:34 +02:00
Hubert Chathi 1dd8c908c5 crypto: Error when sending keys to previously-verified users with identity-based strategy (#3896) 2024-09-03 18:06:32 +01:00
Stefan Ceriu 5b14fe6f34 crypto: fix OIDC cross-signing reset flows after backend authorization failure response change (#3933) 2024-09-03 14:43:46 +00:00
Ivan Enderlin a737421875 chore(ui): Rename variables.
This is not a timestamp but a regular stamp. Make it clear with the
variable names.
2024-09-03 15:52:05 +02:00
Ivan Enderlin 49252b5342 test: Restore Complement Crypto. 2024-09-03 11:52:32 +02:00
Richard van der Hoff d8b0f9f3d7 crypto: add cryptostore integ test
Add a new integration test for
`CryptoStore::get_inbound_group_sessions_for_device_batch`
2024-09-02 18:07:38 +01:00
Richard van der Hoff 1de99161e2 indexeddb: implement get_inbound_group_sessions_for_device_batch 2024-09-02 18:07:38 +01:00
Richard van der Hoff 675f576343 indexeddb: add new index on inbound_group_sessions
Add an index on `(sender_key, sender_data_type, session_id)`.
2024-09-02 18:07:38 +01:00
Richard van der Hoff 7cf8e9eb9b indexeddb: add new fields to InboundGroupSessionIndexedDbObject
Add new `session_id`, `sender_key` and `sender_data_type` properties to stored
inbound group session objects.
2024-09-02 18:07:38 +01:00
Richard van der Hoff 7bcc920514 sqlite: add get_inbound_group_sessions_for_device_batch 2024-09-02 18:07:38 +01:00
Richard van der Hoff 12653fb2b6 sqlite: add new curve_key and sender_data_type columns 2024-09-02 18:07:38 +01:00
Richard van der Hoff eeaf31ce53 crypto: implement MemoryStore::get_inbound_group_sessions_for_device_batch 2024-09-02 18:07:38 +01:00
Richard van der Hoff 228a117ccb crypto: add get_inbound_group_sessions_for_device_batch to CryptoStore 2024-09-02 18:07:38 +01:00
Andy Balaam 3f408a9a36 crypto: Create a SenderDataType enum 2024-09-02 18:07:38 +01:00
Kévin Commaille 7f4e79e2a3 Add link to SQLite docs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-02 17:44:30 +02:00
Kévin Commaille 7807ed8bda sqlite: Update last access time first to force write transaction
Avoids errors when the read transaction tries to upgrade to a write transaction.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-02 17:44:30 +02:00
Richard van der Hoff f8dd5c76d2 crypto: Add tests for sender_data after receiving megolm sessions 2024-09-02 15:36:45 +01:00
Richard van der Hoff 6068d3e870 crypto: Pull out Session::build_encrypted_event
Break up `encrypt` a bit, so that we can write tests that do something slightly
different.
2024-09-02 15:36:45 +01:00
Richard van der Hoff 76d161ac9a crypto: expose SenderDataFinder::find_using_device_data
Turns out that most of the places we call `find_using_device_keys`, we already
have a DeviceData. So we might as well pass that in directly, rather than
extracting the device keys and then rebuilding a DeviceData.
2024-09-02 15:30:29 +01:00
Richard van der Hoff ca42657abb crypto: Log the received device keys on an encrypted olm message
Attempt to summarise the received keys.
2024-09-02 15:14:46 +01:00
Ivan Enderlin 4636a9177e chore(sdk): Rename HomeserverConfig variants.
This patch renames `HomeserverConfig::Url` to `HomeserverUrl`, and
`HomeserverConfig::ServerNameOrUrl` to `ServerNameOrHomeserverUrl`.
Funnily, the methods on `ClientBuilder` doesn't need to be renamed to
match the new naming since they already use this naming!
2024-09-02 15:51:41 +02:00
Ivan Enderlin 5e662e855d fix(sdk): Don't subscribe to already subscribed rooms.
When subscribing to an already subscribed room, the subscription state
was reset, the subscription was resent by cancelling the in-flight
request. All this is useless and can create a feeling of lag.

This patch checks if a room is subscribed first. If it is, nothing
happens. If it is not, the room subscription is created, and the
in-flight request is cancelled.

Tests are updated to reflect this new change.

Note: room subscription settings are not taken into account in this
“presence” pattern. It means that if we subscribe to an already
subscribed room but with different settings, nothing will happen. The
server will ignore the new settings anywhere for the moment.
2024-09-02 14:52:42 +02:00
Benjamin Bouvier 513c80df5e timeline: add comment to TimelineFocus::PinnedEvents 2024-09-02 14:29:08 +02:00
Benjamin Bouvier 956dda1073 timeline: rename TimelineInner to TimelineController 2024-09-02 14:29:08 +02:00
Benjamin Bouvier de6016de1e timeline: remove Inner in TimelineInnerStateTransaction 2024-09-02 14:29:08 +02:00
Benjamin Bouvier 72f42a46d8 timeline: remove Inner in TimelineInnerState 2024-09-02 14:29:08 +02:00
Benjamin Bouvier 03a4cc46ad timeline: remove Inner in TimelineInnerSettings 2024-09-02 14:29:08 +02:00
Benjamin Bouvier 63ca064f93 timeline: remove Inner in TimelineInnerMetadata 2024-09-02 14:29:08 +02:00
Ivan Enderlin a19cb24567 fix(sdk): Sliding sync discovers the proxy on the server, not the homserver.
The `.well-known` file is located on the server, not the homeserver.
This patch fixes that along with the associated tests.
2024-09-02 14:18:45 +02:00
Ivan Enderlin 99c44ee883 fix(sdk): Don't confuse server and homeserver.
This patch fixes an error where the `homeserver` is used for the
`server` value.
2024-09-02 14:00:10 +02:00
Ivan Enderlin d0c5d87a96 doc(sdk): Improve documentation of HomeserverConfig. 2024-09-02 14:00:10 +02:00
Ivan Enderlin 222be6983c test(sdk): Test HomeserverConfig::discover.
This API is tested via `Client` and `ClientBuilder` but it's preferable
to unit testing it too, it makes things clearer and cleaner.
2024-09-02 14:00:10 +02:00
Ivan Enderlin f3bc24e98f chore(sdk): Extract HomeserverConfig & co. in its own module.
This patch moves `client/builder.rs` into `client/builder/mod.rs`.
This patch also moves the `HomeserverConfig` type and its siblings
(`discover_homeserver`, `UrlScheme` etc.) into its own module `client/
builder/homeserver_config.rs`.

This is purely for cleaning up.
2024-09-02 14:00:10 +02:00
Ivan Enderlin 22a19e26d3 feat(sdk): Add Client::server.
This patch adds `Client::server`: the URL of the server.

Not to be confused with the `Client::homeserver`. The `server`
holds some information, like the `.well-known` file, to discover the
homeserver. The homeserver is the client-server Matrix API.

`server` is usually the server part in a user ID, e.g. with
`@mnt_io:matrix.org`, here `matrix.org` is the server, whilst
`matrix-client.matrix.org` is the homeserver.

This patch also moves the code about homeserver discovery in the
`HomeserverConfig::discover` new method. A new struct is introduced to
hold the result, to replace a 4-tuples.

`Client::server` is also now a `Option<_>` because in the case of
`HomeserverConfig::Url`, the server cannot be known.

This patch also removes several clones here and there.

Finally, this patch updates a test to quickly test the new behaviour. A
next patch will introduce proper tests.
2024-09-02 14:00:10 +02:00
Benjamin Bouvier b9628301ec timeline: beef up comment around TimelineEventContext::should_add_new_items 2024-09-02 13:45:12 +02:00
Benjamin Bouvier b4683c0ff5 timeline: reinterpret LiveTimelineUpdatesAllowed as TimelineFocusKind
It makes the code simpler to understand, in my opinion.
2024-09-02 13:45:12 +02:00
Benjamin Bouvier cb3c5ab1ce timeline: move the decision to add a timeline item upwards
Especially for remote items, it should be in sync with `should_add` as
it's used in this method, otherwise read receipt tracking will not work
correctly.
2024-09-02 13:45:12 +02:00
Benjamin Bouvier 8c5ffc9a96 timeline: don't clear the internal counter in presence of local echoes 2024-09-02 13:21:36 +02:00
Benjamin Bouvier 9ec46ddf2c timeline(tests): use the EventFactory in more tests 2024-09-02 13:21:36 +02:00
Benjamin Bouvier bfb04f2ddd event factory: allow having unsigned data too
And use the event factory in more timeline tests.
2024-09-02 13:21:36 +02:00
Andy Balaam eecd00cd98 indexeddb: Pass the db transaction into do_schema_upgrade closures
For some operations (notably: adding an index to an existing object store), we
need access to the database transation during the upgrade operation.
2024-09-02 11:22:43 +01:00
Benjamin Bouvier b8d90286aa testing: enforce a test_ prefix for tests
This will only apply to `async_test` functions, but I think this is a
win:

1. for consistency within the codebase, since I've started doing so in
many places,
2. because these function names will clearly identify these functions as
tests, in the call tree interfaces, when rendered using the LSP
show-callers/show-callees functionality.
2024-09-02 12:02:43 +02:00
Kévin Commaille 424d01d964 Revert "doc(sdk): Update CHANGELOG.md."
This reverts commit 711a753533.
2024-09-02 11:32:04 +02:00
Kévin Commaille 84e4552da7 Revert "feat(sdk): Remove NotificationSettings::subscribe_to_changes."
This reverts commit 4e291205d5.
2024-09-02 11:32:04 +02:00
Benjamin Bouvier c4624cc863 timeline: add local echo handling for *all* timelines
Including non-live timelines (pinned event timelines and permalinked
timelines). This makes it possible to see that you're adding a reaction
etc. in real time, while it wasn't the case anymore.

Fixes #3906.
2024-09-02 11:08:25 +02:00
Benjamin Bouvier c3973589c8 timeline(test): add an integration test for non-live timelines not handling local echoes 2024-09-02 11:08:25 +02:00
Benjamin Bouvier 224292ab3e testing: remove EventBuilder::make_sync_reaction which is unused 2024-08-29 16:21:56 +02:00
Benjamin Bouvier 0311f30182 timeline: get rid of one use of sync_timeline_event 2024-08-29 16:21:56 +02:00
Benjamin Bouvier 0877445273 timeline: add assert_let_timeout for testing purposes 2024-08-29 16:21:56 +02:00
Richard van der Hoff 7f02447c78 indexeddb: remove InboundGroupSessionIndexedDbObject::new
We're going to add some more fields soon, so a `new` method is increasingly
unhelpful.

Replace it with a helper for the tests.
2024-08-29 11:11:07 +01:00
Richard van der Hoff 308d658224 indexeddb: add InboundGroupSessionIndexedDbObject::from_session
factor out the two copies of this code into a common function, and inline the
call to `new` while we're there
2024-08-29 11:11:07 +01:00
Richard van der Hoff 9808ad7d16 indexeddb: fix a typo in a comment 2024-08-29 11:11:07 +01:00
Benjamin Bouvier 06fc220268 ffi: use the send queue when sending an edit with Room::edit
This will make it possible to send updates to observers, update local
echoes, and so on, making it closer to the edit functions from the
timeline.
2024-08-29 09:34:31 +03:00
Benjamin Bouvier f399f229ae send queue: remove outdated comment
The future is now, and has been for quite a while, in fact.
2024-08-29 09:34:31 +03:00
Kévin Commaille a8b38d271e sqlite: Merge init and run_migrations for SqliteEventCacheStore
This was copied from SqliteStateStore, but the reason
that they are separated there is because some migrations
require the store cipher.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-29 09:31:40 +03:00
Kévin Commaille 66e901bb9b sqlite: Make migrations atomic
Setting the version number only when all migrations are done
means that the version will be wrong if a migration fails.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-29 09:31:40 +03:00
Benjamin Bouvier 9edca06d3b http client: don't infinitely retry network failures if there's no retry limit
Otherwise, this would mean that logged out clients would infinitely
repeat network requests failing in the background.

Without this fix, the added test will time out, endlessly reattempting
network requests.
2024-08-28 17:26:16 +02:00
Benjamin Bouvier 47444cc671 sdk-base: hack to avoid over-recursion when evaluating Send/Sync bounds in rustc
See
https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823
for the gory details.
2024-08-28 16:49:46 +02:00
Benjamin Bouvier e1fe1ca129 timeline: add support for local reactions to local echoes 2024-08-28 16:49:46 +02:00
Benjamin Bouvier 6181387776 timeline: allow reactions in local|remote event timeline items 2024-08-28 16:49:46 +02:00
Benjamin Bouvier 81a75508dc send queue: early return if there are no dependent events to handle 2024-08-28 16:49:46 +02:00
Benjamin Bouvier f6faf6267e send queue: add integration test for sending and aborting reactions 2024-08-28 16:49:46 +02:00
Benjamin Bouvier 25db0b2cc0 send queue: handle reactions in the send queue 2024-08-28 16:49:46 +02:00
Benjamin Bouvier 2d6aec7319 send queue: put a LocalEcho's event content into a new enum 2024-08-28 16:49:46 +02:00
Kévin Commaille d17a49e827 sqlite: Add more methods to SqliteKeyValueStoreAsyncConnExt
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-28 15:45:12 +02:00
Kévin Commaille 1321e92b30 sqlite: Rename SqliteObjectStoreExt to SqliteKeyValueStoreAsyncConnExt
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-28 15:45:12 +02:00
Kévin Commaille 6e62f8b269 sqlite: Rename utils::ConnectionExt to SqliteKeyValueStoreConnExt
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-28 15:45:12 +02:00
Kévin Commaille 6e369aecc9 sqlite: Rename SqliteConn to SqliteAsyncConn and SqliteObjectExt to SqliteAsyncConnExt
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-28 15:45:12 +02:00
Richard van der Hoff ece33059e1 indexeddb: Reinstate crypto store integration tests
We are no longer running the cryptostore integration tests for indexeddb
without encryption. I think this was accidentally removed in 96b615ba.
2024-08-28 11:49:22 +02:00
Benjamin Bouvier 468ee53644 sliding sync version(chore): address review comments 2024-08-27 17:25:12 +02:00
Ivan Enderlin 067b1e0020 feat(ffi): Add Client::available_sliding_sync_versions.
This patch adds bindings to `Client::available_sliding_sync_versions`
to `matrix-sdk-ffi`.

This patch also moves `Client::sliding_sync_version` from “private” to
“public” FFI API, in thee sense that this method is now exported with
UniFFI.
2024-08-27 17:25:12 +02:00
Ivan Enderlin 96c7f36d75 feat(sdk): Add Client::available_sliding_sync_versions.
Previous patches have unified all sliding sync versions behind a
single type: `Version`. More recent previous patches have introduced
`VersionBuilder` so that a `ClientBuilder` can use them to coerce or
find the best `Version` possible. This patch implements a last missing
piece: `Client::available_sliding_sync_versions` will report all
available `Version`s at a given time. This is useful when a `Client`
is already built, and a session has been opened/a user is logged,
but someone has to take the decision whether it's useful to switch to
another sliding sync version or not.
2024-08-27 17:25:12 +02:00
Ivan Enderlin 01f88f1c4b test: Disable Complement Crypto for a short period. 2024-08-27 17:25:12 +02:00
Ivan Enderlin 9e5b06902d test(sdk): Add 2 more tests for auto-discovery of sliding sync. 2024-08-27 17:25:12 +02:00
Ivan Enderlin 245f011fb6 test(sdk): Restore disabled tests. 2024-08-27 17:25:12 +02:00
Ivan Enderlin e1f623cf56 feat: Use the new sliding_sync::VersionBuilder. 2024-08-27 17:25:12 +02:00
Ivan Enderlin 01fd365f67 feat(sdk): Add sliding_sync::VersionBuilder.
This patch adds a builder for `sliding_sync::Version`. It is a similar
enum except that it has `DiscoverProxy` and `DiscoverNative` to
automatically configure `Version::Proxy` or `Version::Native`.
2024-08-27 17:25:12 +02:00
Ivan Enderlin 875f59133b feat(sdk): ClientBuilder extracts get_supported_versions.
This patch changes the type of
`discover_homeserver_from_server_name_or_url`. It now returns a `Url`
instead of a `String` for the homeserver URL. It also returns an
`Option<get_supported_versions::Response>` in addition to the other
values.

The change from `String` to `Url` is necessary to avoid a
double-parsing. It was parsed in `build()` but previously in
`discover_homeserver_from_server_name_or_url` in the last branch.

The addition of `get_supported_versions::Response` is necessary for the
next patch. It's going to be helpful to auto-discover sliding sync in
Synapse. The change happens here because
`get_supported_versions::Response` is already received in
`discover_homeserver_from_server_name_or_url`. This patch makes it easy
to re-use it so that the request is sent only once.

This patch therefore changes `check_is_homeserver` a little bit to
become `get_supported_versions`, and inlines its previous call inside
`discover_homeserver_from_server_name_or_url`.
2024-08-27 17:25:12 +02:00
Ivan Enderlin de8537f8b0 feat: Use sliding_sync::Version everywhere.
This patch replaces all the API using simplified sliding sync, or
sliding sync proxy, by a unified `sliding_sync::Version` type!

This patch disables auto-discovery for the moment. It will be re-enable
with the next patches.
2024-08-27 17:25:12 +02:00
Ivan Enderlin d9ffe2867f feat(sdk): Add sliding_sync::Version. 2024-08-27 17:25:12 +02:00
Daniel Salinas b908fa78b9 The length param from Truncate was not being exposed 2024-08-27 15:17:37 +02:00
Kévin Commaille f568e8c4d3 sdk: Handle a clippy warning
On my version of clippy nightly, these lines triggered the
`filter_map_bool_then` lint.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-27 14:10:20 +03:00
Richard van der Hoff 8005515ec5 common: generate wasm bindings for ShieldStateCode on wasm architectures (#3888)
If we're building for the wasm architecture, jump through the hoops to
tell wasm_bindgen about `ShieldStateCode`. This solves the need to 
declare an identical copy of `ShieldStateCode` in the wasm bindings.
2024-08-27 11:02:37 +00:00
Hubert Chathi 758171931d refactor collect_session_recipients in advance of some behavioural changes (#3884)
This is part of https://github.com/matrix-org/matrix-rust-sdk/pull/3662,
pulled out to into a separate PR. Recent changes in `main` made it
pretty much impossible to merge this section of code from `main` into
that PR, and Rich wanted to see the refactoring bits separate from the
behavioural changes. So I've re-written the refactoring.

Pulls the `match` on `sharing_strategy` outside of the `for` loop, and
moves any code that is specific to one strategy into the appropriate
branch.
2024-08-27 11:59:38 +01:00
Daniel Salinas 47671a182d Expose login with email + password on the client 2024-08-26 18:23:25 +02:00
Kévin Commaille b9d49b85c3 sqlite: Use transaction for chunked queries
Allows the operation to be atomic.
Also allows to chunk part of a transaction.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-26 18:20:41 +02:00
Benjamin Bouvier 5753c53ea7 test utils: use a macro for assert_next_with_timeout
This makes for better error locations, and avoids monomorphizing on the
stream type in every call site with a different type.
2024-08-26 17:52:43 +02:00
Kévin Commaille fe143ffbed sqlite: Remove SqliteObjectEventCacheStoreExt trait
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-26 10:40:12 +02:00
Kévin Commaille 25e406f669 sqlite: Remove unused path field
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-26 10:40:12 +02:00
Kévin Commaille 154f86aa20 sqlite: Rename deadpool_sqlite::Object to SqliteConn consistently
So we know that its always the same type when reading the code.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-26 10:40:12 +02:00
Doug c769e32b41 ffi: Expose the new cache store path to the bindings. 2024-08-23 15:15:03 +02:00
Jorge Martín c78639d880 sdk-ui: fix pinned events benchmark 2024-08-23 14:12:32 +02:00
Jorge Martín af5107f529 sdk: add RequestConfig to Room::event_with_context to match Room::event, save its results to the event cache too. 2024-08-23 14:12:32 +02:00
Jorge Martín d6d9cd129a sdk: add fn RoomEventCache::save_events to save several events using the same RwLock for the cache, which should be faster 2024-08-23 14:12:32 +02:00
Jorge Martín d1d4c6417c sdk-ui: fix and add new pinned events tests.
This commit contains a new `assert_next_matches_with_timeout!` macro that will take a `Stream` and wait for a little while until its next item is ready, then match it to a pattern.
2024-08-23 14:12:32 +02:00
Jorge Martín d5630bc5cd sdk-ui: load pinned events with their related events
This way any reactions/redactions/edits, etc. will be taken into account when building the timeline event.
2024-08-23 14:12:32 +02:00
Jorge Martín 8a6ac05519 sdk-ui: enable handling new synced timeline events for TimelineFocus::PinnedEvents 2024-08-23 14:12:32 +02:00
Jorge Martín a3262740ac sdk-ui: handle events related to a pinned event in TimelineEventHandler 2024-08-23 14:12:32 +02:00
Andy Balaam fb63c8dfbc crypto: Move should_recalculate_sender_data to OlmMachine 2024-08-23 11:19:42 +01:00
Hubert Chathi e01c1aecbb crypto: Add variants to SenderData to represent different verification states 2024-08-23 11:19:42 +01:00
Kévin Commaille 6e5601db1f Enable web-sys features for tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-23 10:59:27 +01:00
Kévin Commaille 5586a81703 chore: Use ruma::time instead of instant
This is a reexport of web-time.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-23 10:59:27 +01:00
Benjamin Bouvier be133886b7 sdk-ui: remove dependency to async_trait \o/ 2024-08-23 09:43:00 +02:00
Benjamin Bouvier 6fb38179a4 room edit: don't use async_trait for EventSource either 2024-08-23 09:43:00 +02:00
Benjamin Bouvier 912cb233f5 paginator: don't use async_trait for PaginableRoom
This divides compile times for matrix-sdk by 2, on my machine (33
seconds -> 16).
2024-08-23 09:43:00 +02:00
Benjamin Bouvier 4042db7d50 paginator: use a generic type instead of a boxed trait
Who has two thumbs and wants to make a trait not use async_trait, and
thus get rid of trait object safety?
2024-08-23 09:43:00 +02:00
Benjamin Bouvier 4273dff33e timeline: get rid of async_trait for our own traits 2024-08-23 09:43:00 +02:00
Benjamin Bouvier 5f27f487e6 pinned events: get rid of async_trait for PinnedEventsRoom too 2024-08-23 09:43:00 +02:00
Benjamin Bouvier 6cbd197514 timeline: don't use async_trait for RoomExt
It's one fewer pathologically slow type query during type-checking, and
a build time for matrix-sdk-ui 5 seconds lower.
2024-08-23 09:43:00 +02:00
Benjamin Bouvier c10894bed6 timeline: inline update_timeline_item in all callers
It was found that it's making the whole build slower because it's
hitting a pathologically slow path in type-checking. Considering that it
doesn't do much, let's get rid of it and inline it instead.

After this, compiles times are reduced from 30 seconds to 22 seconds on
my machine
2024-08-23 09:43:00 +02:00
Benjamin Bouvier 2291268679 timeline: get rid of Clone for Flow and TimelineEventContext
These are function parameters, they shouldn't be cloned for no good
reason.
2024-08-23 09:43:00 +02:00
Benjamin Bouvier 94e9005132 room list service: get rid of the abstract action system and replace it with functions
After this, compiles times for matrix-sdk-ui are reduced from 44 seconds
to 30 seconds on my machine.
2024-08-23 09:43:00 +02:00
Ivan Enderlin 3a1c374a13 chore(sdk): Rename StickyData::commit to ::on_commit. 2024-08-23 09:18:14 +02:00
Ivan Enderlin 2ac71fc89c test(ui): Fix a test, finally.
This patch fixes a test. It now fails, for my own personal joy. The new
behaviour is much better.
2024-08-23 09:18:14 +02:00
Ivan Enderlin a484964d4d feat(sdk): Do not send a room subscription that has already been sent.
This patch uses the new `RoomSubscriptionState` enum to filter
room subscriptions that have already been sent, when building a
`http::Request` with the sticky parameters.

By default, to start, a `http::request::RoomSubscription` is in the
state `Pending`, i.e. it's not sent yet. Once the sticky parameters are
committed, the state is updated to `Applied`. When the sticky parameters
are applied, only the `Pending` room subscriptions are added.

This patch contains one test to specifically assert this behaviour.
2024-08-23 09:18:14 +02:00
Ivan Enderlin 5f159d4418 feat(sdk): Add RoomSubscriptionState.
This patch introduces the `RoomSubscriptionState` type, to represent
whether a room subscription has already been correctly sent to the
server.
2024-08-23 09:18:14 +02:00
Ivan Enderlin aeaedf7e5b feat(sdk): Add StickyData::commit.
This patch adds the `commit` method on the `StickyData` trait. It is
called by `SlidingSyncStickyManager::maybe_commit` when we are sure the
data can be validated because of a valid response to the sent request.
2024-08-23 09:18:14 +02:00
Benjamin Bouvier 2db031cec5 timeline: rename ReactionStatus::Remote to ReactionStatus::RemoteToRemote 2024-08-22 17:46:11 +02:00
Benjamin Bouvier 3329e75708 test: add a test that if a redaction request fails, we add the annotation back 2024-08-22 17:46:11 +02:00
Benjamin Bouvier cdd6c23e15 test: add a test for redacting a reaction that was being sent 2024-08-22 17:46:11 +02:00
Benjamin Bouvier 6dc8d3980e timeline: only use the send queue to send reactions, and nothing else 2024-08-22 17:46:11 +02:00
Benjamin Bouvier a9c0dc3da4 timeline: introduce new methods send/redact in the RoomDataProvider 2024-08-22 17:46:11 +02:00
Timo c04dd18440 MatrixRTC: Update ruma revision.
This revision includes renaming `focus_select` (wrong) to `focus_selection` (correct).
2024-08-22 15:59:46 +02:00
Jorge Martin Espinosa 794bf98a1b sdk: add relationships cache to EventCache (#3870)
## Changes

- Creates a separate `AllEventsCache` struct holding both the actual all
events cache map and the relationship one. This new cache wrapper also
holds a single `RwLock` to both inner caches, as they are independent
from each other.
- When a new event is saved either from `/sync` or another HS request,
it'll be saved to both the 'all events' cache and the relationship map.
- Add tests for the relationship cache.
2024-08-22 14:24:19 +02:00
Richard van der Hoff e88f14a1f9 ffi: expose new SessionRecipientCollectionErrors to application
Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3842
2024-08-22 12:47:16 +01:00
Benjamin Bouvier b95c189a18 test mocks: deduplicate mock_encryption_state 2024-08-22 10:45:50 +02:00
Benjamin Bouvier 92fe72f83a test: add a mocks mod in matrix-sdk-test to reuse across different integration tests 2024-08-22 10:45:50 +02:00
Kévin Commaille 01e2db1f52 base: Move media cache to new EventCacheStore trait (#3858)
Allows to save media in a different path than the state store.

This adds a "last_access" field to the SQLite implementation, to prepare
for future work on a media retention policy.

This removes the IndexedDB media cache implementation, because as far as
I know it is currently unused, and I have no idea how to implement
efficiently the planned media retention policy with a key-value store.

Closes #1810.

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

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-22 10:36:43 +02:00
Ivan Enderlin 40d447dc69 fix(sdk): Clear all sliding sync room subscriptions when session expires.
This patch clears all sliding sync room subscriptions when a session
expires. Indeed, we might not want to request all room subscriptions
when the session restarts. Imagine if the client has subscribed to 400
rooms and the session expires: once the session restarts, it will ask
for 400 room subscriptions, which is a lot and will result in a quite
slow response.
2024-08-21 18:34:11 +02:00
Stefan Ceriu 4cc5790c64 ffi: expose room membership through the RoomListItem and allow invited rooms to be build differently than "full" ones (#3869)
We're finding ourselves in the situation in which we can't interact with
invites through normal Room APIs as `full_room`s can't be build from the
RoomListItem. Full rooms require the timeline to be configured before
use and the timeline can't be configured because encryption cannot be
fetched for invited rooms on homeservers that have previews disabled
(see #3848 and #3850)

In response we now expose the room's membership directly from the
`RoomListItem` so that the final client can chose which of the 2 rooms
types (invited or full) to ask for before using aforementioned APIs.

Powers https://github.com/element-hq/element-x-ios/pull/3189
2024-08-21 16:55:03 +03:00
Jorge Martín fbc9db9b15 ffi: add info to FFI room redaction event 2024-08-21 13:29:36 +02:00
Ivan Enderlin f6b21e6ce9 chore(sdk): Clean documentation and remove a useless pub(super).
This patch is a small cleanup.
2024-08-21 13:11:09 +02:00
Ivan Enderlin 05542f7ba8 feat(sdk): Sliding sync has a timeout if all lists require a timeout.
This patch updates when sliding sync requests have a `timeout`.

Prior to this patch, all requests had a `timeout` query, set to the
`poll_timeout` duration value. However it means: if there is no data
to return, wait `timeout` milliseconds for new data before returning.
This definition is correct. Problem: if the current range of a list
has no data, the server will wait! It means that, in a situation where
there is no update at all, but the client is fetching all rooms batch by
batch, it will wait `poll_timeout` for each batch!

The behaviour described above is absolutely correct. Some server
implementations are less strict though, and we didn't realise our code
was doing that, because the server had some optimisations to ignore the
timeout if the range wasn't covering all the rooms. Nonetheless, a new
server implementation (namely Synapse) is strict, and it confirms we
have a bug here.

This patch then configures a `timeout` if all lists require a timeout,
otherwise there is no `timeout`, which is equivalent to `timeout=0`.
2024-08-21 13:11:09 +02:00
Ivan Enderlin b06bb42d3e feat(sdk): Add SlidingSyncList::requires_timeout.
This patchs adds the `SlidingSyncList::requires_timeout` method to know
exactly when a list should trigger a `timeout` on the request.
2024-08-21 13:11:09 +02:00
Ivan Enderlin e4d0f2291f feat(sdk): Add SlidingSyncListRequestGenerator::is_selective.
This patch adds a small helper:
`SlidingSyncListRequestGenerator::is_selective`.
2024-08-21 13:11:09 +02:00
Ivan Enderlin 31f84d7534 feat(sdk): Implement SlidingSyncListLoadingState::is_fully_loaded.
This patch implements and tests
`SlidingSyncListLoadingState::is_fully_loaded` for more convenience.
2024-08-21 13:11:09 +02:00
Ivan Enderlin 4c5b537825 test(sdk): Rename tests.
This patch renames tests.
2024-08-21 13:11:09 +02:00
Richard van der Hoff d9e6bfa678 Add a tip about using RustRover 2024-08-21 09:41:54 +01:00
Stefan Ceriu f0d98602a9 timeline: use the EncryptionInfo provided by the replacement event when processing edits
- this prevents issues where spoofing the sender field is enough to spoof and edit and display wrong decorations in the app
- fixes matrix-org/internal-config/issues/1549
2024-08-21 10:31:33 +02:00
Richard van der Hoff a27ebaabae ffi: add ClientBuilder::room_key_recipient_strategy
... and pass it through to the underlying ClientBuilder.
2024-08-20 18:22:56 +01:00
Richard van der Hoff 9b78903705 crypto: add ClientBuilder::with_room_key_recipient_strategy
... and pass it into the constructed BaseClient.
2024-08-20 18:22:56 +01:00
Richard van der Hoff 0151f32425 crypto: add BaseClient::room_key_recipient_strategy field
... and use it when sharing a room key.
2024-08-20 18:22:56 +01:00
Andy Balaam ca09917d84 crypto: Update the comment about source of truth for in sqlite 2024-08-20 15:36:34 +01:00
Andy Balaam 78924ed877 crypto: In sqlite, use the SQL column value for backed_up everywhere
Most times we pulled an InboundGroupSession from the sqlite DB, we were
overriding whatever value for `backed_up` was stored inside the pickled
value, and using the value stored in the SQL column.

But when we pulled a single InboundGroupSession from the DB by ID, we
did not override it.

I am fairly sure this was an accidental oversight, so this change
corrects it, and unifies the code with other places we create these
objects.
2024-08-20 15:36:34 +01:00
Andy Balaam 668a267c9b crypto: Merge deserialize_pickled_inbound_group_session into unpickle_inbound_group_session 2024-08-20 13:16:17 +01:00
Andy Balaam fed08fad76 crypto: Utility deserialize_and_unpickle_inbound_group_session for sqlite store 2024-08-20 13:16:17 +01:00
Richard van der Hoff 651b61414e crypto: add a note about libolm only being used in tests
It seems that the fact that matrix-rust-sdk contains `olm-rs` in its
`Cargo.lock` sparked panic, so let's attempt to fend off future concerns by
adding a comment.
2024-08-20 12:16:33 +01:00
Benjamin Bouvier bbefad34bc test: reuse the internal EventFactory of the TestTimeline in more places
Also rename a few `factory` to `f`, for consistency with the rest of the
testing code.
2024-08-20 12:11:11 +02:00
Benjamin Bouvier b00f58a28d test: get rid of TestTimeline::handle_live_message_event in favor of TestTimeline::handle_live_event
By using the `EventFactory` a bit more.

Part of #3716.
2024-08-20 12:11:11 +02:00
Kévin Commaille 3b3474688b sdk: Upgrade mas-oidc-client
Gets rid of the old version of the http crate.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-19 23:14:27 +02:00
Ivan Enderlin ed19bf7bc5 fix(ffi): New fields formatter to remove duplicated span fields.
This patch fixes a bug in the tracing system. It introduces one
fields formatter _per layer_ to force the fields to be recorded in
different span extensions, and thus to remove the duplicated fields in
`FormattedFields`.

The patch contains links to the bug report in `tokio-rs/tracing`. This
patch is a workaround.
2024-08-19 14:44:04 +02:00
Ivan Enderlin eeb325abb7 chore(ffi): Format code. 2024-08-19 14:44:04 +02:00
Richard van der Hoff 9d7dd1a6d6 crypto: update changelog 2024-08-19 13:13:58 +01:00
Richard van der Hoff 820f4ee711 crypto: tests for errors on verification violation 2024-08-19 13:13:58 +01:00
Richard van der Hoff 271ba98ba9 crypto: test: factor out EncryptionSettings helper 2024-08-19 13:13:58 +01:00
Richard van der Hoff 7575c256d4 crypto: test: move unsigned_of_verified_setup helper function
I want to use this in more tests, so move it out of the middle of the test
module.
2024-08-19 13:13:58 +01:00
Richard van der Hoff cd0d79dd88 crypto: Key sharing option to error for verification violation 2024-08-19 13:13:58 +01:00
Richard van der Hoff d35e3405ab crypto: Add UserIdentityData::was_previously_verified
Since `was_preivously_verified` is implemented for both variants, we can add a
helper here.
2024-08-19 13:13:58 +01:00
Ivan Enderlin 711a753533 doc(sdk): Update CHANGELOG.md. 2024-08-19 07:22:29 +02:00
Ivan Enderlin 4e291205d5 feat(sdk): Remove NotificationSettings::subscribe_to_changes.
This patch removes `NotificationSettings::subscribe_to_changes` because
it's not used anywhere in our code except in tests. It is indeed part of
the public API but I'm not aware of anyone using it for the moment. It
only adds complexity in the code.
2024-08-19 07:22:29 +02:00
Richard van der Hoff b497577717 crypto: use UserIdentities utility functions
... instead of lots of `match` and `own()` and `other()`.
2024-08-16 16:58:27 +01:00
Richard van der Hoff 9077310eb2 crypto: Add utility wrappers to UserIdentities
We have a bunch of methods which are the same in both `OtherUserIdentity` and
`OwnUserIdentity`, so add some convenience methods to access them.
2024-08-16 16:58:27 +01:00
Richard van der Hoff b4d265e997 crypto: update changelog 2024-08-16 15:07:36 +01:00
Richard van der Hoff dd810f4803 crypto: Track if our own identity was previously verified
... and expose new methods to access it.
2024-08-16 15:07:36 +01:00
Richard van der Hoff a6b78d8d53 crypto: replace OwnUserIdentityData::verified with enum
I want to make this a tri-state, so let's start by making it an enum.
2024-08-16 15:07:36 +01:00
Richard van der Hoff 96c4e4c49e crypto: update OtherUserIdentityData::is_device_signed to return bool
Once again: since all the callers end up calling `.is_ok()` on the result, and
the name implies it should return a bool, let's just return a bool.
2024-08-16 12:34:10 +01:00
Richard van der Hoff 5f9a4fc6d1 crypto: update OwnUserIdentityData::is_device_signed to return bool
As before: since all the callers end up calling `.is_ok()` on the result, and
the name implies it should return a bool, let's just return a bool.
2024-08-16 12:34:10 +01:00
Richard van der Hoff 31b25b8754 crypto: update OwnUserIdentityData::is_identity_signed to return bool
Since all the callers end up calling `.is_ok()` on the result, and the name
implies it should return a bool, let's just return a bool.
2024-08-16 12:34:10 +01:00
Ivan Enderlin fa6066b810 test(sdk): Test Room::cached_user_defined_notification_mode.
This patch adds a test for `Room::cached_user_defined_notification_mode`.
2024-08-15 14:51:16 +03:00
Ivan Enderlin 102da7cb9a feat(ffi): Add RoomInfo::cached_user_defined_notification_mode.
This patch replaces `RoomInfo::user_defined_notification_mode` by
its cached variant: `cached_user_defined_notification_mode`, and
call `Room::cached_user_defined_notification_mode` which will boost
performance when computing a new `RoomInfo`.
2024-08-15 14:51:16 +03:00
Ivan Enderlin 1c39086b5a feat(sdk): Cache the user-defined notification mode on each sync.
This patch caches the user-defined notification mode on each sync for
the sake of performance.
2024-08-15 14:51:16 +03:00
Ivan Enderlin 651e8fcd48 test(base): Split test_room_info_deserialization_without_optional_items.
This patch splits/copies the
`test_room_info_deserialization_without_optional_items` test into the
same test + `test_room_info_deserialization`.

It appears that the _without optional items_ part has been forgotten.
In the past, the test has been updated to test optional items. The
initial idea (based on my understanding of the comments) is to test
potentially old `RoomInfo` can still be deserialized today. So this test
must never be changed, except if a non-optional field is added.

For this reason, this patch removes from this test the assertions about
optional fields. A new test, named `test_room_info_deserialization` is
created, and tests all the fields, including the optional ones.

One important thing:
`test_room_info_deserialization_without_optional_items` now runs
even if the `experimental-sliding-sync` feature is absent. It was
required only because of `latest_event`, but that's an optional
field! However, the `test_room_info_deserialization` requires the
`experimental-sliding-sync` feature, as it tests `latest_event` but
also `recency_stamp`.

Finally, `test_room_info_deserialization` tests
`cached_user_defined_notification_mode`.
2024-08-15 14:51:16 +03:00
Ivan Enderlin ad947132ed feat(sdk): Room::user_defined_notification_mode caches its result.
This patch updates `matrix_sdk::Room::user_defined_notification_mode`
to cache its result if some mode has been found. The cached result can
be retrieve with
`matrix_sdk_base::Room::cached_user_defined_notification_mode`.
2024-08-15 14:51:16 +03:00
Ivan Enderlin 2a3525f7be chore(sdk): Add missing copyright. 2024-08-15 14:51:16 +03:00
Ivan Enderlin 7e2c773d21 chore(base): Move RoomNotificationMode into matrix-sdk-base.
This patch moves the `RoomNotificationMode` type from `matrix-sdk`
to `matrix-sdk-base` because it's going to be shared across multiple
crates.
2024-08-15 14:51:16 +03:00
Jorge Martín 6becbf61c9 fixup! Fixes after rebase 2024-08-14 17:38:24 +02:00
Jorge Martín 1155f75612 fixup! fix review comments 2024-08-14 17:38:24 +02:00
Jorge Martín 5cd29830a3 fixup! Fix clippy after merge 2024-08-14 17:38:24 +02:00
Jorge Martín 34370b1525 fixup! Fix test 2024-08-14 17:38:24 +02:00
Jorge Martín 1132074ae0 sdk-base: Make sure we only send a notable membership update when membership does change 2024-08-14 17:38:24 +02:00
Jorge Martín e5af5a32fa sdk-base: Add RoomInfoNotableUpdateReasons::MEMBERSHIP
This fixes joined rooms not being bumped to the top of the timeline and left rooms not disappearing from the room list.
2024-08-14 17:38:24 +02:00
Richard van der Hoff 1e8dd5dd41 crypto: Update changelog 2024-08-14 14:57:42 +01:00
Richard van der Hoff 5431c0fdd6 crypto: test: add tests for error_on_verified_user_problem 2024-08-14 14:57:42 +01:00
Richard van der Hoff a240b87ba6 crypto: test: factor out redundant variable
This thing was confusing. What is "legacy" about it?
2024-08-14 14:57:42 +01:00
Richard van der Hoff 324cf2e007 crypto: test: factor out create_test_outbound_group_session helper 2024-08-14 14:57:42 +01:00
Valere 66142317d4 crypto: key sharing error for verified user with unverified devices 2024-08-14 14:57:42 +01:00
Richard van der Hoff f66c74e878 crypto: extend CollectionStrategy::DeviceBasedStrategy
Add (as yet unimplemented) `error_on_verified_user_problem` option
2024-08-14 14:57:42 +01:00
Richard van der Hoff 1862a3e254 crypto: change EncryptionSettings::new to take a CollectStrategy
Again, the list of boolean arguments is confusing.
2024-08-14 14:57:42 +01:00
Richard van der Hoff dadc85c4fc crypto: remove CollectStrategy::new_device_based
The list of boolean arguments is confusing. We may as well just construct the
`DeviceBasedStrategy` directly.
2024-08-14 14:57:42 +01:00
Richard van der Hoff d8c1094939 crypto: add OwnUserIdentityData::is_identity_verified
... and use it to remove a bit of duplicated code.
2024-08-14 14:57:42 +01:00
Richard van der Hoff ace937fcee crypto: rafactor split_recipients_withhelds_for_user
Use a for loop rather than `partition_map`. We're about to add a third list, so
partition_map won't work.

(partition_map ends up using Vec::push under the hood, so this is pretty much
equivalent.)
2024-08-14 14:57:42 +01:00
Richard van der Hoff 1e58c0382c crypto: minor cleanups in is_session_overshared_for_user 2024-08-14 14:57:42 +01:00
Valere ce95cc06e0 crypto: extract function that checks if session is shared too much 2024-08-14 14:57:42 +01:00
Andrew Ferrazzutti 3803792518 rtc: Handle non-MXID call member event state keys (#3836)
Update Ruma dependency to expect call membership state events with state
keys that are arbitrary strings, not just pure MXIDs.

When a call membership state key does not exactly match the format of an
MXID, treat it as a valid state key if it starts with an MXID followed
by an underscore, with that MXID designating the owner of the event.

(The state key may also be optionally prefixed with an underscore, which
is permitted as a way to bypass pre-MSC3757 authorization rules against
sending state events with state keys that do not exactly match the
sender's MXID.)

---------

Signed-off-by: Andrew Ferrazzutti <andrewf@element.io>
Co-authored-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-08-14 08:43:02 +00:00
Richard van der Hoff 4ece38af4f test: document methods in PreviouslyVerifiedTestData 2024-08-13 16:38:58 +01:00
Richard van der Hoff 4baa82d3a6 test: add new methods for device IDs 2024-08-13 16:38:58 +01:00
Richard van der Hoff 8b01e3e209 test: make internal functions private 2024-08-13 16:38:58 +01:00
Richard van der Hoff 4a69cc9110 test: inline device_keys_payload_bob_unsigned_device
This is only used in one place, so is a bit pointless.
2024-08-13 16:38:58 +01:00
Benjamin Bouvier 766786e2f1 pinned events(refactor): get rid of the PinnedEventCache
This commit makes use of the `RoomEventCache` instead of the
`PinnedEventCache` for a room, so the latter can be removed.
2024-08-13 17:17:16 +02:00
Benjamin Bouvier e67e2289e2 event cache(refactor): use a map keyed by event id for retrieving events
And add unit tests.
2024-08-13 17:17:16 +02:00
Benjamin Bouvier 5954ee18b7 pinned events(refactor): rename event_with_config to fetch_event
The `with_config` is now redundant, and `fetch` makes it clear it's
hitting network.
2024-08-12 16:01:39 +02:00
Benjamin Bouvier e15ddf6ad9 pinned events(refactor): simplify handling of AddTimelineEvents
The timeline already listens to changes to the pinned events list (via a
stream), so there's no need to fully reload all the pinned events every
time we receive a new event that's pinned. Technically it may avoid one
or a few lookups, but this is cheap and a subsequent commit/PR will
merge the pinned event cache into the event cache.
2024-08-12 16:01:39 +02:00
Benjamin Bouvier 19b6495f2b pinned events(refactor) misc tiny refactorings and renamings 2024-08-12 16:01:39 +02:00
Benjamin Bouvier 0ba6adbf34 pinned events(chore): add doc comments 2024-08-12 16:01:39 +02:00
Benjamin Bouvier 3886a55ad8 pinned events(chore): add licence header to pinned_events_loader.rs 2024-08-12 16:01:39 +02:00
Benjamin Bouvier 8f59f45183 pinned events(refactor): don't store max_concurrent_requests as a field
since it's used only once
2024-08-12 16:01:39 +02:00
Benjamin Bouvier a12a244a89 pinned events(refactor): lower logs from info to debug 2024-08-12 16:01:39 +02:00
Benjamin Bouvier f3587a44dc pinned events(refactor): fetch all the pinned events concurrently
Also log the reason why one couldn't be fetched, instead of discarding
silently.
2024-08-12 16:01:39 +02:00
Benjamin Bouvier c76ea95f65 pinned events(refactor): sort loaded pinned events in place
Instead of creating a collection, then using itertools to sort it, then
allocating a new vector from that.
2024-08-12 16:01:39 +02:00
Ivan Enderlin bdfc300e4c feat(sdk): compute_limited is only useful for the SS proxy.
This patch restricts the call to `compute_limited` to the sliding sync
proxy implementation (aka MCS3575). It is not necessary for the sliding
sync native implementation (aka Simplified MSC3575). The proxy doesn't
implement the `limited` flag, contrary to Synapse. Let's not run
workarounds when we don't need them.
2024-08-12 15:06:01 +02:00
Ivan Enderlin d143c6198c fix(sdk): Remove SlidingSyncInner::past_positions.
The patch https://github.com/matrix-org/matrix-rust-sdk/pull/2395 has
introduced `SlidingSyncInner::past_positions` as a mechanism to filter
duplicated responses. It was a problem because the sliding sync `ops`
could easily create corrupted states if they were applied more than
once.

Since https://github.com/matrix-org/matrix-rust-sdk/pull/3664/, `ops`
are ignored.

Now, `past_positions` create a problem with the sliding sync native
implementation inside Synapse because `pos` can stay the same between
multiple responses.

While `past_positions` was helpful to fix bugs in the past, it's no
longer necessary today. Moreover, it breaks an invariant about `pos`: we
must consider it as a blackbox. It means we must ignore if a `pos` value
has been received in the past or not. This invariant has been broken for
good reasons, but it now creates new issues.

This patch removes `past_positions`, along with the associated code
(like `Error::ResponseAlreadyReceived` for example).
2024-08-12 14:26:31 +02:00
Ivan Enderlin 35b62a1a4a doc(sdk): Fix a markup.
Even if the visual output is the same, it's semantically better to have
`<a><code>…</code></a>` rather than `<code><a>…</a></code>` I believe.
2024-08-12 13:47:28 +02:00
Ivan Enderlin 704fe6719f feat(sdk): Add a log for must_process_rooms_response.
This patch adds a `trace` log for `must_process_rooms_response`. That's
useful for debugging weird bugs.
2024-08-12 13:47:28 +02:00
Ivan Enderlin 0b9e07a386 chore(sdk): Change visibility of SlidingSyncList::invalidate_sticky_data.
This patch changes the visibility of
`SlidingSyncList::invalidate_sticky_data` from `pub` to `pub(super)`.
This is the only place where it must be accessible from.
2024-08-12 13:47:28 +02:00
Ivan Enderlin 0a28c222f5 test(ui): Improve test_room_subscription.
This patch asserts that when subscribing to a new room, the old room
subscriptions are still present. Is it the behaviour we want? Probably
not, but this is the standard behaviour right now, and we need to assert
it.
2024-08-12 13:47:28 +02:00
Richard van der Hoff 31dbca6c28 testing: create ruma_response_from_json
We had *two* copies of `response_from_file`, and all calls to them were always
immediately followed by an operation to parse the response as a Ruma response
object.

We can save a whole lot of boilerplate with a generic function that wraps the
json into an HTTP response *and* parses it into a Ruma object.
2024-08-12 12:39:02 +01:00
Richard van der Hoff f96e82f833 indexeddb: Future-proofing: accept any db schema version up to 99
... so that next time we make a non-breaking change to the schema, it doesn't
break rollback
2024-08-12 12:37:56 +01:00
Richard van der Hoff f2792801c3 indexeddb: Add missing do_schema_upgrade call from v11 migration
We weren't updating the database schema version immediately after the v10 -> v11
migration. This was fine in practice, because (a) for now, there is no v12
migration so we ended up setting the schema version immediately anyway; (b) the
migration is idempotent.

However, it's inconsistent with the other migrations and confusing, and is
about to make my test fail, so let's clean it up.
2024-08-12 12:37:56 +01:00
Benjamin Bouvier a0c8b71236 refactor(sdk): merge Room::event and Room::event_with_config
It's better to have fewer public APIs, especially when there's little
annoyance to have it. We could use a request builder that converts into
a Future, too, but considering there's only a single optional parameter,
it's fine to include it in the function's signature.
2024-08-12 11:51:54 +02:00
Erik Johnston 101f6bd57b Sync: Don't spuriously show spinner
We should only show the spinner if the *first* sliding sync request is
taking a while. If we have received some data and the second request
takes a while, that is OK.

For the state transition of `Init -> SettingUp` this is handled
correctly, however for `Terminated -> Recovering -> Running` we waited
until the second request returned before hiding the sync spinner. This
meant that if the first request returned quickly the app would show new
data and *then* the sync spinner would show (if the second request took
time).

This situation occurs frequently with the new SSS API, where if all the
new data was returned in the first sync then the second sync would
block waiting for new data, triggering the sync spinner.
2024-08-12 11:47:18 +02:00
Benjamin Bouvier fa394fc45b test: remove unused TestClientBuilder::http_proxy method 2024-08-12 11:44:34 +02:00
Ivan Enderlin 16ca282ae4 chore(ui): Add logs inside RoomListService::sync.
This patch adds logs inside the `RoomListService::sync` method to know
what are the current states.
2024-08-12 10:03:22 +02:00
Ivan Enderlin be404f6666 feat(sdk): Subscribe to many rooms only via Sliding Sync.
This patch changes the `SlidingSync::subscribe_to_room` method to
`subscribe_to_rooms`. Note the plural form. It's now mandatory to
subscribe to a set of rooms. The idea is to avoid calling this method
repeatedly. Why? Because each time the method is called, it sends a
`SlidingSyncInternalMessage` of kind `SyncLoopSkipOverCurrentIteration`,
i.e. it cancels the in-flight sliding sync request, to start over with
a new one (with the new room subscription). A problem arises when the
async runtime (here, Tokio) is busy: in this case, the internal message
channel can be filled pretty easily because its size is 8. Messages
are not consumed as fast as they are inserted. By changing this API:
subscribing to multiple rooms will result in a single internal message,
instead of one per room.

Consequently, the rest of the patch moves the `subscribe` method of
`room_list_service::Room` to `room_list_service::RoomListService`
because it now concerns multiple rooms instead of a single one.
2024-08-09 11:58:59 +03:00
Stefan Ceriu 89ce8870a9 ffi: provide manual cancellation mechanism for identity/cross-signing reset handles
- works around swift issue where nil-ing the handle is not enough for it to get cancelled
2024-08-09 10:27:56 +03:00
Ivan Enderlin 70f46d48af chore(ffi): More idiomatic format!. 2024-08-08 17:54:49 +02:00
Timo 58f4e2b1ad FFI Element Call: Fix the permission list returned by the get_element_call_required_permissions ffi function. 2024-08-08 17:54:49 +02:00
Damir Jelić 58c35ee507 Add the rust-crypto-reviewers to the code owners for the crypto crate 2024-08-08 14:51:30 +02:00
Damir Jelić 40c347846e docs: Mention that the custom types in the crypto crate implement zeroize 2024-08-08 14:04:11 +02:00
Richard van der Hoff 9c809b900d Merge pull request #3813 from matrix-org/rav/split_machine_tests
crypto: split up the `machine.rs` behemoth
2024-08-08 12:00:39 +01:00
Damir Jelić a03893ee3f ci: Enable CI for draft PRs (#3815)
Our CI used to be quite slow and used up a lot of CI time, so as an
optimization we disabled CI for draft PRs.

This is a bit annoying since people want to open draft PRs to check if
CI passes, but without triggering any review requests.

Since our CI is nowadays a bit more efficient let's see if we can enable
it for draft PRs.
2024-08-08 12:58:51 +02:00
Richard van der Hoff 045374c604 Merge remote-tracking branch 'origin/main' into rav/split_machine_tests 2024-08-08 11:44:12 +01:00
Richard van der Hoff f3fe06be7d Merge pull request #3795 from matrix-org/valere/invisible_crypto/verification_pin
Crypto | Add support for verified identities change detection.
2024-08-08 11:10:47 +01:00
Richard van der Hoff 2220973adc crypto: move room_settings tests to their own file 2024-08-07 17:20:02 +01:00
Richard van der Hoff d8b0b2e097 crypto: move encrypted to-device tests to new file 2024-08-07 17:18:52 +01:00
Richard van der Hoff 93f6bdecb3 crypto: move decryption verification state tests to a new file 2024-08-07 17:17:51 +01:00
Richard van der Hoff 85b74edf80 crypto: move interactive verification tests to a new file 2024-08-07 17:17:02 +01:00
Richard van der Hoff 6f5442c5ce crypto: Move some olm-related tests to a separate file 2024-08-07 17:15:45 +01:00
Richard van der Hoff 18e27f3090 crypto: move OlmMachine tests to a separate file 2024-08-07 17:05:47 +01:00
Richard van der Hoff 066fdf99c3 crypto: Move create_session test helper to test_helpers 2024-08-07 17:02:47 +01:00
Richard van der Hoff 2c0d858833 crypto: Pull out OlmMachine test helpers to a new module 2024-08-07 16:10:26 +01:00
Richard van der Hoff 0b5f9aec5e crypto: promote machine module to directory 2024-08-07 16:08:56 +01:00
Valere 1b05380b60 Crypto: Verified identity changes - Add API at UserIdentity level + test 2024-08-07 15:06:19 +02:00
Valere 072b5d5605 Fix typo in method name 2024-08-07 13:36:20 +02:00
Valere 7e9f4fc5a0 CodeReview: Clarify comment 2024-08-07 13:34:12 +02:00
Valere 2e410e5d94 CodeReview: rename verification_latch to previously verified 2024-08-07 13:34:12 +02:00
Valere d81b12f389 Review: Fix false postitive typo in b64 string 2024-08-07 13:34:12 +02:00
Valere 58c4075ff5 Review: CI fix identity serialization test 2024-08-07 13:34:12 +02:00
Valere 3c9ae33be1 crypto: Verified identity changes - Migration of existing data 2024-08-07 13:34:12 +02:00
Valere c171c518c6 crypto: Verified identity changes Fix test with memory store
Clone of identities prevent detect changes due shared Arc. Force serializing/deserializing
2024-08-07 13:32:55 +02:00
Valere 20f717ec31 crypto: Verified Identity changes - Update latch on own trust change 2024-08-07 13:32:55 +02:00
Valere 0e01b1d93c crypto: Verified identity changes - Update latch on identity update 2024-08-07 13:29:58 +02:00
Valere 3d3f93a33d crypto: Verified identity changes TDD 2024-08-07 13:29:57 +02:00
Valere b4fe3059c3 crypto: Remember and detect verified identity changes for #1129 2024-08-07 13:29:57 +02:00
Jorge Martín 57963dcf36 ffi: add method for the ClientBuilder to provide a RequestConfig for the client 2024-08-07 11:48:34 +02:00
Jorge Martín 7d9fdc4f05 sdk-ui: add fn Room::event_with_config
This method works the same as `Room::event` but you can provide a custom `RequestConfig` to it.

It's especially useful for the pinned events timeline, since we need a max number of retries and a max number of concurrent requests. With this we can remove some unnecessary complexity.
2024-08-07 11:48:15 +02:00
Jorge Martín 1c4f035c99 sdk-ui: reverse (again) the order in the pinned events timeline
This way it matches the rest of timelines in the SDK, I reversed it here because I didn't realise most clients just do this reversal of ordering themselves. As they do, they need the same order for this timeline too to be able to reuse their existing logic.
2024-08-06 19:33:00 +02:00
Doug 71e4f60fa5 chore: Update the human readable description for the SentInClear shield. 2024-08-06 17:22:38 +03:00
Damir Jelić c3848ca016 crypto: Update the changelog 2024-08-06 15:10:13 +02:00
Damir Jelić a74f6bcc3f Add the Olm Session cache back in the CryptoStoreWrapper 2024-08-06 15:10:13 +02:00
Damir Jelić ba7fb7fc36 Convert all SessionStore locks to be async locks 2024-08-06 15:10:13 +02:00
Damir Jelić 96b615ba8e Remove Olm Session cache from the individual crypto store implementations 2024-08-06 15:10:13 +02:00
Jorge Martín c83fa3a532 sdk-ui: move conversion between Content::RoomPinnedEvents from ffi to sdk-ui 2024-08-06 10:37:18 +02:00
Jorge Martín d509d79472 ffi: add RoomPinnedEventsChange and a diffing step to know what happened in the last pinning/unpinning events action 2024-08-06 10:37:18 +02:00
Andy Balaam ffba842919 crypto: Don't recalculate SenderData if the sender is known but not verified 2024-08-06 09:34:21 +01:00
Jorge Martín 91c10ae213 sdk-ui: add PinnedEventsLoaderError::TimelineReloadFailed.
This error will be returned when the room has pinned event ids but the timeline couldn't load any of them.

Also fix tests.
2024-08-05 16:56:49 +02:00
Andy Balaam efdd6d2693 crypto: Use most-trusted SenderData available when decrypting 2024-08-05 15:46:08 +01:00
Andy Balaam 08bc563e9e crypto: Methods on SenderData for comparing trust level 2024-08-05 15:46:08 +01:00
Jorge Martín 71b5ab4d07 sdk-ui: fix pinned_events tests 2024-08-05 14:31:03 +02:00
Jorge Martín 4569b25d98 bench: fix benchmark by adding encryption event and clearing the cache in each iteration 2024-08-05 14:31:03 +02:00
Jorge Martín 9f7f1a98fb ffi: base the ffi::Room::clear_pinned_events method in sdk::Room::clear_pinned_events 2024-08-05 14:31:03 +02:00
Jorge Martín 4432b332fa sdk: add Room::clear_pinned_events 2024-08-05 14:31:03 +02:00
Jorge Martín d846563df3 sdk-ui: use reversed chronological order for TimelineFocus::PinnedEvents 2024-08-05 14:31:03 +02:00
Doug b453a0204e sdk-ui: Make the SentInClear shield red. 2024-08-05 10:48:06 +02:00
Doug 4c220ed030 shields: Put the Code inside the Colour instead of the Colour inside the Code. 2024-08-05 10:48:06 +02:00
Doug 037badf7a4 chore: Strongly typed ShieldStates. 2024-08-05 10:48:06 +02:00
Jorge Martín fba61751d5 sdk-ui: extract MAX_CONCURRENT_REQUESTS const for PinnedEventsLoader 2024-08-02 18:54:28 +02:00
Jorge Martín 2bd4db8a23 sdk-ui: use PinnedEventsLoader::update_if_needed when new timeline events are received. Using PinnedEventsLoader::load_events here was a mistake. 2024-08-02 18:54:28 +02:00
Jorge Martín 4de1375a76 ffi: add bindings for Room::clear_pinned_events_cache 2024-08-02 18:54:28 +02:00
Jorge Martín 1353406b80 sdk-ui: add Room::clear_pinned_events_cache to remove any cached pinned events from a room 2024-08-02 18:54:28 +02:00
Jorge Martín e1bffaee21 sdk: add PinnedEventCache::remove_bulk to be able to remove events from the cache 2024-08-02 18:54:28 +02:00
Jorge Martín 0496ef4313 sdk-ui: add test checking the pinned event cache is kept for different room instances 2024-08-02 18:54:28 +02:00
Jorge Martín 674605aeab sdk-ui: use the moved PinnedEventCache instead of the one encapsulated in PinnedEventsLoader 2024-08-02 18:54:28 +02:00
Jorge Martín f1b20a8ea5 sdk: add PinnedEventCache to Client.
This ensures the cache keeps the events even when the associated `Room` is dropped, which is what we want when using it to cache the pinned events for rooms in the client.

Add `fn Client::pinned_event_cached()` to get a reference to it.
2024-08-02 18:54:28 +02:00
Jorge Martín d1fe27c969 sdk: move PinnedEventCache from sdk-ui 2024-08-02 18:54:28 +02:00
Jorge Martín 769a627496 ci: try fixing the kotlin bindings tests by using NDK 27 2024-08-02 17:28:36 +02:00
Timo aff7aefd28 WidgetDriver: don't specify the length of the serializer. Its not needed for the json serializer. 2024-08-02 15:14:09 +01:00
Timo d60b9d3da2 Changelog: Add delayed event to the widget driver.
Also mention the breaking change in the public widget capabilities interface.
2024-08-02 15:14:09 +01:00
Timo 8d5dc18dd3 IntegrationTests: Add integration tests for the WidgetDriver delayed events. 2024-08-02 15:14:09 +01:00
Timo 49790c9f91 WidgetDriver: wire up everything in the widget process action handler. 2024-08-02 15:14:09 +01:00
Timo 2e8b135859 WidgetDriver: Add interacting with delayed event to the matrix driver.
In `matrix.rs` we add methods to interact with the matrix homeserver. And in `machine/mod.rs` we implement the widgetMachine cases for handling (checking capabilities and using the matrixDriver) delayed events.
2024-08-02 15:14:09 +01:00
Timo 7614bfb716 WidgetDriver: Introduce new capabilities for sending and updating delayed events.
Those capabilities are also exposed to the FFI.
+ related tests
2024-08-02 15:14:09 +01:00
Timo 0407dd7eb1 WidgetDriver: Introduce new Delay Event update widget action types.
Also update the naming from `future` to `delay` in existing types.
2024-08-02 15:14:09 +01:00
Timo 16a638400c chore: Update ruma to a version supporting authenticated delayed events. 2024-08-02 15:14:09 +01:00
torrybr be6bc444f8 chore: fix formatting 2024-08-02 15:42:54 +03:00
torrybr 36091a9ef3 sdk: refactor beacon_info tests into their own folder 2024-08-02 15:42:54 +03:00
Stefan Ceriu 1160383d71 sdk: fix identity reset not actually disabling backups when not enabled locally, resulting in conflicts and failing to correctly setup the newly reset session 2024-08-02 15:25:42 +03:00
Stefan Ceriu d7f3914673 sdk: throw an error instead of silently failing when disabling backups if they weren't previously enabled locally
- also change back the state from `disabling` to `unknown`
2024-08-02 15:25:42 +03:00
Jorge Martín 1ca4377baf ffi: add FFI bindings for creating the new pinned events focused timeline 2024-08-02 13:11:24 +02:00
Jorge Martín ab12f2f7ca sdk-ui: add and fix tests 2024-08-02 13:11:24 +02:00
Jorge Martín 6889430474 benchmark: add a benchmark to measure how loading pinned events performs 2024-08-02 13:11:24 +02:00
Jorge Martín 382c573973 sdk-ui: add TimelineFocus::PinnedEvents and TimelineFocusData::PinnedEvents to load the live pinned events for a room.
Add `PinnedEventsLoader` to encapsulate this logic, being able to provide a max number of events to load and how many can be loaded at the same time. Also implement an event cache for it.

 Add `PinnedEventsRoom` trait to use it in the same way as `PaginableRoom`, only for pinned events.
2024-08-02 13:11:24 +02:00
Jorge Martín 643c9a0b8e sdb-base: add methods for getting the pinned event ids of a Room as a stream and checking whether an event is pinned in the room or not 2024-08-02 13:11:24 +02:00
Kévin Commaille 689bf9b4dc base-sdk: Properly update direct targets of rooms with m.direct event
The code used to only add new targets to rooms
but never remove the ones that are not in the event anymore.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-02 12:46:37 +02:00
Richard van der Hoff 21efd60dba crypto: clarify/expand comments in Device::is_owner_of_session
I found some of this comment a bit hard to grok, so I've expanded it a
bit. Hope it's clear to others.
2024-08-02 10:04:08 +02:00
Valere 0850c0c1c9 feat(crypto): Add support for master key local pinning (#3639)
This patch ensures that we retain the master key for a given UserIdentityData object, even when a new and different identity arrives via the `/keys/query` endpoint. This concept, called pinning, is similar to certificate pinning in web browsers.

Retaining the master key allows us to detect changes and notify the user accordingly.
2024-08-02 09:13:14 +02:00
Doug aee8728418 sdk-ui: Fix a bug where local echoes were showing unencrypted shields. 2024-08-01 14:28:33 +03:00
Kevin Boos 7c5c5a8f30 timeline: re-export ReactionsByKeyBySender and ReactionInfo types (#3787)
This tiny change allows one to easily name the return type of
`EventTimelineItem::reactions()` such that you can use it in a function
signature. Same goes for the `ReactionInfo` type.

I think this was likely just a small oversight in recent changes to the
reactions API, so hopefully it's not controversial.

Without this, it's impossible to write a function that uses
`ReactionsByKeyBySender` or `ReactionInfo` in the signature.

Signed-off-by: Kevin Boos <kevinaboos@gmail.com>
2024-08-01 11:37:16 +01:00
Stefan Ceriu bfa069d6c0 Merge pull request #3783 from matrix-org/doug/sent-in-clear
UI Timeline: Build a ShieldState for an unencrypted event in an encrypted room.
2024-08-01 11:47:52 +03:00
Doug de502f42c6 chore: Add docs & todo on EventTimelineItem::from_latest_event 2024-07-31 19:08:31 +01:00
Doug dc1a34cd56 chore: Fix tests. 2024-07-31 15:51:40 +01:00
Doug 11a26d7fc4 chore: Make the room encryption state optional on timeline items. 2024-07-31 14:45:00 +01:00
Doug 1abbaa8607 ui: Add tests for Sent In Clear shield. 2024-07-31 13:06:36 +01:00
Doug bae0f304f7 ui: Add a ShieldState for "Sent in clear". 2024-07-31 13:06:36 +01:00
Andy Balaam 25df9a11c5 crypto: Rename OlmMachine::get_or_update_verification_state 2024-07-31 12:27:30 +01:00
Andy Balaam 1f937278b2 crypto: Store the updated SenderData in the store when we calculate it 2024-07-31 12:27:30 +01:00
Andy Balaam e2ff2daf5d crypto: Extract a function to convert SenderData->VerificationState 2024-07-31 12:27:30 +01:00
Damir Jelić 8760ea8440 chore: Update the bytemuck crate 2024-07-31 10:40:23 +02:00
Andy Balaam d03d3cff17 crypto: Remove unused retry_details from SenderData 2024-07-30 14:34:23 +01:00
Andy Balaam f88c6dff4d crypto: Use SenderDataFinder to implement get_verification_state
This avoids repeating equivalent code.
2024-07-30 13:50:58 +01:00
Andy Balaam d3835bb992 crypto: Return an error if is_owner_of_session fails 2024-07-30 13:50:58 +01:00
Andy Balaam 93a0fe500b crypto: Provide specific error types for SenderDataFinder 2024-07-30 13:50:58 +01:00
Andy Balaam 1f369c5912 crypto: Support a mismatched identity variant in session creation errors 2024-07-30 13:50:58 +01:00
Andy Balaam 2289c813f1 crypto: Allow testing SenderDataFinder with imported sessions 2024-07-30 13:50:58 +01:00
Andy Balaam 0a9431d01d crypto: Do not provide device ID to EncryptionInfo when device is not the owner 2024-07-30 13:50:58 +01:00
Stefan Ceriu f51eebb55f Merge pull request #3759 from matrix-org/stefan/crypto-identity-reset
ffi: add high level method for resetting the user's identity and deleting all associated secrets
2024-07-30 13:29:01 +03:00
Stefan Ceriu 8895e532bb Handle the situation where the backend skips user interactive authentication
- disable backups and recovery before requesting the reset handle
- attempt device key upload
- re-enable backups both when UIAA is required and when not
2024-07-30 13:13:26 +03:00
Richard van der Hoff 40e3a96ae3 Make message-ids feature the default (#3776)
The feature is now a no-op.

Will fix (I hope)
https://github.com/element-hq/element-android/issues/8872
2024-07-30 10:49:21 +01:00
Stefan Ceriu 8fe2b37354 ffi: Expose identity reset mechanism 2024-07-29 16:52:03 +03:00
Stefan Ceriu db064626fa sdk: Add high level method for resetting the user's identity and deleting all associated secrets 2024-07-29 16:52:03 +03:00
Andy Balaam 0b46a7e29c crypto: Make the return type of is_owner_of_session more specific 2024-07-29 14:50:22 +01:00
Andy Balaam 449e8b40b8 crypto: Extract a new error type: MismatchedIdentityKeysError 2024-07-29 14:50:22 +01:00
Andy Balaam d7224a7ede crypto: Store device_id in SenderData
This means we have all the information inside SenderData to populate
VerificationStatus and DeviceId for EncryptionInfo, so we can share the
code between SenderDataFinder and get_verification_state.
2024-07-29 11:11:44 +01:00
Jorge Martin Espinosa e0833110f2 Merge pull request #3771 from torrybr/feat/send-beacon
sdk: basic support for sending location beacons
2024-07-29 09:47:55 +02:00
torrybr 3756eeb385 test: move beacon tests into own file 2024-07-26 23:55:19 -04:00
torrybr dd252937c1 test: verify test_send_location_beacon_with_expired_live_share 2024-07-26 23:18:12 -04:00
torrybr ab6d039369 sdk: basic support for sending live location beacons 2024-07-26 22:57:16 -04:00
Jorge Martín 6fca1e81ed ffi: expose pin_event and unpin_event, also the currently pinned event ids in RoomInfo 2024-07-26 14:20:53 +01:00
Jorge Martín ab0494549e sdk-ui: add Timeline::pin_event and Timeline::unpin_event 2024-07-26 14:20:53 +01:00
Jorge Martín 15bf675e5e sdk-base: add Member::can_pin_or_unpin_event 2024-07-26 14:20:53 +01:00
Jorge Martín 95637c57da ffi: expose Room::can_user_pin_unpin check 2024-07-26 14:20:53 +01:00
Jorge Martín 8f90a76cb4 sdk: Add Room::can_user_pin_unpin check 2024-07-26 14:20:53 +01:00
Andy Balaam 844923dd44 crypto: Make TestOptions a builder 2024-07-26 12:47:41 +01:00
Andy Balaam 9efc6494d1 crypto: Check the device owns the session in SenderDataFinder
and add a flag to the SenderData struct to store the fact that this
check failed if it did.
2024-07-26 12:47:41 +01:00
Andy Balaam 5191737389 crypto: Require passing a session when we find SenderData 2024-07-26 12:47:41 +01:00
Damir Jelić 4fdc78f565 doc: Shorten one of our doc examples 2024-07-26 13:29:37 +02:00
torrybr 5bbe022e97 sdk: basic support for sending and stopping live location shares 2024-07-26 10:17:50 +01:00
Stefan Ceriu 6faf3f75e0 Merge pull request #3766 from matrix-org/update-dco
update DCO
2024-07-26 08:39:21 +03:00
Josh Simmons 38e842fc0f update DCO 2024-07-25 14:31:32 -07:00
Benjamin Bouvier 73759fc361 sdk-base: enable the "rand" feature on ruma there too 2024-07-25 15:20:34 +02:00
Benjamin Bouvier c1fda3a601 send queue: use the transaction id generated and saved in the db for network queries
Intense facepalm energy here.
2024-07-25 15:20:34 +02:00
Benjamin Bouvier 689f006c07 send queue: get rid of all the non-canonical events after canonicalization 2024-07-25 15:20:34 +02:00
Benjamin Bouvier 6f0da7e91b send queue: canonicalize dependent events keyed by parent transaction id 2024-07-25 15:20:34 +02:00
Benjamin Bouvier ce68ad4968 state store: add a transparent newtype ChildTransactionId to help distinguish the parent from the child transaction id 2024-07-25 15:20:34 +02:00
Benjamin Bouvier 02a929c614 timeline: flatten ReactionSenderData into PendingReaction 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 7a660749ed timeline: map from the reaction local-or-remote id to the item it's reacting to 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 6e82e4f14f timeline: store pending reactions without the indirection to Reactions::map 2024-07-25 15:13:23 +02:00
Benjamin Bouvier f0015bb10d timeline: store reaction by key by sender, instead of by key by local or remote id
This makes it impossible to represent states like "there's a local *and*
a remote echo for the same sender for a given reaction", or multiple
reactions from the same sender to the same event, and so on.
2024-07-25 15:13:23 +02:00
Benjamin Bouvier aa4f606171 timeline: reorganize handle_reaction 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 3a386121a1 timeline: remove Deref for ReactionGroup 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 3d40a5c30c ffi: remove the Reaction::count field
It's exactly the same as `Reaction::senders`'s length.
2024-07-25 15:13:23 +02:00
Benjamin Bouvier 4c76255689 timeline: move reaction-related structs to the reaction module, move meta's reaction fields to the Reactions object
No changes in functionality, pure code motion.
2024-07-25 15:13:23 +02:00
Benjamin Bouvier 37d9fa784a timeline: clear more things in TimelineInnerMetadata when clearing the timeline 2024-07-25 15:13:23 +02:00
Benjamin Bouvier fa1cf32883 timeline: move the clearing of TimelineInnerMetadata to its own function 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 002767a146 timeline: move the removal of reactions to pending_reactions 2024-07-25 15:13:23 +02:00
Benjamin Bouvier 5cd4462d27 dependencies: get rid of custom fork of openidconnect-rs
There's a published 4.0.0-alpha.2 version that compiles and doesn't
require the custom changes we needed.

Part of #3742.
2024-07-25 13:14:26 +02:00
Benjamin Bouvier 2eb6930988 send queue: add an own transaction id for dependent events
The previously named `transaction_id` is also renamed to
`parent_transaction_id` to make it clearer.
2024-07-24 17:54:25 +02:00
Benjamin Bouvier 0246863af3 send queue: make use of dependent events to remember an intent to edit/redact an event
This should get rid of most of the race conditions while
editing/redacting an event, and this paves the way for sending reactions
via the send queue.
2024-07-24 17:54:25 +02:00
Benjamin Bouvier d973fef280 send queue: add QueueStorage::client() helper method 2024-07-24 17:54:25 +02:00
Benjamin Bouvier 54c6f0517f send queue: canonicalize multiple dependent events into a more restricted list 2024-07-24 17:54:25 +02:00
Benjamin Bouvier 9a7f18c62c state store: add dependent queued events tables and operations 2024-07-24 17:54:25 +02:00
Damir Jelić f0ef37efae tests: Add tests for the cross-signing reset 2024-07-24 11:03:54 +02:00
Damir Jelić d9e91344aa examples: Update the cross-signing bootstrap example to use the new method 2024-07-24 11:03:54 +02:00
Damir Jelić 4883f3fa77 examples: Add a reset-cross-signing command to the oidc example 2024-07-24 11:03:54 +02:00
Damir Jelić 0d00bda0c6 encryption: Add a method to reset cross-signing keys 2024-07-24 11:03:54 +02:00
Damir Jelić 947a1b1aeb sdk: Refactor the way we determine which HTTP error is permannent 2024-07-24 11:03:54 +02:00
Andy Balaam ece0c6d703 crypto: Simplify error types in SenderDataFinder
Remove `Result` where it is not needed, and switch to `CryptoStoreError`
instead of `OlmError` where possible. Soon, this will allow us to call
some of these methods from places that don't know about `OlmError`.
2024-07-24 07:52:46 +01:00
Jorge Martín 79010af9e2 sdk-base: add pinned events to BaseRoomInfo to keep track of them.
Also add `Room::pinned_events(&self)` to get the current pinned events at any time.
2024-07-23 17:19:53 +02:00
Andy Balaam 99da0ff18d crypto: Simplify the interface of search_for_device 2024-07-23 15:03:19 +01:00
Andy Balaam 2045b326b9 crypto: Re-use existing get_device_from_curve_key method
These calls are equivalent because the old code called
`self.get_user_devices` with a `timeout` of `None`, which meant the call
to `wait_if_user_pending` inside was a no-op.
2024-07-23 14:56:47 +01:00
Doug dfdea0cb2e sdk: Ignore the sliding sync proxy value when using SSS.
Update crates/matrix-sdk/src/client/builder.rs

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Doug <6060466+pixlwave@users.noreply.github.com>
2024-07-23 11:34:49 +02:00
Doug 56e3780808 ffi: Use the SDK's (tested) logic for overriding the sliding sync proxy. 2024-07-23 11:34:49 +02:00
Benjamin Kampmann 96763aec42 sdk: Add a set_account_data method to the Room struct (#3740)
As it says on the tin. Needed the functions but they were missing. They are analogous to the GlobalAccountData setters.

Signed-off-by: Benjamin Kampmann <ben@acter.global>
2024-07-23 10:34:49 +02:00
Benjamin Bouvier 925c5b2233 workspace: update dependencies
Fixes #3744.
2024-07-22 15:01:06 +02:00
Benjamin Bouvier 5242f647f3 make_reply_event: don't require the event cache to be enabled for the API to work 2024-07-22 12:50:37 +02:00
Benjamin Bouvier ed2ab3ffe6 timeline: rename any_timeline_item_by_txn_id to item_by_transaction_id 2024-07-22 11:32:09 +02:00
Benjamin Bouvier 30c401ac75 timeline: rename item_by_transaction_id to local_item_by_transaction_id 2024-07-22 11:32:09 +02:00
Benjamin Bouvier bbae5364b3 timeline: update comments and error types for Timeline::redact
Yay, one fewer error type for the timeline.
2024-07-22 11:32:09 +02:00
Benjamin Bouvier ec057cf354 timeline: update comments around Timeline::edit 2024-07-22 11:32:09 +02:00
Benjamin Bouvier d4ad2b26cd timeline: remove unused error variants 2024-07-22 11:32:09 +02:00
Benjamin Bouvier 9b1b67fa09 timeline: rationalize edit/abort 2024-07-22 11:32:09 +02:00
Benjamin Bouvier 07f5289e7e release: Update Cargo.lock file 2024-07-22 10:57:25 +02:00
Damir Jelić d65e33ca6a Merge branch '0.7-release' into main 2024-07-19 12:02:40 +02:00
Ivan Enderlin 730d5a3803 fix(base): Fix a bug when an invite has no timestamp.
The sliding sync proxy has a bug: despite the presence of
`m.room.create` in `bump_event_types`, an invite with an `m.room.create`
event will not have a `timestamp`. Thus, such a room cannot be sorted
reliably.

This patch fixes this problem with a terrible hack, where it tries to
find an `origin_server_ts` value from within the `invite_state` state
events.

Please read the comment to learn more.
2024-07-19 11:39:14 +02:00
Ivan Enderlin 23a232e99f fix(base): Add the from_simplified_sliding_sync argument to BaseClien::process_sliding_sync. 2024-07-19 11:39:14 +02:00
Damir Jelić 4f79a15fa9 qrcode: Bump the version 2024-07-19 11:05:14 +02:00
Damir Jelić dda080c497 sqlite: Bump the version 2024-07-19 10:18:27 +02:00
Damir Jelić 11d5e56892 chore: Fix a formatting issue that snuck in while doing a security release 2024-07-18 17:31:55 +02:00
Damir Jelić 8b0d6afe4b Merge branch '0.7-release' into main 2024-07-18 17:26:18 +02:00
Damir Jelić a18f90bfaa chore: Fix some invalid test data 2024-07-18 17:10:00 +02:00
Damir Jelić 60ed367fd9 chore: Format the changelog a bit better 2024-07-18 17:10:00 +02:00
Damir Jelić 1157067dba chore: Prepare the matrix-sdk-crypto release 0.7.2 2024-07-18 17:10:00 +02:00
Damir Jelić 8efdba6136 crypto: Fix UserIdentity::is_verified to take into account our own identity
The `UserIdentity::is_verified()` method in the matrix-sdk-crypto crate
before version 0.7.2 doesn't take into account the verification status
of the user's own identity while performing the check and may as a result
return a value contrary to what is implied by its name and documentation.

This patch fixes this and adds a regression test.

The method itself is not used internally and as such has not a larger
impact.

Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2024-07-18 17:10:00 +02:00
Damir Jelić 1029e51eb3 chore: Use a released version of vodozemac (#3721) 2024-07-18 17:10:00 +02:00
Ivan Enderlin 5679a45f75 fix(ui): RoomList reacts to all RoomInfoNotableUpdate.
The `RoomList` provides a `Stream<Item = Vec<VectorDiff<Room>>>`.
This `Stream` receives updates from 2 sources: `RoomList::entries`,
and `Receiver<RoomInfoNotableUpdate>`. When a `RoomInfo` is
updated, a notable update is emitted and broadcasted. The
`RoomList` was filtering these notable updates by _reasons_ (namely
`RoomInfoNotableUpdateReasons`).

This is great and it's a good idea since we can filter which
`RoomInfoNotableUpdate` will trigger a `RoomList` update. However, too
many _reasons_ were hidden/implicit, and it creates several regressions
because (i) these _reasons_ were implicit, (ii) since the business rules
are not defined, there is no tests for that (not in this SDK, not in
apps like ElementX). It means we discover missing _reasons_ bug after
bug. It's not pleasant.

The reality is: we are in the middle of big changes, mostly with room
list client-side sorting and simplified sliding sync. We want to relax
a little bit. This patch then disable the feature _filter updates by
reasons_. The `RoomList` will update to all `RoomInfoNotableUpdate` for
the moment. We will get back to this optimisation later.
2024-07-18 16:29:45 +02:00
Damir Jelić 76a7052149 crypto: Fix UserIdentity::is_verified to take into account our own identity
The `UserIdentity::is_verified()` method in the matrix-sdk-crypto crate
before version 0.7.2 doesn't take into account the verification status
of the user's own identity while performing the check and may as a result
return a value contrary to what is implied by its name and documentation.

This patch fixes this and adds a regression test.

The method itself is not used internally and as such has not a larger
impact.

Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2024-07-18 16:20:47 +02:00
Ivan Enderlin 2aecf2950d fix(sdk): Add m.room.create to bump_event_types for sliding sync.
This patch updates the `rev` of our `ruma`'s fork to match the latest
commit of our `feat-sss` branch, see
https://github.com/ruma/ruma/commit/f25b3220d0c3ece7720020ed180af4955a855402.
It adds `m.room.create` in the `bump_event_types` of a
`v4::SyncRequestList` (equivalent of a `v5::request::List`).
2024-07-18 10:43:55 +02:00
Doug 4bbb6bd60c ffi: Don't add a custom SS proxy in ClientBuilder when using SSS. 2024-07-18 09:48:53 +02:00
Doug e37f65c46b ffi: Allow restoring an existing session with SSS enabled. 2024-07-18 09:48:53 +02:00
Doug 92b4c2a469 ffi: Expose client builder method to disable built in CAs. 2024-07-18 09:10:42 +02:00
Doug 20eb1db0f9 sdk: Allow building a Client with the built in CAs disabled. 2024-07-18 09:10:42 +02:00
Damir Jelić 30e95bf992 chore: Use a released version of vodozemac (#3721) 2024-07-17 18:07:39 +02:00
Ivan Enderlin dd20c37f35 doc(sdk): Fix some typos in the documentation. 2024-07-17 16:33:37 +02:00
Ivan Enderlin ea2a27075a feat(ffi,base,ui,sdk): Migrate from sliding sync to simplified sliding sync.
This patch migrates the entire SDK to sliding sync to simplified sliding
sync.
2024-07-17 16:33:37 +02:00
Ivan Enderlin c22de4c035 feat(sdk) Remove delta_token from sliding sync.
Simplified sliding sync doesn't have `delta_token`. This patch removes
it. Note: even the current sliding sync proxy doesn't use it.
2024-07-17 16:33:37 +02:00
Ivan Enderlin f84ce6a34f feat(sdk): Remove sort and bump_event_types from sliding sync.
Simplified sliding sync no longer has `sort` or `bump_event_types`
because they are static values on the implementation/server side. This
patch removes them here.
2024-07-17 16:33:37 +02:00
Ivan Enderlin 508176a2c7 feat(ui): Remove is_tombstoned filter from sliding sync.
Simplified sliding sync no longer has the `is_tombstoned` filter. This
patch removes it.
2024-07-17 16:33:37 +02:00
Ivan Enderlin dc9b975fc0 feat(base): Rename recency_timestamp to recency_stamp.
This patch renames “recency timestamp” to “recency stamp”. It prepares
the fact that simplified sliding sync has a `bump_stamp` instead of a
`timestamp`. The notion of _timestamp_ must be removed.
2024-07-17 16:33:37 +02:00
Ivan Enderlin ea9f79a006 feat(sdk): Remove SlidingSync::unsubscribe_from_room.
Simplified sliding sync no longer has the concept of unsubscribing from
a room. This patch removes this API.
2024-07-17 16:33:37 +02:00
Ivan Enderlin 1b92a034fd feat(sdk) Transform Simplified MSC3575 to MSC3575 in SlidingSync::sync_once.
This patch extracts most of `SlidingSync::sync_once` into a
method named `SlidingSync::send_sync_request`. The name mimics
the `SlidingSync::generate_sync_request` similar method: first we
_generate_, then we _send_.

The `SlidingSync::send_sync_request` is generic over the `Request` and
`::sync_once` passes the correct type depending of whether Simplified
MSC3575 is enabled.
2024-07-17 16:33:37 +02:00
Ivan Enderlin 5a4bf780fb feat(base): Create an http module for sliding_sync.
This patch creates an `http` module containing all the sliding sync types
types (from Simplified MSC3575 or simply MSC3575).
2024-07-17 16:33:37 +02:00
Ivan Enderlin c1a92bb3a9 chore(base): Move sliding_sync into its own module.
This patch moves the `sliding_sync` file into its own module.
2024-07-17 16:33:37 +02:00
Ivan Enderlin e3b950a9f0 feat(client): Add the Client::is_simplified_sliding_sync_enabled field.
This patch is the first over two patches to change the support
of Simplified MSC3575 from a compiler feature flag to a runtime
configuration. This configuration is held by the `Client`.
By default, it's turned off inside `matrix-sdk-ffi` and
`matrix-sdk-integration-testing`, otherwise it's turned on.
2024-07-17 16:33:37 +02:00
Kévin Commaille b79fdefbfd sdk-base: Split code into compute_summary and make less db requests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-17 14:19:53 +02:00
Kévin Commaille e3ad875293 sdk-base: Use left and banned members as heroes as a last resort
As suggested in the spec.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-17 14:19:53 +02:00
Kévin Commaille 3f6df64386 sdk-base: Use invited members too when computing heroes locally
The spec is clear that heroes are joined and invited members.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-17 14:19:53 +02:00
Kévin Commaille 3bab1c3584 sdk-base: Do not treat left rooms differently than joined rooms
Otherwise they always show up as "Empty" when we don't have a room summary.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-17 14:19:53 +02:00
Marco Antonio Alvarez 897f1cfef1 matrix_sdk_base: make sticker events suitable as latest_event (#3715)
Currently on Element X if you receive a sticker in a room, the room list
will show the room as updated but it will show the latest event that is
not a sticker. This change fixes that.

Signed-off-by: 
Marco Antonio Alvarez <surakin@gmail.com>

---------

Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com>
Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-07-17 13:09:48 +02:00
Kévin Commaille 1cc8292034 sdk: Allow to request animated thumbnails (#3710)
New feature from Matrix 1.11.

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

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-17 10:18:53 +00:00
Benjamin Bouvier d2c6a83175 sliding sync: only emit the recency_timestamp notable update if it's changed value 2024-07-17 10:32:49 +02:00
Benjamin Bouvier 91f8cfb48c integration tests: handle possibly limited timelines in test_stale_local_echo_time_abort_edit 2024-07-17 10:32:49 +02:00
Benjamin Bouvier 2fde15da79 integration tests: raise test_left_room join timeout a bit 2024-07-17 10:32:49 +02:00
Benjamin Bouvier 9d3fcb9290 room list: add logging explaining why we received an update 2024-07-17 10:32:49 +02:00
Benjamin Bouvier 171e4ef8f8 integration tests: make test_stale_local_echo_time_abort_edit resilient to more races 2024-07-17 10:32:49 +02:00
Benjamin Bouvier 55d0a3cdb8 integration tests: make test_event_with_context wait for as long as it claims
The previous linear backoff would retry overall 10 times the value, and
10x30 == 300ms, not 3 seconds.
2024-07-17 10:32:49 +02:00
Benjamin Bouvier 1f1310c797 integration tests: add a small helper for getting a room with a linear backoff 2024-07-17 10:32:49 +02:00
Benjamin Bouvier c9e2db1fe5 test(sdk): make test_room_info_notable_update_deduplication better 2024-07-17 10:32:49 +02:00
Benjamin Bouvier af469f6bbd Revert "test(integration-testing): Remove a flaky and useless test."
This reverts commit 03e1fd78a6.
2024-07-17 10:32:49 +02:00
Benjamin Bouvier 8a52d6f2a3 notable room updates: have a change to the unread-marker event cause a notable update
Also log out why an account data event couldn't be deserialized.
2024-07-17 10:14:23 +02:00
Andy Balaam 8ebf3c02c6 doc: Remove extra word from a doc comment 2024-07-17 08:11:05 +01:00
Damir Jelić 8fab34cc8c chore: Fix some typos 2024-07-16 20:13:59 +02:00
Damir Jelić 99010ed83b crypto: Get rid of all the read-only mentions
There were some leftovers from the rename of the ReadOnlyDevice and
identity structs. The store still referenced them.

This gets rid of all the mentions and improves the documentation of the
store methods for devices and identities.
2024-07-16 20:13:59 +02:00
Doug 5428339b27 xtask(swift): Put the headers in a module subdirectory.
Fixes a conflict with any other UniFFI library built the same way.
2024-07-16 17:25:15 +02:00
Andy Balaam 942b2f937c logging: Extract debug log code into a separate function.
This reduces the work we do to calculate changed devices etc. when DEBUG
logging is not enabled, but more importantly (to me) it makes clear
that this code is only used for logging.
2024-07-16 16:00:53 +01:00
Andy Balaam 8845550e72 crypto: Calculate sender data for incoming sessions
Part of https://github.com/matrix-org/matrix-rust-sdk/issues/3543.
Builds on top of https://github.com/matrix-org/matrix-rust-sdk/pull/3556

Implements the "fast lane" as described in
https://github.com/matrix-org/matrix-rust-sdk/issues/3544

This will begin to populate `InboundGroupSession`s with the new
`SenderData` struct introduced in
https://github.com/matrix-org/matrix-rust-sdk/pull/3556 but it will only
do it when the information is already available in the store. Future PRs
for this issue will query Matrix APIs using spawned async tasks.

Future issues will do retries and migration of old sessions.

---------

Signed-off-by: Andy Balaam <mail@artificialworlds.net>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-07-16 13:04:32 +01:00
Damir Jelić 84c9280349 docs: Fix the OIDC QR code login example in the docs 2024-07-16 13:56:45 +02:00
Timo 5dbd5f1adf element call(ffi): add widget permissions for room create and call member state keys with device id (#3706)
It is possible to remove the m.call capability because we now have
merged and released a version that does not request it anymore on
call.element.io

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-07-16 11:38:48 +00:00
Benjamin Bouvier 1730ec5155 test(sdk): make the test_delayed_decryption_latest_event faster and more robust 2024-07-16 11:30:12 +02:00
Ivan Enderlin 8e65099f3d test(sdk): Rewrite test_delayed_decryption_latest_event.
This patch rewrites the `test_delayed_decryption_latest_event` test a
little bit. It does exactly the same things, but in a simpler way: it
removes multiple `sleep` and remove 2 sliding sync loops.

First off, the `SyncService` already starts the `RoomListService and the
 `EncryptionSync` service. Both of them have their own sliding sync
loop. The test doesn't need other sliding sync loops in their own tasks,
this is not necessary at all: it's just pretty confusing and doesn't
reflect the reality, i.e. how these API are supposed to be used.

Second, it also tests the room for Bob is seen as encrypted.

Third, the `VectorDiff::Reset` is tested before the event from Bob
is sent. It's not only for clarity: it makes the test more robust for
future modifications.

Fourth, instead of waiting with a `sleep` for the event from Bob to be
received by Alice, we instead wait on the room list's stream of Alice to
receive an update. It's more robust this way and reflects the real usage
of this API. It also helps to remove an intermediate `assert_pending!`
that is no longer necessary because we are waiting on the stream just
after.

Finally, just like for the previous modification, this patch removes
another `sleep` for the to-device event from Bob to be received by
Alice, and instead wait on the room list's stream to receive an update.
It's again more robust and reflects the real usage of this API. Plus, it
makes the last `assert_pending!` macro to not be flaky.
2024-07-16 11:30:12 +02:00
Ivan Enderlin 62137e5a3e fix(sdk): SlidingSyncBuilder restores the rooms from the cache.
This patch fixes a bug where rooms stored in the sliding sync cache
aren't restored by the `SlidingSyncBuilder`.

This patch also removes `SlidingSyncBuilder::rooms` fields which was
used but never modified. It's dead code.
2024-07-16 10:34:45 +02:00
Ivan Enderlin 84dfb78e1d test(sdk): Test rooms are restored in the sliding sync cache.
This patch updates a test to ensure that rooms are restored correctly by
the sliding sync cache.

This test fails :-].
2024-07-16 10:34:45 +02:00
Benjamin Bouvier 81d388a55b errors: allow EventCacheError to be a subset of matrix_sdk::Error
This required breaking a type reference cycle, by introducing a box.
2024-07-15 13:49:23 +02:00
Benjamin Bouvier 7b4f480b2a timeline: use Room::make_edit_event instead of doing adhoc edition 2024-07-15 13:49:23 +02:00
Benjamin Bouvier 034bd64e2c sdk: add a Room::make_edit_event() method to create an edit event for a m.room.message 2024-07-15 13:49:23 +02:00
Benjamin Bouvier 328c4767a0 event cache: add RoomEventCache::event() to get an event by id in a single room 2024-07-15 13:49:23 +02:00
Benjamin Bouvier 7a85b7abdc timeline: and yet another red herring warn is removed
Same reason as two commits ago; presence of a transaction id doesn't
mean it's related to the current session.
2024-07-15 10:21:16 +02:00
Benjamin Bouvier 851784c8c0 nit(timeline): remove else after return 2024-07-15 10:21:16 +02:00
Benjamin Bouvier faa961eb7a timeline: remove another red herring
Again, a transaction id received from the remote flow doesn't mean it
corresponds to a local echo sent *this particular session*, so no need
to warn about it.
2024-07-15 10:21:16 +02:00
Benjamin Bouvier a7011d8ac0 ffi: remove lag about timeline reset
We do reset a timeline the first time, to fill the initial items, so
this is a red herring.
2024-07-15 10:21:16 +02:00
Benjamin Bouvier e6525c093f timeline: remove red herring log
This log can happen when an event is received, has a transaction id (in
the data received from sync), and doesn't have a corresponding timeline
item (be it local or remote). There's no reason to warn about this,
because this would happen in most cases, for new incoming events coming
from sync, and this pollutes the logs of rageshakes.
2024-07-15 10:21:16 +02:00
Damir Jelić 57eb225506 chore: Bump our bytes dependency
The version we were using was yanked.
2024-07-15 09:38:31 +02:00
Ivan Enderlin e1a607b6cf Merge pull request #3669 from matrix-org/misc/update-uniffi-to-0.28.0
chore: update UniFFI to `v0.28.0`
2024-07-15 09:11:28 +02:00
Ivan Enderlin 03e1fd78a6 test(integration-testing): Remove a flaky and useless test.
This patch removes the `test_room_info_notable_update_deduplication`
test. First off, it's flaky because sometimes Synapse lags, or sends
another events, which makes the test to fail. Second, the same feature
is tested inside the `matrix_sdk_ui::room_list_service` test suite,
with `test_room_sorting` and `test_room_latest_event`, and inside the
`matrix_sdk_base::sliding_sync` test suite, with a better granularity.
And lastly, this test doesn't test what it says: there is no room info
notable update deduplication whatsoever. I personally don't believe it
has ever existed. This test isn't necessary.
2024-07-12 18:55:34 +02:00
Damir Jelić a5dbfa66a7 encryption: Rename the ReadOnly user identity types 2024-07-12 18:06:34 +02:00
Damir Jelić 6f0d3b663b encryption: Rename ReadOnlyDevice to DeviceData
ReadOnlyDevice is not particularly useful as a description of why we
have two device types. This commit renames it into DeviceData, as this
struct is used to hold the device keys and additional local device data.

I'm not quite sure why it took me so long to come up with a better name.

Please forgive me past readers.
2024-07-12 18:06:34 +02:00
Damir Jelić 9d464eb908 tests: Add a snapshot of a SQLite database to perform regression tests
The test database was created using a slightly modified `oidc-cli`
example, to turn of the database encryption, on commit
d6dca91df86413b0cbf193a4be191835dd81862e
2024-07-12 18:06:34 +02:00
Alexis Métaireau 48f11ea025 Enforce the redundant_clone Clippy lint rule.
Fixes #3683
2024-07-11 15:28:33 +02:00
Valere d9b2b53f83 feat(timeline): Expose shield state for EventTimelineItem (#3679) 2024-07-11 13:18:19 +02:00
Jorge Martin Espinosa 6bcd07fd7b Merge branch 'main' into misc/update-uniffi-to-0.28.0
Signed-off-by: Jorge Martin Espinosa <jorgem@element.io>
2024-07-11 11:27:44 +02:00
Ivan Enderlin bacf85d807 chore: Use anyhow from the workspace. 2024-07-11 11:16:17 +02:00
Ivan Enderlin b163368be0 chore: Use futures-util from the workspace. 2024-07-11 11:16:17 +02:00
Ivan Enderlin 5ebfd7bc55 chore: Use tokio from the workspace. 2024-07-11 11:16:17 +02:00
Ivan Enderlin 0d264d209f chore: Use tracing-subscriber from the workspace. 2024-07-11 11:16:17 +02:00
Ivan Enderlin ea8628e210 chore(labs): multiverse uses tokio from the workspace. 2024-07-11 11:16:17 +02:00
Timo f4078fdf68 widget-driver: rename all mentions of future in the context of future events.
We need to disambigute future events and rust futures.
2024-07-10 18:27:53 +02:00
Timo c366bae428 widget_driver: doc and test changes (review) 2024-07-10 18:27:53 +02:00
Timo cf1ec862c2 changelog: add future events to the widget-driver/api. 2024-07-10 18:27:53 +02:00
Timo 10db61575f widget-driver: add integration test for future events. 2024-07-10 18:27:53 +02:00
Timo 2e936702c8 widget-driver: Fix widget action format and add test. 2024-07-10 18:27:53 +02:00
Timo 5922fb8ff3 widget-driver: add tests for future events in widget send action. 2024-07-10 18:27:53 +02:00
Timo f6c2a28682 widget-driver: Support for sending futures events through the widget api. 2024-07-10 18:27:53 +02:00
Benjamin Bouvier 6ee2919576 event cache: add EventCache::event() to get an event by id 2024-07-10 17:43:11 +02:00
Benjamin Bouvier b0f60d2bf7 event cache: remove allow(dead_code) and remove dead code 2024-07-10 17:43:11 +02:00
Benjamin Bouvier 61fb0aeafc timeline: move TimelineEventContext::encryption_info to the remote flow
Since it's only used for remote events, and a local echo couldn't figure
that out anyways (since it's not sent yet, and that information makes
sense after sending).
2024-07-10 17:41:00 +02:00
Benjamin Bouvier 9e9df163b9 timeline: remove duplicate set_fully_read_event that does the same as handle_fully_read_marker 2024-07-10 15:29:46 +02:00
Benjamin Bouvier 54c037ed03 timeline: add regression tests for adjusting day divider/read marker after cancelling a local echo 2024-07-10 15:29:46 +02:00
Benjamin Bouvier 7d16ca54f4 timeline: remove trailing read markers 2024-07-10 15:29:46 +02:00
Benjamin Bouvier f96f55ccd9 timeline: move the handle_local_echo and handling of RoomSendQueueUpdate to TimelineInner
This will make testing easier.
2024-07-10 15:29:46 +02:00
Benjamin Bouvier afa2e8063d timeline: adjust day divider and read marker items after a local echo has been edited or removed 2024-07-10 15:29:46 +02:00
Ivan Enderlin 25bb8e9d72 chore(base): Format doc. 2024-07-10 15:29:10 +02:00
Ivan Enderlin c59ed5d877 feat(ui): RoomList is refreshed by RoomInfoNotableUpdateReasons::READ_RECEIPT.
This patch listens to `RoomInfoNotableUpdateReasons::READ_RECEIPT` to
update the `RoomLIst` stream.
2024-07-10 15:29:10 +02:00
Ivan Enderlin 396b7eff7d feat(base): New RoomInfoNotableUpdateReasons::READ_RECEIPT!
This patch adds the new `RoomInfoNotableUpdateReasons::READ_RECEIPT`
reason. It detects it and adds the test to ensure it's sent as expected.
2024-07-10 15:29:10 +02:00
Ivan Enderlin a5702e92f1 test(base): Test RoomInfoNotableUpdateReason::RECENCY_TIMESTAMP is sent.
This patch adds a missing test to ensure that a
`RoomInfoNotableUpdateReason::RECENCY_TIMESTAMP` is correctly sent.
2024-07-10 15:29:10 +02:00
Ivan Enderlin c4e45b5660 doc(base): Remove an outdated documentation information.
This patch removes the mention of a returned value whilst the function
returns nothing.
2024-07-10 15:29:10 +02:00
Andy Balaam 847bf5b974 Merge pull request #3677 from matrix-org/andybalaam/rename_msk_master_key
crypto: Rename msk to master_key for consistency with the wider codebase
2024-07-10 13:03:04 +01:00
Andy Balaam 0449ca89ce crypto: Rename msk to master_key for consistency with the wider codebase 2024-07-10 11:15:45 +01:00
Jorge Martin Espinosa 40343aa67e fix(sdk): force room member reload after inviting a user (#3672)
This is needed to prevent the race condition where the invite request finished, the `/sync` one didn't fetch the new membership event yet and we send a message in the room. This message won't be encrypted for the newly invited user and will result in an UTD.

I added a new integration test and I can confirm this [complement-crypto test](https://github.com/matrix-org/complement-crypto/pull/98) now passes instead of being skipped.

Fixes #3622.

---

* fix(sdk): force room member reload after inviting a user

This is needed to prevent the race condition where the invite request finished, the `/sync` one didn't fetch the new membership event yet and we send a message in the room. This message won't be encrypted for the newly invited user and will result in an UTD.

* Use `room.mark_members_missing()` instead, add integration test

* Abort syncing before the test ends

* Resolve nit: else after a return

* Fix race condition where bob may try to join the room before the invite is received

* Remove double sync
2024-07-10 10:01:50 +00:00
Benjamin Bouvier 625652e895 tests: get rid of the non_sync_events! macro 2024-07-10 12:00:17 +02:00
Ivan Enderlin d78b6826b9 chore(sdk): Remove RoomListEntry and ops in Sliding Sync.
This patch removes everything related to the computation of `ops`
from a sliding sync response. With the recent `RoomList`'s client-side
sorting project, we no longer need to handle these `ops`. Moreover, the
simplified sliding sync specification that is coming removes the `ops`.

A `SlidingSyncList` was containing a `rooms` field. It's removed by
this patch. Consequently, all the `SlidingSyncList::room_list` and
`::room_list_stream` methods are also removed.

A `FrozenSlidingSyncList` was containing the `FrozenSlidingSyncRoom`.
This patch moves the `FrozenSlidingSyncRoom`s inside
`FrozenSlidingSync`. Why is it still correct? We only want to keep the
`SlidingSyncRoom::timeline_queue` in the cache for the moment (until
the `EventCache` has a persistent storage). Since a `SlidingSyncList`
no longer holds any information about the rooms, and since `SlidingSync`
itself has all the `SlidingSyncRoom`, this move is natural and still
valid.

Bye bye all this code :'-).
2024-07-10 11:44:07 +02:00
Benjamin Bouvier e1fbfbe603 tests: get rid of EventBuilder::make_message_event_with_id 2024-07-10 11:39:16 +02:00
Benjamin Bouvier a9a4d7b4c8 tests: get rid of EventBuilder::make_reaction_event and TestTimeline::handle_live_reaction 2024-07-10 11:39:16 +02:00
Benjamin Bouvier 8e90783f1f test(timeline): get rid of handle_live_message_event_with_id
The `EventFactory` can build events with a specific `event_id` already.
2024-07-10 11:39:16 +02:00
Benjamin Bouvier a0a076a895 tests: get rid of EventBuilder::make_redaction_event and TestTimeline::handle_live_reaction
The `EventFactory` is improved to support creating those events too,
reducing the number of custom events creators everywhere.
2024-07-10 11:39:16 +02:00
Benjamin Bouvier f7504b4ff2 test(timeline): remove "custom" in handle_back_paginated_custom_event 2024-07-10 11:39:16 +02:00
Benjamin Bouvier d7cbd9d218 test(timeline): get rid of handle_back_paginated_message_event_with_id
Callers can make use of the `EventFactory` which is easier to understand
IMO, and that avoids a special testing function just for this.
2024-07-10 11:39:16 +02:00
Benjamin Bouvier 5a04f5b66a test(timeline): get rid of custom handle_live_event
This is `TimelineInner::add_events_at` with fixed parameters.
2024-07-10 11:39:16 +02:00
Benjamin Bouvier 3294a6c83a test(timeline): use handle_live_event instead of handle_live_custom_event
They're the same picture.</meme>
2024-07-10 11:39:16 +02:00
Richard van der Hoff 8d54bd92d1 crypto: Pass room id and session id to room_keys_withheld_received_stream (#3674) 2024-07-10 09:49:14 +01:00
Richard van der Hoff 2d3e2dab54 crypto: Expose new stream about room_key withheld messages (#3660)
Part of the fix to element-hq/element-web#27653.
2024-07-09 17:55:46 +00:00
Benjamin Bouvier 1bc044349e sdk: use a single field store both the server versions and the unstable features
The only thing is that our client builder allows setting only the server
versions, so this means the new field has to contain two `Option`al
fields. This causes a bit of churn, but isn't too bad in the end.
2024-07-09 12:37:33 +02:00
Benjamin Bouvier 2ca6a0e91e ffi: remove ability to set server versions in the ClientBuilder 👿 2024-07-09 12:37:33 +02:00
Benjamin Bouvier 19fcae4e0b sdk: add a way to reset the in-memory and on-disk caches for server capabilities
This required changing the OnceCell to RwLock<Option<>>, because a
OnceCell can't be reset to another value.
2024-07-09 12:37:33 +02:00
Benjamin Bouvier a76b6faa9f sdk: cache after requesting server capabilities, and attempt to restore them from the cache later 2024-07-09 12:37:33 +02:00
Benjamin Bouvier 2785365b53 store: ServerCapabilities can decode to None if stale 2024-07-09 12:37:33 +02:00
Benjamin Bouvier c7d1a7db4f store: save/restore server capabilities 2024-07-09 12:37:33 +02:00
Benjamin Bouvier 48fc80c643 sdk: fill both the server versions and unstable features when doing the /versions request 2024-07-09 12:37:33 +02:00
Benjamin Bouvier 96475b7f50 media: get rid of the TODO about ClientBuilder option to force use of auth endpoints
Fixes #3650.
2024-07-09 12:37:33 +02:00
Jorge Martín 3d9bdd2bb4 chore: update UniFFI to v0.28.0 2024-07-09 11:23:20 +02:00
Kegan Dougal e5f92947e7 memory-store: release locks earlier to avoid deadlocks
I've been debugging a cause of flakey complement-crypto tests for
about a month now. I was pretty convinced it was deadlocking
somewhere in the memory store `save_changes` code. With additional
logging, it's now clear that the there is an ABBA style deadlock
when `save_changes` is called at the same time as `get_state_events`.

I've also adjusted code for `get_user_ids` as it has a very similar
pattern and also acquires locks in reverse order to `save_changes`,
so is potentially vulnerable to this.
2024-07-09 11:02:41 +02:00
Ivan Enderlin 02dddb47c9 fix(base) Move StateChanges::room_info_notable_changes into a standalone BTreeMap.
This patch extracts `StateChanges::room_info_notable_changes` as a
variable that is passed to `BaseClient::apply_changes`.
2024-07-08 18:47:55 +02:00
Ivan Enderlin 753606779b test(base): Update a test.
This patch updates a test to ensure calling
`Room::on_latest_event_decrypted` will emit a room info notable update
with the `LATEST_EVENT` reason.
2024-07-08 18:47:55 +02:00
Ivan Enderlin 4cf0d7a18b fix(ui): Emit a RoomInfoNotableUpdateReasons::RECENCY_TIMESTAMP ony for non-new rooms.
This patch avoids to emit a
`RoomInfoNotableUpdateReasons::RECENCY_TIMESTAMP` for rooms that are new.
Otherwise the entries in `matrix_sdk_ui::room_list_service::RoomList`
receive a `VectorDiff` because a new room is inserted, and then a
`VectorDiff` because the recency timestamp is updated. The second
`VectorDiff` is useless in this case.
2024-07-08 18:47:55 +02:00
Ivan Enderlin ec7fa76240 feat(base): Revisit the roominfo_update API.
First off, this patch removes the
`RoomInfoNotableUpdate::trigger_room_list_update` field. It is replaced
by a `reasons: RoomInfoNotableUpdateReasons` 8-bit unsigned integer. It
addresses the following issues:

1. When a subscriber receives a `RoomInfoNotableUpdate`, they have no
   idea what has triggered this update.
2. In
   `matrix_sdk_base::sliding_sync::BaseClient::process_sliding_sync_e2ee`,
   we were triggering an update even if the latest event wasn't
   modified: it is a false-positive, it was a bug and a waste of
   resources. Now it's more refined, see the why below.

Second, this patch removes the second `trigger_room_list_update`
argument of `matrix_sdk_base::BaseClient::apply_changes`. This method
now knows where to find the reasons for the room info notable updates,
see next point.

Third, this patch adds a new
`matrix_sdk::StateChanges::room_info_notable_updates` field which is a
B-tree map between an `OwnedRoomId` and a
`RoomInfoNotableUpdateReasons`. The idea is that all places that receive
a `StateChanges` can also create a room info notable update with a
specific reason. This is a finer grained mechanism, and it avoids to
pass new arguments everywhere. It's cleaner.

Finally, it's easier than ever to add a new reason and to propagate it
to subscribers.
2024-07-08 18:47:55 +02:00
Ivan Enderlin 66e02f39ef chore(sdk): Rename RoomInfoUpdate into RoomInfoNotableUpdate.
The patch renames `RoomInfoUpdate` to `RoomInfoNotableUpdate`.
The functions, methods or variables whose names start with
`roominfo_update(.*)`` are renamed `room_info_notable_update$1`.
2024-07-08 18:47:55 +02:00
Ivan Enderlin c8e05173e4 chore(base): Move imports under the correct feature flag.
Some imports are only required under `cfg(feature = "e2e-encryption")`.
Let's fix these warnings.
2024-07-08 18:47:55 +02:00
Johannes Marbach 8e0282ac4a ffi: expose methods for SSO login (#3558)
* ffi: expose methods for SSO login

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Update bindings/matrix-sdk-ffi/Cargo.toml

Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Refactor code to use a separate object to complete the SSO login

* Remove superfluous .workspace

* Remove superfluous field name

* Fix formatting with nightly toolchain

* Fix clippy errors using nightly toolchain

* Move SSO methods over into Client as AuthenticationService will go away very soon

* Add login tests

* Assign tokio runtime and url getter

* Reformat

* Relocate parts of the code as per review comments

* Add url to debugging representation of SsoHandler

* Add example for login_with_sso_callback

* Use unwrap_or_default to avoid creating empty string

* Remove leftover dependencies

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-07-08 14:13:44 +02:00
Kegan Dougal 11cbf849cc base: adjust trace logging in memory store (#3659)
Now we have more information on which locks are implicated, refine the logs to identify the exact lock.
2024-07-05 11:48:49 +02:00
Richard van der Hoff f9a19c5603 Merge pull request #3651 from matrix-org/rav/indexeddb_storage_efficiency_fix_bugs
indexeddb: fix bugs in serialization improvement
2024-07-05 10:43:53 +01:00
Richard van der Hoff fac7221c7e Merge remote-tracking branch 'origin/main' into rav/indexeddb_storage_efficiency_fix_bugs 2024-07-05 10:29:24 +01:00
Benjamin Bouvier d6300bbda7 http_client: log each attempt at sending a request, instead of a single one
When a request fails because of the exponential backoff, it won't be
re-logged again. It would be useful, for the purposes of the send queue
notably, to see when a request is re-attempted.
2024-07-04 18:32:44 +02:00
Benjamin Bouvier b80c2f7197 timeline: use the previous content's membership info when it's missing from the current membership event
Synapse returns a bare `{ "membership": "leave" }` as the content of a
room membership event (for leave membership changes and likely others).
In this case, it'd still be nice to have some kind of display
name/avatar URL to show in UIs; it's possible to reuse information from
the previous member event, if available.
2024-07-04 17:09:34 +02:00
Jorge Martín 07b6425e10 ffi: Timeline::load_reply_details checks if the event exists locally
Before it would go fetch the event from the server using a network request.
2024-07-04 16:13:35 +02:00
Benjamin Kampmann d49cb54b67 Allow to limit the number of concurrent requests made by the sdk (#3625)
Add a new `max_concurrent_requests` parameter in the `RequestConfig` limits the number of http(s) requests the internal sdk client issues concurrently (if > 0). The default behavior is the same as before: there is no limit on concurrent requests issued.

This is especially useful for resource constrained platforms (e.g. mobile platforms), and if your pattern might lead to issuing many requests at the same time (like downloading and caching all avatars at startup).

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

Signed-off-by: Benjamin Kampmann <ben.kampmann@gmail.com>
2024-07-04 16:01:15 +02:00
Ivan Enderlin aaccfdfea5 Merge pull request #3655 from matrix-org/jme/remove-is-encrypted-from-room-info
ffi: remove `is_encrypted` field from `RoomInfo`
2024-07-04 14:21:46 +02:00
Jorge Martín ad4d24f7d0 ffi: remove is_encrypted field from RoomInfo
It turns out this will cause a network request if the encryption info hasn't been loaded before, which is the case for opening a client in offline mode. It will slow down displaying the room list or loading the room info in general.
2024-07-04 13:52:27 +02:00
Ivan Enderlin 66743f5e57 Merge pull request #3654 from Hywan/fix-complement-crypto
test(crypto): Restore Complement crypto since it's been updated
2024-07-04 13:34:59 +02:00
Andy Balaam 03d4a30eb4 crypto: Move device_keys to DecryptedOlmV1Event as per MSC4147 2024-07-04 11:22:38 +01:00
Ivan Enderlin a82a1f505e test(crypto): Restore Complement crypto since it's been updated.
This patch restores Complement crypto since it's been updated to the
latest version of the Rust SDK.
2024-07-04 12:00:12 +02:00
Richard van der Hoff 98e9abd6c9 indexeddb: tests for new schema migration 2024-07-04 09:10:21 +01:00
Richard van der Hoff 2a8e8c1fff indexeddb: improve docs and tests 2024-07-04 09:10:21 +01:00
Ivan Enderlin 9aa277405d Merge pull request #3653 from matrix-org/jme/verify-android-bindings-build-fine
ci: Verify Android bindings build fine
2024-07-04 10:02:06 +02:00
Jorge Martín 3be5311113 ci: Verify Android bindings build fine 2024-07-04 09:40:26 +02:00
Ivan Enderlin a957e70698 Merge pull request #3646 from Hywan/feat-ui-room-list-roominfo-update
feat(ui): Trigger room list update only when necessary
2024-07-03 21:16:06 +02:00
Ivan Enderlin 77feed2447 test: Fix flakyness. 2024-07-03 20:56:23 +02:00
Richard van der Hoff 7b25a1c2f0 indexeddb: update changelog 2024-07-03 19:43:44 +01:00
Richard van der Hoff bb0e50ce02 indexeddb: migrate name and format of backup_version
The name was stupid, and this was the only string that was stored in the legacy
format; we can fix both problems with a cheeky migration.
2024-07-03 19:41:49 +01:00
Richard van der Hoff ab3ea8c467 indexeddb: Improve handling of legacy values in deserialize_value
Turns out legacy unencrypted objects can take many forms, and we need to be
tolerant of them.

Also expose `deserialize_legacy_value` as a separate function, because we're
going to need to special-case it.
2024-07-03 19:05:33 +01:00
Benjamin Bouvier 6679caecf9 ffi: add doc comments to TracingFileConfiguration and TracingConfiguration 2024-07-03 19:14:44 +02:00
Richard van der Hoff 885b2b22bd indexeddb: Revert change from into_serde to serde_wasm_bindgen::from_value
Turns out that `serde_wasm_bindgen` isn't happy with some input. Haven't
figured out why, yet.
2024-07-03 17:59:37 +01:00
Valere d8b2b74a2d feat(sdk-crypto): Add Identity based room key sharing strategy (#3607)
This sharing strategy is defined as part of MSC4153[1].

[1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4153
2024-07-03 16:18:10 +02:00
Ivan Enderlin 76caf7ed05 feat(ui): Trigger room list update only when necessary.
This patch revisits the need to trigger a room list update for
all changes of `RoomInfo`. For the moment, it reduces the scope to
`recency_timestamp` update.

This patch comes with a test to ensure things work as expected.
2024-07-03 16:07:27 +02:00
Benjamin Bouvier cdc3743888 timeline: when aborting fails on a local echo, retry on the matching remote echo 2024-07-03 16:03:13 +02:00
Benjamin Bouvier 9b97a2ed26 timeline: when an edit fails on a stale local echo, retry on the matching remote echo 2024-07-03 16:03:13 +02:00
Benjamin Bouvier aff07c13fc timeline: add integration test showing the issue with editing or aborting with a stale local echo 2024-07-03 16:03:13 +02:00
Benjamin Bouvier 9260942c5d integration testing: rename reactions.rs to timeline.rs 2024-07-03 16:03:13 +02:00
Richard van der Hoff c4413c6ac3 Merge pull request #3645 from matrix-org/rav/indexeddb_storage_efficiency
Indexeddb: more efficient serialization format
2024-07-03 14:37:05 +01:00
Richard van der Hoff 4d12a78341 indexeddb: remove redundant ? operators
Now that `deserialize_value` returns an `IndexeddbCryptoStoreError`, we don't
need these any more.
2024-07-03 14:10:57 +01:00
Richard van der Hoff a6dce1c0d7 Merge remote-tracking branch 'origin/main' into rav/indexeddb_storage_efficiency 2024-07-03 14:10:56 +01:00
Richard van der Hoff 09d53a52ad indexeddb: changelog 2024-07-03 14:10:35 +01:00
Richard van der Hoff 6d46e35d50 indexeddb: Make serialize_value a wrapper for maybe_encrypt_value
... and make `deserialize_value` handle both the old and new formats.

`maybe_encrypt_value` uses a much more efficient representation, so let's
migrate to that.
2024-07-03 14:10:29 +01:00
Richard van der Hoff 87653da2e3 indexeddb: Inline a call to IndexeddbSerializer::serialize_value
I'm going to change the behaviour of `serialize_value`, and we want to preserve
the behaviour of this test.
2024-07-03 14:10:29 +01:00
Richard van der Hoff a38eaf08be indexeddb: Add some tests for IndexeddbSerializer 2024-07-03 14:10:16 +01:00
Richard van der Hoff 786015f18c indexeddb: Make maybe_en/decrypt_value generic 2024-07-03 14:10:10 +01:00
Richard van der Hoff d60ec55e30 Crypto store: clear db before integ tests (#3644)
It's currently possible for integ test results to leak from one test run to the
next (for example, the indexeddb stores hang around in the browser), causing
bad test results.

Extend the test setup routine to clear out the store before the test starts.
2024-07-03 13:39:54 +01:00
Ivan Enderlin 99e284d8b0 Merge pull request #3585 from Hywan/feat-roomlist-sorting-2
feat(ui): Client-side sorting in `RoomList`
2024-07-03 13:07:00 +02:00
Ivan Enderlin 3588b88303 chore(base): Remove LatestEvent::cached_event_origin_ts.
This patch removes the `LatestEvent::cached_event_origin_ts`.
It's no longer necessary to cache this value as the
`matrix_sdk_ui::room_list_service::sorter::recency` sorter no longer
uses it.
2024-07-03 12:23:11 +02:00
Ivan Enderlin b4bbb10ba5 feat(ui): The recency sorter now uses recency_timestamp.
This patch changes the `recency` sorter to use `Room::recency_timestamp`
instead of `LatestEvent::event_origin_server_ts` to sort rooms.
2024-07-03 12:23:11 +02:00
Ivan Enderlin 9a02d6877f feat(base): Store the timestamp from SS in RoomInfo::recency_timestamp.
This patch adds a new field in `RoomInfo`: `recency_timestamp:
Option<MilliSecondsSinceUnixEpoch>>`. Its value comes from a Sliding
Sync Room response, that's why all this API is behind `cfg(feature =
"experimental-sliding-sync")`.
2024-07-03 12:06:08 +02:00
Ivan Enderlin 765b95468a !fixup Remove useless comment. 2024-07-03 09:54:13 +02:00
Ivan Enderlin 813ce6a14d test(ui): Assert the roominfo updates. 2024-07-03 09:54:12 +02:00
Ivan Enderlin 5d68f89372 chore(ui): Remove the RoomListService::rooms cache.
This patch removes the `RoomListService::rooms` cache, since now a
`Room` is pretty cheap to build.

This cache was also used to keep the `Timeline` alive, but it's now
recommended that the consumer of the `Room` keeps its own clone of the
`Timeline` somewhere. We may introduce a cache inside `RoomListService`
for the `Timeline` later.
2024-07-03 09:54:12 +02:00
Ivan Enderlin b525002828 fix(ui): merge_stream_and_receiver gives priority to raw_stream.
This patch rewrites `merge_stream_and_receiver` to switch the order
of `roominfo_update_recv` and `raw_stream`. The idea is to give the
priority to `raw_stream` since it will necessarily trigger the room
items recomputation.

This patch also remove the `for` loop with `Iterator::enumerate`, to
simply use `Iterator::position`: it's more compact and it removes a
`break` (it makes the code simpler to understand).

Finally, this patch renames `merged_stream` into `merged_streams`.
2024-07-03 09:54:12 +02:00
Ivan Enderlin ab190ad29c test: Disable Complement.
Complement uses the FFI `RoomList` API. Since the patch set modifies
this API, Complement is broken. We disable it and will re-enable it once
we have updated Complement.
2024-07-03 09:20:25 +02:00
Ivan Enderlin 76477281c2 chore(labs): multiverse uses RoomList::entries_with_dynamic_controllers. 2024-07-03 09:20:25 +02:00
Ivan Enderlin 2c25103226 chore(labs): Update multiverse to the latest RoomList version. 2024-07-03 09:20:24 +02:00
Ivan Enderlin 606a1510cf feat(ffi) Update RoomList API to the recent changes.
This patch adapts the `RoomList` FFI API to the recent changes to
suport a `Stream<Item = RoomListItem>` instead of a `Stream<Item =
RoomListEntry>`. Behind the scene, it supports client side sorting for
the rooms but this is transparent for this API.

This patch also removes the `RoomListInput` enum as no input is
supporter anymore.

The `entries` method no long returns a `RoomListEntriesResult` but
directly a `TaskHandle`. The given listener will receive the initial
entries as a `VectorDiff::Append`, which first is simpler but also fixe
a potential race condition bug.
2024-07-03 09:20:07 +02:00
Ivan Enderlin ed086afe83 test(ui): Update tests of the RoomList with sorters.
This patch mostly tests that sorting the rooms in the room list by
recency and by name works as expected.
2024-07-03 09:20:07 +02:00
Ivan Enderlin 51ca5a7113 feat(ui) Rename RoomList' sorter or to lexicographic.
This patch renames the `or` sorter to `lexicographic` as it describes
better what it does.
2024-07-03 09:20:07 +02:00
Ivan Enderlin ff4af894e4 feat(ui): The RoomList uses sorters!
This patch “installs” the sorters API for the `RoomList`.
2024-07-03 09:19:27 +02:00
Ivan Enderlin ec80c6ff7b feat(ui): Add the recency, name and or sorters for the RoomList.
This patch adds 3 sorters for the `RoomList`: `recency`, `name` and
`or`.
2024-07-03 09:18:23 +02:00
Ivan Enderlin daf878fa7f feat(base): Add LatestEvent::cached_event_origin_server_ts.
This patch adds a new `cached_event_origin_server_ts` field on
`LatestEvent`, which is a copy of the `origin_server_ts` of the inner
`SyncTimelineEvent`.
2024-07-03 09:17:49 +02:00
Ivan Enderlin 7aa7d1ca53 feat(ui): Remove visible_rooms from RoomListService.
This patch removes the `visible_rooms` sliding sync list from
`RoomListService`. As we are taking the path of doing client-side
sorting, the ordering of the server-side will most likely always
mismatch the ordering of the client-side, thus using `visible_rooms`
with room indices make no sense (indices from server-side won't map
indices on the client-side, so room ranges from client-side won't map
what the server knows).

We used to use `visible_rooms` to “preload” the timeline of rooms in
the user app viewport, with a `timeline_limit` of 20. This should be
replaced by room subscriptions starting from now. For the moment, the
user of `RoomListService` is responsible to do that manually. Maybe
`RoomListService` will handle that automatically in the future.
2024-07-03 09:17:28 +02:00
Ivan Enderlin 1270cdad1a feat(ui): RoomList::entries* manipulates a Room.
This patch is quite big… `RoomList::entries*` now returns `Room`s
instead of `RoomListEntry`s. This patch consequently updates all the
filters to manipulate `Room` instead of `RoomListEntry`. No more
`Client` is needed in the filters.

This patch also disables the `RoomList` integration test suite in order
to keep this patch “small”.
2024-06-30 21:19:27 +02:00
Ivan Enderlin 73b481a8fc chore(cargo): Update eyeball-im and eyeball-im-util.
The idea is to get the `SortBy` stream adapter.
2024-06-30 21:19:25 +02:00
Damir Jelić f25916cb5c chore: Remove an unused import 2024-05-13 12:48:30 +02:00
Damir Jelić 637e830e85 chore: Fix the formatting 2024-05-13 12:48:30 +02:00
Damir Jelić 04362cdc36 chore(crypto): Bump the version of the crypto crate to 0.7.1 2024-05-13 12:31:26 +02:00
Valere fa10bbb5dd fix(crypto): Avoid incorrect usage of private backup key
This fixes instances of key backup corruption and prevents inadvertently
logging the private backup key to the logs.
2024-05-13 12:31:26 +02:00
Benjamin Bouvier 4164effbf9 Bump matrix-sdk to 0.7.1 2024-01-22 11:27:03 +01:00
Sami J. Mäkinen 917e8c291e Upgrade aquamarine dependency 2024-01-22 11:25:56 +01:00
476 changed files with 62068 additions and 24976 deletions
-47
View File
@@ -1,13 +1,3 @@
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
# when a `--target` is passed to Cargo. Historically this was not the case, and
# because of that, cross-compilation would not set the rustflags configured
# below in `target.'cfg(...)'` for them, resulting in cache invalidation.
#
# Since this is an unstable feature (enabled at the bottom of the file), this
# setting is unfortunately ignored on stable toolchains, but it's still better
# to have it apply on nightly than using the old behavior for all toolchains.
target-applies-to-host = false
[alias]
xtask = "run --package xtask --"
uniffi-bindgen = "run --package uniffi-bindgen --"
@@ -15,42 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
# Exclude tarpaulin, android and ios from extra lints since on stable, without
# the nightly-only target-applies-to-host setting at the top, cross compilation
# and otherwise changing cfg's can be very bad for caching. These should never
# be the default either and don't have much target-specific code that would
# benefit from the extra lints.
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
rustflags = [
"-Wrust_2018_idioms",
"-Wsemicolon_in_expressions_from_macros",
"-Wunused_extern_crates",
"-Wunused_import_braces",
"-Wunused_qualifications",
"-Wtrivial_casts",
"-Wtrivial_numeric_casts",
"-Wclippy::cloned_instead_of_copied",
"-Wclippy::dbg_macro",
"-Wclippy::inefficient_to_string",
"-Wclippy::macro_use_imports",
"-Wclippy::mut_mut",
"-Wclippy::needless_borrow",
"-Wclippy::nonstandard_macro_braces",
"-Wclippy::str_to_string",
"-Wclippy::todo",
"-Wclippy::unused_async",
]
[target.'cfg(target_arch = "wasm32")']
rustflags = [
# We have some types that are !Send and/or !Sync only on wasm, it would be
# slightly more efficient, but also pretty annoying, to wrap them in Rc
# where we would use Arc on other platforms.
"-Aclippy::arc_with_non_send_sync",
]
# activate the target-applies-to-host feature.
# Required for `target-applies-to-host` at the top to take effect.
[unstable]
rustdoc-map = true
target-applies-to-host = true
+61
View File
@@ -0,0 +1,61 @@
# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
[graph]
all-features = true
exclude = [
# dev only dependency
"criterion"
]
[advisories]
version = 2
ignore = [
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
]
[licenses]
version = 2
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"ISC",
"MIT",
"MPL-2.0",
"Zlib",
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
{ allow = ["CDDL-1.0"], crate = "inferno" },
{ allow = ["LicenseRef-ring"], crate = "ring" },
]
[[licenses.clarify]]
name = "ring"
expression = "LicenseRef-ring"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[bans]
# We should disallow this, but it's currently a PITA.
multiple-versions = "allow"
wildcards = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
# A patch override for the bindings fixing a bug for Android before upstream
# releases a new version.
"https://github.com/element-hq/tracing.git",
# Sam as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
"https://github.com/jplatte/async-compat",
]
+1
View File
@@ -1 +1,2 @@
* @matrix-org/rust
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
+7
View File
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
-13
View File
@@ -1,13 +0,0 @@
name: Security audit
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
name: Run Benchmarks
runs-on: ubuntu-latest
environment: matrix-rust-bot
if: github.event_name == 'push' || !github.event.pull_request.draft
if: github.event_name == 'push'
steps:
- name: Checkout the repo
+71 -4
View File
@@ -61,10 +61,77 @@ jobs:
- name: Build library & generate bindings
run: target/debug/xtask ci bindings
test-android:
name: matrix-rust-components-kotlin
needs: xtask
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout Rust SDK
uses: actions/checkout@v4
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
ref: main
- name: Use JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Install android sdk
uses: malinskiy/action-android/install-sdk@release/0.1.4
- name: Install android ndk
uses: nttld/setup-ndk@v1
id: install-ndk
with:
ndk-version: r27
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
- name: Delete cargo config
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- 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: Install Rust dependencies
run: |
rustup target add x86_64-linux-android
cargo install cargo-ndk
- name: Build SDK bindings for Android
# Building for x86_64-linux-android as it's the most prone to breaking and building for every arch is too much
run: |
echo "Building SDK for x86_64-linux-android and creating bindings"
target/debug/xtask kotlin build-android-library --package full-sdk --only-target x86_64-linux-android --src-dir rust-components-kotlin/sdk/sdk-android/src/main
echo "Copying the result binary to the Android project"
cd rust-components-kotlin
echo "Building the Kotlin bindings"
./gradlew :sdk:sdk-android:assembleDebug
test-apple:
name: matrix-rust-components-swift
needs: xtask
runs-on: macos-12
runs-on: macos-14
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
@@ -108,7 +175,7 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev
complement-crypto:
name: "Run Complement Crypto tests"
@@ -119,12 +186,12 @@ jobs:
test-crypto-apple-framework-generation:
name: Generate Crypto FFI Apple XCFramework
runs-on: macos-12
runs-on: macos-14
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
+24 -24
View File
@@ -26,7 +26,6 @@ jobs:
test-matrix-sdk-features:
name: 🐧 [m], ${{ matrix.name }}
needs: xtask
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
strategy:
@@ -41,7 +40,6 @@ jobs:
- markdown
- socks
- sso-login
- image-proc
steps:
- name: Checkout
@@ -50,6 +48,11 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
@@ -79,7 +82,6 @@ jobs:
name: 🐧 [m]-examples
needs: xtask
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
@@ -111,12 +113,16 @@ jobs:
name: 🐧 [m]-crypto
needs: xtask
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -141,7 +147,6 @@ jobs:
test-all-crates:
name: ${{ matrix.name }}
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ${{ matrix.os }}
strategy:
@@ -169,6 +174,12 @@ jobs:
with:
tool: protoc@3.20.3
- name: Install libsqlite
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
@@ -194,7 +205,6 @@ jobs:
test-wasm:
name: 🕸️ ${{ matrix.name }}
needs: xtask
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
@@ -270,7 +280,6 @@ jobs:
formatting:
name: Check Formatting
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
@@ -289,20 +298,18 @@ jobs:
typos:
name: Spell Check with Typos
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.20.10
uses: crate-ci/typos@v1.27.3
clippy:
name: Run clippy
needs: xtask
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout the repo
@@ -337,14 +344,13 @@ jobs:
integration-tests:
name: Integration test
if: github.event_name == 'push' || !github.event.pull_request.draft
runs-on: ubuntu-latest
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
services:
# sliding sync needs a postgres container
# synapse needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -362,21 +368,10 @@ jobs:
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# run sliding sync and point it at the postgres container and synapse container.
# the postgres container needs to be above this to make sure it has started prior to this service.
slidingsync:
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./coverage.yml
env:
SYNCV3_SERVER: "http://synapse:8008"
SYNCV3_SECRET: "SUPER_CI_SECRET"
SYNCV3_BINDADDR: ":8118"
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
ports:
- 8118:8118
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -387,6 +382,11 @@ jobs:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
+7 -15
View File
@@ -25,12 +25,10 @@ jobs:
code_coverage:
name: Code Coverage
runs-on: "ubuntu-latest"
if: github.event_name == 'push' || !github.event.pull_request.draft
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
services:
# sliding sync needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -48,21 +46,10 @@ jobs:
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# run sliding sync and point it at the postgres container and synapse container.
# the postgres container needs to be above this to make sure it has started prior to this service.
slidingsync:
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./ci.yml
env:
SYNCV3_SERVER: "http://synapse:8008"
SYNCV3_SECRET: "SUPER_CI_SECRET"
SYNCV3_BINDADDR: ":8118"
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
ports:
- 8118:8118
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -75,6 +62,11 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -102,7 +94,7 @@ jobs:
run: |
rustup run stable cargo tarpaulin \
--skip-clean --profile cov --out xml \
--features experimental-widgets,testing,image-proc
--features experimental-widgets,testing
env:
CARGO_PROFILE_COV_INHERITS: 'dev'
CARGO_PROFILE_COV_DEBUG: 1
+14
View File
@@ -0,0 +1,14 @@
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
on:
pull_request:
paths:
- '**/Cargo.toml'
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -0,0 +1,12 @@
name: Detects unused dependencies
on:
pull_request: { branches: "*" }
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Machete
uses: bnjbvr/cargo-machete@main
-3
View File
@@ -23,7 +23,6 @@ jobs:
docs:
name: All crates
runs-on: ubuntu-latest
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout repository
@@ -52,8 +51,6 @@ jobs:
# Keep in sync with xtask docs
- name: Build documentation
env:
# Work around https://github.com/rust-lang/cargo/issues/10744
CARGO_TARGET_APPLIES_TO_HOST: "true"
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
run:
cargo doc --no-deps --workspace --features docsrs
+1 -1
View File
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.0.0
- uses: actions/checkout@v4
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+3 -3
View File
@@ -1,9 +1,9 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Codecov
name: Upload code coverage
on:
# This workflow is triggered after every successfull execution
# This workflow is triggered after every successful execution
# of `coverage` workflow.
workflow_run:
workflows: ["Code Coverage"]
@@ -64,7 +64,7 @@ jobs:
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
os-name: 🐧
cachekey-id: linux
- os: macos-12
- os: macos-14
os-name: 🍏
cachekey-id: macos
@@ -57,7 +57,7 @@ jobs:
id: xtask-cache
with:
path: target/debug/xtask
# use the cache key calculated in the step above. Bit of an awkard
# use the cache key calculated in the step above. Bit of an awkward
# syntax
key: |
${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }}
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+1
View File
@@ -28,6 +28,7 @@ Abl = "Abl"
Som = "Som"
Ba = "Ba"
Yur = "Yur" # as found in crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs
TYE = "TYE" # as found in testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
[files]
extend-exclude = [
+119
View File
@@ -0,0 +1,119 @@
# Architecture
The SDK is split into multiple layers:
```
WASM (external crate matrix-rust-sdk-crypto-wasm)
/
/ uniffi
/ /
/ bindings (matrix-sdk-ffi)
crypto |
bindings |
| |
| UI (matrix-sdk-ui)
| \
| \
| main (matrix-sdk)
| / /
crypto /
\ /
store (matrix-sdk-base, + all the store impls)
|
common (matrix-sdk-common)
```
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
`MemoryStore` which is defined in `matrix-sdk-base`.
## `crates/matrix-sdk`
This is the main crate, and one that is expected to be used by most consumers. Notable data types
include:
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
sync, etc.
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
allows running queries that are room-specific, notably sending events.
## `crates/matrix-sdk-base`
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
storage backends, as well as dummy in-memory implementations of these traits.
## `crates/matrix-sdk-common`
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
crates (the only crate it's using is test helpers).
## `crates/matrix-sdk-crypto`
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
## `crates/matrix-sdk-indexeddb`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
indexeddb backend (for use in Web browsers, via WebAssembly).
## `crates/matrix-sdk-qrcode`
Implementation of QR codes for interactive verifications, used in the crypto crate.
## `crates/matrix-sdk-sqlite`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
SQLite backend.
## `crates/matrix-sdk-store-encryption`
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
implement encryption at rest can use those primitives.
## `crates/matrix-sdk-ui`
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
everything related to crypto and E2EE for the current `Client`.
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
retrieving the list of current rooms, and exposing its entries.
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
down.
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
(aggregations) into single timeline items.
## `bindings/matrix-sdk-crypto-ffi/`
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
Element apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi/`
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi-macros/`
Macros used in `bindings/matrix-sdk-ffi`.
## `testing/matrix-sdk-test/`
Common test helpers, used by all the other crates.
## `testing/matrix-sdk-test-macros/`
Implementation of the `#[async_test]` test macro.
## `testing/matrix-sdk-integration-testing/`
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
setup is provided to ease running the tests, and it is compatible for running with Podman too.
# Inspiration
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
+175 -37
View File
@@ -29,54 +29,187 @@ integration tests that need a running synapse instance. These tests reside in
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
synapse for testing purposes.
## Commit messages and PR title guidelines
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit
should have a *good commit message*.
## Pull requests
An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes
tests. This is useful to make the review process easier (help your reviewer), but also when running
bisections, helping identifying which commit introduced a regression.
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
each commit should have a *good commit message*.
A *good commit message* should be composed of:
A *proper PR title* would be a one-liner summary of the changes in the PR,
following the same guidelines of a good commit message, including the
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
be a good PR title.
- a prefix to indicate which area/feature is related by the commit
- a short description that would give sufficient context for a reviewer to guess what the commit is
about.
(An additional bad example of a bad PR title would be `mynickname/branch name`,
that is, just the branch name.)
Examples of commit messages that aren't so useful:
# Writing changelog entries
- “add new method“
- “enhance performance“
- “fix receipts“
We aim to maintain clear and informative changelogs that accurately reflect the
changes in our project. This guide will help you write useful changelog entries
using git-cliff, which fetches changelog entries from commit messages.
Examples of good commit messages:
## Commit message format
- “ffi: Add new method `frobnicate_the_foos`
- “indexeddb: Break up the request inside `get_inbound_group_sessions`
- “read_receipts: Store receipts locally, fixing #12345
Commit messages should be formatted as Conventional Commits. In addition, some
git trailers are supported and have special meaning (see below).
A *proper PR title* would be a one-liner summary of the changes in the PR, following the
same guidelines of a good commit message, including the area/feature prefix. Something like
`FFI: Allow logs files to be pruned.` would be a good PR title.
### Conventional commits
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
branch name.)
Conventional Commits are structured as follows:
Having good commit messages and PR titles also helps with reviews, scanning the `git log` of
the project, and writing the [*This week in
Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK.
```
<type>(<scope>): <short summary>
```
The type of changes which will be included in changelogs is one of the following:
* `feat`: A new feature
* `fix`: A bug fix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
* `ci`: Changes to CI configuration files and scripts
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
### Changelog trailer
In addition to the Conventional Commit format, you can use the `Changelog` git
trailer to specify the changelog message explicitly. When that trailer is
present, its value will be used as the changelog entry instead of the commit's
leading line. The `Breaking-Change` git trailer can be used in a similar manner
if the changelog entry should be marked as a breaking change.
#### Example commit message
```
feat: Add a method to encode Ed25519 public keys to Base64
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
stringify Ed25519 and thus present them to users. It's also commonly used when
Ed25519 keys need to be inserted into JSON.
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
stringify the Ed25519 public key.
```
In this commit message, the content specified in the `Changelog` trailer will be
used for the changelog entry.
Be careful to add at least one whitespace after new lines to create a paragraph.
### Security fixes
Commits addressing security vulnerabilities must include specific trailers for
vulnerability metadata. These commits are required to include at least the
`Security-Impact` trailer to indicate that the commit is a security fix.
Security issues have some additional git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
Example:
```
fix(crypto): Use a constant-time Base64 encoder for secret key material
This patch fixes a security issue around a side-channel vulnerability[1]
when decoding secret key material using Base64.
In some circumstances an attacker can obtain information about secret
secret key material via a controlled-channel and side-channel attack.
This patch avoids the side-channel by switching to the base64ct crate
for the encoding, and more importantly, the decoding of secret key
material.
Security-Impact: Low
CVE: CVE-2024-40640
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
Changelog: Use a constant-time Base64 encoder for secret key material
to mitigate side-channel attacks leaking secret key material.
```
## Review process
To streamline the review process and make it easier for maintainers to review
your contributions, follow these basic rules:
1. Do not force push after a review has started. This helps maintainers track
incremental changes without confusion and makes it easier to follow the
evolution of the code.
2. Do not mix moves and refactoring with functional changes. Keep these in
separate commits for clarity. This ensures that the purpose of each commit is
clear and easy to review.
3. Each commit must compile. If commits dont compile, git bisect becomes
unusable, which hampers the debugging process and makes it harder to identify
the source of issues.
4. Commits should only introduce test failures if they are proving that a bug
exists. New features should never introduce test failures. Test failures
should only be used to demonstrate existing bugs, not as part of adding new
functionality.
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
delays. Create small, focused commits that address a single topic. Use a
combination of [git add] -p or git checkout -p to split changes into logical
units. This makes your work easier to review and reduces the chance of
introducing unrelated changes.
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
### Addressing review comments using fixup commits
So you posted a PR and the maintainers aren't quite happy with it. Here are some
guidelines to make the maintainers life easier and increase the chances that
your PR will be reviewed swiftly.
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
commits. These commits mark your changes as corrections of specific previous
commits in the PR.
Example:
```bash
git commit --fixup=<commit-hash>
```
This command creates a new commit that refers to an existing one, making it
easier to rebase and squash later while showing reviewers the history of fixes.
For extra points, link to the fixup commit in the thread where the change was
requested.
2. After all requested changes were addressed, feel free to re-request a review.
People might not notice that all changes were addressed.
3. Once the PR has been approved, rebase your PR to squash all the fixup
commits, the [autosquash] option can help with this.
```bash
git rebase main --interactive --autosquash
```
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
[autosquash]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash
## Sign off
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
adopted the same lightweight approach that the [Linux Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
This is a simple declaration that you wrote the contribution or otherwise have the right
to contribute it to Matrix:
```
Developer Certificate of Origin
@@ -123,11 +256,6 @@ include the line in your commit or pull request comment:
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
@@ -138,3 +266,13 @@ on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/main
```
## Tips for working on the `matrix-rust-sdk` with specific IDEs
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
with all features enabled, causing an error in `matrix-sdk` ("only one of the
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
`native-tls` or `rustls-tls` feature definitions:
![Screenshot of RustRover](.img/rustrover-disable-feature.png)
Generated
+738 -1276
View File
File diff suppressed because it is too large Load Diff
+53 -23
View File
@@ -10,6 +10,9 @@ members = [
"uniffi-bindgen",
"xtask",
]
exclude = [
"testing/data",
]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
@@ -28,23 +31,21 @@ async-trait = "0.1.60"
as_variant = "1.2.0"
base64 = "0.22.0"
byteorder = "1.4.3"
eyeball = { version = "0.8.7", features = ["tracing"] }
eyeball-im = { version = "0.4.1", features = ["tracing"] }
eyeball-im-util = "0.5.1"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.5.1", features = ["tracing"] }
eyeball-im-util = "0.7.0"
futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = [
"alloc",
] }
futures-util = "0.3.26"
growable-bloom-filter = "2.1.0"
http = "1.1.0"
imbl = "2.0.0"
imbl = "3.0.0"
itertools = "0.12.0"
once_cell = "1.16.0"
pin-project-lite = "0.2.9"
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e51c5fafba9aa5f0", features = [
ruma = { version = "0.11.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -53,9 +54,12 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e5
"compat-encrypted-stickers",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc4075"
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e51c5fafba9aa5f0" }
ruma-common = "0.14.1"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
@@ -63,27 +67,29 @@ sha2 = "0.10.8"
similar-asserts = "1.5.0"
stream_assert = "0.1.1"
thiserror = "1.0.38"
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
tokio = { version = "1.39.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
uniffi = { version = "0.27.1" }
uniffi_bindgen = { version = "0.27.1" }
tracing-subscriber = "0.3.18"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.0"
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "4ef989c6a8eba0bc809e285a081c56320a9bbf1e" }
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
wiremock = "0.6.0"
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
@@ -115,10 +121,34 @@ opt-level = 3
[patch.crates-io]
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
[workspace.lints.rust]
rust_2018_idioms = "warn"
semicolon_in_expressions_from_macros = "warn"
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
[workspace.lints.clippy]
assigning_clones = "allow"
box_default = "allow"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
str_to_string = "warn"
todo = "warn"
unused_async = "warn"
redundant_clone = "warn"
-21
View File
@@ -1,21 +0,0 @@
# Releasing `matrix-rust-sdk`
- Make sure to bump all the crates to *the same version number*, and commit that (along with the
changes to the `Cargo.lock` file).
- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`.
- Push the tag: `git push origin 0.7.0`
- Publish all the crates, in topological order of the dependency tree:
```
cargo publish -p matrix-sdk-test-macros
cargo publish -p matrix-sdk-test
cargo publish -p matrix-sdk-common
cargo publish -p matrix-sdk-qrcode
cargo publish -p matrix-sdk-store-encryption
cargo publish -p matrix-sdk-crypto
cargo publish -p matrix-sdk-base
cargo publish -p matrix-sdk-sqlite
cargo publish -p matrix-sdk-indexeddb
cargo publish -p matrix-sdk
cargo publish -p matrix-sdk-ui
```
+47
View File
@@ -0,0 +1,47 @@
# Releasing and publishing the SDK
While the release process can be handled manually, `cargo-release` has been
configured to make it more convenient.
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
that no pull request is required to cut a release. However, since the SDK
repo is set up so that each push requires a pull request, we need to slightly
deviate from the default workflow. A `cargo-xtask` has been created to make the
process as smooth as possible.
The procedure is as follows:
1. Switch to a release branch:
```bash
git switch -c release-x.y.z
  ```
2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md`
file using `git cliff`, and bump the version in the `Cargo.toml` file.
```bash
cargo xtask release prepare --execute minor|patch|rc
```
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
satisfied, push the branch and open a PR.
```bash
git push --set-upstream origin/release-x.y.z
```
4. Pass the review and merge the branch as you would with any other branch.
5. Create tags for your new release, publish the release on crates.io and push
the tags:
```bash
# Switch to main first.
git switch main
# Pull in the now-merged release commit(s).
git pull
# Create tags, publish the release on crates.io, and push the tags.
cargo xtask release publish --execute
```
For more information on cargo-release: https://github.com/crate-ci/cargo-release
+6 -1
View File
@@ -13,12 +13,14 @@ matrix-sdk-base = { workspace = true }
matrix-sdk-crypto = { workspace = true }
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test = { workspace = true }
matrix-sdk-ui = { workspace = true }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite"] }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = "3.3.0"
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
wiremock = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
@@ -34,3 +36,6 @@ harness = false
[[bench]]
name = "room_bench"
harness = false
[package.metadata.release]
release = false
+7 -16
View File
@@ -3,14 +3,11 @@ use std::{ops::Deref, sync::Arc};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
use matrix_sdk_sqlite::SqliteCryptoStore;
use matrix_sdk_test::response_from_file;
use matrix_sdk_test::ruma_response_from_json;
use ruma::{
api::{
client::{
keys::{claim_keys, get_keys},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,
api::client::{
keys::{claim_keys, get_keys},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
};
@@ -28,25 +25,19 @@ fn alice_device_id() -> &'static DeviceId {
fn keys_query_response() -> get_keys::v3::Response {
let data = include_bytes!("crypto_bench/keys_query.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
ruma_response_from_json(&data)
}
fn keys_claim_response() -> claim_keys::v3::Response {
let data = include_bytes!("crypto_bench/keys_claim.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
claim_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
ruma_response_from_json(&data)
}
fn huge_keys_query_response() -> get_keys::v3::Response {
let data = include_bytes!("crypto_bench/keys_query_2000_members.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::v3::Response::try_from_http_response(data)
.expect("Can't parse the `/keys/query` response")
ruma_response_from_json(&data)
}
pub fn keys_query(c: &mut Criterion) {
+150 -6
View File
@@ -1,20 +1,32 @@
use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::utils::IntoRawStateEventContent;
use matrix_sdk::{
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
utils::IntoRawStateEventContent,
};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::EventBuilder;
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
api::client::membership::get_member_events,
device_id,
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
owned_room_id,
owned_room_id, owned_user_id,
serde::Raw,
user_id, OwnedUserId,
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
};
use serde::Serialize;
use serde_json::json;
use tokio::runtime::Builder;
use wiremock::{
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
Mock, MockServer, Request, ResponseTemplate,
};
pub fn receive_all_members_benchmark(c: &mut Criterion) {
const MEMBERS_IN_ROOM: usize = 100000;
@@ -62,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
.block_on(sqlite_store.save_changes(&changes))
.expect("initial filling of sqlite failed");
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
let base_client = BaseClient::with_store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
@@ -99,6 +114,135 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
group.finish();
}
pub fn load_pinned_events_benchmark(c: &mut Criterion) {
const PINNED_EVENTS_COUNT: usize = 100;
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let sender_id = owned_user_id!("@sender:example.com");
let f = EventFactory::new().room(&room_id).sender(&sender_id);
let (client, server) = runtime.block_on(logged_in_client_with_server());
let mut sync_response_builder = SyncResponseBuilder::new();
let mut joined_room_builder =
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
.collect();
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
{
"content": {
"pinned": pinned_event_ids
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.pinned_events",
"unsigned": {
"age": 703422
}
}
)));
let response_json =
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
runtime.block_on(mock_sync(&server, response_json, None));
let sync_settings = SyncSettings::default();
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
runtime.block_on(server.reset());
runtime.block_on(
Mock::given(method("GET"))
.and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*"))
.respond_with(move |r: &Request| {
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
let event_id_str = segments[6];
let event_id = EventId::parse(event_id_str).expect("Invalid event id in response");
let event = f
.text_msg(format!("Message {event_id_str}"))
.event_id(&event_id)
.server_ts(MilliSecondsSinceUnixEpoch::now())
.into_raw_sync();
ResponseTemplate::new(200)
.set_delay(Duration::from_millis(50))
.set_body_json(event.json())
})
.mount(&server),
);
let room = client.get_room(&room_id).expect("Room not found");
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
let count = PINNED_EVENTS_COUNT;
let name = format!("{count} pinned events");
let mut group = c.benchmark_group("Test");
group.throughput(Throughput::Elements(count as u64));
group.sample_size(10);
let client = Arc::new(client);
{
let client = client.clone();
runtime.spawn_blocking(move || {
client.event_cache().subscribe().unwrap();
});
}
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
b.to_async(&runtime).iter(|| async {
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
// Reset cache so it always loads the events from the mocked endpoint
client.event_cache().empty_immutable_cache().await;
let timeline = Timeline::builder(&room)
.with_focus(TimelineFocus::PinnedEvents {
max_events_to_load: 100,
max_concurrent_requests: 10,
})
.build()
.await
.expect("Could not create timeline");
let (items, _) = timeline.subscribe().await;
assert_eq!(items.len(), PINNED_EVENTS_COUNT + 1);
timeline.clear().await;
});
});
{
let _guard = runtime.enter();
runtime.block_on(server.reset());
drop(server);
}
group.finish();
}
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
let mut mock_builder = Mock::given(method("GET"))
.and(path("/_matrix/client/r0/sync"))
.and(header("authorization", "Bearer 1234"));
if let Some(since) = since {
mock_builder = mock_builder.and(query_param("since", since));
} else {
mock_builder = mock_builder.and(query_param_is_missing("since"));
}
mock_builder
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(server)
.await;
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
@@ -114,6 +258,6 @@ fn criterion() -> Criterion {
criterion_group! {
name = room;
config = criterion();
targets = receive_all_members_benchmark,
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
}
criterion_main!(room);
+8 -2
View File
@@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
@@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
+8 -4
View File
@@ -22,10 +22,11 @@ bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
[dependencies]
anyhow = { workspace = true }
futures-util = "0.3.28"
futures-util = { workspace = true }
hmac = "0.12.1"
http = { workspace = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
matrix-sdk-ffi-macros = { workspace = true }
pbkdf2 = "0.12.2"
rand = { workspace = true }
ruma = { workspace = true }
@@ -33,9 +34,9 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
uniffi = { workspace = true , features = ["cli"]}
uniffi = { workspace = true, features = ["cli"] }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
[lints]
workspace = true
[package.metadata.release]
release = false
+5 -2
View File
@@ -5,6 +5,9 @@ use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-ffi/build.rs] too!
fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
@@ -18,11 +21,11 @@ fn setup_x86_64_android_workaround() {
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
@@ -69,7 +69,7 @@ impl BackupRecoveryKey {
const PBKDF_ROUNDS: i32 = 500_000;
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl BackupRecoveryKey {
/// Create a new random [`BackupRecoveryKey`].
#[allow(clippy::new_without_default)]
@@ -53,7 +53,7 @@ impl Drop for DehydratedDevices {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl DehydratedDevices {
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
let inner = self.runtime.block_on(self.inner.create())?;
@@ -107,7 +107,7 @@ impl Drop for RehydratedDevice {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RehydratedDevice {
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
@@ -133,7 +133,7 @@ impl Drop for DehydratedDevice {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl DehydratedDevice {
pub fn keys_for_upload(
&self,
+86 -22
View File
@@ -33,7 +33,7 @@ pub use error::{
use js_int::UInt;
pub use logger::{set_logger, Logger};
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
use matrix_sdk_crypto::{
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
@@ -196,7 +196,7 @@ impl From<anyhow::Error> for MigrationError {
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate(
data: MigrationData,
path: String,
@@ -338,10 +338,6 @@ async fn save_changes(
processed_steps += 1;
listener(processed_steps, total_steps);
// The Sessions were created with incorrect device keys, so clear the cache
// so that they'll get recreated with correct ones.
store.clear_caches().await;
Ok(())
}
@@ -363,7 +359,7 @@ async fn save_changes(
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_sessions(
data: SessionMigrationData,
path: String,
@@ -434,7 +430,7 @@ fn collect_sessions(
// the session cache after migration) so we don't need to worry about
// signatures.
let device_keys = DeviceKeys::new(
user_id.clone(),
user_id,
device_id.clone(),
Default::default(),
BTreeMap::from([
@@ -536,7 +532,7 @@ fn collect_sessions(
/// * `passphrase` - The passphrase that should be used to encrypt the data at
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_room_settings(
room_settings: HashMap<String, RoomSettings>,
path: String,
@@ -562,7 +558,7 @@ pub fn migrate_room_settings(
}
/// Callback that will be passed over the FFI to report progress
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressListener {
/// The callback that should be called on the Rust side
///
@@ -672,6 +668,9 @@ pub struct EncryptionSettings {
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
pub only_allow_trusted_devices: bool,
/// Should fail to send when a verified user has unverified devices, or when
/// a previously verified user replaces their identity.
pub error_on_verified_user_problem: bool,
}
impl From<EncryptionSettings> for RustEncryptionSettings {
@@ -681,7 +680,10 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
rotation_period: Duration::from_secs(v.rotation_period),
rotation_period_msgs: v.rotation_period_msgs,
history_visibility: v.history_visibility.into(),
sharing_strategy: CollectStrategy::new_device_based(v.only_allow_trusted_devices),
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
only_allow_trusted_devices: v.only_allow_trusted_devices,
error_on_verified_user_problem: v.error_on_verified_user_problem,
},
}
}
}
@@ -726,19 +728,24 @@ pub enum ShieldColor {
#[allow(missing_docs)]
pub struct ShieldState {
color: ShieldColor,
code: Option<ShieldStateCode>,
message: Option<String>,
}
impl From<RustShieldState> for ShieldState {
fn from(value: RustShieldState) -> Self {
match value {
RustShieldState::Red { message } => {
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
}
RustShieldState::Grey { message } => {
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
}
RustShieldState::None => Self { color: ShieldColor::None, message: None },
RustShieldState::Red { code, message } => Self {
color: ShieldColor::Red,
code: Some(code),
message: Some(message.to_owned()),
},
RustShieldState::Grey { code, message } => Self {
color: ShieldColor::Grey,
code: Some(code),
message: Some(message.to_owned()),
},
RustShieldState::None => Self { color: ShieldColor::None, code: None, message: None },
}
}
}
@@ -787,7 +794,7 @@ pub struct BackupKeys {
backup_version: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl BackupKeys {
/// Get the recovery key that we're holding on to.
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
@@ -884,7 +891,7 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn version_info() -> VersionInfo {
VersionInfo {
version: matrix_sdk_crypto::VERSION.to_owned(),
@@ -908,16 +915,73 @@ pub struct VersionInfo {
pub git_description: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn version() -> String {
matrix_sdk_crypto::VERSION.to_owned()
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn vodozemac_version() -> String {
vodozemac::VERSION.to_owned()
}
/// The encryption component of PkEncryption support.
///
/// This struct can be created using a [`Curve25519PublicKey`] corresponding to
/// a `PkDecryption` object, allowing messages to be encrypted for the
/// associated decryption object.
#[derive(uniffi::Object)]
pub struct PkEncryption {
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
}
#[matrix_sdk_ffi_macros::export]
impl PkEncryption {
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
/// encoded as Base64.
///
/// The public key should come from an existing `PkDecryption` object.
/// Returns a `DecodeError` if the Curve25519 key could not be decoded
/// correctly.
#[uniffi::constructor]
pub fn from_base64(key: &str) -> Result<Arc<Self>, DecodeError> {
let key = vodozemac::Curve25519PublicKey::from_base64(key)
.map_err(matrix_sdk_crypto::backups::DecodeError::PublicKey)?;
let inner = vodozemac::pk_encryption::PkEncryption::from_key(key);
Ok(Self { inner }.into())
}
/// Encrypt a message using this [`PkEncryption`] object.
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
use vodozemac::base64_encode;
let message = self.inner.encrypt(plaintext.as_ref());
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
PkMessage {
ciphertext: base64_encode(ciphertext),
mac: base64_encode(mac),
ephemeral_key: ephemeral_key.to_base64(),
}
}
}
/// A message that was encrypted using a [`PkEncryption`] object.
#[derive(uniffi::Record)]
pub struct PkMessage {
/// The ciphertext of the message.
pub ciphertext: String,
/// The message authentication code of the message.
///
/// *Warning*: This does not authenticate the ciphertext.
pub mac: String,
/// The ephemeral Curve25519 key of the message which was used to derive the
/// individual message key.
pub ephemeral_key: String,
}
uniffi::setup_scaffolding!();
#[cfg(test)]
+2 -2
View File
@@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
/// Trait that can be used to forward Rust logs over FFI to a language specific
/// logger.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait Logger: Send {
/// Called every time the Rust side wants to post a log line.
fn log(&self, log_line: String);
@@ -42,7 +42,7 @@ pub struct LoggerWrapper {
}
/// Set the logger that should be used to forward Rust logs over FFI.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn set_logger(logger: Box<dyn Logger>) {
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
+22 -29
View File
@@ -17,7 +17,8 @@ use matrix_sdk_crypto::{
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
UserIdentity as SdkUserIdentity,
};
use ruma::{
api::{
@@ -37,11 +38,12 @@ use ruma::{
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
AnySyncMessageLikeEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
@@ -177,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -285,10 +287,7 @@ impl OlmMachine {
if let Some(identity) =
self.runtime.block_on(self.inner.get_identity(&user_id, None))?
{
match identity {
UserIdentities::Own(i) => i.is_verified(),
UserIdentities::Other(i) => i.is_verified(),
}
identity.is_verified()
} else {
false
},
@@ -316,8 +315,8 @@ impl OlmMachine {
if let Some(user_identity) = user_identity {
Ok(match user_identity {
UserIdentities::Own(i) => self.runtime.block_on(i.verify())?,
UserIdentities::Other(i) => self.runtime.block_on(i.verify())?,
SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?,
SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?,
}
.into())
} else {
@@ -530,11 +529,11 @@ impl OlmMachine {
) -> Result<SyncChangesResult, CryptoStoreError> {
let to_device: ToDevice = serde_json::from_str(&events)?;
let device_changes: RumaDeviceLists = device_changes.into();
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
.into_iter()
.map(|(k, v)| {
(
DeviceKeyAlgorithm::from(k),
OneTimeKeyAlgorithm::from(k),
v.clamp(0, i32::MAX)
.try_into()
.expect("Couldn't convert key counts into an UInt"),
@@ -542,8 +541,8 @@ impl OlmMachine {
})
.collect();
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
let (to_device_events, room_key_infos) = self.runtime.block_on(
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
@@ -863,12 +862,14 @@ impl OlmMachine {
/// * `strict_shields` - If `true`, messages will be decorated with strict
/// warnings (use `false` to match legacy behaviour where unsafe keys have
/// lower severity warnings and unverified identities are not decorated).
/// * `decryption_settings` - The setting for decrypting messages.
pub fn decrypt_room_event(
&self,
event: String,
room_id: String,
handle_verification_events: bool,
strict_shields: bool,
decryption_settings: DecryptionSettings,
) -> Result<DecryptedEvent, DecryptionError> {
// Element Android wants only the content and the type and will create a
// decrypted event with those two itself, this struct makes sure we
@@ -884,10 +885,14 @@ impl OlmMachine {
let event: Raw<_> = serde_json::from_str(&event)?;
let room_id = RoomId::parse(room_id)?;
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(
&event,
&room_id,
&decryption_settings,
))?;
if handle_verification_events {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
if let Ok(e) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
@@ -905,8 +910,7 @@ impl OlmMachine {
}
}
let encryption_info =
decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info");
let encryption_info = decrypted.encryption_info;
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
@@ -1528,17 +1532,6 @@ impl OlmMachine {
}
.into()
}
/// Clear any in-memory caches because they may be out of sync with the
/// underlying data store.
///
/// The crypto store layer is caching olm sessions for a given device.
/// When used in a multi-process context this cache will get outdated.
/// If the machine is used by another process, the cache must be
/// invalidating when the main process is resumed.
pub async fn clear_crypto_cache(&self) {
self.inner.clear_crypto_cache().await
}
}
impl OlmMachine {
+11 -5
View File
@@ -1,8 +1,8 @@
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
use crate::CryptoStoreError;
/// Enum representing cross signing identities of our own user or some other
/// Enum representing cross signing identity of our own user or some other
/// user.
#[derive(uniffi::Enum)]
pub enum UserIdentity {
@@ -18,6 +18,8 @@ pub enum UserIdentity {
user_signing_key: String,
/// The public self-signing key of our identity.
self_signing_key: String,
/// True if this identity was verified at some point but is not anymore.
has_verification_violation: bool,
},
/// The user identity of other users.
Other {
@@ -27,13 +29,15 @@ pub enum UserIdentity {
master_key: String,
/// The public self-signing key of our identity.
self_signing_key: String,
/// True if this identity was verified at some point but is not anymore.
has_verification_violation: bool,
},
}
impl UserIdentity {
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result<Self, CryptoStoreError> {
Ok(match i {
UserIdentities::Own(i) => {
SdkUserIdentity::Own(i) => {
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
@@ -44,9 +48,10 @@ impl UserIdentity {
master_key: serde_json::to_string(&master)?,
user_signing_key: serde_json::to_string(&user_signing)?,
self_signing_key: serde_json::to_string(&self_signing)?,
has_verification_violation: i.has_verification_violation(),
}
}
UserIdentities::Other(i) => {
SdkUserIdentity::Other(i) => {
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
@@ -54,6 +59,7 @@ impl UserIdentity {
user_id: i.user_id().to_string(),
master_key: serde_json::to_string(&master)?,
self_signing_key: serde_json::to_string(&self_signing)?,
has_verification_violation: i.has_verification_violation(),
}
}
})
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
/// Listener that will be passed over the FFI to report changes to a SAS
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SasListener: Send {
/// The callback that should be called on the Rust side
///
@@ -82,7 +82,7 @@ pub struct Verification {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Verification {
/// Try to represent the `Verification` as an `Sas` verification object,
/// returns `None` if the verification is not a `Sas` verification.
@@ -112,7 +112,7 @@ pub struct Sas {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Sas {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -276,7 +276,7 @@ impl Sas {
/// Listener that will be passed over the FFI to report changes to a QrCode
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrCodeListener: Send {
/// The callback that should be called on the Rust side
///
@@ -328,7 +328,7 @@ pub struct QrCode {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCode {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult {
/// Listener that will be passed over the FFI to report changes to a
/// verification request.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationRequestListener: Send {
/// The callback that should be called on the Rust side
///
@@ -562,7 +562,7 @@ pub struct VerificationRequest {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl VerificationRequest {
/// The id of the other user that is participating in this verification
/// request.
@@ -752,7 +752,7 @@ impl VerificationRequest {
RustVerificationRequestState::Ready {
their_methods,
our_methods,
other_device_id: _,
other_device_data: _,
} => VerificationRequestState::Ready {
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
+24
View File
@@ -0,0 +1,24 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
name = "matrix-sdk-ffi-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
[lib]
proc-macro = true
test = false
doctest = false
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.18"
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
[lints]
workspace = true
+14
View File
@@ -0,0 +1,14 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
# matrix-sdk-ffi-macros
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use proc_macro::TokenStream;
use quote::quote;
use syn::{ImplItem, Item, TraitItem};
/// Attribute to specify the async runtime parameter for the `uniffi`
/// export macros if there any `async fn`s in the input.
#[proc_macro_attribute]
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
let has_async_fn = |item| {
if let Item::Fn(fun) = &item {
if fun.sig.asyncness.is_some() {
return true;
}
} else if let Item::Impl(blk) = &item {
for item in &blk.items {
if let ImplItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
}
false
};
let attr2 = proc_macro2::TokenStream::from(attr);
let item2 = proc_macro2::TokenStream::from(item.clone());
let res = match syn::parse(item) {
Ok(item) => match has_async_fn(item) {
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
false => quote! { #[uniffi::export(#attr2)] },
},
Err(e) => e.into_compile_error(),
};
quote! {
#res
#item2
}
.into()
}
+26 -5
View File
@@ -2,13 +2,34 @@
Breaking changes:
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
`SendingFromUnverifiedDevice`. These indicate that your own device is not
properly cross-signed, which is a requirement when using the identity-based
strategy, and can only be returned when using the identity-based strategy.
In addition, the `VerifiedUserHasUnsignedDevice` and
`VerifiedUserChangedIdentity` variants can be returned when using the
identity-based strategy, in addition to when using the device-based strategy
with `error_on_verified_user_problem` is set.
- `EventSendState` now has two additional variants: `VerifiedUserHasUnsignedDevice` and
`VerifiedUserChangedIdentity`. These reflect problems with verified users in the room
and as such can only be returned when the room key recipient strategy has
`error_on_verified_user_problem` set.
- The `AuthenticationService` has been removed:
- Instead of calling `configure_homeserver`, build your own client with the `serverNameOrHomeserverUrl` builder method to keep the same behaviour.
- The parts of `AuthenticationError` related to discovery will be represented in the `ClientBuildError` returned when calling `build()`.
- The remaining methods can be found on the built `Client`.
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (or fails to present).
- The rest of `AuthenticationError` is now found in the OidcError type.
- Instead of calling `configure_homeserver`, build your own client with the `serverNameOrHomeserverUrl` builder
method to keep the same behaviour.
- The parts of `AuthenticationError` related to discovery will be represented in the `ClientBuildError` returned
when calling `build()`.
- The remaining methods can be found on the built `Client`.
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
or fails to present).
- The rest of `AuthenticationError` is now found in the OidcError type.
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
- The `get_element_call_required_permissions` function now requires the device_id.
Additions:
- Add `Encryption::get_user_identity` which returns `UserIdentity`
- Add `ClientBuilder::room_key_recipient_strategy`
+7 -4
View File
@@ -28,19 +28,19 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
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"] }
sanitize-filename-reader-friendly = "2.2.1"
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-core = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
url = { workspace = true }
zeroize = { workspace = true }
@@ -82,3 +82,6 @@ features = [
[lints]
workspace = true
[package.metadata.release]
release = false
+5 -2
View File
@@ -5,6 +5,9 @@ use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-crypto-ffi/build.rs] too!
fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
@@ -18,11 +21,11 @@ fn setup_x86_64_android_workaround() {
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
+67 -7
View File
@@ -1,4 +1,8 @@
use std::collections::HashMap;
use std::{
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
use matrix_sdk::{
oidc::{
@@ -15,25 +19,27 @@ use matrix_sdk::{
};
use url::Url;
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
pub(crate) url: String,
pub(crate) sliding_sync_proxy: Option<String>,
pub(crate) sliding_sync_version: SlidingSyncVersion,
pub(crate) supports_oidc_login: bool,
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
pub(crate) supports_password_login: bool,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl HomeserverLoginDetails {
/// The URL of the currently configured homeserver.
pub fn url(&self) -> String {
self.url.clone()
}
/// The URL of the discovered or manually set sliding sync proxy,
/// if any.
pub fn sliding_sync_proxy(&self) -> Option<String> {
self.sliding_sync_proxy.clone()
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
self.sliding_sync_version.clone()
}
/// Whether the current homeserver supports login using OIDC.
@@ -41,12 +47,66 @@ impl HomeserverLoginDetails {
self.supports_oidc_login
}
/// The prompts advertised by the authentication issuer for use in the login
/// URL.
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
self.supported_oidc_prompts.clone()
}
/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> bool {
self.supports_password_login
}
}
/// An object encapsulating the SSO login flow
#[derive(uniffi::Object)]
pub struct SsoHandler {
/// The wrapped Client.
pub(crate) client: Arc<Client>,
/// The underlying URL for authentication.
pub(crate) url: String,
}
#[matrix_sdk_ffi_macros::export]
impl SsoHandler {
/// Returns the URL for starting SSO authentication. The URL should be
/// opened in a web view. Once the web view succeeds, call `finish` with
/// the callback URL.
pub fn url(&self) -> String {
self.url.clone()
}
/// Completes the SSO login process.
pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
let auth = self.client.inner.matrix_auth();
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
let builder =
auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
Ok(())
}
}
impl Debug for SsoHandler {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum SsoError {
#[error("The supplied callback URL used to complete SSO is invalid.")]
CallbackUrlInvalid,
#[error("Logging in with the token from the supplied callback URL failed.")]
LoginWithTokenFailed,
#[error("An error occurred: {message}")]
Generic { message: String },
}
/// The configuration to use when authenticating with OIDC.
#[derive(uniffi::Record)]
pub struct OidcConfiguration {
+511 -95
View File
@@ -1,13 +1,16 @@
use std::{
collections::HashMap,
mem::ManuallyDrop,
fmt::Debug,
path::Path,
sync::{Arc, RwLock},
};
use anyhow::{anyhow, Context as _};
use matrix_sdk::{
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
MediaThumbnailSettings,
},
oidc::{
registrations::{ClientId, OidcRegistrations},
requests::account_management::AccountManagementActionFull,
@@ -16,12 +19,13 @@ use matrix_sdk::{
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
requests::Prompt as SdkOidcPrompt,
},
OidcAuthorizationData, OidcSession,
},
reqwest::StatusCode,
ruma::{
api::client::{
media::get_content_thumbnail::v3::Method,
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
@@ -36,7 +40,8 @@ use matrix_sdk::{
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
sliding_sync::Version as SdkSlidingSyncVersion,
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -44,31 +49,37 @@ use matrix_sdk_ui::notification_client::{
};
use mime::Mime;
use ruma::{
api::client::{alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo},
api::client::{
alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo,
uiaa::UserIdentifier,
},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{json, Value};
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
use crate::{
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError},
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
encryption::Encryption,
notification::NotificationClient,
notification_settings::NotificationSettings,
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
ruma::AuthData,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utils::AsyncRuntimeDropped,
ClientError,
};
@@ -132,25 +143,25 @@ impl From<PushFormat> for RumaPushFormat {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientDelegate: Sync + Send {
fn did_receive_auth_error(&self, is_soft_logout: bool);
fn did_refresh_tokens(&self);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientSessionDelegate: Sync + Send {
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
fn save_session_in_keychain(&self, session: Session);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressWatcher: Send + Sync {
fn transmission_progress(&self, progress: TransmissionProgress);
}
/// A listener to the global (client-wide) error reporter of the send queue.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomErrorListener: Sync + Send {
/// Called every time the send queue has ran into an error for a given room,
/// which will disable the send queue for that particular room.
@@ -174,58 +185,52 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: ManuallyDrop<MatrixClient>,
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
}
impl Drop for Client {
fn drop(&mut self) {
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl Client {
pub async fn new(
sdk_client: MatrixClient,
cross_process_refresh_lock_id: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let ctrl = session_verification_controller.clone();
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
}
});
let cross_process_store_locks_holder_name =
sdk_client.cross_process_store_locks_holder_name().to_owned();
let client = Client {
inner: ManuallyDrop::new(sdk_client),
inner: AsyncRuntimeDropped::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
};
if let Some(process_id) = cross_process_refresh_lock_id {
if enable_oidc_refresh_lock {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates when enabling the cross-process lock"
))?;
}
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
client
.inner
.oidc()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
}
if let Some(session_delegate) = session_delegate {
@@ -252,18 +257,43 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Information about login options for the client's homeserver.
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok();
let oidc = self.inner.oidc();
let (supports_oidc_login, supported_oidc_prompts) = match oidc
.fetch_authentication_issuer()
.await
{
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
Ok(metadata) => {
let prompts = metadata
.prompt_values_supported
.as_ref()
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
(true, prompts)
}
Err(error) => {
error!("Failed to fetch OIDC provider metadata: {error}");
(true, Default::default())
}
},
Err(error) => {
error!("Failed to fetch authentication issuer: {error}");
(false, Default::default())
}
};
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
let sliding_sync_proxy = self.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
let sliding_sync_version = self.sliding_sync_version();
Arc::new(HomeserverLoginDetails {
url: self.homeserver(),
sliding_sync_proxy,
sliding_sync_version,
supports_oidc_login,
supported_oidc_prompts,
supports_password_login,
})
}
@@ -287,13 +317,79 @@ impl Client {
Ok(())
}
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// Login using JWT
/// This is an implementation of the custom_login https://docs.rs/matrix-sdk/latest/matrix_sdk/matrix_auth/struct.MatrixAuth.html#method.login_custom
/// For more information on logging in with JWT: https://element-hq.github.io/synapse/latest/jwt.html
pub async fn custom_login_with_jwt(
&self,
jwt: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<(), ClientError> {
let data = json!({ "token": jwt }).as_object().unwrap().clone();
let mut builder = self.inner.matrix_auth().login_custom("org.matrix.login.jwt", data)?;
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
}
/// Login using an email and password.
pub async fn login_with_email(
&self,
email: String,
password: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<(), ClientError> {
let mut builder = self
.inner
.matrix_auth()
.login_identifier(UserIdentifier::Email { address: email }, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
}
/// Returns a handler to start the SSO login process.
pub(crate) async fn start_sso_login(
self: &Arc<Self>,
redirect_url: String,
idp_id: Option<String>,
) -> Result<Arc<SsoHandler>, SsoError> {
let auth = self.inner.matrix_auth();
let url = auth
.get_sso_login_url(redirect_url.as_str(), idp_id.as_deref())
.await
.map_err(|e| SsoError::Generic { message: e.to_string() })?;
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
}
/// Requests the URL needed for opening a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns. If a failure occurs and a callback isn't available, make sure
/// to call `abort_oidc_login` to inform the client of this.
pub async fn url_for_oidc_login(
/// to call `abort_oidc_auth` to inform the client of this.
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: OidcPrompt,
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
@@ -314,14 +410,15 @@ impl Client {
static_registrations,
)?;
let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?;
let data =
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
Ok(Arc::new(data))
}
/// Aborts an existing OIDC login operation that might have been cancelled,
/// failed etc.
pub async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
self.inner.oidc().abort_authorization(&authorization_data.state).await;
}
@@ -341,7 +438,7 @@ impl Client {
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
body: Option<String>,
filename: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
@@ -353,8 +450,8 @@ impl Client {
.inner
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
body,
&MediaRequestParameters { source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
@@ -366,17 +463,11 @@ impl Client {
/// Restores the client from a `Session`.
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
let sliding_sync_proxy = session.sliding_sync_proxy.clone();
let sliding_sync_version = session.sliding_sync_version.clone();
let auth_session: AuthSession = session.try_into()?;
self.restore_session_inner(auth_session).await?;
if let Some(sliding_sync_proxy) = sliding_sync_proxy {
let sliding_sync_proxy = Url::parse(&sliding_sync_proxy)
.map_err(|error| ClientError::Generic { msg: error.to_string() })?;
self.inner.set_sliding_sync_proxy(Some(sliding_sync_proxy));
}
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
Ok(())
}
@@ -407,7 +498,7 @@ impl Client {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Respawn tasks for rooms that had unsent events. At this point we've just
// created the subscriber, so it'll be notified about errors.
q.respawn_tasks_for_rooms_with_unsent_events().await;
q.respawn_tasks_for_rooms_with_unsent_requests().await;
loop {
match subscriber.recv().await {
@@ -427,6 +518,15 @@ impl Client {
let http_client = self.inner.http_client();
Ok(http_client.get(url).send().await?.text().await?)
}
/// 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?)
}
}
impl Client {
@@ -439,13 +539,6 @@ impl Client {
Ok(())
}
/// The sliding sync proxy of the homeserver. It is either set automatically
/// during discovery or manually via `set_sliding_sync_proxy` or `None`
/// when not configured.
pub fn sliding_sync_proxy(&self) -> Option<Url> {
self.inner.sliding_sync_proxy()
}
/// Whether or not the client's homeserver supports the password login flow.
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
let login_types = self.inner.matrix_auth().get_login_types().await?;
@@ -457,8 +550,24 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
self.inner.sliding_sync_version().into()
}
/// Find all sliding sync versions that are available.
///
/// Be careful: This method may hit the store and will send new requests for
/// each call. It can be costly to call it repeatedly.
///
/// If `.well-known` or `/versions` is unreachable, it will simply move
/// potential sliding sync versions aside. No error will be reported.
pub async fn available_sliding_sync_versions(&self) -> Vec<SlidingSyncVersion> {
self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
}
pub fn set_delegate(
self: Arc<Self>,
delegate: Option<Box<dyn ClientDelegate>>,
@@ -492,6 +601,10 @@ impl Client {
&self,
action: Option<AccountManagementAction>,
) -> Result<Option<String>, ClientError> {
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
return Ok(None);
}
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
Ok(url) => Ok(url.map(|u| u.to_string())),
Err(e) => {
@@ -557,7 +670,7 @@ impl Client {
}
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let response = self.inner.create_room(request.into()).await?;
let response = self.inner.create_room(request.try_into()?).await?;
Ok(String::from(response.room_id()))
}
@@ -590,7 +703,7 @@ impl Client {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<String, ClientError> {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = self.inner.media().upload(&mime_type, data);
let request = self.inner.media().upload(&mime_type, data, None);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
@@ -612,10 +725,11 @@ impl Client {
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
debug!(?source, "requesting media file");
Ok(self
.inner
.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
.await?)
}
@@ -627,17 +741,17 @@ impl Client {
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
debug!(source = ?media_source, width, height, "requesting media thumbnail");
Ok(self
.inner
.media()
.get_media_content(
&MediaRequest {
&MediaRequestParameters {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Scale,
width: UInt::new(width).unwrap(),
height: UInt::new(height).unwrap(),
}),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
UInt::new(width).unwrap(),
UInt::new(height).unwrap(),
)),
},
true,
)
@@ -734,6 +848,21 @@ impl Client {
self.inner.homeserver().to_string()
}
/// The URL of the server.
///
/// Not to be confused with the `Self::homeserver`. `server` is usually
/// the server part in a user ID, e.g. with `@mnt_io:matrix.org`, here
/// `matrix.org` is the server, whilst `matrix-client.matrix.org` is the
/// homeserver (at the time of writing — 2024-08-28).
///
/// This value is optional depending on how the `Client` has been built.
/// If it's been built from a homeserver URL directly, we don't know the
/// server. However, if the `Client` has been built from a server URL or
/// name, then the homeserver has been discovered, and we know both.
pub fn server(&self) -> Option<String> {
self.inner.server().map(ToString::to_string)
}
pub fn rooms(&self) -> Vec<Arc<Room>> {
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
}
@@ -872,6 +1001,20 @@ impl Client {
Ok(Arc::new(Room::new(room)))
}
/// Knock on a room to join it using its ID or alias.
pub async fn knock(
&self,
room_id_or_alias: String,
reason: Option<String>,
server_names: Vec<String>,
) -> Result<Arc<Room>, ClientError> {
let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
let server_names =
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
let room = self.inner.knock(room_id, reason, server_names).await?;
Ok(Arc::new(Room::new(room)))
}
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
Ok(self
.inner
@@ -894,10 +1037,21 @@ impl Client {
pub async fn resolve_room_alias(
&self,
room_alias: String,
) -> Result<ResolvedRoomAlias, ClientError> {
) -> Result<Option<ResolvedRoomAlias>, ClientError> {
let room_alias = RoomAliasId::parse(&room_alias)?;
let response = self.inner.resolve_room_alias(&room_alias).await?;
Ok(response.into())
match self.inner.resolve_room_alias(&room_alias).await {
Ok(response) => Ok(Some(response.into())),
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
Some(StatusCode::NOT_FOUND) => Ok(None),
_ => Err(http_error.into()),
},
Err(error) => Err(error.into()),
}
}
/// Checks if a room alias exists in the current homeserver.
pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
}
/// Given a room id, get the preview of a room, to interact with it.
@@ -909,7 +1063,7 @@ impl Client {
&self,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
let via_servers = via_servers
@@ -922,16 +1076,16 @@ impl Client {
// rustc win that one fight.
let room_id: &RoomId = &room_id;
let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}
/// Given a room alias, get the preview of a room, to interact with it.
pub async fn get_room_preview_from_room_alias(
&self,
room_alias: String,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
@@ -939,13 +1093,76 @@ impl Client {
// rustc win that one fight.
let room_alias: &RoomAliasId = &room_alias;
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}
/// Waits until an at least partially synced room is received, and returns
/// it.
///
/// **Note: this function will loop endlessly until either it finds the room
/// or an externally set timeout happens.**
pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
let room_id = RoomId::parse(room_id)?;
Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await)))
}
/// Lets the user know whether this is an `m.login.password` based
/// auth and if the account can actually be deactivated
pub fn can_deactivate_account(&self) -> bool {
matches!(self.inner.auth_api(), Some(AuthApi::Matrix(_)))
}
/// Deactivate this account definitively.
/// Similarly to `encryption::reset_identity` this
/// will only work with password-based authentication (`m.login.password`)
///
/// # Arguments
///
/// * `auth_data` - This request uses the [User-Interactive Authentication
/// API][uiaa]. The first request needs to set this to `None` and will
/// always fail and the same request needs to be made but this time with
/// some `auth_data` provided.
pub async fn deactivate_account(
&self,
auth_data: Option<AuthData>,
erase_data: bool,
) -> Result<(), ClientError> {
if let Some(auth_data) = auth_data {
_ = self.inner.account().deactivate(None, Some(auth_data.into()), erase_data).await?;
} else {
_ = self.inner.account().deactivate(None, None, erase_data).await?;
}
Ok(())
}
/// Checks if a room alias is not in use yet.
///
/// Returns:
/// - `Ok(true)` if the room alias is available.
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
/// status code).
/// - An `Err` otherwise.
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
let alias = RoomAliasId::parse(alias)?;
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
}
/// Creates a new room alias associated with the provided room id.
pub async fn create_room_alias(
&self,
room_alias: String,
room_id: String,
) -> Result<(), ClientError> {
let room_alias = RoomAliasId::parse(room_alias)?;
let room_id = RoomId::parse(room_id)?;
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
fn call(&self, ignored_user_ids: Vec<String>);
}
@@ -1055,9 +1272,9 @@ impl Client {
let auth_api = client.auth_api().context("Missing authentication API")?;
let homeserver_url = client.homeserver().into();
let sliding_sync_proxy = client.sliding_sync_proxy().map(|url| url.to_string());
let sliding_sync_version = client.sliding_sync_version();
Session::new(auth_api, homeserver_url, sliding_sync_proxy)
Session::new(auth_api, homeserver_url, sliding_sync_version.into())
}
fn save_session(
@@ -1166,16 +1383,23 @@ pub struct CreateRoomParameters {
pub avatar: Option<String>,
#[uniffi(default = None)]
pub power_level_content_override: Option<PowerLevels>,
#[uniffi(default = None)]
pub join_rule_override: Option<JoinRule>,
#[uniffi(default = None)]
pub canonical_alias: Option<String>,
}
impl From<CreateRoomParameters> for create_room::v3::Request {
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
type Error = ClientError;
fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
let mut request = create_room::v3::Request::new();
request.name = value.name;
request.topic = value.topic;
request.is_direct = value.is_direct;
request.visibility = value.visibility.into();
request.preset = Some(value.preset.into());
request.room_alias_name = value.canonical_alias;
request.invite = match value.invite {
Some(invite) => invite
.iter()
@@ -1203,6 +1427,12 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
if let Some(join_rule_override) = value.join_rule_override {
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
if let Some(power_levels) = value.power_level_content_override {
@@ -1211,12 +1441,14 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
request.power_level_content_override = Some(power_levels);
}
Err(e) => {
error!("Failed to serialize power levels, error: {e}");
return Err(ClientError::Generic {
msg: format!("Failed to serialize power levels, error: {e}"),
})
}
}
}
request
Ok(request)
}
}
@@ -1284,15 +1516,15 @@ pub struct Session {
/// Additional data for this session if OpenID Connect was used for
/// authentication.
pub oidc_data: Option<String>,
/// The URL for the sliding sync proxy used for this session.
pub sliding_sync_proxy: Option<String>,
/// The sliding sync version used for this session.
pub sliding_sync_version: SlidingSyncVersion,
}
impl Session {
fn new(
auth_api: AuthApi,
homeserver_url: String,
sliding_sync_proxy: Option<String>,
sliding_sync_version: SlidingSyncVersion,
) -> Result<Session, ClientError> {
match auth_api {
// Build the session from the regular Matrix Auth Session.
@@ -1310,7 +1542,7 @@ impl Session {
device_id: device_id.to_string(),
homeserver_url,
oidc_data: None,
sliding_sync_proxy,
sliding_sync_version,
})
}
// Build the session from the OIDC UserSession.
@@ -1347,7 +1579,7 @@ impl Session {
device_id: device_id.to_string(),
homeserver_url,
oidc_data,
sliding_sync_proxy,
sliding_sync_version,
})
}
_ => Err(anyhow!("Unknown authentication API").into()),
@@ -1365,7 +1597,7 @@ impl TryFrom<Session> for AuthSession {
device_id,
homeserver_url: _,
oidc_data,
sliding_sync_proxy: _,
sliding_sync_version: _,
} = value;
if let Some(oidc_data) = oidc_data {
@@ -1502,7 +1734,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn gen_transaction_id() -> String {
TransactionId::new().to_string()
}
@@ -1520,7 +1752,7 @@ impl MediaFileHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> Result<String, ClientError> {
@@ -1553,3 +1785,187 @@ impl MediaFileHandle {
)
}
}
#[derive(Clone, uniffi::Enum)]
pub enum SlidingSyncVersion {
None,
Proxy { url: String },
Native,
}
impl From<SdkSlidingSyncVersion> for SlidingSyncVersion {
fn from(value: SdkSlidingSyncVersion) -> Self {
match value {
SdkSlidingSyncVersion::None => Self::None,
SdkSlidingSyncVersion::Proxy { url } => Self::Proxy { url: url.to_string() },
SdkSlidingSyncVersion::Native => Self::Native,
}
}
}
impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
type Error = ClientError;
fn try_from(value: SlidingSyncVersion) -> Result<Self, Self::Error> {
Ok(match value {
SlidingSyncVersion::None => Self::None,
SlidingSyncVersion::Proxy { url } => Self::Proxy {
url: Url::parse(&url).map_err(|e| ClientError::Generic { msg: e.to_string() })?,
},
SlidingSyncVersion::Native => Self::Native,
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum OidcPrompt {
/// The Authorization Server must not display any authentication or consent
/// user interface pages.
None,
/// The Authorization Server should prompt the End-User for
/// reauthentication.
Login,
/// The Authorization Server should prompt the End-User for consent before
/// returning information to the Client.
Consent,
/// The Authorization Server should prompt the End-User to select a user
/// account.
///
/// This enables an End-User who has multiple accounts at the Authorization
/// Server to select amongst the multiple accounts that they might have
/// current sessions for.
SelectAccount,
/// The Authorization Server should prompt the End-User to create a user
/// account.
///
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
Create,
/// An unknown value.
Unknown { value: String },
}
impl From<&SdkOidcPrompt> for OidcPrompt {
fn from(value: &SdkOidcPrompt) -> Self {
match value {
SdkOidcPrompt::None => Self::None,
SdkOidcPrompt::Login => Self::Login,
SdkOidcPrompt::Consent => Self::Consent,
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
SdkOidcPrompt::Create => Self::Create,
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
_ => Self::Unknown { value: value.to_string() },
}
}
}
impl From<OidcPrompt> for SdkOidcPrompt {
fn from(value: OidcPrompt) -> Self {
match value {
OidcPrompt::None => Self::None,
OidcPrompt::Login => Self::Login,
OidcPrompt::Consent => Self::Consent,
OidcPrompt::SelectAccount => Self::SelectAccount,
OidcPrompt::Create => Self::Create,
OidcPrompt::Unknown { value } => Self::Unknown(value),
}
}
}
/// The rule used for users wishing to join this room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum JoinRule {
/// Anyone can join the room without any prior action.
Public,
/// A user who wishes to join the room must first receive an invite to the
/// room from someone already inside of the room.
Invite,
/// Users can join the room if they are invited, or they can request an
/// invite to the room.
///
/// They can be allowed (invited) or denied (kicked/banned) access.
Knock,
/// Reserved but not yet implemented by the Matrix specification.
Private,
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s.
Restricted { rules: Vec<AllowRule> },
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s, or they can request
/// an invite to the room.
KnockRestricted { rules: Vec<AllowRule> },
/// A custom join rule, up for interpretation by the consumer.
Custom {
/// The string representation for this custom rule.
repr: String,
},
}
/// An allow rule which defines a condition that allows joining a room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AllowRule {
/// Only a member of the `room_id` Room can join the one this rule is used
/// in.
RoomMembership { room_id: String },
}
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
type Error = ClientError;
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
match value {
JoinRule::Public => Ok(Self::Public),
JoinRule::Invite => Ok(Self::Invite),
JoinRule::Knock => Ok(Self::Knock),
JoinRule::Private => Ok(Self::Private),
JoinRule::Restricted { rules } => {
let rules = allow_rules_from(rules)?;
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::KnockRestricted { rules } => {
let rules = allow_rules_from(rules)?;
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
}
}
}
fn allow_rules_from(
value: Vec<AllowRule>,
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
let mut ret = Vec::with_capacity(value.len());
for rule in value {
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
match rule {
Ok(rule) => ret.push(rule),
Err(error) => return Err(error),
}
}
Ok(ret)
}
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
type Error = ClientError;
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
match value {
AllowRule::RoomMembership { room_id } => {
let room_id = RoomId::parse(room_id)?;
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
room_id,
)))
}
}
}
}
+206 -97
View File
@@ -1,14 +1,18 @@
use std::{fs, path::PathBuf, sync::Arc};
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
use futures_util::StreamExt;
use matrix_sdk::{
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
crypto::{
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
CollectStrategy, TrustRequirement,
},
encryption::{BackupDownloadStrategy, EncryptionSettings},
reqwest::Certificate,
ruma::{
api::{error::UnknownVersionError, MatrixVersion},
ServerName, UserId,
ruma::{ServerName, UserId},
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError,
@@ -43,7 +47,7 @@ pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
@@ -79,7 +83,7 @@ pub enum HumanQrLoginError {
Declined,
#[error("An unknown error has happened.")]
Unknown,
#[error("The homeserver doesn't provide a sliding sync proxy in its configuration.")]
#[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,
@@ -155,7 +159,7 @@ pub enum QrLoginProgress {
Done,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: Sync + Send {
fn on_update(&self, state: QrLoginProgress);
}
@@ -191,12 +195,13 @@ pub enum ClientBuildError {
WellKnownLookupFailed(RumaApiError),
#[error(transparent)]
WellKnownDeserializationError(DeserializationError),
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
SlidingSyncNotAvailable,
#[error(transparent)]
#[allow(dead_code)] // rustc's drunk, this is used
SlidingSync(MatrixSlidingSyncError),
#[error(transparent)]
SlidingSyncVersion(VersionBuilderError),
#[error(transparent)]
Sdk(MatrixClientBuildError),
#[error("Failed to build the client: {message}")]
Generic { message: String },
}
@@ -212,10 +217,9 @@ impl From<MatrixClientBuildError> for ClientBuildError {
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(e)) => {
ClientBuildError::WellKnownDeserializationError(e)
}
MatrixClientBuildError::SlidingSyncNotAvailable => {
ClientBuildError::SlidingSyncNotAvailable
MatrixClientBuildError::SlidingSyncVersion(e) => {
ClientBuildError::SlidingSyncVersion(e)
}
_ => ClientBuildError::Sdk(e),
}
}
@@ -247,59 +251,69 @@ impl From<ClientError> for ClientBuildError {
#[derive(Clone, uniffi::Object)]
pub struct ClientBuilder {
session_path: Option<String>,
session_paths: Option<SessionPaths>,
username: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
server_versions: Option<Vec<String>>,
passphrase: Zeroizing<Option<String>>,
user_agent: Option<String>,
requires_sliding_sync: bool,
sliding_sync_proxy: Option<String>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_refresh_lock_id: Option<String>,
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,
request_config: Option<RequestConfig>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
session_path: None,
session_paths: None,
username: None,
homeserver_cfg: None,
server_versions: None,
passphrase: Zeroizing::new(None),
user_agent: None,
requires_sliding_sync: false,
sliding_sync_proxy: None,
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_refresh_lock_id: None,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
session_delegate: None,
additional_root_certificates: Default::default(),
disable_built_in_root_certificates: false,
encryption_settings: EncryptionSettings {
auto_enable_cross_signing: false,
backup_download_strategy:
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
auto_enable_backups: false,
},
room_key_recipient_strategy: Default::default(),
decryption_trust_requirement: TrustRequirement::Untrusted,
request_config: Default::default(),
})
}
pub fn enable_cross_process_refresh_lock(
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
process_id: String,
session_delegate: Box<dyn ClientSessionDelegate>,
holder_name: String,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_refresh_lock_id = Some(process_id);
builder.session_delegate = Some(session_delegate.into());
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
Arc::new(builder)
}
@@ -312,14 +326,15 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Sets the path that the client will use to store its data once logged in.
/// This path **must** be unique per session as the data stores aren't
/// capable of handling multiple users.
/// Sets the paths that the client will use to store its data and caches.
/// Both paths **must** be unique per session as the SDK stores aren't
/// capable of handling multiple users, however it is valid to use the
/// same path for both stores on a single session.
///
/// Leaving this unset tells the client to use an in-memory data store.
pub fn session_path(self: Arc<Self>, path: String) -> Arc<Self> {
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_path = Some(path);
builder.session_paths = Some(SessionPaths { data_path, cache_path });
Arc::new(builder)
}
@@ -329,12 +344,6 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn server_versions(self: Arc<Self>, versions: Vec<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.server_versions = Some(versions);
Arc::new(builder)
}
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
@@ -365,15 +374,12 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn requires_sliding_sync(self: Arc<Self>) -> Arc<Self> {
pub fn sliding_sync_version_builder(
self: Arc<Self>,
version_builder: SlidingSyncVersionBuilder,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.requires_sliding_sync = true;
Arc::new(builder)
}
pub fn sliding_sync_proxy(self: Arc<Self>, sliding_sync_proxy: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.sliding_sync_proxy = sliding_sync_proxy;
builder.sliding_sync_version_builder = version_builder;
Arc::new(builder)
}
@@ -405,6 +411,15 @@ impl ClientBuilder {
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,
@@ -434,20 +449,58 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set the strategy to be used for picking recipient devices when sending
/// an encrypted message.
pub fn room_key_recipient_strategy(self: Arc<Self>, strategy: CollectStrategy) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.room_key_recipient_strategy = strategy;
Arc::new(builder)
}
/// Set the trust requirement to be used when decrypting events.
pub fn room_decryption_trust_requirement(
self: Arc<Self>,
trust_requirement: TrustRequirement,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.decryption_trust_requirement = trust_requirement;
Arc::new(builder)
}
/// Add a default request config to this client.
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.request_config = Some(config);
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();
if let Some(session_path) = &builder.session_path {
let data_path = PathBuf::from(session_path);
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
inner_builder =
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
if let Some(session_paths) = &builder.session_paths {
let data_path = PathBuf::from(&session_paths.data_path);
let cache_path = PathBuf::from(&session_paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
"Creating directory and using it as the store path."
cache_path = %cache_path.to_string_lossy(),
"Creating directories for data and cache stores.",
);
fs::create_dir_all(&data_path)?;
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
fs::create_dir_all(&cache_path)?;
inner_builder = inner_builder.sqlite_store_with_cache_path(
&data_path,
&cache_path,
builder.passphrase.as_deref(),
);
} else {
debug!("Not using a store path.");
}
@@ -479,19 +532,27 @@ impl ClientBuilder {
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.
if let Ok(cert) = Certificate::from_der(&certificate) {
certificates.push(cert);
} else {
let cert =
Certificate::from_pem(&certificate).map_err(|e| ClientBuildError::Generic {
message: format!("Failed to add a root certificate {e:?}"),
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);
certificates.push(cert);
}
}
}
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 let Some(proxy) = builder.proxy {
inner_builder = inner_builder.proxy(proxy);
}
@@ -508,48 +569,64 @@ impl ClientBuilder {
inner_builder = inner_builder.user_agent(user_agent);
}
if let Some(server_versions) = builder.server_versions {
inner_builder = inner_builder.server_versions(
server_versions
.iter()
.map(|s| MatrixVersion::try_from(s.as_str()))
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
);
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);
match builder.sliding_sync_version_builder {
SlidingSyncVersionBuilder::None => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
}
SlidingSyncVersionBuilder::Proxy { url } => {
inner_builder = inner_builder.sliding_sync_version_builder(
MatrixSlidingSyncVersionBuilder::Proxy {
url: Url::parse(&url)
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
},
)
}
SlidingSyncVersionBuilder::Native => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
}
SlidingSyncVersionBuilder::DiscoverProxy => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
}
SlidingSyncVersionBuilder::DiscoverNative => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
}
}
inner_builder = inner_builder.with_encryption_settings(builder.encryption_settings);
if builder.requires_sliding_sync {
inner_builder = inner_builder.requires_sliding_sync();
if let Some(config) = builder.request_config {
let mut updated_config = matrix_sdk::config::RequestConfig::default();
if let Some(retry_limit) = config.retry_limit {
updated_config = updated_config.retry_limit(retry_limit);
}
if let Some(timeout) = config.timeout {
updated_config = updated_config.timeout(Duration::from_millis(timeout));
}
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
if max_concurrent_requests > 0 {
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
max_concurrent_requests as usize,
));
}
}
if let Some(retry_timeout) = config.retry_timeout {
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
}
inner_builder = inner_builder.request_config(updated_config);
}
let sdk_client = inner_builder.build().await?;
// At this point, `sdk_client` might contain a `sliding_sync_proxy` that has
// been configured by the homeserver (if it's a `ServerName` and the
// `.well-known` file is filled as expected).
//
// If `builder.sliding_sync_proxy` contains `Some(_)`, it means one wants to
// overwrite this value. It would be an error to call
// `sdk_client.set_sliding_sync_proxy()` with `None`, as it would erase the
// `sliding_sync_proxy` if any, and it's not the intended behavior.
//
// So let's call `sdk_client.set_sliding_sync_proxy()` if and only if there is
// `Some(_)` value in `builder.sliding_sync_proxy`. That's really important: It
// might not break an existing app session, but it is likely to break a new
// session, which not immediate to detect if there is no test.
if let Some(sliding_sync_proxy) = builder.sliding_sync_proxy {
sdk_client.set_sliding_sync_proxy(Some(Url::parse(&sliding_sync_proxy)?));
}
Ok(Arc::new(
Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)
.await?,
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
}
@@ -578,7 +655,7 @@ impl ClientBuilder {
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
let client = builder.build().await.map_err(|e| match e {
ClientBuildError::SlidingSyncNotAvailable => HumanQrLoginError::SlidingSyncNotAvailable,
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
_ => {
error!("Couldn't build the client {e:?}");
HumanQrLoginError::Unknown
@@ -606,3 +683,35 @@ impl ClientBuilder {
Ok(client)
}
}
#[derive(Clone)]
/// The store paths the client will use when built.
struct SessionPaths {
/// The path that the client will use to store its data.
data_path: String,
/// The path that the client will use to store its caches. This path can be
/// the same as the data path if you prefer to keep everything in one place.
cache_path: String,
}
#[derive(Clone, uniffi::Record)]
/// The config to use for HTTP requests by default in this client.
pub struct RequestConfig {
/// Max number of retries.
retry_limit: Option<u64>,
/// Timeout for a request in milliseconds.
timeout: Option<u64>,
/// Max number of concurrent requests. No value means no limits.
max_concurrent_requests: Option<u64>,
/// Base delay between retries.
retry_timeout: Option<u64>,
}
#[derive(Clone, uniffi::Enum)]
pub enum SlidingSyncVersionBuilder {
None,
Proxy { url: String },
Native,
DiscoverProxy,
DiscoverNative,
}
+3 -2
View File
@@ -11,11 +11,12 @@ pub struct ElementCallWellKnown {
/// Element specific well-known settings
#[derive(Deserialize, uniffi::Record)]
pub struct ElementWellKnown {
call: ElementCallWellKnown,
call: Option<ElementCallWellKnown>,
registration_helper_url: Option<String>,
}
/// Helper function to parse a string into a ElementWellKnown struct
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
}
+184 -7
View File
@@ -6,10 +6,11 @@ use matrix_sdk::{
encryption::{backups, recovery},
};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
use crate::{client::Client, error::ClientError, task_handle::TaskHandle};
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
#[derive(uniffi::Object)]
pub struct Encryption {
@@ -23,22 +24,22 @@ pub struct Encryption {
pub(crate) _client: Arc<Client>,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupStateListener: Sync + Send {
fn on_update(&self, status: BackupState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
fn on_update(&self, status: BackupUploadState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
fn on_update(&self, status: VerificationState);
}
@@ -162,7 +163,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
fn on_update(&self, status: EnableRecoveryProgress);
}
@@ -212,7 +213,7 @@ impl From<encryption::VerificationState> for VerificationState {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
@@ -315,6 +316,7 @@ impl Encryption {
pub async fn enable_recovery(
&self,
wait_for_backups_to_upload: bool,
mut passphrase: Option<String>,
progress_listener: Box<dyn EnableRecoveryProgressListener>,
) -> Result<String> {
let recovery = self.inner.recovery();
@@ -325,6 +327,12 @@ impl Encryption {
recovery.enable()
};
let enable = if let Some(passphrase) = &passphrase {
enable.with_passphrase(passphrase)
} else {
enable
};
let mut progress_stream = enable.subscribe_to_progress();
let task = RUNTIME.spawn(async move {
@@ -337,6 +345,7 @@ impl Encryption {
let ret = enable.await?;
task.abort();
passphrase.zeroize();
Ok(ret)
}
@@ -357,6 +366,22 @@ impl Encryption {
Ok(result?)
}
/// Completely reset the current user's crypto identity: reset the cross
/// signing keys, delete the existing backup and recovery key.
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
if let Some(reset_handle) = self
.inner
.recovery()
.reset_identity()
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
{
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
}
Ok(None)
}
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
let result = self.inner.recovery().recover(&recovery_key).await;
@@ -374,6 +399,7 @@ impl Encryption {
listener: Box<dyn VerificationStateListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.verification_state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
while let Some(verification_state) = subscriber.next().await {
listener.on_update(verification_state.into());
@@ -386,4 +412,155 @@ impl Encryption {
pub async fn wait_for_e2ee_initialization_tasks(&self) {
self.inner.wait_for_e2ee_initialization_tasks().await;
}
/// Get the E2EE identity of a user.
///
/// This method always tries to fetch the identity from the store, which we
/// only have if the user is tracked, meaning that we are both members
/// of the same encrypted room. If no user is found locally, a request will
/// be made to the homeserver.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that the identity belongs to.
///
/// Returns a `UserIdentity` if one is found. Returns an error if there
/// was an issue with the crypto store or with the request to the
/// homeserver.
///
/// This will always return `None` if the client hasn't been logged in.
pub async fn user_identity(
&self,
user_id: String,
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
Ok(Some(identity)) => {
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
}
Ok(None) => {
info!("No identity found in the store.");
}
Err(error) => {
error!("Failed fetching identity from the store: {}", error);
}
};
info!("Requesting identity from the server.");
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
}
}
/// The E2EE identity of a user.
#[derive(uniffi::Object)]
pub struct UserIdentity {
inner: matrix_sdk::encryption::identities::UserIdentity,
}
#[matrix_sdk_ffi_macros::export]
impl UserIdentity {
/// Remember this identity, ensuring it does not result in a pin violation.
///
/// When we first see a user, we assume their cryptographic identity has not
/// been tampered with by the homeserver or another entity with
/// man-in-the-middle capabilities. We remember this identity and call this
/// action "pinning".
///
/// If the identity presented for the user changes later on, the newly
/// presented identity is considered to be in "pin violation". This
/// method explicitly accepts the new identity, allowing it to replace
/// the previously pinned one and bringing it out of pin violation.
///
/// UIs should display a warning to the user when encountering an identity
/// which is not verified and is in pin violation.
pub(crate) async fn pin(&self) -> Result<(), ClientError> {
Ok(self.inner.pin().await?)
}
/// Get the public part of the Master key of this user identity.
///
/// The public part of the Master key is usually used to uniquely identify
/// the identity.
///
/// Returns None if the master key does not actually contain any keys.
pub(crate) fn master_key(&self) -> Option<String> {
self.inner.master_key().get_first_key().map(|k| k.to_base64())
}
/// Is the user identity considered to be verified.
///
/// If the identity belongs to another user, our own user identity needs to
/// be verified as well for the identity to be considered to be verified.
pub fn is_verified(&self) -> bool {
self.inner.is_verified()
}
}
#[derive(uniffi::Object)]
pub struct IdentityResetHandle {
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
}
#[matrix_sdk_ffi_macros::export]
impl IdentityResetHandle {
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
/// process is using.
pub fn auth_type(&self) -> CrossSigningResetAuthType {
self.inner.auth_type().into()
}
/// This method starts the identity reset process and
/// will go through the following steps:
///
/// 1. Disable backing up room keys and delete the active backup
/// 2. Disable recovery and delete secret storage
/// 3. Go through the cross-signing key reset flow
/// 4. Finally, re-enable key backups only if they were enabled before
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
if let Some(auth) = auth {
self.inner
.reset(Some(auth.into()))
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })
} else {
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
}
}
pub async fn cancel(&self) {
self.inner.cancel().await;
}
}
#[derive(uniffi::Enum)]
pub enum CrossSigningResetAuthType {
/// The homeserver requires user-interactive authentication.
Uiaa,
// /// OIDC is used for authentication and the user needs to open a URL to
// /// approve the upload of cross-signing keys.
Oidc {
info: OidcCrossSigningResetInfo,
},
}
impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningResetAuthType {
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
match value {
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
}
}
}
#[derive(uniffi::Record)]
pub struct OidcCrossSigningResetInfo {
/// The URL where the user can approve the reset of the cross-signing keys.
pub approval_url: String,
}
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
Self { approval_url: value.approval_url.to_string() }
}
}
+113 -3
View File
@@ -1,13 +1,16 @@
use std::fmt::Display;
use std::{collections::HashMap, fmt, fmt::Display};
use matrix_sdk::{
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
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 uniffi::UnexpectedUniFFICallbackError;
use crate::room_list::RoomListError;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
@@ -128,18 +131,120 @@ impl From<RoomError> for ClientError {
}
}
impl From<RoomListError> for ClientError {
fn from(e: RoomListError) -> Self {
Self::new(e)
}
}
impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
}
}
impl From<EditError> for ClientError {
fn from(e: EditError) -> Self {
Self::new(e)
}
}
impl From<RoomSendQueueError> for ClientError {
fn from(e: RoomSendQueueError) -> Self {
Self::new(e)
}
}
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
/// String.
///
/// Represent a failed to send unrecoverable error of an event sent via the
/// send_queue. It is a serializable representation of a client error, see
/// `From` implementation for more details. These errors can not be
/// automatically retried, but yet some manual action can be taken before retry
/// sending. If not the only solution is to delete the local event.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum QueueWedgeError {
/// This error occurs when there are some insecure devices in the room, and
/// the current encryption setting prohibit sharing with them.
InsecureDevices {
/// The insecure devices as a Map of userID to deviceID.
user_device_map: HashMap<String, Vec<String>>,
},
/// This error occurs when a previously verified user is not anymore, and
/// the current encryption setting prohibit sharing when it happens.
IdentityViolations {
/// The users that are expected to be verified but are not.
users: Vec<String>,
},
/// It is required to set up cross-signing and properly erify the current
/// session before sending.
CrossVerificationRequired,
/// Some media content to be sent has disappeared from the cache.
MissingMediaContent,
/// Some mime type couldn't be parsed.
InvalidMimeType { mime_type: String },
/// Other errors.
GenericApiError { msg: String },
}
/// Simple display implementation that strips out userIds/DeviceIds to avoid
/// accidental logging.
impl Display for QueueWedgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QueueWedgeError::InsecureDevices { .. } => {
f.write_str("There are insecure devices in the room")
}
QueueWedgeError::IdentityViolations { .. } => {
f.write_str("Some users that were previously verified are not anymore")
}
QueueWedgeError::CrossVerificationRequired => {
f.write_str("Own verification is required")
}
QueueWedgeError::MissingMediaContent => {
f.write_str("Media to be sent disappeared from local storage")
}
QueueWedgeError::InvalidMimeType { mime_type } => {
write!(f, "Invalid mime type '{mime_type}' for media upload")
}
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
}
}
}
impl From<SdkQueueWedgeError> for QueueWedgeError {
fn from(value: SdkQueueWedgeError) -> Self {
match value {
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
user_device_map: user_device_map
.iter()
.map(|(user_id, devices)| {
(
user_id.to_string(),
devices.iter().map(|device_id| device_id.to_string()).collect(),
)
})
.collect(),
},
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
},
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
SdkQueueWedgeError::InvalidMimeType { mime_type } => {
Self::InvalidMimeType { mime_type }
}
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
@@ -211,3 +316,8 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
Self::Generic { msg: e.to_string() }
}
}
/// Something has not been implemented yet.
#[derive(thiserror::Error, Debug)]
#[error("not implemented yet")]
pub struct NotYetImplemented;
+59 -8
View File
@@ -1,8 +1,14 @@
use anyhow::{bail, Context};
use ruma::events::{
room::message::Relation, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
events::{
room::{message::Relation, redaction::SyncRoomRedactionEvent},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
},
EventId,
};
use crate::{
@@ -14,7 +20,7 @@ use crate::{
#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineEvent {
pub fn event_id(&self) -> String {
self.0.event_id().to_string()
@@ -99,7 +105,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
let original_content = get_state_event_original_content(content)?;
StateEventContent::RoomMemberContent {
user_id: state_key,
membership_state: original_content.membership.into(),
membership_state: original_content.membership.try_into()?,
}
}
AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName,
@@ -135,7 +141,7 @@ pub enum MessageLikeEventContent {
ReactionContent { related_event_id: String },
RoomEncrypted,
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
RoomRedaction,
RoomRedaction { redacted_event_id: Option<String>, reason: Option<String> },
Sticker,
}
@@ -200,7 +206,17 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
in_reply_to_event_id,
}
}
AnySyncMessageLikeEvent::RoomRedaction(_) => MessageLikeEventContent::RoomRedaction,
AnySyncMessageLikeEvent::RoomRedaction(c) => {
let (redacted_event_id, reason) = match c {
SyncRoomRedactionEvent::Original(o) => {
let id =
if o.content.redacts.is_some() { o.content.redacts } else { o.redacts };
(id.map(|id| id.to_string()), o.content.reason)
}
SyncRoomRedactionEvent::Redacted(_) => (None, None),
};
MessageLikeEventContent::RoomRedaction { redacted_event_id, reason }
}
AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker,
_ => bail!("Unsupported Event Type"),
};
@@ -339,3 +355,38 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
}
}
}
/// Contains the 2 possible identifiers of an event, either it has a remote
/// event id or a local transaction id, never both or none.
#[derive(Clone, uniffi::Enum)]
pub enum EventOrTransactionId {
EventId { event_id: String },
TransactionId { transaction_id: String },
}
impl From<TimelineEventItemId> for EventOrTransactionId {
fn from(value: TimelineEventItemId) -> Self {
match value {
TimelineEventItemId::EventId(event_id) => {
EventOrTransactionId::EventId { event_id: event_id.to_string() }
}
TimelineEventItemId::TransactionId(transaction_id) => {
EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() }
}
}
}
}
impl TryFrom<EventOrTransactionId> for TimelineEventItemId {
type Error = IdParseError;
fn try_from(value: EventOrTransactionId) -> Result<Self, Self::Error> {
match value {
EventOrTransactionId::EventId { event_id } => {
Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?))
}
EventOrTransactionId::TransactionId { transaction_id } => {
Ok(TimelineEventItemId::TransactionId(transaction_id.into()))
}
}
}
}
@@ -0,0 +1,24 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk::crypto::IdentityState;
#[derive(uniffi::Record)]
pub struct IdentityStatusChange {
/// The user ID of the user whose identity status changed
pub user_id: String,
/// The new state of the identity of the user.
pub changed_to: IdentityState,
}
+3 -19
View File
@@ -2,24 +2,6 @@
#![allow(unused_qualifications, clippy::new_without_default)]
macro_rules! unwrap_or_clone_arc_into_variant {
(
$arc:ident $(, .$field:tt)?, $pat:pat => $body:expr
) => {
#[allow(unused_variables)]
match &(*$arc)$(.$field)? {
$pat => {
#[warn(unused_variables)]
match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? {
$pat => Some($body),
_ => unreachable!(),
}
},
_ => None,
}
};
}
mod authentication;
mod chunk_iterator;
mod client;
@@ -29,10 +11,12 @@ mod encryption;
mod error;
mod event;
mod helpers;
mod identity_status_change;
mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_alias;
mod room_directory_search;
mod room_info;
mod room_list;
@@ -61,7 +45,7 @@ use self::{
uniffi::include_scaffolding!("api");
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn sdk_git_sha() -> String {
env!("VERGEN_GIT_SHA").to_owned()
}
+1 -1
View File
@@ -88,7 +88,7 @@ pub struct NotificationClient {
pub(crate) _client: Arc<Client>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl NotificationClient {
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
@@ -49,7 +49,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
}
/// Delegate to notify of changes in push rules
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait NotificationSettingsDelegate: Sync + Send {
fn settings_did_change(&self);
}
@@ -98,7 +98,7 @@ impl Drop for NotificationSettings {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl NotificationSettings {
pub fn set_delegate(&self, delegate: Option<Box<dyn NotificationSettingsDelegate>>) {
if let Some(delegate) = delegate {
+69 -8
View File
@@ -1,7 +1,13 @@
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_core::Subscriber;
use tracing_subscriber::{
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
field::RecordFields,
fmt::{
self,
format::{DefaultFields, Writer},
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::SubscriberExt,
registry::LookupSpan,
util::SubscriberInitExt,
@@ -97,17 +103,19 @@ where
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
let mut first = true;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
first = false;
write!(writer, "{}", span.metadata().name())?;
let ext = span.extensions();
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
first = false;
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
@@ -133,7 +141,25 @@ where
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
// 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);
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 +
@@ -145,8 +171,26 @@ where
Layer::and_then(
file_layer,
config.write_to_stdout_or_system.then(|| {
// 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 FieldsFormatterFormStdoutOrSystem(DefaultFields);
impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
#[cfg(not(target_os = "android"))]
return fmt::layer()
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
.event_format(EventFormatter::new())
// See comment above.
.with_ansi(false)
@@ -154,6 +198,7 @@ where
#[cfg(target_os = "android")]
return fmt::layer()
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
.event_format(EventFormatter::for_logcat())
// See comment above.
.with_ansi(false)
@@ -164,24 +209,40 @@ where
)
}
/// Configuration to save logs to (rotated) log-files.
#[derive(uniffi::Record)]
pub struct TracingFileConfiguration {
/// Base location for all the log files.
path: String,
/// Prefix for the log files' names.
file_prefix: String,
/// Optional suffix for the log file's names.
file_suffix: Option<String>,
/// Maximum number of rotated files.
///
/// If not set, there's no max limit, i.e. the number of log files is
/// unlimited.
max_files: Option<u64>,
}
#[derive(uniffi::Record)]
pub struct TracingConfiguration {
/// A filter line following the [RUST_LOG format].
///
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
filter: String,
/// Controls whether to print to stdout or, equivalent, the system logs on
/// Android.
/// Whether to log to stdout, or in the logcat on Android.
write_to_stdout_or_system: bool,
/// If set, configures rotated log files where to write additional logs.
write_to_files: Option<TracingFileConfiguration>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn setup_tracing(config: TracingConfiguration) {
log_panics();
+169 -14
View File
@@ -1,9 +1,13 @@
use std::sync::Arc;
use std::{collections::HashMap, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::StreamExt;
use matrix_sdk::{
crypto::LocalTrust,
event_cache::paginator::PaginatorError,
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
};
@@ -16,11 +20,12 @@ use ruma::{
call::notify,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
message::RoomMessageEventContentWithoutRelation,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
},
TimelineEventType,
},
EventId, Int, RoomAliasId, UserId,
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
@@ -30,19 +35,21 @@ use crate::{
chunk_iterator::ChunkIterator,
error::{ClientError, MediaInfoError, RoomError},
event::{MessageLikeEventType, StateEventType},
identity_status_change::IdentityStatusChange,
room_info::RoomInfo,
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{FocusEventError, ReceiptType, Timeline},
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
utils::u64_to_uint,
TaskHandle,
};
#[derive(uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum Membership {
Invited,
Joined,
Left,
Knocked,
}
impl From<RoomState> for Membership {
@@ -51,6 +58,7 @@ impl From<RoomState> for Membership {
RoomState::Invited => Membership::Invited,
RoomState::Joined => Membership::Joined,
RoomState::Left => Membership::Left,
RoomState::Knocked => Membership::Knocked,
}
}
}
@@ -73,7 +81,7 @@ impl Room {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Room {
pub fn id(&self) -> String {
self.inner.room_id().to_string()
@@ -154,7 +162,12 @@ impl Room {
/// the user who invited the logged-in user to a room.
pub async fn inviter(&self) -> Option<RoomMember> {
if self.inner.state() == RoomState::Invited {
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
self.inner
.invite_details()
.await
.ok()
.and_then(|a| a.inviter)
.and_then(|m| m.try_into().ok())
} else {
None
}
@@ -225,6 +238,28 @@ impl Room {
Ok(Timeline::new(timeline))
}
pub async fn pinned_events_timeline(
&self,
internal_id_prefix: Option<String>,
max_events_to_load: u16,
max_concurrent_requests: u16,
) -> Result<Arc<Timeline>, ClientError> {
let room = &self.inner;
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
if let Some(internal_id_prefix) = internal_id_prefix {
builder = builder.with_internal_id_prefix(internal_id_prefix);
}
let timeline = builder
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
.build()
.await?;
Ok(Timeline::new(timeline))
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
}
@@ -242,7 +277,7 @@ impl Room {
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
Ok(member.into())
Ok(member.try_into().context("Unknown state membership")?)
}
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
@@ -519,6 +554,11 @@ impl Room {
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,
@@ -550,6 +590,31 @@ impl Room {
})))
}
pub fn subscribe_to_identity_status_changes(
&self,
listener: Box<dyn IdentityStatusChangeListener>,
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let status_changes = room.subscribe_to_identity_status_changes().await;
if let Ok(status_changes) = status_changes {
// TODO: what to do with failures?
let mut status_changes = pin!(status_changes);
while let Some(identity_status_changes) = status_changes.next().await {
listener.call(
identity_status_changes
.into_iter()
.map(|change| {
let user_id = change.user_id.to_string();
IdentityStatusChange { user_id, changed_to: change.changed_to }
})
.collect(),
);
}
}
})))
}
/// Set (or unset) a flag on the room to indicate that the user has
/// explicitly marked it as unread.
pub async fn set_unread_flag(&self, new_value: bool) -> Result<(), ClientError> {
@@ -568,7 +633,7 @@ impl Room {
}
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
let power_levels = self.inner.room_power_levels().await?;
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
Ok(RoomPowerLevels::from(power_levels))
}
@@ -690,10 +755,95 @@ impl Room {
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
Ok(self.inner.clear_composer_draft().await?)
}
/// Edit an event given its event id.
///
/// Useful outside the context of a timeline, or when a timeline doesn't
/// have the full content of an event.
pub async fn edit(
&self,
event_id: String,
new_content: Arc<RoomMessageEventContentWithoutRelation>,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
let replacement_event = self
.inner
.make_edit_event(&event_id, EditedContent::RoomMessage((*new_content).clone()))
.await?;
self.inner.send_queue().send(replacement_event).await?;
Ok(())
}
/// Remove verification requirements for the given users and
/// resend messages that failed to send because their identities were no
/// longer verified (in response to
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity`)
///
/// # Arguments
///
/// * `user_ids` - The list of users identifiers received in the error
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo the send error applies to
pub async fn withdraw_verification_and_resend(
&self,
user_ids: Vec<String>,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let user_ids: Vec<OwnedUserId> =
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
let encryption = self.inner.client().encryption();
for user_id in user_ids {
if let Some(user_identity) = encryption.get_user_identity(&user_id).await? {
user_identity.withdraw_verification().await?;
}
}
send_handle.try_resend().await?;
Ok(())
}
/// Set the local trust for the given devices to `LocalTrust::Ignored`
/// and resend messages that failed to send because said devices are
/// unverified (in response to
/// `SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice`).
/// # Arguments
///
/// * `devices` - The map of users identifiers to device identifiers
/// received in the error
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo the send error applies to
pub async fn ignore_device_trust_and_resend(
&self,
devices: HashMap<String, Vec<String>>,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let encryption = self.inner.client().encryption();
for (user_id, device_ids) in devices.iter() {
let user_id = UserId::parse(user_id)?;
for device_id in device_ids {
let device_id: OwnedDeviceId = device_id.as_str().into();
if let Some(device) = encryption.get_device(&user_id, &device_id).await? {
device.set_local_trust(LocalTrust::Ignored).await?;
}
}
}
send_handle.try_resend().await?;
Ok(())
}
}
/// Generates a `matrix.to` permalink to the given room alias.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn matrix_to_room_alias_permalink(
room_alias: String,
) -> std::result::Result<String, ClientError> {
@@ -749,16 +899,21 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomInfoListener: Sync + Send {
fn call(&self, room_info: RoomInfo);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TypingNotificationsListener: Sync + Send {
fn call(&self, typing_user_ids: Vec<String>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IdentityStatusChangeListener: Sync + Send {
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
}
#[derive(uniffi::Object)]
pub struct RoomMembersIterator {
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
@@ -770,7 +925,7 @@ impl RoomMembersIterator {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomMembersIterator {
fn len(&self) -> u32 {
self.chunk_iterator.len()
@@ -779,7 +934,7 @@ impl RoomMembersIterator {
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
self.chunk_iterator
.next(chunk_size)
.map(|members| members.into_iter().map(|m| m.into()).collect())
.map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect())
}
}
+17
View File
@@ -0,0 +1,17 @@
use matrix_sdk::RoomDisplayName;
/// Verifies the passed `String` matches the expected room alias format:
///
/// This means it's lowercase, with no whitespace chars, has a single leading
/// `#` char and a single `:` separator between the local and domain parts, and
/// the local part only contains characters that can't be percent encoded.
#[matrix_sdk_ffi_macros::export]
fn is_room_alias_format_valid(alias: String) -> bool {
matrix_sdk::utils::is_room_alias_format_valid(alias)
}
/// Transforms a Room's display name into a valid room alias name.
#[matrix_sdk_ffi_macros::export]
fn room_alias_name_from_room_display_name(room_name: String) -> String {
RoomDisplayName::Named(room_name).to_room_alias_name()
}
@@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use ruma::ServerName;
use tokio::sync::RwLock;
use super::RUNTIME;
@@ -68,6 +69,12 @@ impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescriptio
}
}
/// A helper for performing room searches in the room directory.
/// The way this is intended to be used is:
///
/// 1. Register a callback using [`RoomDirectorySearch::results`].
/// 2. Start the room search with [`RoomDirectorySearch::search`].
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
#[derive(uniffi::Object)]
pub struct RoomDirectorySearch {
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
@@ -79,30 +86,51 @@ impl RoomDirectorySearch {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomDirectorySearch {
/// Asks the server for the next page of the current search.
pub async fn next_page(&self) -> Result<(), ClientError> {
let mut inner = self.inner.write().await;
inner.next_page().await?;
Ok(())
}
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
/// Starts a filtered search for the server.
///
/// If the `filter` is not provided it will search for all the rooms.
/// You can specify a `batch_size` to control the number of rooms to fetch
/// per request.
///
/// If the `via_server` is not provided it will search in the current
/// homeserver by default.
///
/// This method will clear the current search results and start a new one.
pub async fn search(
&self,
filter: Option<String>,
batch_size: u32,
via_server_name: Option<String>,
) -> Result<(), ClientError> {
let server = via_server_name.map(ServerName::parse).transpose()?;
let mut inner = self.inner.write().await;
inner.search(filter, batch_size).await?;
inner.search(filter, batch_size, server).await?;
Ok(())
}
/// Get the number of pages that have been loaded so far.
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
let inner = self.inner.read().await;
Ok(inner.loaded_pages() as u32)
}
/// Get whether the search is at the last page.
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
let inner = self.inner.read().await;
Ok(inner.is_at_last_page())
}
/// Registers a callback to receive new search results when starting a
/// search or getting new paginated results.
pub async fn results(
&self,
listener: Box<dyn RoomDirectorySearchEntriesListener>,
@@ -169,7 +197,7 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+14 -7
View File
@@ -11,6 +11,7 @@ use crate::{
#[derive(uniffi::Record)]
pub struct RoomInfo {
id: String,
creator: Option<String>,
/// The room's name from the room state event if received from sync, or one
/// that's been computed otherwise.
display_name: Option<String>,
@@ -23,7 +24,6 @@ pub struct RoomInfo {
is_space: bool,
is_tombstoned: bool,
is_favourite: bool,
is_encrypted: bool,
canonical_alias: Option<String>,
alternative_aliases: Vec<String>,
membership: Membership,
@@ -40,7 +40,7 @@ pub struct RoomInfo {
user_power_levels: HashMap<String, i64>,
highlight_count: u64,
notification_count: u64,
user_defined_notification_mode: Option<RoomNotificationMode>,
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
/// Whether this room has been explicitly marked as unread
@@ -54,6 +54,8 @@ pub struct RoomInfo {
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
/// The currently pinned event ids
pinned_event_ids: Vec<String>,
}
impl RoomInfo {
@@ -65,9 +67,12 @@ impl RoomInfo {
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();
Ok(Self {
id: room.room_id().to_string(),
creator: room.creator().as_ref().map(ToString::to_string),
display_name: room.cached_display_name().map(|name| name.to_string()),
raw_name: room.name(),
topic: room.topic(),
@@ -77,7 +82,6 @@ impl RoomInfo {
is_space: room.is_space(),
is_tombstoned: room.is_tombstoned(),
is_favourite: room.is_favourite(),
is_encrypted: room.is_encrypted().await.unwrap_or(false),
canonical_alias: room.canonical_alias().map(Into::into),
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
membership: room.state().into(),
@@ -87,7 +91,10 @@ impl RoomInfo {
.await
.ok()
.and_then(|details| details.inviter)
.map(Into::into),
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
_ => None,
},
heroes: room.heroes().into_iter().map(Into::into).collect(),
@@ -97,9 +104,8 @@ impl RoomInfo {
user_power_levels,
highlight_count: unread_notification_counts.highlight_count,
notification_count: unread_notification_counts.notification_count,
user_defined_notification_mode: room
.user_defined_notification_mode()
.await
cached_user_defined_notification_mode: room
.cached_user_defined_notification_mode()
.map(Into::into),
has_room_call: room.has_active_room_call(),
active_room_call_participants: room
@@ -111,6 +117,7 @@ impl RoomInfo {
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
})
}
}
+277 -206
View File
@@ -1,38 +1,34 @@
use std::{fmt::Debug, sync::Arc, time::Duration};
#![allow(deprecated)]
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt, TryFutureExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::{
v4::RoomSubscription as RumaRoomSubscription,
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
assign, RoomId,
},
RoomListEntry as MatrixRoomListEntry,
use matrix_sdk::ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
};
use matrix_sdk_ui::{
room_list_service::{
filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_unread, RoomCategory,
},
BoxedFilterFn,
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_unread, BoxedFilterFn, RoomCategory,
},
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;
use crate::{
error::ClientError,
room::Room,
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
utils::AsyncRuntimeDropped,
TaskHandle, RUNTIME,
};
@@ -56,6 +52,8 @@ pub enum RoomListError {
InitializingTimeline { 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:?}")]
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
}
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
@@ -65,7 +63,6 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
match value {
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
UnknownList(list_name) => Self::UnknownList { list_name },
InputCannotBeApplied(_) => Self::InputCannotBeApplied,
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
TimelineAlreadyExists(room_id) => {
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
@@ -84,34 +81,13 @@ impl From<ruma::IdParseError> for RoomListError {
}
}
#[derive(uniffi::Record)]
pub struct RoomListRange {
pub start: u32,
pub end_inclusive: u32,
}
#[derive(uniffi::Enum)]
pub enum RoomListInput {
Viewport { ranges: Vec<RoomListRange> },
}
impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
fn from(value: RoomListInput) -> Self {
match value {
RoomListInput::Viewport { ranges } => Self::Viewport(
ranges.iter().map(|range| range.start..=range.end_inclusive).collect(),
),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomListService {
pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomListService {
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
let state_stream = self.inner.state();
@@ -141,10 +117,6 @@ impl RoomListService {
}))
}
async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> {
self.inner.apply_input(input.into()).await.map(|_| ()).map_err(Into::into)
}
fn sync_indicator(
&self,
delay_before_showing_in_ms: u32,
@@ -164,6 +136,19 @@ impl RoomListService {
}
})))
}
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
let room_ids = room_ids
.into_iter()
.map(|room_id| {
RoomId::parse(&room_id).map_err(|_| RoomListError::InvalidRoomId { error: room_id })
})
.collect::<Result<Vec<_>, _>>()?;
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
Ok(())
}
}
#[derive(uniffi::Object)]
@@ -172,7 +157,7 @@ pub struct RoomList {
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomList {
fn loading_state(
&self,
@@ -192,45 +177,98 @@ impl RoomList {
})
}
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> RoomListEntriesResult {
let (entries, entries_stream) = self.inner.entries();
RoomListEntriesResult {
entries: entries.into_iter().map(Into::into).collect(),
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(entries_stream);
while let Some(diff) = entries_stream.next().await {
listener.on_update(diff.into_iter().map(Into::into).collect());
}
}))),
}
}
fn entries_with_dynamic_adapters(
&self,
self: Arc<Self>,
page_size: u32,
listener: Box<dyn RoomListEntriesListener>,
) -> RoomListEntriesWithDynamicAdaptersResult {
let (entries_stream, dynamic_entries_controller) =
self.inner.entries_with_dynamic_adapters(
page_size.try_into().unwrap(),
self.room_list_service.inner.client().roominfo_update_receiver(),
);
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self.clone();
let utd_hook = self.room_list_service.utd_hook.clone();
RoomListEntriesWithDynamicAdaptersResult {
controller: Arc::new(RoomListDynamicEntriesController::new(
dynamic_entries_controller,
self.room_list_service.inner.client(),
)),
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(entries_stream);
// The following code deserves a bit of explanation.
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
// returns a `Stream` with a lifetime bounds to its `self` (`RoomList`). This is
// problematic here as this `Stream` is returned as part of
// `RoomListEntriesWithDynamicAdaptersResult` but it is not possible to store
// `RoomList` with it inside the `Future` that is run inside the `TaskHandle`
// that consumes this `Stream`. We have a lifetime issue: `RoomList` doesn't
// live long enough!
//
// To solve this issue, the trick is to store the `RoomList` inside the
// `RoomListEntriesWithDynamicAdaptersResult`. Alright, but then we have another
// lifetime issue! `RoomList` cannot move inside this struct because it is
// borrowed by `entries_with_dynamic_adapters`. Indeed, the struct is built
// after the `Stream` is obtained.
//
// To solve this issue, we need to build the struct field by field, starting
// with `this`, and use a reference to `this` to call
// `entries_with_dynamic_adapters`. This is unsafe because a couple of
// invariants must hold, but all this is legal and correct if the invariants are
// properly fulfilled.
while let Some(diff) = entries_stream.next().await {
listener.on_update(diff.into_iter().map(Into::into).collect());
}
}))),
// Create the struct result with uninitialized fields.
let mut result = MaybeUninit::<RoomListEntriesWithDynamicAdaptersResult>::uninit();
let ptr = result.as_mut_ptr();
// Initialize the first field `this`.
//
// SAFETY: `ptr` is correctly aligned, this is guaranteed by `MaybeUninit`.
unsafe {
addr_of_mut!((*ptr).this).write(this);
}
// Get a reference to `this`. It is only borrowed, it's not moved.
let this =
// SAFETY: `ptr` is correct aligned, the `this` field is correctly aligned,
// is dereferenceable and points to a correctly initialized value as done
// in the previous line.
unsafe { addr_of_mut!((*ptr).this).as_ref() }
// SAFETY: `this` contains a non null value.
.unwrap();
// Now we can create `entries_stream` and `dynamic_entries_controller` by
// borrowing `this`, which is going to live long enough since it will live as
// long as `entries_stream` and `dynamic_entries_controller`.
let (entries_stream, dynamic_entries_controller) =
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
// FFI dance to make those values consumable by foreign language, nothing fancy
// here, that's the real code for this method.
let dynamic_entries_controller =
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(entries_stream);
while let Some(diffs) = entries_stream.next().await {
listener.on_update(
diffs
.into_iter()
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
.collect(),
);
}
})));
// Initialize the second field `controller`.
//
// SAFETY: `ptr` is correctly aligned.
unsafe {
addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
}
// Initialize the third and last field `entries_stream`.
//
// SAFETY: `ptr` is correctly aligned.
unsafe {
addr_of_mut!((*ptr).entries_stream).write(entries_stream);
}
// The result is complete, let's return it!
//
// SAFETY: `result` is fully initialized, all its fields have received a valid
// value.
Arc::new(unsafe { result.assume_init() })
}
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
@@ -238,16 +276,22 @@ impl RoomList {
}
}
#[derive(uniffi::Record)]
pub struct RoomListEntriesResult {
pub entries: Vec<RoomListEntry>,
pub entries_stream: Arc<TaskHandle>,
#[derive(uniffi::Object)]
pub struct RoomListEntriesWithDynamicAdaptersResult {
this: Arc<RoomList>,
controller: Arc<RoomListDynamicEntriesController>,
entries_stream: Arc<TaskHandle>,
}
#[derive(uniffi::Record)]
pub struct RoomListEntriesWithDynamicAdaptersResult {
pub controller: Arc<RoomListDynamicEntriesController>,
pub entries_stream: Arc<TaskHandle>,
#[matrix_sdk_ffi_macros::export]
impl RoomListEntriesWithDynamicAdaptersResult {
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
self.controller.clone()
}
fn entries_stream(&self) -> Arc<TaskHandle> {
self.entries_stream.clone()
}
}
#[derive(uniffi::Record)]
@@ -317,65 +361,80 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListServiceState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListLoadingState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
}
#[derive(uniffi::Enum)]
pub enum RoomListEntriesUpdate {
Append { values: Vec<RoomListEntry> },
Append { values: Vec<Arc<RoomListItem>> },
Clear,
PushFront { value: RoomListEntry },
PushBack { value: RoomListEntry },
PushFront { value: Arc<RoomListItem> },
PushBack { value: Arc<RoomListItem> },
PopFront,
PopBack,
Insert { index: u32, value: RoomListEntry },
Set { index: u32, value: RoomListEntry },
Insert { index: u32, value: Arc<RoomListItem> },
Set { index: u32, value: Arc<RoomListItem> },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<RoomListEntry> },
Reset { values: Vec<Arc<RoomListItem>> },
}
impl From<VectorDiff<matrix_sdk::RoomListEntry>> for RoomListEntriesUpdate {
fn from(other: VectorDiff<matrix_sdk::RoomListEntry>) -> Self {
match other {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(Into::into).collect() }
}
impl RoomListEntriesUpdate {
fn from(
vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
match vector_diff {
VectorDiff::Append { values } => Self::Append {
values: values
.into_iter()
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
.collect(),
},
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PushFront { value } => {
Self::PushFront { value: Arc::new(RoomListItem::from(value, utd_hook)) }
}
VectorDiff::PushBack { value } => {
Self::PushBack { value: Arc::new(RoomListItem::from(value, utd_hook)) }
}
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: u32::try_from(index).unwrap(), value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: u32::try_from(index).unwrap(), value: value.into() }
}
VectorDiff::Insert { index, value } => Self::Insert {
index: u32::try_from(index).unwrap(),
value: Arc::new(RoomListItem::from(value, utd_hook)),
},
VectorDiff::Set { index, value } => Self::Set {
index: u32::try_from(index).unwrap(),
value: Arc::new(RoomListItem::from(value, utd_hook)),
},
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
VectorDiff::Truncate { length } => {
Self::Truncate { length: u32::try_from(length).unwrap() }
}
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(Into::into).collect() }
}
VectorDiff::Reset { values } => Self::Reset {
values: values
.into_iter()
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
.collect(),
},
}
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
}
@@ -383,23 +442,20 @@ pub trait RoomListEntriesListener: Send + Sync + Debug {
#[derive(uniffi::Object)]
pub struct RoomListDynamicEntriesController {
inner: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
client: matrix_sdk::Client,
}
impl RoomListDynamicEntriesController {
fn new(
dynamic_entries_controller: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
client: &matrix_sdk::Client,
) -> Self {
Self { inner: dynamic_entries_controller, client: client.clone() }
Self { inner: dynamic_entries_controller }
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomListDynamicEntriesController {
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
self.inner.set_filter(filter)
self.inner.set_filter(kind.into())
}
fn add_one_page(&self) {
@@ -441,33 +497,29 @@ impl From<RoomListFilterCategory> for RoomCategory {
}
}
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
/// a `BoxedFilterFn`.
struct FilterWrapper(BoxedFilterFn);
impl FilterWrapper {
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
fn from(value: RoomListEntriesDynamicFilterKind) -> Self {
use RoomListEntriesDynamicFilterKind as Kind;
match value {
Kind::All { filters } => Self(Box::new(new_filter_all(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
Kind::Any { filters } => Self(Box::new(new_filter_any(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
Kind::NonLeft => Self(Box::new(new_filter_non_left(client))),
Kind::Joined => Self(Box::new(new_filter_joined(client))),
Kind::Unread => Self(Box::new(new_filter_unread(client))),
Kind::Favourite => Self(Box::new(new_filter_favourite(client))),
Kind::Invite => Self(Box::new(new_filter_invite(client))),
Kind::Category { expect } => Self(Box::new(new_filter_category(client, expect.into()))),
Kind::None => Self(Box::new(new_filter_none())),
Kind::All { filters } => Box::new(new_filter_all(
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
)),
Kind::Any { filters } => Box::new(new_filter_any(
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
)),
Kind::NonLeft => Box::new(new_filter_non_left()),
Kind::Joined => Box::new(new_filter_joined()),
Kind::Unread => Box::new(new_filter_unread()),
Kind::Favourite => Box::new(new_filter_favourite()),
Kind::Invite => Box::new(new_filter_invite()),
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
Kind::None => Box::new(new_filter_none()),
Kind::NormalizedMatchRoomName { pattern } => {
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))
Box::new(new_filter_normalized_match_room_name(&pattern))
}
Kind::FuzzyMatchRoomName { pattern } => {
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
Box::new(new_filter_fuzzy_match_room_name(&pattern))
}
}
}
@@ -479,7 +531,16 @@ pub struct RoomListItem {
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
impl RoomListItem {
fn from(
value: matrix_sdk_ui::room_list_service::Room,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
Self { inner: Arc::new(value), utd_hook }
}
}
#[matrix_sdk_ffi_macros::export]
impl RoomListItem {
fn id(&self) -> String {
self.inner.id().to_string()
@@ -504,14 +565,82 @@ impl RoomListItem {
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
Ok(RoomInfo::new(self.inner.inner_room()).await?)
}
/// The room's current membership state.
fn membership(&self) -> Membership {
self.inner.inner_room().state().into()
}
/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited.
///
/// ⚠️ Holding on to this room instance after it has been joined is not
/// safe. Use `full_room` instead.
#[deprecated(note = "Please use `preview_room` instead.")]
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Invited) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited],
actual: self.membership(),
});
}
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited or knocked rooms.
///
/// An error will be returned if the room is in a state other than invited
/// or knocked.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
// Validate internal room state.
let membership = self.membership();
if !matches!(membership, Membership::Invited | Membership::Knocked) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited, Membership::Knocked],
actual: membership,
}
.into());
}
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() {
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
(room_or_alias_id, server_names)
};
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
/// Build a full `Room` FFI object, filling its associated timeline.
///
/// If its internal timeline hasn't been initialized, it'll fail.
/// An error will be returned if the room is a state different than joined
/// or if its internal timeline hasn't been initialized.
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
if let Some(timeline) = self.inner.timeline() {
Ok(Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
@@ -574,66 +703,8 @@ impl RoomListItem {
self.inner.is_encrypted().await.unwrap_or(false)
}
fn subscribe(&self, settings: Option<RoomSubscription>) {
self.inner.subscribe(settings.map(Into::into));
}
fn unsubscribe(&self) {
self.inner.unsubscribe();
}
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
}
}
#[derive(Clone, Debug, uniffi::Enum)]
pub enum RoomListEntry {
Empty,
Invalidated { room_id: String },
Filled { room_id: String },
}
impl From<MatrixRoomListEntry> for RoomListEntry {
fn from(value: MatrixRoomListEntry) -> Self {
(&value).into()
}
}
impl From<&MatrixRoomListEntry> for RoomListEntry {
fn from(value: &MatrixRoomListEntry) -> Self {
match value {
MatrixRoomListEntry::Empty => Self::Empty,
MatrixRoomListEntry::Filled(room_id) => Self::Filled { room_id: room_id.to_string() },
MatrixRoomListEntry::Invalidated(room_id) => {
Self::Invalidated { room_id: room_id.to_string() }
}
}
}
}
#[derive(uniffi::Record)]
pub struct RequiredState {
pub key: String,
pub value: String,
}
#[derive(uniffi::Record)]
pub struct RoomSubscription {
pub required_state: Option<Vec<RequiredState>>,
pub timeline_limit: Option<u32>,
pub include_heroes: Option<bool>,
}
impl From<RoomSubscription> for RumaRoomSubscription {
fn from(val: RoomSubscription) -> Self {
assign!(RumaRoomSubscription::default(), {
required_state: val.required_state.map(|r|
r.into_iter().map(|s| (s.key.into(), s.value)).collect()
).unwrap_or_default(),
timeline_limit: val.timeline_limit.map(|u| u.into()),
include_heroes: val.include_heroes,
})
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event().await.map(Into::into)
}
}
@@ -643,7 +714,7 @@ pub struct UnreadNotificationsCount {
notification_count: u32,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl UnreadNotificationsCount {
fn highlight_count(&self) -> u32 {
self.highlight_count
+37 -20
View File
@@ -1,7 +1,7 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::UserId;
use crate::error::ClientError;
use crate::error::{ClientError, NotYetImplemented};
#[derive(Clone, uniffi::Enum)]
pub enum MembershipState {
@@ -19,43 +19,58 @@ pub enum MembershipState {
/// The user has left.
Leave,
/// A custom membership state value.
Custom { value: String },
}
impl From<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self {
impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
type Error = NotYetImplemented;
fn try_from(
m: matrix_sdk::ruma::events::room::member::MembershipState,
) -> Result<Self, Self::Error> {
match m {
matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban,
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
MembershipState::Invite
matrix_sdk::ruma::events::room::member::MembershipState::Ban => {
Ok(MembershipState::Ban)
}
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
Ok(MembershipState::Invite)
}
matrix_sdk::ruma::events::room::member::MembershipState::Join => {
Ok(MembershipState::Join)
}
matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join,
matrix_sdk::ruma::events::room::member::MembershipState::Knock => {
MembershipState::Knock
Ok(MembershipState::Knock)
}
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
MembershipState::Leave
Ok(MembershipState::Leave)
}
matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => {
Ok(MembershipState::Custom { value: m.to_string() })
}
_ => {
tracing::warn!("Other membership state change not yet implemented");
Err(NotYetImplemented)
}
_ => unimplemented!(
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
),
}
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
RoomMemberRole::suggested_role_for_power_level(power_level)
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
// It's not possible to expose methods on an Enum through Uniffi ☹️
role.suggested_power_level()
}
/// Generates a `matrix.to` permalink to the given userID.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
let user_id = UserId::parse(user_id)?;
Ok(user_id.matrix_to_uri().to_string())
@@ -74,18 +89,20 @@ pub struct RoomMember {
pub suggested_role_for_power_level: RoomMemberRole,
}
impl From<SdkRoomMember> for RoomMember {
fn from(m: SdkRoomMember) -> Self {
RoomMember {
impl TryFrom<SdkRoomMember> for RoomMember {
type Error = NotYetImplemented;
fn try_from(m: SdkRoomMember) -> Result<Self, Self::Error> {
Ok(RoomMember {
user_id: m.user_id().to_string(),
display_name: m.display_name().map(|s| s.to_owned()),
avatar_url: m.avatar_url().map(|a| a.to_string()),
membership: m.membership().clone().into(),
membership: m.membership().clone().try_into()?,
is_name_ambiguous: m.name_ambiguous(),
power_level: m.power_level(),
normalized_power_level: m.normalized_power_level(),
is_ignored: m.is_ignored(),
suggested_role_for_power_level: m.suggested_role_for_power_level(),
}
})
}
}
+116 -30
View File
@@ -1,9 +1,73 @@
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
use ruma::space::SpaceRoomJoinRule;
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
utils::AsyncRuntimeDropped,
};
/// A room preview for a room. It's intended to be used to represent rooms that
/// aren't joined yet.
#[derive(uniffi::Object)]
pub struct RoomPreview {
inner: SdkRoomPreview,
client: AsyncRuntimeDropped<Client>,
}
#[matrix_sdk_ffi_macros::export]
impl RoomPreview {
/// Returns the room info the preview contains.
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
let info = &self.inner;
Ok(RoomPreviewInfo {
room_id: info.room_id.to_string(),
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
name: info.name.clone(),
topic: info.topic.clone(),
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
num_joined_members: info.num_joined_members,
num_active_members: info.num_active_members,
room_type: info.room_type.as_ref().into(),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
})
}
/// Leave the room if the room preview state is either joined, invited or
/// knocked.
///
/// Will return an error otherwise.
pub async fn leave(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.leave().await.map_err(Into::into)
}
/// Get the user who created the invite, if any.
pub async fn inviter(&self) -> Option<RoomMember> {
let room = self.client.get_room(&self.inner.room_id)?;
let invite_details = room.invite_details().await.ok()?;
invite_details.inviter.and_then(|m| m.try_into().ok())
}
}
impl RoomPreview {
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
Self { client, inner }
}
}
/// The preview of a room, be it invited/joined/left, or not.
#[derive(uniffi::Record)]
pub struct RoomPreview {
pub struct RoomPreviewInfo {
/// The room id for this room.
pub room_id: String,
/// The canonical alias for the room.
@@ -16,38 +80,60 @@ pub struct RoomPreview {
pub avatar_url: Option<String>,
/// The number of joined members.
pub num_joined_members: u64,
/// The number of active members, if known (joined + invited).
pub num_active_members: Option<u64>,
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: Option<String>,
pub room_type: RoomType,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
/// Is the room joined by the current user?
pub is_joined: bool,
/// Is the current user invited to this room?
pub is_invited: bool,
/// is the join rule public for this room?
pub is_public: bool,
/// Can we knock (or restricted-knock) to this room?
pub can_knock: bool,
/// The membership state for the current user, if known.
pub membership: Option<Membership>,
/// The join rule for this room (private, public, knock, etc.).
pub join_rule: JoinRule,
/// Whether the room is direct or not, if known.
pub is_direct: Option<bool>,
}
impl RoomPreview {
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
Self {
room_id: preview.room_id.to_string(),
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
name: preview.name,
topic: preview.topic,
avatar_url: preview.avatar_url.map(|url| url.to_string()),
num_joined_members: preview.num_joined_members,
room_type: preview.room_type.map(|room_type| room_type.to_string()),
is_history_world_readable: preview.is_world_readable,
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
can_knock: matches!(
preview.join_rule,
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
),
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
type Error = ();
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
Ok(match join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
SpaceRoomJoinRule::Private => JoinRule::Private,
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
SpaceRoomJoinRule::Public => JoinRule::Public,
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
_ => {
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
return Err(());
}
})
}
}
/// The type of room for a [`RoomPreviewInfo`].
#[derive(Debug, Clone, uniffi::Enum)]
pub enum RoomType {
/// It's a plain chat room.
Room,
/// It's a space that can group several rooms.
Space,
/// It's a custom implementation.
Custom { value: String },
}
impl From<Option<&RumaRoomType>> for RoomType {
fn from(value: Option<&RumaRoomType>) -> Self {
match value {
Some(RumaRoomType::Space) => RoomType::Space,
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
// SAFETY: this was checked in the match branch above
value: value.unwrap().to_string(),
},
_ => RoomType::Room,
}
}
}
+111 -44
View File
@@ -54,12 +54,43 @@ use tracing::info;
use crate::{
error::{ClientError, MediaInfoError},
helpers::unwrap_or_clone_arc,
timeline::MessageContent,
utils::u64_to_uint,
};
#[derive(uniffi::Enum)]
pub enum AuthData {
/// Password-based authentication (`m.login.password`).
Password { password_details: AuthDataPasswordDetails },
}
#[derive(uniffi::Record)]
pub struct AuthDataPasswordDetails {
/// One of the user's identifiers.
identifier: String,
/// The plaintext password.
password: String,
}
impl From<AuthData> for ruma::api::client::uiaa::AuthData {
fn from(value: AuthData) -> ruma::api::client::uiaa::AuthData {
match value {
AuthData::Password { password_details } => {
let user_id = ruma::UserId::parse(password_details.identifier).unwrap();
ruma::api::client::uiaa::AuthData::Password(ruma::api::client::uiaa::Password::new(
user_id.into(),
password_details.password,
))
}
}
}
}
/// Parse a matrix entity from a given URI, be it either
/// a `matrix.to` link or a `matrix:` URI
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
return Some(MatrixEntity {
@@ -123,33 +154,33 @@ impl From<&RumaMatrixId> for MatrixId {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_new(
msgtype: MessageType,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_markdown(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_markdown_as_emote(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_html(
body: String,
html_body: String,
@@ -159,7 +190,7 @@ pub fn message_event_content_from_html(
)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_html_as_emote(
body: String,
html_body: String,
@@ -197,6 +228,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
}
}
#[derive(Clone)]
pub struct Mentions {
pub user_ids: Vec<String>,
pub room: bool,
@@ -230,6 +262,23 @@ pub enum MessageType {
Other { msgtype: String, body: String },
}
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
/// If the filename field is present in a media message, clients should treat
/// body as a caption instead of a file name. Otherwise, the body is the
/// file name.
///
/// So:
/// - if a media has a filename and a caption, the body is the caption, filename
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = serde_json::Error;
@@ -241,35 +290,39 @@ impl TryFrom<MessageType> for RumaMessageType {
}))
}
MessageType::Image { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
RumaImageMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Image(event_content)
}
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
RumaAudioMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Audio(event_content)
}
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
RumaVideoMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Video(event_content)
}
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
RumaFileMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::File(event_content)
}
MessageType::Notice { content } => {
@@ -303,18 +356,18 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
@@ -323,18 +376,18 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
},
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
@@ -408,18 +461,20 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<ImageInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<AudioInfo>,
pub audio: Option<UnstableAudioDetailsContent>,
@@ -428,18 +483,20 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<VideoInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
}
@@ -804,7 +861,7 @@ impl From<&RumaFileInfo> for FileInfo {
}
}
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum PollKind {
Disclosed,
Undisclosed,
@@ -831,3 +888,13 @@ impl From<RumaPollKind> for PollKind {
}
}
}
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
/// [`MessageContent`] value.
#[matrix_sdk_ffi_macros::export]
pub fn content_without_relation_from_message(
message: MessageContent,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
let msg_type = message.msg_type.try_into()?;
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type)))
}
@@ -1,15 +1,16 @@
use std::sync::{Arc, RwLock};
use anyhow::Context as _;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest},
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
};
use ruma::UserId;
use tracing::{error, info};
use super::RUNTIME;
use crate::error::ClientError;
@@ -20,7 +21,7 @@ pub struct SessionVerificationEmoji {
description: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationEmoji {
pub fn symbol(&self) -> String {
self.symbol.clone()
@@ -37,8 +38,20 @@ pub enum SessionVerificationData {
Decimals { values: Vec<u16> },
}
#[uniffi::export(callback_interface)]
/// Details about the incoming verification request
#[derive(Debug, uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
flow_id: String,
device_id: String,
display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: u64,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: SessionVerificationData);
@@ -58,19 +71,53 @@ pub struct SessionVerificationController {
sas_verification: Arc<RwLock<Option<SasVerification>>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationController {
pub async fn is_verified(&self) -> Result<bool, ClientError> {
let device =
self.encryption.get_own_device().await?.context("Our own device is missing")?;
Ok(device.is_cross_signed_by_owner())
}
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
*self.delegate.write().unwrap() = delegate;
}
/// Set this particular request as the currently active one and register for
/// events pertaining it.
/// * `sender_id` - The user requesting verification.
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
pub async fn acknowledge_verification_request(
&self,
sender_id: String,
flow_id: String,
) -> Result<(), ClientError> {
let sender_id = UserId::parse(sender_id.clone())?;
let verification_request = self
.encryption
.get_verification_request(&sender_id, flow_id)
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Accept the previously acknowledged verification request
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification_request) = verification_request {
let methods = vec![VerificationMethod::SasV1];
verification_request.accept_with_methods(methods).await?;
}
Ok(())
}
/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
@@ -78,30 +125,41 @@ impl SessionVerificationController {
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request);
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Transition the current verification request into a SAS verification
/// flow.
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
match verification.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
match verification_request.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
}
}
@@ -109,31 +167,37 @@ impl SessionVerificationController {
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn approve_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.confirm().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.confirm().await?)
}
/// Reject the short auth string
pub async fn decline_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.mismatch().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.mismatch().await?)
}
/// Cancel the current verification request
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
verification.cancel().await?;
}
Ok(())
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
Ok(verification_request.cancel().await?)
}
}
@@ -149,58 +213,88 @@ impl SessionVerificationController {
}
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
match event {
// TODO: Use the changes stream for this as well once we expose
// VerificationRequest::changes() in the main crate.
AnyToDeviceEvent::KeyVerificationStart(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(verification) = self
.encryption
.get_verification(
self.user_identity.user_id(),
event.content.transaction_id.as_str(),
)
.await
{
if let Some(sas_verification) = verification.sas() {
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);
if sas_verification.accept().await.is_ok() {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let Some(request) = self
.encryption
.get_verification_request(&event.sender, &event.content.transaction_id)
.await
else {
error!("Failed retrieving verification request");
return;
};
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(),
});
}
}
}
async fn listen_to_verification_request_changes(
verification_request: VerificationRequest,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
delegate: Delegate,
) {
let mut stream = verification_request.changes();
while let Some(state) = stream.next().await {
match state {
VerificationRequestState::Transitioned { verification } => {
let Some(verification) = verification.sas() else {
error!("Invalid, non-sas verification flow. Returning.");
return;
};
*sas_verification.write().unwrap() = Some(verification.clone());
if verification.accept().await.is_ok() {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let delegate = delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
verification,
delegate,
));
} else if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_fail()
}
}
}
AnyToDeviceEvent::KeyVerificationReady(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
VerificationRequestState::Ready { .. } => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_accept_verification_request()
}
}
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_accept_verification_request()
VerificationRequestState::Cancelled(..) => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_cancel();
}
}
_ => {}
}
_ => (),
}
}
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
match &*self.verification_request.read().unwrap() {
Some(verification) => verification.flow_id() == transaction_id,
None => false,
}
}
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
let mut stream = sas.changes();
while let Some(state) = stream.next().await {
+6 -6
View File
@@ -51,7 +51,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
fn on_update(&self, state: SyncServiceState);
}
@@ -62,7 +62,7 @@ pub struct SyncService {
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncService {
pub fn room_list_service(&self) -> Arc<RoomListService> {
Arc::new(RoomListService {
@@ -110,11 +110,11 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock(app_identifier);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
@@ -153,7 +153,7 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
+1 -1
View File
@@ -17,7 +17,7 @@ impl TaskHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TaskHandle {
// Cancel a task handle.
pub fn cancel(&self) {
+98 -84
View File
@@ -15,52 +15,53 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use ruma::events::{
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
FullStateEventContent,
};
use tracing::warn;
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::{room::MediaSource, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
match value {
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContentKind::Sticker {
TimelineItemContent::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
source: Arc::new(MediaSource::from(content.source.clone())),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::CallNotify => TimelineItemContentKind::CallNotify,
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: if let FullStateEventContent::Original { content, .. } =
membership.content()
{
content.displayname.clone()
} else {
None
},
change: membership.change().map(Into::into),
},
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
reason,
}
}
Content::ProfileChange(profile) => {
let (display_name, prev_display_name) = profile
.displayname_change()
@@ -75,42 +76,74 @@ impl TimelineItemContent {
)
})
.unzip();
TimelineItemContentKind::ProfileChange {
TimelineItemContent::ProfileChange {
display_name: display_name.flatten(),
prev_display_name: prev_display_name.flatten(),
avatar_url: avatar_url.flatten(),
prev_avatar_url: prev_avatar_url.flatten(),
}
}
Content::OtherState(state) => TimelineItemContentKind::State {
Content::OtherState(state) => TimelineItemContent::State {
state_key: state.state_key().to_owned(),
content: state.content().into(),
},
Content::FailedToParseMessageLike { event_type, error } => {
TimelineItemContentKind::FailedToParseMessageLike {
TimelineItemContent::FailedToParseMessageLike {
event_type: event_type.to_string(),
error: error.to_string(),
}
}
Content::FailedToParseState { event_type, state_key, error } => {
TimelineItemContentKind::FailedToParseState {
TimelineItemContent::FailedToParseState {
event_type: event_type.to_string(),
state_key: state_key.to_string(),
state_key,
error: error.to_string(),
}
}
}
}
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub in_reply_to: Option<Arc<InReplyToDetails>>,
pub thread_root: Option<String>,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
Self {
msg_type: value.msgtype().clone().into(),
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum TimelineItemContent {
Message {
content: MessageContent,
},
RedactedMessage,
Sticker {
body: String,
@@ -135,6 +168,7 @@ pub enum TimelineItemContentKind {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
@@ -158,36 +192,6 @@ pub enum TimelineItemContentKind {
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
pub fn msgtype(&self) -> MessageType {
self.0.msgtype().clone().into()
}
pub fn body(&self) -> String {
self.0.msgtype().body().to_owned()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
self.0.in_reply_to().map(InReplyToDetails::from)
}
pub fn is_threaded(&self) -> bool {
self.0.is_threaded()
}
pub fn is_edited(&self) -> bool {
self.0.is_edited()
}
pub fn content(&self) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone()))
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
@@ -199,14 +203,25 @@ impl InReplyToDetails {
}
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(event.content().to_owned())),
content: event.content().clone().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
@@ -219,11 +234,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
}
}
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
@@ -267,7 +282,6 @@ impl EncryptedMessage {
#[derive(Clone, uniffi::Record)]
pub struct Reaction {
pub key: String,
pub count: u64,
pub senders: Vec<ReactionSenderData>,
}
@@ -337,7 +351,7 @@ pub enum OtherState {
RoomHistoryVisibility,
RoomJoinRules,
RoomName { name: Option<String> },
RoomPinnedEvents,
RoomPinnedEvents { change: RoomPinnedEventsChange },
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
RoomServerAcl,
RoomThirdPartyInvite { display_name: Option<String> },
@@ -380,7 +394,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
};
Self::RoomName { name }
}
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
Content::RoomPowerLevels(c) => match c {
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
users: power_level_user_changes(content, prev_content)
@@ -418,15 +432,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
#[derive(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContentKind {
impl From<PollResult> for TimelineItemContent {
fn from(value: PollResult) -> Self {
TimelineItemContentKind::Poll {
TimelineItemContent::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
+2 -2
View File
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// level + target) it is called with. Please make sure that the number of
/// different combinations of those parameters this can be called with is
/// constant in the final executable.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
Mutex::new(BTreeMap::new());
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
#[derive(uniffi::Object)]
pub struct Span(tracing::Span);
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Span {
/// Create a span originating at the given callsite (file, line and column).
///
+45
View File
@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::TOKIO1 as RUNTIME;
use ruma::UInt;
use tracing::warn;
@@ -21,3 +24,45 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt {
UInt::MAX
})
}
/// Tiny wrappers for data types that must be dropped in the context of an async
/// runtime.
///
/// This is useful whenever such a data type may transitively call some
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
/// at the moment), like done in some `deadpool` drop impls.
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
impl<T> AsyncRuntimeDropped<T> {
/// Create a new wrapper for this type that will be dropped under an async
/// runtime.
pub fn new(val: T) -> Self {
Self(ManuallyDrop::new(val))
}
}
impl<T> Drop for AsyncRuntimeDropped<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.0);
}
}
}
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
impl<T> Deref for AsyncRuntimeDropped<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
+128 -26
View File
@@ -5,6 +5,7 @@ use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter},
};
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::{room::Room, RUNTIME};
@@ -15,7 +16,7 @@ pub struct WidgetDriverAndHandle {
pub handle: Arc<WidgetDriverHandle>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
Ok(WidgetDriverAndHandle {
@@ -29,7 +30,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
#[derive(uniffi::Object)]
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl WidgetDriver {
pub async fn run(
&self,
@@ -96,7 +97,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
/// * `room` - A matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to adapt
/// to the client. e.g. language, font-scale...
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
pub async fn generate_webview_url(
widget_settings: WidgetSettings,
room: Arc<Room>,
@@ -241,7 +242,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
///
/// * `props` - A struct containing the configuration parameters for a element
/// call widget.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn new_virtual_element_call_widget(
props: VirtualElementCallWidgetOptions,
) -> Result<WidgetSettings, ParseError> {
@@ -261,42 +262,84 @@ pub fn new_virtual_element_call_widget(
/// Editing and extending the capabilities from this function is also possible,
/// but should only be done as temporal workarounds until this function is
/// adjusted
#[uniffi::export]
pub fn get_element_call_required_permissions(own_user_id: String) -> WidgetCapabilities {
#[matrix_sdk_ffi_macros::export]
pub fn get_element_call_required_permissions(
own_user_id: String,
own_device_id: String,
) -> WidgetCapabilities {
use ruma::events::StateEventType;
let read_send = vec![
// To read and send rageshake requests from other room members
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read and send encryption keys
// TODO change this to the appropriate to-device version once ready
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
// To read and send custom EC reactions. They are different to normal `m.reaction`
// because they can be send multiple times to the same event.
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.reaction".to_owned(),
},
// This allows send raise hand reactions.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::Reaction.to_string(),
},
// This allows to detect if someone does not raise their hand anymore.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::RoomRedaction.to_string(),
},
];
WidgetCapabilities {
read: vec![
// TODO: we really should not have this permission in here, since it is not used
// anymore. The only reason `org.matrix.msc3401.call` is still here is to
// not break current EC deployments. (EC still expects to get this
// permission even though its not using it.) https://github.com/element-hq/element-call/pull/2399 needs to be merged and deployed
WidgetEventFilter::StateWithType { event_type: "org.matrix.msc3401.call".to_owned() },
// To compute the current state of the matrixRTC session.
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.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.
WidgetEventFilter::StateWithType {
event_type: StateEventType::RoomEncryption.to_string(),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
// This allows the widget to check the room version, so it can know about
// version-specific auth rules (namely MSC3779).
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
]
.into_iter()
.chain(read_send.clone())
.collect(),
send: vec![
// To send the call participation state event (main MatrixRTC event).
// This is required for legacy state events (using only one event for all devices with
// a membership array). TODO: remove once legacy call member events are
// sunset.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: own_user_id.clone(),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
// `delayed_event`` version for session memberhips
// [MSC3779](https://github.com/matrix-org/matrix-spec-proposals/pull/3779), with no leading underscore.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("{own_user_id}_{own_device_id}"),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
// The same as above but with an underscore.
// To work around the issue that state events starting with `@` have to be matrix id's
// but we use mxId+deviceId.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}"),
},
],
]
.into_iter()
.chain(read_send)
.collect(),
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
}
}
@@ -326,7 +369,7 @@ impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
#[derive(uniffi::Object)]
pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl WidgetDriverHandle {
/// Receive a message from the widget driver.
///
@@ -358,6 +401,10 @@ pub struct WidgetCapabilities {
/// This means clients should not offer to open the widget in a separate
/// browser/tab/webview that is not connected to the postmessage widget-api.
pub requires_client: bool,
/// This allows the widget to ask the client to update delayed events.
pub update_delayed_event: bool,
/// This allows the widget to send events with a delay.
pub send_delayed_event: bool,
}
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
@@ -366,6 +413,8 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
read: value.read.into_iter().map(Into::into).collect(),
send: value.send.into_iter().map(Into::into).collect(),
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
}
}
}
@@ -376,12 +425,14 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
read: value.read.into_iter().map(Into::into).collect(),
send: value.send.into_iter().map(Into::into).collect(),
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
}
}
}
/// Different kinds of filters that could be applied to the timeline events.
#[derive(uniffi::Enum)]
#[derive(uniffi::Enum, Clone)]
pub enum WidgetEventFilter {
/// Matches message-like events with the given `type`.
MessageLikeWithType { event_type: String },
@@ -433,7 +484,7 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait WidgetCapabilitiesProvider: Send + Sync {
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
}
@@ -504,3 +555,54 @@ impl From<url::ParseError> for ParseError {
}
}
}
#[cfg(test)]
mod tests {
use matrix_sdk::widget::Capabilities;
use super::get_element_call_required_permissions;
#[test]
fn element_call_permissions_are_correct() {
let widget_cap = get_element_call_required_permissions(
"@my_user:my_domain.org".to_owned(),
"ABCDEFGHI".to_owned(),
);
// We test two things:
// Converting the WidgetCapability (ffi struct) to Capabilities (rust sdk
// struct)
let cap = Into::<Capabilities>::into(widget_cap);
// Converting Capabilities (rust sdk struct) to a json list.
let cap_json_repr = serde_json::to_string(&cap).unwrap();
// Converting to a Vec<String> allows to check if the required elements exist
// without breaking the test each time the order of permissions might
// change.
let permission_array: Vec<String> = serde_json::from_str(&cap_json_repr).unwrap();
let cap_assert = |capability: &str| {
assert!(
permission_array.contains(&capability.to_owned()),
"The \"{}\" capability was missing from the element call capability list.",
capability
);
};
cap_assert("io.element.requires_client");
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.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.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
}
}
+47
View File
@@ -0,0 +1,47 @@
# This git-cliff configuration file is used to generate weekly reports for This
# Week in Matrix amongst others.
[changelog]
header = """
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
"""
body = """
{% for commit in commits %}
{% set_global commit_message = commit.message -%}
{% for footer in commit.footers -%}
{% if footer.token | lower == "changelog" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "breaking-change" -%}
{% set_global commit_message = footer.value -%}
{% endif -%}
{% endfor -%}
- {{ commit_message | upper_first }}
{% endfor %}
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = true
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
]
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore", skip = true },
{ message = "^style", group = "Styling", skip = true },
{ message = "^test", skip = true },
{ message = "^ci", skip = true },
]
filter_commits = true
tag_pattern = "[0-9]*"
skip_tags = ""
ignore_tags = ""
date_order = false
sort_commits = "newest"
+91
View File
@@ -0,0 +1,91 @@
# This git-cliff configuration file is used to generate release reports.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{% set_global commit_message = commit.message -%}
{% set_global breaking = commit.breaking -%}
{% for footer in commit.footers -%}
{% if footer.token | lower == "changelog" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "breaking-change" -%}
{% set_global commit_message = footer.value -%}
{% elif footer.token | lower == "security-impact" -%}
{% set_global security_impact = footer.value -%}
{% elif footer.token | lower == "cve" -%}
{% set_global cve = footer.value -%}
{% elif footer.token | lower == "github-advisory" -%}
{% set_global github_advisory = footer.value -%}
{% endif -%}
{% endfor -%}
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
{% if security_impact -%}
(\
*{{ security_impact | upper_first }}*\
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
)
{% endif -%}
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ footer = "Security-Impact:", group = "Security" },
{ footer = "CVE:", group = "Security" },
{ footer = "GitHub-Advisory:", group = "Security" },
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore", skip = true },
{ message = "^style", group = "Styling", skip = true },
{ message = "^test", skip = true },
{ message = "^ci", skip = true },
]
# forbid parsers from skipping breaking changes
protect_breaking_commits = true
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "[0-9]*"
# regex for skipping tags
skip_tags = ""
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
+72 -6
View File
@@ -1,15 +1,81 @@
# unreleased
# Changelog
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Bug Fixes
- Add more invalid characters for room aliases.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- `AmbiguityCache` contains the room member's user ID.
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
request an animated thumbnail They both take a `MediaThumbnailSettings`
instead of `MediaThumbnailSize`.
- Consider knocked members to be part of the room for display name
disambiguation.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
- Allow aborting media uploads.
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
### Refactor
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
- Rename `AmbiguityMap` to `DisplayNameUsers`.
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
- `AmbiguityCache` contains the room member's user ID
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
don't acquire the lock for every room they read.
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- `Client::get_stripped_rooms` has finally been removed.
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
- [**breaking**] The `StateStore` methods to access data in the media cache
where moved to a separate `EventCacheStore` trait.
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
# 0.7.0
+19 -6
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.7.0"
version = "0.8.0"
[package.metadata.docs.rs]
all-features = true
@@ -21,9 +21,19 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
message-ids = ["matrix-sdk-crypto?/message-ids"]
experimental-sliding-sync = ["ruma/unstable-msc3575"]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi"]
experimental-sliding-sync = [
"ruma/unstable-msc3575",
"ruma/unstable-msc4186",
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
# helpers for testing features build upon this
testing = [
@@ -40,6 +50,7 @@ assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.4.0", features = ["serde"] }
decancer = "3.2.4"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
@@ -50,7 +61,9 @@ matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867"] }
regex = "1.11.0"
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
unicode-normalization = "0.1.24"
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
@@ -66,7 +79,7 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
web-time = "1.1.0"
similar-asserts = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -17,7 +17,10 @@
use std::fmt;
pub use matrix_sdk_common::debug::*;
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
use ruma::{
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
serde::Raw,
};
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
/// that only prints the event type of each item.
@@ -46,6 +49,20 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
}
}
/// A wrapper around a knocked on 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 `knock_state`.
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KnockedRoom")
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
.finish()
}
}
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
@@ -14,9 +14,11 @@
//! SDK-specific variations of response types from Ruma.
use std::{collections::BTreeMap, fmt, iter};
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
pub use matrix_sdk_common::deserialized_responses::*;
use once_cell::sync::Lazy;
use regex::Regex;
use ruma::{
events::{
room::{
@@ -31,6 +33,7 @@ use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
@@ -67,6 +70,178 @@ pub struct AmbiguityChanges {
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::MXID_PATTERN)
.expect("We should be able to create a regex from our static MXID pattern")
});
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
.expect("We should be able to create a regex from our static left-to-right pattern")
});
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
.expect("We should be able to create a regex from our static hidden characters pattern")
});
/// Regex to match `i` characters.
///
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
});
/// Regex to match `0` characters.
///
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
});
/// Regex to match a couple of dot-like characters, also matches an actual dot.
///
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
/// ambiguous.
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
});
/// A high-level wrapper for strings representing display names.
///
/// This wrapper provides attempts to determine whether a display name
/// contains characters that could make it ambiguous or easily confused
/// with similar names.
///
///
/// # Examples
///
/// ```
/// use matrix_sdk_base::deserialized_responses::DisplayName;
///
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
///
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
/// ```
///
/// ```
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
/// let display_name = DisplayName::new("@alice:localhost");
///
/// // The display name looks like an MXID, which makes it ambiguous.
/// assert!(display_name.is_inherently_ambiguous());
/// ```
#[derive(Debug, Clone, Eq)]
pub struct DisplayName {
raw: String,
decancered: Option<String>,
}
impl Hash for DisplayName {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(decancered) = &self.decancered {
decancered.hash(state);
} else {
self.raw.hash(state);
}
}
}
impl PartialEq for DisplayName {
fn eq(&self, other: &Self) -> bool {
match (self.decancered.as_deref(), other.decancered.as_deref()) {
(None, None) => self.raw == other.raw,
(None, Some(_)) | (Some(_), None) => false,
(Some(this), Some(other)) => this == other,
}
}
}
impl DisplayName {
/// Regex pattern matching an MXID.
const MXID_PATTERN: &str = "@.+[:.].+";
/// Regex pattern matching some left-to-right formatting marks:
/// * LTR and RTL marks U+200E and U+200F
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
/// Regex pattern matching bunch of unicode control characters and otherwise
/// misleading/invisible characters.
///
/// This includes:
/// * various width spaces U+2000 - U+200D
/// * Combining characters U+0300 - U+036F
/// * Blank/invisible characters (U2800, U2062-U2063)
/// * Arabic Letter RTL mark U+061C
/// * Zero width no-break space (BOM) U+FEFF
const HIDDEN_CHARACTERS_PATTERN: &str =
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
/// Creates a new [`DisplayName`] from the given raw string.
///
/// The raw display name is transformed into a Unicode-normalized form, with
/// common confusable characters removed to reduce ambiguity.
///
/// **Note**: If removing confusable characters fails,
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
/// [`DisplayName::as_normalized_str()`] will return `None.
pub fn new(raw: &str) -> Self {
let normalized = raw.nfd().collect::<String>();
let replaced = DOT_REGEX.replace_all(&normalized, ":");
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
// We re-run the dot replacement because decancer normalized a lot of weird
// characets into a `.`, it just doesn't do that for /u{1d16d}.
let replaced = DOT_REGEX.replace_all(&replaced, ":");
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
replaced.to_string()
});
Self { raw: raw.to_owned(), decancered }
}
/// Is this display name considered to be ambiguous?
///
/// If the display name has cancer (i.e. fails normalisation or has a
/// different normalised form) or looks like an MXID, then it's ambiguous.
pub fn is_inherently_ambiguous(&self) -> bool {
// If we look like an MXID or have hidden characters then we're ambiguous.
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
}
/// Returns the underlying raw and and unsanitized string of this
/// [`DisplayName`].
pub fn as_raw_str(&self) -> &str {
&self.raw
}
/// Returns the underlying normalized and and sanitized string of this
/// [`DisplayName`].
///
/// Returns `None` if normalization failed during construction of this
/// [`DisplayName`].
pub fn as_normalized_str(&self) -> Option<&str> {
self.decancered.as_deref()
}
fn has_hidden_characters(&self) -> bool {
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
}
fn looks_like_an_mxid(&self) -> bool {
self.decancered
.as_deref()
.map(|d| MXID_REGEX.is_match(d))
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
}
}
/// A deserialized response for the rooms members API call.
///
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
@@ -294,10 +469,12 @@ impl MemberEvent {
///
/// It there is no `displayname` in the event's content, the localpart or
/// the user ID is returned.
pub fn display_name(&self) -> &str {
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart())
pub fn display_name(&self) -> DisplayName {
DisplayName::new(
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart()),
)
}
}
@@ -310,3 +487,240 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
}
}
}
#[cfg(test)]
mod test {
macro_rules! assert_display_name_eq {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
similar_asserts::assert_eq!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_display_name_ne {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
assert_ne!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
name.is_inherently_ambiguous(),
"The display {:?} should be considered amgibuous",
name
);
};
}
macro_rules! assert_not_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
!name.is_inherently_ambiguous(),
"The display {:?} should not be considered amgibuous",
name
);
};
}
#[test]
fn test_display_name_inherently_ambiguous() {
// These should not be inherently ambiguous, only if another similarly looking
// display name appears should they be considered to be ambiguous.
assert_not_ambiguous!("Alice");
assert_not_ambiguous!("Carol");
assert_not_ambiguous!("Car0l");
assert_not_ambiguous!("Ivan");
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
assert_not_ambiguous!("Sahasrahla");
// Left to right is fine, if it's the only one in the room.
assert_not_ambiguous!("\u{202e}alharsahas");
// These on the other hand contain invisible chars.
assert_ambiguous!("Sa̴hasrahla");
assert_ambiguous!("Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_capitalization() {
// Display name with different capitalization
assert_display_name_eq!("Alice", "alice");
}
#[test]
fn test_display_name_equality_different_names() {
// Different display names
assert_display_name_ne!("Alice", "Carol");
}
#[test]
fn test_display_name_equality_capital_l() {
// Different display names
assert_display_name_eq!("Hello", "HeIlo");
}
#[test]
fn test_display_name_equality_confusable_zero() {
// Different display names
assert_display_name_eq!("Carol", "Car0l");
}
#[test]
fn test_display_name_equality_cyrilic() {
// Display name with scritpure symbols
assert_display_name_eq!("alice", "аlice");
}
#[test]
fn test_display_name_equality_scriptures() {
// Display name with scritpure symbols
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
}
#[test]
fn test_display_name_equality_frakturs() {
// Display name with fraktur symbols
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
}
#[test]
fn test_display_name_equality_circled() {
// Display name with circled symbols
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
}
#[test]
fn test_display_name_equality_squared() {
// Display name with squared symbols
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
}
#[test]
fn test_display_name_equality_big_unicode() {
// Display name with big unicode letters
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
}
#[test]
fn test_display_name_equality_left_to_right() {
// Display name with a left-to-right character
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
}
#[test]
fn test_display_name_equality_diacritical() {
// Display name with a diacritical mark.
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
}
#[test]
fn test_display_name_equality_zero_width_joiner() {
// Display name with a zero-width joiner
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
}
#[test]
fn test_display_name_equality_zero_width_space() {
// Display name with zero-width space.
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_ligatures() {
// Display name with a ligature.
assert_display_name_eq!("ff", "\u{FB00}");
}
#[test]
fn test_display_name_confusable_mxid_colon() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid\u{0589}domain.tld");
assert_ambiguous!("@mxid\u{05c3}domain.tld");
assert_ambiguous!("@mxid\u{0703}domain.tld");
assert_ambiguous!("@mxid\u{0a83}domain.tld");
assert_ambiguous!("@mxid\u{16ec}domain.tld");
assert_ambiguous!("@mxid\u{205a}domain.tld");
assert_ambiguous!("@mxid\u{2236}domain.tld");
assert_ambiguous!("@mxid\u{fe13}domain.tld");
assert_ambiguous!("@mxid\u{fe52}domain.tld");
assert_ambiguous!("@mxid\u{fe30}domain.tld");
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
}
#[test]
fn test_display_name_confusable_mxid_dot() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain\u{0701}tld");
assert_ambiguous!("@mxid:domain\u{0702}tld");
assert_ambiguous!("@mxid:domain\u{2024}tld");
assert_ambiguous!("@mxid:domain\u{fe52}tld");
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_a() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_l() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain.tId");
assert_ambiguous!("@mxid:domain.t\u{217c}d");
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
assert_ambiguous!("@mxid:domain.t\u{2223}d");
}
}
+8
View File
@@ -61,4 +61,12 @@ pub enum Error {
/// function with invalid parameters
#[error("receive_all_members function was called with invalid parameters")]
InvalidReceiveMembersParameters,
/// This request failed because the local data wasn't sufficient.
#[error("Local cache doesn't contain all necessary data to perform the action.")]
InsufficientData,
/// There was a [`serde_json`] deserialization error.
#[error(transparent)]
DeserializationError(#[from] serde_json::error::Error),
}
@@ -0,0 +1,30 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
pub mod store;
/// The kind of event the event storage holds.
pub type Event = SyncTimelineEvent;
/// The kind of gap the event storage holds.
#[derive(Clone, Debug)]
pub struct Gap {
/// The token to use in the query, extracted from a previous "from" /
/// "end" field of a `/messages` response.
pub prev_token: String,
}
@@ -0,0 +1,317 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use async_trait::async_trait;
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
};
use super::DynEventCacheStore;
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
/// `EventCacheStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`event_cache_store_integration_tests!`] macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreIntegrationTests {
/// Test media content storage.
async fn test_media_content(&self);
/// Test replacing a MXID.
async fn test_replace_media_key(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let request_thumbnail = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
Method::Crop,
uint!(100),
uint!(100),
)),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequestParameters {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone())
.await
.expect("adding media again failed");
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
// Let's add the thumbnail media.
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
.await
.expect("adding thumbnail failed");
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// Let's add another media with a different URI.
self.add_media_content(&request_other_file, other_content.clone())
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
}
async fn test_replace_media_key(&self) {
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
let req = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let content = "hello".as_bytes().to_owned();
// Media isn't present in the cache.
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
// Add the media.
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
// Sanity-check: media is found after adding it.
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
// Replacing a media request works.
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
let new_req = MediaRequestParameters {
source: MediaSource::Plain(new_uri.to_owned()),
format: MediaFormat::File,
};
self.replace_media_key(&req, &new_req)
.await
.expect("replacing the media request key failed");
// Finding with the previous request doesn't work anymore.
assert!(
self.get_media_content(&req).await.unwrap().is_none(),
"unexpected media found with the old key"
);
// Finding with the new request does work.
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
}
}
/// Macro building to allow your `EventCacheStore` implementation to run the
/// entire tests suite locally.
///
/// You need to provide a `async fn get_event_cache_store() ->
/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
/// store on the same level you invoke the macro.
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache::store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
/// # };
///
/// #[cfg(test)]
/// mod tests {
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
///
/// async fn get_event_cache_store(
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
/// Ok(MyStore::new())
/// }
///
/// event_cache_store_integration_tests!();
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! event_cache_store_integration_tests {
() => {
mod event_cache_store_integration_tests {
use matrix_sdk_test::async_test;
use $crate::event_cache::store::{
EventCacheStoreIntegrationTests, IntoEventCacheStore,
};
use super::get_event_cache_store;
#[async_test]
async fn test_media_content() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_media_content().await;
}
#[async_test]
async fn test_replace_media_key() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_replace_media_key().await;
}
}
};
}
/// Macro generating tests for the event cache store, related to time (mostly
/// for the cross-process lock).
#[allow(unused_macros)]
#[macro_export]
macro_rules! event_cache_store_integration_tests_time {
() => {
#[cfg(not(target_arch = "wasm32"))]
mod event_cache_store_integration_tests_time {
use std::time::Duration;
use matrix_sdk_test::async_test;
use $crate::event_cache::store::IntoEventCacheStore;
use super::get_event_cache_store;
#[async_test]
async fn test_lease_locks() {
let store = get_event_cache_store().await.unwrap().into_event_cache_store();
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert!(acquired0);
// Should extend the lease automatically (same holder).
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(acquired2);
// Should extend the lease automatically (same holder + time is ok).
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(acquired3);
// Another attempt at taking the lock should fail, because it's taken.
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired4);
// Even if we insist.
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired5);
// That's a nice test we got here, go take a little nap.
tokio::time::sleep(Duration::from_millis(50)).await;
// Still too early.
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired55);
// Ok you can take another nap then.
tokio::time::sleep(Duration::from_millis(250)).await;
// At some point, we do get the lock.
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
assert!(acquired6);
tokio::time::sleep(Duration::from_millis(1)).await;
// The other gets it almost immediately too.
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert!(acquired7);
tokio::time::sleep(Duration::from_millis(1)).await;
// But when we take a longer lease...
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired8);
// It blocks the other user.
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(!acquired9);
// We can hold onto our lease.
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired10);
}
}
};
}
@@ -0,0 +1,153 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
use async_trait::async_trait;
use matrix_sdk_common::{
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{MxcUri, OwnedMxcUri};
use super::{EventCacheStore, EventCacheStoreError, Result};
use crate::media::{MediaRequestParameters, UniqueKey as _};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct MemoryStore {
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
leases: StdRwLock<HashMap<String, (String, Instant)>>,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
impl Default for MemoryStore {
fn default() -> Self {
Self {
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
leases: Default::default(),
}
}
}
impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Self::default()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStore for MemoryStore {
type Error = EventCacheStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
) -> Result<()> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
// Now, let's add it.
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut medias = self.media.write().unwrap();
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
*mxc = to.uri().to_owned();
*key = to.unique_key();
}
Ok(())
}
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
let expected_key = request.unique_key();
let media = self.media.read().unwrap();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
let expected_key = request.unique_key();
let mut media = self.media.write().unwrap();
let Some(index) = media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
(media_uri == &expected_key).then_some(position)
})
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{EventCacheStore, MemoryStore, Result};
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
Ok(MemoryStore::new())
}
event_cache_store_integration_tests!();
event_cache_store_integration_tests_time!();
}
@@ -0,0 +1,183 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The event cache stores holds events and downloaded media when the cache was
//! activated to save bandwidth at the cost of increased storage space usage.
//!
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
//! into the event cache for the actual storage. By default this brings an
//! in-memory store.
use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
mod memory_store;
mod traits;
use matrix_sdk_common::store_locks::{
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::EventCacheStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
};
/// The high-level public type to represent an `EventCacheStore` lock.
#[derive(Clone)]
pub struct EventCacheStoreLock {
/// The inner cross process lock that is used to lock the `EventCacheStore`.
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
/// The store itself.
///
/// That's the only place where the store exists.
store: Arc<DynEventCacheStore>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for EventCacheStoreLock {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive()
}
}
impl EventCacheStoreLock {
/// Create a new lock around the [`EventCacheStore`].
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessStoreLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
where
S: IntoEventCacheStore,
{
let store = store.into_event_cache_store();
Self {
cross_process_lock: CrossProcessStoreLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
),
store,
}
}
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
}
}
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
/// When this structure is dropped (falls out of scope), the lock will be
/// unlocked.
pub struct EventCacheStoreLockGuard<'a> {
/// The cross process lock guard.
#[allow(unused)]
cross_process_lock_guard: CrossProcessStoreLockGuard,
/// A reference to the store.
store: &'a DynEventCacheStore,
}
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
}
}
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
type Target = DynEventCacheStore;
fn deref(&self) -> &Self::Target {
self.store
}
}
/// Event cache store specific error type.
#[derive(Debug, thiserror::Error)]
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
#[error("The event cache store failed to be unlocked")]
Locked,
/// An unencrypted store was tried to be unlocked with a passphrase.
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
Unencrypted,
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
Encryption(#[from] StoreEncryptionError),
/// The store failed to encode or decode some data.
#[error("Error encoding or decoding data from the event cache store: {0}")]
Codec(#[from] Utf8Error),
/// The database format has changed in a backwards incompatible way.
#[error(
"The database format of the event cache store changed in an incompatible way, \
current version: {0}, latest version: {1}"
)]
UnsupportedDatabaseVersion(usize, usize),
}
impl EventCacheStoreError {
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
}
/// An `EventCacheStore` specific result type.
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
/// make it usable inside the cross process lock.
#[derive(Clone, Debug)]
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BackingStore for LockableEventCacheStore {
type LockError = EventCacheStoreError;
async fn try_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> std::result::Result<bool, Self::LockError> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
@@ -0,0 +1,204 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::MxcUri;
use super::EventCacheStoreError;
use crate::media::MediaRequestParameters;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStore: AsyncTraitDeps {
/// The error type used by this event cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
/// Try to take a lock using the given store.
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
) -> Result<(), Self::Error>;
/// Replaces the given media's content key with another one.
///
/// This should be used whenever a temporary (local) MXID has been used, and
/// it must now be replaced with its actual remote counterpart (after
/// uploading some content, or creating an empty MXC URI).
///
/// ⚠ No check is performed to ensure that the media formats are consistent,
/// i.e. it's possible to update with a thumbnail key a media that was
/// keyed as a file before. The caller is responsible of ensuring that
/// the replacement makes sense, according to their use case.
///
/// This should not raise an error when the `from` parameter points to an
/// unknown media, and it should silently continue in this case.
///
/// # Arguments
///
/// * `from` - The previous `MediaRequest` of the file.
///
/// * `to` - The new `MediaRequest` of the file.
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// This should not raise an error when the `uri` parameter points to an
/// unknown media, and it should return an Ok result in this case.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
}
#[repr(transparent)]
struct EraseEventCacheStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
type Error = EventCacheStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content).await.map_err(Into::into)
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.replace_media_key(from, to).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
}
/// A type-erased [`EventCacheStore`].
pub type DynEventCacheStore = dyn EventCacheStore<Error = EventCacheStoreError>;
/// A type that can be type-erased into `Arc<dyn EventCacheStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-base`, but it is automatically implemented for everything that
/// implements `EventCacheStore`.
pub trait IntoEventCacheStore {
#[doc(hidden)]
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
}
impl<T> IntoEventCacheStore for T
where
T: EventCacheStore + Sized + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
Arc::new(EraseEventCacheStoreError(self))
}
}
// Turns a given `Arc<T>` into `Arc<DynEventCacheStore>` by attaching the
// `EventCacheStore` impl vtable of `EraseEventCacheStoreError<T>`.
impl<T> IntoEventCacheStore for Arc<T>
where
T: EventCacheStore + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
let ptr: *const T = Arc::into_raw(self);
let ptr_erased = ptr as *const EraseEventCacheStoreError<T>;
// SAFETY: EraseEventCacheStoreError is repr(transparent) so T and
// EraseEventCacheStoreError<T> have the same layout and ABI
unsafe { Arc::from_raw(ptr_erased) }
}
}
+116 -26
View File
@@ -6,15 +6,22 @@
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::{
poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent,
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::message::SyncRoomMessageEvent,
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
};
use ruma::{
events::{
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
AnySyncStateEvent,
},
MxcUri, OwnedEventId,
MxcUri, OwnedEventId, UserId,
};
use serde::{Deserialize, Serialize};
@@ -29,6 +36,8 @@ use crate::MinimalRoomMemberEvent;
pub enum PossibleLatestEvent<'a> {
/// This message is suitable - it is an m.room.message
YesRoomMessage(&'a SyncRoomMessageEvent),
/// This message is suitable - it is a sticker
YesSticker(&'a SyncStickerEvent),
/// This message is suitable - it is a poll
YesPoll(&'a SyncUnstablePollStartEvent),
@@ -38,6 +47,10 @@ pub enum PossibleLatestEvent<'a> {
/// This message is suitable - it's a call notification
YesCallNotify(&'a SyncCallNotifyEvent),
/// This state event is suitable - it's a knock membership change
/// that can be handled by the current user.
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
// Later: YesState(),
// Later: YesReaction(),
/// Not suitable - it's a state event
@@ -51,7 +64,10 @@ pub enum PossibleLatestEvent<'a> {
/// Decide whether an event could be stored as the latest event in a room.
/// Returns a LatestEvent representing our decision.
#[cfg(feature = "e2e-encryption")]
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
pub fn is_suitable_for_latest_event<'a>(
event: &'a AnySyncTimelineEvent,
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
) -> PossibleLatestEvent<'a> {
match event {
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
@@ -68,9 +84,8 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
if is_replacement {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
} else {
return PossibleLatestEvent::YesRoomMessage(message);
}
return PossibleLatestEvent::YesRoomMessage(message);
}
return PossibleLatestEvent::YesRoomMessage(message);
@@ -88,6 +103,10 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
PossibleLatestEvent::YesCallNotify(notify)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
PossibleLatestEvent::YesSticker(sticker)
}
// Encrypted events are not suitable
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
PossibleLatestEvent::NoEncrypted
@@ -100,8 +119,29 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
// suitable
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
// We don't currently support state events
AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType,
// We don't currently support most state events
AnySyncTimelineEvent::State(state) => {
// But we make an exception for knocked state events *if* the current user
// can either accept or decline them
if let AnySyncStateEvent::RoomMember(member) = state {
if matches!(member.membership(), MembershipState::Knock) {
let can_accept_or_decline_knocks = match power_levels_info {
Some((own_user_id, room_power_levels)) => {
room_power_levels.user_can_invite(own_user_id)
|| room_power_levels.user_can_kick(own_user_id)
}
_ => false,
};
// The current user can act on the knock changes, so they should be
// displayed
if can_accept_or_decline_knocks {
return PossibleLatestEvent::YesKnockedStateEvent(member);
}
}
}
PossibleLatestEvent::NoUnsupportedEventType
}
}
}
@@ -180,7 +220,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
event: value,
sender_profile: None,
sender_name_is_ambiguous: None,
})
});
}
Err(err) => variant_errors.push(err),
}
@@ -271,9 +311,14 @@ mod tests {
},
SessionDescription,
},
poll::unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
UnstablePollStartContentBlock,
poll::{
unstable_response::{
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
},
unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
UnstablePollAnswer, UnstablePollStartContentBlock,
},
},
relation::Replacement,
room::{
@@ -320,7 +365,7 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.msgtype.msgtype(), "m.image");
@@ -343,7 +388,7 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
@@ -367,7 +412,7 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
@@ -389,12 +434,12 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
fn test_stickers_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
content: StickerEventContent::new(
@@ -410,7 +455,29 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
);
}
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
let event =
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
content: UnstablePollResponseEventContent::new(
vec![String::from("option1")],
owned_event_id!("$1"),
),
event_id: owned_event_id!("$2"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -438,7 +505,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
@@ -460,7 +527,10 @@ mod tests {
}),
));
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoEncrypted
);
}
#[test]
@@ -477,7 +547,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
@@ -501,7 +571,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -532,9 +602,12 @@ mod tests {
json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
}
},
}
@@ -548,6 +621,23 @@ mod tests {
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The previous format can also be deserialized.
let serialized = json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
}
},
}
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The even older format can also be deserialized.
let serialized = json!({
"latest_event": event
});
+9 -4
View File
@@ -15,6 +15,7 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![warn(missing_docs, missing_debug_implementations)]
pub use matrix_sdk_common::*;
@@ -27,14 +28,17 @@ mod client;
pub mod debug;
pub mod deserialized_responses;
mod error;
pub mod event_cache;
pub mod latest_event;
pub mod media;
pub mod notification_settings;
mod response_processors;
mod rooms;
pub mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;
pub mod sliding_sync;
pub mod store;
pub mod sync;
@@ -52,11 +56,12 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomInfoUpdate,
RoomMember, RoomMemberships, RoomState, RoomStateFilter,
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
RoomStateFilter,
};
pub use store::{
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
};
pub use utils::{
+51 -15
View File
@@ -14,6 +14,7 @@ use ruma::{
},
MxcUri, UInt,
};
use serde::{Deserialize, Serialize};
const UNIQUE_SEPARATOR: &str = "_";
@@ -25,27 +26,27 @@ pub trait UniqueKey {
}
/// The requested format of a media file.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MediaFormat {
/// The file that was uploaded.
File,
/// A thumbnail of the file that was uploaded.
Thumbnail(MediaThumbnailSize),
Thumbnail(MediaThumbnailSettings),
}
impl UniqueKey for MediaFormat {
fn unique_key(&self) -> String {
match self {
Self::File => "file".into(),
Self::Thumbnail(size) => size.unique_key(),
Self::Thumbnail(settings) => settings.unique_key(),
}
}
}
/// The requested size of a media thumbnail.
#[derive(Clone, Debug)]
pub struct MediaThumbnailSize {
/// The desired settings of a media thumbnail.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaThumbnailSettings {
/// The desired resizing method.
pub method: Method,
@@ -56,11 +57,44 @@ pub struct MediaThumbnailSize {
/// The desired height of the thumbnail. The actual thumbnail may not match
/// the size specified.
pub height: UInt,
/// If we want to request an animated thumbnail from the homeserver.
///
/// If it is `true`, the server should return an animated thumbnail if
/// the media supports it.
///
/// Defaults to `false`.
pub animated: bool,
}
impl UniqueKey for MediaThumbnailSize {
impl MediaThumbnailSettings {
/// Constructs a new `MediaThumbnailSettings` with the given method, width
/// and height.
///
/// Requests a non-animated thumbnail by default.
pub fn with_method(method: Method, width: UInt, height: UInt) -> Self {
Self { method, width, height, animated: false }
}
/// Constructs a new `MediaThumbnailSettings` with the given width and
/// height.
///
/// Requests scaling, and a non-animated thumbnail.
pub fn new(width: UInt, height: UInt) -> Self {
Self { method: Method::Scale, width, height, animated: false }
}
}
impl UniqueKey for MediaThumbnailSettings {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
if self.animated {
key.push_str(UNIQUE_SEPARATOR);
key.push_str("animated");
}
key
}
}
@@ -73,9 +107,11 @@ impl UniqueKey for MediaSource {
}
}
/// A request for media data.
#[derive(Clone, Debug)]
pub struct MediaRequest {
/// Parameters for a request for retrieve media data.
///
/// This is used as a key in the media cache too.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaRequestParameters {
/// The source of the media file.
pub source: MediaSource,
@@ -83,7 +119,7 @@ pub struct MediaRequest {
pub format: MediaFormat,
}
impl MediaRequest {
impl MediaRequestParameters {
/// Get the [`MxcUri`] from `Self`.
pub fn uri(&self) -> &MxcUri {
match &self.source {
@@ -93,7 +129,7 @@ impl MediaRequest {
}
}
impl UniqueKey for MediaRequest {
impl UniqueKey for MediaRequestParameters {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
}
@@ -189,14 +225,14 @@ mod tests {
fn test_media_request_url() {
let mxc_uri = mxc_uri!("mxc://homeserver/media");
let plain = MediaRequest {
let plain = MediaRequestParameters {
source: MediaSource::Plain(mxc_uri.to_owned()),
format: MediaFormat::File,
};
assert_eq!(plain.uri(), mxc_uri);
let file = MediaRequest {
let file = MediaRequestParameters {
source: MediaSource::Encrypted(Box::new(
serde_json::from_value(json!({
"url": mxc_uri,
@@ -0,0 +1,28 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
//! Some shared types about notification settings.
use serde::{Deserialize, Serialize};
/// Enum representing the push notification modes for a room.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum RoomNotificationMode {
/// Receive notifications for all messages.
AllMessages,
/// Receive notifications for mentions and keywords only.
MentionsAndKeywordsOnly,
/// Do not receive any notifications.
Mute,
}
+2 -4
View File
@@ -203,7 +203,7 @@ impl RoomReadReceipts {
/// Returns whether a new event triggered a new unread/notification/mention.
#[inline(always)]
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
if marks_as_unread(&event.event, user_id) {
if marks_as_unread(event.raw(), user_id) {
self.num_unread += 1;
}
@@ -408,7 +408,7 @@ impl ReceiptSelector {
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
for ev in new_events {
// Get the `sender` field, if any, or skip this event.
let Ok(Some(sender)) = ev.event.get_field::<OwnedUserId>("sender") else { continue };
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
if sender == user_id {
// Get the event id, if any, or skip this event.
let Some(event_id) = ev.event_id() else { continue };
@@ -449,8 +449,6 @@ fn events_intersects<'a>(
/// that has been just received for an event that came in a previous sync.
///
/// See this module's documentation for more information.
///
/// Returns a boolean indicating if a field changed value in the read receipts.
#[instrument(skip_all, fields(room_id = %room_id))]
pub(crate) fn compute_unread_counts(
user_id: &UserId,
@@ -0,0 +1,160 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashMap, HashSet},
mem,
};
use ruma::{
events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType},
serde::Raw,
OwnedUserId, RoomId,
};
use tracing::{debug, instrument, trace, warn};
use crate::{store::Store, RoomInfo, StateChanges};
/// Applies a function to an existing `RoomInfo` if present in changes, or one
/// loaded from the database.
fn map_info<F: FnOnce(&mut RoomInfo)>(
room_id: &RoomId,
changes: &mut StateChanges,
store: &Store,
f: F,
) {
if let Some(info) = changes.room_infos.get_mut(room_id) {
f(info);
} else if let Some(room) = store.room(room_id) {
let mut info = room.clone_info();
f(&mut info);
changes.add_room(info);
} else {
debug!(room = %room_id, "couldn't find room in state changes or store");
}
}
#[must_use]
pub(crate) struct AccountDataProcessor {
parsed_events: Vec<AnyGlobalAccountDataEvent>,
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
}
impl AccountDataProcessor {
/// Creates a new processor for global account data.
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
let mut raw_by_type = BTreeMap::new();
let mut parsed_events = Vec::new();
for raw_event in events {
let event = match raw_event.deserialize() {
Ok(e) => e,
Err(e) => {
let event_type: Option<String> = raw_event.get_field("type").ok().flatten();
warn!(event_type, "Failed to deserialize a global account data event: {e}");
continue;
}
};
raw_by_type.insert(event.event_type(), raw_event.clone());
parsed_events.push(event);
}
Self { raw_by_type, parsed_events }
}
/// Returns the push rules found by this processor.
pub fn push_rules(&self) -> Option<&Raw<AnyGlobalAccountDataEvent>> {
self.raw_by_type.get(&GlobalAccountDataEventType::PushRules)
}
/// Processes the direct rooms in a sync response:
///
/// Given a [`StateChanges`] instance, processes any direct room info
/// from the global account data and adds it to the room infos to
/// save.
#[instrument(skip_all)]
pub(crate) fn process_direct_rooms(
&self,
events: &[AnyGlobalAccountDataEvent],
store: &Store,
changes: &mut StateChanges,
) {
for event in events {
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedUserId>>::new();
for (user_id, rooms) in direct_event.content.iter() {
for room_id in rooms {
new_dms.entry(room_id).or_default().insert(user_id.clone());
}
}
let rooms = store.rooms();
let mut old_dms = rooms
.iter()
.filter_map(|r| {
let direct_targets = r.direct_targets();
(!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets))
})
.collect::<HashMap<_, _>>();
// Update the direct targets of rooms if they changed.
for (room_id, new_direct_targets) in new_dms {
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
if old_direct_targets == new_direct_targets {
continue;
}
}
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
map_info(room_id, changes, store, |info| {
info.base_info.dm_targets = new_direct_targets;
});
}
// Remove the targets of old direct chats.
for room_id in old_dms.keys() {
trace!(?room_id, "Unmarking room as direct room");
map_info(room_id, changes, store, |info| {
info.base_info.dm_targets.clear();
});
}
}
}
/// Applies the processed data to the state changes.
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
// Fill in the content of `changes.account_data`.
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
// Process direct rooms.
let has_new_direct_room_data = self
.parsed_events
.iter()
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
if has_new_direct_room_data {
self.process_direct_rooms(&self.parsed_events, store, changes);
} else if let Ok(Some(direct_account_data)) =
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
{
debug!("Found direct room data in the Store, applying it");
if let Ok(direct_account_data) = direct_account_data.deserialize() {
self.process_direct_rooms(&[direct_account_data], store, changes);
} else {
warn!("Failed to deserialize direct room account data");
}
}
}
}
+13 -5
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeSet, HashMap},
sync::Arc,
};
@@ -30,7 +30,8 @@ use ruma::{
};
use crate::{
deserialized_responses::{MemberEvent, SyncOrStrippedState},
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
store::ambiguity_map::is_display_name_ambiguous,
MinimalRoomMemberEvent,
};
@@ -67,8 +68,10 @@ impl RoomMember {
} = room_info;
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
let display_name_ambiguous =
users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1);
let display_name = event.display_name();
let display_name_ambiguous = users_display_names
.get(&display_name)
.is_some_and(|s| is_display_name_ambiguous(&display_name, s));
let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
Self {
@@ -194,6 +197,11 @@ impl RoomMember {
self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
}
/// Whether this user can pin or unpin events based on the power levels.
pub fn can_pin_or_unpin_event(&self) -> bool {
self.can_send_state(StateEventType::RoomPinnedEvents)
}
/// Whether this user can notify everybody in the room by writing `@room` in
/// a message.
///
@@ -240,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> {
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
pub(crate) max_power_level: i64,
pub(crate) room_creator: Option<OwnedUserId>,
pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet<OwnedUserId>>,
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
}
+106 -13
View File
@@ -11,11 +11,16 @@ use std::{
use bitflags::bitflags;
pub use members::RoomMember;
pub use normal::{Room, RoomHero, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter};
pub use normal::{
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
RoomStateFilter,
};
use regex::Regex;
use ruma::{
assign,
events::{
call::member::CallMemberEventContent,
beacon_info::BeaconInfoEventContent,
call::member::{CallMemberEventContent, CallMemberStateKey},
macros::EventContent,
room::{
avatar::RoomAvatarEventContent,
@@ -27,6 +32,7 @@ use ruma::{
join_rules::RoomJoinRulesEventContent,
member::MembershipState,
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
@@ -44,7 +50,7 @@ use crate::MinimalStateEvent;
/// The name of the room, either from the metadata or calculated
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DisplayName {
pub enum RoomDisplayName {
/// The room has been named explicitly as
Named(String),
/// The room has a canonical alias that should be used
@@ -59,14 +65,48 @@ pub enum DisplayName {
Empty,
}
impl fmt::Display for DisplayName {
const WHITESPACE_REGEX: &str = r"\s+";
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
impl RoomDisplayName {
/// Transforms the current display name into the name part of a
/// `RoomAliasId`.
pub fn to_room_alias_name(&self) -> String {
let room_name = match self {
Self::Named(name) => name,
Self::Aliased(name) => name,
Self::Calculated(name) => name,
Self::EmptyWas(name) => name,
Self::Empty => "",
};
let whitespace_regex =
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
let symbol_regex =
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
// Replace whitespaces with `-`
let sanitised = whitespace_regex.replace_all(room_name, "-");
// Remove non-ASCII characters and ASCII control characters
let sanitised =
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
// Remove other problematic ASCII symbols
let sanitised = symbol_regex.replace_all(&sanitised, "");
// Lowercased
sanitised.to_lowercase()
}
}
impl fmt::Display for RoomDisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
RoomDisplayName::Named(s)
| RoomDisplayName::Calculated(s)
| RoomDisplayName::Aliased(s) => {
write!(f, "{s}")
}
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
DisplayName::Empty => write!(f, "Empty Room"),
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
RoomDisplayName::Empty => write!(f, "Empty Room"),
}
}
}
@@ -78,6 +118,9 @@ impl fmt::Display for DisplayName {
pub struct BaseRoomInfo {
/// The avatar URL of this room.
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
/// All shared live location beacons of this room.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
/// The canonical alias of this room.
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
/// The `m.room.create` event content of this room.
@@ -104,7 +147,8 @@ pub struct BaseRoomInfo {
/// All minimal state events that containing one or more running matrixRTC
/// memberships.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
pub(crate) rtc_member_events:
BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
/// Whether this room has been manually marked as unread.
#[serde(default)]
pub(crate) is_marked_unread: bool,
@@ -114,6 +158,8 @@ pub struct BaseRoomInfo {
/// others, and this field collects them.
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
pub(crate) notable_tags: RoomNotableTags,
/// The `m.room.pinned_events` of this room.
pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
}
impl BaseRoomInfo {
@@ -138,6 +184,9 @@ impl BaseRoomInfo {
/// Returns true if the event modified the info, false otherwise.
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
match ev {
AnySyncStateEvent::BeaconInfo(b) => {
self.beacons.insert(b.state_key().clone(), b.into());
}
// No redacted branch - enabling encryption cannot be undone.
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
self.encryption = Some(encryption.content.clone());
@@ -182,15 +231,18 @@ impl BaseRoomInfo {
let mut o_ev = o_ev.clone();
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
// add the new event.
self.rtc_member
// Add the new event.
self.rtc_member_events
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
// Remove all events that don't contain any memberships anymore.
self.rtc_member.retain(|_, ev| {
self.rtc_member_events.retain(|_, ev| {
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
});
}
AnySyncStateEvent::RoomPinnedEvents(p) => {
self.pinned_events = p.as_original().map(|p| p.content.clone());
}
_ => return false,
}
@@ -250,6 +302,11 @@ impl BaseRoomInfo {
// wont have call information.
return false;
}
AnyStrippedStateEvent::RoomPinnedEvents(p) => {
if let Some(pinned) = p.content.pinned.clone() {
self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
}
}
_ => return false,
}
@@ -279,7 +336,8 @@ impl BaseRoomInfo {
} else if self.topic.has_event_id(redacts) {
self.topic.as_mut().unwrap().redact(&room_version);
} else {
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
self.rtc_member_events
.retain(|_, member_event| member_event.event_id() != Some(redacts));
}
}
@@ -332,6 +390,7 @@ impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar: None,
beacons: BTreeMap::new(),
canonical_alias: None,
create: None,
dm_targets: Default::default(),
@@ -343,9 +402,10 @@ impl Default for BaseRoomInfo {
name: None,
tombstone: None,
topic: None,
rtc_member: BTreeMap::new(),
rtc_member_events: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
pinned_events: None,
}
}
}
@@ -516,6 +576,7 @@ mod tests {
use ruma::events::tag::{TagInfo, TagName, Tags};
use super::{BaseRoomInfo, RoomNotableTags};
use crate::RoomDisplayName;
#[test]
fn test_handle_notable_tags_favourite() {
@@ -546,4 +607,36 @@ mod tests {
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
}
#[test]
fn test_room_alias_from_room_display_name_lowercases() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_whitespace() {
assert_eq!(
"room-alias",
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! HTTP types for MSC4186 or MSC3585.
//!
//! This module provides unified namings for types from MSC3575 and
//! MSC4186.
/// HTTP types from MSC3575, renamed to match the MSC4186 namings.
pub mod msc3575 {
use ruma::api::client::sync::sync_events::v4;
pub use v4::{Request, Response};
/// HTTP types related to a `Request`.
pub mod request {
pub use super::v4::{
AccountDataConfig as AccountData, ExtensionsConfig as Extensions,
ReceiptsConfig as Receipts, RoomDetailsConfig as RoomDetails, RoomSubscription,
SyncRequestList as List, SyncRequestListFilters as ListFilters,
ToDeviceConfig as ToDevice, TypingConfig as Typing,
};
}
/// HTTP types related to a `Response`.
pub mod response {
pub use super::v4::{
AccountData, Extensions, Receipts, SlidingSyncRoom as Room,
SlidingSyncRoomHero as RoomHero, SyncList as List, ToDevice, Typing,
};
}
}
/// HTTP types from MSC4186.
pub mod msc4186 {
pub use ruma::api::client::sync::sync_events::v5::*;
}
pub use msc4186::*;
+346 -89
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
sync::Arc,
};
@@ -24,28 +24,24 @@ use ruma::{
},
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tracing::trace;
use tracing::{instrument, trace};
use super::{DynStateStore, Result, StateChanges};
use crate::{
deserialized_responses::{AmbiguityChange, RawMemberEvent},
deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent},
store::StateStoreExt,
};
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[derive(Debug)]
struct AmbiguityMap {
display_name: String,
/// A map of users that use a certain display name.
#[derive(Debug, Clone)]
struct DisplayNameUsers {
display_name: DisplayName,
users: BTreeSet<OwnedUserId>,
}
impl AmbiguityMap {
impl DisplayNameUsers {
/// Remove the given [`UserId`] from the map, marking that the [`UserId`]
/// doesn't use the display name anymore.
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
self.users.remove(user_id);
@@ -56,6 +52,8 @@ impl AmbiguityMap {
}
}
/// Add the given [`UserId`] from the map, marking that the [`UserId`]
/// is using the display name.
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
let ambiguous_user =
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
@@ -65,46 +63,73 @@ impl AmbiguityMap {
ambiguous_user
}
/// How many users are using this display name.
fn user_count(&self) -> usize {
self.users.len()
}
/// Is the display name considered to be ambiguous.
fn is_ambiguous(&self) -> bool {
self.user_count() > 1
is_display_name_ambiguous(&self.display_name, &self.users)
}
}
fn is_member_active(membership: &MembershipState) -> bool {
use MembershipState::*;
matches!(membership, Join | Invite | Knock)
}
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[instrument(ret)]
pub(crate) fn is_display_name_ambiguous(
display_name: &DisplayName,
users_with_display_name: &BTreeSet<OwnedUserId>,
) -> bool {
trace!("Checking if a display name is ambiguous");
display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1
}
impl AmbiguityCache {
/// Create a new [`AmbiguityCache`] backed by the given state store.
pub fn new(store: Arc<DynStateStore>) -> Self {
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
}
/// Handle a newly received [`SyncRoomMemberEvent`] for the given room.
pub async fn handle_event(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<()> {
// Synapse seems to have a bug where it puts the same event into the
// state and the timeline sometimes.
// Synapse seems to have a bug where it puts the same event into the state and
// the timeline sometimes.
//
// Since our state, e.g. the old display name, already ended up inside
// the state changes and we're pulling stuff out of the cache if it's
// there calculating this twice for the same event will result in an
// incorrect AmbiguityChange overwriting the correct one. In other
// words, this method is not idempotent so we make it by ignoring
// duplicate events.
// Since our state, e.g. the old display name, already ended up inside the state
// changes and we're pulling stuff out of the cache if it's there calculating
// this twice for the same event will result in an incorrect AmbiguityChange
// overwriting the correct one. In other words, this method is not idempotent so
// we make it by ignoring duplicate events.
if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) {
return Ok(());
}
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
let (mut old_map, mut new_map) =
self.calculate_changes(changes, room_id, member_event).await?;
let display_names_same = match (&old_map, &new_map) {
(Some(a), Some(b)) => a.display_name == b.display_name,
_ => false,
};
// If the user's display name didn't change, then there's nothing more to
// calculate here.
if display_names_same {
return Ok(());
}
@@ -126,16 +151,21 @@ impl AmbiguityCache {
trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}");
self.add_change(room_id, member_event.event_id().to_owned(), change);
self.changes
.entry(room_id.to_owned())
.or_default()
.insert(member_event.event_id().to_owned(), change);
Ok(())
}
/// Update the [`AmbiguityCache`] state for the given room with a pair of
/// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`].
fn update(
&mut self,
room_id: &RoomId,
old_map: Option<AmbiguityMap>,
new_map: Option<AmbiguityMap>,
old_map: Option<DisplayNameUsers>,
new_map: Option<DisplayNameUsers>,
) {
let entry = self.cache.entry(room_id.to_owned()).or_default();
@@ -148,74 +178,102 @@ impl AmbiguityCache {
}
}
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
/// Get the previously used display name, if any, of the member described in
/// the given new [`SyncRoomMemberEvent`].
async fn get_old_display_name(
&self,
changes: &StateChanges,
room_id: &RoomId,
new_event: &SyncRoomMemberEvent,
) -> Result<Option<String>> {
let user_id = new_event.state_key();
let old_event = if let Some(m) = changes
.state
.get(room_id)
.and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str()))
{
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, user_id).await?
};
let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) };
if is_member_active(old_event.membership()) {
let display_name = if let Some(d) = changes
.profiles
.get(room_id)
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
{
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, user_id)
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
old_event.original_content().and_then(|c| c.displayname.clone())
};
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
} else {
Ok(None)
}
}
async fn get(
/// Get the [`DisplayNameUsers`] for the given display name in the given
/// room.
///
/// This method will get the [`DisplayNameUsers`] from the cache, if the
/// cache doesn't contain such an entry, it falls back to the state
/// store.
async fn get_users_with_display_name(
&mut self,
room_id: &RoomId,
display_name: &DisplayName,
) -> Result<DisplayNameUsers> {
Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) {
DisplayNameUsers { display_name: display_name.clone(), users: u.clone() }
} else {
let users_with_display_name =
self.store.get_users_with_display_name(room_id, display_name).await?;
DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name }
})
}
/// Calculate the change in the users that use a display name a
/// [`SyncRoomMemberEvent`] will cause for a given room.
///
/// Returns the [`DisplayNameUsers`] before the member event is applied and
/// the [`DisplayNameUsers`] after the member event is applied to the
/// room state.
async fn calculate_changes(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
use MembershipState::*;
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
}) {
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, member_event.state_key()).await?
};
// FIXME: Use let chains once stable
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
if matches!(event.membership(), Join | Invite) {
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
}) {
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, member_event.state_key())
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
event.original_content().and_then(|c| c.displayname.clone())
};
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
} else {
None
}
} else {
None
};
) -> Result<(Option<DisplayNameUsers>, Option<DisplayNameUsers>)> {
let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?;
let old_map = if let Some(old_name) = old_display_name.as_deref() {
let old_display_name_map =
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
u.clone()
} else {
self.store.get_users_with_display_name(room_id, old_name).await?
};
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
let old_display_name = DisplayName::new(old_name);
Some(self.get_users_with_display_name(room_id, &old_display_name).await?)
} else {
None
};
let new_map = if matches!(member_event.membership(), Join | Invite) {
let new_map = if is_member_active(member_event.membership()) {
let new = member_event
.as_original()
.and_then(|ev| ev.content.displayname.as_deref())
.unwrap_or_else(|| member_event.state_key().localpart());
// We don't allow other users to set the display name, so if we
// have a more trusted version of the display
// name use that.
// We don't allow other users to set the display name, so if we have a more
// trusted version of the display name use that.
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
new
} else if let Some(old) = old_display_name.as_deref() {
@@ -224,22 +282,221 @@ impl AmbiguityCache {
new
};
let new_display_name_map = if let Some(u) =
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
{
u.clone()
} else {
self.store.get_users_with_display_name(room_id, new_display_name).await?
};
let new_display_name = DisplayName::new(new_display_name);
Some(AmbiguityMap {
display_name: new_display_name.to_owned(),
users: new_display_name_map,
})
Some(self.get_users_with_display_name(room_id, &new_display_name).await?)
} else {
None
};
Ok((old_map, new_map))
}
#[cfg(test)]
fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool {
self.cache
.get(room_id)
.and_then(|display_names| {
display_names
.get(display_name)
.map(|user_ids| is_display_name_ambiguous(display_name, user_ids))
})
.unwrap_or_else(|| {
panic!(
"The display name {:?} should be part of the cache {:?}",
display_name, self.cache
)
})
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::async_test;
use ruma::{room_id, server_name, user_id, EventId};
use serde_json::json;
use super::*;
use crate::store::{IntoStateStore, MemoryStore};
fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent {
let server_name = server_name!("localhost");
serde_json::from_value(json!({
"content": {
"displayname": display_name,
"membership": "join"
},
"event_id": EventId::new(server_name),
"origin_server_ts": 152037280,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
}))
.expect("We should be able to deserialize the static member event")
}
macro_rules! assert_ambiguity {
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)?
) => {
assert_ambiguity!(
[ $( ($user, $display_name) ),* ],
[ $( ($check_display_name, $ambiguous) ),* ],
"The test failed the ambiguity assertions"
)
};
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ],
$description:literal $(,)?
) => {
let store = MemoryStore::new();
let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store());
let changes = Default::default();
let room_id = room_id!("!foo:bar");
macro_rules! add_display_name {
($u:literal, $n:literal) => {
let event = generate_event(user_id!($u), $n);
ambiguity_cache
.handle_event(&changes, room_id, &event)
.await
.expect("We should be able to handle a member event to calculate the ambiguity.");
};
}
macro_rules! assert_display_name_ambiguity {
($n:literal, $a:expr) => {
let display_name = DisplayName::new($n);
if ambiguity_cache.check(room_id, &display_name) != $a {
let foo = if $a { "be" } else { "not be" };
panic!("{}: the display name {} should {} ambiguous", $description, $n, foo);
}
};
}
$(
add_display_name!($user, $display_name);
)*
$(
assert_display_name_ambiguity!($check_display_name, $ambiguous);
)*
};
}
#[async_test]
async fn test_disambiguation() {
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("alice", false)],
"Alice is alone in the room"
);
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("Alice", false)],
"Alice is alone in the room and has a capitalized display name"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "alice")],
[("alice", true)],
"Alice and bob share a display name"
);
assert_ambiguity!(
[
("@alice:localhost", "alice"),
("@bob:localhost", "alice"),
("@carol:localhost", "carol")
],
[("alice", true), ("carol", false)],
"Alice and Bob share a display name, while Carol is unique"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")],
[("alice", true)],
"Alice and Bob share a display name that is differently capitalized"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
[("alice", true)],
"Bob tries to impersonate Alice using a cyrilic а"
);
assert_ambiguity!(
[("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")],
[("@bob:localhost", true)],
"Alice tries to impersonate bob using an mxid"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using scripture symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using fraktur symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using circled symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using squared symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using big unicode letters"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using left to right shenanigans"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a diacritical mark"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")],
[("ff", true)],
"Bob tries to impersonate Alice using a ligature"
);
}
}
@@ -1,6 +1,6 @@
//! Trait and macro of integration tests for StateStore implementations.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
@@ -8,7 +8,7 @@ use async_trait::async_trait;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_test::test_json;
use ruma::{
api::client::media::get_content_thumbnail::v3::Method,
api::MatrixVersion,
event_id,
events::{
presence::PresenceEvent,
@@ -21,24 +21,25 @@ use ruma::{
message::RoomMessageEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
MediaSource,
},
AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, AnyMessageLikeEventContent,
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent,
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
SyncStateEvent,
},
mxc_uri, owned_mxc_uri, room_id,
owned_event_id, owned_mxc_uri, room_id,
serde::Raw,
uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
};
use serde_json::{json, value::Value as JsonValue};
use super::DynStateStore;
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
ServerCapabilities,
};
use crate::{
deserialized_responses::MemberEvent,
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
store::{Result, SerializableEventContent, StateStoreExt},
store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt},
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
};
@@ -51,8 +52,6 @@ use crate::{
pub trait StateStoreIntegrationTests {
/// Populate the given `StateStore`.
async fn populate(&self) -> Result<()>;
/// Test media content storage.
async fn test_media_content(&self);
/// Test room topic redaction.
async fn test_topic_redaction(&self) -> Result<()>;
/// Test populating the store.
@@ -75,8 +74,6 @@ pub trait StateStoreIntegrationTests {
async fn test_receipts_saving(&self);
/// Test custom storage.
async fn test_custom_storage(&self) -> Result<()>;
/// Test invited room saving.
async fn test_persist_invited_room(&self) -> Result<()>;
/// Test stripped and non-stripped room member saving.
async fn test_stripped_non_stripped(&self) -> Result<()>;
/// Test room removal.
@@ -89,6 +86,12 @@ pub trait StateStoreIntegrationTests {
async fn test_display_names_saving(&self);
/// Test operations with the send queue.
async fn test_send_queue(&self);
/// Test priority of operations with the send queue.
async fn test_send_queue_priority(&self);
/// Test operations related to send queue dependents.
async fn test_send_queue_dependents(&self);
/// Test saving/restoring server capabilities.
async fn test_server_capabilities_saving(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -115,7 +118,7 @@ impl StateStoreIntegrationTests for DynStateStore {
serde_json::from_value::<Raw<AnyGlobalAccountDataEvent>>(pushrules_json.clone())
.unwrap();
let pushrules_event = pushrules_raw.deserialize().unwrap();
changes.add_account_data(pushrules_event, pushrules_raw);
changes.account_data.insert(pushrules_event.event_type(), pushrules_raw);
let mut room = RoomInfo::new(room_id, RoomState::Joined);
room.mark_as_left();
@@ -139,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore {
room.handle_state_event(&topic_event);
changes.add_state_event(room_id, topic_event, topic_raw);
let mut room_ambiguity_map = BTreeMap::new();
let mut room_ambiguity_map = HashMap::new();
let mut room_profiles = BTreeMap::new();
let member_json: &JsonValue = &test_json::MEMBER;
let member_event: SyncRoomMemberEvent =
serde_json::from_value(member_json.clone()).unwrap();
let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap();
let displayname = DisplayName::new(
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
);
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
room_profiles.insert(user_id.to_owned(), (&member_event).into());
@@ -209,110 +214,6 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(())
}
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file =
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
let request_thumbnail = MediaRequest {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Crop,
width: uint!(100),
height: uint!(100),
}),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequest {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone())
.await
.expect("adding media again failed");
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
// Let's add the thumbnail media.
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
.await
.expect("adding thumbnail failed");
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// Let's add another media with a different URI.
self.add_media_content(&request_other_file, other_content.clone())
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
}
async fn test_topic_redaction(&self) -> Result<()> {
let room_id = room_id();
self.populate().await?;
@@ -358,14 +259,13 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_populate_store(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
self.populate().await?;
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
assert!(self.get_presence_event(user_id).await?.is_some());
assert_eq!(self.get_room_infos().await?.len(), 2, "Expected to find 2 room infos");
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 1, "Expected to find 1 stripped room info");
assert!(self
.get_account_data_event(GlobalAccountDataEventType::PushRules)
.await?
@@ -395,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"Expected to find 1 joined user ids"
);
assert_eq!(
self.get_users_with_display_name(room_id, "example").await?.len(),
self.get_users_with_display_name(room_id, &display_name).await?.len(),
2,
"Expected to find 2 display names for room"
);
@@ -570,6 +470,36 @@ impl StateStoreIntegrationTests for DynStateStore {
);
}
async fn test_server_capabilities_saving(&self) {
let versions = &[MatrixVersion::V1_1, MatrixVersion::V1_2, MatrixVersion::V1_11];
let server_caps = ServerCapabilities::new(
versions,
[("org.matrix.experimental".to_owned(), true)].into(),
);
self.set_kv_data(
StateStoreDataKey::ServerCapabilities,
StateStoreDataValue::ServerCapabilities(server_caps.clone()),
)
.await
.unwrap();
assert_let!(
Ok(Some(StateStoreDataValue::ServerCapabilities(stored_caps))) =
self.get_kv_data(StateStoreDataKey::ServerCapabilities).await
);
assert_eq!(stored_caps, server_caps);
let (stored_versions, stored_features) = stored_caps.maybe_decode().unwrap();
assert_eq!(stored_versions, versions);
assert_eq!(stored_features.len(), 1);
assert_eq!(stored_features.get("org.matrix.experimental"), Some(&true));
self.remove_kv_data(StateStoreDataKey::ServerCapabilities).await.unwrap();
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerCapabilities).await, Ok(None));
}
async fn test_sync_token_saving(&self) {
let sync_token_1 = "t392-516_47314_0_7_1";
let sync_token_2 = "t392-516_47314_0_7_2";
@@ -992,25 +922,12 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(())
}
async fn test_persist_invited_room(&self) -> Result<()> {
self.populate().await?;
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 1);
Ok(())
}
async fn test_stripped_non_stripped(&self) -> Result<()> {
let room_id = room_id!("!test_stripped_non_stripped:localhost");
let user_id = user_id();
assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none());
assert_eq!(self.get_room_infos().await.unwrap().len(), 0);
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 0);
let mut changes = StateChanges::default();
changes
@@ -1027,9 +944,6 @@ impl StateStoreIntegrationTests for DynStateStore {
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
assert!(matches!(member_event, MemberEvent::Sync(_)));
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 0);
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
assert_eq!(members, vec![user_id.to_owned()]);
@@ -1043,9 +957,6 @@ impl StateStoreIntegrationTests for DynStateStore {
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
assert!(matches!(member_event, MemberEvent::Stripped(_)));
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 1);
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
assert_eq!(members, vec![user_id.to_owned()]);
@@ -1056,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_room_removal(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
let stripped_room_id = stripped_room_id();
self.populate().await?;
@@ -1063,9 +975,6 @@ impl StateStoreIntegrationTests for DynStateStore {
self.remove_room(room_id).await?;
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert_eq!(stripped_rooms.len(), 1);
assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
assert!(
@@ -1087,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"still joined users found"
);
assert!(
self.get_users_with_display_name(room_id, "example").await?.is_empty(),
self.get_users_with_display_name(room_id, &display_name).await?.is_empty(),
"still display names found"
);
assert!(self
@@ -1118,9 +1027,6 @@ impl StateStoreIntegrationTests for DynStateStore {
self.remove_room(stripped_room_id).await?;
assert!(self.get_room_infos().await?.is_empty(), "still room info found");
#[allow(deprecated)]
let stripped_rooms = self.get_stripped_room_infos().await?;
assert!(stripped_rooms.is_empty(), "still stripped room info found");
Ok(())
}
@@ -1245,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_display_names_saving(&self) {
let room_id = room_id!("!test_display_names_saving:localhost");
let user_id = user_id();
let user_display_name = "User";
let user_display_name = DisplayName::new("User");
let second_user_id = user_id!("@second:localhost");
let third_user_id = user_id!("@third:localhost");
let other_display_name = "Raoul";
let unknown_display_name = "Unknown";
let other_display_name = DisplayName::new("Raoul");
let unknown_display_name = DisplayName::new("Unknown");
// No event in store.
let mut display_names = vec![user_display_name.to_owned()];
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert!(users.is_empty());
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert!(names.is_empty());
@@ -1267,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore {
.insert(user_display_name.to_owned(), [user_id.to_owned()].into());
self.save_changes(&changes).await.unwrap();
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 1);
@@ -1282,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore {
self.save_changes(&changes).await.unwrap();
display_names.push(other_display_name.to_owned());
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap();
assert_eq!(users.len(), 2);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 2);
@@ -1306,7 +1212,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let room_id = room_id!("!test_send_queue:localhost");
// No queued event in store at first.
let events = self.load_send_queue_events(room_id).await.unwrap();
let events = self.load_send_queue_requests(room_id).await.unwrap();
assert!(events.is_empty());
// Saving one thing should work.
@@ -1314,20 +1220,20 @@ impl StateStoreIntegrationTests for DynStateStore {
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
.unwrap();
self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// Reading it will work.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 1);
{
assert_eq!(pending[0].transaction_id, txn0);
let deserialized = pending[0].event.deserialize().unwrap();
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "msg0");
assert!(!pending[0].is_wedged);
assert!(!pending[0].is_wedged());
}
// Saving another three things should work.
@@ -1338,11 +1244,11 @@ impl StateStoreIntegrationTests for DynStateStore {
)
.unwrap();
self.save_send_queue_event(room_id, txn, event).await.unwrap();
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
}
// Reading all the events should work.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
// All the events should be retrieved, in the same order.
assert_eq!(pending.len(), 4);
@@ -1350,27 +1256,36 @@ impl StateStoreIntegrationTests for DynStateStore {
assert_eq!(pending[0].transaction_id, txn0);
for i in 0..4 {
let deserialized = pending[i].event.deserialize().unwrap();
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), format!("msg{i}"));
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
// Marking an event as wedged works.
let txn2 = &pending[2].transaction_id;
self.update_send_queue_event_status(room_id, txn2, true).await.unwrap();
self.update_send_queue_request_status(
room_id,
txn2,
Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }),
)
.await
.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
// All the events should be retrieved, in the same order.
assert_eq!(pending.len(), 4);
assert_eq!(pending[0].transaction_id, txn0);
assert_eq!(pending[2].transaction_id, *txn2);
assert!(pending[2].is_wedged);
assert!(pending[2].is_wedged());
let error = pending[2].clone().error.unwrap();
let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg);
assert_eq!(generic_error, "Oops");
for i in 0..4 {
if i != 2 {
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
}
@@ -1379,37 +1294,37 @@ impl StateStoreIntegrationTests for DynStateStore {
&RoomMessageEventContent::text_plain("wow that's a cool test").into(),
)
.unwrap();
self.update_send_queue_event(room_id, txn2, event0).await.unwrap();
self.update_send_queue_request(room_id, txn2, event0.into()).await.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 4);
{
assert_eq!(pending[2].transaction_id, *txn2);
let deserialized = pending[2].event.deserialize().unwrap();
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "wow that's a cool test");
assert!(!pending[2].is_wedged);
assert!(!pending[2].is_wedged());
for i in 0..4 {
if i != 2 {
let deserialized = pending[i].event.deserialize().unwrap();
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), format!("msg{i}"));
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
}
}
// Removing an event works.
self.remove_send_queue_event(room_id, &txn0).await.unwrap();
self.remove_send_queue_request(room_id, &txn0).await.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 3);
assert_eq!(pending[1].transaction_id, *txn2);
@@ -1427,7 +1342,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
.unwrap();
self.save_send_queue_event(room_id2, txn.clone(), event).await.unwrap();
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
}
// Add and remove one event for room3.
@@ -1437,18 +1352,182 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
.unwrap();
self.save_send_queue_event(room_id3, txn.clone(), event).await.unwrap();
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
self.remove_send_queue_event(room_id3, &txn).await.unwrap();
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
}
// Query all the rooms which have unsent events. Per the previous steps,
// it should be room1 and room2, not room3.
let outstanding_rooms = self.load_rooms_with_unsent_events().await.unwrap();
let outstanding_rooms = self.load_rooms_with_unsent_requests().await.unwrap();
assert_eq!(outstanding_rooms.len(), 2);
assert!(outstanding_rooms.iter().any(|room| room == room_id));
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
}
async fn test_send_queue_priority(&self) {
let room_id = room_id!("!test_send_queue:localhost");
// No queued event in store at first.
let events = self.load_send_queue_requests(room_id).await.unwrap();
assert!(events.is_empty());
// Saving one request should work.
let low0_txn = TransactionId::new();
let ev0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
.unwrap();
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
// Saving one request with higher priority should work.
let high_txn = TransactionId::new();
let ev1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
.unwrap();
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
// Saving another request with the low priority should work.
let low1_txn = TransactionId::new();
let ev2 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
.unwrap();
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
// The requests should be ordered from higher priority to lower, and when equal,
// should use the insertion order instead.
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 3);
{
assert_eq!(pending[0].transaction_id, high_txn);
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "high");
}
{
assert_eq!(pending[1].transaction_id, low0_txn);
let deserialized = pending[1].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low0");
}
{
assert_eq!(pending[2].transaction_id, low1_txn);
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low1");
}
}
async fn test_send_queue_dependents(&self) {
let room_id = room_id!("!test_send_queue_dependents:localhost");
// Save one send queue event to start with.
let txn0 = TransactionId::new();
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// No dependents, to start with.
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
// Save a redaction for that event.
let child_txn = ChildTransactionId::new();
self.save_dependent_queued_request(
room_id,
&txn0,
child_txn.clone(),
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn0);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].parent_key.is_none());
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Update the event id.
let event_id = owned_event_id!("$1");
let num_updated = self
.update_dependent_queued_request(
room_id,
&txn0,
SentRequestKey::Event(event_id.clone()),
)
.await
.unwrap();
assert_eq!(num_updated, 1);
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn0);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => {
assert_eq!(*eid, event_id);
});
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Now remove it.
let removed = self
.remove_dependent_queued_request(room_id, &dependents[0].own_transaction_id)
.await
.unwrap();
assert!(removed);
// It worked.
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
// Now, inserting a dependent event and removing the original send queue event
// will NOT remove the dependent event.
let txn1 = TransactionId::new();
let event1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
.unwrap();
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
self.save_dependent_queued_request(
room_id,
&txn0,
ChildTransactionId::new(),
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 1);
self.save_dependent_queued_request(
room_id,
&txn1,
ChildTransactionId::new(),
DependentQueuedRequestKind::EditEvent {
new_content: SerializableEventContent::new(
&RoomMessageEventContent::text_plain("edit").into(),
)
.unwrap(),
},
)
.await
.unwrap();
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 2);
// Remove event0 / txn0.
let removed = self.remove_send_queue_request(room_id, &txn0).await.unwrap();
assert!(removed);
// This has removed none of the dependent events.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 2);
}
}
/// Macro building to allow your StateStore implementation to run the entire
@@ -1479,137 +1558,134 @@ impl StateStoreIntegrationTests for DynStateStore {
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! statestore_integration_tests {
(with_media_tests) => {
mod statestore_integration_tests {
$crate::statestore_integration_tests!(@inner);
#[async_test]
async fn test_media_content() {
let store = get_store().await.unwrap().into_state_store();
store.test_media_content().await;
}
}
};
() => {
mod statestore_integration_tests {
$crate::statestore_integration_tests!(@inner);
}
};
use matrix_sdk_test::async_test;
use $crate::store::{
IntoStateStore, Result as StoreResult, StateStoreIntegrationTests,
};
(@inner) => {
use matrix_sdk_test::async_test;
use super::get_store;
use $crate::store::{IntoStateStore, Result as StoreResult, StateStoreIntegrationTests};
#[async_test]
async fn test_topic_redaction() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_topic_redaction().await
}
use super::get_store;
#[async_test]
async fn test_populate_store() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_populate_store().await
}
#[async_test]
async fn test_topic_redaction() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_topic_redaction().await
}
#[async_test]
async fn test_member_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_member_saving().await
}
#[async_test]
async fn test_populate_store() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_populate_store().await
}
#[async_test]
async fn test_filter_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_filter_saving().await
}
#[async_test]
async fn test_member_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_member_saving().await
}
#[async_test]
async fn test_user_avatar_url_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_user_avatar_url_saving().await
}
#[async_test]
async fn test_filter_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_filter_saving().await
}
#[async_test]
async fn test_server_capabilities_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_server_capabilities_saving().await
}
#[async_test]
async fn test_user_avatar_url_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_user_avatar_url_saving().await
}
#[async_test]
async fn test_sync_token_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_sync_token_saving().await
}
#[async_test]
async fn test_sync_token_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_sync_token_saving().await
}
#[async_test]
async fn test_utd_hook_manager_data_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_utd_hook_manager_data_saving().await;
}
#[async_test]
async fn test_utd_hook_manager_data_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_utd_hook_manager_data_saving().await;
}
#[async_test]
async fn test_stripped_member_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_stripped_member_saving().await
}
#[async_test]
async fn test_stripped_member_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_stripped_member_saving().await
}
#[async_test]
async fn test_power_level_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_power_level_saving().await
}
#[async_test]
async fn test_power_level_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_power_level_saving().await
}
#[async_test]
async fn test_receipts_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_receipts_saving().await;
}
#[async_test]
async fn test_receipts_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_receipts_saving().await;
}
#[async_test]
async fn test_custom_storage() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_custom_storage().await
}
#[async_test]
async fn test_custom_storage() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_custom_storage().await
}
#[async_test]
async fn test_stripped_non_stripped() -> StoreResult<()> {
let store = get_store().await.unwrap().into_state_store();
store.test_stripped_non_stripped().await
}
#[async_test]
async fn test_persist_invited_room() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_persist_invited_room().await
}
#[async_test]
async fn test_room_removal() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_room_removal().await
}
#[async_test]
async fn test_stripped_non_stripped() -> StoreResult<()> {
let store = get_store().await.unwrap().into_state_store();
store.test_stripped_non_stripped().await
}
#[async_test]
async fn test_profile_removal() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_profile_removal().await
}
#[async_test]
async fn test_room_removal() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_room_removal().await
}
#[async_test]
async fn test_presence_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_presence_saving().await;
}
#[async_test]
async fn test_profile_removal() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_profile_removal().await
}
#[async_test]
async fn test_display_names_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_display_names_saving().await;
}
#[async_test]
async fn test_presence_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_presence_saving().await;
}
#[async_test]
async fn test_send_queue() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue().await;
}
#[async_test]
async fn test_display_names_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_display_names_saving().await;
}
#[async_test]
async fn test_send_queue_priority() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_priority().await;
}
#[async_test]
async fn test_send_queue() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue().await;
#[async_test]
async fn test_send_queue_dependents() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_dependents().await;
}
}
};
}
+139 -158
View File
@@ -14,13 +14,11 @@
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
num::NonZeroUsize,
sync::RwLock as StdRwLock,
};
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::{instant::Instant, ring_buffer::RingBuffer};
use ruma::{
canonical_json::{redact, RedactedBecause},
events::{
@@ -31,36 +29,40 @@ use ruma::{
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
},
serde::Raw,
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomId,
OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
time::Instant,
CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId,
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
};
use tracing::{debug, instrument, trace, warn};
use super::{
traits::{ComposerDraft, QueuedEvent, SerializableEventContent},
Result, RoomInfo, StateChanges, StateStore, StoreError,
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
traits::{ComposerDraft, ServerCapabilities},
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
StateChanges, StateStore, StoreError,
};
use crate::{
deserialized_responses::RawAnySyncOrStrippedState,
media::{MediaRequest, UniqueKey as _},
MinimalRoomMemberEvent, RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
store::QueueWedgeError,
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
};
/// In-Memory, non-persistent implementation of the `StateStore`
/// In-memory, non-persistent implementation of the `StateStore`.
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct MemoryStore {
recently_visited_rooms: StdRwLock<HashMap<OwnedUserId, Vec<OwnedRoomId>>>,
composer_drafts: StdRwLock<HashMap<OwnedRoomId, ComposerDraft>>,
user_avatar_url: StdRwLock<HashMap<OwnedUserId, OwnedMxcUri>>,
sync_token: StdRwLock<Option<String>>,
server_capabilities: StdRwLock<Option<ServerCapabilities>>,
filters: StdRwLock<HashMap<String, String>>,
utd_hook_manager_data: StdRwLock<Option<GrowableBloom>>,
account_data: StdRwLock<HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
profiles: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<String, BTreeSet<OwnedUserId>>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>>,
members: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MembershipState>>>,
room_info: StdRwLock<HashMap<OwnedRoomId, RoomInfo>>,
room_state: StdRwLock<
@@ -86,40 +88,9 @@ pub struct MemoryStore {
HashMap<(String, Option<String>), HashMap<OwnedEventId, HashMap<OwnedUserId, Receipt>>>,
>,
>,
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedEvent>>>,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
impl Default for MemoryStore {
fn default() -> Self {
Self {
recently_visited_rooms: Default::default(),
composer_drafts: Default::default(),
user_avatar_url: Default::default(),
sync_token: Default::default(),
filters: Default::default(),
utd_hook_manager_data: Default::default(),
account_data: Default::default(),
profiles: Default::default(),
display_names: Default::default(),
members: Default::default(),
room_info: Default::default(),
room_state: Default::default(),
room_account_data: Default::default(),
stripped_room_state: Default::default(),
stripped_members: Default::default(),
presence: Default::default(),
room_user_receipts: Default::default(),
room_event_receipts: Default::default(),
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
custom: Default::default(),
send_queue_events: Default::default(),
}
}
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedRequest>>>,
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>>,
}
impl MemoryStore {
@@ -175,6 +146,12 @@ impl StateStore for MemoryStore {
StateStoreDataKey::SyncToken => {
self.sync_token.read().unwrap().clone().map(StateStoreDataValue::SyncToken)
}
StateStoreDataKey::ServerCapabilities => self
.server_capabilities
.read()
.unwrap()
.clone()
.map(StateStoreDataValue::ServerCapabilities),
StateStoreDataKey::Filter(filter_name) => self
.filters
.read()
@@ -255,6 +232,13 @@ impl StateStore for MemoryStore {
value.into_composer_draft().expect("Session data not a composer draft"),
);
}
StateStoreDataKey::ServerCapabilities => {
*self.server_capabilities.write().unwrap() = Some(
value
.into_server_capabilities()
.expect("Session data not containing server capabilities"),
);
}
}
Ok(())
@@ -263,6 +247,9 @@ impl StateStore for MemoryStore {
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> {
match key {
StateStoreDataKey::SyncToken => *self.sync_token.write().unwrap() = None,
StateStoreDataKey::ServerCapabilities => {
*self.server_capabilities.write().unwrap() = None
}
StateStoreDataKey::Filter(filter_name) => {
self.filters.write().unwrap().remove(filter_name);
}
@@ -352,9 +339,13 @@ impl StateStore for MemoryStore {
trace!("room state");
{
let mut room_state = self.room_state.write().unwrap();
trace!("room state: got room_state lock");
let mut stripped_room_state = self.stripped_room_state.write().unwrap();
trace!("room state: got stripped_room_state lock");
let mut members = self.members.write().unwrap();
trace!("room state: got members lock");
let mut stripped_members = self.stripped_members.write().unwrap();
trace!("room state: got stripped_members lock");
for (room, event_types) in &changes.state {
for (event_type, events) in event_types {
@@ -575,21 +566,18 @@ impl StateStore for MemoryStore {
Some(state_events.values().cloned().map(to_enum).collect())
}
Ok(get_events(
&self.stripped_room_state.read().unwrap(),
room_id,
&event_type,
RawAnySyncOrStrippedState::Stripped,
)
.or_else(|| {
get_events(
&self.room_state.read().unwrap(),
room_id,
&event_type,
RawAnySyncOrStrippedState::Sync,
)
})
.unwrap_or_default())
let state_map = self.stripped_room_state.read().unwrap();
Ok(get_events(&state_map, room_id, &event_type, RawAnySyncOrStrippedState::Stripped)
.or_else(|| {
drop(state_map); // release the lock on stripped_room_state
get_events(
&self.room_state.read().unwrap(),
room_id,
&event_type,
RawAnySyncOrStrippedState::Sync,
)
})
.unwrap_or_default())
}
async fn get_state_events_for_keys(
@@ -669,6 +657,7 @@ impl StateStore for MemoryStore {
.collect())
}
#[instrument(skip(self, memberships))]
async fn get_user_ids(
&self,
room_id: &RoomId,
@@ -697,41 +686,23 @@ impl StateStore for MemoryStore {
})
.unwrap_or_default()
}
let v = get_user_ids_inner(&self.stripped_members.read().unwrap(), room_id, memberships);
let state_map = self.stripped_members.read().unwrap();
let v = get_user_ids_inner(&state_map, room_id, memberships);
if !v.is_empty() {
return Ok(v);
}
drop(state_map); // release the stripped_members lock
Ok(get_user_ids_inner(&self.members.read().unwrap(), room_id, memberships))
}
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
StateStore::get_user_ids(self, room_id, RoomMemberships::INVITE).await
}
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
StateStore::get_user_ids(self, room_id, RoomMemberships::JOIN).await
}
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
Ok(self.room_info.read().unwrap().values().cloned().collect())
}
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>> {
Ok(self
.room_info
.read()
.unwrap()
.values()
.filter(|r| matches!(r.state(), RoomState::Invited))
.cloned()
.collect())
}
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>> {
Ok(self
.display_names
@@ -745,21 +716,18 @@ impl StateStore for MemoryStore {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
if display_names.is_empty() {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
}
let read_guard = &self.display_names.read().unwrap();
let Some(room_names) = read_guard.get(room_id) else {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
};
Ok(display_names
.iter()
.filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone())))
.collect())
Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect())
}
async fn get_account_data_event(
@@ -817,58 +785,6 @@ impl StateStore for MemoryStore {
Ok(self.custom.write().unwrap().remove(key))
}
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
// Now, let's add it.
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
let media = self.media.read().unwrap();
let expected_key = request.unique_key();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = request.unique_key();
let Some(index) = media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
(media_uri == &expected_key).then_some(position)
})
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
}
Ok(())
}
async fn remove_room(&self, room_id: &RoomId) -> Result<()> {
self.profiles.write().unwrap().remove(room_id);
self.display_names.write().unwrap().remove(room_id);
@@ -884,26 +800,27 @@ impl StateStore for MemoryStore {
Ok(())
}
async fn save_send_queue_event(
async fn save_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: OwnedTransactionId,
event: SerializableEventContent,
kind: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
self.send_queue_events
.write()
.unwrap()
.entry(room_id.to_owned())
.or_default()
.push(QueuedEvent { event, transaction_id, is_wedged: false });
.push(QueuedRequest { kind, transaction_id, error: None, priority });
Ok(())
}
async fn update_send_queue_event(
async fn update_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
content: SerializableEventContent,
kind: QueuedRequestKind,
) -> Result<bool, Self::Error> {
if let Some(entry) = self
.send_queue_events
@@ -914,15 +831,15 @@ impl StateStore for MemoryStore {
.iter_mut()
.find(|item| item.transaction_id == transaction_id)
{
entry.event = content;
entry.is_wedged = false;
entry.kind = kind;
entry.error = None;
Ok(true)
} else {
Ok(false)
}
}
async fn remove_send_queue_event(
async fn remove_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
@@ -945,18 +862,22 @@ impl StateStore for MemoryStore {
Ok(false)
}
async fn load_send_queue_events(
async fn load_send_queue_requests(
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedEvent>, Self::Error> {
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
) -> Result<Vec<QueuedRequest>, Self::Error> {
let mut ret =
self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone();
// Inverted order of priority, use stable sort to keep insertion order.
ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority));
Ok(ret)
}
async fn update_send_queue_event_status(
async fn update_send_queue_request_status(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
wedged: bool,
error: Option<QueueWedgeError>,
) -> Result<(), Self::Error> {
if let Some(entry) = self
.send_queue_events
@@ -967,14 +888,74 @@ impl StateStore for MemoryStore {
.iter_mut()
.find(|item| item.transaction_id == transaction_id)
{
entry.is_wedged = wedged;
entry.error = error;
}
Ok(())
}
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
Ok(self.send_queue_events.read().unwrap().keys().cloned().collect())
}
async fn save_dependent_queued_request(
&self,
room: &RoomId,
parent_transaction_id: &TransactionId,
own_transaction_id: ChildTransactionId,
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error> {
self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push(
DependentQueuedRequest {
kind: content,
parent_transaction_id: parent_transaction_id.to_owned(),
own_transaction_id,
parent_key: None,
},
);
Ok(())
}
async fn update_dependent_queued_request(
&self,
room: &RoomId,
parent_txn_id: &TransactionId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error> {
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
let mut num_updated = 0;
for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) {
d.parent_key = Some(sent_parent_key.clone());
num_updated += 1;
}
Ok(num_updated)
}
async fn remove_dependent_queued_request(
&self,
room: &RoomId,
txn_id: &ChildTransactionId,
) -> Result<bool, Self::Error> {
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
if let Some(pos) = dependents.iter().position(|item| item.own_transaction_id == *txn_id) {
dependents.remove(pos);
Ok(true)
} else {
Ok(false)
}
}
/// List all the dependent send queue events.
///
/// This returns absolutely all the dependent send queue events, whether
/// they have an event id or not.
async fn load_dependent_queued_requests(
&self,
room: &RoomId,
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default())
}
}
#[cfg(test)]
@@ -985,5 +966,5 @@ mod tests {
Ok(MemoryStore::new())
}
statestore_integration_tests!(with_media_tests);
statestore_integration_tests!();
}
@@ -111,8 +111,10 @@ impl RoomInfoV1 {
} = self;
RoomInfo {
version: 0,
room_id,
room_state: room_type,
prev_room_state: None,
notification_counts,
summary,
members_synced,
@@ -125,6 +127,9 @@ impl RoomInfoV1 {
base_info: base_info.migrate(create),
warned_about_unknown_room_version: Arc::new(false.into()),
cached_display_name: None,
cached_user_defined_notification_mode: None,
#[cfg(feature = "experimental-sliding-sync")]
recency_stamp: None,
}
}
}
@@ -197,6 +202,7 @@ impl BaseRoomInfoV1 {
Box::new(BaseRoomInfo {
avatar,
beacons: BTreeMap::new(),
canonical_alias,
create,
dm_targets,
@@ -208,9 +214,10 @@ impl BaseRoomInfoV1 {
name,
tombstone,
topic,
rtc_member: BTreeMap::new(),
rtc_member_events: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
pinned_events: None,
})
}
}
+103 -32
View File
@@ -21,7 +21,7 @@
//! store.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt,
ops::Deref,
result::Result as StdResult,
@@ -29,9 +29,7 @@ use std::{
sync::{Arc, RwLock as StdRwLock},
};
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::{Vector, VectorDiff};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::Stream;
use once_cell::sync::OnceCell;
@@ -57,24 +55,32 @@ use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tokio::sync::{broadcast, Mutex, RwLock};
use tracing::warn;
use crate::{
rooms::{normal::RoomInfoUpdate, RoomInfo, RoomState},
deserialized_responses::DisplayName,
event_cache::store as event_cache_store,
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
};
pub(crate) mod ambiguity_map;
mod memory_store;
pub mod migration_helpers;
mod send_queue;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::StateStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
send_queue::{
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind,
SentMediaInfo, SentRequestKey, SerializableEventContent,
},
traits::{
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, QueuedEvent,
SerializableEventContent, StateStore, StateStoreDataKey, StateStoreDataValue,
StateStoreExt,
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
},
};
@@ -169,6 +175,36 @@ impl Store {
&self.sync_lock
}
/// Load the room infos from the inner `StateStore`.
///
/// Applies migrations to the room infos if needed.
async fn load_room_infos(&self) -> Result<Vec<RoomInfo>> {
let mut room_infos = self.inner.get_room_infos().await?;
let mut migrated_room_infos = Vec::with_capacity(room_infos.len());
for room_info in room_infos.iter_mut() {
if room_info.apply_migrations(self.inner.clone()).await {
migrated_room_infos.push(room_info.clone());
}
}
if !migrated_room_infos.is_empty() {
let changes = StateChanges {
room_infos: migrated_room_infos
.into_iter()
.map(|room_info| (room_info.room_id.clone(), room_info))
.collect(),
..Default::default()
};
if let Err(error) = self.inner.save_changes(&changes).await {
warn!("Failed to save migrated room infos: {error}");
}
}
Ok(room_infos)
}
/// Set the meta of the session.
///
/// Restores the state of this `Store` from the given `SessionMeta` and the
@@ -178,10 +214,10 @@ impl Store {
pub async fn set_session_meta(
&self,
session_meta: SessionMeta,
roominfo_update_sender: &broadcast::Sender<RoomInfoUpdate>,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
{
let room_infos = self.inner.get_room_infos().await?;
let room_infos = self.load_room_infos().await?;
let mut rooms = self.rooms.write().unwrap();
@@ -190,7 +226,7 @@ impl Store {
&session_meta.user_id,
self.inner.clone(),
room_info,
roominfo_update_sender.clone(),
room_info_notable_update_sender.clone(),
);
let new_room_id = new_room.room_id().to_owned();
@@ -230,7 +266,6 @@ impl Store {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
#[cfg(not(target_arch = "wasm32"))]
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
self.rooms.read().unwrap().stream()
}
@@ -240,13 +275,19 @@ impl Store {
self.rooms.read().unwrap().get(room_id).cloned()
}
/// Check if a room exists.
#[cfg(feature = "experimental-sliding-sync")]
pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool {
self.rooms.read().unwrap().get(room_id).is_some()
}
/// Lookup the `Room` for the given `RoomId`, or create one, if it didn't
/// exist yet in the store
pub fn get_or_create_room(
&self,
room_id: &RoomId,
room_type: RoomState,
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
) -> Room {
let user_id =
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
@@ -255,10 +296,27 @@ impl Store {
.write()
.unwrap()
.get_or_create(room_id, || {
Room::new(user_id, self.inner.clone(), room_id, room_type, roominfo_update_sender)
Room::new(
user_id,
self.inner.clone(),
room_id,
room_type,
room_info_notable_update_sender,
)
})
.clone()
}
/// Forget the room with the given room ID.
///
/// # Arguments
///
/// * `room_id` - The id of the room that should be forgotten.
pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
self.inner.remove_room(room_id).await?;
self.rooms.write().unwrap().remove(room_id);
Ok(())
}
}
#[cfg(not(tarpaulin_include))]
@@ -307,8 +365,10 @@ pub struct StateChanges {
/// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`.
pub room_account_data:
BTreeMap<OwnedRoomId, BTreeMap<RoomAccountDataEventType, Raw<AnyRoomAccountDataEvent>>>,
/// A map of `RoomId` to `RoomInfo`.
/// A map of `OwnedRoomId` to `RoomInfo`.
pub room_infos: BTreeMap<OwnedRoomId, RoomInfo>,
/// A map of `RoomId` to `ReceiptEventContent`.
pub receipts: BTreeMap<OwnedRoomId, ReceiptEventContent>,
@@ -325,7 +385,7 @@ pub struct StateChanges {
/// A map from room id to a map of a display name and a set of user ids that
/// share that display name in the given room.
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
}
impl StateChanges {
@@ -344,15 +404,6 @@ impl StateChanges {
self.room_infos.insert(room.room_id.clone(), room);
}
/// Update the `StateChanges` struct with the given `AnyBasicEvent`.
pub fn add_account_data(
&mut self,
event: AnyGlobalAccountDataEvent,
raw_event: Raw<AnyGlobalAccountDataEvent>,
) {
self.account_data.insert(event.event_type(), raw_event);
}
/// Update the `StateChanges` struct with the given room with a new
/// `AnyBasicEvent`.
pub fn add_room_account_data(
@@ -419,21 +470,27 @@ impl StateChanges {
}
}
/// Configuration for the state store and, when `encryption` is enabled, for the
/// crypto store.
/// Configuration for the various stores.
///
/// By default, this always includes a state store and an event cache store.
/// When the `e2e-encryption` feature is enabled, this also includes a crypto
/// store.
///
/// # Examples
///
/// ```
/// # use matrix_sdk_base::store::StoreConfig;
///
/// let store_config = StoreConfig::new();
/// let store_config =
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
/// ```
#[derive(Clone)]
pub struct StoreConfig {
#[cfg(feature = "e2e-encryption")]
pub(crate) crypto_store: Arc<DynCryptoStore>,
pub(crate) state_store: Arc<DynStateStore>,
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
cross_process_store_locks_holder_name: String,
}
#[cfg(not(tarpaulin_include))]
@@ -445,12 +502,20 @@ impl fmt::Debug for StoreConfig {
impl StoreConfig {
/// Create a new default `StoreConfig`.
///
/// To learn more about `cross_process_store_locks_holder_name`, please read
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
#[must_use]
pub fn new() -> Self {
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
Self {
#[cfg(feature = "e2e-encryption")]
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
state_store: Arc::new(MemoryStore::new()),
event_cache_store: event_cache_store::EventCacheStoreLock::new(
event_cache_store::MemoryStore::new(),
cross_process_store_locks_holder_name.clone(),
),
cross_process_store_locks_holder_name,
}
}
@@ -468,10 +533,16 @@ impl StoreConfig {
self.state_store = store.into_state_store();
self
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::new()
/// Set a custom implementation of an `EventCacheStore`.
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
where
S: event_cache_store::IntoEventCacheStore,
{
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
event_cache_store,
self.cross_process_store_locks_holder_name.clone(),
);
self
}
}

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