Compare commits

...

1424 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
Benjamin Bouvier 10fd5d0ff6 sdk: don't clobber the DM list if we failed to deserialize the previous m.direct event
Report a deserialization failure and do not mark the room as a DM,
instead of incorrectly restarting from an empty DM list.

Fixes #3410.
2024-07-03 12:00:54 +02:00
Benjamin Bouvier 2be846669e multiverse: add Events view
This allows seeing the events directly from the room event cache. I'm
hoping this will give us some interesting insights about duplicates,
among other things.
2024-07-03 11:30:52 +02:00
Benjamin Bouvier 02b095491d integration tests: simplify maintaining temp directories alive 2024-07-03 11:30:42 +02:00
Benjamin Bouvier 68670baa30 integration test: common out the first bits of ClientBuilder creation in TestClientBuilder
DRY.
2024-07-03 11:30:42 +02:00
Benjamin Bouvier 664c71b822 integration tests: make randomize_username() the default
Most tests would randomize the username when creating a
`TestClientBuilder`; make it the default, since it's a sensible choice,
and avoids interference between different tests / test runs.

A single test required an actual non-randomized username, so a specific
way to opt out from this new default behavior has been introduced.
2024-07-03 11:30:42 +02:00
Richard van der Hoff 5d716f969d Stores: Remove StoreCipher::{en,de}crypt_value_[base64_]typed (#3638)
* Use `IndexeddbSerializer` more widely in test code

reuse `IndexeddbSerializer::maybe_encrypt_value` instead of re-inventing it.

* Rewrite `StoreCipher::decrypt_value_base64_typed`

Instead of un-base64-ing and calling `decrypt_value_typed` (which deserializes
the result`), call `decrypt_value_base64_data` (which un-base64s before
decrypting but does not deserialize the result), then deserialize.

This makes it more symmetrical with `encrypt_value_base64_typed`, and helps me
get rid of `decrypt_value_typed` (which is barely used.)

* Fix docs on `StoreCipher::encrypt_value_base64_typed`

looks like they got C&Ped from `encrypt_value_typed`.

* Inline `StoreCipher::{encrypt,decrypt}_value_typed`

Each of these are quite simple, are only used in two places, and their
existence melts my brain.

* Rewrite `IndexeddbSerializer::maybe_{encrypt,decrypt}_value`

... to use `en/decrypt_value_base64_data` instead of
`en/decrypt_value_base64_typed`.

We have to have the de/serialization code for the unencrypted case anyway, so
using the higher-level method isn't helping us much.

* Remove unused `StoreCipher::{en,de}crypt_value_base64_typed`

Outside of tests, these things are totally unused.
2024-07-03 09:42:45 +01: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
Kegan Dougal 09dc9b913d base: Add trace logging to memory store (#3642)
* Add trace logging to memory store

To help debug https://github.com/matrix-org/complement-crypto/issues/77

* Missing import
* Review comments
2024-07-02 16:20:53 +00:00
Kévin Commaille 0a7184e594 ui: Implement Clone for RepliedToInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-02 16:58:48 +02:00
Kévin Commaille 14c8a96f55 ui: Expose identifier or RepliedToInfo and EditInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-02 16:58:48 +02:00
Benjamin Bouvier fbeb77ae3e sdk-store-encryption: use ZeroizeOnDrop instead of [zeroize(drop)]
One item of https://github.com/matrix-org/matrix-rust-sdk/issues/3272.
2024-07-02 14:33:19 +02:00
Benjamin Bouvier a1b557b20a ffi(client builder): reduce indent in build_with_qr_code using a let else statement
No functional changes.
2024-07-02 14:05:09 +02:00
Benjamin Bouvier b8fc5f4764 ffi(client builder): inline build_inner into its one caller
No functional changes.
2024-07-02 14:05:09 +02:00
Benjamin Bouvier d531a7cc01 ffi(client builder): inline enable_cross_process_refresh_lock_inner into its one caller
No functional changes.
2024-07-02 14:05:09 +02:00
Benjamin Bouvier 84a57ce690 ffi(client builder): remove unused inner field from the builder 2024-07-02 14:05:09 +02:00
Benjamin Bouvier 263c86b508 send queue: use being_sent as a lock for touching storage
There were two disconnected sources of truth for the state of event to
be sent:

- it can or cannot be in the in-memory `being_sent` map
- it can or cannot be in the database

Unfortunately, this led to subtle race conditions when it comes to
editing/aborting. The following sequence of operations was possible:

- try to send an event: a local echo is added to storage, but it's not
marked as being sent yet
- the task wakes up, finds the local echo in the storage,...
- try to edit/abort the event: the event is not marked as being sent
yet, so we think we can edit/abort it
- ... having found the local echo, it is marked as being sent.

This would result in the event misleadlingly not being aborted/edited,
while it should have been.

Now, there's already a lock on the `being_sent` map, so we can hold onto
it while we're touching storage, making sure that there aren't two
callers trying to manipulate storage *and* `being_sent` at the same
time.

This is pretty tricky to test properly, since this requires super
precise timing control over the state store, so there's no test for
this. I can confirm this avoids some weirdness I observed with
`multiverse` though.
2024-07-01 16:19:48 +02:00
Benjamin Bouvier 2f125e97ee send queue: make SendHandle::abort/update more precise
Using `SendHandle::abort()` after the event has been sent would look
like a successful abort of the event, while it's not the case; this
fixes this by having the state store backends return whether they've
touched an entry in the database.
2024-07-01 16:19:48 +02:00
Benjamin Bouvier 16aa6df0b8 timeline: update test expectation after previous bugfix 2024-07-01 15:44:09 +02:00
Benjamin Bouvier 96422b3c32 send queue: wake up the sending task after editing an event
It could be that the last event in a room's send queue has been marked
as wedged. In that case, the task will sleep until it's notified again.
If the event is being edited, then nothing would wake up the task; a
manual wakeup might be required in that case.

The new integration test shows the issue; the last `assert_update` would
fail with a timeout before this patch.
2024-07-01 15:44:09 +02:00
Damir Jelić eac20766bd fixup! fix(crypto): Rename the device keys property for Olm messages 2024-07-01 15:19:04 +02:00
Damir Jelić 53cdac9661 fix(crypto): Rename the device keys property for Olm messages
Commit d41af396c implemented MSC4147, which puts the device keys into a
Olm message. It failed to adhere to the unstable prefix proposed in the
MSC:

> Until this MSC is accepted, the new property should be named
> org.matrix.msc4147.device_keys.

This patch fixes this.
2024-07-01 15:19:04 +02:00
Andy Balaam 1de3d16d25 Merge pull request #3629 from matrix-org/andybalaam/typos-specific-exceptions
typos: Exclude specific words from typo checking instead of whole files
2024-07-01 14:00:55 +01:00
Andy Balaam 858150b6b6 typos: Exclude specific words from typo checking instead of whole files 2024-07-01 13:40:52 +01:00
Damir Jelić a34e19617a recovery: Ensure that we don't miss updates to the backup state
This patch switches the way we update the recovery state upon changes in
the backup state. Previously two places updated the recovery state after
the backup state changed:

    1. A method living in the recovery subsystem that the backup
       subsystem itself calls.
    2. An event handler which is called when we receive a m.secret.send
       event.

The first method is a hack because it introduces a circular dependency
between the recovery and backup subsystems.

More importantly, the second method can miss updates, because the backup
subsystem has a similar event handler which then processes the secret we
received and if the secret was a backup recovery key, enables backups.

Depending on the order these event handlers are called, the recovery
subsystem might update the recovery state before the secret has been
handled.

The backup subsystem provides an async stream which broadcasts updates
to the backup state, letting the recovery subsystem listen to this
stream and update its state if we notice such updates fixes both
problems we listed above. The method in the first bullet point was
completely removed, the event handler is kept for other secret types but
we don't rely on it for the backup state anymore.
2024-07-01 14:38:26 +02:00
Damir Jelić 9e4164f6f7 encryption: Log when the backup and recovery state changes 2024-07-01 14:38:26 +02:00
Damir Jelić a819dd568d utils: Return the old value in the set method of ChannelObservable 2024-07-01 14:38:26 +02:00
Kévin Commaille 7fee66da11 ui: Rename EventItemIdentifier to TimelineEventItemId
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 14:32:16 +02:00
Kévin Commaille 9f929f7670 ui: Docs fixes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 14:32:16 +02:00
Kévin Commaille b9d0aa9b54 ui: Expose content of RepliedToInfo and EditInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 14:32:16 +02:00
Kévin Commaille f0867262c7 ui: Get rid of TimelineEventItemId
There is EventItemIdentifier for the same purpose.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 14:32:16 +02:00
Benjamin Bouvier 2507a450df send queue: add useful logs when aborting/editing a local echo 2024-07-01 14:12:44 +02:00
Kévin Commaille a7e4849f25 sdk: Use strongly-typed strings where it makes sense
Where a string is clearly documented as a room ID, event ID or mxc URI,
use OwnedRoomId, OwnedEventId and OwnedMxcURI, respectively.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 12:55:15 +02:00
Damir Jelić 769fe8bb7d ci: Add a github workflow to fail when fixup commits exist
We tend to use fixup commits quite a bit, and in the heat of the moment
we sometimes forget to squash them before the final merge.

This should prevent us from doing so.
2024-07-01 12:30:02 +02:00
Benjamin Bouvier 52fa00e474 sdk: remove Error::InconsistentState as it was unused 2024-07-01 08:57:09 +02:00
Benjamin Bouvier 17d18321f2 send queue: mark ConcurrentRequestFailed as recoverable
This will still disable the room's send queue, but the embedder may then
decide whether to re-enable the queue or not, based on network
connectivity. Ideally, we'd implement some retry mechanism here too, but
since we're at a different layer than the HTTP one, we can't get that
"for free", so let the embedder decide what to do here.
2024-07-01 08:56:55 +02:00
Kévin Commaille 374da7674e crypto: Remove assert_matches2 from regular dependencies
It was added as a regular dependency in #3517
but it is only used in tests.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-07-01 08:36:32 +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
Stefan Ceriu 38a18c3c8e feat(ffi): add Element specific well known struct and a way to deserialize it from external clients 2024-06-28 16:58:55 +02:00
Stefan Ceriu 70fddc0e1b feat(ffi): expose method for reading the server name part of the current user's identifier. 2024-06-28 16:58:55 +02:00
Stefan Ceriu 84796ab32a feat(ffi): expose method for sending generic GET requests through the SDK's inner HTTP client. 2024-06-28 16:58:55 +02:00
Andy Balaam 6464d21813 crypto: Storage changes for keeping sender data with InboundGroupSessions (#3556)
Signed-off-by: Andy Balaam <mail@artificialworlds.net>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-06-28 14:14:55 +02:00
Hubert Chathi cb4c575234 Reshare Megolm session after the other party unwedges (#3604)
For each recipient device, we keep an "Olm wedging index" that indicates (approximately) how many times they tried to unwedge the Olm session. When we share a Megolm session, we store the Olm wedging index. This allows us to tell whether the Olm session was unwedged since we shared the Megolm session, so that we know if we should re-share it.

We detect an attempt to unwedge the Olm session by checking if the Olm message that we decrypted is from a brand new Olm session. This is not entirely accurate, since this could happen, for example, if Alice and Bob create Olm sessions at the same time. This may result in some Megolm sessions being re-shared unnecessarily, but should not make a huge difference.
2024-06-28 10:08:58 +02:00
Benjamin Bouvier 1c92633a23 ffi: add new error conversions into ClientError
*sigh*
2024-06-27 18:53:00 +02:00
Benjamin Bouvier c3cdc4526e ffi: rename Timeline::edit to Timeline::edit_by_event_id, and introduce a more general Timeline::edit 2024-06-27 18:53:00 +02:00
Benjamin Bouvier 58e2b618b7 timeline: allow editing a local echo too 2024-06-27 18:53:00 +02:00
Benjamin Bouvier e808153473 timeline: react to send queue local echo updates (for room-message events) 2024-06-27 18:53:00 +02:00
Benjamin Bouvier d49190ade4 send queue: allow editing a local echo 2024-06-27 18:53:00 +02:00
Benjamin Bouvier 45c6efcc65 state store: add method to update the content of a send queue event
It also resets the wedged state, so that the queue will retry to send
this event later. This will show useful in the following case: when an
event is too big, we can now retry to send it, even if it was blocked,
by splitting the event instead of copy/abort/recreate it.
2024-06-27 18:53:00 +02:00
Benjamin Bouvier 43d9abae73 base: tweak SerializableEventContent::new signature
The first parameter doesn't need to be taken by ownership, reference is
sufficient.
2024-06-27 18:53:00 +02:00
Benjamin Bouvier 394effbc72 send queue: rename AbortSendHandle to SendHandle 2024-06-27 18:53:00 +02:00
Hubert Chathi d41af396cc Embed device keys in Olm-encrypted messages (#3517)
This patch implements MSC4147[1].

Signed-off-by: Hubert Chathi <hubertc@matrix.org>

[1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4147
2024-06-27 12:18:52 +02:00
Ivan Enderlin 37c125c306 Merge pull request #3615 from Hywan/feat-base-synctimelinevent-constructor
chore(base): Use constructors of `SyncTimelineEvent` to simplify code
2024-06-27 11:11:16 +02:00
Benjamin Bouvier 8e8d793f0d fixup! state store: change schema for send_queue_events table 2024-06-26 19:42:14 +02:00
Benjamin Bouvier 29ac3e0dda fixup! state store: change schema for send_queue_events table 2024-06-26 19:42:14 +02:00
Benjamin Bouvier 7f0b035e86 ffi: respawn send queue tasks just before subscribing to the client-wide send q errors 2024-06-26 19:42:14 +02:00
Benjamin Bouvier 7daf493313 send queue: improve the module doc comment 2024-06-26 19:42:14 +02:00
Benjamin Bouvier 419dd57116 tracing timers: drive-by fix recursive macro usage 2024-06-26 19:42:14 +02:00
Benjamin Bouvier a4098291af send queue: add a way to spawn tasks for all the rooms which had unsent events 2024-06-26 19:42:14 +02:00
Benjamin Bouvier 62801f1a6c state store: change schema for send_queue_events table
It turns out that so as to be able to read the room ids, they need to be
*values*, not only *keys* (since keys are one-way hashed). This means we
need to duplicate the room_id field in indexeddb/sqlite, so each entry
contains both the room_id as a key (for queries) and as a value (to
return it).

Since there's no meaningful migration we can apply, the way to go is to
drop the pending events table and recreate it from the ground up. It is
assumed that no one has used the store on indexeddb; otherwise the
workaround would be to drop and recreate it.
2024-06-26 19:42:14 +02:00
Benjamin Bouvier 24f62a5912 indexeddb: rename (de)serialize_event to (de)serialize_value 2024-06-26 19:42:14 +02:00
Ivan Enderlin d59b28e121 chore(base): Use constructors of SyncTimelineEvent to simplify code. 2024-06-26 16:59:22 +02:00
Andy Balaam ed9ab879c9 Merge pull request #3614 from zecakeh/reexport-eyeball-im
ui: Reexport eyeball-im
2024-06-26 15:22:10 +01:00
Kévin Commaille 28d6401c4f ui: Make public all timeline module errors
A few were missing in the public API like SendEventError and RedactEventError and their dependencies.
This uses a wildcard because it should be rare to not want to expose an error type.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 15:58:33 +02:00
Kévin Commaille b8c2005422 ui: Reexport eyeball-im
A user of the library needs to add this crate as a dependency just to be able to match on `VectorDiff`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 15:46:43 +02:00
Damir Jelić fefdfd2e4b test: Add a test to verify that interactive self-verification works
This also checks that secrets are gossiped from one device to the
other and that the recovery and backup states are correctly updated.
2024-06-26 14:31:31 +02:00
Damir Jelić d4bbdfd106 test(integration): Allow the creation of multiple clients for the same user 2024-06-26 14:31:31 +02:00
Kévin Commaille 973df115f8 ci: Bump the version of Rust nightly again
Should get rid of the `rewriting_static` noise when running rustfmt

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille eaf7a9e350 chore: rustfmt fixes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille dad2e6eafd ci: Bump the version of Rust nightly
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille 56739202cf sdk: Disable new rustc warnings
The issue here seems to be that
the `panic!` and `unreachable!()` macros used in the tests return `!`.
In the future, `!` will not fallback to `()`, which is what `dependency_on_unit_never_type_fallback` checks.
`add_event_handler` expects a function that returns an `EventHandlerResult`, but it is only implemented for `()`, not for `!`.

A solution could be to implement that trait for `!` but it is an unstable feature right now.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille a31d362137 crypto: Disable clippy false positives
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille 0cc7103fd9 docs: Fix indentation of list items paragraphs
Thanks to the new doc_lazy_continuation clippy lint in nightly.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:10:36 +02:00
Kévin Commaille 22171d58de sdk: Mark openidconnect crate as optional
It is only needed with the experimental-oidc and e2e-encryption features.
The former is less likely to be enabled so use it to enable the dependency.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-26 14:09:40 +02:00
Damir Jelić 3be84a5a30 refactor(sdk-crypto): Room key sharing, introduce extensible strategy
This patch set does two things:

1. Extracted the logic to collect the devices that should receive a room key.
2. Introduce a new CollectStrategy enum which defines which rules are
   used to collect recipient devices for a room key. Currently only the
   existing rules have beenmoved under this enum.
2024-06-25 16:54:25 +02:00
Valere ca6537badc Fix: post rename fix, update test relative paths for json inputs 2024-06-25 16:35:11 +02:00
Kévin Commaille 1221d151df sdk: Add support for authenticated media requests (#3598)
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-25 14:16:58 +00:00
Valere 90f92ac950 Refactor: Move file to match project module naming conventions 2024-06-25 16:01:54 +02:00
Benjamin Bouvier 4d6ee63760 sdk: retry all requests which previous response was a plain 429 without an errcode
This can happen if there's a load-balancer or any modification of the
response by a reverse proxy (e.g. rewrite 5XX errors into 429, to not
let a reverse proxy mark the upstream server as being down, as
Cloudflare seems to do).

As a result, such requests will be retried in multiple places, including
when sending something with the send queue. Also, the send queue will
mark these errors as recoverable instead of unrecoverable.

No test, because the change really is trivial and a regression test
didn't seem worth it, for once.
2024-06-25 13:47:01 +02:00
Benjamin Bouvier 58c687b71b send queue: mark reloaded local echoes as wedged or not
Part of #3361.
2024-06-25 13:30:24 +02:00
Damir Jelić 0ab0678be3 fix(sdk): when an encryption event is received, mark the room encryption as set #3602
RoomInfo::handle_state_event(...) will check this condition so we don't have to later ask the HS whether the room is encrypted or not through room::is_encrypted().

Also, as we need some way to get this info in the room list in our clients, two ways of doing this were added the FFI crate, one through RoomInfo and another one through RoomListItem.
2024-06-25 13:00:03 +02:00
Jorge Martín 0162f6ba49 Add doc clarification for ffi::room_list_item.is_encrypted() 2024-06-25 12:42:59 +02:00
Benjamin Bouvier 0701c7d652 send queue: allow sending raw events from the send queue
This requires a bit of API rejiggering, but turns out not so bad
actually.
2024-06-25 11:31:34 +02:00
Benjamin Bouvier 2855b0f6a8 sdk: bump ruma to get the bug-fix for the serialization issue related to thread replies 2024-06-25 11:31:34 +02:00
Valere 5c93372e8a refactor(sdk-crypto) - RoomKey Sharing | More test 2024-06-25 11:17:54 +02:00
Valere d6e523c1d1 refactor(sdk-crypto) - RoomKey Sharing | add settings for strategy 2024-06-25 11:16:24 +02:00
Valere 9cb068da25 refactor(sdk-crypto) - RoomKey Sharing | extract function 2024-06-25 10:30:36 +02:00
Valere 944972c27a refactor(sdk-crypto) - RoomKey Sharing | extract share module 2024-06-25 09:21:28 +02:00
Jorge Martín d8900bd6d7 Fix clippy 2024-06-24 16:18:18 +02:00
Jorge Martín 912e75c1f8 ffi: add RoomListItem::is_encrypted() function. 2024-06-24 16:01:19 +02:00
Jorge Martín 004941b6b4 ffi: add encryption info to RoomInfo 2024-06-24 16:00:29 +02:00
Jorge Martín 0bc40eadc3 sdk-base: when a m.room.encryption event is received, mark the room encryption as set.
`RoomInfo::handle_state_event` will check this condition so we don't have to later ask the HS whether the room is encrypted or not through `room::is_encrypted()`.
2024-06-24 16:00:07 +02:00
Benjamin Bouvier 74b770f4d6 send queue: use state-store backed storage for remembering events to be sent 2024-06-24 13:56:10 +02:00
Benjamin Bouvier e1e4422670 sdk: update ruma to custom fork with proper event content deserialization 2024-06-24 13:56:10 +02:00
Benjamin Bouvier 90f73195b1 send queue: introduce fallible fake state store in the send queue code 2024-06-24 13:56:10 +02:00
Richard van der Hoff 1b48bf7dc6 crypto: Log content of received m.room_key_withheld messages (#3591) 2024-06-24 11:47:53 +01:00
Doug e89659b69d ffi: Tidy up authentication.rs file.
(Nothing changed, just moving things around)
2024-06-24 10:56:04 +02:00
Doug 6d728be32d ffi: Split up AuthenticationError between ClientBuildError and a new OidcError. 2024-06-24 10:56:04 +02:00
Doug 5cbc803347 ffi: Refactor authentication_service.rs to authentication.rs 2024-06-24 10:56:04 +02:00
Doug 2d479e0177 ffi: Remove the AuthenticationService 2024-06-24 10:56:04 +02:00
Kévin Commaille 730c287201 chore: Fix new clippy nightly lints
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-23 20:20:09 +02:00
Johannes Marbach 3f1b9fe524 fix(ffi): Downgrade security-framework to 2.10.0
Fixes: #3596
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-06-21 22:59:08 +02:00
Andy Balaam 2b1bddb2f0 Merge pull request #3587 from matrix-org/doug/client-oidc-helpers
sdk: Move the OIDC helper methods from the FFI's `AuthenticationService` into `Client`
2024-06-21 10:14:35 +01:00
Doug 837cfaed48 sdk: Clarify InvalidState OIDC check and prepare abort method for the FFI. 2024-06-20 15:54:06 +01:00
Doug 655ced2c81 docs: Clarify some of the OIDC helper methods more. 2024-06-20 15:54:06 +01:00
Doug 748f40c250 sdk: Add ClientBuilder::requires_sliding_sync method. 2024-06-20 14:27:55 +02:00
Doug 8bf104c55e ffi: Add ClientBuilder::requires_sliding_sync method. 2024-06-20 14:27:55 +02:00
Doug db38d25b5b sdk: Test the OIDC helper methods. 2024-06-20 13:23:04 +01:00
Damir Jelić af141575c9 chore: Update the changelog 2024-06-20 14:06:01 +02:00
Ivan Enderlin 6701fd0685 Merge pull request #3586 from Hywan/feat-ui-room-list-new-is-infallible 2024-06-20 13:47:28 +02:00
Damir Jelić 6dde95c865 examples: Add a recovery command to the oidc-cli example
This adds support to input your recovery key to the OIDC example which
will allow the OIDC example client to be verified and have access to all
the secrets (cross-signing keys and the backup recovery key).

Not particularly useful right now, but once the OIDC example is able to
log in other devices via a QR code it becomes necessary to have access
to all the secrets.
2024-06-20 13:43:14 +02:00
Doug 082dda0b24 sdk: Add the OIDC helper methods from the FFI. 2024-06-20 12:05:23 +01:00
Benjamin Bouvier 4ee56fa62e room: rename the encrypted log field to is_room_encrypted (#3572) 2024-06-20 12:18:00 +02:00
Benjamin Bouvier c06b6d91c3 timeline: get rid of the indirection for the Unsupported* errors
No idea why we had these wrappers, and IMO they just add unnecessary
noise, so let's get rid of them.
2024-06-20 10:11:47 +02:00
Benjamin Bouvier fd9d3bd3a1 timeline: refactor edit_info/replied_to_info 2024-06-20 10:11:47 +02:00
Doug 735ae1ce75 ffi: Move OIDC logic into Client. 2024-06-19 18:17:27 +01:00
Kévin Commaille a2235d50c1 crypto: Decrypt events bundled in unsigned object of another event
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-06-19 17:40:49 +02:00
Ivan Enderlin 96f4a1f085 feat(sdk): room_list_service::Room::new is now infallible.
This patch makes `Room::new` infallible, i.e. it no longer returns a
`Result<Self, _>` but `Self` directly.
2024-06-19 17:14:56 +02:00
Ivan Enderlin 0cd93a2f06 Merge pull request #3578 from Hywan/fix-sdk-linked-chunk-substract-overflow
fix(sdk): Fix a substract with overflow in `LinkedChunk`
2024-06-19 14:05:08 +02:00
Ivan Enderlin edcd573b6f fix(sdk): Fix a substract with overflow in LinkedChunk. 2024-06-19 12:17:23 +02:00
Damir Jelić a6c962b9b0 chore: Fix the compilation of the benchmarks outside of the workspace root
The workspace root enables some features which are required to compile
the benchmarks, but if you decide to just compile the benchmarks these
features won't be enabled since they aren't specified in the Cargo.toml
file of the benchmarks.

Let's define all the required features so compilation works in both
cases.
2024-06-19 12:06:53 +02:00
Mauro 306a9f7d3b Allow edit and reply to work also for events that have not yet been paginated (#3553)
Fixes #3538 

The current implementation for send_reply and edit only work with timeline items that have already been paginated.
However given the fact that by restoring drafts, we may restore a reply to an event for timeline where such event has not been paginated, sending such reply would fail (same for the edit event).

So I reworked a bit the code here to use. only the event id, and reuse the existing timeline if available, otherwise we can fetch the event and synthethise the content and still be able to successfully send the event.

This is the third part of the breakdown of the following PR: https://github.com/matrix-org/matrix-rust-sdk/pull/3439
2024-06-19 11:58:57 +02:00
dependabot[bot] 2834fa135a build(deps): bump curve25519-dalek from 4.1.2 to 4.1.3
Bumps [curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/dalek-cryptography/curve25519-dalek/releases)
- [Commits](https://github.com/dalek-cryptography/curve25519-dalek/compare/curve25519-4.1.2...curve25519-4.1.3)

---
updated-dependencies:
- dependency-name: curve25519-dalek
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 11:41:05 +02:00
Richard van der Hoff 4822057163 sdk: Fix key backup download for ratcheted sessions (#3568)
Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3446 by checking
that we have the key for the event in question, not just the key for the
session.
2024-06-19 09:40:19 +01:00
Ivan Enderlin 6340eeac0b Merge pull request #3068 from Hywan/feat-roomlist-sorting
feat(base): Implement `Client::rooms_stream`
2024-06-19 08:27:37 +02:00
Ivan Enderlin 717c68d9b4 test(sdk): Test Client::rooms_stream.
This patch adds an integration test for `Client::rooms_stream`.
2024-06-18 17:37:08 +02:00
Ivan Enderlin 1e639c431f doc(base): Improve documentation. 2024-06-18 17:37:07 +02:00
Ivan Enderlin dd2ef57d37 feat(base): Extract ObservableMap into its own module. 2024-06-18 17:37:07 +02:00
Richard van der Hoff 565f97409e sdk: Rewrite BackupDownloadTask
Change this so that it fires off a task for each UTD event, rather than each
megolm session. This is a step towards considering the message index when
deciding whether to carry on with the download.
2024-06-18 14:17:36 +01:00
Richard van der Hoff 792402eca2 sdk: Expose WeakClient::strong_count
... allowing you to see if the client is alive without the overhead of
constructng a ref to the client.
2024-06-18 14:17:36 +01:00
Richard van der Hoff 53e3a7c242 sdk: BackupDownloadTask: use a new struct for the mpsc queue
We'll need to add more data here soon.
2024-06-18 14:17:36 +01:00
Richard van der Hoff dc2111f156 sdk: Push deserialization of UTD events down to BackupDownloadTask
This means the higher-level classes can be agnostic about the encryption
algorithm; we don't actually care about that until BackupDownloadTask.
2024-06-18 14:17:36 +01:00
Richard van der Hoff f1802d9bbe sdk: update Backups::utd_event_handler api
Make it take a `Raw<OriginalSyncRoomEncryptedEvent>`, to be consistent with
`Room::decrypt_event`.
2024-06-18 14:17:36 +01:00
Richard van der Hoff 2b5d2e97c0 sdk: add documentation on Backups::utd_event_handler 2024-06-18 14:17:36 +01:00
Benjamin Bouvier 0b9daa2702 timeline: when deduplicating event meta, keep the most recent instead of the oldest
Not doing this leads to an invalid ordering of events, as shown in the
test: if we increase the timeline limit of a room using sliding sync,
we'll receive a duplicate event, and if we ignore it, it'll be in an
invalid position. The solution is to keep the most recent event (and
remove the old one from event_meta).
2024-06-18 14:39:27 +02:00
Doug 304350ffd4 ffi: Expose room heroes (adding support for avatars). (#3503)
This PR makes 2 changes:
- Updates the storage of room heroes to be a single array containing the user's complete profile.
- Exposes these to the FFI so that client apps can use these for avatar colours/clusters.

Closes #2702 (again, now with avatars 🖼️)

---

* rooms: Store heroes as a complete user profile.

* ffi: Expose room heroes on Room and RoomInfo.

* chore: Remove TODO comment.

* Update crates/matrix-sdk-base/src/rooms/normal.rs

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

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-06-18 10:10:09 +00:00
Benjamin Bouvier 8988094df5 sdk: introduce RetryKind next to the HttpError to determine errors that should be retried
retry kind: make `characterize_retry_kind` a method of `HttpError`

---

retry kind: mark all network errors as transient

This should cause more backoff in general, if the client is disconnected
of the grid, or the remote server is, which is good behavior to have in
general.

---

http error: introduce another rustfmt-formated impl block for HttpError

The previous `impl` block was marked as skipping rustfmt, which led to
weird formatting behavior.

Changes are formatting only.
2024-06-17 17:15:07 +02:00
Benjamin Bouvier f93df8cb9e send queue: improve testing with a lil macro 2024-06-17 17:15:07 +02:00
Benjamin Bouvier ced8c4c821 send queue: add a test that simulates unreachable network/server 2024-06-17 17:15:07 +02:00
Benjamin Bouvier f45098a61d send queue: flag unrecoverable errors, and don't block the queue on those 2024-06-17 17:15:07 +02:00
Richard van der Hoff d146ba869a sdk: Refactor key-backup integration tests (#3560)
* sdk: Integ tests: factor out some helper methods

A few common methods for logic shared between the tests. This serves two
purposes:

 * It reduces duplication, this making it easier to maintain the tests by
   reducing the number of places that we have to change things.

 * It makes the tests easier to read. By factoring out discrete steps, it's
   much easier to get an overview of what a test is doing than when it's all in
   one big function.

* sdk: Integ tests: timeout if nothing happens

If the test is going to fail, let's have it do so properly rather than
mysteriously getting stuck.
2024-06-17 15:42:48 +01:00
Ivan Enderlin 01d6a9e7b7 chore(base): Make Clippy happy. 2024-06-17 15:16:09 +02:00
Ivan Enderlin e9121ab1ff feat(base): Create an ObservableMap for wasm32-unknown-unknown.
This patch creates an `ObservableMap` for `wasm32-unknown-unknown` which
simply wraps a `BTreeMap`, and without the `stream` method. Indeed, the
first implementation uses `eyeball_im::ObservableVector` which requires
`Send` and `Sync` on its values, which cannot compile to Wasm.
2024-06-17 15:16:07 +02:00
Ivan Enderlin 6e7f53dc19 feat(sdk): Implement Client::rooms_stream.
This patch implements `Client::rooms_stream` by forwarding the
result of `matrix_sdk_base::Client::rooms_stream`, and mapping the
`matrix_sdk_base::Room` to `matrix_sdk::Room`.
2024-06-17 15:15:43 +02:00
Ivan Enderlin e028710cf9 test: Fix a compiler warning if e2e-encryption and experimental-sliding-sync aren't enabled. 2024-06-17 14:46:45 +02:00
Ivan Enderlin f9e8f1178b feat(base): Implement Client::rooms_stream.
This patch implements `Client::rooms_stream`, which forwards the result
of the recently added `Store::rooms_stream` method.
2024-06-17 14:46:45 +02:00
Ivan Enderlin fd4505b026 fix(base): Define matrix_sdk_base::latest_event also if e2e-encryption is enabled.
Running `cargo test -p matrix-sdk-base --features e2e-encryption`
makes the code to fail to compile because `crate::latest_event` isn't
defined. And indeed, it must be defined if `experimental-sliding-sync`
is enabled, but also if `e2e-encryption` is enabled.
2024-06-17 14:46:45 +02:00
Ivan Enderlin 11bced4876 feat(base): Implement Store::rooms_stream.
This patch implements a new `Store::rooms_stream` method that forwards
the result of `ObservableMap::stream`.
2024-06-17 14:46:44 +02:00
Ivan Enderlin 6dac77fd5e feat(base): Implement ObservableMap::stream.
This patch basically implements `ObservableMap::stream` which returns a
batched stream of the values.
2024-06-17 14:46:44 +02:00
Ivan Enderlin a67e7f2795 feat(base): Rename Rooms to generic ObservableMap + add tests.
This patch rewrites `Rooms` to a generic `ObservableMap` struct. It also
adds documentation and tests for this type.
2024-06-17 14:46:44 +02:00
Ivan Enderlin 5870dafe2e feat(base): Make Store::rooms observable.
This patch updates `Store::rooms` from a
`Arc<StdRwLock<BTreeMap<OwnedRoomId, Room>>>` to a
`Arc<StdRwLock<Rooms>>` where `Rooms` is a new type that mimics a
`BTreeMap` but that is observable. It uses an `ObservableVector`
for saving us the costs of writing a new `ObservableMap` type in
`eyeball-im`. It would have too much implications that are clearly
not necessary. The major one being that an `ObservableMap` must emit
`MapDiff`, but we expect `VectorDiff` everywhere in the SDK where rooms
are observable. It would break too many API and too many projects.

The benchmark is coming, but here are the results (for 10'000 rooms,
extreme case):

* `get_rooms` and `get_rooms_filtered` are twice faster (in practise, it
  means 650µs is saved per call),
* `get_room` and `get_or_create_room` are 10% slower (in practise, it
  means 8-10ns is lost per call).

Overall, I believe these results are acceptable, and even an improvement
for the first one.
2024-06-17 14:46:44 +02:00
Ivan Enderlin 3ffeef55a6 Merge pull request #3561 from Hywan/fix-base-box-stream
chore(base): Remove `BoxStream` as it's never used
2024-06-17 14:46:09 +02:00
Timo cb7814b362 MatrixRTC: bump ruma to be compatible with MSC4143.
Adapt types for the new ruma version.
2024-06-17 14:21:22 +02:00
Ivan Enderlin 2ecd54a3b2 chore(base): Remove BoxStream as it's never used.
This patch removes the public type alias `BoxedStream` which is never
used in our code.
2024-06-17 13:08:59 +02:00
Ivan Enderlin b34a2cdfc9 Merge pull request #3559 from matrix-org/feat-sdk-sliding-sync-get-room-id
chore(sdk): Remove the useless `SlidingSyncList::get_room_id` method
2024-06-17 13:02:28 +02:00
Ivan Enderlin 304c225c8f chore(sdk): Remove the useless SlidingSyncList::get_room_id method.
This patch removes the useless `SlidingSyncList::get_room_id` method as
I don't see how it can be useful for anybody. It's mostly dead public
code, let's clean that up.
2024-06-17 12:13:26 +02:00
Benjamin Bouvier 72adaf4119 sdk: slightly better doc for HttpError 2024-06-17 10:50:06 +02:00
Benjamin Bouvier c656ad4068 sdk: prefer direct use of IntoHttp ctor instead of implicit #[from] conversions 2024-06-17 10:50:06 +02:00
Benjamin Bouvier c33a7b77e1 sdk: prefer direct use of RefreshToken ctor instead of implicit #[from] conversions
This might be controversial, but I do strongly prefer explicit over
implicit, in this case: it helps looking at the use cases, when using
the LSP's goto_references().
2024-06-17 10:50:06 +02:00
Benjamin Bouvier 7f64580ce2 sdk: use Error::AuthenticationRequired instead of HttpError::AuthenticationRequired
The HttpError variant was misplaced, as it was always used when the
`user_id()` is missing, which is causing `Error::AuthenticationRequired`
errors everywhere else in the code base. This patch streamlines usage of
`Error::AuthenticationRequired`, and gets rid of the `HttpError`
variant.
2024-06-17 10:50:06 +02:00
Benjamin Bouvier 08d7bd0cee sdk: get rid of the UnableToCloneRequest error which is absolutely unused 2024-06-17 10:50:06 +02:00
Ivan Enderlin 6ecefd6bc7 Merge pull request #3554 from Hywan/chore-sdk-remove-get-prefix 2024-06-13 23:59:52 +02:00
Ivan Enderlin b21b8c4a53 doc(base): Update the CHANGELOG.md file. 2024-06-13 16:52:37 +02:00
Ivan Enderlin 771ddcb91e chore(base): Remove the get_ of some Client's methods.
This patch renames `Client::get_rooms`, `::get_rooms_filtered` and
`::get_room` to respectively `::rooms`, `::rooms_filtered` and `::room`.
This `get_` prefix isn't really Rust idiomatic.
2024-06-13 16:51:20 +02:00
Ivan Enderlin 408c12fc66 doc(base): Update the CHANGELOG.md file. 2024-06-13 16:44:54 +02:00
Ivan Enderlin adff893835 feat(base): Faster Store::rooms_filtered.
Just like in https://github.com/matrix-org/matrix-rust-sdk/pull/3552,
this patch updates `Store::rooms_filtered` to not call `Store::room` so
that it doesn't lock `rooms` for each item, thus making it way faster.
2024-06-13 16:44:54 +02:00
Ivan Enderlin 81e328d090 chore: Remove the get_ of some Store's methods.
This patch renames `Store::get_rooms`, `::get_rooms_filtered` and
`::get_room` to respectively `::rooms`, `::rooms_filtered` and `::room`.
This `get_` prefix isn't really Rust idiomatic.
2024-06-13 16:40:32 +02:00
Benjamin Bouvier b1c09ed61b ffi: get rid of Timeline::latest_event()
It's unlikely to be useful as per the `Timeline` object: rather, callers
should make use of `Room::latest_event()`.
2024-06-13 16:28:40 +02:00
Ivan Enderlin 9ccf94dc3c Merge pull request #3552 from Hywan/fix-base-store-get-rooms-fast
feat(base): Make `Store::get_rooms` wayyy faster
2024-06-13 16:21:46 +02:00
Ivan Enderlin 15ab77629f feat(base): Make Store::get_rooms wayyy faster.
`Store::get_rooms` worked as follows: For each room ID, call

* Take the read lock for `rooms`,
* For each entry in `rooms`, take the room ID (the key), then call `Store::get_room`, which…
* Take the read lock for `rooms`,
* Based on the room ID (the key), read the room (the value) from `rooms`.

So calling `get_rooms` calls `get_room` for each room, which takes the lock every time.

This patch modifies that by fetching the values (the rooms) directly
from `rooms`, without calling `get_room`.

In my benchmark, it took 1.2ms to read 10'000 rooms. Now it takes 542µs.
Performance time has improved by -55.8%.
2024-06-13 16:03:25 +02:00
Ivan Enderlin 5178cbbfc7 Merge pull request #3551 from Hywan/fix-ui-roomlist-slidingsyncroom
feat(ui): `RoomListService::room` is no longer async!
2024-06-13 15:20:47 +02:00
Ivan Enderlin a7ff0587a6 fix(ui): Replace the RwLock by a Mutex in RoomListService::rooms.
This patch replaces the `RwLock` by a `Mutex` in
`RoomListService::rooms` because:

1. it removes a race condition,
2. `RwLock` was used because the lock could have been taken for a long
   time due to the previous `.await` point. It's no longer the case, so we
   can blindly use a `Mutex` here.
2024-06-13 15:03:59 +02:00
Ivan Enderlin edec6e7558 feat(ui): RoomListService::room is no longer async.
This patch makes `RoomListService::room` synchronous. It no longer reads
a `SlidingSyncRoom` from `SlidingSync`, then it not needs to be async
anymore. This patch replaces the `RwLock` of `RoomListService::rooms`
from `tokio::sync` to `std::sync`.

The patch updates all calls to `RoomListService::room` to remove the
`.await` point.
2024-06-13 14:47:43 +02:00
Ivan Enderlin 868e821427 Merge pull request #3550 from Hywan/fix-ui-timeline-duplicated-events
fix(ui): Change the behaviour when a duplicated event is received by the `Timeline`
2024-06-13 14:43:44 +02:00
Ivan Enderlin de5d80547b Merge pull request #3541 from Hywan/chore-ffi-roominfo-latest-event
chore(ffi): Remove `RoomInfo::latest_event`
2024-06-13 14:40:38 +02:00
Ivan Enderlin 5e755c5d51 fix(ui): room_list_service::Room::new no longer receives a SlidingSyncRoom.
This patch updates `room_list_service::Room::new` to take a `&Client`
and a `&RoomId` instead of a `SlidingSyncRoom`. The `SlidingSyncRoom` is
only used in `Room::default_room_timeline_builder` and is fetched there
from the `SlidingSync` instance lazily. It confines the
`SlidingSyncRoom` to one single method for `Room` now.
2024-06-13 14:39:39 +02:00
Ivan Enderlin d46e65805a chore(ui): Rename TimelineInnerState::add_event to add_or_update_event.
This patch renames `add_event` to `add_or_update_event` because it is
what it does.
2024-06-13 14:21:17 +02:00
Damir Jelić 9b05d0d822 crypto: Use the server name in the QR code login data (#3537)
Using the resolved homeserver URL causes problems if we need to inspect
the well-known configuration of the homeserver, for example, if the
server name is matrix.org, but the homeserver URL is server.matrix.org,
the well-known might be only available for the former.

This is why we also need to receive the former, i.e. the server name in
the QR code data.
2024-06-13 14:16:48 +02:00
Ivan Enderlin 01a36c90c3 chore(ffi): Remove RoomInfo::latest_event.
This patch removes `RoomInfo::latest_event`. To get the latest event,
one has to use `RoomListItem::latest_event` because it supports
local events and remote events. It was supposed to be the case of
`Room::room_info` too, but only when the method was called: Once the
`RoomInfo` was created, the latest event inside `RoomInfo`
was static, not dynamic. Also, this code wasn't tested
contrary to `RoomListItem::latest_event` which uses
`matrix_sdk_ui::room_list_service::Room::latest_event` which is itself
tested.
2024-06-13 14:13:48 +02:00
Ivan Enderlin 6da17b3f02 Merge pull request #3540 from Hywan/fix-ui-roomlist-latest-event
chore(ui): `room_list_service::Room::latest_event` no longer uses `SlidingSyncRoom` (+ drop `SlidingSyncRoomExt`)
2024-06-13 14:08:27 +02:00
Ivan Enderlin 15ed91e047 fix(ui): Change the behaviour when a duplicated event is received by Timeline.
This patch changes the behaviour of the `Timeline` when a duplicated
event is received. Prior to this patch, the old event was removed, and
the one was added. With this patch, the old event is kept, and the new
one is skipped.

This is important for https://github.com/matrix-org/matrix-rust-sdk/pull/3512
where events are mapped to the timeline item indices. When an event is
removed, it becomes difficult to keep the mapping correct.

This patch also adds duplication detection for pagination (with
`TimelineItemPosition::Start`).
2024-06-13 14:05:22 +02:00
Ivan Enderlin 6d1289482a chore(ui): Remove SlidingSyncRoomExt.
This patch removes the `matrix_sdk_ui::timeline::SlidingSyncRoomExt`
trait as it's no longer necessary since `room_list::Room::latest_event`
no longer uses `SlidingSyncRoom` to fetch the latest event.
2024-06-13 13:47:42 +02:00
Ivan Enderlin 8021d72389 feat(ui): Fetch the latest event from Room instead of SlidingSyncRoom in room_list::Room.
This patch updates `matrix_sdk_ui::room_list::Room::latest_event`
to fetch the latest event from `matrix_sdk::Room` instead of
`matrix_sdk::sliding_sync::SlidingSyncRoom`. It removes one dependency
to `SlidingSyncRoom` and it simplifies the code.
2024-06-13 12:00:22 +02:00
Johannes Marbach f770248b24 fix(bindings): use the same library name for all platforms in the xcframework
Fixes: #3528
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-06-12 17:17:36 +02:00
Benjamin Bouvier 2a79956caa ffi: call the initial events in the same task that listens to updates
This avoids a race condition where the caller hasn't set up the initial
items or the listener, and the listener is called *before* the initial
items have been used.
2024-06-12 16:16:41 +02:00
Mauro 871116eee9 feat (bindings): added an API to retrieve a loaded reply (#3534)
second part of the #3439 breakdown

The context for this API, is for the composer preview an reply without the need of it being actively in the timeline.
2024-06-12 14:00:50 +00:00
Benjamin Bouvier 03c16cb9f6 deduplicating handler: rename NotFinishedYet to Cancelled and add a few comments around the code future 2024-06-12 15:39:21 +02:00
Benjamin Bouvier 34a9289978 deduplicating handler: add a regression test for cancellation of a deduplicated query 2024-06-12 15:39:21 +02:00
Benjamin Bouvier ad63d28cfa deduplicating handler: use a new QueryState data structure 2024-06-12 15:39:21 +02:00
Benjamin Bouvier ac0992953e deduplicating handler: repeat request until it's performed at least once 2024-06-12 15:39:21 +02:00
Mauro d035eb1d90 feat (bindings): added APIs to save and load composer drafts (#3531)
This PR is the first piece of the breakdown of the following PR: https://github.com/matrix-org/matrix-rust-sdk/pull/3439 where only the core functionalities of the feature are implemented, without addressing the issue of editing and replying to events that have not yet been paginated.
2024-06-11 15:32:07 +00:00
Benjamin Bouvier 0ec90c23da base: move the initial filling of the display name cache into the sync methods
It's not perfect, but it's honest work.
2024-06-11 15:44:53 +02:00
Benjamin Bouvier c72384f7d4 base: don't regenerate the display name when creating a room, store the cached display name in RoomInfo
So revert a few changes to make some functions async, etc.
2024-06-11 15:44:53 +02:00
Benjamin Bouvier 5a5a5797e9 ci: fix flakeyness of test_room_notification_count in an innovative way
The test looks at updates of `RoomInfo`s, but these depends on external
factors like the server sending them in one part or multiple ones.

Instead of trying to figure out which partial updates the server sent,
wait for the stream of `RoomInfo`s to stabilize by trying to get the
latest item from the stream, then wait up to N seconds for another item
to show up, and continue as long as items come in.

This should allow us to get rid of some code that was required to
prevent flakey updates.
2024-06-11 15:44:53 +02:00
Benjamin Bouvier e22162a23a base: rename computed_display_name to compute_display_name and remove computed_ in the cached one 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 73f977986e sdk: use the cached computed display name in more places 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 2d444284cc base: rename calculate_room_name to compute_display_name_from_heroes 2024-06-11 15:44:53 +02:00
Benjamin Bouvier c0c9d16412 ui: make use of the cached computed display name in the room name matchers 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 78a1634341 base: add a cache to the computed display name 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 78e2ab9d99 base: make get_or_create_room async
Since this is a good place where to recompute the cached display name at
start.
2024-06-11 15:44:53 +02:00
Benjamin Bouvier f1553ae7f8 sliding sync: add some simple positive tests for room names too 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 9a1a3e67c3 base(code motion): move calculate_room_name into the one file where it's used 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 82ae74fee9 base: get rid of intermediate calculate_name() function 2024-06-11 15:44:53 +02:00
Benjamin Bouvier 8d66d1bab0 base: fix guessed number of members as it should relate to joined users, not joined+invited
The previous code would use `ACTIVE` which means "either joined or
invited" members, but the code thereafter considered this to be the
number of joined members.

Also replaced a call to `self.members()` (which does a lot of work under
the hood) with a call to `self.joined_user_ids()`, since we only
retrieve the `.len()` of the resulting vector.
2024-06-11 15:44:53 +02:00
Benjamin Bouvier 66d43c1088 base: directly call calculate_room_name since it's a free-function
Instead of calling it through `BaseRoomInfo::calculate_room_name`.
2024-06-11 15:44:53 +02:00
Denis Kasak ad79f9c0ac docs: Update message key forwarding algorithm model.
- Change incorrect "key sharing" references to "key forwarding".
- Explain that this is a feature that needs to be enabled.
- Regenerate the key forwarding algorithm diagram.
- Provide a spec link and mention alternative names.
2024-06-11 14:51:32 +02:00
Richard van der Hoff 1510f87fa0 UTD hook: stop re-reporting of UTDs on app relaunch (#3519)
Use a Bloom filter to keep track of which events we have already reported to the parent UTD hook.
We load the data from database on startup, and flush it out immediately on every update.
2024-06-10 18:56:35 +01:00
Ivan Enderlin a2f868d1a7 Merge pull request #3527 from Hywan/chore-ui-timeline-event-handler-add
chore(ui): Rewrite a bit `TimelineEventHandler::add`
2024-06-10 18:31:51 +02:00
Ivan Enderlin 17befab414 chore(ui): Rewrite a bit TimelineEventHandler::add.
This patch renames `TimelineEventHandler::add` to `add_item`.

This patch also removes the `should_add` argument. The `add_item`
is called conditionally everytime. I believe this is much cleaner.
Otherwise the method should have been called `maybe_add_item` with a
return type or something that indicates whether the item is added. This
patch makes it clear and remove one possible state in `add_item`.
2024-06-10 18:18:05 +02:00
Ivan Enderlin abda959796 chore(ui): Make TimelineInnerMetadata::next_internal_id private
chore(ui): Make `TimelineInnerMetadata::next_internal_id` private
2024-06-10 10:21:54 +02:00
Ivan Enderlin 1a343d93e7 chore(ui): Remove a useless clone
chore(ui): Remove a useless clone
2024-06-10 10:19:45 +02:00
Ivan Enderlin 2a70587dda chore(ui): Make TimelineInnerMetadata::next_internal_id private.
This patch updates `TimelineEventHandler` to re-use the public API and
avoid using `TimelineInnerMeta::next_internal_id`. The consequence is
that `next_internal_id` can now be private instead of public. It's
better to have isolated API in this code.
2024-06-10 10:06:08 +02:00
Ivan Enderlin d403c12464 chore(ui): Remove a useless clone.
This patch removes a useless clone of `TimelineInner::items`. This clone
was technically cheap because it's a `Vector` behind the scene, which is
cheap to clone, but still, it's not necessary at all to clone it.
2024-06-10 10:03:49 +02:00
Benjamin Bouvier e5487da160 send queue: control enabled on a per-room basis in addition to globally 2024-06-10 09:44:44 +02:00
Benjamin Bouvier 9c1d62a039 sdk: finish renaming "sending queue" to "send queue" 2024-06-10 09:21:35 +02:00
Ivan Enderlin 1c9171d917 Merge pull request #3515 from Hywan/doc-sdk-sliding-sync-mode-typo 2024-06-10 07:13:27 +02:00
Timo 931c564942 widget API(ffi): Add StateEventType::RoomEncryption to capabilities (#3506)
also add TODO comment to remove `org.matrix.msc3401.call` once possible.
2024-06-06 19:31:56 +02:00
Benjamin Bouvier 632de475a1 send queue: try to avoid a race when disabling then aborting a send 2024-06-06 18:30:15 +02:00
Benjamin Bouvier a21ea06bed day dividers: demote an assert to an error message
Because of the task cancellation that can happen at any place in the
code base, it's really hard to predict in which situation a
day-divider-adjuster should have run or not, so just demote the assert
to an error. The intent is that, if we see errors with day dividers in
the future, they'll be reported along rageshakes and we'll notice this
in the logs.
2024-06-06 18:05:21 +02:00
Benjamin Bouvier 66330a20b3 sdk: rename "sending queue" to "send queue" 2024-06-06 17:56:17 +02:00
Doug 3f272a72c4 chore: Update some docs on the authentication service. 2024-06-06 17:01:46 +02:00
Doug 81b55a4ca8 ffi: Use a single client in the authentication service.
We no longer need the in-memory store as we provide a path for the new session.
2024-06-06 17:01:46 +02:00
Doug d43812ac04 ffi: Replace the Client's base_path with a session_path.
The SDK no longer handles subdirectories for the user, this becomes the app's responsibility.
2024-06-06 17:01:46 +02:00
Doug 2c87333106 oidc: Supply a direct path to the registrations file instead of a base path. 2024-06-06 17:01:46 +02:00
Ivan Enderlin 1aacbaf681 doc(sdk): Fix a formatting typo. 2024-06-06 11:34:38 +02:00
Benjamin Bouvier 5093af87b1 clippy: disallow useless asyncs (#3513)
Using async when not required will increase compile times, and propagate async-ness to the callers, transitively.

See also the [lint description](https://rust-lang.github.io/rust-clippy/master/#/unused_async).

Since we only had a few false positives, I've enabled it by default.
2024-06-05 17:13:10 +02:00
Benjamin Bouvier 23cc1e31c1 ffi: make more methods async, rename a few variables/functions, etc. 2024-06-05 14:58:41 +02:00
Benjamin Bouvier 96824055f8 ffi: add new method to retrieve a timeline item by transaction id 2024-06-05 14:58:41 +02:00
Benjamin Bouvier cb9e2bda92 timeline: mark local echoes as not editable
Also reunify two methods that did the same thing, with slightly
different semantics, and test the one that wasn't tested at all.

Note that `is_editable()` was already exposed to the FFI and used in EX
apps.
2024-06-05 14:58:41 +02:00
Benjamin Bouvier 04befa3ab5 timeline: get rid of the Cancelled state
It was used after a previous local echo couldn't be sent (i.e. the
sending queue failed to send it). However, it was slightly incorrect to
mark those as cancelled, since those local echoes would still have
corresponding items in the sending queue, and would be retried as soon
as the sending queue was reenabled and could send events.

Instead, this is removed: it means that previously cancelled events
would be in the NotSentYet state, which is correct. (At the limit, we
could even get rid of the SendingFailed variant, since the sending queue
will automatically retry as soon as possible.)
2024-06-05 14:58:41 +02:00
Benjamin Bouvier fd15333ba2 timeline: add a redact method to get rid of local or remote events 2024-06-05 14:58:41 +02:00
Benjamin Bouvier 68c61ef8e1 send queue: add an abort handle to all local echoes 2024-06-05 14:58:41 +02:00
Benjamin Bouvier fd9266e960 ffi: expose the abort handle after sending an event 2024-06-05 14:58:41 +02:00
Benjamin Bouvier 5ea7ef01e5 ffi: expose the global sending queue primitives 2024-06-05 14:58:41 +02:00
Benjamin Bouvier 3693c582c3 integration tests: update reactions test to check if an event is a local echo
The test relied on the fact that sending an event from a given timeline
is not observable from another timeline. Indeed, it sent a message using
a first timeline object, then constructed a second timeline object and
expected only the remote event to be in there.

Now, the sending queue is shared across all instances of a Room, thus
all instances of a Timeline, and the second timeline can see the local
echo for the message sent by the first timeline.

The "fix" is thus in the test structure itself: when waiting for the
remote echo to be there, check that the timeline item doesn't pertain to
a local echo, i.e. is a remote echo.
2024-06-05 14:58:41 +02:00
Benjamin Bouvier 8101879799 timeline: clarify in some names that events are remote 2024-06-05 14:58:41 +02:00
Benjamin Bouvier ec95ca6ed1 timeline: only spawn the local echo listener if we're a live timeline
Technically, the test already passed before the change in the builder,
because `TimelineEventHandler::handle_event` already filters out local
events on non-live timelines, but it's a waste of resources to even
spawn the local echo listener task in that case, so let's not do it.
2024-06-05 14:58:41 +02:00
Benjamin Bouvier b88381a289 timeline: use the new sending queue mechanism to send and receive local echoes 2024-06-05 14:58:41 +02:00
Benjamin Bouvier 3917ba67c9 ffi: add a reference to the FFI Client to the Encryption API object
By keeping a reference to the FFI Client, we make sure that the SDK
Client is dropped while in a tokio runtime context. This makes it
possible for an `Encryption` (FFI) object to outlive the `Client` (FFI)
object without crashing (because deadpool requires to be in a tokio
runtime context when dropping).
2024-06-05 13:48:39 +02:00
Benjamin Bouvier c1030562c3 authentication service: inline details_from_client() 2024-06-05 13:48:39 +02:00
Benjamin Bouvier e6185b3827 authentication service(ffi): don't set the base_path for the temporary discovery client
Following https://github.com/matrix-org/matrix-rust-sdk/pull/3473, we
made it so that if there's a base-path but no username, then we'll
create a sqlite database.

Unfortunately, this introduced a bad side-effect: for the temporary
client used during homeserver resolution, this would create a temporary
sqlite database.

The "fix" is to not set the base path for the temporary client, and only
for the other caller of `new_client_builder()`, manually. The long term
fix is to get rid of the `AuthenticationService` so we can test it
properly.
2024-06-05 13:48:39 +02:00
Benjamin Bouvier 13df762dc1 ffi: add more logs related to the path in the client builder 2024-06-05 13:48:39 +02:00
Ivan Enderlin ec5f94f0b4 feat(base): Update Ruma to get RoomSummary::heroes as Vec<OwnedUserId>
feat(base): Update Ruma to get `RoomSummary::heroes` as `Vec<OwnedUserId>`
2024-06-05 10:51:38 +02:00
Ivan Enderlin 420a69f9de feat(base): Update Ruma to get RoomSummary::heroes as Vec<OwnedUserId>.
This patch updates Ruma to 75e8829 so that `RoomSummary::heroes` is now
a `Vec<OwnedUserId>` instead of a `Vec<String>`. This patch updates our
code accordingly by removing the parsing of heroes as `OwnedUserId`.
2024-06-05 10:38:03 +02:00
Benjamin Bouvier ec5f5bc104 room preview integration test: explicitly request some state events to sync
Another attempt at fixing #3483.
2024-06-05 10:08:52 +02:00
Benjamin Bouvier 9d46da9f11 clippy: fix additional errors
We should investigate why some errors were not found in CI tasks,
though.
2024-06-05 09:34:31 +02:00
Benjamin Bouvier 13f2c89e06 xtask: force --target-applies-to-host when running Clippy check
Without this, the configs from `.cargo/config.toml` were not read in CI
tasks, causing false positives when running Clippy on CI (i.e. there
were issues observed when compiling locally that were not found when
compiling remotely).

Not entirely sure why it's needed, because I'm seeing the issues when
I'm using `cargo xtask ci clippy` locally, with nothing changed.
2024-06-05 09:34:31 +02:00
Ivan Enderlin d4df2624ff Merge pull request #3444 from surakin/encrypted-stickers
Expose new sticker source field (and drop url field)
2024-06-05 09:04:40 +02:00
Ivan Enderlin 37098bbb43 Merge pull request #3510 from maan2003/push-zluyyrrmtqpz
fix: bump deadpool-sync
2024-06-05 08:52:59 +02:00
Ivan Enderlin 1913d012ee Merge pull request #3509 from matrix-org/jplatte/unexpected-cfgs
Configure unexpected-cfgs via Cargo.toml instead of build scripts
2024-06-05 08:50:21 +02:00
maan2003 80ccbd045f fix: bump deadpool-sync 2024-06-05 03:18:53 +05:30
Jonas Platte b73dbb4a55 Bump nightly version 2024-06-04 23:45:33 +02:00
Jonas Platte 02185f7034 Configure unexpected-cfgs via Cargo.toml instead of build scripts 2024-06-04 23:34:38 +02:00
Marco Antonio Alvarez dadca42776 expose sticker source field
Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com>

go back to old ruma rev
2024-06-04 15:18:04 +02:00
Benjamin Bouvier 5d549f1714 integration test: raise the sync time before considering it as stable
It turns out that the failure came when using the known room path in the
room preview: Alice knows about the room, but for some reason the client
didn't retrieve all the state events from the sliding sync proxy yet.

Before, the sync would be considered stable after 2 seconds. This is too
little, considering that events come from the proxy that listens to
events from synapse. Raising this threshold to 15 seconds should help
getting all the room information from the proxy, and thus get all the
information we expected in the client.
2024-06-04 14:57:26 +02:00
Benjamin Bouvier 77f0b6a4e2 pagination: don't hold onto the waited_for_initial_prev_token lock for too long
While this mutex is taken, in `oldest_token()` we can get a hold of the
RoomEvent mutex, making it so that the `waited_...` token covers the
critical region of room events.

Unfortunately, in `clear()`, we're taking these two locks in the
opposite order, implying that the room events critical region now
overlaps that of the `waited_...` lock.

The way to break the cycle is to keep the `waited_` token for as short
as possible.
2024-06-04 11:46:37 +02:00
Benjamin Bouvier bffcb26e7d multiverse: add support for sending messages and {en|dis}abling the sending queue 2024-06-03 17:42:30 +02:00
Benjamin Bouvier 78608a1b9d sending queue: do a few renamings after the live review
Thanks @Hywan for the review comments!
2024-06-03 15:30:56 +02:00
Benjamin Bouvier 75aba1d17f clippy: declare victory over clippy
So as to not use sync mutexes across await points, we have to use an
async mutex, BUT it can't be immediately called in a wiremock responder,
so we need to shoe-horn a bit: create a new tokio runtime, which can
only be called from another running thread, etc. It's a bit ugly, but I
couldn't find another mechanism to block the responder from returning a
Response immediately; happy to change that anytime if there's a simpler
way.
2024-06-03 15:30:56 +02:00
Benjamin Bouvier c5e8cb71b5 sending queue: add better logging for debug purposes 2024-06-03 15:30:56 +02:00
Benjamin Bouvier a49b3a1b61 sending queue: remove spurious async 2024-06-03 15:30:56 +02:00
Benjamin Bouvier 0e8a707311 sending queue: add a global way to observe the enablement state
A client would require this to know that the sending queue has been
globally disabled, across all the rooms, otherwise they couldn't know
that it needs to be re-enabled later on.
2024-06-03 15:30:56 +02:00
Benjamin Bouvier 2cf36c7a5e sdk: add a sending queue for handling sending room events in the background
This implements the following features:

- enabling/disabling the sending queue at a client-wide granularity,
- one sending queue per room, that lives until the client is shutting
down,
- retrying sending an event three times, if it failed in the past, with
some exponential backoff to attempt to re-send,
- allowing to cancel sending an event we haven't sent yet.

fixup sdk
2024-06-03 15:30:56 +02:00
Benjamin Bouvier 001e88a60d sdk: add an optional RequestConfig to the SendMessageLikeEvent futures 2024-06-03 15:30:56 +02:00
Benjamin Bouvier 4ec34ad25d ffi: get rid of get_timeline_event_content_by_event_id
This can be equivalently retrieved using:

`get_timeline_event_by_event_id(event_id).content().as_message()?.content()`

As per the commit date, this is used in one place in both EXA and EXI,
so it seems fine to change the caller's call sites.

And this avoids one explicit call site of
`Timeline::item_by_event_id()`.
2024-06-03 15:28:17 +02:00
Ivan Enderlin 03b58ea1df Merge pull request #3491 from Hywan/feat-sdk-event-cache-pagination-through-update-2
feat(sdk): Add `RoomPagination::run_backwards(…, until)`, take 2
2024-06-03 10:28:49 +02:00
Ivan Enderlin 59924ea03e doc(sdk): Fix Control to ControlFlow. 2024-06-03 09:11:47 +02:00
Ivan Enderlin b85c819414 Reapply "feat(sdk): Add RoomPagination::run_backwards(…, until)"
This reverts commit 392fd004d9.
2024-06-03 09:09:57 +02:00
Benjamin Bouvier 6012c7d98b chore: remove unused dependencies
Thanks cargo-machete.
2024-05-31 17:26:57 +02:00
Richard van der Hoff b029519248 utd hook: fix re-reporting of late-decrypted events (#3480)
If an event cannot be decrypted after a grace period, it is reported to the application (and thence Posthog) as a UTD event. Currently, if it is then successfully decrypted, the event is then re-reported as a "late decryption".

This does not match the expected behaviour in Posthog - an event is *either* a UTD, or a late decryption; it makes no sense for it to be both. This PR fixes the problem.

I've attempted to make the commits sensible, but I'm not entirely sure I've succeeded. The tests do pass after each commit, though. The interesting change itself is somewhere in the middle; there is non-functional groundwork before and cleanup afterwards.

---

* ui: Factor out UTD report code to a closure

For now, this doesn't help much, but in future there will be more logic here,
and it helps reduce the repetition between the delay and no-delay cases.

* ui: Convert UtdHookManager::pending_delayed to a HashMap

* ui: Store decryption time in `UtdHookManager::pending_delayed`

This is a step on getting rid of `known_utds`

* ui: Fix re-reporting of late decryptions

This fixes the problem where a message that was previously reported as a UTD,
and was then subsequently successfully decrypted, is then re-reported as a late
decryption. This artificially inflated the UTD metrics.

We do this by checking the `pending_delayed` list in `on_late_decrypt`, instead
of the `known_utds` list. There is some associated reordering of code to get
the locking right.

* ui: Remove unused "utd report time" from `UtdHookManager::known_utds`

* ui: Replace `UtdHookManager::known_utds` with `reported_utds`

Keep a list of the UTDs we've actually reported, rather than the union of those
we've reported together with those we might report in a while.

I find this much easier to reason about.

* Address minor review comments

* Reinstate assertion in UTD hook tests

* Reinstate `known_utds`
2024-05-31 14:24:38 +02:00
Stefan Ceriu 61440c3561 ffi: enable panic logging for all supported platforms not only android 2024-05-31 09:06:24 +02:00
Jonas Platte 9c4e547f5a ci: Error if coverage report files are not found 2024-05-30 20:22:47 +02:00
Jonas Platte fa3db13cec ci: Update coverage workflows to use the right file 2024-05-30 20:22:47 +02:00
Jonas Platte eff57ad483 ci: Use one artifact for all coverage files
Instead of three separate artifacts.
2024-05-30 20:22:47 +02:00
Jonas Platte 2c964b07ab ci: Fix bash syntax in last code_coverage job step
It was trying to execute `upload_coverage.yml` before.
2024-05-30 20:22:47 +02:00
Jonas Platte 8c5d60e539 ci: Normalize indentation in upload_coverage.yml 2024-05-30 20:22:47 +02:00
Jonas Platte 63d4b758f8 ci: Only run upload_coverage if coverage workflow succeeded 2024-05-30 20:22:47 +02:00
Jonas Platte 7876c37715 Fix trigger condition for new upload_coverage workflow 2024-05-30 15:25:41 +02:00
Damir Jelić 29f29e981e chore: Go back to depending on Ruma from the Ruma repo 2024-05-30 15:12:45 +02:00
Kévin Commaille 2163b282b5 sdk: Add method to request user identity from homeserver (#3469)
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-30 14:40:21 +02:00
Jonas Platte 40dc706270 Split codecov uploads into a separate workflow
… which runs in the context of the main repo even for PRs, and can be
retried individually without rerunning coverage collection.
2024-05-30 13:53:49 +02:00
Hubert Chathi 0c97c85aea add tests for unknown properties in device keys 2024-05-30 12:16:07 +02:00
Kévin Commaille 0db486b511 crypto: Add SasState::Created variant
To differentiate the SAS state between the party
that sent the verification start and the party that received it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-30 12:09:46 +02:00
Stefan Ceriu 7551d87384 feat: add support for m.call.notify events in the timeline and as a last room message 2024-05-30 11:27:30 +02:00
Damir Jelić 3ac598666a bindings: Remove the InvalidQrCode error variant for the QR login error type. (#3481)
We have a separate error for the QR code decoding method.
2024-05-29 14:40:22 +02:00
Damir Jelić 1380af4f00 bindings: Even more precise errors for the QR code login 2024-05-29 13:45:55 +02:00
Damir Jelić ab08f5e095 bindings(qr): Add an error variant for when the other device is not signed in 2024-05-29 13:45:55 +02:00
Damir Jelić 59d1f349b5 bindings: Expose the method to log in the client using a QR code
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-05-29 08:59:23 +02:00
Damir Jelić 3dbf8a07bd bindings: Move the OIDC metadata validation method into a TryInto method 2024-05-29 08:59:23 +02:00
Benjamin Bouvier 392fd004d9 Revert "feat(sdk): Add RoomPagination::run_backwards(…, until)" 2024-05-28 18:30:43 +02:00
Ivan Enderlin 72314f1014 feat(sdk): Add RoomPagination::run_backwards(…, until)
feat(sdk): Add `RoomPagination::run_backwards(…, until)`
2024-05-28 16:03:17 +02:00
Ivan Enderlin 9a1de1d9e8 test(sdk): Improve tests around RoomPagination::run_backwards. 2024-05-28 15:43:01 +02:00
Damir Jelić 7dd08c3b81 Add an example for the QR code login 2024-05-28 12:43:49 +02:00
Damir Jelić e6dc24a914 feat: Add support to log in using a QR code
This implements one part of MSC4108[1], it implements the case where the
new device scans the QR code.

[1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
2024-05-28 12:43:49 +02:00
Damir Jelić 05f9f85d2d Make errors public 2024-05-28 12:43:49 +02:00
Damir Jelić 96c5515b04 Add a method to ensure device keys are uploaded 2024-05-28 12:43:49 +02:00
Damir Jelić e94b40a75f chore: Add the import secrets bundle method 2024-05-28 12:43:49 +02:00
Damir Jelić dc60c1ee10 chore: Allow setting a custom vodozemac account 2024-05-28 12:43:49 +02:00
Damir Jelić 3e6347cd48 chore: Create a subdirectory for authentication 2024-05-28 12:43:49 +02:00
Ivan Enderlin 7377971da8 test(sdk): Ensure that until can trigger multiple iterations.
This patch adds a test for `until` when it returns
`ControlFlow::Continue` multiple times instead of `ControlFlow::Break`
immediately.
2024-05-28 11:34:05 +02:00
Ivan Enderlin 13df5d9db8 feat(sdk): Add TimelineHasBeenResetWhilePaginating.
This patch adds a new argument to the `until` argument closure of
`RoomPagination::run_backwards`: `timeline_has_been_reset`, which
designates when the timeline has been reset.

A test has been updated accordingly.
2024-05-28 11:32:24 +02:00
Ivan Enderlin 664a64b3f3 feat(sdk): Improve RoomEventCacheUpdate (#3471)
* chore(sdk): Rename `RoomEventCacheUpdate::UpdateReadMarker`.

This patch renames `RoomEventCacheUpdate::UpdateReadMarker` to
`ReadMarker`. The `Update` prefix is already part of the enum name.

* feat(sdk): Rename `RoomEventCacheUpdate::ReadMarker::event_id` to `move_to`.

This patch renames `RoomEventCacheUpdate::ReadMarker::event_id` to
`ReadMarker::move_to` as I feel like it conveys a better semantics.

* feat(sdk): Extract `RoomEventCacheUpdate::Append::ambiguity_changes`.

This patch extracts `RoomEventCacheUpdate::Append::ambiguity_changes`
into a new variant `RoomEventCacheUpdate::Members { ambiguity_changes }
`.

This patch also creates a new private
`RoomEventCacheInner::send_grouped_updates_for_events` method to ensure
the updates are sent in a particular order.

* feat(sdk): Rename `RoomEventCacheUpdate::Append`.

This patch renames `RoomEventCacheUpdate::Append` to `SyncEvents`. The
field `events` is renamed `timeline`.

This patch also renames some variables to clarify the code and to match
the renamings in `RoomEventCacheUpdate`.

* feat(sdk): Rename `RoomEventCacheUpdate::ReadMarker` and `Members`.

This patch renames `ReadMarker { move_to: … }` to `MoveReaderMarkerTo
{ event_id: … }`.

This patch also renames `Members` to `UpdateMembers`.

Finally, this patch renames some other variables to avoid clashes with
terminology in `matrix_sdk_ui`.

* feat(sdk): Split `RoomEventCacheUpdate::SyncEvents`.

This patch splits `RoomEventCacheUpdate::SyncEvents` into 2 new variants:
`AddTimelineEvents` and `AddEphemeralEvents`.

This patch takes this opportunity to update `matrix_sdk_ui::timeline`
a little bit too. `handle_sync_events` is renamed
`handle_ephemeral_events`, and the `SyncTimelineEvent` argument is
removed: it's possible to use `add_events_at` directly to handle the
`SyncTimelineEvent`s.

* fix(sdk): Do not send `RoomEventCacheUpdate` if values are empty.

This patch prevents sending useless `RoomEventCacheUdpate` if their
respective values are empty.

* chore(ui): Update a log message.

* Apply suggestions from code review

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

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-05-28 09:26:38 +00:00
Ivan Enderlin 6c90f7aaff feat(sdk): Add RoomPagination::run_backwards(…, until).
This patch adds a new argument to `RoomPagination::run_backwards`:
`until`. It becomes:

    pub async fn run_backwards<F, B, Fut>(&self,
        batch_size: u16,
        mut until: F
    ) -> Result<B>
    where
        F: FnMut(BackPaginationOutcome) -> Fut,
        Fut: Future<Output = ControlFlow<B, ()>>,

The idea behind `until` is to run pagination _until_ `until` returns
`ControlFlow::Break`, otherwise it continues paginating.

This is useful is many scenearii (cf. the documentation). This is
also and primarily the first step to stop adding events directly from
the pagination, and starts adding events only and strictly only from
`event_cache::RoomEventCacheUpdate` (again, see the `TODO` in the
documentation). This is not done in this patch for the sake of ease
of review.
2024-05-28 10:55:13 +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
524 changed files with 76216 additions and 26352 deletions
-46
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,41 +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",
]
[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 }}
+2 -2
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
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-05-09
toolchain: nightly-2024-06-25
components: rustfmt
- name: Run Benchmarks
+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
+26 -26
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
@@ -279,7 +288,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-05-09
toolchain: nightly-2024-06-25
components: rustfmt
- name: Cargo fmt
@@ -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
@@ -316,7 +323,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-05-09
toolchain: nightly-2024-06-25
components: clippy
- name: Load cache
@@ -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
+33 -24
View File
@@ -1,4 +1,4 @@
name: Code coverage
name: Code Coverage
on:
push:
@@ -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
@@ -110,12 +102,29 @@ jobs:
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
- name: Get PR number and commit SHA
run: |
echo "Storing PR number ${{ github.event.number }}"
echo "${{ github.event.number }}" > pr_number.txt
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
# This stores the coverage report and metadata in artifacts.
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
- name: Store coverage report in artifacts
uses: actions/upload-artifact@v4
with:
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
# Otherwise not required for public repos.
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
# The upload sometimes fails due to https://github.com/codecov/codecov-action/issues/837.
# To make sure that the failure gets flagged clearly in the UI, fail the action.
fail_ci_if_error: true
name: codecov_report
path: |
cobertura.xml
pr_number.txt
commit_sha.txt
if-no-files-found: error
- run: |
echo 'The coverage report was stored in Github artifacts.'
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
+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
+1 -4
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
@@ -37,7 +36,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-05-09
toolchain: nightly-2024-06-25
- name: Install Node.js
uses: actions/setup-node@v4
@@ -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
+12
View File
@@ -0,0 +1,12 @@
name: Git Checks
on: [pull_request]
jobs:
block-fixup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+79
View File
@@ -0,0 +1,79 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
# of `coverage` workflow.
workflow_run:
workflows: ["Code Coverage"]
types:
- completed
jobs:
coverage:
name: Upload coverage report
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@v7
with:
script: |
var fs = require('fs');
// List artifacts of the workflow run that triggered this workflow
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "codecov_report";
});
if (codecovReport.length != 1) {
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
}
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: codecovReport[0].id,
archive_format: 'zip',
});
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
- id: parse_previous_artifacts
run: |
unzip codecov_report.zip
echo "Detected PR is: $(<pr_number.txt)"
echo "Detected commit_sha is: $(<commit_sha.txt)"
# Make the params available as step output
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
+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

+4 -2
View File
@@ -25,6 +25,10 @@ ratatui = "ratatui"
# base64 false positives
Nd = "Nd"
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 = [
@@ -32,6 +36,4 @@ extend-exclude = [
"*.json",
# Fuzzy match patterns that can be understood as typos confusingly.
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
# Hand-crafted base64 session keys that are understood as typos.
"crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs",
]
+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
+842 -1268
View File
File diff suppressed because it is too large Load Diff
+59 -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,59 +31,65 @@ 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/Hywan/ruma", branch = "feat-sliding-sync-list-include-heroes", features = [
ruma = { version = "0.11.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"compat-encrypted-stickers",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc4075"
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
] }
ruma-common = { git = "https://github.com/Hywan/ruma", branch = "feat-sliding-sync-list-include-heroes" }
ruma-common = "0.14.1"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
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 = "826d0aa22a9b5405535927c7691492db4b92a43b" }
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]
@@ -112,7 +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
+7 -2
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 = { 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) {
+159 -10
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,13 +74,21 @@ 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(SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
}))
.block_on(base_client.set_session_meta(
SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
},
None,
))
.expect("Could not set session meta");
base_client.get_or_create_room(&room_id, RoomState::Joined);
let request = get_member_events::v3::Request::new(room_id.clone());
@@ -94,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(
@@ -109,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");
@@ -30,7 +30,7 @@ final class ClientTests: XCTestCase {
.build()
XCTFail("The client should not build when given an invalid username.")
} catch ClientBuildError.Sdk(let message) {
} catch ClientBuildError.ServerUnreachable(let message) {
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
} catch {
XCTFail("Not expecting any other kind of exception")
+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,
+128 -33
View File
@@ -16,7 +16,11 @@ mod responses;
mod users;
mod verification;
use std::{collections::HashMap, sync::Arc, time::Duration};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
time::Duration,
};
use anyhow::Context as _;
pub use backup_recovery_key::{
@@ -29,12 +33,14 @@ 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, Session},
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
EncryptionSettings as RustEncryptionSettings,
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
pub use responses::{
@@ -43,8 +49,8 @@ pub use responses::{
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId,
SecondsSinceUnixEpoch, UserId,
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
RoomId, SecondsSinceUnixEpoch, UserId,
};
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
@@ -185,12 +191,12 @@ impl From<anyhow::Error> for MigrationError {
/// * `path` - The path where the SQLite store should be created.
///
/// * `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.
/// rest in the SQLite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
/// progress of the migration.
#[matrix_sdk_ffi_macros::export]
pub fn migrate(
data: MigrationData,
path: String,
@@ -348,12 +354,12 @@ async fn save_changes(
/// * `path` - The path where the SQLite store should be created.
///
/// * `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.
/// rest in the SQLite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
/// progress of the migration.
#[matrix_sdk_ffi_macros::export]
pub fn migrate_sessions(
data: SessionMigrationData,
path: String,
@@ -419,6 +425,27 @@ fn collect_sessions(
) -> anyhow::Result<(Vec<Session>, Vec<InboundGroupSession>)> {
let mut sessions = Vec::new();
// Create a DeviceKeys struct with enough information to get a working
// Session, but we will won't actually use the Sessions (and we'll clear
// the session cache after migration) so we don't need to worry about
// signatures.
let device_keys = DeviceKeys::new(
user_id,
device_id.clone(),
Default::default(),
BTreeMap::from([
(
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &device_id),
DeviceKey::Ed25519(identity_keys.ed25519),
),
(
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Curve25519, &device_id),
DeviceKey::Curve25519(identity_keys.curve25519),
),
]),
Default::default(),
);
for session_pickle in session_pickles {
let pickle =
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
@@ -439,8 +466,7 @@ fn collect_sessions(
last_use_time,
};
let session =
Session::from_pickle(user_id.clone(), device_id.clone(), identity_keys.clone(), pickle);
let session = Session::from_pickle(device_keys.clone(), pickle)?;
sessions.push(session);
processed_steps += 1;
@@ -471,6 +497,7 @@ fn collect_sessions(
Ok((algorithm, key))
})
.collect::<anyhow::Result<_>>()?,
sender_data: SenderData::legacy(),
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,
@@ -503,9 +530,9 @@ fn collect_sessions(
/// * `path` - The path where the Sqlite store should be created.
///
/// * `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]
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
#[matrix_sdk_ffi_macros::export]
pub fn migrate_room_settings(
room_settings: HashMap<String, RoomSettings>,
path: String,
@@ -531,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
///
@@ -641,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 {
@@ -650,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(),
only_allow_trusted_devices: 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,
},
}
}
}
@@ -695,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 },
}
}
}
@@ -756,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> {
@@ -853,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(),
@@ -877,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)) };
+69 -82
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`
///
@@ -251,12 +253,12 @@ impl OlmMachine {
///
/// * `user_id` - The unique id of the user that the identity belongs to
///
/// * `timeout` - The time in seconds we should wait before returning if
/// the user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
/// and sent out. Namely, this waits for a `/keys/query` response to be
/// received.
/// * `timeout` - The time in seconds we should wait before returning if the
/// user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being
/// processed and sent out. Namely, this waits for a `/keys/query`
/// response to be received.
pub fn get_identity(
&self,
user_id: String,
@@ -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 {
@@ -333,12 +332,12 @@ impl OlmMachine {
///
/// * `device_id` - The id of the device itself.
///
/// * `timeout` - The time in seconds we should wait before returning if
/// the user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
/// and sent out. Namely, this waits for a `/keys/query` response to be
/// received.
/// * `timeout` - The time in seconds we should wait before returning if the
/// user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being
/// processed and sent out. Namely, this waits for a `/keys/query`
/// response to be received.
pub fn get_device(
&self,
user_id: String,
@@ -416,12 +415,12 @@ impl OlmMachine {
///
/// * `user_id` - The id of the device owner.
///
/// * `timeout` - The time in seconds we should wait before returning if
/// the user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being processed
/// and sent out. Namely, this waits for a `/keys/query` response to be
/// received.
/// * `timeout` - The time in seconds we should wait before returning if the
/// user's device list has been marked as stale. Passing a 0 as the
/// timeout means that we won't wait at all. **Note**, this assumes that
/// the requests from [`OlmMachine::outgoing_requests`] are being
/// processed and sent out. Namely, this waits for a `/keys/query`
/// response to be received.
pub fn get_user_devices(
&self,
user_id: String,
@@ -517,7 +516,7 @@ impl OlmMachine {
/// current sync response.
///
/// * `device_changes` - The list of devices that have changed in some way
/// since the previous sync.
/// since the previous sync.
///
/// * `key_counts` - The map of uploaded one-time key types and counts.
pub fn receive_sync_changes(
@@ -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 {
@@ -608,7 +607,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `users` - The list of users for which we would like to establish 1:1
/// Olm sessions for.
/// Olm sessions for.
pub fn get_missing_sessions(
&self,
users: Vec<String>,
@@ -729,11 +728,11 @@ impl OlmMachine {
/// # Arguments
///
/// * `room_id` - The unique id of the room, note that this doesn't strictly
/// need to be a Matrix room, it just needs to be an unique identifier for
/// the group that will participate in the conversation.
/// need to be a Matrix room, it just needs to be an unique identifier for
/// the group that will participate in the conversation.
///
/// * `users` - The list of users which are considered to be members of the
/// room and should receive the room key.
/// room and should receive the room key.
///
/// * `settings` - The settings that should be used for the room key.
pub fn share_room_key(
@@ -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())?;
@@ -935,7 +939,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `event` - The undecryptable event that we would wish to request a room
/// key for.
/// key for.
///
/// * `room_id` - The id of the room the event was sent to.
pub fn request_room_key(
@@ -960,10 +964,10 @@ impl OlmMachine {
/// # Arguments
///
/// * `passphrase` - The passphrase that should be used to encrypt the key
/// export.
/// export.
///
/// * `rounds` - The number of rounds that should be used when expanding the
/// passphrase into an key.
/// passphrase into an key.
pub fn export_room_keys(
&self,
passphrase: String,
@@ -986,7 +990,7 @@ impl OlmMachine {
/// * `passphrase` - The passphrase that was used to encrypt the key export.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the key import.
/// progress of the key import.
pub fn import_room_keys(
&self,
keys: String,
@@ -1013,7 +1017,7 @@ impl OlmMachine {
/// * `keys` - The serialized version of the unencrypted key export.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the key import.
/// progress of the key import.
pub fn import_decrypted_room_keys(
&self,
keys: String,
@@ -1038,10 +1042,10 @@ impl OlmMachine {
/// * `keys` - The serialized version of the unencrypted key export.
///
/// * `backup_version` - The version of the backup that these keys came
/// from.
/// from.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the key import.
/// progress of the key import.
pub fn import_room_keys_from_backup(
&self,
keys: String,
@@ -1101,7 +1105,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to fetch the
/// verification requests.
/// verification requests.
pub fn get_verification_requests(&self, user_id: String) -> Vec<Arc<VerificationRequest>> {
let Ok(user_id) = UserId::parse(user_id) else {
return vec![];
@@ -1122,7 +1126,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to fetch the
/// verification requests.
/// verification requests.
///
/// * `flow_id` - The ID that uniquely identifies the verification flow.
pub fn get_verification_request(
@@ -1142,10 +1146,10 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user which we would like to request to
/// verify.
/// verify.
///
/// * `methods` - The list of verification methods we want to advertise to
/// support.
/// support.
pub fn verification_request_content(
&self,
user_id: String,
@@ -1158,8 +1162,7 @@ impl OlmMachine {
let methods = methods.into_iter().map(VerificationMethod::from).collect();
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
let content =
self.runtime.block_on(identity.verification_request_content(Some(methods)));
let content = identity.verification_request_content(Some(methods));
Some(serde_json::to_string(&content)?)
} else {
None
@@ -1172,18 +1175,18 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user which we would like to request to
/// verify.
/// verify.
///
/// * `room_id` - The ID of the room that represents a DM with the given
/// user.
/// user.
///
/// * `event_id` - The event ID of the `m.key.verification.request` event
/// that we sent out to request the verification to begin. The content for
/// this request can be created using the [verification_request_content()]
/// method.
/// that we sent out to request the verification to begin. The content for
/// this request can be created using the [verification_request_content()]
/// method.
///
/// * `methods` - The list of verification methods we advertised as
/// supported in the `m.key.verification.request` event.
/// supported in the `m.key.verification.request` event.
///
/// [verification_request_content()]: Self::verification_request_content
pub fn request_verification(
@@ -1202,11 +1205,7 @@ impl OlmMachine {
let methods = methods.into_iter().map(VerificationMethod::from).collect();
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
let request = self.runtime.block_on(identity.request_verification(
&room_id,
&event_id,
Some(methods),
));
let request = identity.request_verification(&room_id, &event_id, Some(methods));
Some(
VerificationRequest { inner: request, runtime: self.runtime.handle().to_owned() }
@@ -1222,12 +1221,12 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user which we would like to request to
/// verify.
/// verify.
///
/// * `device_id` - The ID of the device that we wish to verify.
///
/// * `methods` - The list of verification methods we advertised as
/// supported in the `m.key.verification.request` event.
/// supported in the `m.key.verification.request` event.
pub fn request_verification_with_device(
&self,
user_id: String,
@@ -1243,8 +1242,7 @@ impl OlmMachine {
if let Some(device) =
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
{
let (verification, request) =
self.runtime.block_on(device.request_verification_with_methods(methods));
let (verification, request) = device.request_verification_with_methods(methods);
Some(RequestVerificationResult {
verification: VerificationRequest {
@@ -1297,7 +1295,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to fetch the
/// verification.
/// verification.
///
/// * `flow_id` - The ID that uniquely identifies the verification flow.
pub fn get_verification(&self, user_id: String, flow_id: String) -> Option<Arc<Verification>> {
@@ -1317,7 +1315,7 @@ impl OlmMachine {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to start the
/// SAS verification.
/// SAS verification.
///
/// * `device_id` - The ID of device we would like to verify.
///
@@ -1534,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
///
@@ -28,8 +28,11 @@ pub trait SasListener: Send {
/// An Enum describing the state the SAS verification is in.
#[derive(uniffi::Enum)]
pub enum SasState {
/// The verification has been started, the protocols that should be used
/// have been proposed and can be accepted.
/// The verification has been created, the protocols that should be used
/// have been proposed to the other party.
Created,
/// The verification has been started, the other party proposed the
/// protocols that should be used and that can be accepted.
Started,
/// The verification has been accepted and both sides agreed to a set of
/// protocols that will be used for the verification process.
@@ -58,6 +61,7 @@ pub enum SasState {
impl From<RustSasState> for SasState {
fn from(s: RustSasState) -> Self {
match s {
RustSasState::Created { .. } => Self::Created,
RustSasState::Started { .. } => Self::Started,
RustSasState::Accepted { .. } => Self::Accepted,
RustSasState::KeysExchanged { emojis, decimals } => Self::KeysExchanged {
@@ -78,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.
@@ -108,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 {
@@ -165,8 +169,8 @@ impl Sas {
/// # Arguments
///
/// * `cancel_code` - The error code for why the verification was cancelled,
/// manual cancellatio usually happens with `m.user` cancel code. The full
/// list of cancel codes can be found in the [spec]
/// manual cancellatio usually happens with `m.user` cancel code. The full
/// list of cancel codes can be found in the [spec]
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
@@ -272,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
///
@@ -324,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 {
@@ -387,8 +391,8 @@ impl QrCode {
/// # Arguments
///
/// * `cancel_code` - The error code for why the verification was cancelled,
/// manual cancellatio usually happens with `m.user` cancel code. The full
/// list of cancel codes can be found in the [spec]
/// manual cancellatio usually happens with `m.user` cancel code. The full
/// list of cancel codes can be found in the [spec]
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel
pub fn cancel(&self, cancel_code: String) -> Option<OutgoingVerificationRequest> {
@@ -518,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
///
@@ -558,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.
@@ -636,12 +640,12 @@ impl VerificationRequest {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to accept the
/// verification requests.
/// verification requests.
///
/// * `flow_id` - The ID that uniquely identifies the verification flow.
///
/// * `methods` - A list of verification methods that we want to advertise
/// as supported.
/// as supported.
pub fn accept(&self, methods: Vec<String>) -> Option<OutgoingVerificationRequest> {
let methods = methods.into_iter().map(VerificationMethod::from).collect();
self.inner.accept_with_methods(methods).map(|r| r.into())
@@ -659,10 +663,10 @@ impl VerificationRequest {
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to start the
/// SAS verification.
/// SAS verification.
///
/// * `flow_id` - The ID of the verification request that initiated the
/// verification flow.
/// verification flow.
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
@@ -677,11 +681,11 @@ impl VerificationRequest {
///
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to start the
/// QR code verification.
/// * `user_id` - The ID of the user for which we would like to start the QR
/// code verification.
///
/// * `flow_id` - The ID of the verification request that initiated the
/// verification flow.
/// verification flow.
pub fn start_qr_verification(&self) -> Result<Option<Arc<QrCode>>, CryptoStoreError> {
Ok(self
.runtime
@@ -697,14 +701,14 @@ impl VerificationRequest {
///
/// # Arguments
///
/// * `user_id` - The ID of the user for which we would like to start the
/// QR code verification.
/// * `user_id` - The ID of the user for which we would like to start the QR
/// code verification.
///
/// * `flow_id` - The ID of the verification request that initiated the
/// verification flow.
/// verification flow.
///
/// * `data` - The data that was extracted from the scanned QR code as an
/// base64 encoded string, without padding.
/// base64 encoded string, without padding.
pub fn scan_qr_code(&self, data: String) -> Option<ScanResult> {
let data = base64_decode(data).ok()?;
let data = QrVerificationData::from_bytes(data).ok()?;
@@ -748,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()
}
+35
View File
@@ -0,0 +1,35 @@
# unreleased
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.
- `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`
+8 -6
View File
@@ -27,20 +27,20 @@ async-compat = "0.2.1"
eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
log-panics = { version = "2", features = ["with-backtrace"] }
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-stream = { workspace = true, features = ["time"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
url = { workspace = true }
zeroize = { workspace = true }
@@ -48,7 +48,6 @@ uuid = { version = "1.4.1", features = ["v4"] }
language-tags = "0.3.2"
[target.'cfg(target_os = "android")'.dependencies]
log-panics = { version = "2", features = ["with-backtrace"] }
paranoid-android = "0.2.1"
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
@@ -83,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");
@@ -0,0 +1,243 @@
use std::{
collections::HashMap,
fmt::{self, Debug},
sync::Arc,
};
use matrix_sdk::{
oidc::{
registrations::OidcRegistrationsError,
types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
},
OidcError as SdkOidcError,
},
Error,
};
use url::Url;
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
pub(crate) url: 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,
}
#[matrix_sdk_ffi_macros::export]
impl HomeserverLoginDetails {
/// The URL of the currently configured homeserver.
pub fn url(&self) -> String {
self.url.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.
pub fn supports_oidc_login(&self) -> bool {
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 {
/// The name of the client that will be shown during OIDC authentication.
pub client_name: Option<String>,
/// The redirect URI that will be used when OIDC authentication is
/// successful.
pub redirect_uri: String,
/// A URI that contains information about the client.
pub client_uri: Option<String>,
/// A URI that contains the client's logo.
pub logo_uri: Option<String>,
/// A URI that contains the client's terms of service.
pub tos_uri: Option<String>,
/// A URI that contains the client's privacy policy.
pub policy_uri: Option<String>,
/// An array of e-mail addresses of people responsible for this client.
pub contacts: Option<Vec<String>>,
/// Pre-configured registrations for use with issuers that don't support
/// dynamic client registration.
pub static_registrations: HashMap<String, String>,
/// A file path where any dynamic registrations should be stored.
///
/// Suggested value: `{base_path}/oidc/registrations.json`
pub dynamic_registrations_file: String,
}
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
type Error = OidcError;
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
let redirect_uri =
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
let client_uri = self.client_uri.localized_url()?;
let logo_uri = self.logo_uri.localized_url()?;
let policy_uri = self.policy_uri.localized_url()?;
let tos_uri = self.tos_uri.localized_url()?;
let contacts = self.contacts.clone();
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![
GrantType::RefreshToken,
GrantType::AuthorizationCode,
GrantType::DeviceCode,
]),
// A native client shouldn't use authentication as the credentials could be intercepted.
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
// The server should display the following fields when getting the user's consent.
client_name,
contacts,
client_uri,
logo_uri,
policy_uri,
tos_uri,
..Default::default()
}
.validate()
.map_err(|_| OidcError::MetadataInvalid)
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum OidcError {
#[error(
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
)]
NotSupported,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
MetadataInvalid,
#[error("Failed to use the supplied registrations file path.")]
RegistrationsPathInvalid,
#[error("The supplied callback URL used to complete OIDC is invalid.")]
CallbackUrlInvalid,
#[error("The OIDC login was cancelled by the user.")]
Cancelled,
#[error("An error occurred: {message}")]
Generic { message: String },
}
impl From<SdkOidcError> for OidcError {
fn from(e: SdkOidcError) -> OidcError {
match e {
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<OidcRegistrationsError> for OidcError {
fn from(e: OidcRegistrationsError) -> OidcError {
match e {
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<Error> for OidcError {
fn from(e: Error) -> OidcError {
match e {
Error::Oidc(e) => e.into(),
_ => OidcError::Generic { message: e.to_string() },
}
}
}
/* Helpers */
trait OptionExt {
/// Convenience method to convert a string to a URL and returns it as a
/// Localized URL. No localization is actually performed.
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
}
impl OptionExt for Option<String> {
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
self.as_deref()
.map(|uri| -> Result<Localized<Url>, OidcError> {
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
})
.transpose()
}
}
@@ -1,678 +0,0 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock as StdRwLock},
};
use matrix_sdk::{
encryption::BackupDownloadStrategy,
oidc::{
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
types::{
client_credentials::ClientCredentials,
errors::ClientErrorCode::AccessDenied,
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::{GrantType, Prompt},
},
AuthorizationResponse, Oidc, OidcError,
},
reqwest::StatusCode,
AuthSession, ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError,
};
use ruma::{
api::error::{DeserializationError, FromHttpResponseError},
OwnedUserId,
};
use tokio::sync::RwLock as AsyncRwLock;
use url::Url;
use zeroize::Zeroize;
use super::{client::Client, client_builder::ClientBuilder};
use crate::{
client::ClientSessionDelegate,
client_builder::{CertificateBytes, ClientBuildError},
error::ClientError,
};
#[derive(uniffi::Object)]
pub struct AuthenticationService {
base_path: String,
passphrase: Option<String>,
user_agent: Option<String>,
client: AsyncRwLock<Option<Client>>,
homeserver_details: StdRwLock<Option<Arc<HomeserverLoginDetails>>>,
oidc_configuration: Option<OidcConfiguration>,
custom_sliding_sync_proxy: StdRwLock<Option<String>>,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<CertificateBytes>,
proxy: Option<String>,
}
impl Drop for AuthenticationService {
fn drop(&mut self) {
self.passphrase.zeroize();
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum AuthenticationError {
#[error("A successful call to configure_homeserver must be made first.")]
ClientMissing,
#[error("The supplied server name is invalid.")]
InvalidServerName,
#[error(transparent)]
ServerUnreachable(HttpError),
#[error(transparent)]
WellKnownLookupFailed(RumaApiError),
#[error(transparent)]
WellKnownDeserializationError(DeserializationError),
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
SlidingSyncNotAvailable,
#[error("Login was successful but is missing a valid Session to configure the file store.")]
SessionMissing,
#[error("Failed to use the supplied base path.")]
InvalidBasePath,
#[error(
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
)]
OidcNotSupported,
#[error("Unable to use OIDC as no client metadata has been supplied.")]
OidcMetadataMissing,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
OidcMetadataInvalid,
#[error("The supplied callback URL used to complete OIDC is invalid.")]
OidcCallbackUrlInvalid,
#[error("The OIDC login was cancelled by the user.")]
OidcCancelled,
#[error("An error occurred with OIDC: {message}")]
OidcError { message: String },
#[error("An error occurred: {message}")]
Generic { message: String },
}
impl From<anyhow::Error> for AuthenticationError {
fn from(e: anyhow::Error) -> AuthenticationError {
AuthenticationError::Generic { message: e.to_string() }
}
}
impl From<ClientBuildError> for AuthenticationError {
fn from(e: ClientBuildError) -> AuthenticationError {
match e {
ClientBuildError::Sdk(MatrixClientBuildError::InvalidServerName) => {
AuthenticationError::InvalidServerName
}
ClientBuildError::Sdk(MatrixClientBuildError::Http(e)) => {
AuthenticationError::ServerUnreachable(e)
}
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
FromHttpResponseError::Server(e),
)) => AuthenticationError::WellKnownLookupFailed(e),
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
FromHttpResponseError::Deserialization(e),
)) => AuthenticationError::WellKnownDeserializationError(e),
_ => AuthenticationError::Generic { message: e.to_string() },
}
}
}
impl From<OidcRegistrationsError> for AuthenticationError {
fn from(e: OidcRegistrationsError) -> AuthenticationError {
match e {
OidcRegistrationsError::InvalidBasePath => AuthenticationError::InvalidBasePath,
_ => AuthenticationError::OidcError { message: e.to_string() },
}
}
}
impl From<OidcError> for AuthenticationError {
fn from(e: OidcError) -> AuthenticationError {
AuthenticationError::OidcError { message: e.to_string() }
}
}
/// The configuration to use when authenticating with OIDC.
#[derive(uniffi::Record)]
pub struct OidcConfiguration {
/// The name of the client that will be shown during OIDC authentication.
pub client_name: Option<String>,
/// The redirect URI that will be used when OIDC authentication is
/// successful.
pub redirect_uri: String,
/// A URI that contains information about the client.
pub client_uri: Option<String>,
/// A URI that contains the client's logo.
pub logo_uri: Option<String>,
/// A URI that contains the client's terms of service.
pub tos_uri: Option<String>,
/// A URI that contains the client's privacy policy.
pub policy_uri: Option<String>,
/// An array of e-mail addresses of people responsible for this client.
pub contacts: Option<Vec<String>>,
/// Pre-configured registrations for use with issuers that don't support
/// dynamic client registration.
pub static_registrations: HashMap<String, String>,
}
/// The data required to authenticate against an OIDC server.
#[derive(uniffi::Object)]
pub struct OidcAuthenticationData {
/// The underlying URL for authentication.
url: Url,
/// A unique identifier for the request, used to ensure the response
/// originated from the authentication issuer.
state: String,
}
#[uniffi::export]
impl OidcAuthenticationData {
/// The login URL to use for authentication.
pub fn login_url(&self) -> String {
self.url.to_string()
}
}
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
url: String,
sliding_sync_proxy: Option<String>,
supports_oidc_login: bool,
supports_password_login: bool,
}
#[uniffi::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()
}
/// Whether the current homeserver supports login using OIDC.
pub fn supports_oidc_login(&self) -> bool {
self.supports_oidc_login
}
/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> bool {
self.supports_password_login
}
}
#[uniffi::export(async_runtime = "tokio")]
impl AuthenticationService {
/// Creates a new service to authenticate a user with.
#[uniffi::constructor]
// TODO: This has too many arguments, even clippy agrees. Many of these methods are the same as
// for the `ClientBuilder`. We should let people pass in a `ClientBuilder` and possibly convert
// this to a builder pattern as well.
#[allow(clippy::too_many_arguments)]
pub fn new(
base_path: String,
passphrase: Option<String>,
user_agent: Option<String>,
additional_root_certificates: Vec<Vec<u8>>,
proxy: Option<String>,
oidc_configuration: Option<OidcConfiguration>,
custom_sliding_sync_proxy: Option<String>,
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
cross_process_refresh_lock_id: Option<String>,
) -> Arc<Self> {
Arc::new(AuthenticationService {
base_path,
passphrase,
user_agent,
client: AsyncRwLock::new(None),
homeserver_details: StdRwLock::new(None),
oidc_configuration,
custom_sliding_sync_proxy: StdRwLock::new(custom_sliding_sync_proxy),
session_delegate: session_delegate.map(Into::into),
cross_process_refresh_lock_id,
additional_root_certificates,
proxy,
})
}
pub fn homeserver_details(&self) -> Option<Arc<HomeserverLoginDetails>> {
self.homeserver_details.read().unwrap().clone()
}
/// Updates the service to authenticate with the homeserver for the
/// specified address.
pub async fn configure_homeserver(
&self,
server_name_or_homeserver_url: String,
) -> Result<(), AuthenticationError> {
let mut builder = self.new_client_builder();
builder = builder.server_name_or_homeserver_url(server_name_or_homeserver_url);
let client = builder.build_inner().await?;
let details = self.details_from_client(&client).await?;
// Make sure there's a sliding sync proxy available.
if self.custom_sliding_sync_proxy.read().unwrap().is_none()
&& details.sliding_sync_proxy().is_none()
{
return Err(AuthenticationError::SlidingSyncNotAvailable);
}
*self.client.write().await = Some(client);
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
Ok(())
}
/// Performs a password login using the current homeserver.
pub async fn login(
&self,
username: String,
password: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<Arc<Client>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
// Login and ask the server for the full user ID as this could be different from
// the username that was entered.
client.login(username, password, initial_device_name, device_id).await.map_err(
|e| match e {
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
},
)?;
let whoami = client.whoami().await?;
let session =
client.inner.matrix_auth().session().ok_or(AuthenticationError::SessionMissing)?;
drop(client_guard);
self.finalize_client(session, whoami.user_id).await
}
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns.
pub async fn url_for_oidc_login(
&self,
) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
let oidc = client.inner.oidc();
let issuer = match oidc.fetch_authentication_issuer().await {
Ok(issuer) => issuer,
Err(error) => {
if error
.as_client_api_error()
.is_some_and(|err| err.status_code == StatusCode::NOT_FOUND)
{
return Err(AuthenticationError::OidcNotSupported);
} else {
return Err(AuthenticationError::ServerUnreachable(error));
}
}
};
let Some(oidc_configuration) = &self.oidc_configuration else {
return Err(AuthenticationError::OidcMetadataMissing);
};
let redirect_url = Url::parse(&oidc_configuration.redirect_uri)
.map_err(|_e| AuthenticationError::OidcMetadataInvalid)?;
self.configure_oidc(&oidc, issuer, oidc_configuration).await?;
let mut data_builder = oidc.login(redirect_url, None)?;
// TODO: Add a check for the Consent prompt when MAS is updated.
data_builder = data_builder.prompt(vec![Prompt::Consent]);
let data = data_builder.build().await?;
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
}
/// Completes the OIDC login process.
pub async fn login_with_oidc_callback(
&self,
authentication_data: Arc<OidcAuthenticationData>,
callback_url: String,
) -> Result<Arc<Client>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
let oidc = client.inner.oidc();
let url =
Url::parse(&callback_url).map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
let response = AuthorizationResponse::parse_uri(&url)
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
let code = match response {
AuthorizationResponse::Success(code) => code,
AuthorizationResponse::Error(err) => {
if err.error.error == AccessDenied {
// The user cancelled the login in the web view.
return Err(AuthenticationError::OidcCancelled);
}
return Err(AuthenticationError::OidcError {
message: err.error.error.to_string(),
});
}
};
if code.state != authentication_data.state {
return Err(AuthenticationError::OidcCallbackUrlInvalid);
};
oidc.finish_authorization(code).await?;
oidc.finish_login()
.await
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?;
let user_id = client.inner.user_id().unwrap().to_owned();
let session =
client.inner.oidc().full_session().ok_or(AuthenticationError::SessionMissing)?;
drop(client_guard);
self.finalize_client(session, user_id).await
}
}
impl AuthenticationService {
/// A new client builder pre-configured with the service's base path and
/// user agent if specified
fn new_client_builder(&self) -> Arc<ClientBuilder> {
let mut builder = ClientBuilder::new().base_path(self.base_path.clone());
if let Some(user_agent) = self.user_agent.clone() {
builder = builder.user_agent(user_agent);
}
if let Some(proxy) = &self.proxy {
builder = builder.proxy(proxy.to_owned())
}
builder = builder.add_root_certificates(self.additional_root_certificates.clone());
builder
}
/// Get the homeserver login details from a client.
async fn details_from_client(
&self,
client: &Client,
) -> Result<HomeserverLoginDetails, AuthenticationError> {
let supports_oidc_login = client.inner.oidc().fetch_authentication_issuer().await.is_ok();
let supports_password_login = client.supports_password_login().await.ok().unwrap_or(false);
let sliding_sync_proxy = client.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
let url = client.homeserver();
Ok(HomeserverLoginDetails {
url,
sliding_sync_proxy,
supports_oidc_login,
supports_password_login,
})
}
/// Handle any necessary configuration in order for login via OIDC to
/// succeed. This includes performing dynamic client registration against
/// the homeserver's issuer or restoring a previous registration if one has
/// been stored.
async fn configure_oidc(
&self,
oidc: &Oidc,
issuer: String,
configuration: &OidcConfiguration,
) -> Result<(), AuthenticationError> {
if oidc.client_credentials().is_some() {
tracing::info!("OIDC is already configured.");
return Ok(());
};
let oidc_metadata = self.oidc_metadata(configuration)?;
if self.load_client_registration(oidc, issuer.clone(), oidc_metadata.clone()).await {
tracing::info!("OIDC configuration loaded from disk.");
return Ok(());
}
tracing::info!("Registering this client for OIDC.");
let registration_response =
oidc.register_client(&issuer, oidc_metadata.clone(), None).await?;
// The format of the credentials changes according to the client metadata that
// was sent. Public clients only get a client ID.
let credentials =
ClientCredentials::None { client_id: registration_response.client_id.clone() };
oidc.restore_registered_client(issuer, oidc_metadata, credentials);
tracing::info!("Persisting OIDC registration data.");
self.store_client_registration(oidc).await?;
Ok(())
}
/// Stores the current OIDC dynamic client registration so it can be re-used
/// if we ever log in via the same issuer again.
async fn store_client_registration(&self, oidc: &Oidc) -> Result<(), AuthenticationError> {
let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?)
.map_err(|_| AuthenticationError::OidcError {
message: String::from("Failed to parse issuer URL."),
})?;
let client_id = oidc
.client_credentials()
.ok_or(AuthenticationError::OidcError {
message: String::from("Missing client registration."),
})?
.client_id()
.to_owned();
let metadata = oidc.client_metadata().ok_or(AuthenticationError::OidcError {
message: String::from("Missing client metadata."),
})?;
let registrations = OidcRegistrations::new(
&self.base_path,
metadata.clone(),
self.oidc_static_registrations(),
)?;
registrations.set_and_write_client_id(ClientId(client_id), issuer)?;
Ok(())
}
/// Attempts to load an existing OIDC dynamic client registration for the
/// currently configured issuer.
async fn load_client_registration(
&self,
oidc: &Oidc,
issuer: String,
oidc_metadata: VerifiedClientMetadata,
) -> bool {
let Ok(issuer_url) = Url::parse(&issuer) else {
tracing::error!("Failed to parse {issuer:?}");
return false;
};
let Some(registrations) = OidcRegistrations::new(
&self.base_path,
oidc_metadata.clone(),
self.oidc_static_registrations(),
)
.ok() else {
return false;
};
let Some(client_id) = registrations.client_id(&issuer_url) else {
return false;
};
oidc.restore_registered_client(
issuer,
oidc_metadata,
ClientCredentials::None { client_id: client_id.0 },
);
true
}
/// Creates and verifies OIDC client metadata for the supplied OIDC
/// configuration.
fn oidc_metadata(
&self,
configuration: &OidcConfiguration,
) -> Result<VerifiedClientMetadata, AuthenticationError> {
let redirect_uri = Url::parse(&configuration.redirect_uri)
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
let client_name =
configuration.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
let client_uri = configuration.client_uri.localized_url()?;
let logo_uri = configuration.logo_uri.localized_url()?;
let policy_uri = configuration.policy_uri.localized_url()?;
let tos_uri = configuration.tos_uri.localized_url()?;
let contacts = configuration.contacts.clone();
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![GrantType::RefreshToken, GrantType::AuthorizationCode]),
// A native client shouldn't use authentication as the credentials could be intercepted.
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
// The server should display the following fields when getting the user's consent.
client_name,
contacts,
client_uri,
logo_uri,
policy_uri,
tos_uri,
..Default::default()
}
.validate()
.map_err(|_| AuthenticationError::OidcMetadataInvalid)
}
fn oidc_static_registrations(&self) -> HashMap<Url, ClientId> {
let registrations = self
.oidc_configuration
.as_ref()
.map(|c| c.static_registrations.clone())
.unwrap_or_default();
registrations
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
return None;
};
Some((issuer, ClientId(client_id.clone())))
})
.collect()
}
/// Creates a new client to setup the store path now the user ID is known.
async fn finalize_client(
&self,
session: impl Into<AuthSession>,
user_id: OwnedUserId,
) -> Result<Arc<Client>, AuthenticationError> {
// Take ownership of the client. This means that further attempts to
// `finalize_client` may fail, but we want to make sure that there
// aren't two clients at any point later.
let Some(client) = self.client.write().await.take() else {
return Err(AuthenticationError::ClientMissing);
};
let homeserver_url = client.homeserver();
let sliding_sync_proxy = self
.custom_sliding_sync_proxy
.read()
.unwrap()
.clone()
.or_else(|| client.sliding_sync_proxy().map(|url| url.to_string()));
// Wait for the parent client to finish running its initialization tasks.
client.inner.encryption().wait_for_e2ee_initialization_tasks().await;
// Drop the parent client. Both clients shouldn't be alive at the same time, or
// it may cause issues (when trying to initialize encryption-related tasks at
// the same time).
drop(client);
// Construct the final client.
let mut client = self
.new_client_builder()
.passphrase(self.passphrase.clone())
.homeserver_url(homeserver_url)
.sliding_sync_proxy(sliding_sync_proxy)
.auto_enable_cross_signing(true)
.backup_download_strategy(BackupDownloadStrategy::AfterDecryptionFailure)
.auto_enable_backups(true)
.username(user_id.to_string());
if let Some(proxy) = &self.proxy {
client = client.proxy(proxy.to_owned())
}
if let Some(id) = &self.cross_process_refresh_lock_id {
let Some(ref session_delegate) = self.session_delegate else {
return Err(AuthenticationError::OidcError {
message: "cross-process refresh lock requires session delegate".to_owned(),
});
};
client = client
.enable_cross_process_refresh_lock_inner(id.clone(), session_delegate.clone());
} else if let Some(ref session_delegate) = self.session_delegate {
client = client.set_session_delegate_inner(session_delegate.clone());
}
let client = client.build_inner().await?;
// Restore the client using the session from the login request.
client.restore_session_inner(session).await?;
Ok(Arc::new(client))
}
}
trait OptionExt {
/// Convenience method to convert a string to a URL and returns it as a
/// Localized URL. No localization is actually performed.
fn localized_url(&self) -> Result<Option<Localized<Url>>, AuthenticationError>;
}
impl OptionExt for Option<String> {
fn localized_url(&self) -> Result<Option<Localized<Url>>, AuthenticationError> {
self.as_deref()
.map(|uri| -> Result<Localized<Url>, AuthenticationError> {
Ok(Localized::new(
Url::parse(uri).map_err(|_| AuthenticationError::OidcMetadataInvalid)?,
[],
))
})
.transpose()
}
}
File diff suppressed because it is too large Load Diff
+451 -100
View File
@@ -1,21 +1,32 @@
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},
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,
ClientBuilder as MatrixClientBuilder, IdParseError,
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError,
};
use sanitize_filename_reader_friendly::sanitize;
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use url::Url;
use zeroize::Zeroizing;
use super::client::Client;
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
use super::{client::Client, RUNTIME};
use crate::{
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
};
/// A list of bytes containing a certificate in DER or PEM form.
pub type CertificateBytes = Vec<u8>;
@@ -27,15 +38,193 @@ enum HomeserverConfig {
ServerNameOrUrl(String),
}
/// Data for the QR code login mechanism.
///
/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
/// decoded from a QR code.
#[derive(Debug, uniffi::Object)]
pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
/// The slice of bytes would generally be returned by a QR code decoder.
#[uniffi::constructor]
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
}
/// Error type for the decoding of the [`QrCodeData`].
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum QrCodeDecodeError {
#[error("Error decoding QR code: {error:?}")]
Crypto {
#[from]
error: LoginQrCodeDecodeError,
},
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum HumanQrLoginError {
#[error("Linking with this device is not supported.")]
LinkingNotSupported,
#[error("The sign in was cancelled.")]
Cancelled,
#[error("The sign in was not completed in the required time.")]
Expired,
#[error("A secure connection could not have been established between the two devices.")]
ConnectionInsecure,
#[error("The sign in was declined.")]
Declined,
#[error("An unknown error has happened.")]
Unknown,
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
SlidingSyncNotAvailable,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
OidcMetadataInvalid,
#[error("The other device is not signed in and as such can't sign in other devices.")]
OtherDeviceNotSignedIn,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
fn from(value: qrcode::QRCodeLoginError) -> Self {
use qrcode::{QRCodeLoginError, SecureChannelError};
match value {
QRCodeLoginError::LoginFailure { reason, .. } => match reason {
LoginFailureReason::UnsupportedProtocol => HumanQrLoginError::LinkingNotSupported,
LoginFailureReason::AuthorizationExpired => HumanQrLoginError::Expired,
LoginFailureReason::UserCancelled => HumanQrLoginError::Cancelled,
_ => HumanQrLoginError::Unknown,
},
QRCodeLoginError::Oidc(e) => {
if let Some(e) = e.as_request_token_error() {
match e {
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
DeviceCodeErrorResponseType::ExpiredToken => HumanQrLoginError::Expired,
_ => HumanQrLoginError::Unknown,
}
} else {
HumanQrLoginError::Unknown
}
}
QRCodeLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
},
QRCodeLoginError::UnexpectedMessage { .. }
| QRCodeLoginError::CrossProcessRefreshLock(_)
| QRCodeLoginError::DeviceKeyUpload(_)
| QRCodeLoginError::SessionTokens(_)
| QRCodeLoginError::UserIdDiscovery(_)
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
}
}
}
/// Enum describing the progress of the QR-code login.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum QrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We established a secure channel with the other device.
EstablishingSecureChannel {
/// The check code that the device should display so the other device
/// can confirm that the channel is secure as well.
check_code: u8,
/// The string representation of the check code, will be guaranteed to
/// be 2 characters long, preserving the leading zero if the
/// first digit is a zero.
check_code_string: String,
},
/// We are waiting for the login and for the OIDC provider to give us an
/// access token.
WaitingForToken { user_code: String },
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: Sync + Send {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::Done => Self::Done,
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum ClientBuildError {
#[error("The supplied server name is invalid.")]
InvalidServerName,
#[error(transparent)]
Sdk(#[from] MatrixClientBuildError),
ServerUnreachable(HttpError),
#[error(transparent)]
WellKnownLookupFailed(RumaApiError),
#[error(transparent)]
WellKnownDeserializationError(DeserializationError),
#[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 },
}
impl From<MatrixClientBuildError> for ClientBuildError {
fn from(e: MatrixClientBuildError) -> Self {
match e {
MatrixClientBuildError::InvalidServerName => ClientBuildError::InvalidServerName,
MatrixClientBuildError::Http(e) => ClientBuildError::ServerUnreachable(e),
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Server(e)) => {
ClientBuildError::WellKnownLookupFailed(e)
}
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(e)) => {
ClientBuildError::WellKnownDeserializationError(e)
}
MatrixClientBuildError::SlidingSyncVersion(e) => {
ClientBuildError::SlidingSyncVersion(e)
}
_ => ClientBuildError::Sdk(e),
}
}
}
impl From<IdParseError> for ClientBuildError {
fn from(e: IdParseError) -> ClientBuildError {
ClientBuildError::Generic { message: format!("{e:#}") }
@@ -62,57 +251,70 @@ impl From<ClientError> for ClientBuildError {
#[derive(Clone, uniffi::Object)]
pub struct ClientBuilder {
base_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>,
sliding_sync_proxy: Option<String>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
inner: MatrixClientBuilder,
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 {
base_path: None,
session_paths: None,
username: None,
homeserver_cfg: None,
server_versions: None,
passphrase: Zeroizing::new(None),
user_agent: None,
sliding_sync_proxy: None,
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
inner: MatrixClient::builder(),
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> {
self.enable_cross_process_refresh_lock_inner(process_id, session_delegate.into())
let mut builder = unwrap_or_clone_arc(self);
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)
}
pub fn set_session_delegate(
@@ -124,9 +326,15 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn base_path(self: Arc<Self>, path: String) -> Arc<Self> {
/// 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_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.base_path = Some(path);
builder.session_paths = Some(SessionPaths { data_path, cache_path });
Arc::new(builder)
}
@@ -136,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));
@@ -172,9 +374,12 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn sliding_sync_proxy(self: Arc<Self>, sliding_sync_proxy: Option<String>) -> 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.sliding_sync_proxy = sliding_sync_proxy;
builder.sliding_sync_version_builder = version_builder;
Arc::new(builder)
}
@@ -206,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,
@@ -235,42 +449,60 @@ 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> {
Ok(Arc::new(self.build_inner().await?))
}
}
impl ClientBuilder {
pub(crate) fn enable_cross_process_refresh_lock_inner(
self: Arc<Self>,
process_id: String,
session_delegate: Arc<dyn ClientSessionDelegate>,
) -> 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);
Arc::new(builder)
}
pub(crate) fn set_session_delegate_inner(
self: Arc<Self>,
session_delegate: Arc<dyn ClientSessionDelegate>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_delegate = Some(session_delegate);
Arc::new(builder)
}
pub(crate) async fn build_inner(self: Arc<Self>) -> Result<Client, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = builder.inner;
let mut inner_builder = MatrixClient::builder();
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(),
cache_path = %cache_path.to_string_lossy(),
"Creating directories for data and cache stores.",
);
if let (Some(base_path), Some(username)) = (builder.base_path, &builder.username) {
// Determine store path
let data_path = PathBuf::from(base_path).join(sanitize(username));
fs::create_dir_all(&data_path)?;
fs::create_dir_all(&cache_path)?;
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
inner_builder = inner_builder.sqlite_store_with_cache_path(
&data_path,
&cache_path,
builder.passphrase.as_deref(),
);
} else {
debug!("Not using a store path.");
}
// Determine server either from URL, server name or user ID.
@@ -300,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);
}
@@ -329,38 +569,149 @@ 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 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.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
}
Ok(Client::new(sdk_client, builder.cross_process_refresh_lock_id, builder.session_delegate)
.await?)
/// Finish the building of the client and attempt to log in using the
/// provided [`QrCodeData`].
///
/// This method will build the client and immediately attempt to log the
/// client in using the provided [`QrCodeData`] using the login
/// mechanism described in [MSC4108]. As such this methods requires OIDC
/// support as well as sliding sync support.
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn build_with_qr_code(
self: Arc<Self>,
qr_code_data: &QrCodeData,
oidc_configuration: &OidcConfiguration,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<Arc<Client>, HumanQrLoginError> {
let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data else {
return Err(HumanQrLoginError::OtherDeviceNotSignedIn);
};
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
let client = builder.build().await.map_err(|e| match e {
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
_ => {
error!("Couldn't build the client {e:?}");
HumanQrLoginError::Unknown
}
})?;
let client_metadata =
oidc_configuration.try_into().map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let oidc = client.inner.oidc();
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
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,
}
+22
View File
@@ -0,0 +1,22 @@
use serde::Deserialize;
use crate::ClientError;
/// Well-known settings specific to ElementCall
#[derive(Deserialize, uniffi::Record)]
pub struct ElementCallWellKnown {
widget_url: String,
}
/// Element specific well-known settings
#[derive(Deserialize, uniffi::Record)]
pub struct ElementWellKnown {
call: Option<ElementCallWellKnown>,
registration_helper_url: Option<String>,
}
/// Helper function to parse a string into a ElementWellKnown struct
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
}
+192 -14
View File
@@ -6,38 +6,40 @@ use matrix_sdk::{
encryption::{backups, recovery},
};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
use crate::{error::ClientError, task_handle::TaskHandle};
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
#[derive(uniffi::Object)]
pub struct Encryption {
inner: matrix_sdk::encryption::Encryption,
pub(crate) inner: matrix_sdk::encryption::Encryption,
/// A reference to the FFI client.
///
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
/// would need to repeat the hack done in the FFI `Client::drop` method.
pub(crate) _client: Arc<Client>,
}
impl From<matrix_sdk::encryption::Encryption> for Encryption {
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
Self { inner: value }
}
}
#[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);
}
@@ -161,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);
}
@@ -211,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.
@@ -314,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();
@@ -324,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 {
@@ -336,6 +345,7 @@ impl Encryption {
let ret = enable.await?;
task.abort();
passphrase.zeroize();
Ok(ret)
}
@@ -356,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;
@@ -373,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());
@@ -385,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() }
}
}
+133 -4
View File
@@ -1,12 +1,16 @@
use std::fmt::Display;
use std::{collections::HashMap, fmt, fmt::Display};
use matrix_sdk::{
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
IdParseError, NotificationSettingsError as SdkNotificationSettingsError, StoreError,
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use uniffi::UnexpectedUniFFICallbackError;
use crate::room_list::RoomListError;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
@@ -14,7 +18,7 @@ pub enum ClientError {
}
impl ClientError {
fn new<E: Display>(error: E) -> Self {
pub(crate) fn new<E: Display>(error: E) -> Self {
Self::Generic { msg: error.to_string() }
}
}
@@ -25,6 +29,12 @@ impl From<anyhow::Error> for ClientError {
}
}
impl From<reqwest::Error> for ClientError {
fn from(e: reqwest::Error) -> Self {
Self::new(e)
}
}
impl From<UnexpectedUniFFICallbackError> for ClientError {
fn from(e: UnexpectedUniFFICallbackError) -> Self {
Self::new(e)
@@ -91,6 +101,12 @@ impl From<timeline::Error> for ClientError {
}
}
impl From<timeline::UnsupportedEditItem> for ClientError {
fn from(e: timeline::UnsupportedEditItem) -> Self {
Self::new(e)
}
}
impl From<notification_client::Error> for ClientError {
fn from(e: notification_client::Error) -> Self {
Self::new(e)
@@ -115,12 +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 {
@@ -192,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;
+64 -11
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,
@@ -121,9 +127,9 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
pub enum MessageLikeEventContent {
CallAnswer,
CallInvite,
CallNotify { notify_type: NotifyType },
CallHangup,
CallCandidates,
CallNotify { notify_type: NotifyType },
KeyVerificationReady,
KeyVerificationStart,
KeyVerificationCancel,
@@ -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,
}
@@ -146,14 +152,14 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
let content = match value {
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
AnySyncMessageLikeEvent::CallNotify(content) => {
let original_content = get_message_like_event_original_content(content)?;
MessageLikeEventContent::CallNotify {
notify_type: original_content.notify_type.into(),
}
}
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
MessageLikeEventContent::KeyVerificationReady
}
@@ -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"),
};
@@ -289,6 +305,7 @@ pub enum MessageLikeEventType {
CallCandidates,
CallHangup,
CallInvite,
CallNotify,
KeyVerificationAccept,
KeyVerificationCancel,
KeyVerificationDone,
@@ -314,6 +331,7 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
match val {
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallNotify => Self::CallNotify,
MessageLikeEventType::CallHangup => Self::CallHangup,
MessageLikeEventType::CallCandidates => Self::CallCandidates,
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
@@ -337,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,
}
+5 -20
View File
@@ -2,36 +2,21 @@
#![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_service;
mod authentication;
mod chunk_iterator;
mod client;
mod client_builder;
mod element;
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;
@@ -60,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 {
@@ -308,14 +308,12 @@ impl NotificationSettings {
.await
{
enabled
// Check unstable identifier
} else if let Ok(enabled) = notification_settings
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
.await
{
enabled
} else {
false
// Check unstable identifier
notification_settings
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
.await
.unwrap_or(false)
}
}
+69 -10
View File
@@ -1,14 +1,19 @@
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,
EnvFilter, Layer,
};
#[cfg(target_os = "android")]
pub fn log_panics() {
std::env::set_var("RUST_BACKTRACE", "1");
log_panics::init();
@@ -98,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}}}")?;
}
@@ -134,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 +
@@ -146,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)
@@ -155,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)
@@ -165,26 +209,41 @@ 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) {
#[cfg(target_os = "android")]
log_panics();
tracing_subscriber::registry()
+300 -52
View File
@@ -1,10 +1,15 @@
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},
RoomMemberships, RoomState,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
use mime::Mime;
@@ -15,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;
@@ -29,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::{EventTimelineItem, 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 {
@@ -50,6 +58,7 @@ impl From<RoomState> for Membership {
RoomState::Invited => Membership::Invited,
RoomState::Joined => Membership::Joined,
RoomState::Left => Membership::Left,
RoomState::Knocked => Membership::Knocked,
}
}
}
@@ -72,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()
@@ -81,8 +90,8 @@ impl Room {
/// Returns the room's name from the state event if available, otherwise
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
pub fn display_name(&self) -> Result<String, ClientError> {
Ok(RUNTIME.block_on(self.inner.computed_display_name())?.to_string())
pub fn display_name(&self) -> Option<String> {
Some(self.inner.cached_display_name()?.to_string())
}
/// The raw name as present in the room state event.
@@ -126,6 +135,11 @@ impl Room {
self.inner.state().into()
}
/// Returns the room heroes for this room.
pub fn heroes(&self) -> Vec<RoomHero> {
self.inner.heroes().into_iter().map(Into::into).collect()
}
/// Is there a non expired membership with application "m.call" and scope
/// "m.room" in this room.
pub fn has_active_room_call(&self) -> bool {
@@ -134,7 +148,7 @@ impl Room {
/// Returns a Vec of userId's that participate in the room call.
///
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
/// MatrixRTC memberships with application "m.call" and scope "m.room" are
/// considered. A user can occur twice if they join with two devices.
/// convert to a set depending if the different users are required or the
/// amount of sessions.
@@ -148,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
}
@@ -219,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())?)
}
@@ -236,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> {
@@ -257,38 +298,7 @@ impl Room {
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
// Look for a local event in the `Timeline`.
//
// First off, let's see if a `Timeline` exists…
if let Some(timeline) = self.timeline.read().await.clone() {
// If it contains a `latest_event`…
if let Some(timeline_last_event) = timeline.inner.latest_event().await {
// If it's a local echo…
if timeline_last_event.is_local_echo() {
return Ok(RoomInfo::new(
&self.inner,
Some(Arc::new(EventTimelineItem(timeline_last_event))),
)
.await?);
}
}
}
// Otherwise, create a synthetic [`EventTimelineItem`] using the classical
// [`Room`] path.
let latest_event = match self.inner.latest_event() {
Some(latest_event) => matrix_sdk_ui::timeline::EventTimelineItem::from_latest_event(
self.inner.client(),
self.inner.room_id(),
latest_event,
)
.await
.map(EventTimelineItem)
.map(Arc::new),
None => None,
};
Ok(RoomInfo::new(&self.inner, latest_event).await?)
Ok(RoomInfo::new(&self.inner).await?)
}
pub fn subscribe_to_room_info_updates(
@@ -332,8 +342,8 @@ impl Room {
///
/// * `event_id` - The ID of the event to redact
///
/// * `reason` - The reason for the event being redacted (optional).
/// its transaction ID (optional). If not given one is created.
/// * `reason` - The reason for the event being redacted (optional). its
/// transaction ID (optional). If not given one is created.
pub async fn redact(
&self,
event_id: String,
@@ -544,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,
@@ -575,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> {
@@ -593,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))
}
@@ -651,6 +691,7 @@ impl Room {
/// This function is supposed to be called whenever the user creates a room
/// call. It will send a `m.call.notify` event if:
/// - there is not yet a running call.
///
/// It will configure the notify type: ring or notify based on:
/// - is this a DM room -> ring
/// - is this a group with more than one other member -> notify
@@ -687,10 +728,122 @@ impl Room {
.await?;
Ok(())
}
/// Returns whether the send queue for that particular room is enabled or
/// not.
pub fn is_send_queue_enabled(&self) -> bool {
self.inner.send_queue().is_enabled()
}
/// Enable or disable the send queue for that particular room.
pub fn enable_send_queue(&self, enable: bool) {
self.inner.send_queue().set_enabled(enable);
}
/// Store the given `ComposerDraft` in the state store using the current
/// room id, as identifier.
pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<(), ClientError> {
Ok(self.inner.save_composer_draft(draft.try_into()?).await?)
}
/// Retrieve the `ComposerDraft` stored in the state store for this room.
pub async fn load_composer_draft(&self) -> Result<Option<ComposerDraft>, ClientError> {
Ok(self.inner.load_composer_draft().await?.map(Into::into))
}
/// Remove the `ComposerDraft` stored in the state store for this room.
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
Ok(self.inner.clear_composer_draft().await?)
}
/// 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> {
@@ -746,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>,
@@ -767,7 +925,7 @@ impl RoomMembersIterator {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomMembersIterator {
fn len(&self) -> u32 {
self.chunk_iterator.len()
@@ -776,7 +934,28 @@ 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())
}
}
/// Information about a member considered to be a room hero.
#[derive(uniffi::Record)]
pub struct RoomHero {
/// The user ID of the hero.
user_id: String,
/// The display name of the hero.
display_name: Option<String>,
/// The avatar URL of the hero.
avatar_url: Option<String>,
}
impl From<SdkRoomHero> for RoomHero {
fn from(value: SdkRoomHero) -> Self {
Self {
user_id: value.user_id.to_string(),
display_name: value.display_name.clone(),
avatar_url: value.avatar_url.as_ref().map(ToString::to_string),
}
}
}
@@ -825,3 +1004,72 @@ impl From<RtcApplicationType> for notify::ApplicationType {
}
}
}
/// Current draft of the composer for the room.
#[derive(uniffi::Record)]
pub struct ComposerDraft {
/// The draft content in plain text.
pub plain_text: String,
/// If the message is formatted in HTML, the HTML representation of the
/// message.
pub html_text: Option<String>,
/// The type of draft.
pub draft_type: ComposerDraftType,
}
impl From<SdkComposerDraft> for ComposerDraft {
fn from(value: SdkComposerDraft) -> Self {
let SdkComposerDraft { plain_text, html_text, draft_type } = value;
Self { plain_text, html_text, draft_type: draft_type.into() }
}
}
impl TryFrom<ComposerDraft> for SdkComposerDraft {
type Error = ruma::IdParseError;
fn try_from(value: ComposerDraft) -> std::result::Result<Self, Self::Error> {
let ComposerDraft { plain_text, html_text, draft_type } = value;
Ok(Self { plain_text, html_text, draft_type: draft_type.try_into()? })
}
}
/// The type of draft of the composer.
#[derive(uniffi::Enum)]
pub enum ComposerDraftType {
/// The draft is a new message.
NewMessage,
/// The draft is a reply to an event.
Reply {
/// The ID of the event being replied to.
event_id: String,
},
/// The draft is an edit of an event.
Edit {
/// The ID of the event being edited.
event_id: String,
},
}
impl From<SdkComposerDraftType> for ComposerDraftType {
fn from(value: SdkComposerDraftType) -> Self {
match value {
SdkComposerDraftType::NewMessage => Self::NewMessage,
SdkComposerDraftType::Reply { event_id } => Self::Reply { event_id: event_id.into() },
SdkComposerDraftType::Edit { event_id } => Self::Edit { event_id: event_id.into() },
}
}
}
impl TryFrom<ComposerDraftType> for SdkComposerDraftType {
type Error = ruma::IdParseError;
fn try_from(value: ComposerDraftType) -> std::result::Result<Self, Self::Error> {
let draft_type = match value {
ComposerDraftType::NewMessage => Self::NewMessage,
ComposerDraftType::Reply { event_id } => Self::Reply { event_id: event_id.try_into()? },
ComposerDraftType::Edit { event_id } => Self::Edit { event_id: event_id.try_into()? },
};
Ok(draft_type)
}
}
+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>);
}
+22 -15
View File
@@ -1,15 +1,17 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use matrix_sdk::RoomState;
use crate::{
notification_settings::RoomNotificationMode, room::Membership, room_member::RoomMember,
timeline::EventTimelineItem,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero},
room_member::RoomMember,
};
#[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>,
@@ -25,20 +27,20 @@ pub struct RoomInfo {
canonical_alias: Option<String>,
alternative_aliases: Vec<String>,
membership: Membership,
latest_event: Option<Arc<EventTimelineItem>>,
/// Member who invited the current user to a room that's in the invited
/// state.
///
/// Can be missing if the room membership invite event is missing from the
/// store.
inviter: Option<RoomMember>,
heroes: Vec<RoomHero>,
active_members_count: u64,
invited_members_count: u64,
joined_members_count: u64,
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
@@ -52,13 +54,12 @@ 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 {
pub(crate) async fn new(
room: &matrix_sdk::Room,
latest_event: Option<Arc<EventTimelineItem>>,
) -> matrix_sdk::Result<Self> {
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
let unread_notification_counts = room.unread_notification_counts();
let power_levels_map = room.users_with_power_levels().await;
@@ -66,10 +67,13 @@ 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(),
display_name: room.computed_display_name().await.ok().map(|name| name.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(),
avatar_url: room.avatar_url().map(Into::into),
@@ -81,25 +85,27 @@ impl RoomInfo {
canonical_alias: room.canonical_alias().map(Into::into),
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
membership: room.state().into(),
latest_event,
inviter: match room.state() {
RoomState::Invited => room
.invite_details()
.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(),
active_members_count: room.active_members_count(),
invited_members_count: room.invited_members_count(),
joined_members_count: room.joined_members_count(),
user_power_levels,
highlight_count: unread_notification_counts.highlight_count,
notification_count: unread_notification_counts.notification_count,
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,
})
}
}
+293 -215
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();
@@ -125,11 +101,11 @@ impl RoomListService {
})))
}
async fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
Ok(Arc::new(RoomListItem {
inner: Arc::new(self.inner.room(room_id).await?),
inner: Arc::new(self.inner.room(room_id)?),
utd_hook: self.utd_hook.clone(),
}))
}
@@ -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(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomList {
fn loading_state(
&self,
@@ -192,62 +177,121 @@ 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() })
}
async fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
self.room_list_service.room(room_id).await
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
self.room_list_service.room(room_id)
}
}
#[derive(uniffi::Record)]
pub struct RoomListEntriesResult {
pub entries: Vec<RoomListEntry>,
pub entries_stream: Arc<TaskHandle>,
}
#[derive(uniffi::Record)]
#[derive(uniffi::Object)]
pub struct RoomListEntriesWithDynamicAdaptersResult {
pub controller: Arc<RoomListDynamicEntriesController>,
pub entries_stream: Arc<TaskHandle>,
this: Arc<RoomList>,
controller: Arc<RoomListDynamicEntriesController>,
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()
@@ -489,7 +550,7 @@ impl RoomListItem {
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
fn display_name(&self) -> Option<String> {
RUNTIME.block_on(self.inner.computed_display_name())
self.inner.cached_display_name()
}
fn avatar_url(&self) -> Option<String> {
@@ -504,15 +565,82 @@ impl RoomListItem {
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
let latest_event = self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new);
Ok(RoomInfo::new(self.inner.inner_room(), latest_event).await?)
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
Ok(RoomInfo::new(self.inner.inner_room()).await?)
}
/// Building a `Room`. If its internal timeline hasn't been initialized
/// it'll fail.
async fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
/// 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.
///
/// 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(),
@@ -567,66 +695,16 @@ impl RoomListItem {
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
}
fn subscribe(&self, settings: Option<RoomSubscription>) {
self.inner.subscribe(settings.map(Into::into));
/// Checks whether the room is encrypted or not.
///
/// **Note**: this info may not be reliable if you don't set up
/// `m.room.encryption` as required state.
async fn is_encrypted(&self) -> bool {
self.inner.is_encrypted().await.unwrap_or(false)
}
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)
}
}
@@ -636,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 {
@@ -246,7 +340,10 @@ impl SessionVerificationController {
}
break;
}
SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (),
SasState::Created { .. }
| SasState::Started { .. }
| SasState::Accepted { .. }
| SasState::Confirmed => (),
}
}
}
+33 -14
View File
@@ -25,6 +25,7 @@ use matrix_sdk_ui::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
},
};
use tracing::error;
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
@@ -50,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);
}
@@ -61,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 {
@@ -93,6 +94,7 @@ impl SyncService {
#[derive(Clone, uniffi::Object)]
pub struct SyncServiceBuilder {
client: Client,
builder: MatrixSyncServiceBuilder,
utd_hook: Option<Arc<UtdHookManager>>,
@@ -100,29 +102,46 @@ pub struct SyncServiceBuilder {
impl SyncServiceBuilder {
pub(crate) fn new(client: Client) -> Arc<Self> {
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook: None })
Arc::new(Self {
client: client.clone(),
builder: MatrixSyncService::builder(client),
utd_hook: None,
})
}
}
#[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);
Arc::new(Self { builder, utd_hook: this.utd_hook })
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
pub fn with_utd_hook(self: Arc<Self>, delegate: Box<dyn UnableToDecryptDelegate>) -> Arc<Self> {
pub async fn with_utd_hook(
self: Arc<Self>,
delegate: Box<dyn UnableToDecryptDelegate>,
) -> Arc<Self> {
// UTDs detected before this duration may be reclassified as "late decryption"
// events (or discarded, if they get decrypted fast enough).
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
let this = unwrap_or_clone_arc(self);
let utd_hook = Some(Arc::new(
UtdHookManager::new(Arc::new(UtdHook { delegate }))
.with_max_delay(UTD_HOOK_GRACE_PERIOD),
));
Arc::new(Self { builder: this.builder, utd_hook })
let mut utd_hook = UtdHookManager::new(Arc::new(UtdHook { delegate }), this.client.clone())
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
if let Err(e) = utd_hook.reload_from_store().await {
error!("Unable to reload UTD hook data from data store: {}", e);
// Carry on with the setup anyway; we shouldn't fail setup just
// because the UTD hook failed to load its data.
}
Arc::new(Self {
client: this.client,
builder: this.builder,
utd_hook: Some(Arc::new(utd_hook)),
})
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
@@ -134,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) {
+107 -78
View File
@@ -15,48 +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::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(),
url: content.url.to_string(),
source: Arc::new(MediaSource::from(content.source.clone())),
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
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()
@@ -71,47 +76,79 @@ 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,
info: ImageInfo,
url: String,
source: Arc<MediaSource>,
},
Poll {
question: String,
@@ -123,6 +160,7 @@ pub enum TimelineItemContentKind {
has_been_edited: bool,
},
CallInvite,
CallNotify,
UnableToDecrypt {
msg: EncryptedMessage,
},
@@ -130,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>,
@@ -153,45 +192,36 @@ 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()
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
impl InReplyToDetails {
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
Self { event_id, event }
}
}
#[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(),
},
@@ -204,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 },
}
@@ -252,7 +282,6 @@ impl EncryptedMessage {
#[derive(Clone, uniffi::Record)]
pub struct Reaction {
pub key: String,
pub count: u64,
pub senders: Vec<ReactionSenderData>,
}
@@ -322,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> },
@@ -365,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)
@@ -403,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> {
+6 -6
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).
///
@@ -107,10 +107,10 @@ impl Span {
/// target passed for second and following creation of a span with the same
/// callsite will be ignored.
///
/// This function leaks a little bit of memory for each unique (file + line
/// + level + target + name) 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.
/// This function leaks a little bit of memory for each unique (file +
/// line + level + target + name) 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.
///
/// For a span to have an effect, you must `.enter()` it at the start of a
/// logical unit of work and `.exit()` it at the end of the same (including
+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())
}
}
+134 -23
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>,
@@ -236,10 +237,12 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
/// This function returns a `WidgetSettings` object which can be used
/// to setup a widget using `run_client_widget_api`
/// and to generate the correct url for the widget.
/// # Arguments
/// * - `props` A struct containing the configuration parameters for a element
///
/// # Arguments
///
/// * `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> {
@@ -259,35 +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![
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() },
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
// 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: "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,
}
}
@@ -317,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.
///
@@ -349,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 {
@@ -357,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,
}
}
}
@@ -367,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 },
@@ -424,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;
}
@@ -495,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"
@@ -1,4 +1,4 @@
This models the room key sharing algorithm as a decision tree and provides
This models the room key forwarding algorithm as a decision tree and provides
tooling to render it as a PDF or PNG.
# Usage
@@ -1,5 +1,5 @@
digraph {
label="Matrix room key sharing algorithm"
label="Matrix room key forwarding algorithm"
fontname="Fira Sans"
ratio=0.5
@@ -16,10 +16,10 @@ digraph {
/* End states */
allow_verified [label="Share the entire session from the earliest known index.\n\nOk(None)", color=4, fillcolor=3]
allow_limited [label="Share a limited session starting from index i, which is the index we previously shared at.\n\nOk(Some(i))", color=4, fillcolor=3]
refuse_device_key_changed [label="Sender key changed, refuse to share.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
refuse_not_shared [label="Session was never shared with this device, refuse to share.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
refuse_device_key_changed [label="Sender key changed, refuse to forward.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
refuse_not_shared [label="Session was never shared with this device, refuse to forward.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
/* Checks */
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

+77 -4
View File
@@ -1,8 +1,81 @@
# unreleased
# Changelog
- 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
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`.
- `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`.
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- [**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
+20 -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,16 +50,20 @@ 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 }
growable-bloom-filter = { workspace = true }
http = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "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 }
@@ -65,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"] }
-7
View File
@@ -1,7 +0,0 @@
fn main() {
// Prevent unnecessary rerunning of this build script
println!("cargo:rerun-if-changed=build.rs");
// Prevent nightly CI from erroring on tarpaulin_include attribute
println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)");
}
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) }
}
}
+151 -26
View File
@@ -1,17 +1,27 @@
//! Utilities for working with events to decide whether they are suitable for
//! use as a [crate::Room::latest_event].
#![cfg(feature = "experimental-sliding-sync")]
#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
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, relation::RelationType},
MxcUri, OwnedEventId,
events::{
room::{
member::{MembershipState, SyncRoomMemberEvent},
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
AnySyncStateEvent,
},
MxcUri, OwnedEventId, UserId,
};
use serde::{Deserialize, Serialize};
@@ -26,12 +36,21 @@ 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),
/// This message is suitable - it is a call invite
YesCallInvite(&'a SyncCallInviteEvent),
/// 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
@@ -45,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)) => {
@@ -62,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);
@@ -78,6 +99,14 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
PossibleLatestEvent::YesCallInvite(invite)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
PossibleLatestEvent::YesCallNotify(notify)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
PossibleLatestEvent::YesSticker(sticker)
}
// Encrypted events are not suitable
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
PossibleLatestEvent::NoEncrypted
@@ -90,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
}
}
}
@@ -170,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),
}
@@ -256,11 +306,19 @@ mod tests {
events::{
call::{
invite::{CallInviteEventContent, SyncCallInviteEvent},
notify::{
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
},
SessionDescription,
},
poll::unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
UnstablePollStartContentBlock,
poll::{
unstable_response::{
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
},
unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
UnstablePollAnswer, UnstablePollStartContentBlock,
},
},
relation::Replacement,
room::{
@@ -277,7 +335,7 @@ mod tests {
},
sticker::{StickerEventContent, SyncStickerEvent},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
UnsignedRoomRedactionEvent,
},
@@ -307,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");
@@ -330,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?");
@@ -354,12 +412,34 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallInvite(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_call_notifications_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
content: CallNotifyEventContent::new(
"call_id".into(),
ApplicationType::Call,
NotifyType::Ring,
Mentions::new(),
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[test]
fn test_stickers_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
content: StickerEventContent::new(
@@ -375,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
);
}
@@ -403,7 +505,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
@@ -425,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]
@@ -442,7 +547,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
@@ -466,7 +571,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -497,9 +602,12 @@ mod tests {
json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
}
},
}
@@ -513,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
});
+12 -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,10 +56,14 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomInfoUpdate, RoomMember,
RoomMemberships, RoomState, RoomStateFilter,
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
RoomStateFilter,
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
};
pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
};
+52 -16
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())
}
@@ -114,7 +150,7 @@ pub trait MediaEventContent {
impl MediaEventContent for StickerEventContent {
fn source(&self) -> Option<MediaSource> {
Some(MediaSource::Plain(self.url.clone()))
Some(MediaSource::from(self.source.clone()))
}
fn thumbnail_source(&self) -> Option<MediaSource> {
@@ -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,
}
+13 -19
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,
@@ -563,6 +561,7 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
match event {
AnySyncMessageLikeEvent::CallAnswer(_)
| AnySyncMessageLikeEvent::CallInvite(_)
| AnySyncMessageLikeEvent::CallNotify(_)
| AnySyncMessageLikeEvent::CallHangup(_)
| AnySyncMessageLikeEvent::CallCandidates(_)
| AnySyncMessageLikeEvent::CallNegotiate(_)
@@ -761,17 +760,16 @@ mod tests {
#[test]
fn test_count_unread_and_mentions() {
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
SyncTimelineEvent::new_with_push_actions(
sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions,
}
)
}
let user_id = user_id!("@alice:example.org");
@@ -846,17 +844,13 @@ mod tests {
// When provided with one event, that's not the receipt event, we don't count
// it.
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
SyncTimelineEvent {
event: sync_timeline_event!({
"sender": "@bob:example.org",
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
encryption_info: None,
push_actions: Vec::new(),
}
SyncTimelineEvent::new(sync_timeline_event!({
"sender": "@bob:example.org",
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}))
}
let mut receipts = RoomReadReceipts {
@@ -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>>,
}

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