Compare commits

...

316 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
309 changed files with 21061 additions and 8909 deletions
-48
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,43 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
# Exclude tarpaulin, android and ios from extra lints since on stable, without
# the nightly-only target-applies-to-host setting at the top, cross compilation
# and otherwise changing cfg's can be very bad for caching. These should never
# be the default either and don't have much target-specific code that would
# benefit from the extra lints.
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
rustflags = [
"-Wrust_2018_idioms",
"-Wsemicolon_in_expressions_from_macros",
"-Wunused_extern_crates",
"-Wunused_import_braces",
"-Wunused_qualifications",
"-Wtrivial_casts",
"-Wtrivial_numeric_casts",
"-Wclippy::cloned_instead_of_copied",
"-Wclippy::dbg_macro",
"-Wclippy::inefficient_to_string",
"-Wclippy::macro_use_imports",
"-Wclippy::mut_mut",
"-Wclippy::needless_borrow",
"-Wclippy::nonstandard_macro_braces",
"-Wclippy::str_to_string",
"-Wclippy::todo",
"-Wclippy::unused_async",
"-Wclippy::redundant_clone",
]
[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
+1 -3
View File
@@ -10,7 +10,7 @@ exclude = [
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-0370", reason = "Waiting for a Aquamarine release" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
]
[licenses]
@@ -54,8 +54,6 @@ allow-git = [
"https://github.com/element-hq/tracing.git",
# Sam as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
+5 -5
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -69,10 +69,10 @@ jobs:
steps:
- name: Checkout Rust SDK
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
@@ -136,7 +136,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
@@ -191,7 +191,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
+32 -12
View File
@@ -40,15 +40,19 @@ jobs:
- markdown
- socks
- sso-login
- image-proc
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- 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:
@@ -81,7 +85,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -112,7 +116,12 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
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
@@ -158,13 +167,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
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:
@@ -220,7 +235,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -268,7 +283,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
@@ -286,10 +301,10 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.25.0
uses: crate-ci/typos@v1.27.3
clippy:
name: Run clippy
@@ -298,7 +313,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -356,7 +371,7 @@ jobs:
# 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:v1.115.0 # 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
@@ -365,7 +380,12 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4.2.0
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
+8 -3
View File
@@ -49,7 +49,7 @@ jobs:
# 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:v1.115.0 # 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
@@ -58,10 +58,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
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
@@ -89,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
@@ -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 -3
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -51,8 +51,6 @@ jobs:
# Keep in sync with xtask docs
- name: Build documentation
env:
# Work around https://github.com/rust-lang/cargo/issues/10744
CARGO_TARGET_APPLIES_TO_HOST: "true"
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
run:
cargo doc --no-deps --workspace --features docsrs
+1 -1
View File
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.0
- uses: actions/checkout@v4
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.0
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
+3 -3
View File
@@ -1,6 +1,6 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Codecov
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
@@ -58,13 +58,13 @@ jobs:
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v4.2.0
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@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4
- name: Calculate cache key
id: cachekey
+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).
+96 -27
View File
@@ -29,43 +29,112 @@ 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
@@ -126,7 +195,7 @@ requested.
commits, the [autosquash] option can help with this.
```bash
git rebase main --autosquash
git rebase main --interactive --autosquash
```
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
Generated
+158 -627
View File
File diff suppressed because it is too large Load Diff
+33 -16
View File
@@ -32,13 +32,11 @@ as_variant = "1.2.0"
base64 = "0.22.0"
byteorder = "1.4.3"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.5.0", features = ["tracing"] }
eyeball-im-util = "0.6.0"
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 = "3.0.0"
@@ -47,7 +45,7 @@ once_cell = "1.16.0"
pin-project-lite = "0.2.9"
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a", features = [
ruma = { version = "0.11.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -61,7 +59,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21d
"unstable-msc4075",
"unstable-msc4140",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" }
ruma-common = "0.14.1"
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
@@ -81,16 +79,17 @@ 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]
@@ -130,8 +129,26 @@ tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "c
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
+3
View File
@@ -36,3 +36,6 @@ harness = false
[[bench]]
name = "room_bench"
harness = false
[package.metadata.release]
release = false
+10 -5
View File
@@ -74,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
.block_on(sqlite_store.save_changes(&changes))
.expect("initial filling of sqlite failed");
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
let base_client = BaseClient::with_store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
@@ -171,8 +174,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
);
let room = client.get_room(&room_id).expect("Room not found");
assert!(!room.pinned_event_ids().is_empty());
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
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");
@@ -191,8 +195,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
b.to_async(&runtime).iter(|| async {
assert!(!room.pinned_event_ids().is_empty());
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
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;
+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");
@@ -26,6 +26,7 @@ futures-util = { workspace = true }
hmac = "0.12.1"
http = { 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 }
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
[lints]
workspace = true
[package.metadata.release]
release = false
@@ -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,
+9 -9
View File
@@ -196,7 +196,7 @@ impl From<anyhow::Error> for MigrationError {
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate(
data: MigrationData,
path: String,
@@ -359,7 +359,7 @@ async fn save_changes(
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_sessions(
data: SessionMigrationData,
path: String,
@@ -532,7 +532,7 @@ fn collect_sessions(
/// * `passphrase` - The passphrase that should be used to encrypt the data at
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_room_settings(
room_settings: HashMap<String, RoomSettings>,
path: String,
@@ -558,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
///
@@ -794,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> {
@@ -891,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(),
@@ -915,12 +915,12 @@ 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()
}
@@ -935,7 +935,7 @@ pub struct PkEncryption {
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl PkEncryption {
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
/// encoded as Base64.
+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)) };
@@ -42,7 +42,8 @@ use ruma::{
},
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};
@@ -178,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -528,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"),
@@ -540,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 {
@@ -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,6 +29,8 @@ 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,
},
}
@@ -44,6 +48,7 @@ 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(),
}
}
SdkUserIdentity::Other(i) => {
@@ -54,6 +59,7 @@ impl UserIdentity {
user_id: i.user_id().to_string(),
master_key: serde_json::to_string(&master)?,
self_signing_key: serde_json::to_string(&self_signing)?,
has_verification_violation: i.has_verification_violation(),
}
}
})
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
/// Listener that will be passed over the FFI to report changes to a SAS
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SasListener: Send {
/// The callback that should be called on the Rust side
///
@@ -82,7 +82,7 @@ pub struct Verification {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Verification {
/// Try to represent the `Verification` as an `Sas` verification object,
/// returns `None` if the verification is not a `Sas` verification.
@@ -112,7 +112,7 @@ pub struct Sas {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Sas {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -276,7 +276,7 @@ impl Sas {
/// Listener that will be passed over the FFI to report changes to a QrCode
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrCodeListener: Send {
/// The callback that should be called on the Rust side
///
@@ -328,7 +328,7 @@ pub struct QrCode {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCode {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult {
/// Listener that will be passed over the FFI to report changes to a
/// verification request.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationRequestListener: Send {
/// The callback that should be called on the Rust side
///
@@ -562,7 +562,7 @@ pub struct VerificationRequest {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl VerificationRequest {
/// The id of the other user that is participating in this verification
/// request.
@@ -752,7 +752,7 @@ impl VerificationRequest {
RustVerificationRequestState::Ready {
their_methods,
our_methods,
other_device_id: _,
other_device_data: _,
} => VerificationRequestState::Ready {
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
+24
View File
@@ -0,0 +1,24 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
name = "matrix-sdk-ffi-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
[lib]
proc-macro = true
test = false
doctest = false
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.18"
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
[lints]
workspace = true
+14
View File
@@ -0,0 +1,14 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
# matrix-sdk-ffi-macros
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use proc_macro::TokenStream;
use quote::quote;
use syn::{ImplItem, Item, TraitItem};
/// Attribute to specify the async runtime parameter for the `uniffi`
/// export macros if there any `async fn`s in the input.
#[proc_macro_attribute]
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
let has_async_fn = |item| {
if let Item::Fn(fun) = &item {
if fun.sig.asyncness.is_some() {
return true;
}
} else if let Item::Impl(blk) = &item {
for item in &blk.items {
if let ImplItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
}
false
};
let attr2 = proc_macro2::TokenStream::from(attr);
let item2 = proc_macro2::TokenStream::from(item.clone());
let res = match syn::parse(item) {
Ok(item) => match has_async_fn(item) {
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
false => quote! { #[uniffi::export(#attr2)] },
},
Err(e) => e.into_compile_error(),
};
quote! {
#res
#item2
}
.into()
}
+4 -1
View File
@@ -28,11 +28,11 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
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 }
@@ -82,3 +82,6 @@ features = [
[lints]
workspace = true
[package.metadata.release]
release = false
+10 -3
View File
@@ -19,17 +19,18 @@ use matrix_sdk::{
};
use url::Url;
use crate::client::{Client, SlidingSyncVersion};
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,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl HomeserverLoginDetails {
/// The URL of the currently configured homeserver.
pub fn url(&self) -> String {
@@ -46,6 +47,12 @@ impl HomeserverLoginDetails {
self.supports_oidc_login
}
/// The prompts advertised by the authentication issuer for use in the login
/// URL.
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
self.supported_oidc_prompts.clone()
}
/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> bool {
self.supports_password_login
@@ -62,7 +69,7 @@ pub struct SsoHandler {
pub(crate) url: String,
}
#[uniffi::export(async_runtime = "tokio")]
#[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
+308 -63
View File
@@ -1,7 +1,6 @@
use std::{
collections::HashMap,
fmt::Debug,
mem::ManuallyDrop,
path::Path,
sync::{Arc, RwLock},
};
@@ -9,7 +8,8 @@ use std::{
use anyhow::{anyhow, Context as _};
use matrix_sdk::{
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSettings,
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
MediaThumbnailSettings,
},
oidc::{
registrations::{ClientId, OidcRegistrations},
@@ -19,12 +19,13 @@ use matrix_sdk::{
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
requests::Prompt as SdkOidcPrompt,
},
OidcAuthorizationData, OidcSession,
},
reqwest::StatusCode,
ruma::{
api::client::{
media::get_content_thumbnail::v3::Method,
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
@@ -40,7 +41,7 @@ use matrix_sdk::{
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
sliding_sync::Version as SdkSlidingSyncVersion,
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -54,7 +55,8 @@ use ruma::{
},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
@@ -77,6 +79,7 @@ use crate::{
ruma::AuthData,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utils::AsyncRuntimeDropped,
ClientError,
};
@@ -140,25 +143,25 @@ impl From<PushFormat> for RumaPushFormat {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientDelegate: Sync + Send {
fn did_receive_auth_error(&self, is_soft_logout: bool);
fn did_refresh_tokens(&self);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientSessionDelegate: Sync + Send {
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
fn save_session_in_keychain(&self, session: Session);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressWatcher: Send + Sync {
fn transmission_progress(&self, progress: TransmissionProgress);
}
/// A listener to the global (client-wide) error reporter of the send queue.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomErrorListener: Sync + Send {
/// Called every time the send queue has ran into an error for a given room,
/// which will disable the send queue for that particular room.
@@ -182,58 +185,52 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: ManuallyDrop<MatrixClient>,
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
}
impl Drop for Client {
fn drop(&mut self) {
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl Client {
pub async fn new(
sdk_client: MatrixClient,
cross_process_refresh_lock_id: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let ctrl = session_verification_controller.clone();
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
}
});
let cross_process_store_locks_holder_name =
sdk_client.cross_process_store_locks_holder_name().to_owned();
let client = Client {
inner: ManuallyDrop::new(sdk_client),
inner: AsyncRuntimeDropped::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
};
if let Some(process_id) = cross_process_refresh_lock_id {
if enable_oidc_refresh_lock {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates when enabling the cross-process lock"
))?;
}
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
client
.inner
.oidc()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
}
if let Some(session_delegate) = session_delegate {
@@ -260,11 +257,35 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Information about login options for the client's homeserver.
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok();
let oidc = self.inner.oidc();
let (supports_oidc_login, supported_oidc_prompts) = match oidc
.fetch_authentication_issuer()
.await
{
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
Ok(metadata) => {
let prompts = metadata
.prompt_values_supported
.as_ref()
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
(true, prompts)
}
Err(error) => {
error!("Failed to fetch OIDC provider metadata: {error}");
(true, Default::default())
}
},
Err(error) => {
error!("Failed to fetch authentication issuer: {error}");
(false, Default::default())
}
};
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
let sliding_sync_version = self.sliding_sync_version();
@@ -272,6 +293,7 @@ impl Client {
url: self.homeserver(),
sliding_sync_version,
supports_oidc_login,
supported_oidc_prompts,
supports_password_login,
})
}
@@ -360,13 +382,14 @@ impl Client {
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
}
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// Requests the URL needed for opening a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns. If a failure occurs and a callback isn't available, make sure
/// to call `abort_oidc_login` to inform the client of this.
pub async fn url_for_oidc_login(
/// to call `abort_oidc_auth` to inform the client of this.
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: OidcPrompt,
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
@@ -387,14 +410,15 @@ impl Client {
static_registrations,
)?;
let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?;
let data =
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
Ok(Arc::new(data))
}
/// Aborts an existing OIDC login operation that might have been cancelled,
/// failed etc.
pub async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
self.inner.oidc().abort_authorization(&authorization_data.state).await;
}
@@ -414,7 +438,7 @@ impl Client {
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
body: Option<String>,
filename: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
@@ -426,8 +450,8 @@ impl Client {
.inner
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
body,
&MediaRequestParameters { source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
@@ -474,7 +498,7 @@ impl Client {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Respawn tasks for rooms that had unsent events. At this point we've just
// created the subscriber, so it'll be notified about errors.
q.respawn_tasks_for_rooms_with_unsent_events().await;
q.respawn_tasks_for_rooms_with_unsent_requests().await;
loop {
match subscriber.recv().await {
@@ -526,7 +550,7 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
@@ -577,6 +601,10 @@ impl Client {
&self,
action: Option<AccountManagementAction>,
) -> Result<Option<String>, ClientError> {
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
return Ok(None);
}
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
Ok(url) => Ok(url.map(|u| u.to_string())),
Err(e) => {
@@ -642,7 +670,7 @@ impl Client {
}
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let response = self.inner.create_room(request.into()).await?;
let response = self.inner.create_room(request.try_into()?).await?;
Ok(String::from(response.room_id()))
}
@@ -675,7 +703,7 @@ impl Client {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<String, ClientError> {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = self.inner.media().upload(&mime_type, data);
let request = self.inner.media().upload(&mime_type, data, None);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
@@ -697,10 +725,11 @@ impl Client {
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
debug!(?source, "requesting media file");
Ok(self
.inner
.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
.await?)
}
@@ -712,14 +741,14 @@ impl Client {
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
debug!(source = ?media_source, width, height, "requesting media thumbnail");
Ok(self
.inner
.media()
.get_media_content(
&MediaRequest {
&MediaRequestParameters {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
Method::Scale,
UInt::new(width).unwrap(),
UInt::new(height).unwrap(),
)),
@@ -972,6 +1001,20 @@ impl Client {
Ok(Arc::new(Room::new(room)))
}
/// Knock on a room to join it using its ID or alias.
pub async fn knock(
&self,
room_id_or_alias: String,
reason: Option<String>,
server_names: Vec<String>,
) -> Result<Arc<Room>, ClientError> {
let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
let server_names =
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
let room = self.inner.knock(room_id, reason, server_names).await?;
Ok(Arc::new(Room::new(room)))
}
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
Ok(self
.inner
@@ -994,10 +1037,21 @@ impl Client {
pub async fn resolve_room_alias(
&self,
room_alias: String,
) -> Result<ResolvedRoomAlias, ClientError> {
) -> Result<Option<ResolvedRoomAlias>, ClientError> {
let room_alias = RoomAliasId::parse(&room_alias)?;
let response = self.inner.resolve_room_alias(&room_alias).await?;
Ok(response.into())
match self.inner.resolve_room_alias(&room_alias).await {
Ok(response) => Ok(Some(response.into())),
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
Some(StatusCode::NOT_FOUND) => Ok(None),
_ => Err(http_error.into()),
},
Err(error) => Err(error.into()),
}
}
/// Checks if a room alias exists in the current homeserver.
pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
}
/// Given a room id, get the preview of a room, to interact with it.
@@ -1009,7 +1063,7 @@ impl Client {
&self,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
let via_servers = via_servers
@@ -1022,16 +1076,16 @@ impl Client {
// rustc win that one fight.
let room_id: &RoomId = &room_id;
let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}
/// Given a room alias, get the preview of a room, to interact with it.
pub async fn get_room_preview_from_room_alias(
&self,
room_alias: String,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
@@ -1039,9 +1093,9 @@ impl Client {
// rustc win that one fight.
let room_alias: &RoomAliasId = &room_alias;
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}
/// Waits until an at least partially synced room is received, and returns
@@ -1083,9 +1137,32 @@ impl Client {
Ok(())
}
/// Checks if a room alias is not in use yet.
///
/// Returns:
/// - `Ok(true)` if the room alias is available.
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
/// status code).
/// - An `Err` otherwise.
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
let alias = RoomAliasId::parse(alias)?;
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
}
/// Creates a new room alias associated with the provided room id.
pub async fn create_room_alias(
&self,
room_alias: String,
room_id: String,
) -> Result<(), ClientError> {
let room_alias = RoomAliasId::parse(room_alias)?;
let room_id = RoomId::parse(room_id)?;
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
fn call(&self, ignored_user_ids: Vec<String>);
}
@@ -1306,16 +1383,23 @@ pub struct CreateRoomParameters {
pub avatar: Option<String>,
#[uniffi(default = None)]
pub power_level_content_override: Option<PowerLevels>,
#[uniffi(default = None)]
pub join_rule_override: Option<JoinRule>,
#[uniffi(default = None)]
pub canonical_alias: Option<String>,
}
impl From<CreateRoomParameters> for create_room::v3::Request {
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
type Error = ClientError;
fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
let mut request = create_room::v3::Request::new();
request.name = value.name;
request.topic = value.topic;
request.is_direct = value.is_direct;
request.visibility = value.visibility.into();
request.preset = Some(value.preset.into());
request.room_alias_name = value.canonical_alias;
request.invite = match value.invite {
Some(invite) => invite
.iter()
@@ -1343,6 +1427,12 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
if let Some(join_rule_override) = value.join_rule_override {
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
if let Some(power_levels) = value.power_level_content_override {
@@ -1351,12 +1441,14 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
request.power_level_content_override = Some(power_levels);
}
Err(e) => {
error!("Failed to serialize power levels, error: {e}");
return Err(ClientError::Generic {
msg: format!("Failed to serialize power levels, error: {e}"),
})
}
}
}
request
Ok(request)
}
}
@@ -1642,7 +1734,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn gen_transaction_id() -> String {
TransactionId::new().to_string()
}
@@ -1660,7 +1752,7 @@ impl MediaFileHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> Result<String, ClientError> {
@@ -1724,3 +1816,156 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum OidcPrompt {
/// The Authorization Server must not display any authentication or consent
/// user interface pages.
None,
/// The Authorization Server should prompt the End-User for
/// reauthentication.
Login,
/// The Authorization Server should prompt the End-User for consent before
/// returning information to the Client.
Consent,
/// The Authorization Server should prompt the End-User to select a user
/// account.
///
/// This enables an End-User who has multiple accounts at the Authorization
/// Server to select amongst the multiple accounts that they might have
/// current sessions for.
SelectAccount,
/// The Authorization Server should prompt the End-User to create a user
/// account.
///
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
Create,
/// An unknown value.
Unknown { value: String },
}
impl From<&SdkOidcPrompt> for OidcPrompt {
fn from(value: &SdkOidcPrompt) -> Self {
match value {
SdkOidcPrompt::None => Self::None,
SdkOidcPrompt::Login => Self::Login,
SdkOidcPrompt::Consent => Self::Consent,
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
SdkOidcPrompt::Create => Self::Create,
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
_ => Self::Unknown { value: value.to_string() },
}
}
}
impl From<OidcPrompt> for SdkOidcPrompt {
fn from(value: OidcPrompt) -> Self {
match value {
OidcPrompt::None => Self::None,
OidcPrompt::Login => Self::Login,
OidcPrompt::Consent => Self::Consent,
OidcPrompt::SelectAccount => Self::SelectAccount,
OidcPrompt::Create => Self::Create,
OidcPrompt::Unknown { value } => Self::Unknown(value),
}
}
}
/// The rule used for users wishing to join this room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum JoinRule {
/// Anyone can join the room without any prior action.
Public,
/// A user who wishes to join the room must first receive an invite to the
/// room from someone already inside of the room.
Invite,
/// Users can join the room if they are invited, or they can request an
/// invite to the room.
///
/// They can be allowed (invited) or denied (kicked/banned) access.
Knock,
/// Reserved but not yet implemented by the Matrix specification.
Private,
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s.
Restricted { rules: Vec<AllowRule> },
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s, or they can request
/// an invite to the room.
KnockRestricted { rules: Vec<AllowRule> },
/// A custom join rule, up for interpretation by the consumer.
Custom {
/// The string representation for this custom rule.
repr: String,
},
}
/// An allow rule which defines a condition that allows joining a room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AllowRule {
/// Only a member of the `room_id` Room can join the one this rule is used
/// in.
RoomMembership { room_id: String },
}
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
type Error = ClientError;
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
match value {
JoinRule::Public => Ok(Self::Public),
JoinRule::Invite => Ok(Self::Invite),
JoinRule::Knock => Ok(Self::Knock),
JoinRule::Private => Ok(Self::Private),
JoinRule::Restricted { rules } => {
let rules = allow_rules_from(rules)?;
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::KnockRestricted { rules } => {
let rules = allow_rules_from(rules)?;
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
}
}
}
fn allow_rules_from(
value: Vec<AllowRule>,
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
let mut ret = Vec::with_capacity(value.len());
for rule in value {
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
match rule {
Ok(rule) => ret.push(rule),
Err(error) => return Err(error),
}
}
Ok(ret)
}
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
type Error = ClientError;
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
match value {
AllowRule::RoomMembership { room_id } => {
let room_id = RoomId::parse(room_id)?;
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
room_id,
)))
}
}
}
}
+23 -16
View File
@@ -47,7 +47,7 @@ pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
@@ -159,7 +159,7 @@ pub enum QrLoginProgress {
Done,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: Sync + Send {
fn on_update(&self, state: QrLoginProgress);
}
@@ -260,7 +260,8 @@ pub struct ClientBuilder {
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_refresh_lock_id: Option<String>,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
disable_built_in_root_certificates: bool,
@@ -270,7 +271,7 @@ pub struct ClientBuilder {
request_config: Option<RequestConfig>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
@@ -284,7 +285,8 @@ impl ClientBuilder {
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_refresh_lock_id: None,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
session_delegate: None,
additional_root_certificates: Default::default(),
disable_built_in_root_certificates: false,
@@ -300,14 +302,18 @@ impl ClientBuilder {
})
}
pub fn enable_cross_process_refresh_lock(
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
process_id: String,
session_delegate: Box<dyn ClientSessionDelegate>,
holder_name: String,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_refresh_lock_id = Some(process_id);
builder.session_delegate = Some(session_delegate.into());
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
Arc::new(builder)
}
@@ -472,6 +478,11 @@ impl ClientBuilder {
let builder = unwrap_or_clone_arc(self);
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);
@@ -614,12 +625,8 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
Ok(Arc::new(
Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)
.await?,
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub struct ElementWellKnown {
}
/// Helper function to parse a string into a ElementWellKnown struct
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
}
+48 -14
View File
@@ -6,6 +6,7 @@ use matrix_sdk::{
encryption::{backups, recovery},
};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
@@ -23,22 +24,22 @@ pub struct Encryption {
pub(crate) _client: Arc<Client>,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupStateListener: Sync + Send {
fn on_update(&self, status: BackupState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
fn on_update(&self, status: BackupUploadState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
fn on_update(&self, status: VerificationState);
}
@@ -162,7 +163,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
fn on_update(&self, status: EnableRecoveryProgress);
}
@@ -212,7 +213,7 @@ impl From<encryption::VerificationState> for VerificationState {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
@@ -398,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());
@@ -413,16 +415,40 @@ impl Encryption {
/// Get the E2EE identity of a user.
///
/// Returns Ok(None) if this user does not exist.
/// 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.
///
/// Returns an error if there was a problem contacting the crypto store, or
/// if our client is not logged in.
pub async fn get_user_identity(
/// # 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> {
let identity = self.inner.get_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|i| Arc::new(UserIdentity { inner: i })))
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 })))
}
}
@@ -432,7 +458,7 @@ pub struct UserIdentity {
inner: matrix_sdk::encryption::identities::UserIdentity,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl UserIdentity {
/// Remember this identity, ensuring it does not result in a pin violation.
///
@@ -461,6 +487,14 @@ impl UserIdentity {
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)]
@@ -468,7 +502,7 @@ pub struct IdentityResetHandle {
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl IdentityResetHandle {
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
/// process is using.
+106 -2
View File
@@ -1,13 +1,16 @@
use std::fmt::Display;
use std::{collections::HashMap, fmt, fmt::Display};
use matrix_sdk::{
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use uniffi::UnexpectedUniFFICallbackError;
use crate::room_list::RoomListError;
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
@@ -128,6 +131,12 @@ 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)
@@ -146,6 +155,96 @@ impl From<RoomSendQueueError> for ClientError {
}
}
/// 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 {
@@ -217,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;
+2 -2
View File
@@ -20,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()
@@ -105,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,
+2 -1
View File
@@ -16,6 +16,7 @@ mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_alias;
mod room_directory_search;
mod room_info;
mod room_list;
@@ -44,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 {
+1 -1
View File
@@ -242,7 +242,7 @@ pub struct TracingConfiguration {
write_to_files: Option<TracingFileConfiguration>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn setup_tracing(config: TracingConfiguration) {
log_panics();
+24 -40
View File
@@ -25,8 +25,7 @@ use ruma::{
},
TimelineEventType,
},
EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId,
UserId,
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
@@ -40,16 +39,17 @@ use crate::{
room_info::RoomInfo,
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{FocusEventError, ReceiptType, Timeline},
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
utils::u64_to_uint,
TaskHandle,
};
#[derive(Debug, uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum Membership {
Invited,
Joined,
Left,
Knocked,
}
impl From<RoomState> for Membership {
@@ -58,6 +58,7 @@ impl From<RoomState> for Membership {
RoomState::Invited => Membership::Invited,
RoomState::Joined => Membership::Joined,
RoomState::Left => Membership::Left,
RoomState::Knocked => Membership::Knocked,
}
}
}
@@ -80,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()
@@ -161,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
}
@@ -271,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> {
@@ -627,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))
}
@@ -783,10 +789,8 @@ impl Room {
pub async fn withdraw_verification_and_resend(
&self,
user_ids: Vec<String>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let user_ids: Vec<OwnedUserId> =
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
@@ -798,7 +802,7 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
@@ -816,10 +820,8 @@ impl Room {
pub async fn ignore_device_trust_and_resend(
&self,
devices: HashMap<String, Vec<String>>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let encryption = self.inner.client().encryption();
for (user_id, device_ids) in devices.iter() {
@@ -834,32 +836,14 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
/// Attempt to manually resend messages that failed to send due to issues
/// that should now have been fixed.
///
/// This is useful for example, when there's a
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
/// the user may have re-verified on a different device and would now
/// like to send the failed message that's waiting on this device.
///
/// # Arguments
///
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo that should be unwedged.
pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> {
let transaction_id: &TransactionId = transaction_id.as_str().into();
self.inner.send_queue().unwedge(transaction_id).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> {
@@ -915,17 +899,17 @@ 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>);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IdentityStatusChangeListener: Sync + Send {
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
}
@@ -941,7 +925,7 @@ impl RoomMembersIterator {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomMembersIterator {
fn len(&self) -> u32 {
self.chunk_iterator.len()
@@ -950,7 +934,7 @@ impl RoomMembersIterator {
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
self.chunk_iterator
.next(chunk_size)
.map(|members| members.into_iter().map(|m| m.into()).collect())
.map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect())
}
}
+17
View File
@@ -0,0 +1,17 @@
use matrix_sdk::RoomDisplayName;
/// Verifies the passed `String` matches the expected room alias format:
///
/// This means it's lowercase, with no whitespace chars, has a single leading
/// `#` char and a single `:` separator between the local and domain parts, and
/// the local part only contains characters that can't be percent encoded.
#[matrix_sdk_ffi_macros::export]
fn is_room_alias_format_valid(alias: String) -> bool {
matrix_sdk::utils::is_room_alias_format_valid(alias)
}
/// Transforms a Room's display name into a valid room alias name.
#[matrix_sdk_ffi_macros::export]
fn room_alias_name_from_room_display_name(room_name: String) -> String {
RoomDisplayName::Named(room_name).to_room_alias_name()
}
@@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use ruma::ServerName;
use tokio::sync::RwLock;
use super::RUNTIME;
@@ -68,6 +69,12 @@ impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescriptio
}
}
/// A helper for performing room searches in the room directory.
/// The way this is intended to be used is:
///
/// 1. Register a callback using [`RoomDirectorySearch::results`].
/// 2. Start the room search with [`RoomDirectorySearch::search`].
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
#[derive(uniffi::Object)]
pub struct RoomDirectorySearch {
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
@@ -79,30 +86,51 @@ impl RoomDirectorySearch {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomDirectorySearch {
/// Asks the server for the next page of the current search.
pub async fn next_page(&self) -> Result<(), ClientError> {
let mut inner = self.inner.write().await;
inner.next_page().await?;
Ok(())
}
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
/// Starts a filtered search for the server.
///
/// If the `filter` is not provided it will search for all the rooms.
/// You can specify a `batch_size` to control the number of rooms to fetch
/// per request.
///
/// If the `via_server` is not provided it will search in the current
/// homeserver by default.
///
/// This method will clear the current search results and start a new one.
pub async fn search(
&self,
filter: Option<String>,
batch_size: u32,
via_server_name: Option<String>,
) -> Result<(), ClientError> {
let server = via_server_name.map(ServerName::parse).transpose()?;
let mut inner = self.inner.write().await;
inner.search(filter, batch_size).await?;
inner.search(filter, batch_size, server).await?;
Ok(())
}
/// Get the number of pages that have been loaded so far.
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
let inner = self.inner.read().await;
Ok(inner.loaded_pages() as u32)
}
/// Get whether the search is at the last page.
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
let inner = self.inner.read().await;
Ok(inner.is_at_last_page())
}
/// Registers a callback to receive new search results when starting a
/// search or getting new paginated results.
pub async fn results(
&self,
listener: Box<dyn RoomDirectorySearchEntriesListener>,
@@ -169,7 +197,7 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+6 -2
View File
@@ -67,7 +67,8 @@ 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().iter().map(|id| id.to_string()).collect();
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(),
@@ -90,7 +91,10 @@ impl RoomInfo {
.await
.ok()
.and_then(|details| details.inviter)
.map(Into::into),
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
_ => None,
},
heroes: room.heroes().into_iter().map(Into::into).collect(),
+65 -62
View File
@@ -1,13 +1,12 @@
#![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::UnreadNotificationsCount as RumaUnreadNotificationsCount,
assign, RoomId,
},
sliding_sync::http,
use matrix_sdk::ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
};
use matrix_sdk_ui::{
room_list_service::filters::{
@@ -19,14 +18,17 @@ use matrix_sdk_ui::{
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;
use crate::{
error::ClientError,
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
utils::AsyncRuntimeDropped,
TaskHandle, RUNTIME,
};
@@ -51,7 +53,7 @@ pub enum RoomListError {
#[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: Membership, actual: Membership },
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
}
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
@@ -85,7 +87,7 @@ pub struct 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();
@@ -135,11 +137,7 @@ impl RoomListService {
})))
}
fn subscribe_to_rooms(
&self,
room_ids: Vec<String>,
settings: Option<RoomSubscription>,
) -> Result<(), RoomListError> {
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
let room_ids = room_ids
.into_iter()
.map(|room_id| {
@@ -147,10 +145,7 @@ impl RoomListService {
})
.collect::<Result<Vec<_>, _>>()?;
self.inner.subscribe_to_rooms(
&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
settings.map(Into::into),
);
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
Ok(())
}
@@ -162,7 +157,7 @@ pub struct RoomList {
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomList {
fn loading_state(
&self,
@@ -188,7 +183,6 @@ impl RoomList {
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self.clone();
let client = self.room_list_service.inner.client();
let utd_hook = self.room_list_service.utd_hook.clone();
// The following code deserves a bit of explanation.
@@ -236,10 +230,7 @@ impl RoomList {
// 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(),
client.room_info_notable_update_receiver(),
);
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.
@@ -292,7 +283,7 @@ pub struct RoomListEntriesWithDynamicAdaptersResult {
entries_stream: Arc<TaskHandle>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomListEntriesWithDynamicAdaptersResult {
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
self.controller.clone()
@@ -370,17 +361,17 @@ 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);
}
@@ -443,7 +434,7 @@ impl RoomListEntriesUpdate {
}
}
#[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>);
}
@@ -461,7 +452,7 @@ impl RoomListDynamicEntriesController {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomListDynamicEntriesController {
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
self.inner.set_filter(kind.into())
@@ -549,7 +540,7 @@ impl RoomListItem {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomListItem {
fn id(&self) -> String {
self.inner.id().to_string()
@@ -584,23 +575,60 @@ impl RoomListItem {
}
/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited
/// 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
/// 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: Membership::Invited,
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
@@ -608,7 +636,7 @@ impl RoomListItem {
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: Membership::Joined,
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
@@ -680,38 +708,13 @@ impl RoomListItem {
}
}
#[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: u32,
pub include_heroes: Option<bool>,
}
impl From<RoomSubscription> for http::request::RoomSubscription {
fn from(val: RoomSubscription) -> Self {
assign!(http::request::RoomSubscription::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.into(),
include_heroes: val.include_heroes,
})
}
}
#[derive(uniffi::Object)]
pub struct UnreadNotificationsCount {
highlight_count: u32,
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,
}
}
}
+41 -68
View File
@@ -90,7 +90,7 @@ impl From<AuthData> for ruma::api::client::uiaa::AuthData {
/// 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 {
@@ -154,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,
@@ -190,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,
@@ -262,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;
@@ -273,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.raw_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.raw_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.raw_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.raw_filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::File(event_content)
}
MessageType::Notice { content } => {
@@ -335,9 +356,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -347,9 +365,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -361,9 +376,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -373,9 +385,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -452,15 +461,6 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -471,15 +471,6 @@ pub struct ImageMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -492,15 +483,6 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -511,15 +493,6 @@ pub struct VideoMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -918,7 +891,7 @@ impl From<RumaPollKind> for PollKind {
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
/// [`MessageContent`] value.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn content_without_relation_from_message(
message: MessageContent,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
@@ -1,15 +1,16 @@
use std::sync::{Arc, RwLock};
use anyhow::Context as _;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest},
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
};
use ruma::UserId;
use tracing::{error, info};
use super::RUNTIME;
use crate::error::ClientError;
@@ -20,7 +21,7 @@ pub struct SessionVerificationEmoji {
description: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationEmoji {
pub fn symbol(&self) -> String {
self.symbol.clone()
@@ -37,8 +38,20 @@ pub enum SessionVerificationData {
Decimals { values: Vec<u16> },
}
#[uniffi::export(callback_interface)]
/// Details about the incoming verification request
#[derive(Debug, uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
flow_id: String,
device_id: String,
display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: u64,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: SessionVerificationData);
@@ -58,19 +71,53 @@ pub struct SessionVerificationController {
sas_verification: Arc<RwLock<Option<SasVerification>>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationController {
pub async fn is_verified(&self) -> Result<bool, ClientError> {
let device =
self.encryption.get_own_device().await?.context("Our own device is missing")?;
Ok(device.is_cross_signed_by_owner())
}
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
*self.delegate.write().unwrap() = delegate;
}
/// Set this particular request as the currently active one and register for
/// events pertaining it.
/// * `sender_id` - The user requesting verification.
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
pub async fn acknowledge_verification_request(
&self,
sender_id: String,
flow_id: String,
) -> Result<(), ClientError> {
let sender_id = UserId::parse(sender_id.clone())?;
let verification_request = self
.encryption
.get_verification_request(&sender_id, flow_id)
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Accept the previously acknowledged verification request
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification_request) = verification_request {
let methods = vec![VerificationMethod::SasV1];
verification_request.accept_with_methods(methods).await?;
}
Ok(())
}
/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
@@ -78,30 +125,41 @@ impl SessionVerificationController {
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request);
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Transition the current verification request into a SAS verification
/// flow.
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
match verification.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
match verification_request.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
}
}
@@ -109,31 +167,37 @@ impl SessionVerificationController {
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn approve_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.confirm().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.confirm().await?)
}
/// Reject the short auth string
pub async fn decline_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.mismatch().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.mismatch().await?)
}
/// Cancel the current verification request
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
verification.cancel().await?;
}
Ok(())
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
Ok(verification_request.cancel().await?)
}
}
@@ -149,58 +213,88 @@ impl SessionVerificationController {
}
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
match event {
// TODO: Use the changes stream for this as well once we expose
// VerificationRequest::changes() in the main crate.
AnyToDeviceEvent::KeyVerificationStart(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(verification) = self
.encryption
.get_verification(
self.user_identity.user_id(),
event.content.transaction_id.as_str(),
)
.await
{
if let Some(sas_verification) = verification.sas() {
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);
if sas_verification.accept().await.is_ok() {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let Some(request) = self
.encryption
.get_verification_request(&event.sender, &event.content.transaction_id)
.await
else {
error!("Failed retrieving verification request");
return;
};
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(),
});
}
}
}
async fn listen_to_verification_request_changes(
verification_request: VerificationRequest,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
delegate: Delegate,
) {
let mut stream = verification_request.changes();
while let Some(state) = stream.next().await {
match state {
VerificationRequestState::Transitioned { verification } => {
let Some(verification) = verification.sas() else {
error!("Invalid, non-sas verification flow. Returning.");
return;
};
*sas_verification.write().unwrap() = Some(verification.clone());
if verification.accept().await.is_ok() {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let delegate = delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
verification,
delegate,
));
} else if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_fail()
}
}
}
AnyToDeviceEvent::KeyVerificationReady(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
VerificationRequestState::Ready { .. } => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_accept_verification_request()
}
}
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_accept_verification_request()
VerificationRequestState::Cancelled(..) => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_cancel();
}
}
_ => {}
}
_ => (),
}
}
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
match &*self.verification_request.read().unwrap() {
Some(verification) => verification.flow_id() == transaction_id,
None => false,
}
}
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
let mut stream = sas.changes();
while let Some(state) = stream.next().await {
+6 -6
View File
@@ -51,7 +51,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
fn on_update(&self, state: SyncServiceState);
}
@@ -62,7 +62,7 @@ pub struct SyncService {
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncService {
pub fn room_list_service(&self) -> Arc<RoomListService> {
Arc::new(RoomListService {
@@ -110,11 +110,11 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock(app_identifier);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
@@ -153,7 +153,7 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
+1 -1
View File
@@ -17,7 +17,7 @@ impl TaskHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TaskHandle {
// Cancel a task handle.
pub fn cancel(&self) {
@@ -16,7 +16,7 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::room::MediaSource;
use ruma::events::{room::MediaSource, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
@@ -49,11 +49,18 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
}
Content::MembershipChange(membership) => TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
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
@@ -161,6 +168,7 @@ pub enum TimelineItemContent {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
@@ -195,7 +203,7 @@ impl InReplyToDetails {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
+286 -241
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
@@ -31,7 +31,8 @@ use matrix_sdk::{
Error,
};
use matrix_sdk_ui::timeline::{
EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
use ruma::{
@@ -52,7 +53,7 @@ use ruma::{
},
AnyMessageLikeEventContent,
},
EventId, OwnedTransactionId,
EventId,
};
use tokio::{
sync::Mutex,
@@ -80,6 +81,9 @@ use crate::{
mod content;
pub use content::MessageContent;
use matrix_sdk::utils::formatted_body_from;
use crate::error::QueueWedgeError;
#[derive(uniffi::Object)]
#[repr(transparent)]
@@ -97,37 +101,24 @@ impl Timeline {
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
}
fn build_thumbnail_info(
&self,
thumbnail_url: String,
thumbnail_info: ThumbnailInfo,
) -> Result<Thumbnail, RoomError> {
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
Ok(Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
})
}
async fn send_attachment(
&self,
filename: String,
mime_type: Mime,
mime_type: Option<String>,
attachment_config: AttachmentConfig,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Result<(), RoomError> {
let request = self.inner.send_attachment(filename, mime_type, attachment_config);
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let mut request = self.inner.send_attachment(filename, mime_type, attachment_config);
if use_send_queue {
request = request.use_send_queue();
}
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
@@ -142,7 +133,42 @@ impl Timeline {
}
}
#[uniffi::export(async_runtime = "tokio")]
fn build_thumbnail_info(
thumbnail_url: Option<String>,
thumbnail_info: Option<ThumbnailInfo>,
) -> Result<AttachmentConfig, RoomError> {
match (thumbnail_url, thumbnail_info) {
(None, None) => Ok(AttachmentConfig::new()),
(Some(thumbnail_url), Some(thumbnail_info)) => {
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let thumbnail = Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
};
Ok(AttachmentConfig::with_thumbnail(thumbnail))
}
_ => {
warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined");
Ok(AttachmentConfig::new())
}
}
}
#[matrix_sdk_ffi_macros::export]
impl Timeline {
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
@@ -245,7 +271,7 @@ impl Timeline {
msg: Arc<RoomMessageEventContentWithoutRelation>,
) -> Result<Arc<SendHandle>, ClientError> {
match self.inner.send((*msg).to_owned().with_relation(None).into()).await {
Ok(handle) => Ok(Arc::new(SendHandle { inner: Mutex::new(Some(handle)) })),
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
Err(err) => {
error!("error when sending a message: {err}");
Err(anyhow::anyhow!(err).into())
@@ -253,6 +279,7 @@ impl Timeline {
}
}
#[allow(clippy::too_many_arguments)]
pub fn send_image(
self: Arc<Self>,
url: String,
@@ -261,33 +288,32 @@ impl Timeline {
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_image_info = BaseImageInfo::try_from(&image_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Image(base_image_info);
let attachment_config = match (thumbnail_url, image_info.thumbnail_info) {
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
}
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
self.send_attachment(
url,
image_info.mimetype,
attachment_config,
progress_watcher,
use_send_queue,
)
.await
}))
}
#[allow(clippy::too_many_arguments)]
pub fn send_video(
self: Arc<Self>,
url: String,
@@ -296,30 +322,28 @@ impl Timeline {
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Video(base_video_info);
let attachment_config = match (thumbnail_url, video_info.thumbnail_info) {
(Some(thumbnail_url), Some(thumbnail_image_info)) => {
let thumbnail =
self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
}
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
let attachment_config = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
self.send_attachment(
url,
video_info.mimetype,
attachment_config,
progress_watcher,
use_send_queue,
)
.await
}))
}
@@ -330,26 +354,32 @@ impl Timeline {
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Audio(base_audio_info);
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
self.send_attachment(
url,
audio_info.mimetype,
attachment_config,
progress_watcher,
use_send_queue,
)
.await
}))
}
#[allow(clippy::too_many_arguments)]
pub fn send_voice_message(
self: Arc<Self>,
url: String,
@@ -358,24 +388,29 @@ impl Timeline {
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info =
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
self.send_attachment(
url,
audio_info.mimetype,
attachment_config,
progress_watcher,
use_send_queue,
)
.await
}))
}
@@ -383,21 +418,31 @@ impl Timeline {
self: Arc<Self>,
url: String,
file_info: FileInfo,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let mime_str =
file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let base_file_info: BaseFileInfo =
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::File(base_file_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(
url,
file_info.mimetype,
attachment_config,
progress_watcher,
use_send_queue,
)
.await
}))
}
@@ -493,11 +538,30 @@ impl Timeline {
&self,
event_or_transaction_id: EventOrTransactionId,
new_content: EditedContent,
) -> Result<bool, ClientError> {
self.inner
.edit_by_id(&(event_or_transaction_id.try_into()?), new_content.try_into()?)
) -> Result<(), ClientError> {
match self
.inner
.edit(&event_or_transaction_id.clone().try_into()?, new_content.clone().try_into()?)
.await
.map_err(Into::into)
{
Ok(()) => Ok(()),
Err(timeline::Error::EventNotInTimeline(_)) => {
// If we couldn't edit, assume it was an (remote) event that wasn't in the
// timeline, and try to edit it via the room itself.
let event_id = match event_or_transaction_id {
EventOrTransactionId::EventId { event_id } => EventId::parse(event_id)?,
EventOrTransactionId::TransactionId { .. } => {
warn!("trying to apply an edit to a local echo that doesn't exist in this timeline, aborting");
return Ok(());
}
};
let room = self.inner.room();
let edit_event = room.make_edit_event(&event_id, new_content.try_into()?).await?;
room.send_queue().send(edit_event).await?;
Ok(())
}
Err(err) => Err(err)?,
}
}
pub async fn send_location(
@@ -530,19 +594,21 @@ impl Timeline {
/// Toggle a reaction on an event.
///
/// The `unique_id` parameter is a string returned by
/// the `TimelineItem::unique_id()` method. As such, this method works both
/// on local echoes and remote items.
///
/// Adds or redacts a reaction based on the state of the reaction at the
/// time it is called.
///
/// This method works both on local echoes and remote items.
///
/// When redacting a previous reaction, the redaction reason is not set.
///
/// Ensures that only one reaction is sent at a time to avoid race
/// conditions and spamming the homeserver with requests.
pub async fn toggle_reaction(&self, unique_id: String, key: String) -> Result<(), ClientError> {
self.inner.toggle_reaction(&unique_id, &key).await?;
pub async fn toggle_reaction(
&self,
item_id: EventOrTransactionId,
key: String,
) -> Result<(), ClientError> {
self.inner.toggle_reaction(&item_id.try_into()?, &key).await?;
Ok(())
}
@@ -573,26 +639,6 @@ impl Timeline {
Ok(item.into())
}
/// Get the current timeline item for the given transaction ID, if any.
///
/// This will always return a local echo, if found.
///
/// It's preferable to store the timeline items in the model for your UI, if
/// possible, instead of just storing IDs and coming back to the timeline
/// object to look up items.
pub async fn get_event_timeline_item_by_transaction_id(
&self,
transaction_id: String,
) -> Result<EventTimelineItem, ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let item = self
.inner
.local_item_by_transaction_id(&transaction_id)
.await
.context("Item with given transaction ID not found")?;
Ok(item.into())
}
/// Redacts an event from the timeline.
///
/// Only works for events that exist as timeline items.
@@ -607,10 +653,7 @@ impl Timeline {
event_or_transaction_id: EventOrTransactionId,
reason: Option<String>,
) -> Result<(), ClientError> {
self.inner
.redact_by_id(&(event_or_transaction_id.try_into()?), reason.as_deref())
.await
.map_err(Into::into)
Ok(self.inner.redact(&(event_or_transaction_id.try_into()?), reason.as_deref()).await?)
}
/// Load the reply details for the given event id.
@@ -683,12 +726,19 @@ impl Timeline {
}
}
/// A handle to perform actions onto a local echo.
#[derive(uniffi::Object)]
pub struct SendHandle {
inner: Mutex<Option<matrix_sdk::send_queue::SendHandle>>,
}
#[uniffi::export(async_runtime = "tokio")]
impl SendHandle {
fn new(handle: matrix_sdk::send_queue::SendHandle) -> Self {
Self { inner: Mutex::new(Some(handle)) }
}
}
#[matrix_sdk_ffi_macros::export]
impl SendHandle {
/// Try to abort the sending of the current event.
///
@@ -705,10 +755,32 @@ impl SendHandle {
.await
.map_err(|err| anyhow::anyhow!("error when saving in store: {err}"))?)
} else {
warn!("trying to abort an send handle that's already been actioned");
warn!("trying to abort a send handle that's already been actioned");
Ok(false)
}
}
/// Attempt to manually resend messages that failed to send due to issues
/// that should now have been fixed.
///
/// This is useful for example, when there's a
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
/// the user may have re-verified on a different device and would now
/// like to send the failed message that's waiting on this device.
///
/// # Arguments
///
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo that should be unwedged.
pub async fn try_resend(self: Arc<Self>) -> Result<(), ClientError> {
let locked = self.inner.lock().await;
if let Some(handle) = locked.as_ref() {
handle.unwedge().await?;
} else {
warn!("trying to unwedge a send handle that's been aborted");
}
Ok(())
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -723,12 +795,12 @@ pub enum FocusEventError {
Other { msg: String },
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TimelineListener: Sync + Send {
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait PaginationStatusListener: Sync + Send {
fn on_update(&self, status: LiveBackPaginationStatus);
}
@@ -778,7 +850,7 @@ impl TimelineDiff {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineDiff {
pub fn change(&self) -> TimelineChange {
match self {
@@ -866,6 +938,23 @@ pub enum TimelineChange {
Reset,
}
#[derive(Clone, uniffi::Record)]
pub struct TimelineUniqueId {
id: String,
}
impl From<&SdkTimelineUniqueId> for TimelineUniqueId {
fn from(value: &SdkTimelineUniqueId) -> Self {
Self { id: value.0.clone() }
}
}
impl From<&TimelineUniqueId> for SdkTimelineUniqueId {
fn from(value: &TimelineUniqueId) -> Self {
Self(value.id.clone())
}
}
#[repr(transparent)]
#[derive(Clone, uniffi::Object)]
pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
@@ -878,7 +967,7 @@ impl TimelineItem {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineItem {
pub fn as_event(self: Arc<Self>) -> Option<EventTimelineItem> {
let event_item = self.0.as_event()?;
@@ -893,8 +982,9 @@ impl TimelineItem {
}
}
pub fn unique_id(&self) -> String {
self.0.unique_id().to_owned()
/// An opaque unique identifier for this timeline item.
pub fn unique_id(&self) -> TimelineUniqueId {
self.0.unique_id().into()
}
pub fn fmt_debug(&self) -> String {
@@ -908,42 +998,12 @@ pub enum EventSendState {
/// The local event has not been sent yet.
NotSentYet,
/// One or more verified users in the room has an unsigned device.
///
/// Happens only when the room key recipient strategy (as set by
/// [`ClientBuilder::room_key_recipient_strategy`]) has
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem) set.
VerifiedUserHasUnsignedDevice {
/// The unsigned devices belonging to verified users. A map from user ID
/// to a list of device IDs.
devices: HashMap<String, Vec<String>>,
},
/// One or more verified users in the room has changed identity since they
/// were verified.
///
/// Happens only when the room key recipient strategy (as set by
/// [`ClientBuilder::room_key_recipient_strategy`]) has
/// [`error_on_verified_user_problem`](CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem)
/// set, or when using [`CollectStrategy::IdentityBasedStrategy`].
VerifiedUserChangedIdentity {
/// The users that were previously verified, but are no longer
users: Vec<String>,
},
/// The user does not have cross-signing set up, but
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
CrossSigningNotSetup,
/// The current device is not verified, but
/// [`CollectStrategy::IdentityBasedStrategy`] was used.
SendingFromUnverifiedDevice,
/// The local event has been sent to the server, but unsuccessfully: The
/// sending has failed.
SendingFailed {
/// Stringified error message.
error: String,
/// The error reason, with information for the user.
error: QueueWedgeError,
/// Whether the error is considered recoverable or not.
///
/// An error that's recoverable will disable the room's send queue,
@@ -951,6 +1011,7 @@ pub enum EventSendState {
/// decides to cancel sending it.
is_recoverable: bool,
},
/// The local event has been sent successfully to the server.
Sent { event_id: String },
}
@@ -962,46 +1023,17 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
match value {
NotSentYet => Self::NotSentYet,
SendingFailed { error, is_recoverable } => {
event_send_state_from_sending_failed(error, *is_recoverable)
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
Self::SendingFailed {
is_recoverable: *is_recoverable,
error: as_queue_wedge_error.into(),
}
}
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
}
}
}
fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> EventSendState {
use matrix_sdk::crypto::{OlmError, SessionRecipientCollectionError::*};
match error {
// Special-case the SessionRecipientCollectionErrors, to pass the information they contain
// back to the application.
Error::OlmError(OlmError::SessionRecipientCollectionError(error)) => match error {
VerifiedUserHasUnsignedDevice(devices) => {
let devices = devices
.iter()
.map(|(user_id, devices)| {
(
user_id.to_string(),
devices.iter().map(|device_id| device_id.to_string()).collect(),
)
})
.collect();
EventSendState::VerifiedUserHasUnsignedDevice { devices }
}
VerifiedUserChangedIdentity(bad_users) => EventSendState::VerifiedUserChangedIdentity {
users: bad_users.iter().map(|user_id| user_id.to_string()).collect(),
},
CrossSigningNotSetup => EventSendState::CrossSigningNotSetup,
SendingFromUnverifiedDevice => EventSendState::SendingFromUnverifiedDevice,
},
_ => EventSendState::SendingFailed { error: error.to_string(), is_recoverable },
}
}
/// Recommended decorations for decrypted messages, representing the message's
/// authenticity properties.
#[derive(uniffi::Enum, Clone)]
@@ -1032,7 +1064,7 @@ impl From<SdkShieldState> for ShieldState {
#[derive(Clone, uniffi::Record)]
pub struct EventTimelineItem {
is_local: bool,
/// Indicates that an event is remote.
is_remote: bool,
event_or_transaction_id: EventOrTransactionId,
sender: String,
@@ -1042,17 +1074,16 @@ pub struct EventTimelineItem {
content: TimelineItemContent,
timestamp: u64,
reactions: Vec<Reaction>,
debug_info_provider: Arc<EventTimelineItemDebugInfoProvider>,
local_send_state: Option<EventSendState>,
read_receipts: HashMap<String, Receipt>,
origin: Option<EventItemOrigin>,
can_be_replied_to: bool,
shields_provider: Arc<EventShieldsProvider>,
lazy_provider: Arc<LazyTimelineItemProvider>,
}
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
let reactions = value
fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
let reactions = item
.reactions()
.iter()
.map(|(k, v)| Reaction {
@@ -1066,28 +1097,25 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
.collect(),
})
.collect();
let value = Arc::new(value);
let debug_info_provider = Arc::new(EventTimelineItemDebugInfoProvider(value.clone()));
let shields_provider = Arc::new(EventShieldsProvider(value.clone()));
let item = Arc::new(item);
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
let read_receipts =
value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
item.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect();
Self {
is_local: value.is_local_echo(),
is_remote: !value.is_local_echo(),
event_or_transaction_id: value.identifier().into(),
sender: value.sender().to_string(),
sender_profile: value.sender_profile().into(),
is_own: value.is_own(),
is_editable: value.is_editable(),
content: value.content().clone().into(),
timestamp: value.timestamp().0.into(),
is_remote: !item.is_local_echo(),
event_or_transaction_id: item.identifier().into(),
sender: item.sender().to_string(),
sender_profile: item.sender_profile().into(),
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
timestamp: item.timestamp().0.into(),
reactions,
debug_info_provider,
local_send_state: value.send_state().map(|s| s.into()),
local_send_state: item.send_state().map(|s| s.into()),
read_receipts,
origin: value.origin(),
can_be_replied_to: value.can_be_replied_to(),
shields_provider,
origin: item.origin(),
can_be_replied_to: item.can_be_replied_to(),
lazy_provider,
}
}
}
@@ -1103,22 +1131,6 @@ impl From<ruma::events::receipt::Receipt> for Receipt {
}
}
/// Wrapper to retrieve the debug info lazily instead of immediately
/// transforming it for each timeline event.
#[derive(uniffi::Object)]
pub struct EventTimelineItemDebugInfoProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
#[uniffi::export]
impl EventTimelineItemDebugInfoProvider {
fn get(&self) -> EventTimelineItemDebugInfo {
EventTimelineItemDebugInfo {
model: format!("{:#?}", self.0),
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct EventTimelineItemDebugInfo {
model: String,
@@ -1202,13 +1214,30 @@ impl SendAttachmentJoinHandle {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SendAttachmentJoinHandle {
/// Wait until the attachment has been sent.
///
/// If the sending had been cancelled, will return immediately.
pub async fn join(&self) -> Result<(), RoomError> {
let join_hdl = self.join_hdl.clone();
RUNTIME.spawn(async move { (&mut *join_hdl.lock().await).await.unwrap() }).await.unwrap()
let handle = self.join_hdl.clone();
let mut locked_handle = handle.lock().await;
let join_result = (&mut *locked_handle).await;
match join_result {
Ok(res) => res,
Err(err) => {
if err.is_cancelled() {
return Ok(());
}
error!("task panicked! resuming panic from here.");
panic::resume_unwind(err.into_panic());
}
}
}
/// Cancel the current sending task.
///
/// A subsequent call to [`Self::join`] will return immediately.
pub fn cancel(&self) {
self.abort_hdl.abort();
}
@@ -1270,13 +1299,29 @@ impl TryFrom<EditedContent> for SdkEditedContent {
}
}
/// Wrapper to retrieve the shields info lazily.
/// Wrapper to retrieve some timeline item info lazily.
#[derive(Clone, uniffi::Object)]
pub struct EventShieldsProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
#[uniffi::export]
impl EventShieldsProvider {
#[matrix_sdk_ffi_macros::export]
impl LazyTimelineItemProvider {
/// Returns the shields for this event timeline item.
fn get_shields(&self, strict: bool) -> Option<ShieldState> {
self.0.get_shield(strict).map(Into::into)
}
/// Returns some debug information for this event timeline item.
fn debug_info(&self) -> EventTimelineItemDebugInfo {
EventTimelineItemDebugInfo {
model: format!("{:#?}", self.0),
original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()),
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
}
}
/// For local echoes, return the associated send handle; returns `None` for
/// remote echoes.
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
}
}
@@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
+2 -2
View File
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// level + target) it is called with. Please make sure that the number of
/// different combinations of those parameters this can be called with is
/// constant in the final executable.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
Mutex::new(BTreeMap::new());
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
#[derive(uniffi::Object)]
pub struct Span(tracing::Span);
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Span {
/// Create a span originating at the given callsite (file, line and column).
///
+45
View File
@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::TOKIO1 as RUNTIME;
use ruma::UInt;
use tracing::warn;
@@ -21,3 +24,45 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt {
UInt::MAX
})
}
/// Tiny wrappers for data types that must be dropped in the context of an async
/// runtime.
///
/// This is useful whenever such a data type may transitively call some
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
/// at the moment), like done in some `deadpool` drop impls.
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
impl<T> AsyncRuntimeDropped<T> {
/// Create a new wrapper for this type that will be dropped under an async
/// runtime.
pub fn new(val: T) -> Self {
Self(ManuallyDrop::new(val))
}
}
impl<T> Drop for AsyncRuntimeDropped<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.0);
}
}
}
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
impl<T> Deref for AsyncRuntimeDropped<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
+42 -27
View File
@@ -5,6 +5,7 @@ use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter},
};
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::{room::Room, RUNTIME};
@@ -15,7 +16,7 @@ pub struct WidgetDriverAndHandle {
pub handle: Arc<WidgetDriverHandle>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
Ok(WidgetDriverAndHandle {
@@ -29,7 +30,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
#[derive(uniffi::Object)]
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl WidgetDriver {
pub async fn run(
&self,
@@ -96,7 +97,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
/// * `room` - A matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to adapt
/// to the client. e.g. language, font-scale...
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
pub async fn generate_webview_url(
widget_settings: WidgetSettings,
room: Arc<Room>,
@@ -241,7 +242,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
///
/// * `props` - A struct containing the configuration parameters for a element
/// call widget.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn new_virtual_element_call_widget(
props: VirtualElementCallWidgetOptions,
) -> Result<WidgetSettings, ParseError> {
@@ -261,13 +262,38 @@ 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]
#[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![
// To compute the current state of the matrixRTC session.
@@ -278,19 +304,13 @@ pub fn get_element_call_required_permissions(
WidgetEventFilter::StateWithType {
event_type: StateEventType::RoomEncryption.to_string(),
},
// To read rageshake requests from other room members
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read encryption keys
// TODO change this to the appropriate to-device version once ready
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
@@ -313,15 +333,10 @@ pub fn get_element_call_required_permissions(
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}"),
},
// To request other room members to send rageshakes
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To send this user's encryption keys
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
]
.into_iter()
.chain(read_send)
.collect(),
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
@@ -354,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.
///
@@ -417,7 +432,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
}
/// 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 },
@@ -469,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;
}
+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"
+71 -11
View File
@@ -1,21 +1,81 @@
# unreleased
# Changelog
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Bug Fixes
- Add more invalid characters for room aliases.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- 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
- `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`.
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- `Client::get_stripped_rooms` has finally been removed.
- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail
- They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`.
- The `StateStore` methods to access data in the media cache where moved to a separate
`EventCacheStore` trait.
- The `instant` module was removed, use the `ruma::time` module instead.
- [**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
+5 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.8.0"
[package.metadata.docs.rs]
all-features = true
@@ -50,6 +50,7 @@ assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.4.0", features = ["serde"] }
decancer = "3.2.4"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
@@ -60,7 +61,9 @@ matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
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 }
@@ -76,6 +79,7 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
similar-asserts = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+244 -98
View File
@@ -14,22 +14,21 @@
// limitations under the License.
#[cfg(feature = "e2e-encryption")]
use std::ops::Deref;
use std::sync::Arc;
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt, iter,
sync::Arc,
ops::Deref,
};
use eyeball::{SharedObservable, Subscriber};
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::{Vector, VectorDiff};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::Stream;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
store::DynCryptoStore, CollectStrategy, DecryptionSettings, EncryptionSettings,
EncryptionSyncChanges, OlmError, OlmMachine, ToDeviceRequest, TrustRequirement,
EncryptionSyncChanges, OlmError, OlmMachine, RoomEventDecryptionResult, ToDeviceRequest,
TrustRequirement,
};
#[cfg(feature = "e2e-encryption")]
use ruma::events::{
@@ -42,6 +41,7 @@ use ruma::{
api::client as api,
events::{
ignored_user_list::IgnoredUserListEvent,
marked_unread::MarkedUnreadEventContent,
push_rules::{PushRulesEvent, PushRulesEventContent},
room::{
member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent},
@@ -68,9 +68,9 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
error::{Error, Result},
event_cache_store::DynEventCacheStore,
event_cache::store::EventCacheStoreLock,
response_processors::AccountDataProcessor,
rooms::{
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons},
@@ -92,25 +92,28 @@ use crate::{
pub struct BaseClient {
/// Database
pub(crate) store: Store,
/// The store used by the event cache.
event_cache_store: Arc<DynEventCacheStore>,
event_cache_store: EventCacheStoreLock,
/// The store used for encryption.
///
/// This field is only meant to be used for `OlmMachine` initialization.
/// All operations on it happen inside the `OlmMachine`.
#[cfg(feature = "e2e-encryption")]
crypto_store: Arc<DynCryptoStore>,
/// The olm-machine that is created once the
/// [`SessionMeta`][crate::session::SessionMeta] is set via
/// [`BaseClient::set_session_meta`]
#[cfg(feature = "e2e-encryption")]
olm_machine: Arc<RwLock<Option<OlmMachine>>>,
/// Observable of when a user is ignored/unignored.
pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
/// A sender that is used to communicate changes to room information. Each
/// event contains the room and a boolean whether this event should
/// trigger a room list update.
/// tick contains the room ID and the reasons that have generated this tick.
pub(crate) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
/// The strategy to use for picking recipient devices, when sending an
@@ -134,11 +137,6 @@ impl fmt::Debug for BaseClient {
}
impl BaseClient {
/// Create a new default client.
pub fn new() -> Self {
BaseClient::with_store_config(StoreConfig::default())
}
/// Create a new client.
///
/// # Arguments
@@ -168,8 +166,12 @@ impl BaseClient {
/// Clones the current base client to use the same crypto store but a
/// different, in-memory store config, and resets transient state.
#[cfg(feature = "e2e-encryption")]
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
let config = StoreConfig::new().state_store(MemoryStore::new());
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder_name: &str,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
.state_store(MemoryStore::new());
let config = config.crypto_store(self.crypto_store.clone());
let copy = Self {
@@ -202,8 +204,12 @@ impl BaseClient {
/// different, in-memory store config, and resets transient state.
#[cfg(not(feature = "e2e-encryption"))]
#[allow(clippy::unused_async)]
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
let config = StoreConfig::new().state_store(MemoryStore::new());
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder: &str,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
.state_store(MemoryStore::new());
Ok(Self::with_store_config(config))
}
@@ -228,7 +234,6 @@ impl BaseClient {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
#[cfg(not(target_arch = "wasm32"))]
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
self.store.rooms_stream()
}
@@ -244,14 +249,13 @@ impl BaseClient {
}
/// Get a reference to the store.
#[allow(unknown_lints, clippy::explicit_auto_deref)]
pub fn store(&self) -> &DynStateStore {
&*self.store
self.store.deref()
}
/// Get a reference to the event cache store.
pub fn event_cache_store(&self) -> &DynEventCacheStore {
&*self.event_cache_store
pub fn event_cache_store(&self) -> &EventCacheStoreLock {
&self.event_cache_store
}
/// Is the client logged in.
@@ -343,6 +347,13 @@ impl BaseClient {
Ok(())
}
/// Attempt to decrypt the given raw event into a `SyncTimelineEvent`.
///
/// In the case of a decryption error, returns a `SyncTimelineEvent`
/// representing the decryption error; in the case of problems with our
/// application, returns `Err`.
///
/// Returns `Ok(None)` if encryption is not configured.
#[cfg(feature = "e2e-encryption")]
async fn decrypt_sync_room_event(
&self,
@@ -355,24 +366,37 @@ impl BaseClient {
let decryption_settings = DecryptionSettings {
sender_device_trust_requirement: self.decryption_trust_requirement,
};
let event: SyncTimelineEvent =
olm.decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await?.into();
if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.event.deserialize() {
match &e {
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
original_event,
)) => {
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
self.handle_verification_event(&e, room_id).await?;
let event = match olm
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
let event: SyncTimelineEvent = decrypted.into();
if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() {
match &e {
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
original_event,
)) => {
if let MessageType::VerificationRequest(_) =
&original_event.content.msgtype
{
self.handle_verification_event(&e, room_id).await?;
}
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
self.handle_verification_event(&e, room_id).await?;
}
_ => (),
}
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
self.handle_verification_event(&e, room_id).await?;
}
_ => (),
event
}
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
SyncTimelineEvent::new_utd_event(event.clone(), utd_info)
}
};
Ok(Some(event))
}
@@ -384,6 +408,7 @@ impl BaseClient {
room: &Room,
limited: bool,
events: Vec<Raw<AnySyncTimelineEvent>>,
ignore_state_events: bool,
prev_batch: Option<String>,
push_rules: &Ruleset,
user_ids: &mut BTreeSet<OwnedUserId>,
@@ -395,14 +420,16 @@ impl BaseClient {
let mut timeline = Timeline::new(limited, prev_batch);
let mut push_context = self.get_push_room_context(room, room_info, changes).await?;
for event in events {
let mut event: SyncTimelineEvent = event.into();
for raw_event in events {
// Start by assuming we have a plaintext event. We'll replace it with a
// decrypted or UTD event below if necessary.
let mut event = SyncTimelineEvent::new(raw_event);
match event.event.deserialize() {
match event.raw().deserialize() {
Ok(e) => {
#[allow(clippy::single_match)]
match &e {
AnySyncTimelineEvent::State(s) => {
AnySyncTimelineEvent::State(s) if !ignore_state_events => {
match s {
AnySyncStateEvent::RoomMember(member) => {
Box::pin(ambiguity_cache.handle_event(
@@ -432,10 +459,12 @@ impl BaseClient {
}
}
let raw_event: Raw<AnySyncStateEvent> = event.event.clone().cast();
let raw_event: Raw<AnySyncStateEvent> = event.raw().clone().cast();
changes.add_state_event(room.room_id(), s.clone(), raw_event);
}
AnySyncTimelineEvent::State(_) => { /* do nothing */ }
AnySyncTimelineEvent::MessageLike(
AnySyncMessageLikeEvent::RoomRedaction(r),
) => {
@@ -443,8 +472,8 @@ impl BaseClient {
room_info.room_version().unwrap_or(&RoomVersionId::V1);
if let Some(redacts) = r.redacts(room_version) {
room_info.handle_redaction(r, event.event.cast_ref());
let raw_event = event.event.clone().cast();
room_info.handle_redaction(r, event.raw().cast_ref());
let raw_event = event.raw().clone().cast();
changes.add_redaction(room.room_id(), redacts, raw_event);
}
@@ -455,10 +484,10 @@ impl BaseClient {
AnySyncMessageLikeEvent::RoomEncrypted(
SyncMessageLikeEvent::Original(_),
) => {
if let Ok(Some(e)) = Box::pin(
self.decrypt_sync_room_event(&event.event, room.room_id()),
if let Some(e) = Box::pin(
self.decrypt_sync_room_event(event.raw(), room.room_id()),
)
.await
.await?
{
event = e;
}
@@ -494,14 +523,14 @@ impl BaseClient {
}
if let Some(context) = &push_context {
let actions = push_rules.get_actions(&event.event, context);
let actions = push_rules.get_actions(event.raw(), context);
if actions.iter().any(Action::should_notify) {
notifications.entry(room.room_id().to_owned()).or_default().push(
Notification {
actions: actions.to_owned(),
event: RawAnySyncOrStrippedTimelineEvent::Sync(
event.event.clone(),
event.raw().clone(),
),
},
);
@@ -520,11 +549,22 @@ impl BaseClient {
Ok(timeline)
}
/// Handles the stripped state events in `invite_state`, modifying the
/// room's info and posting notifications as needed.
///
/// * `room` - The [`Room`] to modify.
/// * `events` - The contents of `invite_state` in the form of list of pairs
/// of raw stripped state events with their deserialized counterpart.
/// * `push_rules` - The push rules for this room.
/// * `room_info` - The current room's info.
/// * `changes` - The accumulated list of changes to apply once the
/// processing is finished.
/// * `notifications` - Notifications to post for the current room.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub(crate) async fn handle_invited_state(
&self,
room: &Room,
events: &[Raw<AnyStrippedStateEvent>],
events: &[(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)],
push_rules: &Ruleset,
room_info: &mut RoomInfo,
changes: &mut StateChanges,
@@ -532,22 +572,12 @@ impl BaseClient {
) -> Result<()> {
let mut state_events = BTreeMap::new();
for raw_event in events {
match raw_event.deserialize() {
Ok(e) => {
room_info.handle_stripped_state_event(&e);
state_events
.entry(e.event_type())
.or_insert_with(BTreeMap::new)
.insert(e.state_key().to_owned(), raw_event.clone());
}
Err(err) => {
warn!(
room_id = ?room_info.room_id,
"Couldn't deserialize stripped state event: {err:?}",
);
}
}
for (raw_event, event) in events {
room_info.handle_stripped_state_event(event);
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
}
changes.stripped_state.insert(room_info.room_id().to_owned(), state_events.clone());
@@ -656,6 +686,25 @@ impl BaseClient {
}
}
// Helper to update the unread marker for stable and unstable prefixes.
fn on_unread_marker(
room_id: &RoomId,
content: &MarkedUnreadEventContent,
room_info: &mut RoomInfo,
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
) {
if room_info.base_info.is_marked_unread != content.unread {
// Notify the room list about a manual read marker change if the
// value's changed.
room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
}
room_info.base_info.is_marked_unread = content.unread;
}
// Handle new events.
for raw_event in events {
match raw_event.deserialize() {
@@ -665,19 +714,24 @@ impl BaseClient {
match event {
AnyRoomAccountDataEvent::MarkedUnread(event) => {
on_room_info(room_id, changes, self, |room_info| {
if room_info.base_info.is_marked_unread != event.content.unread {
// Notify the room list about a manual read marker change if the
// value's changed.
room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
}
room_info.base_info.is_marked_unread = event.content.unread;
on_unread_marker(
room_id,
&event.content,
room_info,
room_info_notable_updates,
);
});
}
AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => {
on_room_info(room_id, changes, self, |room_info| {
on_unread_marker(
room_id,
&event.content.0,
room_info,
room_info_notable_updates,
);
});
}
AnyRoomAccountDataEvent::Tag(event) => {
on_room_info(room_id, changes, self, |room_info| {
room_info.base_info.handle_notable_tags(&event.content.tags);
@@ -762,6 +816,8 @@ impl BaseClient {
room: &Room,
) -> Option<(Box<LatestEvent>, usize)> {
let enc_events = room.latest_encrypted_events();
let power_levels = room.power_levels().await.ok();
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
// Walk backwards through the encrypted events, looking for one we can decrypt
for (i, event) in enc_events.iter().enumerate().rev() {
@@ -773,15 +829,15 @@ impl BaseClient {
if let Ok(Some(decrypted)) = decrypt_sync_room_event.await {
// We found an event we can decrypt
if let Ok(any_sync_event) = decrypted.event.deserialize() {
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
// We can deserialize it to find its type
match is_suitable_for_latest_event(&any_sync_event) {
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesCallNotify(_)
| PossibleLatestEvent::YesSticker(_) => {
// The event is the right type for us to use as latest_event
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
return Some((Box::new(LatestEvent::new(decrypted)), i));
}
_ => (),
@@ -792,6 +848,32 @@ impl BaseClient {
None
}
/// User has knocked on a room.
///
/// Update the internal and cached state accordingly. Return the final Room.
pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
let room = self.store.get_or_create_room(
room_id,
RoomState::Knocked,
self.room_info_notable_update_sender.clone(),
);
if room.state() != RoomState::Knocked {
let _sync_lock = self.sync_lock().lock().await;
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.store.save_changes(&changes).await?; // Update the store
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
Ok(room)
}
/// User has joined a room.
///
/// Update the internal and cached state accordingly. Return the final Room.
@@ -955,6 +1037,7 @@ impl BaseClient {
&room,
new_info.timeline.limited,
new_info.timeline.events,
false,
new_info.timeline.prev_batch,
&push_rules,
&mut user_ids,
@@ -1049,6 +1132,7 @@ impl BaseClient {
&room,
new_info.timeline.limited,
new_info.timeline.events,
false,
new_info.timeline.prev_batch,
&push_rules,
&mut user_ids,
@@ -1090,13 +1174,16 @@ impl BaseClient {
self.room_info_notable_update_sender.clone(),
);
let invite_state =
Self::deserialize_stripped_state_events(&new_info.invite_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_invited();
room_info.mark_state_fully_synced();
self.handle_invited_state(
&room,
&new_info.invite_state.events,
&invite_state,
&push_rules,
&mut room_info,
&mut changes,
@@ -1109,6 +1196,34 @@ impl BaseClient {
new_rooms.invite.insert(room_id, new_info);
}
for (room_id, new_info) in response.rooms.knock {
let room = self.store.get_or_create_room(
&room_id,
RoomState::Knocked,
self.room_info_notable_update_sender.clone(),
);
let knock_state = Self::deserialize_stripped_state_events(&new_info.knock_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
room_info.mark_state_fully_synced();
self.handle_invited_state(
&room,
&knock_state,
&push_rules,
&mut room_info,
&mut changes,
&mut notifications,
)
.await?;
changes.add_room(room_info);
new_rooms.knocked.insert(room_id, new_info);
}
account_data_processor.apply(&mut changes, &self.store).await;
changes.presence = response
@@ -1217,7 +1332,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
let mut user_ids = BTreeSet::new();
let mut ambiguity_map: BTreeMap<String, BTreeSet<OwnedUserId>> = BTreeMap::new();
let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
for raw_event in &response.chunk {
let member = match raw_event.deserialize() {
@@ -1248,7 +1363,11 @@ impl BaseClient {
if let StateEvent::Original(e) = &member {
if let Some(d) = &e.content.displayname {
ambiguity_map.entry(d.clone()).or_default().insert(member.state_key().clone());
let display_name = DisplayName::new(d);
ambiguity_map
.entry(display_name)
.or_default()
.insert(member.state_key().clone());
}
}
@@ -1377,6 +1496,17 @@ impl BaseClient {
self.store.room(room_id)
}
/// Forget the room with the given room ID.
///
/// The room will be dropped from the room list and the store.
///
/// # Arguments
///
/// * `room_id` - The id of the room that should be forgotten.
pub async fn forget_room(&self, room_id: &RoomId) -> StoreResult<()> {
self.store.forget_room(room_id).await
}
/// Get the olm machine.
#[cfg(feature = "e2e-encryption")]
pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
@@ -1540,6 +1670,21 @@ impl BaseClient {
.collect()
}
pub(crate) fn deserialize_stripped_state_events(
raw_events: &[Raw<AnyStrippedStateEvent>],
) -> Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)> {
raw_events
.iter()
.filter_map(|raw_event| match raw_event.deserialize() {
Ok(event) => Some((raw_event.clone(), event)),
Err(e) => {
warn!("Couldn't deserialize stripped state event: {e}");
None
}
})
.collect()
}
/// Returns a new receiver that gets future room info notable updates.
///
/// Learn more by reading the [`RoomInfoNotableUpdate`] type.
@@ -1548,12 +1693,6 @@ impl BaseClient {
}
}
impl Default for BaseClient {
fn default() -> Self {
Self::new()
}
}
fn handle_room_member_event_for_profiles(
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
@@ -1596,8 +1735,9 @@ mod tests {
use super::BaseClient;
use crate::{
store::StateStoreExt, test_utils::logged_in_base_client, DisplayName, RoomState,
SessionMeta,
store::{StateStoreExt, StoreConfig},
test_utils::logged_in_base_client,
RoomDisplayName, RoomState, SessionMeta,
};
#[async_test]
@@ -1728,7 +1868,7 @@ mod tests {
assert_eq!(room.state(), RoomState::Invited);
assert_eq!(
room.compute_display_name().await.expect("fetching display name failed"),
DisplayName::Calculated("Kyra".to_owned())
RoomDisplayName::Calculated("Kyra".to_owned())
);
}
@@ -1804,7 +1944,9 @@ mod tests {
let user_id = user_id!("@alice:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1862,7 +2004,9 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1922,7 +2066,9 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
+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,
}
@@ -20,7 +20,7 @@ use ruma::{
};
use super::DynEventCacheStore;
use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings};
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
/// `EventCacheStore` integration tests.
///
@@ -31,6 +31,9 @@ use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings};
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))]
@@ -38,11 +41,13 @@ pub trait EventCacheStoreIntegrationTests {
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file =
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
let request_thumbnail = MediaRequest {
let request_file = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
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),
@@ -50,7 +55,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequest {
let request_other_file = MediaRequestParameters {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
@@ -139,6 +144,44 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
"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
@@ -150,7 +193,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache_store::{
/// # use matrix_sdk_base::event_cache::store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
@@ -174,7 +217,9 @@ 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 $crate::event_cache::store::{
EventCacheStoreIntegrationTests, IntoEventCacheStore,
};
use super::get_event_cache_store;
@@ -184,6 +229,89 @@ macro_rules! event_cache_store_integration_tests {
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);
}
}
};
}
@@ -12,14 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{num::NonZeroUsize, sync::RwLock as StdRwLock};
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
use async_trait::async_trait;
use matrix_sdk_common::ring_buffer::RingBuffer;
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::{MediaRequest, UniqueKey as _};
use crate::media::{MediaRequestParameters, UniqueKey as _};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
@@ -28,6 +30,7 @@ use crate::media::{MediaRequest, UniqueKey as _};
#[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.
@@ -35,7 +38,10 @@ 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)) }
Self {
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
leases: Default::default(),
}
}
}
@@ -51,7 +57,20 @@ impl MemoryStore {
impl EventCacheStore for MemoryStore {
type Error = EventCacheStoreError;
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
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.
@@ -60,18 +79,35 @@ impl EventCacheStore for MemoryStore {
Ok(())
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
let media = self.media.read().unwrap();
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: &MediaRequest) -> Result<()> {
let mut media = self.media.write().unwrap();
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)
@@ -113,4 +149,5 @@ mod tests {
}
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
}
}
@@ -19,7 +19,7 @@ use matrix_sdk_common::AsyncTraitDeps;
use ruma::MxcUri;
use super::EventCacheStoreError;
use crate::media::MediaRequest;
use crate::media::MediaRequestParameters;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
@@ -29,6 +29,14 @@ 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
@@ -38,10 +46,35 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequest,
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
@@ -49,7 +82,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequest,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
@@ -57,11 +90,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>;
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.
@@ -83,22 +122,42 @@ impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
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: &MediaRequest,
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: &MediaRequest,
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: &MediaRequest) -> Result<(), Self::Error> {
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
@@ -1,85 +0,0 @@
// 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::str::Utf8Error;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
mod memory_store;
mod traits;
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},
};
/// 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>;
+78 -17
View File
@@ -12,7 +12,17 @@ use ruma::events::{
room::message::SyncRoomMessageEvent,
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
};
use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId};
use ruma::{
events::{
room::{
member::{MembershipState, SyncRoomMemberEvent},
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
AnySyncStateEvent,
},
MxcUri, OwnedEventId, UserId,
};
use serde::{Deserialize, Serialize};
use crate::MinimalRoomMemberEvent;
@@ -37,6 +47,10 @@ pub enum PossibleLatestEvent<'a> {
/// This message is suitable - it's a call notification
YesCallNotify(&'a SyncCallNotifyEvent),
/// This state event is suitable - it's a knock membership change
/// that can be handled by the current user.
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
// Later: YesState(),
// Later: YesReaction(),
/// Not suitable - it's a state event
@@ -50,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)) => {
@@ -102,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
}
}
}
@@ -327,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");
@@ -350,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?");
@@ -374,7 +412,7 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
@@ -396,7 +434,7 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
@@ -417,7 +455,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
);
}
@@ -439,7 +477,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -467,7 +505,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
@@ -489,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]
@@ -506,7 +547,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
@@ -530,7 +571,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -561,9 +602,12 @@ mod tests {
json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
}
},
}
@@ -577,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
});
+4 -3
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,7 +28,7 @@ mod client;
pub mod debug;
pub mod deserialized_responses;
mod error;
pub mod event_cache_store;
pub mod event_cache;
pub mod latest_event;
pub mod media;
pub mod notification_settings;
@@ -55,12 +56,12 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo,
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
RoomStateFilter,
};
pub use store::{
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
};
pub use utils::{
+27 -27
View File
@@ -14,6 +14,7 @@ use ruma::{
},
MxcUri, UInt,
};
use serde::{Deserialize, Serialize};
const UNIQUE_SEPARATOR: &str = "_";
@@ -25,7 +26,7 @@ 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,
@@ -43,9 +44,9 @@ impl UniqueKey for MediaFormat {
}
}
/// 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,19 +57,6 @@ pub struct MediaThumbnailSize {
/// The desired height of the thumbnail. The actual thumbnail may not match
/// the size specified.
pub height: UInt,
}
impl UniqueKey for MediaThumbnailSize {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
}
}
/// The desired settings of a media thumbnail.
#[derive(Clone, Debug)]
pub struct MediaThumbnailSettings {
/// The desired size of the thumbnail.
pub size: MediaThumbnailSize,
/// If we want to request an animated thumbnail from the homeserver.
///
@@ -82,14 +70,24 @@ pub struct MediaThumbnailSettings {
impl MediaThumbnailSettings {
/// Constructs a new `MediaThumbnailSettings` with the given method, width
/// and height.
pub fn new(method: Method, width: UInt, height: UInt) -> Self {
Self { size: MediaThumbnailSize { method, width, height }, animated: false }
///
/// 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 {
let mut key = self.size.unique_key();
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
if self.animated {
key.push_str(UNIQUE_SEPARATOR);
@@ -109,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,
@@ -119,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 {
@@ -129,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())
}
@@ -225,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,
+2 -2
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 };
+8 -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 {
@@ -245,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>>,
}
+73 -5
View File
@@ -15,6 +15,7 @@ pub use normal::{
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
RoomStateFilter,
};
use regex::Regex;
use ruma::{
assign,
events::{
@@ -49,7 +50,7 @@ use crate::MinimalStateEvent;
/// The name of the room, either from the metadata or calculated
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DisplayName {
pub enum RoomDisplayName {
/// The room has been named explicitly as
Named(String),
/// The room has a canonical alias that should be used
@@ -64,14 +65,48 @@ pub enum DisplayName {
Empty,
}
impl fmt::Display for DisplayName {
const WHITESPACE_REGEX: &str = r"\s+";
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
impl RoomDisplayName {
/// Transforms the current display name into the name part of a
/// `RoomAliasId`.
pub fn to_room_alias_name(&self) -> String {
let room_name = match self {
Self::Named(name) => name,
Self::Aliased(name) => name,
Self::Calculated(name) => name,
Self::EmptyWas(name) => name,
Self::Empty => "",
};
let whitespace_regex =
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
let symbol_regex =
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
// Replace whitespaces with `-`
let sanitised = whitespace_regex.replace_all(room_name, "-");
// Remove non-ASCII characters and ASCII control characters
let sanitised =
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
// Remove other problematic ASCII symbols
let sanitised = symbol_regex.replace_all(&sanitised, "");
// Lowercased
sanitised.to_lowercase()
}
}
impl fmt::Display for RoomDisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
RoomDisplayName::Named(s)
| RoomDisplayName::Calculated(s)
| RoomDisplayName::Aliased(s) => {
write!(f, "{s}")
}
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
DisplayName::Empty => write!(f, "Empty Room"),
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
RoomDisplayName::Empty => write!(f, "Empty Room"),
}
}
}
@@ -541,6 +576,7 @@ mod tests {
use ruma::events::tag::{TagInfo, TagName, Tags};
use super::{BaseRoomInfo, RoomNotableTags};
use crate::RoomDisplayName;
#[test]
fn test_handle_notable_tags_favourite() {
@@ -571,4 +607,36 @@ mod tests {
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
}
#[test]
fn test_room_alias_from_room_display_name_lowercases() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_whitespace() {
assert_eq!(
"room-alias",
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
);
}
}
+153 -72
View File
@@ -23,6 +23,8 @@ use std::{
use bitflags::bitflags;
use eyeball::{SharedObservable, Subscriber};
use futures_util::{Stream, StreamExt};
#[cfg(feature = "experimental-sliding-sync")]
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
use matrix_sdk_common::ring_buffer::RingBuffer;
#[cfg(feature = "experimental-sliding-sync")]
@@ -41,6 +43,7 @@ use ruma::{
join_rules::JoinRule,
member::{MembershipState, RoomMemberEventContent},
pinned_events::RoomPinnedEventsEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
redaction::SyncRoomRedactionEvent,
tombstone::RoomTombstoneEventContent,
},
@@ -58,18 +61,18 @@ use tokio::sync::broadcast;
use tracing::{debug, field::debug, info, instrument, warn};
use super::{
members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent,
members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
RoomMember, RoomNotableTags,
};
#[cfg(feature = "experimental-sliding-sync")]
use crate::latest_event::LatestEvent;
use crate::{
deserialized_responses::{MemberEvent, RawSyncOrStrippedState},
deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState},
notification_settings::RoomNotificationMode,
read_receipts::RoomReadReceipts,
store::{DynStateStore, Result as StoreResult, StateStoreExt},
sync::UnreadNotificationsCount,
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
Error, MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
};
/// Indicates that a notable update of `RoomInfo` has been applied, and why.
@@ -182,15 +185,17 @@ impl RoomSummary {
}
/// Enum keeping track in which state the room is, e.g. if our own user is
/// joined, invited, or has left the room.
/// joined, RoomState::Invited, or has left the room.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomState {
/// The room is in a joined state.
Joined,
/// The room is in a left state.
Left,
/// The room is in a invited state.
/// The room is in an invited state.
Invited,
/// The room is in a knocked state.
Knocked,
}
impl From<&MembershipState> for RoomState {
@@ -201,7 +206,7 @@ impl From<&MembershipState> for RoomState {
MembershipState::Ban => Self::Left,
MembershipState::Invite => Self::Invited,
MembershipState::Join => Self::Joined,
MembershipState::Knock => Self::Left,
MembershipState::Knock => Self::Knocked,
MembershipState::Leave => Self::Left,
_ => panic!("Unexpected MembershipState: {}", membership_state),
}
@@ -273,6 +278,11 @@ impl Room {
self.inner.read().room_state
}
/// Get the previous state of the room, if it had any.
pub fn prev_state(&self) -> Option<RoomState> {
self.inner.read().prev_room_state
}
/// Whether this room's [`RoomType`] is `m.space`.
pub fn is_space(&self) -> bool {
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
@@ -436,6 +446,9 @@ impl Room {
},
}
}
// TODO: implement logic once we have the stripped events as we'd have with an Invite
RoomState::Knocked => Ok(false),
}
}
@@ -496,6 +509,17 @@ impl Room {
self.inner.read().base_info.max_power_level
}
/// Get the current power levels of this room.
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
Ok(self
.store
.get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
.await?
.ok_or(Error::InsufficientData)?
.deserialize()?
.power_levels())
}
/// Get the `m.room.name` of this room.
///
/// The returned string may be empty if the event has been redacted, or it's
@@ -548,8 +572,8 @@ impl Room {
/// [`Self::cached_display_name`].
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
pub async fn compute_display_name(&self) -> StoreResult<DisplayName> {
let update_cache = |new_val: DisplayName| {
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
let update_cache = |new_val: RoomDisplayName| {
self.inner.update_if(|info| {
if info.cached_display_name.as_ref() != Some(&new_val) {
info.cached_display_name = Some(new_val.clone());
@@ -567,13 +591,13 @@ impl Room {
if let Some(name) = inner.name() {
let name = name.trim().to_owned();
drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`.
return Ok(update_cache(DisplayName::Named(name)));
return Ok(update_cache(RoomDisplayName::Named(name)));
}
if let Some(alias) = inner.canonical_alias() {
let alias = alias.alias().trim().to_owned();
drop(inner); // See above comment.
return Ok(update_cache(DisplayName::Aliased(alias)));
return Ok(update_cache(RoomDisplayName::Aliased(alias)));
}
inner.summary.clone()
@@ -679,7 +703,7 @@ impl Room {
///
/// This cache is refilled every time we call
/// [`Self::compute_display_name`].
pub fn cached_display_name(&self) -> Option<DisplayName> {
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
self.inner.read().cached_display_name.clone()
}
@@ -795,8 +819,7 @@ impl Room {
})
.collect::<BTreeMap<_, _>>();
let display_names =
member_events.iter().map(|e| e.display_name().to_owned()).collect::<Vec<_>>();
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
let room_info = self.member_room_info(&display_names).await?;
let mut members = Vec::new();
@@ -859,8 +882,8 @@ impl Room {
/// Get the `RoomMember` with the given `user_id`.
///
/// Returns `None` if the member was never part of this room, otherwise
/// return a `RoomMember` that can be in a joined, invited, left, banned
/// state.
/// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
/// banned state.
///
/// Async because it can read from storage.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
@@ -876,7 +899,7 @@ impl Room {
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let display_names = [event.display_name().to_owned()];
let display_names = [event.display_name()];
let room_info = self.member_room_info(&display_names).await?;
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
@@ -887,7 +910,7 @@ impl Room {
/// Async because it can read from storage.
async fn member_room_info<'a>(
&self,
display_names: &'a [String],
display_names: &'a [DisplayName],
) -> StoreResult<MemberRoomInfo<'a>> {
let max_power_level = self.max_power_level();
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
@@ -995,7 +1018,7 @@ impl Room {
}
/// Returns the current pinned event ids for this room.
pub fn pinned_event_ids(&self) -> Vec<OwnedEventId> {
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
self.inner.read().pinned_event_ids()
}
}
@@ -1032,6 +1055,9 @@ pub struct RoomInfo {
/// The state of the room.
pub(crate) room_state: RoomState,
/// The previous state of the room, if any.
pub(crate) prev_room_state: Option<RoomState>,
/// The unread notifications counts, as returned by the server.
///
/// These might be incorrect for encrypted rooms, since the server doesn't
@@ -1076,7 +1102,7 @@ pub struct RoomInfo {
/// Filled by calling [`Room::compute_display_name`]. It's automatically
/// filled at start when creating a room, or on every successful sync.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cached_display_name: Option<DisplayName>,
pub(crate) cached_display_name: Option<RoomDisplayName>,
/// Cached user defined notification mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1117,6 +1143,7 @@ impl RoomInfo {
version: 1,
room_id: room_id.into(),
room_state,
prev_room_state: None,
notification_counts: Default::default(),
summary: Default::default(),
members_synced: false,
@@ -1137,22 +1164,30 @@ impl RoomInfo {
/// Mark this Room as joined.
pub fn mark_as_joined(&mut self) {
self.room_state = RoomState::Joined;
self.set_state(RoomState::Joined);
}
/// Mark this Room as left.
pub fn mark_as_left(&mut self) {
self.room_state = RoomState::Left;
self.set_state(RoomState::Left);
}
/// Mark this Room as invited.
pub fn mark_as_invited(&mut self) {
self.room_state = RoomState::Invited;
self.set_state(RoomState::Invited);
}
/// Mark this Room as knocked.
pub fn mark_as_knocked(&mut self) {
self.set_state(RoomState::Knocked);
}
/// Set the membership RoomState of this Room
pub fn set_state(&mut self, room_state: RoomState) {
self.room_state = room_state;
if room_state != self.room_state {
self.prev_room_state = Some(self.room_state);
self.room_state = room_state;
}
}
/// Mark this Room as having all the members synced.
@@ -1261,9 +1296,12 @@ impl RoomInfo {
if let Some(latest_event) = &mut self.latest_event {
tracing::trace!("Checking if redaction applies to latest event");
if latest_event.event_id().as_deref() == Some(redacts) {
match apply_redaction(&latest_event.event().event, _raw, room_version) {
match apply_redaction(latest_event.event().raw(), _raw, room_version) {
Some(redacted) => {
latest_event.event_mut().event = redacted;
// Even if the original event was encrypted, redaction removes all its
// fields so it cannot possibly be successfully decrypted after redaction.
latest_event.event_mut().kind =
TimelineEventKind::PlainText { event: redacted };
debug!("Redacted latest event");
}
None => {
@@ -1557,8 +1595,8 @@ impl RoomInfo {
}
/// Returns the current pinned event ids for this room.
pub fn pinned_event_ids(&self) -> Vec<OwnedEventId> {
self.base_info.pinned_events.clone().map(|c| c.pinned).unwrap_or_default()
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
self.base_info.pinned_events.clone().map(|c| c.pinned)
}
/// Checks if an `EventId` is currently pinned.
@@ -1685,6 +1723,8 @@ bitflags! {
const INVITED = 0b00000010;
/// The room is in a left state.
const LEFT = 0b00000100;
/// The room is in a knocked state.
const KNOCKED = 0b00001000;
}
}
@@ -1699,6 +1739,7 @@ impl RoomStateFilter {
RoomState::Joined => Self::JOINED,
RoomState::Left => Self::LEFT,
RoomState::Invited => Self::INVITED,
RoomState::Knocked => Self::KNOCKED,
};
self.contains(bit_state)
@@ -1725,7 +1766,10 @@ impl RoomStateFilter {
/// Calculate room name according to step 3 of the [naming algorithm].
///
/// [naming algorithm]: https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room
fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&str>) -> DisplayName {
fn compute_display_name_from_heroes(
num_joined_invited: u64,
mut heroes: Vec<&str>,
) -> RoomDisplayName {
let num_heroes = heroes.len() as u64;
let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
@@ -1747,12 +1791,12 @@ fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&st
// User is alone.
if num_joined_invited <= 1 {
if names.is_empty() {
DisplayName::Empty
RoomDisplayName::Empty
} else {
DisplayName::EmptyWas(names)
RoomDisplayName::EmptyWas(names)
}
} else {
DisplayName::Calculated(names)
RoomDisplayName::Calculated(names)
}
}
@@ -1808,8 +1852,9 @@ mod tests {
use crate::latest_event::LatestEvent;
use crate::{
rooms::RoomNotableTags,
store::{IntoStateStore, MemoryStore, StateChanges, StateStore},
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta,
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
RoomInfoNotableUpdateReasons, SessionMeta,
};
#[test]
@@ -1827,6 +1872,7 @@ mod tests {
version: 1,
room_id: room_id!("!gda78o:server.tld").into(),
room_state: RoomState::Invited,
prev_room_state: None,
notification_counts: UnreadNotificationsCount {
highlight_count: 1,
notification_count: 2,
@@ -1844,9 +1890,9 @@ mod tests {
last_prev_batch: Some("pb".to_owned()),
sync_info: SyncInfo::FullySynced,
encryption_state_synced: true,
latest_event: Some(Box::new(LatestEvent::new(
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(),
))),
latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new(
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
)))),
base_info: Box::new(
assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
),
@@ -1861,6 +1907,7 @@ mod tests {
"version": 1,
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"prev_room_state": null,
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
@@ -1880,10 +1927,7 @@ mod tests {
"encryption_state_synced": true,
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"sender": "@u:i.uk",
},
"kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
},
},
"base_info": {
@@ -1932,6 +1976,7 @@ mod tests {
let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"prev_room_state": null,
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
@@ -2008,7 +2053,8 @@ mod tests {
let info_json = json!({
"room_id": "!gda78o:server.tld",
"room_state": "Invited",
"room_state": "Joined",
"prev_room_state": "Invited",
"notification_counts": {
"highlight_count": 1,
"notification_count": 2,
@@ -2048,7 +2094,8 @@ mod tests {
let info: RoomInfo = serde_json::from_value(info_json).unwrap();
assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
assert_eq!(info.room_state, RoomState::Invited);
assert_eq!(info.room_state, RoomState::Joined);
assert_eq!(info.prev_room_state, Some(RoomState::Invited));
assert_eq!(info.notification_counts.highlight_count, 1);
assert_eq!(info.notification_counts.notification_count, 2);
assert_eq!(
@@ -2081,7 +2128,7 @@ mod tests {
assert_eq!(
info.cached_display_name.as_ref(),
Some(&DisplayName::Calculated("lol".to_owned())),
Some(&RoomDisplayName::Calculated("lol".to_owned())),
);
assert_eq!(
info.cached_user_defined_notification_mode.as_ref(),
@@ -2093,7 +2140,9 @@ mod tests {
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -2171,7 +2220,9 @@ mod tests {
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -2286,7 +2337,7 @@ mod tests {
#[async_test]
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
let (_, room) = make_room_test_helper(RoomState::Joined);
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2296,7 +2347,7 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
}
@@ -2307,20 +2358,20 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Named("Test Room".to_owned())
RoomDisplayName::Named("Test Room".to_owned())
);
}
#[async_test]
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
let (_, room) = make_room_test_helper(RoomState::Invited);
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2333,7 +2384,7 @@ mod tests {
});
room.inner.update(|info| info.base_info.name = Some(room_name));
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2343,7 +2394,7 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
}
@@ -2354,13 +2405,13 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Named("Test Room".to_owned())
RoomDisplayName::Named("Test Room".to_owned())
);
}
@@ -2402,7 +2453,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2424,7 +2475,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2454,7 +2505,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2479,7 +2530,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2534,7 +2585,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
);
}
@@ -2583,7 +2634,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
);
}
@@ -2613,7 +2664,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::EmptyWas("Matthew".to_owned())
RoomDisplayName::EmptyWas("Matthew".to_owned())
);
}
@@ -2627,7 +2678,9 @@ mod tests {
use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -3009,34 +3062,34 @@ mod tests {
#[test]
fn test_calculate_room_name() {
let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
actual = compute_display_name_from_heroes(5, vec![]);
assert_eq!(DisplayName::Calculated("5 people".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
actual = compute_display_name_from_heroes(0, vec![]);
assert_eq!(DisplayName::Empty, actual);
assert_eq!(RoomDisplayName::Empty, actual);
actual = compute_display_name_from_heroes(1, vec![]);
assert_eq!(DisplayName::Empty, actual);
assert_eq!(RoomDisplayName::Empty, actual);
actual = compute_display_name_from_heroes(1, vec!["a"]);
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
}
#[test]
@@ -3166,4 +3219,32 @@ mod tests {
let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
assert_eq!(new_room_info.version, 1);
}
#[async_test]
async fn test_prev_room_state_is_updated() {
let (_store, room) = make_room_test_helper(RoomState::Invited);
assert_eq!(room.prev_state(), None);
assert_eq!(room.state(), RoomState::Invited);
// Invited -> Joined
let mut room_info = room.clone_info();
room_info.mark_as_joined();
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
assert_eq!(room.prev_state(), Some(RoomState::Invited));
assert_eq!(room.state(), RoomState::Joined);
// No change when the same state is used
let mut room_info = room.clone_info();
room_info.mark_as_joined();
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
assert_eq!(room.prev_state(), Some(RoomState::Invited));
assert_eq!(room.state(), RoomState::Joined);
// Joined -> Left
let mut room_info = room.clone_info();
room_info.mark_as_left();
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
assert_eq!(room.prev_state(), Some(RoomState::Joined));
assert_eq!(room.state(), RoomState::Left);
}
}
+343 -80
View File
@@ -27,10 +27,13 @@ use ruma::api::client::sync::sync_events::v5;
#[cfg(feature = "e2e-encryption")]
use ruma::events::AnyToDeviceEvent;
use ruma::{
api::client::sync::sync_events::v3::{self, InvitedRoom},
events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent},
api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom},
events::{
room::member::MembershipState, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, StateEventType,
},
serde::Raw,
JsOption, OwnedRoomId, RoomId, UInt,
JsOption, OwnedRoomId, RoomId, UInt, UserId,
};
use tracing::{debug, error, instrument, trace, warn};
@@ -47,6 +50,7 @@ use crate::{
normal::{RoomHero, RoomInfoNotableUpdateReasons},
RoomState,
},
ruma::assign,
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
Room, RoomInfo,
@@ -167,13 +171,20 @@ impl BaseClient {
let mut notifications = Default::default();
let mut rooms_account_data = extensions.account_data.rooms.clone();
let user_id = self
.session_meta()
.expect("Sliding sync shouldn't run without an authenticated user.")
.user_id
.to_owned();
for (room_id, response_room_data) in rooms {
let (room_info, joined_room, left_room, invited_room) = self
let (room_info, joined_room, left_room, invited_room, knocked_room) = self
.process_sliding_sync_room(
room_id,
response_room_data,
&mut rooms_account_data,
&store,
&user_id,
&account_data_processor,
&mut changes,
&mut room_info_notable_updates,
@@ -196,6 +207,10 @@ impl BaseClient {
if let Some(invited_room) = invited_room {
new_rooms.invite.insert(room_id.clone(), invited_room);
}
if let Some(knocked_room) = knocked_room {
new_rooms.knocked.insert(room_id.clone(), knocked_room);
}
}
// Handle read receipts and typing notifications independently of the rooms:
@@ -260,7 +275,7 @@ impl BaseClient {
.or_insert_with(LeftRoomUpdate::default)
.account_data
.append(&mut raw.to_vec()),
RoomState::Invited => {}
RoomState::Invited | RoomState::Knocked => {}
}
}
}
@@ -341,27 +356,32 @@ impl BaseClient {
room_data: &http::response::Room,
rooms_account_data: &mut BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
store: &Store,
user_id: &UserId,
account_data_processor: &AccountDataProcessor,
changes: &mut StateChanges,
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
ambiguity_cache: &mut AmbiguityCache,
with_msc4186: bool,
) -> Result<(RoomInfo, Option<JoinedRoomUpdate>, Option<LeftRoomUpdate>, Option<InvitedRoom>)>
{
) -> Result<(
RoomInfo,
Option<JoinedRoomUpdate>,
Option<LeftRoomUpdate>,
Option<InvitedRoom>,
Option<KnockedRoom>,
)> {
// This method may change `room_data` (see the terrible hack describes below)
// with `timestamp` and `invite_state. We don't want to change the `room_data`
// from outside this method, hence `Cow` is perfectly suited here.
let mut room_data = Cow::Borrowed(room_data);
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = {
let mut state_events = Vec::new();
// Read state events from the `required_state` field.
state_events.extend(Self::deserialize_state_events(&room_data.required_state));
let state_events = Self::deserialize_state_events(&room_data.required_state);
// Read state events from the `timeline` field.
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
// Don't read state events from the `timeline` field, because they might be
// incomplete or staled already. We must only read state events from
// `required_state`.
state_events.into_iter().unzip()
};
@@ -403,14 +423,22 @@ impl BaseClient {
}
}
let stripped_state: Option<Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)>> =
room_data
.invite_state
.as_ref()
.map(|invite_state| Self::deserialize_stripped_state_events(invite_state));
#[allow(unused_mut)] // Required for some feature flag combinations
let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership(
room_data.as_ref(),
&state_events,
store,
room_id,
room_info_notable_updates,
);
let (mut room, mut room_info, invited_room, knocked_room) = self
.process_sliding_sync_room_membership(
&state_events,
stripped_state.as_ref(),
store,
user_id,
room_id,
room_info_notable_updates,
);
room_info.mark_state_partially_synced();
@@ -429,7 +457,8 @@ impl BaseClient {
let push_rules = self.get_push_rules(account_data_processor).await?;
if let Some(invite_state) = &room_data.invite_state {
// This will be used for both invited and knocked rooms.
if let Some(invite_state) = &stripped_state {
self.handle_invited_state(
&room,
invite_state,
@@ -454,6 +483,7 @@ impl BaseClient {
&room,
room_data.limited,
room_data.timeline.clone(),
true,
room_data.prev_batch.clone(),
&push_rules,
&mut user_ids,
@@ -512,6 +542,7 @@ impl BaseClient {
)),
None,
None,
None,
))
}
@@ -525,9 +556,12 @@ impl BaseClient {
ambiguity_changes,
)),
None,
None,
)),
RoomState::Invited => Ok((room_info, None, None, invited_room)),
RoomState::Invited => Ok((room_info, None, None, invited_room, None)),
RoomState::Knocked => Ok((room_info, None, None, None, knocked_room)),
}
}
@@ -538,13 +572,14 @@ impl BaseClient {
/// otherwise. https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md#room-list-parameters
fn process_sliding_sync_room_membership(
&self,
room_data: &http::response::Room,
state_events: &[AnySyncStateEvent],
stripped_state: Option<&Vec<(Raw<AnyStrippedStateEvent>, AnyStrippedStateEvent)>>,
store: &Store,
user_id: &UserId,
room_id: &RoomId,
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
) -> (Room, RoomInfo, Option<InvitedRoom>) {
if let Some(invite_state) = &room_data.invite_state {
) -> (Room, RoomInfo, Option<InvitedRoom>, Option<KnockedRoom>) {
if let Some(stripped_state) = stripped_state {
let room = store.get_or_create_room(
room_id,
RoomState::Invited,
@@ -552,20 +587,34 @@ impl BaseClient {
);
let mut room_info = room.clone_info();
// We don't actually know what events are inside invite_state. In theory, they
// might not contain an m.room.member event, or they might set the
// membership to something other than invite. This would be very
// weird behaviour by the server, because invite_state is supposed
// to contain an m.room.member. We will call handle_invited_state, which will
// reflect any information found in the real events inside
// invite_state, but we default to considering this room invited
// simply because invite_state exists. This is needed in the normal
// case, because the sliding sync server tries to send minimal state,
// meaning that we normally actually just receive {"type": "m.room.member"} with
// no content at all.
room_info.mark_as_invited();
// We need to find the membership event since it could be for either an invited
// or knocked room
let membership_event_content = stripped_state.iter().find_map(|(_, event)| {
if let AnyStrippedStateEvent::RoomMember(membership_event) = event {
if membership_event.state_key == user_id {
return Some(membership_event.content.clone());
}
}
None
});
(room, room_info, Some(InvitedRoom::from(v3::InviteState::from(invite_state.clone()))))
if let Some(membership_event_content) = membership_event_content {
if membership_event_content.membership == MembershipState::Knock {
// If we have a `Knock` membership state, set the room as such
room_info.mark_as_knocked();
let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect();
let knock_state = assign!(v3::KnockState::default(), { events: raw_events });
let knocked_room =
assign!(KnockedRoom::default(), { knock_state: knock_state });
return (room, room_info, None, Some(knocked_room));
}
}
// Otherwise assume it's an invited room
room_info.mark_as_invited();
let raw_events = stripped_state.iter().map(|(raw, _)| raw.clone()).collect::<Vec<_>>();
let invited_room = InvitedRoom::from(v3::InviteState::from(raw_events));
(room, room_info, Some(invited_room), None)
} else {
let room = store.get_or_create_room(
room_id,
@@ -591,7 +640,7 @@ impl BaseClient {
room_info_notable_updates,
);
(room, room_info, None)
(room, room_info, None, None)
}
}
@@ -629,33 +678,6 @@ impl BaseClient {
}
}
}
pub(crate) fn deserialize_state_events_from_timeline(
raw_events: &[Raw<AnySyncTimelineEvent>],
) -> Vec<(Raw<AnySyncStateEvent>, AnySyncStateEvent)> {
raw_events
.iter()
.filter_map(|raw_event| {
// If it contains `state_key`, we assume it's a state event.
if raw_event.get_field::<serde::de::IgnoredAny>("state_key").transpose().is_some() {
match raw_event.deserialize_as::<AnySyncStateEvent>() {
Ok(event) => {
// SAFETY: Casting `AnySyncTimelineEvent` to `AnySyncStateEvent` is safe
// because we checked that there is a `state_key`.
Some((raw_event.clone().cast(), event))
}
Err(error) => {
warn!("Couldn't deserialize state event from timeline: {error}");
None
}
}
} else {
None
}
})
.collect()
}
}
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
@@ -673,17 +695,41 @@ async fn cache_latest_events(
changes: Option<&StateChanges>,
store: Option<&Store>,
) {
use crate::{
deserialized_responses::DisplayName, store::ambiguity_map::is_display_name_ambiguous,
};
let mut encrypted_events =
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
// Try to get room power levels from the current changes
let power_levels_from_changes = || {
let state_changes = changes?.state.get(room_info.room_id())?;
let room_power_levels_state =
state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?;
match room_power_levels_state.deserialize().ok()? {
AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()),
_ => None,
}
};
// If we didn't get any info, try getting it from local data
let power_levels = match power_levels_from_changes() {
Some(power_levels) => Some(power_levels),
None => room.power_levels().await.ok(),
};
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
for event in events.iter().rev() {
if let Ok(timeline_event) = event.event.deserialize() {
match is_suitable_for_latest_event(&timeline_event) {
if let Ok(timeline_event) = event.raw().deserialize() {
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesCallNotify(_)
| PossibleLatestEvent::YesSticker(_) => {
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
// We found a suitable latest event. Store it.
// In order to make the latest event fast to read, we want to keep the
@@ -710,11 +756,13 @@ async fn cache_latest_events(
.as_original()
.and_then(|profile| profile.content.displayname.as_ref())
.and_then(|display_name| {
let display_name = DisplayName::new(display_name);
changes.ambiguity_maps.get(room.room_id()).and_then(
|map_for_room| {
map_for_room
.get(display_name)
.map(|user_ids| user_ids.len() > 1)
map_for_room.get(&display_name).map(|users| {
is_display_name_ambiguous(&display_name, users)
})
},
)
});
@@ -757,7 +805,7 @@ async fn cache_latest_events(
// Check how many encrypted events we have seen. Only store another if we
// haven't already stored the maximum number.
if encrypted_events.len() < encrypted_events.capacity() {
encrypted_events.push(event.event.clone());
encrypted_events.push(event.raw().clone());
}
}
_ => {
@@ -854,7 +902,10 @@ mod tests {
};
use assert_matches::assert_matches;
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_common::{
deserialized_responses::{SyncTimelineEvent, UnableToDecryptInfo, UnableToDecryptReason},
ring_buffer::RingBuffer,
};
use matrix_sdk_test::async_test;
use ruma::{
api::client::sync::sync_events::UnreadNotificationsCount,
@@ -982,6 +1033,7 @@ mod tests {
assert!(sync_resp.rooms.join.contains_key(room_id));
assert!(!sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
@@ -1038,6 +1090,7 @@ mod tests {
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(!sync_resp.rooms.leave.contains_key(room_id));
assert!(sync_resp.rooms.invite.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
@@ -1066,6 +1119,78 @@ mod tests {
assert_eq!(client_room.compute_display_name().await.unwrap().to_string(), "The Name");
}
#[async_test]
async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() {
// Given a logged-in client,
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
// When the room is properly set as knocked with the current user id as state
// key,
let mut room = http::response::Room::new();
set_room_knocked(&mut room, &user_id);
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// The room is knocked.
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Knocked);
}
#[async_test]
async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room(
) {
// Given a logged-in client,
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@w:e.uk");
// When the room is set as knocked with a random user id as state key,
let mut room = http::response::Room::new();
set_room_knocked(&mut room, user_id);
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// The room is invited since the membership event doesn't belong to the current
// user.
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Invited);
}
#[async_test]
async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room(
) {
// Given a logged-in client,
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
// When the room has the wrong membership state in its invite_state
let mut room = http::response::Room::new();
let event = Raw::new(&json!({
"type": "m.room.member",
"sender": user_id,
"content": {
"is_direct": true,
"membership": "join",
},
"state_key": user_id,
}))
.expect("Failed to make raw event")
.cast();
room.invite_state = Some(vec![event]);
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// The room is marked as invited.
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Invited);
}
#[async_test]
async fn test_left_a_room_from_required_state_event() {
// Given a logged-in client
@@ -1096,6 +1221,7 @@ mod tests {
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
@@ -1137,6 +1263,7 @@ mod tests {
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
}
@@ -1160,8 +1287,8 @@ mod tests {
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
// The room is NOT left because state events from `timeline` must be IGNORED!
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
}
#[async_test]
@@ -1637,6 +1764,119 @@ mod tests {
);
}
#[async_test]
async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() {
let own_user_id = user_id!("@me:e.uk");
// Given a logged-in client
let client = logged_in_base_client(Some(own_user_id)).await;
let room_id = room_id!("!r:e.uk");
// Give the current user invite or kick permissions in this room
let power_levels = json!({
"sender":"@alice:example.com",
"state_key":"",
"type":"m.room.power_levels",
"event_id": "$idb",
"origin_server_ts": 12344445,
"content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } },
"room_id": room_id,
});
// And a knock member state event
let knock_event = json!({
"sender":"@alice:example.com",
"state_key":"@alice:example.com",
"type":"m.room.member",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content":{"membership": "knock"},
"room_id": room_id,
});
// When the sliding sync response contains a timeline
let events = &[knock_event];
let mut room = room_with_timeline(events);
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// Then the room holds the latest knock state event
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())),
"$ida"
);
}
#[async_test]
async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() {
let own_user_id = user_id!("@me:e.uk");
// Given a logged-in client
let client = logged_in_base_client(Some(own_user_id)).await;
let room_id = room_id!("!r:e.uk");
// Set the user as a user with no permission to invite or kick other users in
// this room
let power_levels = json!({
"sender":"@alice:example.com",
"state_key":"",
"type":"m.room.power_levels",
"event_id": "$idb",
"origin_server_ts": 12344445,
"content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } },
"room_id": room_id,
});
// And a knock member state event
let knock_event = json!({
"sender":"@alice:example.com",
"state_key":"@alice:example.com",
"type":"m.room.member",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content":{"membership": "knock"},
"room_id": room_id,
});
// When the sliding sync response contains a timeline
let events = &[knock_event];
let mut room = room_with_timeline(events);
room.required_state.push(Raw::new(&power_levels).unwrap().cast());
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// Then the room doesn't hold the knock state event as the latest event
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.latest_event().is_none());
}
#[async_test]
async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() {
// Given a logged-in client
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
// And a join member state event
let join_event = json!({
"sender":"@alice:example.com",
"state_key":"@alice:example.com",
"type":"m.room.member",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content":{"membership": "join"},
"room_id": room_id,
});
// When the sliding sync response contains a timeline
let events = &[join_event];
let room = room_with_timeline(events);
let response = response_with_room(room_id, room);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
// Then the room doesn't hold the join state event as the latest event
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.latest_event().is_none());
}
#[async_test]
async fn test_cached_latest_event_can_be_redacted() {
// Given a logged-in client
@@ -1683,7 +1923,7 @@ mod tests {
// But it's now redacted
assert_matches!(
latest_event.event().event.deserialize().unwrap(),
latest_event.event().raw().deserialize().unwrap(),
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Redacted(_)
))
@@ -2275,7 +2515,7 @@ mod tests {
// The newly created room has no pinned event ids
let room = client.get_room(room_id).unwrap();
let pinned_event_ids = room.pinned_event_ids();
assert!(pinned_event_ids.is_empty());
assert_matches!(pinned_event_ids, None);
// Load new pinned event id
let mut room_response = http::response::Room::new();
@@ -2288,7 +2528,7 @@ mod tests {
let response = response_with_room(room_id, room_response);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
let pinned_event_ids = room.pinned_event_ids();
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert_eq!(pinned_event_ids.len(), 1);
assert_eq!(pinned_event_ids[0], pinned_event_id);
@@ -2302,7 +2542,7 @@ mod tests {
));
let response = response_with_room(room_id, room_response);
client.process_sliding_sync(&response, &(), true).await.expect("Failed to process sync");
let pinned_event_ids = room.pinned_event_ids();
let pinned_event_ids = room.pinned_event_ids().unwrap();
assert!(pinned_event_ids.is_empty());
}
@@ -2396,7 +2636,7 @@ mod tests {
}
fn make_encrypted_event(id: &str) -> SyncTimelineEvent {
SyncTimelineEvent::new(
SyncTimelineEvent::new_utd_event(
Raw::from_json_string(
json!({
"type": "m.room.encrypted",
@@ -2414,6 +2654,10 @@ mod tests {
.to_string(),
)
.unwrap(),
UnableToDecryptInfo {
session_id: Some("".to_owned()),
reason: UnableToDecryptReason::MissingMegolmSession,
},
)
}
@@ -2579,6 +2823,25 @@ mod tests {
));
}
fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) {
// MSC3575 shows an almost-empty event to indicate that we are invited to a
// room. Just the type is supplied.
let evt = Raw::new(&json!({
"type": "m.room.member",
"sender": knocker,
"content": {
"is_direct": true,
"membership": "knock",
},
"state_key": knocker,
}))
.expect("Failed to make raw event")
.cast();
room.invite_state = Some(vec![evt]);
}
fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) {
room.required_state.push(make_membership_event(user_id, MembershipState::Join));
}
+346 -89
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
sync::Arc,
};
@@ -24,28 +24,24 @@ use ruma::{
},
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tracing::trace;
use tracing::{instrument, trace};
use super::{DynStateStore, Result, StateChanges};
use crate::{
deserialized_responses::{AmbiguityChange, RawMemberEvent},
deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent},
store::StateStoreExt,
};
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[derive(Debug)]
struct AmbiguityMap {
display_name: String,
/// A map of users that use a certain display name.
#[derive(Debug, Clone)]
struct DisplayNameUsers {
display_name: DisplayName,
users: BTreeSet<OwnedUserId>,
}
impl AmbiguityMap {
impl DisplayNameUsers {
/// Remove the given [`UserId`] from the map, marking that the [`UserId`]
/// doesn't use the display name anymore.
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
self.users.remove(user_id);
@@ -56,6 +52,8 @@ impl AmbiguityMap {
}
}
/// Add the given [`UserId`] from the map, marking that the [`UserId`]
/// is using the display name.
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
let ambiguous_user =
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
@@ -65,46 +63,73 @@ impl AmbiguityMap {
ambiguous_user
}
/// How many users are using this display name.
fn user_count(&self) -> usize {
self.users.len()
}
/// Is the display name considered to be ambiguous.
fn is_ambiguous(&self) -> bool {
self.user_count() > 1
is_display_name_ambiguous(&self.display_name, &self.users)
}
}
fn is_member_active(membership: &MembershipState) -> bool {
use MembershipState::*;
matches!(membership, Join | Invite | Knock)
}
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[instrument(ret)]
pub(crate) fn is_display_name_ambiguous(
display_name: &DisplayName,
users_with_display_name: &BTreeSet<OwnedUserId>,
) -> bool {
trace!("Checking if a display name is ambiguous");
display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1
}
impl AmbiguityCache {
/// Create a new [`AmbiguityCache`] backed by the given state store.
pub fn new(store: Arc<DynStateStore>) -> Self {
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
}
/// Handle a newly received [`SyncRoomMemberEvent`] for the given room.
pub async fn handle_event(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<()> {
// Synapse seems to have a bug where it puts the same event into the
// state and the timeline sometimes.
// Synapse seems to have a bug where it puts the same event into the state and
// the timeline sometimes.
//
// Since our state, e.g. the old display name, already ended up inside
// the state changes and we're pulling stuff out of the cache if it's
// there calculating this twice for the same event will result in an
// incorrect AmbiguityChange overwriting the correct one. In other
// words, this method is not idempotent so we make it by ignoring
// duplicate events.
// Since our state, e.g. the old display name, already ended up inside the state
// changes and we're pulling stuff out of the cache if it's there calculating
// this twice for the same event will result in an incorrect AmbiguityChange
// overwriting the correct one. In other words, this method is not idempotent so
// we make it by ignoring duplicate events.
if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) {
return Ok(());
}
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
let (mut old_map, mut new_map) =
self.calculate_changes(changes, room_id, member_event).await?;
let display_names_same = match (&old_map, &new_map) {
(Some(a), Some(b)) => a.display_name == b.display_name,
_ => false,
};
// If the user's display name didn't change, then there's nothing more to
// calculate here.
if display_names_same {
return Ok(());
}
@@ -126,16 +151,21 @@ impl AmbiguityCache {
trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}");
self.add_change(room_id, member_event.event_id().to_owned(), change);
self.changes
.entry(room_id.to_owned())
.or_default()
.insert(member_event.event_id().to_owned(), change);
Ok(())
}
/// Update the [`AmbiguityCache`] state for the given room with a pair of
/// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`].
fn update(
&mut self,
room_id: &RoomId,
old_map: Option<AmbiguityMap>,
new_map: Option<AmbiguityMap>,
old_map: Option<DisplayNameUsers>,
new_map: Option<DisplayNameUsers>,
) {
let entry = self.cache.entry(room_id.to_owned()).or_default();
@@ -148,74 +178,102 @@ impl AmbiguityCache {
}
}
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
/// Get the previously used display name, if any, of the member described in
/// the given new [`SyncRoomMemberEvent`].
async fn get_old_display_name(
&self,
changes: &StateChanges,
room_id: &RoomId,
new_event: &SyncRoomMemberEvent,
) -> Result<Option<String>> {
let user_id = new_event.state_key();
let old_event = if let Some(m) = changes
.state
.get(room_id)
.and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str()))
{
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, user_id).await?
};
let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) };
if is_member_active(old_event.membership()) {
let display_name = if let Some(d) = changes
.profiles
.get(room_id)
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
{
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, user_id)
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
old_event.original_content().and_then(|c| c.displayname.clone())
};
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
} else {
Ok(None)
}
}
async fn get(
/// Get the [`DisplayNameUsers`] for the given display name in the given
/// room.
///
/// This method will get the [`DisplayNameUsers`] from the cache, if the
/// cache doesn't contain such an entry, it falls back to the state
/// store.
async fn get_users_with_display_name(
&mut self,
room_id: &RoomId,
display_name: &DisplayName,
) -> Result<DisplayNameUsers> {
Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) {
DisplayNameUsers { display_name: display_name.clone(), users: u.clone() }
} else {
let users_with_display_name =
self.store.get_users_with_display_name(room_id, display_name).await?;
DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name }
})
}
/// Calculate the change in the users that use a display name a
/// [`SyncRoomMemberEvent`] will cause for a given room.
///
/// Returns the [`DisplayNameUsers`] before the member event is applied and
/// the [`DisplayNameUsers`] after the member event is applied to the
/// room state.
async fn calculate_changes(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
use MembershipState::*;
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
}) {
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, member_event.state_key()).await?
};
// FIXME: Use let chains once stable
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
if matches!(event.membership(), Join | Invite) {
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
}) {
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, member_event.state_key())
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
event.original_content().and_then(|c| c.displayname.clone())
};
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
} else {
None
}
} else {
None
};
) -> Result<(Option<DisplayNameUsers>, Option<DisplayNameUsers>)> {
let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?;
let old_map = if let Some(old_name) = old_display_name.as_deref() {
let old_display_name_map =
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
u.clone()
} else {
self.store.get_users_with_display_name(room_id, old_name).await?
};
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
let old_display_name = DisplayName::new(old_name);
Some(self.get_users_with_display_name(room_id, &old_display_name).await?)
} else {
None
};
let new_map = if matches!(member_event.membership(), Join | Invite) {
let new_map = if is_member_active(member_event.membership()) {
let new = member_event
.as_original()
.and_then(|ev| ev.content.displayname.as_deref())
.unwrap_or_else(|| member_event.state_key().localpart());
// We don't allow other users to set the display name, so if we
// have a more trusted version of the display
// name use that.
// We don't allow other users to set the display name, so if we have a more
// trusted version of the display name use that.
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
new
} else if let Some(old) = old_display_name.as_deref() {
@@ -224,22 +282,221 @@ impl AmbiguityCache {
new
};
let new_display_name_map = if let Some(u) =
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
{
u.clone()
} else {
self.store.get_users_with_display_name(room_id, new_display_name).await?
};
let new_display_name = DisplayName::new(new_display_name);
Some(AmbiguityMap {
display_name: new_display_name.to_owned(),
users: new_display_name_map,
})
Some(self.get_users_with_display_name(room_id, &new_display_name).await?)
} else {
None
};
Ok((old_map, new_map))
}
#[cfg(test)]
fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool {
self.cache
.get(room_id)
.and_then(|display_names| {
display_names
.get(display_name)
.map(|user_ids| is_display_name_ambiguous(display_name, user_ids))
})
.unwrap_or_else(|| {
panic!(
"The display name {:?} should be part of the cache {:?}",
display_name, self.cache
)
})
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::async_test;
use ruma::{room_id, server_name, user_id, EventId};
use serde_json::json;
use super::*;
use crate::store::{IntoStateStore, MemoryStore};
fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent {
let server_name = server_name!("localhost");
serde_json::from_value(json!({
"content": {
"displayname": display_name,
"membership": "join"
},
"event_id": EventId::new(server_name),
"origin_server_ts": 152037280,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
}))
.expect("We should be able to deserialize the static member event")
}
macro_rules! assert_ambiguity {
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)?
) => {
assert_ambiguity!(
[ $( ($user, $display_name) ),* ],
[ $( ($check_display_name, $ambiguous) ),* ],
"The test failed the ambiguity assertions"
)
};
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ],
$description:literal $(,)?
) => {
let store = MemoryStore::new();
let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store());
let changes = Default::default();
let room_id = room_id!("!foo:bar");
macro_rules! add_display_name {
($u:literal, $n:literal) => {
let event = generate_event(user_id!($u), $n);
ambiguity_cache
.handle_event(&changes, room_id, &event)
.await
.expect("We should be able to handle a member event to calculate the ambiguity.");
};
}
macro_rules! assert_display_name_ambiguity {
($n:literal, $a:expr) => {
let display_name = DisplayName::new($n);
if ambiguity_cache.check(room_id, &display_name) != $a {
let foo = if $a { "be" } else { "not be" };
panic!("{}: the display name {} should {} ambiguous", $description, $n, foo);
}
};
}
$(
add_display_name!($user, $display_name);
)*
$(
assert_display_name_ambiguity!($check_display_name, $ambiguous);
)*
};
}
#[async_test]
async fn test_disambiguation() {
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("alice", false)],
"Alice is alone in the room"
);
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("Alice", false)],
"Alice is alone in the room and has a capitalized display name"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "alice")],
[("alice", true)],
"Alice and bob share a display name"
);
assert_ambiguity!(
[
("@alice:localhost", "alice"),
("@bob:localhost", "alice"),
("@carol:localhost", "carol")
],
[("alice", true), ("carol", false)],
"Alice and Bob share a display name, while Carol is unique"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")],
[("alice", true)],
"Alice and Bob share a display name that is differently capitalized"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
[("alice", true)],
"Bob tries to impersonate Alice using a cyrilic а"
);
assert_ambiguity!(
[("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")],
[("@bob:localhost", true)],
"Alice tries to impersonate bob using an mxid"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using scripture symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using fraktur symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using circled symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using squared symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using big unicode letters"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using left to right shenanigans"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a diacritical mark"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")],
[("ff", true)],
"Bob tries to impersonate Alice using a ligature"
);
}
}
@@ -1,6 +1,6 @@
//! Trait and macro of integration tests for StateStore implementations.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
@@ -33,10 +33,13 @@ use ruma::{
};
use serde_json::{json, value::Value as JsonValue};
use super::{DependentQueuedEventKind, DynStateStore, ServerCapabilities};
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
ServerCapabilities,
};
use crate::{
deserialized_responses::MemberEvent,
store::{traits::ChildTransactionId, Result, SerializableEventContent, StateStoreExt},
store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt},
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
};
@@ -83,6 +86,8 @@ pub trait StateStoreIntegrationTests {
async fn test_display_names_saving(&self);
/// Test operations with the send queue.
async fn test_send_queue(&self);
/// Test priority of operations with the send queue.
async fn test_send_queue_priority(&self);
/// Test operations related to send queue dependents.
async fn test_send_queue_dependents(&self);
/// Test saving/restoring server capabilities.
@@ -137,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore {
room.handle_state_event(&topic_event);
changes.add_state_event(room_id, topic_event, topic_raw);
let mut room_ambiguity_map = BTreeMap::new();
let mut room_ambiguity_map = HashMap::new();
let mut room_profiles = BTreeMap::new();
let member_json: &JsonValue = &test_json::MEMBER;
let member_event: SyncRoomMemberEvent =
serde_json::from_value(member_json.clone()).unwrap();
let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap();
let displayname = DisplayName::new(
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
);
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
room_profiles.insert(user_id.to_owned(), (&member_event).into());
@@ -252,6 +259,8 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_populate_store(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
self.populate().await?;
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
@@ -286,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"Expected to find 1 joined user ids"
);
assert_eq!(
self.get_users_with_display_name(room_id, "example").await?.len(),
self.get_users_with_display_name(room_id, &display_name).await?.len(),
2,
"Expected to find 2 display names for room"
);
@@ -958,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_room_removal(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
let stripped_room_id = stripped_room_id();
self.populate().await?;
@@ -986,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"still joined users found"
);
assert!(
self.get_users_with_display_name(room_id, "example").await?.is_empty(),
self.get_users_with_display_name(room_id, &display_name).await?.is_empty(),
"still display names found"
);
assert!(self
@@ -1141,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_display_names_saving(&self) {
let room_id = room_id!("!test_display_names_saving:localhost");
let user_id = user_id();
let user_display_name = "User";
let user_display_name = DisplayName::new("User");
let second_user_id = user_id!("@second:localhost");
let third_user_id = user_id!("@third:localhost");
let other_display_name = "Raoul";
let unknown_display_name = "Unknown";
let other_display_name = DisplayName::new("Raoul");
let unknown_display_name = DisplayName::new("Unknown");
// No event in store.
let mut display_names = vec![user_display_name.to_owned()];
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert!(users.is_empty());
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert!(names.is_empty());
@@ -1163,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore {
.insert(user_display_name.to_owned(), [user_id.to_owned()].into());
self.save_changes(&changes).await.unwrap();
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 1);
@@ -1178,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore {
self.save_changes(&changes).await.unwrap();
display_names.push(other_display_name.to_owned());
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap();
assert_eq!(users.len(), 2);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 2);
@@ -1202,7 +1212,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let room_id = room_id!("!test_send_queue:localhost");
// No queued event in store at first.
let events = self.load_send_queue_events(room_id).await.unwrap();
let events = self.load_send_queue_requests(room_id).await.unwrap();
assert!(events.is_empty());
// Saving one thing should work.
@@ -1210,20 +1220,20 @@ impl StateStoreIntegrationTests for DynStateStore {
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
.unwrap();
self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// Reading it will work.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 1);
{
assert_eq!(pending[0].transaction_id, txn0);
let deserialized = pending[0].event.deserialize().unwrap();
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "msg0");
assert!(!pending[0].is_wedged);
assert!(!pending[0].is_wedged());
}
// Saving another three things should work.
@@ -1234,11 +1244,11 @@ impl StateStoreIntegrationTests for DynStateStore {
)
.unwrap();
self.save_send_queue_event(room_id, txn, event).await.unwrap();
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
}
// Reading all the events should work.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
// All the events should be retrieved, in the same order.
assert_eq!(pending.len(), 4);
@@ -1246,27 +1256,36 @@ impl StateStoreIntegrationTests for DynStateStore {
assert_eq!(pending[0].transaction_id, txn0);
for i in 0..4 {
let deserialized = pending[i].event.deserialize().unwrap();
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), format!("msg{i}"));
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
// Marking an event as wedged works.
let txn2 = &pending[2].transaction_id;
self.update_send_queue_event_status(room_id, txn2, true).await.unwrap();
self.update_send_queue_request_status(
room_id,
txn2,
Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }),
)
.await
.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
// All the events should be retrieved, in the same order.
assert_eq!(pending.len(), 4);
assert_eq!(pending[0].transaction_id, txn0);
assert_eq!(pending[2].transaction_id, *txn2);
assert!(pending[2].is_wedged);
assert!(pending[2].is_wedged());
let error = pending[2].clone().error.unwrap();
let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg);
assert_eq!(generic_error, "Oops");
for i in 0..4 {
if i != 2 {
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
}
@@ -1275,37 +1294,37 @@ impl StateStoreIntegrationTests for DynStateStore {
&RoomMessageEventContent::text_plain("wow that's a cool test").into(),
)
.unwrap();
self.update_send_queue_event(room_id, txn2, event0).await.unwrap();
self.update_send_queue_request(room_id, txn2, event0.into()).await.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 4);
{
assert_eq!(pending[2].transaction_id, *txn2);
let deserialized = pending[2].event.deserialize().unwrap();
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "wow that's a cool test");
assert!(!pending[2].is_wedged);
assert!(!pending[2].is_wedged());
for i in 0..4 {
if i != 2 {
let deserialized = pending[i].event.deserialize().unwrap();
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), format!("msg{i}"));
assert!(!pending[i].is_wedged);
assert!(!pending[i].is_wedged());
}
}
}
// Removing an event works.
self.remove_send_queue_event(room_id, &txn0).await.unwrap();
self.remove_send_queue_request(room_id, &txn0).await.unwrap();
// And it is reflected.
let pending = self.load_send_queue_events(room_id).await.unwrap();
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 3);
assert_eq!(pending[1].transaction_id, *txn2);
@@ -1323,7 +1342,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
.unwrap();
self.save_send_queue_event(room_id2, txn.clone(), event).await.unwrap();
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
}
// Add and remove one event for room3.
@@ -1333,19 +1352,77 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
.unwrap();
self.save_send_queue_event(room_id3, txn.clone(), event).await.unwrap();
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
self.remove_send_queue_event(room_id3, &txn).await.unwrap();
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
}
// Query all the rooms which have unsent events. Per the previous steps,
// it should be room1 and room2, not room3.
let outstanding_rooms = self.load_rooms_with_unsent_events().await.unwrap();
let outstanding_rooms = self.load_rooms_with_unsent_requests().await.unwrap();
assert_eq!(outstanding_rooms.len(), 2);
assert!(outstanding_rooms.iter().any(|room| room == room_id));
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
}
async fn test_send_queue_priority(&self) {
let room_id = room_id!("!test_send_queue:localhost");
// No queued event in store at first.
let events = self.load_send_queue_requests(room_id).await.unwrap();
assert!(events.is_empty());
// Saving one request should work.
let low0_txn = TransactionId::new();
let ev0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
.unwrap();
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
// Saving one request with higher priority should work.
let high_txn = TransactionId::new();
let ev1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
.unwrap();
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
// Saving another request with the low priority should work.
let low1_txn = TransactionId::new();
let ev2 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
.unwrap();
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
// The requests should be ordered from higher priority to lower, and when equal,
// should use the insertion order instead.
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 3);
{
assert_eq!(pending[0].transaction_id, high_txn);
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "high");
}
{
assert_eq!(pending[1].transaction_id, low0_txn);
let deserialized = pending[1].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low0");
}
{
assert_eq!(pending[2].transaction_id, low1_txn);
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low1");
}
}
async fn test_send_queue_dependents(&self) {
let room_id = room_id!("!test_send_queue_dependents:localhost");
@@ -1354,53 +1431,61 @@ impl StateStoreIntegrationTests for DynStateStore {
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
.unwrap();
self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// No dependents, to start with.
assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty());
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
// Save a redaction for that event.
let child_txn = ChildTransactionId::new();
self.save_dependent_send_queue_event(
self.save_dependent_queued_request(
room_id,
&txn0,
child_txn.clone(),
DependentQueuedEventKind::Redact,
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
// It worked.
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn0);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].event_id.is_none());
assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact);
assert!(dependents[0].parent_key.is_none());
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Update the event id.
let event_id = owned_event_id!("$1");
let num_updated =
self.update_dependent_send_queue_event(room_id, &txn0, event_id.clone()).await.unwrap();
let num_updated = self
.update_dependent_queued_request(
room_id,
&txn0,
SentRequestKey::Event(event_id.clone()),
)
.await
.unwrap();
assert_eq!(num_updated, 1);
// It worked.
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn0);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert_eq!(dependents[0].event_id.as_ref(), Some(&event_id));
assert_matches!(dependents[0].kind, DependentQueuedEventKind::Redact);
assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => {
assert_eq!(*eid, event_id);
});
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Now remove it.
let removed = self
.remove_dependent_send_queue_event(room_id, &dependents[0].own_transaction_id)
.remove_dependent_queued_request(room_id, &dependents[0].own_transaction_id)
.await
.unwrap();
assert!(removed);
// It worked.
assert!(self.list_dependent_send_queue_events(room_id).await.unwrap().is_empty());
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
// Now, inserting a dependent event and removing the original send queue event
// will NOT remove the dependent event.
@@ -1408,23 +1493,23 @@ impl StateStoreIntegrationTests for DynStateStore {
let event1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
.unwrap();
self.save_send_queue_event(room_id, txn1.clone(), event1).await.unwrap();
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
self.save_dependent_send_queue_event(
self.save_dependent_queued_request(
room_id,
&txn0,
ChildTransactionId::new(),
DependentQueuedEventKind::Redact,
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 1);
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 1);
self.save_dependent_send_queue_event(
self.save_dependent_queued_request(
room_id,
&txn1,
ChildTransactionId::new(),
DependentQueuedEventKind::Edit {
DependentQueuedRequestKind::EditEvent {
new_content: SerializableEventContent::new(
&RoomMessageEventContent::text_plain("edit").into(),
)
@@ -1433,14 +1518,14 @@ impl StateStoreIntegrationTests for DynStateStore {
)
.await
.unwrap();
assert_eq!(self.list_dependent_send_queue_events(room_id).await.unwrap().len(), 2);
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 2);
// Remove event0 / txn0.
let removed = self.remove_send_queue_event(room_id, &txn0).await.unwrap();
let removed = self.remove_send_queue_request(room_id, &txn0).await.unwrap();
assert!(removed);
// This has removed none of the dependent events.
let dependents = self.list_dependent_send_queue_events(room_id).await.unwrap();
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 2);
}
}
@@ -1590,6 +1675,12 @@ macro_rules! statestore_integration_tests {
store.test_send_queue().await;
}
#[async_test]
async fn test_send_queue_priority() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_priority().await;
}
#[async_test]
async fn test_send_queue_dependents() {
let store = get_store().await.expect("creating store failed").into_state_store();
@@ -36,16 +36,15 @@ use ruma::{
use tracing::{debug, instrument, trace, warn};
use super::{
traits::{
ChildTransactionId, ComposerDraft, QueuedEvent, SerializableEventContent,
ServerCapabilities,
},
DependentQueuedEvent, DependentQueuedEventKind, Result, RoomInfo, StateChanges, StateStore,
StoreError,
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
traits::{ComposerDraft, ServerCapabilities},
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
StateChanges, StateStore, StoreError,
};
use crate::{
deserialized_responses::RawAnySyncOrStrippedState, MinimalRoomMemberEvent, RoomMemberships,
StateStoreDataKey, StateStoreDataValue,
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
store::QueueWedgeError,
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
};
/// In-memory, non-persistent implementation of the `StateStore`.
@@ -63,7 +62,7 @@ pub struct MemoryStore {
utd_hook_manager_data: StdRwLock<Option<GrowableBloom>>,
account_data: StdRwLock<HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
profiles: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<String, BTreeSet<OwnedUserId>>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>>,
members: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MembershipState>>>,
room_info: StdRwLock<HashMap<OwnedRoomId, RoomInfo>>,
room_state: StdRwLock<
@@ -90,8 +89,8 @@ pub struct MemoryStore {
>,
>,
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedEvent>>>,
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedEvent>>>,
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedRequest>>>,
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>>,
}
impl MemoryStore {
@@ -703,7 +702,7 @@ impl StateStore for MemoryStore {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>> {
Ok(self
.display_names
@@ -717,21 +716,18 @@ impl StateStore for MemoryStore {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
if display_names.is_empty() {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
}
let read_guard = &self.display_names.read().unwrap();
let Some(room_names) = read_guard.get(room_id) else {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
};
Ok(display_names
.iter()
.filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone())))
.collect())
Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect())
}
async fn get_account_data_event(
@@ -804,26 +800,27 @@ impl StateStore for MemoryStore {
Ok(())
}
async fn save_send_queue_event(
async fn save_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: OwnedTransactionId,
event: SerializableEventContent,
kind: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
self.send_queue_events
.write()
.unwrap()
.entry(room_id.to_owned())
.or_default()
.push(QueuedEvent { event, transaction_id, is_wedged: false });
.push(QueuedRequest { kind, transaction_id, error: None, priority });
Ok(())
}
async fn update_send_queue_event(
async fn update_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
content: SerializableEventContent,
kind: QueuedRequestKind,
) -> Result<bool, Self::Error> {
if let Some(entry) = self
.send_queue_events
@@ -834,15 +831,15 @@ impl StateStore for MemoryStore {
.iter_mut()
.find(|item| item.transaction_id == transaction_id)
{
entry.event = content;
entry.is_wedged = false;
entry.kind = kind;
entry.error = None;
Ok(true)
} else {
Ok(false)
}
}
async fn remove_send_queue_event(
async fn remove_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
@@ -865,18 +862,22 @@ impl StateStore for MemoryStore {
Ok(false)
}
async fn load_send_queue_events(
async fn load_send_queue_requests(
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedEvent>, Self::Error> {
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
) -> Result<Vec<QueuedRequest>, Self::Error> {
let mut ret =
self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone();
// Inverted order of priority, use stable sort to keep insertion order.
ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority));
Ok(ret)
}
async fn update_send_queue_event_status(
async fn update_send_queue_request_status(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
wedged: bool,
error: Option<QueueWedgeError>,
) -> Result<(), Self::Error> {
if let Some(entry) = self
.send_queue_events
@@ -887,50 +888,50 @@ impl StateStore for MemoryStore {
.iter_mut()
.find(|item| item.transaction_id == transaction_id)
{
entry.is_wedged = wedged;
entry.error = error;
}
Ok(())
}
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
Ok(self.send_queue_events.read().unwrap().keys().cloned().collect())
}
async fn save_dependent_send_queue_event(
async fn save_dependent_queued_request(
&self,
room: &RoomId,
parent_transaction_id: &TransactionId,
own_transaction_id: ChildTransactionId,
content: DependentQueuedEventKind,
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error> {
self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push(
DependentQueuedEvent {
DependentQueuedRequest {
kind: content,
parent_transaction_id: parent_transaction_id.to_owned(),
own_transaction_id,
event_id: None,
parent_key: None,
},
);
Ok(())
}
async fn update_dependent_send_queue_event(
async fn update_dependent_queued_request(
&self,
room: &RoomId,
parent_txn_id: &TransactionId,
event_id: OwnedEventId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error> {
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
let mut num_updated = 0;
for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) {
d.event_id = Some(event_id.clone());
d.parent_key = Some(sent_parent_key.clone());
num_updated += 1;
}
Ok(num_updated)
}
async fn remove_dependent_send_queue_event(
async fn remove_dependent_queued_request(
&self,
room: &RoomId,
txn_id: &ChildTransactionId,
@@ -949,10 +950,10 @@ impl StateStore for MemoryStore {
///
/// This returns absolutely all the dependent send queue events, whether
/// they have an event id or not.
async fn list_dependent_send_queue_events(
async fn load_dependent_queued_requests(
&self,
room: &RoomId,
) -> Result<Vec<DependentQueuedEvent>, Self::Error> {
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default())
}
}
@@ -114,6 +114,7 @@ impl RoomInfoV1 {
version: 0,
room_id,
room_state: room_type,
prev_room_state: None,
notification_counts,
summary,
members_synced,
+44 -23
View File
@@ -21,7 +21,7 @@
//! store.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt,
ops::Deref,
result::Result as StdResult,
@@ -29,9 +29,7 @@ use std::{
sync::{Arc, RwLock as StdRwLock},
};
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::{Vector, VectorDiff};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::Stream;
use once_cell::sync::OnceCell;
@@ -60,7 +58,8 @@ use tokio::sync::{broadcast, Mutex, RwLock};
use tracing::warn;
use crate::{
event_cache_store::{DynEventCacheStore, IntoEventCacheStore},
deserialized_responses::DisplayName,
event_cache::store as event_cache_store,
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
};
@@ -68,16 +67,20 @@ use crate::{
pub(crate) mod ambiguity_map;
mod memory_store;
pub mod migration_helpers;
mod send_queue;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::StateStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
send_queue::{
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind,
SentMediaInfo, SentRequestKey, SerializableEventContent,
},
traits::{
ChildTransactionId, ComposerDraft, ComposerDraftType, DependentQueuedEvent,
DependentQueuedEventKind, DynStateStore, IntoStateStore, QueuedEvent,
SerializableEventContent, ServerCapabilities, StateStore, StateStoreDataKey,
StateStoreDataValue, StateStoreExt,
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
},
};
@@ -263,7 +266,6 @@ impl Store {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
#[cfg(not(target_arch = "wasm32"))]
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
self.rooms.read().unwrap().stream()
}
@@ -304,6 +306,17 @@ impl Store {
})
.clone()
}
/// Forget the room with the given room ID.
///
/// # Arguments
///
/// * `room_id` - The id of the room that should be forgotten.
pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
self.inner.remove_room(room_id).await?;
self.rooms.write().unwrap().remove(room_id);
Ok(())
}
}
#[cfg(not(tarpaulin_include))]
@@ -372,7 +385,7 @@ pub struct StateChanges {
/// A map from room id to a map of a display name and a set of user ids that
/// share that display name in the given room.
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
}
impl StateChanges {
@@ -468,14 +481,16 @@ impl StateChanges {
/// ```
/// # use matrix_sdk_base::store::StoreConfig;
///
/// let store_config = StoreConfig::new();
/// let store_config =
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
/// ```
#[derive(Clone)]
pub struct StoreConfig {
#[cfg(feature = "e2e-encryption")]
pub(crate) crypto_store: Arc<DynCryptoStore>,
pub(crate) state_store: Arc<DynStateStore>,
pub(crate) event_cache_store: Arc<DynEventCacheStore>,
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
cross_process_store_locks_holder_name: String,
}
#[cfg(not(tarpaulin_include))]
@@ -487,14 +502,20 @@ impl fmt::Debug for StoreConfig {
impl StoreConfig {
/// Create a new default `StoreConfig`.
///
/// To learn more about `cross_process_store_locks_holder_name`, please read
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
#[must_use]
pub fn new() -> Self {
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
Self {
#[cfg(feature = "e2e-encryption")]
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
state_store: Arc::new(MemoryStore::new()),
event_cache_store: crate::event_cache_store::MemoryStore::new()
.into_event_cache_store(),
event_cache_store: event_cache_store::EventCacheStoreLock::new(
event_cache_store::MemoryStore::new(),
cross_process_store_locks_holder_name.clone(),
),
cross_process_store_locks_holder_name,
}
}
@@ -514,14 +535,14 @@ impl StoreConfig {
}
/// Set a custom implementation of an `EventCacheStore`.
pub fn event_cache_store(mut self, event_cache_store: impl IntoEventCacheStore) -> Self {
self.event_cache_store = event_cache_store.into_event_cache_store();
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
where
S: event_cache_store::IntoEventCacheStore,
{
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
event_cache_store,
self.cross_process_store_locks_holder_name.clone(),
);
self
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::new()
}
}
+140 -160
View File
@@ -14,189 +14,137 @@
//! An [`ObservableMap`] implementation.
#[cfg(not(target_arch = "wasm32"))]
mod impl_non_wasm32 {
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
use eyeball_im::{ObservableVector, Vector, VectorDiff};
use futures_util::Stream;
use eyeball_im::{ObservableVector, Vector, VectorDiff};
use futures_util::Stream;
/// An observable map.
///
/// This is an “observable map” naive implementation. Just like regular
/// hashmap, we have a redirection from a key to a position, and from a
/// position to a value. The (key, position) tuples are stored in an
/// [`HashMap`]. The (position, value) tuples are stored in an
/// [`ObservableVector`]. The (key, position) tuple is only provided for
/// fast _reading_ implementations, like `Self::get` and
/// `Self::get_or_create`. The (position, value) tuples are observable,
/// this is what interests us the most here.
///
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
/// of this custom implementation? Because we want to continue providing
/// `VectorDiff` when observing the changes, so that the rest of the API in
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
/// produce `MapDiff`, which would be quite different.
/// Plus, we would like to re-use all our existing code, test, stream
/// adapters and so on.
///
/// This is a trade-off. This implementation is simple enough for the
/// moment, and basically does the job.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>
where
V: Clone + Send + Sync + 'static,
{
/// The (key, position) tuples.
mapping: HashMap<K, usize>,
/// An observable map.
///
/// This is an “observable map” naive implementation. Just like regular
/// hashmap, we have a redirection from a key to a position, and from a
/// position to a value. The (key, position) tuples are stored in an
/// [`HashMap`]. The (position, value) tuples are stored in an
/// [`ObservableVector`]. The (key, position) tuple is only provided for
/// fast _reading_ implementations, like `Self::get` and
/// `Self::get_or_create`. The (position, value) tuples are observable,
/// this is what interests us the most here.
///
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
/// of this custom implementation? Because we want to continue providing
/// `VectorDiff` when observing the changes, so that the rest of the API in
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
/// produce `MapDiff`, which would be quite different.
/// Plus, we would like to re-use all our existing code, test, stream
/// adapters and so on.
///
/// This is a trade-off. This implementation is simple enough for the
/// moment, and basically does the job.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>
where
V: Clone + 'static,
{
/// The (key, position) tuples.
mapping: HashMap<K, usize>,
/// The values where the indices are the `position` part of
/// `Self::mapping`.
values: ObservableVector<V>,
/// The values where the indices are the `position` part of
/// `Self::mapping`.
values: ObservableVector<V>,
}
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq,
V: Clone + 'static,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self { mapping: HashMap::new(), values: ObservableVector::new() }
}
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq,
V: Clone + Send + Sync + 'static,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self { mapping: HashMap::new(), values: ObservableVector::new() }
}
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
match self.mapping.get(&key) {
Some(position) => {
self.values.set(*position, value);
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
match self.mapping.get(&key) {
Some(position) => {
self.values.set(*position, value);
*position
}
None => {
let position = self.values.len();
*position
}
None => {
let position = self.values.len();
self.values.push_back(value);
self.mapping.insert(key, position);
self.values.push_back(value);
self.mapping.insert(key, position);
position
}
position
}
}
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
self.mapping.get(key).and_then(|position| self.values.get(*position))
}
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
let position = match self.mapping.get(key) {
Some(position) => *position,
None => {
let value = default();
let position = self.values.len();
self.values.push_back(value);
self.mapping.insert(key.to_owned(), position);
position
}
};
self.values
.get(position)
.expect("Value should be present or has just been inserted, but it's missing")
}
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.values.iter()
}
/// Get a [`Stream`] of the values.
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
self.values.subscribe().into_values_and_batched_stream()
}
}
}
#[cfg(target_arch = "wasm32")]
mod impl_wasm32 {
use std::{borrow::Borrow, collections::BTreeMap, hash::Hash};
/// An observable map for Wasm. It's a simple wrapper around `BTreeMap`.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>(BTreeMap<K, V>)
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
V: Clone + 'static;
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq + Ord,
V: Clone + 'static,
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self(BTreeMap::new())
}
self.mapping.get(key).and_then(|position| self.values.get(*position))
}
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) {
self.0.insert(key, value);
}
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
let position = match self.mapping.get(key) {
Some(position) => *position,
None => {
let value = default();
let position = self.values.len();
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
K: Borrow<L>,
L: Hash + Eq + Ord + ?Sized,
{
self.0.get(key)
}
self.values.push_back(value);
self.mapping.insert(key.to_owned(), position);
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
self.0.entry(key.to_owned()).or_insert_with(default)
}
position
}
};
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.0.values()
}
self.values
.get(position)
.expect("Value should be present or has just been inserted, but it's missing")
}
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.values.iter()
}
/// Get a [`Stream`] of the values.
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
self.values.subscribe().into_values_and_batched_stream()
}
/// Remove a `V` value based on their ID, if it exists.
///
/// Returns the removed value.
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
where
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
let position = self.mapping.remove(key)?;
Some(self.values.remove(position))
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use impl_non_wasm32::ObservableMap;
#[cfg(target_arch = "wasm32")]
pub(crate) use impl_wasm32::ObservableMap;
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::VectorDiff;
#[cfg(not(target_arch = "wasm32"))]
use stream_assert::{assert_closed, assert_next_eq, assert_pending};
use super::ObservableMap;
@@ -249,6 +197,33 @@ mod tests {
assert_eq!(map.get(&'c'), Some(&'G'));
}
#[test]
fn test_remove() {
let mut map = ObservableMap::<char, char>::new();
assert!(map.get(&'a').is_none());
assert!(map.get(&'b').is_none());
assert!(map.get(&'c').is_none());
// new items
map.insert('a', 'e');
map.insert('b', 'f');
assert_eq!(map.get(&'a'), Some(&'e'));
assert_eq!(map.get(&'b'), Some(&'f'));
assert!(map.get(&'c').is_none());
// remove one item
assert_eq!(map.remove(&'b'), Some('f'));
assert_eq!(map.get(&'a'), Some(&'e'));
assert_eq!(map.get(&'b'), None);
assert_eq!(map.get(&'c'), None);
// remove a non-existent item
assert_eq!(map.remove(&'c'), None);
}
#[test]
fn test_iter() {
let mut map = ObservableMap::<char, char>::new();
@@ -264,7 +239,6 @@ mod tests {
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_stream() {
let mut map = ObservableMap::<char, char>::new();
@@ -293,6 +267,12 @@ mod tests {
assert_pending!(stream);
// remove one item
map.remove(&'b');
assert_next_eq!(stream, vec![VectorDiff::Remove { index: 0 }]);
assert_pending!(stream);
drop(map);
assert_closed!(stream);
}
@@ -0,0 +1,379 @@
// 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.
//! All data types related to the send queue.
use std::{collections::BTreeMap, fmt, ops::Deref};
use as_variant::as_variant;
use ruma::{
events::{
room::{message::RoomMessageEventContent, MediaSource},
AnyMessageLikeEventContent, EventContent as _, RawExt as _,
},
serde::Raw,
OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt,
};
use serde::{Deserialize, Serialize};
use crate::media::MediaRequestParameters;
/// A thin wrapper to serialize a `AnyMessageLikeEventContent`.
#[derive(Clone, Serialize, Deserialize)]
pub struct SerializableEventContent {
event: Raw<AnyMessageLikeEventContent>,
event_type: String,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for SerializableEventContent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Don't include the event in the debug display.
f.debug_struct("SerializedEventContent")
.field("event_type", &self.event_type)
.finish_non_exhaustive()
}
}
impl SerializableEventContent {
/// Create a [`SerializableEventContent`] from a raw
/// [`AnyMessageLikeEventContent`] along with its type.
pub fn from_raw(event: Raw<AnyMessageLikeEventContent>, event_type: String) -> Self {
Self { event_type, event }
}
/// Create a [`SerializableEventContent`] from an
/// [`AnyMessageLikeEventContent`].
pub fn new(event: &AnyMessageLikeEventContent) -> Result<Self, serde_json::Error> {
Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string()))
}
/// Convert a [`SerializableEventContent`] back into a
/// [`AnyMessageLikeEventContent`].
pub fn deserialize(&self) -> Result<AnyMessageLikeEventContent, serde_json::Error> {
self.event.deserialize_with_type(self.event_type.clone().into())
}
/// Returns the raw event content along with its type.
///
/// Useful for callers manipulating custom events.
pub fn raw(&self) -> (&Raw<AnyMessageLikeEventContent>, &str) {
(&self.event, &self.event_type)
}
}
/// The kind of a send queue request.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum QueuedRequestKind {
/// An event to be sent via the send queue.
Event {
/// The content of the message-like event we'd like to send.
content: SerializableEventContent,
},
/// Content to upload on the media server.
///
/// The bytes must be stored in the media cache, and are identified by the
/// cache key.
MediaUpload {
/// Content type of the media to be uploaded.
///
/// Stored as a `String` because `Mime` which we'd really want to use
/// here, is not serializable. Oh well.
content_type: String,
/// The cache key used to retrieve the media's bytes in the event cache
/// store.
cache_key: MediaRequestParameters,
/// An optional media source for a thumbnail already uploaded.
thumbnail_source: Option<MediaSource>,
/// To which media event transaction does this upload relate?
related_to: OwnedTransactionId,
},
}
impl From<SerializableEventContent> for QueuedRequestKind {
fn from(content: SerializableEventContent) -> Self {
Self::Event { content }
}
}
/// A request to be sent with a send queue.
#[derive(Clone)]
pub struct QueuedRequest {
/// The kind of queued request we're going to send.
pub kind: QueuedRequestKind,
/// Unique transaction id for the queued request, acting as a key.
pub transaction_id: OwnedTransactionId,
/// Error returned when the request couldn't be sent and is stuck in the
/// unrecoverable state.
///
/// `None` if the request is in the queue, waiting to be sent.
pub error: Option<QueueWedgeError>,
/// At which priority should this be handled?
///
/// The bigger the value, the higher the priority at which this request
/// should be handled.
pub priority: usize,
}
impl QueuedRequest {
/// Returns `Some` if the queued request is about sending an event.
pub fn as_event(&self) -> Option<&SerializableEventContent> {
as_variant!(&self.kind, QueuedRequestKind::Event { content } => content)
}
/// True if the request couldn't be sent because of an unrecoverable API
/// error. See [`Self::error`] for more details on the reason.
pub fn is_wedged(&self) -> bool {
self.error.is_some()
}
}
/// Represents 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(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum QueueWedgeError {
/// This error occurs when there are some insecure devices in the room, and
/// the current encryption setting prohibits sharing with them.
#[error("There are insecure devices in the room")]
InsecureDevices {
/// The insecure devices as a Map of userID to deviceID.
user_device_map: BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
},
/// This error occurs when a previously verified user is not anymore, and
/// the current encryption setting prohibits sharing when it happens.
#[error("Some users that were previously verified are not anymore")]
IdentityViolations {
/// The users that are expected to be verified but are not.
users: Vec<OwnedUserId>,
},
/// It is required to set up cross-signing and properly verify the current
/// session before sending.
#[error("Own verification is required")]
CrossVerificationRequired,
/// Media content was cached in the media store, but has disappeared before
/// we could upload it.
#[error("Media content disappeared")]
MissingMediaContent,
/// We tried to upload some media content with an unknown mime type.
#[error("Invalid mime type '{mime_type}' for media")]
InvalidMimeType {
/// The observed mime type that's expected to be invalid.
mime_type: String,
},
/// Other errors.
#[error("Other unrecoverable error: {msg}")]
GenericApiError {
/// Description of the error.
msg: String,
},
}
/// The specific user intent that characterizes a
/// [`DependentQueuedRequestKind`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DependentQueuedRequestKind {
/// The event should be edited.
EditEvent {
/// The new event for the content.
new_content: SerializableEventContent,
},
/// The event should be redacted/aborted/removed.
RedactEvent,
/// The event should be reacted to, with the given key.
ReactEvent {
/// Key used for the reaction.
key: String,
},
/// Upload a file that had a thumbnail.
UploadFileWithThumbnail {
/// Content type for the file itself (not the thumbnail).
content_type: String,
/// Media request necessary to retrieve the file itself (not the
/// thumbnail).
cache_key: MediaRequestParameters,
/// To which media transaction id does this upload relate to?
related_to: OwnedTransactionId,
},
/// Finish an upload by updating references to the media cache and sending
/// the final media event with the remote MXC URIs.
FinishUpload {
/// Local echo for the event (containing the local MXC URIs).
local_echo: RoomMessageEventContent,
/// Transaction id for the file upload.
file_upload: OwnedTransactionId,
/// Information about the thumbnail, if present.
thumbnail_info: Option<FinishUploadThumbnailInfo>,
},
}
/// Detailed record about a thumbnail used when finishing a media upload.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FinishUploadThumbnailInfo {
/// Transaction id for the thumbnail upload.
pub txn: OwnedTransactionId,
/// Thumbnail's width.
pub width: UInt,
/// Thumbnail's height.
pub height: UInt,
}
/// A transaction id identifying a [`DependentQueuedRequest`] rather than its
/// parent [`QueuedRequest`].
///
/// This thin wrapper adds some safety to some APIs, making it possible to
/// distinguish between the parent's `TransactionId` and the dependent event's
/// own `TransactionId`.
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ChildTransactionId(OwnedTransactionId);
impl ChildTransactionId {
/// Returns a new [`ChildTransactionId`].
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(TransactionId::new())
}
}
impl Deref for ChildTransactionId {
type Target = TransactionId;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for ChildTransactionId {
fn from(val: String) -> Self {
Self(val.into())
}
}
impl From<ChildTransactionId> for OwnedTransactionId {
fn from(val: ChildTransactionId) -> Self {
val.0
}
}
impl From<OwnedTransactionId> for ChildTransactionId {
fn from(val: OwnedTransactionId) -> Self {
Self(val)
}
}
/// Information about a media (and its thumbnail) that have been sent to an
/// homeserver.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SentMediaInfo {
/// File that was uploaded by this request.
///
/// If the request related to a thumbnail upload, this contains the
/// thumbnail media source.
pub file: MediaSource,
/// Optional thumbnail previously uploaded, when uploading a file.
///
/// When uploading a thumbnail, this is set to `None`.
pub thumbnail: Option<MediaSource>,
}
/// A unique key (identifier) indicating that a transaction has been
/// successfully sent to the server.
///
/// The owning child transactions can now be resolved.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SentRequestKey {
/// The parent transaction returned an event when it succeeded.
Event(OwnedEventId),
/// The parent transaction returned an uploaded resource URL.
Media(SentMediaInfo),
}
impl SentRequestKey {
/// Converts the current parent key into an event id, if possible.
pub fn into_event_id(self) -> Option<OwnedEventId> {
as_variant!(self, Self::Event)
}
/// Converts the current parent key into information about a sent media, if
/// possible.
pub fn into_media(self) -> Option<SentMediaInfo> {
as_variant!(self, Self::Media)
}
}
/// A request to be sent, depending on a [`QueuedRequest`] to be sent first.
///
/// Depending on whether the parent request has been sent or not, this will
/// either update the local echo in the storage, or materialize an equivalent
/// request implementing the user intent to the homeserver.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DependentQueuedRequest {
/// Unique identifier for this dependent queued request.
///
/// Useful for deletion.
pub own_transaction_id: ChildTransactionId,
/// The kind of user intent.
pub kind: DependentQueuedRequestKind,
/// Transaction id for the parent's local echo / used in the server request.
///
/// Note: this is the transaction id used for the depended-on request, i.e.
/// the one that was originally sent and that's being modified with this
/// dependent request.
pub parent_transaction_id: OwnedTransactionId,
/// If the parent request has been sent, the parent's request identifier
/// returned by the server once the local echo has been sent out.
pub parent_key: Option<SentRequestKey>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for QueuedRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Hide the content from the debug log.
f.debug_struct("QueuedRequest")
.field("transaction_id", &self.transaction_id)
.field("is_wedged", &self.is_wedged())
.finish_non_exhaustive()
}
}
+123 -260
View File
@@ -14,9 +14,8 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt,
ops::Deref,
sync::Arc,
};
@@ -29,11 +28,10 @@ use ruma::{
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptThread, ReceiptType},
AnyGlobalAccountDataEvent, AnyMessageLikeEventContent, AnyRoomAccountDataEvent,
EmptyStateKey, EventContent as _, GlobalAccountDataEvent, GlobalAccountDataEventContent,
GlobalAccountDataEventType, RawExt as _, RedactContent, RedactedStateEventContent,
RoomAccountDataEvent, RoomAccountDataEventContent, RoomAccountDataEventType,
StateEventType, StaticEventContent, StaticStateEventContent,
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, EmptyStateKey, GlobalAccountDataEvent,
GlobalAccountDataEventContent, GlobalAccountDataEventType, RedactContent,
RedactedStateEventContent, RoomAccountDataEvent, RoomAccountDataEventContent,
RoomAccountDataEventType, StateEventType, StaticEventContent, StaticStateEventContent,
},
serde::Raw,
time::SystemTime,
@@ -42,9 +40,15 @@ use ruma::{
};
use serde::{Deserialize, Serialize};
use super::{StateChanges, StoreError};
use super::{
send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest,
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, StateChanges,
StoreError,
};
use crate::{
deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState},
deserialized_responses::{
DisplayName, RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState,
},
MinimalRoomMemberEvent, RoomInfo, RoomMemberships,
};
@@ -204,7 +208,7 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>, Self::Error>;
/// Get all the users that use the given display names in the given room.
@@ -217,8 +221,8 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error>;
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error>;
/// Get an event out of the account data store.
///
@@ -342,7 +346,7 @@ pub trait StateStore: AsyncTraitDeps {
/// * `room_id` - The `RoomId` of the room to delete.
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error>;
/// Save an event to be sent by a send queue later.
/// Save a request to be sent by a send queue later (e.g. sending an event).
///
/// # Arguments
///
@@ -351,95 +355,109 @@ pub trait StateStore: AsyncTraitDeps {
/// (and its transaction). Note: this is expected to be randomly generated
/// and thus unique.
/// * `content` - Serializable event content to be sent.
async fn save_send_queue_event(
async fn save_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: OwnedTransactionId,
content: SerializableEventContent,
request: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error>;
/// Updates a send queue event with the given content, and resets its wedged
/// status to false.
/// Updates a send queue request with the given content, and resets its
/// error status.
///
/// # Arguments
///
/// * `room_id` - The `RoomId` of the send queue's room.
/// * `transaction_id` - The unique key identifying the event to be sent
/// * `transaction_id` - The unique key identifying the request to be sent
/// (and its transaction).
/// * `content` - Serializable event content to replace the original one.
///
/// Returns true if an event has been updated, or false otherwise.
async fn update_send_queue_event(
/// Returns true if a request has been updated, or false otherwise.
async fn update_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
content: SerializableEventContent,
content: QueuedRequestKind,
) -> Result<bool, Self::Error>;
/// Remove an event previously inserted with [`Self::save_send_queue_event`]
/// from the database, based on its transaction id.
/// Remove a request previously inserted with
/// [`Self::save_send_queue_request`] from the database, based on its
/// transaction id.
///
/// Returns true if an event has been removed, or false otherwise.
async fn remove_send_queue_event(
/// Returns true if something has been removed, or false otherwise.
async fn remove_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
) -> Result<bool, Self::Error>;
/// Loads all the send queue events for the given room.
async fn load_send_queue_events(
/// Loads all the send queue requests for the given room.
///
/// The resulting vector of queued requests should be ordered from higher
/// priority to lower priority, and respect the insertion order when
/// priorities are equal.
async fn load_send_queue_requests(
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedEvent>, Self::Error>;
) -> Result<Vec<QueuedRequest>, Self::Error>;
/// Updates the send queue wedged status for a given send queue event.
async fn update_send_queue_event_status(
/// Updates the send queue error status (wedge) for a given send queue
/// request.
async fn update_send_queue_request_status(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
wedged: bool,
error: Option<QueueWedgeError>,
) -> Result<(), Self::Error>;
/// Loads all the rooms which have any pending events in their send queue.
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error>;
/// Loads all the rooms which have any pending requests in their send queue.
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error>;
/// Add a new entry to the list of dependent send queue event for an event.
async fn save_dependent_send_queue_event(
/// Add a new entry to the list of dependent send queue requests for a
/// parent request.
async fn save_dependent_queued_request(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
own_txn_id: ChildTransactionId,
content: DependentQueuedEventKind,
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error>;
/// Update a set of dependent send queue events with an event id,
/// effectively marking them as ready.
/// Update a set of dependent send queue requests with a key identifying the
/// homeserver's response, effectively marking them as ready.
///
/// Returns the number of updated events.
async fn update_dependent_send_queue_event(
/// ⚠ Beware! There's no verification applied that the parent key type is
/// compatible with the dependent event type. The invalid state may be
/// lazily filtered out in `load_dependent_queued_requests`.
///
/// Returns the number of updated requests.
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
event_id: OwnedEventId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error>;
/// Remove a specific dependent send queue event by id.
/// Remove a specific dependent send queue request by id.
///
/// Returns true if the dependent send queue event has been indeed removed.
async fn remove_dependent_send_queue_event(
/// Returns true if the dependent send queue request has been indeed
/// removed.
async fn remove_dependent_queued_request(
&self,
room: &RoomId,
own_txn_id: &ChildTransactionId,
) -> Result<bool, Self::Error>;
/// List all the dependent send queue events.
/// List all the dependent send queue requests.
///
/// This returns absolutely all the dependent send queue events, whether
/// they have an event id or not. They must be returned in insertion order.
async fn list_dependent_send_queue_events(
/// This returns absolutely all the dependent send queue requests, whether
/// they have a parent event id or not. As a contract for implementors, they
/// must be returned in insertion order.
async fn load_dependent_queued_requests(
&self,
room: &RoomId,
) -> Result<Vec<DependentQueuedEvent>, Self::Error>;
) -> Result<Vec<DependentQueuedRequest>, Self::Error>;
}
#[repr(transparent)]
@@ -551,7 +569,7 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>, Self::Error> {
self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into)
}
@@ -559,8 +577,8 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error> {
self.0.get_users_with_display_names(room_id, display_names).await.map_err(Into::into)
}
@@ -625,93 +643,97 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
self.0.remove_room(room_id).await.map_err(Into::into)
}
async fn save_send_queue_event(
async fn save_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: OwnedTransactionId,
content: SerializableEventContent,
) -> Result<(), Self::Error> {
self.0.save_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into)
}
async fn update_send_queue_event(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
content: SerializableEventContent,
) -> Result<bool, Self::Error> {
self.0.update_send_queue_event(room_id, transaction_id, content).await.map_err(Into::into)
}
async fn remove_send_queue_event(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
) -> Result<bool, Self::Error> {
self.0.remove_send_queue_event(room_id, transaction_id).await.map_err(Into::into)
}
async fn load_send_queue_events(
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedEvent>, Self::Error> {
self.0.load_send_queue_events(room_id).await.map_err(Into::into)
}
async fn update_send_queue_event_status(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
wedged: bool,
content: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
self.0
.update_send_queue_event_status(room_id, transaction_id, wedged)
.save_send_queue_request(room_id, transaction_id, content, priority)
.await
.map_err(Into::into)
}
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
self.0.load_rooms_with_unsent_events().await.map_err(Into::into)
async fn update_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
content: QueuedRequestKind,
) -> Result<bool, Self::Error> {
self.0.update_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into)
}
async fn save_dependent_send_queue_event(
async fn remove_send_queue_request(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
) -> Result<bool, Self::Error> {
self.0.remove_send_queue_request(room_id, transaction_id).await.map_err(Into::into)
}
async fn load_send_queue_requests(
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedRequest>, Self::Error> {
self.0.load_send_queue_requests(room_id).await.map_err(Into::into)
}
async fn update_send_queue_request_status(
&self,
room_id: &RoomId,
transaction_id: &TransactionId,
error: Option<QueueWedgeError>,
) -> Result<(), Self::Error> {
self.0
.update_send_queue_request_status(room_id, transaction_id, error)
.await
.map_err(Into::into)
}
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
self.0.load_rooms_with_unsent_requests().await.map_err(Into::into)
}
async fn save_dependent_queued_request(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
own_txn_id: ChildTransactionId,
content: DependentQueuedEventKind,
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error> {
self.0
.save_dependent_send_queue_event(room_id, parent_txn_id, own_txn_id, content)
.save_dependent_queued_request(room_id, parent_txn_id, own_txn_id, content)
.await
.map_err(Into::into)
}
async fn update_dependent_send_queue_event(
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
event_id: OwnedEventId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error> {
self.0
.update_dependent_send_queue_event(room_id, parent_txn_id, event_id)
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
.await
.map_err(Into::into)
}
async fn remove_dependent_send_queue_event(
async fn remove_dependent_queued_request(
&self,
room_id: &RoomId,
own_txn_id: &ChildTransactionId,
) -> Result<bool, Self::Error> {
self.0.remove_dependent_send_queue_event(room_id, own_txn_id).await.map_err(Into::into)
self.0.remove_dependent_queued_request(room_id, own_txn_id).await.map_err(Into::into)
}
async fn list_dependent_send_queue_events(
async fn load_dependent_queued_requests(
&self,
room_id: &RoomId,
) -> Result<Vec<DependentQueuedEvent>, Self::Error> {
self.0.list_dependent_send_queue_events(room_id).await.map_err(Into::into)
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
}
}
@@ -1100,165 +1122,6 @@ impl StateStoreDataKey<'_> {
pub const COMPOSER_DRAFT: &'static str = "composer_draft";
}
/// A thin wrapper to serialize a `AnyMessageLikeEventContent`.
#[derive(Clone, Serialize, Deserialize)]
pub struct SerializableEventContent {
event: Raw<AnyMessageLikeEventContent>,
event_type: String,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for SerializableEventContent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Don't include the event in the debug display.
f.debug_struct("SerializedEventContent")
.field("event_type", &self.event_type)
.finish_non_exhaustive()
}
}
impl SerializableEventContent {
/// Create a [`SerializableEventContent`] from a raw
/// [`AnyMessageLikeEventContent`] along with its type.
pub fn from_raw(event: Raw<AnyMessageLikeEventContent>, event_type: String) -> Self {
Self { event_type, event }
}
/// Create a [`SerializableEventContent`] from an
/// [`AnyMessageLikeEventContent`].
pub fn new(event: &AnyMessageLikeEventContent) -> Result<Self, serde_json::Error> {
Ok(Self::from_raw(Raw::new(event)?, event.event_type().to_string()))
}
/// Convert a [`SerializableEventContent`] back into a
/// [`AnyMessageLikeEventContent`].
pub fn deserialize(&self) -> Result<AnyMessageLikeEventContent, serde_json::Error> {
self.event.deserialize_with_type(self.event_type.clone().into())
}
/// Returns the raw event content along with its type.
///
/// Useful for callers manipulating custom events.
pub fn raw(self) -> (Raw<AnyMessageLikeEventContent>, String) {
(self.event, self.event_type)
}
}
/// An event to be sent with a send queue.
#[derive(Clone)]
pub struct QueuedEvent {
/// The content of the message-like event we'd like to send.
pub event: SerializableEventContent,
/// Unique transaction id for the queued event, acting as a key.
pub transaction_id: OwnedTransactionId,
/// If the event couldn't be sent because of an API error, it's marked as
/// wedged, and won't ever be peeked for sending. The only option is to
/// remove it.
pub is_wedged: bool,
}
/// The specific user intent that characterizes a [`DependentQueuedEvent`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DependentQueuedEventKind {
/// The event should be edited.
Edit {
/// The new event for the content.
new_content: SerializableEventContent,
},
/// The event should be redacted/aborted/removed.
Redact,
/// The event should be reacted to, with the given key.
React {
/// Key used for the reaction.
key: String,
},
}
/// A transaction id identifying a [`DependentQueuedEvent`] rather than its
/// parent [`QueuedEvent`].
///
/// This thin wrapper adds some safety to some APIs, making it possible to
/// distinguish between the parent's `TransactionId` and the dependent event's
/// own `TransactionId`.
#[repr(transparent)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ChildTransactionId(OwnedTransactionId);
impl ChildTransactionId {
/// Returns a new [`ChildTransactionId`].
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(TransactionId::new())
}
}
impl Deref for ChildTransactionId {
type Target = TransactionId;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for ChildTransactionId {
fn from(val: String) -> Self {
Self(val.into())
}
}
impl From<ChildTransactionId> for OwnedTransactionId {
fn from(val: ChildTransactionId) -> Self {
val.0
}
}
/// An event to be sent, depending on a [`QueuedEvent`] to be sent first.
///
/// Depending on whether the event has been sent or not, this will either update
/// the local echo in the storage, or send an event equivalent to the user
/// intent to the homeserver.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DependentQueuedEvent {
/// Unique identifier for this dependent queued event.
///
/// Useful for deletion.
pub own_transaction_id: ChildTransactionId,
/// The kind of user intent.
pub kind: DependentQueuedEventKind,
/// Transaction id for the parent's local echo / used in the server request.
///
/// Note: this is the transaction id used for the depended-on event, i.e.
/// the one that was originally sent and that's being modified with this
/// dependent event.
pub parent_transaction_id: OwnedTransactionId,
/// If the parent event has been sent, the parent's event identifier
/// returned by the server once the local echo has been sent out.
///
/// Note: this is the event id used for the depended-on event after it's
/// been sent, not for a possible event that could have been sent
/// because of this [`DependentQueuedEvent`].
pub event_id: Option<OwnedEventId>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for QueuedEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Hide the content from the debug log.
f.debug_struct("QueuedEvent")
.field("transaction_id", &self.transaction_id)
.field("is_wedged", &self.is_wedged)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::{now_timestamp_ms, ServerCapabilities};
+15 -2
View File
@@ -19,7 +19,7 @@ use std::{collections::BTreeMap, fmt};
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
use ruma::{
api::client::sync::sync_events::{
v3::InvitedRoom as InvitedRoomUpdate,
v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate},
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
events::{
@@ -33,7 +33,7 @@ use ruma::{
use serde::{Deserialize, Serialize};
use crate::{
debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
store::Store,
};
@@ -77,6 +77,8 @@ pub struct RoomUpdates {
pub join: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
/// The rooms that the user has been invited to.
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
/// The rooms that the user has knocked on.
pub knocked: BTreeMap<OwnedRoomId, KnockedRoomUpdate>,
}
impl RoomUpdates {
@@ -89,6 +91,7 @@ impl RoomUpdates {
.keys()
.chain(self.join.keys())
.chain(self.invite.keys())
.chain(self.knocked.keys())
.filter_map(|room_id| store.room(room_id))
{
let _ = room.compute_display_name().await;
@@ -103,6 +106,7 @@ impl fmt::Debug for RoomUpdates {
.field("leave", &self.leave)
.field("join", &self.join)
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
.field("knocked", &DebugKnockedRoomUpdates(&self.knocked))
.finish()
}
}
@@ -250,6 +254,15 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
}
}
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
}
}
/// A notification triggered by a sync response.
#[derive(Clone)]
pub struct Notification {
+4 -2
View File
@@ -18,12 +18,14 @@
use ruma::{owned_user_id, UserId};
use crate::{BaseClient, SessionMeta};
use crate::{store::StoreConfig, BaseClient, SessionMeta};
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
/// one otherwise.
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let user_id =
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
client
+11
View File
@@ -0,0 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Refactor
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
+4 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.8.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -21,7 +21,10 @@ uniffi = ["dep:uniffi"]
[dependencies]
async-trait = { workspace = true }
eyeball-im = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
imbl = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -297,87 +297,77 @@ pub struct EncryptionInfo {
pub verification_state: VerificationState,
}
/// A customized version of a room event coming from a sync that holds optional
/// encryption info.
#[derive(Clone, Deserialize, Serialize)]
/// Represents a matrix room event that has been returned from `/sync`,
/// after initial processing.
///
/// Previously, this differed from [`TimelineEvent`] by wrapping an
/// [`AnySyncTimelineEvent`] instead of an [`AnyTimelineEvent`], but nowadays
/// they are essentially identical, and one of them should probably be removed.
#[derive(Clone, Debug, Serialize)]
pub struct SyncTimelineEvent {
/// The actual event.
pub event: Raw<AnySyncTimelineEvent>,
/// The encryption info about the event. Will be `None` if the event was not
/// encrypted.
pub encryption_info: Option<EncryptionInfo>,
/// The event itself, together with any information on decryption.
pub kind: TimelineEventKind,
/// The push actions associated with this event.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub push_actions: Vec<Action>,
/// The encryption info about the events bundled in the `unsigned` object.
///
/// Will be `None` if no bundled event was encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
impl SyncTimelineEvent {
/// Create a new `SyncTimelineEvent` from the given raw event.
///
/// This is a convenience constructor for when you don't need to set
/// `encryption_info` or `push_action`, for example inside a test.
/// This is a convenience constructor for a plaintext event when you don't
/// need to set `push_action`, for example inside a test.
pub fn new(event: Raw<AnySyncTimelineEvent>) -> Self {
Self { event, encryption_info: None, push_actions: vec![], unsigned_encryption_info: None }
Self { kind: TimelineEventKind::PlainText { event }, push_actions: vec![] }
}
/// Create a new `SyncTimelineEvent` from the given raw event and push
/// actions.
///
/// This is a convenience constructor for when you don't need to set
/// `encryption_info`, for example inside a test.
/// This is a convenience constructor for a plaintext event, for example
/// inside a test.
pub fn new_with_push_actions(
event: Raw<AnySyncTimelineEvent>,
push_actions: Vec<Action>,
) -> Self {
Self { event, encryption_info: None, push_actions, unsigned_encryption_info: None }
Self { kind: TimelineEventKind::PlainText { event }, push_actions }
}
/// Create a new `SyncTimelineEvent` to represent the given decryption
/// failure.
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: vec![] }
}
/// Get the event id of this `SyncTimelineEvent` if the event has any valid
/// id.
pub fn event_id(&self) -> Option<OwnedEventId> {
self.event.get_field::<OwnedEventId>("event_id").ok().flatten()
self.kind.event_id()
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for SyncTimelineEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let SyncTimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } =
self;
let mut s = f.debug_struct("SyncTimelineEvent");
s.field("event", &DebugRawEvent(event));
s.maybe_field("encryption_info", encryption_info);
if !push_actions.is_empty() {
s.field("push_actions", push_actions);
}
s.maybe_field("unsigned_encryption_info", unsigned_encryption_info);
s.finish()
/// Returns a reference to the (potentially decrypted) Matrix event inside
/// this `TimelineEvent`.
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
self.kind.raw()
}
}
impl From<Raw<AnySyncTimelineEvent>> for SyncTimelineEvent {
fn from(inner: Raw<AnySyncTimelineEvent>) -> Self {
Self::new(inner)
/// If the event was a decrypted event that was successfully decrypted, get
/// its encryption info. Otherwise, `None`.
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
self.kind.encryption_info()
}
/// Takes ownership of this `TimelineEvent`, returning the (potentially
/// decrypted) Matrix event within.
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
self.kind.into_raw()
}
}
impl From<TimelineEvent> for SyncTimelineEvent {
fn from(o: TimelineEvent) -> Self {
// This conversion is unproblematic since a `SyncTimelineEvent` is just a
// `TimelineEvent` without the `room_id`. By converting the raw value in
// this way, we simply cause the `room_id` field in the json to be
// ignored by a subsequent deserialization.
Self {
event: o.event.cast(),
encryption_info: o.encryption_info,
push_actions: o.push_actions.unwrap_or_default(),
unsigned_encryption_info: o.unsigned_encryption_info,
}
Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() }
}
}
@@ -388,60 +378,208 @@ impl From<DecryptedRoomEvent> for SyncTimelineEvent {
}
}
#[derive(Clone)]
impl<'de> Deserialize<'de> for SyncTimelineEvent {
/// Custom deserializer for [`SyncTimelineEvent`], to support older formats.
///
/// Ideally we might use an untagged enum and then convert from that;
/// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497).
///
/// Instead, we first deserialize into an unstructured JSON map, and then
/// inspect the json to figure out which format we have.
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde_json::{Map, Value};
// First, deserialize to an unstructured JSON map
let value = Map::<String, Value>::deserialize(deserializer)?;
// If we have a top-level `event`, it's V0
if value.contains_key("event") {
let v0: SyncTimelineEventDeserializationHelperV0 =
serde_json::from_value(Value::Object(value)).map_err(|e| {
serde::de::Error::custom(format!(
"Unable to deserialize V0-format SyncTimelineEvent: {}",
e
))
})?;
Ok(v0.into())
}
// Otherwise, it's V1
else {
let v1: SyncTimelineEventDeserializationHelperV1 =
serde_json::from_value(Value::Object(value)).map_err(|e| {
serde::de::Error::custom(format!(
"Unable to deserialize V1-format SyncTimelineEvent: {}",
e
))
})?;
Ok(v1.into())
}
}
}
/// Represents a matrix room event that has been returned from a Matrix
/// client-server API endpoint such as `/messages`, after initial processing.
///
/// The "initial processing" includes an attempt to decrypt encrypted events, so
/// the main thing this adds over [`AnyTimelineEvent`] is information on
/// encryption.
///
/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an
/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays
/// they are essentially identical, and one of them should probably be removed.
#[derive(Clone, Debug)]
pub struct TimelineEvent {
/// The actual event.
pub event: Raw<AnyTimelineEvent>,
/// The encryption info about the event. Will be `None` if the event was not
/// encrypted.
pub encryption_info: Option<EncryptionInfo>,
/// The event itself, together with any information on decryption.
pub kind: TimelineEventKind,
/// The push actions associated with this event, if we had sufficient
/// context to compute them.
pub push_actions: Option<Vec<Action>>,
/// The encryption info about the events bundled in the `unsigned` object.
///
/// Will be `None` if no bundled event was encrypted.
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
impl TimelineEvent {
/// Create a new `TimelineEvent` from the given raw event.
///
/// This is a convenience constructor for when you don't need to set
/// `encryption_info` or `push_action`, for example inside a test.
/// This is a convenience constructor for a plaintext event when you don't
/// need to set `push_action`, for example inside a test.
pub fn new(event: Raw<AnyTimelineEvent>) -> Self {
Self { event, encryption_info: None, push_actions: None, unsigned_encryption_info: None }
Self {
// This conversion is unproblematic since a `SyncTimelineEvent` is just a
// `TimelineEvent` without the `room_id`. By converting the raw value in
// this way, we simply cause the `room_id` field in the json to be
// ignored by a subsequent deserialization.
kind: TimelineEventKind::PlainText { event: event.cast() },
push_actions: None,
}
}
/// Create a new `TimelineEvent` to represent the given decryption failure.
pub fn new_utd_event(event: Raw<AnySyncTimelineEvent>, utd_info: UnableToDecryptInfo) -> Self {
Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None }
}
/// Returns a reference to the (potentially decrypted) Matrix event inside
/// this `TimelineEvent`.
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
self.kind.raw()
}
/// If the event was a decrypted event that was successfully decrypted, get
/// its encryption info. Otherwise, `None`.
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
self.kind.encryption_info()
}
/// Takes ownership of this `TimelineEvent`, returning the (potentially
/// decrypted) Matrix event within.
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
self.kind.into_raw()
}
}
impl From<DecryptedRoomEvent> for TimelineEvent {
fn from(decrypted: DecryptedRoomEvent) -> Self {
Self {
// Casting from the more specific `AnyMessageLikeEvent` (i.e. an event without a
// `state_key`) to a more generic `AnyTimelineEvent` (i.e. one that may contain
// a `state_key`) is safe.
event: decrypted.event.cast(),
encryption_info: Some(decrypted.encryption_info),
push_actions: None,
unsigned_encryption_info: decrypted.unsigned_encryption_info,
Self { kind: TimelineEventKind::Decrypted(decrypted), push_actions: None }
}
}
/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with
/// encryption data.
#[derive(Clone, Serialize, Deserialize)]
pub enum TimelineEventKind {
/// A successfully-decrypted encrypted event.
Decrypted(DecryptedRoomEvent),
/// An encrypted event which could not be decrypted.
UnableToDecrypt {
/// The `m.room.encrypted` event. Depending on the source of the event,
/// it could actually be an [`AnyTimelineEvent`] (i.e., it may
/// have a `room_id` property).
event: Raw<AnySyncTimelineEvent>,
/// Information on the reason we failed to decrypt
utd_info: UnableToDecryptInfo,
},
/// An unencrypted event.
PlainText {
/// The actual event. Depending on the source of the event, it could
/// actually be a [`AnyTimelineEvent`] (which differs from
/// [`AnySyncTimelineEvent`] by the addition of a `room_id` property).
event: Raw<AnySyncTimelineEvent>,
},
}
impl TimelineEventKind {
/// Returns a reference to the (potentially decrypted) Matrix event inside
/// this `TimelineEvent`.
pub fn raw(&self) -> &Raw<AnySyncTimelineEvent> {
match self {
// It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does
// *not* contain a `state_key` and *does* contain a `room_id`) into an
// `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not*
// expected to contain a `room_id`). It just means that the `room_id` will be ignored
// in a future deserialization.
TimelineEventKind::Decrypted(d) => d.event.cast_ref(),
TimelineEventKind::UnableToDecrypt { event, .. } => event.cast_ref(),
TimelineEventKind::PlainText { event } => event,
}
}
/// Get the event id of this `TimelineEventKind` if the event has any valid
/// id.
pub fn event_id(&self) -> Option<OwnedEventId> {
self.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
}
/// If the event was a decrypted event that was successfully decrypted, get
/// its encryption info. Otherwise, `None`.
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match self {
TimelineEventKind::Decrypted(d) => Some(&d.encryption_info),
TimelineEventKind::UnableToDecrypt { .. } => None,
TimelineEventKind::PlainText { .. } => None,
}
}
/// Takes ownership of this `TimelineEvent`, returning the (potentially
/// decrypted) Matrix event within.
pub fn into_raw(self) -> Raw<AnySyncTimelineEvent> {
match self {
// It is safe to cast from an `AnyMessageLikeEvent` (i.e. JSON which does
// *not* contain a `state_key` and *does* contain a `room_id`) into an
// `AnySyncTimelineEvent` (i.e. JSON which *may* contain a `state_key` and is *not*
// expected to contain a `room_id`). It just means that the `room_id` will be ignored
// in a future deserialization.
TimelineEventKind::Decrypted(d) => d.event.cast(),
TimelineEventKind::UnableToDecrypt { event, .. } => event.cast(),
TimelineEventKind::PlainText { event } => event,
}
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for TimelineEvent {
impl fmt::Debug for TimelineEventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let TimelineEvent { event, encryption_info, push_actions, unsigned_encryption_info } = self;
let mut s = f.debug_struct("TimelineEvent");
s.field("event", &DebugRawEvent(event));
s.maybe_field("encryption_info", encryption_info);
if let Some(push_actions) = &push_actions {
if !push_actions.is_empty() {
s.field("push_actions", push_actions);
match &self {
Self::PlainText { event } => f
.debug_struct("TimelineEventDecryptionResult::PlainText")
.field("event", &DebugRawEvent(event))
.finish(),
Self::UnableToDecrypt { event, utd_info } => f
.debug_struct("TimelineEventDecryptionResult::UnableToDecrypt")
.field("event", &DebugRawEvent(event))
.field("utd_info", &utd_info)
.finish(),
Self::Decrypted(decrypted) => {
f.debug_tuple("TimelineEventDecryptionResult::Decrypted").field(decrypted).finish()
}
}
s.maybe_field("unsigned_encryption_info", unsigned_encryption_info);
s.finish()
}
}
@@ -458,6 +596,7 @@ pub struct DecryptedRoomEvent {
/// object.
///
/// Will be `None` if no bundled event was encrypted.
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
@@ -520,18 +659,161 @@ pub struct UnableToDecryptInfo {
/// `m.megolm.v1.aes-sha2` algorithm.
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
/// Reason code for the decryption failure
#[serde(default = "unknown_utd_reason")]
pub reason: UnableToDecryptReason,
}
fn unknown_utd_reason() -> UnableToDecryptReason {
UnableToDecryptReason::Unknown
}
/// Reason code for a decryption failure
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UnableToDecryptReason {
/// The reason for the decryption failure is unknown. This is only intended
/// for use when deserializing old UnableToDecryptInfo instances.
#[doc(hidden)]
Unknown,
/// The `m.room.encrypted` event that should have been decrypted is
/// malformed in some way (e.g. unsupported algorithm, missing fields,
/// unknown megolm message type).
MalformedEncryptedEvent,
/// Decryption failed because we're missing the megolm session that was used
/// to encrypt the event.
///
/// TODO: support withheld codes?
MissingMegolmSession,
/// Decryption failed because, while we have the megolm session that was
/// used to encrypt the message, it is ratcheted too far forward.
UnknownMegolmMessageIndex,
/// We found the Megolm session, but were unable to decrypt the event using
/// that session for some reason (e.g. incorrect MAC).
///
/// This represents all `vodozemac::megolm::DecryptionError`s, except
/// `UnknownMessageIndex`, which is represented as
/// `UnknownMegolmMessageIndex`.
MegolmDecryptionFailure,
/// The event could not be deserialized after decryption.
PayloadDeserializationFailure,
/// Decryption failed because of a mismatch between the identity keys of the
/// device we received the room key from and the identity keys recorded in
/// the plaintext of the room key to-device message.
MismatchedIdentityKeys,
/// An encrypted message wasn't decrypted, because the sender's
/// cross-signing identity did not satisfy the requested
/// `TrustRequirement`.
SenderIdentityNotTrusted(VerificationLevel),
}
impl UnableToDecryptReason {
/// Returns true if this UTD is due to a missing room key (and hence might
/// resolve itself if we wait a bit.)
pub fn is_missing_room_key(&self) -> bool {
matches!(self, Self::MissingMegolmSession | Self::UnknownMegolmMessageIndex)
}
}
/// Deserialization helper for [`SyncTimelineEvent`], for the modern format.
///
/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a
/// regular `Deserialize` implementation.
#[derive(Debug, Deserialize)]
struct SyncTimelineEventDeserializationHelperV1 {
/// The event itself, together with any information on decryption.
kind: TimelineEventKind,
/// The push actions associated with this event.
#[serde(default)]
push_actions: Vec<Action>,
}
impl From<SyncTimelineEventDeserializationHelperV1> for SyncTimelineEvent {
fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self {
let SyncTimelineEventDeserializationHelperV1 { kind, push_actions } = value;
SyncTimelineEvent { kind, push_actions }
}
}
/// Deserialization helper for [`SyncTimelineEvent`], for an older format.
#[derive(Deserialize)]
struct SyncTimelineEventDeserializationHelperV0 {
/// The actual event.
event: Raw<AnySyncTimelineEvent>,
/// The encryption info about the event. Will be `None` if the event
/// was not encrypted.
encryption_info: Option<EncryptionInfo>,
/// The push actions associated with this event.
#[serde(default)]
push_actions: Vec<Action>,
/// The encryption info about the events bundled in the `unsigned`
/// object.
///
/// Will be `None` if no bundled event was encrypted.
unsigned_encryption_info: Option<BTreeMap<UnsignedEventLocation, UnsignedDecryptionResult>>,
}
impl From<SyncTimelineEventDeserializationHelperV0> for SyncTimelineEvent {
fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self {
let SyncTimelineEventDeserializationHelperV0 {
event,
encryption_info,
push_actions,
unsigned_encryption_info,
} = value;
let kind = match encryption_info {
Some(encryption_info) => {
TimelineEventKind::Decrypted(DecryptedRoomEvent {
// We cast from `Raw<AnySyncTimelineEvent>` to
// `Raw<AnyMessageLikeEvent>`, which means
// we are asserting that it contains a room_id.
// That *should* be ok, because if this is genuinely a decrypted
// room event (as the encryption_info indicates), then it will have
// a room_id.
event: event.cast(),
encryption_info,
unsigned_encryption_info,
})
}
None => TimelineEventKind::PlainText { event },
};
SyncTimelineEvent { kind, push_actions }
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use ruma::{
event_id,
events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent},
serde::Raw,
user_id,
};
use serde::Deserialize;
use serde_json::json;
use super::{SyncTimelineEvent, TimelineEvent, VerificationState};
use super::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEvent,
TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult,
UnsignedEventLocation, VerificationState,
};
use crate::deserialized_responses::{DeviceLinkProblem, VerificationLevel};
fn example_event() -> serde_json::Value {
@@ -561,7 +843,7 @@ mod tests {
let converted_room_event: SyncTimelineEvent = room_event.into();
let converted_event: AnySyncTimelineEvent =
converted_room_event.event.deserialize().unwrap();
converted_room_event.raw().deserialize().unwrap();
assert_eq!(converted_event.event_id(), "$xxxxx:example.org");
assert_eq!(converted_event.sender(), "@carl:example.com");
@@ -606,4 +888,149 @@ mod tests {
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
);
}
#[test]
fn sync_timeline_event_serialisation() {
let room_event = SyncTimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event: Raw::new(&example_event()).unwrap().cast(),
encryption_info: EncryptionInfo {
sender: user_id!("@sender:example.com").to_owned(),
sender_device: None,
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "xxx".to_owned(),
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
},
unsigned_encryption_info: Some(BTreeMap::from([(
UnsignedEventLocation::RelationsReplace,
UnsignedDecryptionResult::UnableToDecrypt(UnableToDecryptInfo {
session_id: Some("xyz".to_owned()),
reason: UnableToDecryptReason::MalformedEncryptedEvent,
}),
)])),
}),
push_actions: Default::default(),
};
let serialized = serde_json::to_value(&room_event).unwrap();
// Test that the serialization is as expected
assert_eq!(
serialized,
json!({
"kind": {
"Decrypted": {
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
},
"unsigned_encryption_info": {
"RelationsReplace": {"UnableToDecrypt": {
"session_id": "xyz",
"reason": "MalformedEncryptedEvent",
}}
}
}
}
})
);
// And it can be properly deserialized from the new format.
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
// Test that the previous format can also be deserialized.
let serialized = json!({
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
},
});
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
// Test that the previous format, with an undecryptable unsigned event, can also
// be deserialized.
let serialized = json!({
"event": {
"content": {"body": "secret", "msgtype": "m.text"},
"event_id": "$xxxxx:example.org",
"origin_server_ts": 2189,
"room_id": "!someroom:example.com",
"sender": "@carl:example.com",
"type": "m.room.message",
},
"encryption_info": {
"sender": "@sender:example.com",
"sender_device": null,
"algorithm_info": {
"MegolmV1AesSha2": {
"curve25519_key": "xxx",
"sender_claimed_keys": {}
}
},
"verification_state": "Verified",
},
"unsigned_encryption_info": {
"RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}}
}
});
let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap();
assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned()));
assert_matches!(
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
assert_matches!(event.kind, TimelineEventKind::Decrypted(decrypted) => {
assert_matches!(decrypted.unsigned_encryption_info, Some(map) => {
assert_eq!(map.len(), 1);
let (location, result) = map.into_iter().next().unwrap();
assert_eq!(location, UnsignedEventLocation::RelationsReplace);
assert_matches!(result, UnsignedDecryptionResult::UnableToDecrypt(utd_info) => {
assert_eq!(utd_info.session_id, Some("xyz".to_owned()));
assert_eq!(utd_info.reason, UnableToDecryptReason::Unknown);
})
});
});
}
}
+1
View File
@@ -25,6 +25,7 @@ pub mod debug;
pub mod deserialized_responses;
pub mod executor;
pub mod failures_cache;
pub mod linked_chunk;
pub mod ring_buffer;
pub mod store_locks;
pub mod timeout;
@@ -14,7 +14,7 @@
use std::{
collections::VecDeque,
ops::ControlFlow,
ops::{ControlFlow, Not},
sync::{Arc, RwLock},
};
@@ -22,7 +22,7 @@ use eyeball_im::VectorDiff;
use super::{
updates::{ReaderToken, Update, UpdatesInner},
ChunkContent, ChunkIdentifier, Iter,
ChunkContent, ChunkIdentifier, Iter, Position,
};
/// A type alias to represent a chunk's length. This is purely for commodity.
@@ -253,7 +253,8 @@ impl UpdateToVectorDiff {
//
// From the `VectorDiff` “point of view”, this optimisation aims at avoiding
// removing items to push them again later.
let mut mute_push_items = false;
let mut reattaching = false;
let mut detaching = false;
for update in updates {
match update {
@@ -329,46 +330,22 @@ impl UpdateToVectorDiff {
}
Update::PushItems { at: position, items } => {
let expected_chunk_identifier = position.chunk_identifier();
let number_of_chunks = self.chunks.len();
let (offset, (chunk_index, chunk_length)) = self.map_to_offset(position);
let (chunk_index, offset, chunk_length) = {
let control_flow = self.chunks.iter_mut().enumerate().try_fold(
position.index(),
|offset, (chunk_index, (chunk_identifier, chunk_length))| {
if chunk_identifier == &expected_chunk_identifier {
ControlFlow::Break((chunk_index, offset, chunk_length))
} else {
ControlFlow::Continue(offset + *chunk_length)
}
},
);
match control_flow {
// Chunk has been found, and all values have been calculated as
// expected.
ControlFlow::Break(values) => values,
// Chunk has not been found.
ControlFlow::Continue(..) => {
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not
// buggy, and assuming `Self::chunks` is correctly initialized, it
// is not possible to push items on a chunk that does not exist. If
// this predicate fails, it means `LinkedChunk` or
// `ObservableUpdates` contain a bug.
panic!("Pushing items: The chunk is not found");
}
}
};
let is_pushing_back =
chunk_index + 1 == number_of_chunks && position.index() >= *chunk_length;
// Add the number of items to the chunk in `self.chunks`.
*chunk_length += items.len();
// See `mute_push_items` to learn more.
if mute_push_items {
// See `reattaching` to learn more.
if reattaching {
continue;
}
// Optimisation: we can emit a `VectorDiff::Append` in this particular case.
if chunk_index + 1 == self.chunks.len() {
if is_pushing_back && detaching.not() {
diffs.push(VectorDiff::Append { values: items.into() });
}
// No optimisation: let's emit `VectorDiff::Insert`.
@@ -379,15 +356,30 @@ impl UpdateToVectorDiff {
}
}
Update::DetachLastItems { at } => {
let expected_chunk_identifier = at.chunk_identifier();
let new_length = at.index();
Update::RemoveItem { at: position } => {
let (offset, (_chunk_index, chunk_length)) = self.map_to_offset(position);
let length = self
// Remove one item to the chunk in `self.chunks`.
*chunk_length -= 1;
// See `reattaching` to learn more.
if reattaching {
continue;
}
// Let's emit a `VectorDiff::Remove`.
diffs.push(VectorDiff::Remove { index: offset });
}
Update::DetachLastItems { at: position } => {
let expected_chunk_identifier = position.chunk_identifier();
let new_length = position.index();
let chunk_length = self
.chunks
.iter_mut()
.find_map(|(chunk_identifier, length)| {
(*chunk_identifier == expected_chunk_identifier).then_some(length)
.find_map(|(chunk_identifier, chunk_length)| {
(*chunk_identifier == expected_chunk_identifier).then_some(chunk_length)
})
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
// assuming `Self::chunks` is correctly initialized, it is not possible to
@@ -395,23 +387,63 @@ impl UpdateToVectorDiff {
// it means `LinkedChunk` or `ObservableUpdates` contain a bug.
.expect("Detach last items: The chunk is not found");
*length = new_length;
*chunk_length = new_length;
// Entering the _detaching_ mode.
detaching = true;
}
Update::StartReattachItems => {
// Entering the `reattaching` mode.
mute_push_items = true;
// Entering the _reattaching_ mode.
reattaching = true;
}
Update::EndReattachItems => {
// Exiting the `reattaching` mode.
mute_push_items = false;
// Exiting the _reattaching_ mode.
reattaching = false;
// Exiting the _detaching_ mode.
detaching = false;
}
}
}
diffs
}
fn map_to_offset(&mut self, position: &Position) -> (usize, (usize, &mut usize)) {
let expected_chunk_identifier = position.chunk_identifier();
let (offset, (chunk_index, chunk_length)) = {
let control_flow = self.chunks.iter_mut().enumerate().try_fold(
position.index(),
|offset, (chunk_index, (chunk_identifier, chunk_length))| {
if chunk_identifier == &expected_chunk_identifier {
ControlFlow::Break((offset, (chunk_index, chunk_length)))
} else {
ControlFlow::Continue(offset + *chunk_length)
}
},
);
match control_flow {
// Chunk has been found, and all values have been calculated as
// expected.
ControlFlow::Break(values) => values,
// Chunk has not been found.
ControlFlow::Continue(..) => {
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
// assuming `Self::chunks` is correctly initialized, it is not possible to work
// on a chunk that does not exist. If this predicate fails, it means
// `LinkedChunk` or `ObservableUpdates` contain a bug.
panic!("The chunk is not found");
}
}
};
(offset, (chunk_index, chunk_length))
}
}
#[cfg(test)]
@@ -420,7 +452,10 @@ mod tests {
use imbl::{vector, Vector};
use super::{super::LinkedChunk, VectorDiff};
use super::{
super::{EmptyChunk, LinkedChunk},
VectorDiff,
};
fn apply_and_assert_eq<Item>(
accumulator: &mut Vector<Item>,
@@ -435,6 +470,9 @@ mod tests {
match diff {
VectorDiff::Insert { index, value } => accumulator.insert(index, value),
VectorDiff::Append { values } => accumulator.append(values),
VectorDiff::Remove { index } => {
accumulator.remove(index);
}
diff => unimplemented!("{diff:?}"),
}
}
@@ -578,15 +616,83 @@ mod tests {
&[VectorDiff::Insert { index: 0, value: 'm' }],
);
let removed_item = linked_chunk
.remove_item_at(
linked_chunk.item_position(|item| *item == 'c').unwrap(),
EmptyChunk::Remove,
)
.unwrap();
assert_eq!(removed_item, 'c');
assert_items_eq!(
linked_chunk,
['m', 'a', 'w'] ['x'] ['y', 'z', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
);
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// | m | a | w | x | y | z | b | d | i | j | k | l | e | f | g | h |
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// ^
// |
// `c` has been removed
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
let removed_item = linked_chunk
.remove_item_at(
linked_chunk.item_position(|item| *item == 'z').unwrap(),
EmptyChunk::Remove,
)
.unwrap();
assert_eq!(removed_item, 'z');
assert_items_eq!(
linked_chunk,
['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['h']
);
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// | m | a | w | x | y | b | d | i | j | k | l | e | f | g | h |
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// ^
// |
// `z` has been removed
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 5 }]);
linked_chunk
.insert_items_at(['z'], linked_chunk.item_position(|item| *item == 'h').unwrap())
.unwrap();
assert_items_eq!(
linked_chunk,
['m', 'a', 'w'] ['x'] ['y', 'b'] ['d'] ['i', 'j', 'k'] ['l'] ['e', 'f', 'g'] ['z', 'h']
);
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// | m | a | w | x | y | b | d | i | j | k | l | e | f | g | z | h |
// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
// ^^^^
// |
// new!
apply_and_assert_eq(
&mut accumulator,
as_vector.take(),
&[VectorDiff::Insert { index: 14, value: 'z' }],
);
drop(linked_chunk);
assert!(as_vector.take().is_empty());
// Finally, ensure the “reconstitued” vector is the one expected.
assert_eq!(
accumulator,
vector![
'm', 'a', 'w', 'x', 'y', 'z', 'b', 'c', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'h'
]
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
);
}
@@ -622,6 +728,7 @@ mod tests {
PushItems { items: Vec<char> },
PushGap,
ReplaceLastGap { items: Vec<char> },
RemoveItem { item: char },
}
fn as_vector_operation_strategy() -> impl Strategy<Value = AsVectorOperation> {
@@ -633,13 +740,16 @@ mod tests {
1 => prop::collection::vec(prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into()), 0..=25)
.prop_map(|items| AsVectorOperation::ReplaceLastGap { items }),
1 => prop::char::ranges(vec!['a'..='z', 'A'..='Z'].into())
.prop_map(|item| AsVectorOperation::RemoveItem { item }),
]
}
proptest! {
#[test]
fn as_vector_is_correct(
operations in prop::collection::vec(as_vector_operation_strategy(), 10..=50)
operations in prop::collection::vec(as_vector_operation_strategy(), 50..=200)
) {
let mut linked_chunk = LinkedChunk::<10, char, ()>::new_with_update_history();
let mut as_vector = linked_chunk.as_vector().unwrap();
@@ -662,7 +772,17 @@ mod tests {
continue;
};
linked_chunk.replace_gap_at(items, gap_identifier).unwrap();
linked_chunk.replace_gap_at(items, gap_identifier).expect("Failed to replace a gap");
}
AsVectorOperation::RemoveItem { item: expected_item } => {
let Some(position) = linked_chunk
.items().find_map(|(position, item)| (*item == expected_item).then_some(position))
else {
continue;
};
linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
}
}
}
@@ -678,6 +798,9 @@ mod tests {
vector_from_diffs.append(&mut values);
}
VectorDiff::Remove { index } => {
vector_from_diffs.remove(index);
}
_ => unreachable!(),
}
}
@@ -13,6 +13,7 @@
// limitations under the License.
#![allow(dead_code)]
#![allow(rustdoc::private_intra_doc_links)]
//! A linked chunk is the underlying data structure that holds all events.
@@ -56,7 +57,7 @@ macro_rules! assert_items_eq {
let chunk = $iterator .next().expect("next chunk (expect items)");
assert!(chunk.is_items(), "chunk should contain items");
let $crate::event_cache::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
let $crate::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
unreachable!()
};
@@ -406,6 +407,88 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
Ok(())
}
/// Remove item at a specified position in the [`LinkedChunk`].
///
/// `position` must point to a valid item, otherwise the method returns
/// `Err`.
///
/// The chunk containing the item represented by `position` may be empty
/// once the item has been removed. In this case, the chunk can be removed
/// if `empty_chunk` contains [`EmptyChunk::Remove`], otherwise the chunk is
/// kept if `empty_chunk` contains [`EmptyChunk::Keep`].
pub fn remove_item_at(
&mut self,
position: Position,
empty_chunk: EmptyChunk,
) -> Result<Item, Error> {
let chunk_identifier = position.chunk_identifier();
let item_index = position.index();
let mut chunk_ptr = None;
let removed_item;
{
let chunk = self
.links
.chunk_mut(chunk_identifier)
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
let can_unlink_chunk = match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
}
ChunkContent::Items(current_items) => {
let current_items_length = current_items.len();
if item_index > current_items_length {
return Err(Error::InvalidItemIndex { index: item_index });
}
removed_item = current_items.remove(item_index);
if let Some(updates) = self.updates.as_mut() {
updates
.push(Update::RemoveItem { at: Position(chunk_identifier, item_index) })
}
current_items.is_empty()
}
};
// If removing empty chunk is desired, and if the `chunk` can be unlinked, and
// if the `chunk` is not the first one, we can remove it.
if empty_chunk.remove() && can_unlink_chunk && chunk.is_first_chunk().not() {
// Unlink `chunk`.
chunk.unlink(&mut self.updates);
chunk_ptr = Some(chunk.as_ptr());
// We need to update `self.last` if and only if `chunk` _is_ the last chunk. The
// new last chunk is the chunk before `chunk`.
if chunk.is_last_chunk() {
self.links.last = chunk.previous;
}
}
self.length -= 1;
// Stop borrowing `chunk`.
}
if let Some(chunk_ptr) = chunk_ptr {
// `chunk` has been unlinked.
// Re-box the chunk, and let Rust does its job.
//
// SAFETY: `chunk` is unlinked and not borrowed anymore. `LinkedChunk` doesn't
// use it anymore, it's a leak. It is time to re-`Box` it and drop it.
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
}
Ok(removed_item)
}
/// Insert a gap at a specified position in the [`LinkedChunk`].
///
/// Because the `position` can be invalid, this method returns a
@@ -852,6 +935,12 @@ impl ChunkIdentifierGenerator {
#[repr(transparent)]
pub struct ChunkIdentifier(u64);
impl PartialEq<u64> for ChunkIdentifier {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
}
}
/// The position of something inside a [`Chunk`].
///
/// It's a pair of a chunk position and an item index.
@@ -868,6 +957,15 @@ impl Position {
pub fn index(&self) -> usize {
self.1
}
/// Decrement the index part (see [`Self::index`]), i.e. subtract 1.
///
/// # Panic
///
/// This method will panic if it will underflow, i.e. if the index is 0.
pub fn decrement_index(&mut self) {
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
}
}
/// An iterator over a [`LinkedChunk`] that traverses the chunk in backward
@@ -1247,6 +1345,22 @@ where
}
}
/// A type representing what to do when the system has to handle an empty chunk.
#[derive(Debug)]
pub enum EmptyChunk {
/// Keep the empty chunk.
Keep,
/// Remove the empty chunk.
Remove,
}
impl EmptyChunk {
fn remove(&self) -> bool {
matches!(self, Self::Remove)
}
}
#[cfg(test)]
mod tests {
use std::ops::Not;
@@ -1254,8 +1368,8 @@ mod tests {
use assert_matches::assert_matches;
use super::{
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Error, LinkedChunk,
Position,
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, EmptyChunk, Error,
LinkedChunk, Position,
};
#[test]
@@ -1845,6 +1959,310 @@ mod tests {
Ok(())
}
#[test]
fn test_remove_item_at() -> Result<(), Error> {
use super::Update::*;
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 11);
// Ignore previous updates.
let _ = linked_chunk.updates().unwrap().take();
// Remove the last item of the middle chunk, 3 times. The chunk is empty after
// that. The chunk is removed.
{
let position_of_f = linked_chunk.item_position(|item| *item == 'f').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_f, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'f');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 10);
let position_of_e = linked_chunk.item_position(|item| *item == 'e').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_e, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'e');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 9);
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'd');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 8);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(1), 2) },
RemoveItem { at: Position(ChunkIdentifier(1), 1) },
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
RemoveChunk(ChunkIdentifier(1)),
]
);
}
// Remove the first item of the first chunk, 3 times. The chunk is empty after
// that. The chunk is NOT removed because it's the first chunk.
{
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'a');
assert_items_eq!(linked_chunk, ['b', 'c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 7);
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'b');
assert_items_eq!(linked_chunk, ['c'] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 6);
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'c');
assert_items_eq!(linked_chunk, [] ['g', 'h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 5);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
]
);
}
// Remove the first item of the middle chunk, 3 times. The chunk is empty after
// that. The chunk is removed.
{
let first_position = linked_chunk.item_position(|item| *item == 'g').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'g');
assert_items_eq!(linked_chunk, [] ['h', 'i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 4);
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'h');
assert_items_eq!(linked_chunk, [] ['i'] ['j', 'k']);
assert_eq!(linked_chunk.len(), 3);
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'i');
assert_items_eq!(linked_chunk, [] ['j', 'k']);
assert_eq!(linked_chunk.len(), 2);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveChunk(ChunkIdentifier(2)),
]
);
}
// Remove the last item of the last chunk, twice. The chunk is empty after that.
// The chunk is removed.
{
let position_of_k = linked_chunk.item_position(|item| *item == 'k').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_k, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'k');
#[rustfmt::skip]
assert_items_eq!(linked_chunk, [] ['j']);
assert_eq!(linked_chunk.len(), 1);
let position_of_j = linked_chunk.item_position(|item| *item == 'j').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_j, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'j');
assert_items_eq!(linked_chunk, []);
assert_eq!(linked_chunk.len(), 0);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(3), 1) },
RemoveItem { at: Position(ChunkIdentifier(3), 0) },
RemoveChunk(ChunkIdentifier(3)),
]
);
}
// Add a couple more items, delete one, add a gap, and delete more items.
{
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
#[rustfmt::skip]
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d']);
assert_eq!(linked_chunk.len(), 4);
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
linked_chunk.insert_gap_at((), position_of_c)?;
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] ['d']);
assert_eq!(linked_chunk.len(), 4);
// Ignore updates.
let _ = linked_chunk.updates().unwrap().take();
let position_of_c = linked_chunk.item_position(|item| *item == 'c').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_c, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'c');
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d']);
assert_eq!(linked_chunk.len(), 3);
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
let removed_item = linked_chunk.remove_item_at(position_of_d, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'd');
assert_items_eq!(linked_chunk, ['a', 'b'] [-]);
assert_eq!(linked_chunk.len(), 2);
let first_position = linked_chunk.item_position(|item| *item == 'a').unwrap();
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'a');
assert_items_eq!(linked_chunk, ['b'] [-]);
assert_eq!(linked_chunk.len(), 1);
let removed_item = linked_chunk.remove_item_at(first_position, EmptyChunk::Remove)?;
assert_eq!(removed_item, 'b');
assert_items_eq!(linked_chunk, [] [-]);
assert_eq!(linked_chunk.len(), 0);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(6), 0) },
RemoveChunk(ChunkIdentifier(6)),
RemoveItem { at: Position(ChunkIdentifier(4), 0) },
RemoveChunk(ChunkIdentifier(4)),
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
]
);
}
Ok(())
}
#[test]
fn test_remove_item_at_and_keep_empty_chunks() -> Result<(), Error> {
use super::Update::*;
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
linked_chunk.push_items_back(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['d', 'e', 'f'] ['g', 'h']);
assert_eq!(linked_chunk.len(), 8);
// Ignore previous updates.
let _ = linked_chunk.updates().unwrap().take();
// Remove all items from the same chunk. The chunk is empty after that. The
// chunk is NOT removed because we asked to keep it.
{
let position = linked_chunk.item_position(|item| *item == 'd').unwrap();
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'd');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['e', 'f'] ['g', 'h']);
assert_eq!(linked_chunk.len(), 7);
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'e');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] ['f'] ['g', 'h']);
assert_eq!(linked_chunk.len(), 6);
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'f');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['g', 'h']);
assert_eq!(linked_chunk.len(), 5);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
RemoveItem { at: Position(ChunkIdentifier(1), 0) },
]
);
}
// Remove all items from the same chunk. The chunk is empty after that. The
// chunk is NOT removed because we asked to keep it.
{
let position = linked_chunk.item_position(|item| *item == 'g').unwrap();
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'g');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] ['h']);
assert_eq!(linked_chunk.len(), 4);
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'h');
assert_items_eq!(linked_chunk, ['a', 'b', 'c'] [] []);
assert_eq!(linked_chunk.len(), 3);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
RemoveItem { at: Position(ChunkIdentifier(2), 0) },
]
);
}
// Remove all items from the same chunk. The chunk is empty after that. The
// chunk is NOT removed because we asked to keep it.
{
let position = linked_chunk.item_position(|item| *item == 'a').unwrap();
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'a');
assert_items_eq!(linked_chunk, ['b', 'c'] [] []);
assert_eq!(linked_chunk.len(), 2);
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'b');
assert_items_eq!(linked_chunk, ['c'] [] []);
assert_eq!(linked_chunk.len(), 1);
let removed_item = linked_chunk.remove_item_at(position, EmptyChunk::Keep)?;
assert_eq!(removed_item, 'c');
assert_items_eq!(linked_chunk, [] [] []);
assert_eq!(linked_chunk.len(), 0);
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
RemoveItem { at: Position(ChunkIdentifier(0), 0) },
]
);
}
Ok(())
}
#[test]
fn test_insert_gap_at() -> Result<(), Error> {
use super::Update::*;
@@ -76,6 +76,12 @@ pub enum Update<Item, Gap> {
items: Vec<Item>,
},
/// An item has been removed inside a chunk of kind Items.
RemoveItem {
/// The [`Position`] of the item.
at: Position,
},
/// The last items of a chunk have been detached, i.e. the chunk has been
/// truncated.
DetachLastItems {
@@ -322,6 +328,22 @@ where
}
}
impl<Item, Gap> Drop for UpdatesSubscriber<Item, Gap> {
fn drop(&mut self) {
// Remove `Self::token` from `UpdatesInner::last_index_per_reader`.
// This is important so that the garbage collector can do its jobs correctly
// without a dead dangling reader token.
if let Some(updates) = self.updates.upgrade() {
let mut updates = updates.write().unwrap();
// Remove the reader token from `UpdatesInner`.
// It's safe to ignore the result of `remove` here: `None` means the token was
// already removed (note: it should be unreachable).
let _ = updates.last_index_per_reader.remove(&self.token);
}
}
}
#[cfg(test)]
mod tests {
use std::{
@@ -495,7 +517,7 @@ mod tests {
// | d | e | f | g | h | i |
// +---+---+---+---+---+---+
//
// “main” will have its index updated from 3 to 0.
// “main” will have its index updated from 0 to 3.
// “other” will have its index updated from 6 to 3.
{
let updates = linked_chunk.updates().unwrap();
@@ -563,20 +585,20 @@ mod tests {
}
}
struct CounterWaker {
number_of_wakeup: Mutex<usize>,
}
impl Wake for CounterWaker {
fn wake(self: Arc<Self>) {
*self.number_of_wakeup.lock().unwrap() += 1;
}
}
#[test]
fn test_updates_stream() {
use super::Update::*;
struct CounterWaker {
number_of_wakeup: Mutex<usize>,
}
impl Wake for CounterWaker {
fn wake(self: Arc<Self>) {
*self.number_of_wakeup.lock().unwrap() += 1;
}
}
let counter_waker = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
let waker = counter_waker.clone().into();
let mut context = Context::from_waker(&waker);
@@ -646,4 +668,166 @@ mod tests {
// Wakers calls have not changed.
assert_eq!(*counter_waker.number_of_wakeup.lock().unwrap(), 2);
}
#[test]
fn test_updates_multiple_streams() {
use super::Update::*;
let counter_waker1 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
let counter_waker2 = Arc::new(CounterWaker { number_of_wakeup: Mutex::new(0) });
let waker1 = counter_waker1.clone().into();
let waker2 = counter_waker2.clone().into();
let mut context1 = Context::from_waker(&waker1);
let mut context2 = Context::from_waker(&waker2);
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let updates_subscriber1 = linked_chunk.updates().unwrap().subscribe();
pin_mut!(updates_subscriber1);
// Scope for `updates_subscriber2`.
let updates_subscriber2_token = {
let updates_subscriber2 = linked_chunk.updates().unwrap().subscribe();
pin_mut!(updates_subscriber2);
// No update, streams are pending.
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 0);
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 0);
// Let's generate an update.
linked_chunk.push_items_back(['a']);
// The wakers must have been called.
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 1);
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 1);
// There is an update! Right after that, the streams are pending again.
assert_matches!(
updates_subscriber1.as_mut().poll_next(&mut context1),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }]
);
}
);
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
assert_matches!(
updates_subscriber2.as_mut().poll_next(&mut context2),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[PushItems { at: Position(ChunkIdentifier(0), 0), items: vec!['a'] }]
);
}
);
assert_matches!(updates_subscriber2.as_mut().poll_next(&mut context2), Poll::Pending);
// Let's generate two other updates.
linked_chunk.push_items_back(['b']);
linked_chunk.push_items_back(['c']);
// A waker is consumed when called. The first call to `push_items_back` will
// call and consume the wakers. The second call to `push_items_back` will do
// nothing as the wakers have been consumed. New wakers will be registered on
// polling.
//
// So, the waker must have been called only once for the two updates.
assert_eq!(*counter_waker1.number_of_wakeup.lock().unwrap(), 2);
assert_eq!(*counter_waker2.number_of_wakeup.lock().unwrap(), 2);
// Let's poll `updates_subscriber1` only.
assert_matches!(
updates_subscriber1.as_mut().poll_next(&mut context1),
Poll::Ready(Some(items)) => {
assert_eq!(
items,
&[
PushItems { at: Position(ChunkIdentifier(0), 1), items: vec!['b'] },
PushItems { at: Position(ChunkIdentifier(0), 2), items: vec!['c'] },
]
);
}
);
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
// For the sake of this test, we also need to advance the main reader token.
let _ = linked_chunk.updates().unwrap().take();
let _ = linked_chunk.updates().unwrap().take();
// If we inspect the garbage collector state, `a`, `b` and `c` should still be
// present because not all of them have been consumed by `updates_subscriber2`
// yet.
{
let updates = linked_chunk.updates().unwrap();
let inner = updates.inner.read().unwrap();
// Inspect number of updates in memory.
// We get 2 because the garbage collector runs before data are taken, not after:
// `updates_subscriber2` has read `a` only, so `b` and `c` remain.
assert_eq!(inner.len(), 2);
// Inspect the indices.
let indices = &inner.last_index_per_reader;
assert_eq!(indices.get(&updates_subscriber1.token), Some(&2));
assert_eq!(indices.get(&updates_subscriber2.token), Some(&0));
}
// Poll `updates_subscriber1` again: there is no new update so it must be
// pending.
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
// The state of the garbage collector is unchanged: `a`, `b` and `c` are still
// in memory.
{
let updates = linked_chunk.updates().unwrap();
let inner = updates.inner.read().unwrap();
// Inspect number of updates in memory. Value is unchanged.
assert_eq!(inner.len(), 2);
// Inspect the indices. They are unchanged.
let indices = &inner.last_index_per_reader;
assert_eq!(indices.get(&updates_subscriber1.token), Some(&2));
assert_eq!(indices.get(&updates_subscriber2.token), Some(&0));
}
updates_subscriber2.token
// Drop `updates_subscriber2`!
};
// `updates_subscriber2` has been dropped. Poll `updates_subscriber1` again:
// still no new update, but it will run the garbage collector again, and this
// time `updates_subscriber2` is not “retaining” `b` and `c`. The garbage
// collector must be empty.
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Pending);
// Inspect the garbage collector.
{
let updates = linked_chunk.updates().unwrap();
let inner = updates.inner.read().unwrap();
// Inspect number of updates in memory.
assert_eq!(inner.len(), 0);
// Inspect the indices.
let indices = &inner.last_index_per_reader;
assert_eq!(indices.get(&updates_subscriber1.token), Some(&0));
assert_eq!(indices.get(&updates_subscriber2_token), None); // token is unknown!
}
// When dropping the `LinkedChunk`, it closes the stream.
drop(linked_chunk);
assert_matches!(updates_subscriber1.as_mut().poll_next(&mut context1), Poll::Ready(None));
}
}

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