Compare commits

...

632 Commits

Author SHA1 Message Date
Damir Jelić 9ffe5aa6ca chore: Add a description to the test utils crate 2025-09-04 16:42:00 +02:00
Damir Jelić c604e4acd2 chore: Update the cargo lock file for the matrix-sdk-test-utils crate 2025-09-04 16:38:45 +02:00
Damir Jelić f8b343bece chore: Include the test-utils crate in the release
Turns out, we do actually need to release it :(
2025-09-04 16:36:05 +02:00
Damir Jelić 94f8f8c44c Revert "chore: Disable releases for the matrix-sdk-test-utils crate"
This reverts commit f9bf492fdb.
2025-09-04 16:36:05 +02:00
Damir Jelić 4c1f80faf7 chore: Release matrix-sdk version 0.14.0 2025-09-04 16:05:48 +02:00
Damir Jelić f9bf492fdb chore: Disable releases for the matrix-sdk-test-utils crate
The crate is only used as a dev dependency, as such we don't need to
release it.
2025-09-04 16:05:48 +02:00
Damir Jelić 824fc0b62e chore: Add a changelog for the matrix-sdk-search crate 2025-09-04 16:05:48 +02:00
Kévin Commaille 359db7f28b Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-09-04 11:48:48 +02:00
Kévin Commaille 30672e6feb Upgrade Ruma
Use the brand new release.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-09-04 11:48:48 +02:00
Benjamin Bouvier f9b419077d refactor(sdk): limit the quantity of data passed to the read receipt processor
This limits cloning.
2025-09-03 16:56:53 +02:00
Benjamin Bouvier d46f934d57 perf: process read receipts concurrently
By slightly changing the shape of the function used to process read
receipts, we can make it so that it's trivial to run concurrently, which
gives some nice speedups locally.

Distribution of 6 worst case processing time, initial response:

Before:
0.172524963s
0.216173016s
0.252289760s
0.257619156s
0.275838632s
0.280295891s

After:
0.083094692s
0.117074046s
0.130246646s
0.132577343s
0.138685246s
0.170287945s
2025-09-03 16:56:53 +02:00
Damir Jelić 0bed6afc29 fix(multiverse): Define the color of the placeholder text of the input line
This ensures that we're using the same color despite what your color
scheme of your terminal is. Some color schemes might produce unreadable
combinations of foreground color and background color.
2025-09-03 15:45:48 +02:00
Shrey Patel 412d4b80ee refactor(search): Move event processing out. 2025-09-03 15:05:23 +02:00
multi prise bcabf1bda4 Improve perfomance of build_room_key_bundle 2025-09-03 14:18:40 +02:00
Jorge Martín 7767ef6ca3 fix(ffi): Adapt FFI calls to Client::server_vendor_info to the new API
Specially important, the one from `ClientBuilder::build` as it avoids a situation where the builder would infinitely try to get a response for this request and never create the `Client` or fail.
2025-09-03 13:18:14 +02:00
Jorge Martín 6765ca0c39 refactor(sdk): Make Client::server_vendor_info accept an optional request config 2025-09-03 13:18:14 +02:00
Benjamin Bouvier 17abab0d53 chore(ci): bump wasm-pack and the runtime timeout 2025-09-03 12:46:09 +02:00
Benjamin Bouvier cc0bf91a06 feat(ffi): add a sync profiling log pack 2025-09-03 12:46:09 +02:00
Benjamin Bouvier 0b3345f592 feat(sdk): add more timers to sync processing 2025-09-03 12:46:09 +02:00
Benjamin Bouvier 472b934816 refactor(sdk): remove a few useless async 2025-09-02 19:56:23 +02:00
Ivan Enderlin 27a28e55d1 feat(ui): Add is_own and profile to LatestEventValue::Remote.
This patch adds 2 fields to `LatestEventValue::Remote`: `is_own` and
`profile`, which are necessary for the app consumers.
2025-09-02 18:18:26 +02:00
Ivan Enderlin d6a418f46a feat(ffi): Create Room::new_latest_event + LatestEventValue.
This patch creates the `LatestEventValue` in `matrix_sdk_ffi` and
exposes it via `Room::new_latest_event`.
2025-09-02 18:18:26 +02:00
Ivan Enderlin 268e14e4f5 feat(ui): Create timeline::LatestEventValue. 2025-09-02 18:18:26 +02:00
Ivan Enderlin f1190deef9 refactor(ui): TimelineAction::from_event zips two arguments.
This patch zips the `unable_to_decrypt_info` and the `meta` arguments
into a single one. They are strictly related, `meta` has no sense if
`unable_to_decrypt_info` has no sense neither.
2025-09-02 18:18:26 +02:00
Michael Goldenberg ee62cd749f refactor(indexeddb): add migrations and types for media retention metadata index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg cea5c190d8 refactor(indexeddb): add migrations and types for media last access index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg ad4cb4f6c9 refactor(indexeddb): add migrations and types for media source index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg 949e7a6cac refactor(indexeddb): add migrations and types for media content size index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg 8e66963a1e refactor(indexeddb): add types and migrations for storing media via event cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg aa02e31cf6 refactor(indexeddb): add types for representing media and associated metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Michael Goldenberg 57c7972c63 refactor(indexeddb): add foreign (de)serialization for IgnoreMediaRetentionPolicy
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-02 15:38:34 +02:00
Benjamin Bouvier e89ac3d7df tests: add some sliding sync tests for thread subscriptions and catchup 2025-09-02 14:57:49 +02:00
Benjamin Bouvier a3704c3563 tests: rearrange some imports ni the sdk/tests/integration/client file 2025-09-02 14:57:49 +02:00
Benjamin Bouvier 5fb728e8f0 feat(sdk): use local thread subscription data if it's accurate \o/ 2025-09-02 14:57:49 +02:00
Benjamin Bouvier eab62ec0b5 feat(sdk): automatically catch up missing thread subscriptions 2025-09-02 14:57:49 +02:00
Benjamin Bouvier 2fae949a42 feat(sliding sync): add support for the thread subscriptions extension 2025-09-02 14:57:49 +02:00
Benjamin Bouvier 4adbb4aa88 feat(sdk): add support for persisting the thread subscription catchup tokens 2025-09-02 14:57:49 +02:00
Benjamin Bouvier 18affe3edd chore: bump Ruma 2025-09-02 14:57:49 +02:00
multisme ea59bc8955 Implement querying inboundgroupsessions by room_id (#5534)
History sharing: improve efficiency of building key bundle

Signed-off-by: multi
[multiestunhappydev@gmail.com](mailto:multiestunhappydev@gmail.com)

Partially Implement
https://github.com/matrix-org/matrix-rust-sdk/issues/5513

---------

Signed-off-by: multisme <korokoko.toi@gmail.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2025-09-02 12:07:07 +01:00
Shrey Patel 68f6d927f1 test(search): Add tests for edits in search index. 2025-09-02 12:25:53 +02:00
Shrey Patel c3766789cc feat(search): add edits to search index. 2025-09-02 12:25:53 +02:00
Shrey Patel 7c31525f68 test(search): Add tests for indexing redactions. 2025-09-02 12:25:53 +02:00
Shrey Patel b2dd5ce02d feat(search): add deletion from index 2025-09-02 12:25:53 +02:00
dependabot[bot] 1f2b4f87bc chore(deps): bump CodSpeedHQ/action from 3.8.0 to 3.8.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b...76578c2a7ddd928664caa737f0e962e3085d4e7c)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 3.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 08:42:02 +02:00
dependabot[bot] 893c45af74 chore(deps): bump crate-ci/typos from 1.35.5 to 1.35.7
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.35.5 to 1.35.7.
- [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.35.5...v1.35.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 08:41:24 +02:00
Richard van der Hoff a161dfa9a0 crypto: log message index for megolm sessions received over olm
When we receive a to-device message that contains a megolm decryption key, log
the ratchet index of the received key, for debugging.
2025-09-01 17:42:33 +01:00
Damir Jelić 20cd0bedfa chore: Fix a clippy warning about a useless conversion 2025-09-01 16:33:10 +02:00
Damir Jelić d4a1ce06e4 chore: Allow the CDLA license 2025-09-01 16:33:10 +02:00
Damir Jelić e53906a920 chore: Bump vergen
Vergen has split into multiple, more dedicated crates. This bump is
therefore a migration to vergen-gitcl.
2025-09-01 16:33:10 +02:00
Damir Jelić 1e30916754 chore: Bump most of our deps 2025-09-01 16:33:10 +02:00
Damir Jelić 15c46b503c chore: Update our deny config 2025-09-01 10:46:10 +02:00
Damir Jelić 5cd3818841 chore: Fix a clippy warning about unused lifetimes 2025-09-01 10:46:10 +02:00
Damir Jelić 79b7d6d235 chore: Use the upstream tracing repo
The necessary patch was merged[1] now, thanks to Jonas. We still need
the patch section since we don't have a release with the patch.

[1]: https://github.com/tokio-rs/tracing/pull/3000
2025-09-01 10:46:10 +02:00
Damir Jelić 05d0f9e077 chore: Don't patch the paranoid-android crate
Our paranoid-android for was configured to use our fork of the tracing
crates.

But paranoid-android will already use our tracing fork since it's
defined in the patch section. The patch section will override the
dependencies for all of our dependencies as well.

This can be seen in the dependency tree using:
    $ cargo tree -p matrix-sdk-ffi --target=aarch64-linux-android
2025-09-01 10:46:10 +02:00
Damir Jelić 4c1e2d6d51 chore: Fix a warning about a type visibility 2025-09-01 10:46:10 +02:00
Damir Jelić c9162373a1 chore: Bump the tracing version we're using
This gets rid of a vulnerability[1] in tracing-subscriber. The forked
version we're using for the bindings were bumped as well.


[1]: https://github.com/advisories/GHSA-xwfj-jgwm-7wp5
2025-09-01 10:46:10 +02:00
Benjamin Bouvier 9f22f550bf refactor(sdk): avoid duplicating the comparison of bumpstamps 2025-09-01 10:38:34 +02:00
Benjamin Bouvier 7a762035f1 feat(sdk): store the thread subscription bumpstamp and implement the correct upsert semantics 2025-09-01 10:38:34 +02:00
Benjamin Bouvier 8c0a918e6e refactor(sdk): introduce a lightweight ThreadSubscription for external consumers, and rename previous one to StoredThreadSubscription
External consumers are likely not interested about unsubscriptions and
the bump stamp values themselves, so let's not expose these to them.
2025-09-01 10:38:34 +02:00
Benjamin Bouvier 33c317e6d2 refactor(sdk): put the subscription status + bumpstamp back into the stored thread subscription 2025-09-01 10:38:34 +02:00
Eric Eastwood 371ed49670 Document --proxy option in the multiverse client
To be able to inspect network requests flying around as you interact

Spawning from https://github.com/matrix-org/matrix-rust-sdk/issues/5600#issuecomment-3238128112
2025-08-30 09:56:33 +02:00
Stefan Ceriu f0c1c65308 fix(spaces): mitigate eventual race conditions when updating the pagination state (part #2) 2025-08-29 18:35:27 +03:00
Stefan Ceriu ea5063ca84 fix(spaces): address potential race when setting up the room updates listener 2025-08-29 18:35:27 +03:00
Stefan Ceriu dcc07e1049 chore(spaces): acquire and retain an async lock on the pagination token for the duration of the request to futher prevent inconsistencies 2025-08-29 18:35:27 +03:00
Stefan Ceriu 76a0eb1599 fix(spaces): mitigate eventual race conditions when updating the pagination state 2025-08-29 18:35:27 +03:00
Stefan Ceriu a9c16c96e0 chore(spaces): cleanup clone names in room updates listener 2025-08-29 18:35:27 +03:00
Stefan Ceriu 93f8ebba27 chore(spaces): simplify the SpaceGraph interfaces
Fix graph reference crap
2025-08-29 18:35:27 +03:00
Stefan Ceriu 95bb153269 chore(spaces): improve how space graph edge additions work 2025-08-29 18:35:27 +03:00
Stefan Ceriu ff72a09870 chore(spaces): fix typo 2025-08-29 18:35:27 +03:00
Stefan Ceriu 06de58dee9 chore(spaces): switch some flat_maps to filter_map 2025-08-29 18:35:27 +03:00
Stefan Ceriu fbb49e9b65 chore(spaces): simplify the SpaceGraph interfaces 2025-08-29 18:35:27 +03:00
Stefan Ceriu 04728cc1a6 chore(spaces): various documentation fixes 2025-08-29 18:35:27 +03:00
Stefan Ceriu d43d141dc8 chore(spaces): use Client::joined_space_rooms within the space service to reduce the number of iterations required 2025-08-29 18:35:27 +03:00
Stefan Ceriu 3c069f0c5c fix(spaces): compute the initial joined_spaces value when first setting up a subscription
- fixes values being reported only after the first sync update
2025-08-29 18:35:27 +03:00
Stefan Ceriu a2200b6324 chore(spaces): switch from manually dropping JoinHandles to AbortOnDrop 2025-08-29 18:35:27 +03:00
Stefan Ceriu 9caa0817ae docs(spaces): add documentation, comments and examples 2025-08-29 18:35:27 +03:00
Stefan Ceriu b3229041fb chore(spaces): ignore room list change updates if empty 2025-08-29 18:35:27 +03:00
Stefan Ceriu 50446377de change(spaces): publish VectorDiffs instead of a full vectors for joined spaces and space room list subscriptions 2025-08-29 18:35:27 +03:00
Stefan Ceriu 4f02a6d3be chore(spaces): add changelog 2025-08-29 18:35:27 +03:00
Stefan Ceriu 87fdd3c3bf chore(spaces): converge on single naming scheme for all spaces related components on both the UI and the FFI crates 2025-08-29 18:35:27 +03:00
Stefan Ceriu 990fe86fdc change(spaces): make the SpaceService constructor non-async and instead automatically setup a client subscription when requesting the joined services subscription 2025-08-29 18:35:27 +03:00
Stefan Ceriu be2ba26974 fix(spaces): filter out the current parent space from the /hierarchy responses and SpaceServiceRoomList instances 2025-08-29 18:35:27 +03:00
Stefan Ceriu 1392a0e637 change(spaces): put a limit of 1 on the max depth of the /hierarchy calls so only the direct children are fetche 2025-08-29 18:35:27 +03:00
Stefan Ceriu 03ffa4c9a4 feat(spaces): expose the number of children each space room has 2025-08-29 18:35:27 +03:00
Stefan Ceriu f8b5992101 fix(spaces): return the TaskHandle from the FFI space service subscription methods so it can be retained on the client side 2025-08-29 18:35:27 +03:00
Stefan Ceriu 97a5fbebfb feat(ffi): expose SpaceService::subscribe_to_joined_spaces and SpaceServiceRoomList::paginate 2025-08-29 18:35:27 +03:00
Stefan Ceriu 74c2032974 chore(spaces): build a graph from joined spaces parent and child relations to correctly detect all the edges and be able to remove any cycles.
- cycle removing is done through DFS and keeping a list of visited nodes
2025-08-29 18:35:27 +03:00
Stefan Ceriu 274aaf5ba3 change(room_list): request both m.space.parent and m.space.child state events in the sliding sync required state as they're both required to build a full view of the space room hierarchy 2025-08-29 18:35:27 +03:00
Stefan Ceriu 3e72cce7a0 fix(spaces): use wasm compatible executor spawn and JoinHandle 2025-08-29 18:35:27 +03:00
Stefan Ceriu aa4b176ab3 fix(spaces): fix complement-crypto failures because of using an outdated uniffi version
- automatic Arc inference was introduced in 0.27 and complement is using 0.25
2025-08-29 18:35:27 +03:00
Stefan Ceriu ad2f4c731a feat(spaces): have the SpaceRoomList publish updates as known room states change i.e. they get joined or left. 2025-08-29 18:35:27 +03:00
Stefan Ceriu f78015fae1 change(spaces): return only top level joined rooms from SpaceService::joined_spaces and its reactive counterpart 2025-08-29 18:35:27 +03:00
Stefan Ceriu 211a1f5a40 feat(spaces): add a reactive version of the joined_spaces method 2025-08-29 18:35:27 +03:00
Stefan Ceriu b8be1fdb26 feat(spaces): introduce a SpaceRoomList that allows pagination and provides reactive interfaces to its rooms and pagination state 2025-08-29 18:35:27 +03:00
Stefan Ceriu a43e42c170 chore(spaces): introduce a SpaceServiceRoom 2025-08-29 18:35:27 +03:00
Stefan Ceriu f031eaf96b chore(spaces): setup a simple space service and some unit tests 2025-08-29 18:35:27 +03:00
Benjamin Bouvier 3ba31d1e97 tests: clarify that the other thread subscription endpoints are grouped by room 2025-08-29 11:28:29 +02:00
Benjamin Bouvier bbf8f9f900 feat(sdk): add support for msc4308 accompanying endpoint (fetching thread subscriptions) 2025-08-29 11:28:29 +02:00
Benjamin Bouvier 49dc2bb640 chore: bump Ruma
We get test fixes for free, thanks to the new push rules semantics
implemented in Ruma \o/
2025-08-29 11:28:29 +02:00
Skye Elliot 99af951d7a feat(crypto): Add EncryptionSettings::encrypt_state_events
This will be used inside the WASM SDK to introduce a similar field to
its EncryptionSettings struct.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-28 14:00:23 +02:00
Ivan Enderlin a17bf18ff2 chore(sdk): Improve a log message. 2025-08-28 12:58:19 +02:00
Ivan Enderlin 5d87570a33 doc(sdk,base): Fix outdated documentation and typos. 2025-08-28 12:58:19 +02:00
Ivan Enderlin 80806303b5 feat(sdk): The LatestEventValue is stored in RoomInfo and persisted.
This patch updates `LatestEvent::update` to call the new
`LatestEvent::store` method, which will store the new `LatestEventValue`
in the `RoomInfo` struct, and will persist it in the `StateStore`.

This patch also adds the test for this new feature.
2025-08-28 12:58:19 +02:00
Ivan Enderlin 6155772bb1 feat(base): Add RoomInfo::new_latest_event.
This patch adds the new `new_latest_event: LatestEventValue` field in
`RoomInfo`. The `latest_event` is kept for the moment, but it will be
removed once the new API has landed entirely.
2025-08-28 12:58:19 +02:00
Ivan Enderlin 33db267a89 test(base): test_cached_latest_event_can_be_redacted runs only if e2e-encryption is enabled.
This patch puts `test_cached_latest_event_can_be_redacted` as it runs
only if `e2e-encryption` is turned on.
2025-08-28 12:58:19 +02:00
Ivan Enderlin 6fc68dac83 refactor(sdk,base): Move LatestEventValue into matrix_sdk_base.
This patch moves `LatestEventValue` into the
`matrix_sdk_base::latest_event` module. If we want to store this value
in `RoomInfo`, it must be in this crate.

Because it's not allowed to `impl T` where `T` lives in a different
crate, this patch changes the `impl LatestEventValue` to `struct
LatestEventValueBuilder` + `impl LatestEventValueBuilder`. Luckily, all
methods on `LatestEventValue` are only constructors, so the change is
super straightforward.
2025-08-28 12:58:19 +02:00
Ivan Enderlin f3eeb82b0b refactor(sdk): LatestEvent::_room_id becomes _weak_room: WeakRoom.
This patch replaces `OwnedRoomId` by `WeakRoom` in `LatestEvent`. Apart
from simplifying a couple of method' signatures, it also opens the road
for storing the `LatestEventValue` in `RoomInfo`.
2025-08-28 12:58:19 +02:00
Richard van der Hoff 951d22ac24 common: js_tracing: drop TRACE logs
Now that (since https://github.com/matrix-org/matrix-js-sdk/pull/4918) Element
Web uses separate subscribers for each OlmMachine, rather than a single global
one with a separate LevelFilter, EW's logs are very verbose because they
receive all the TRACE logs.

Dropping the TRACE logs on the floor isn't my favourite solution, but
everything else seems to be a bit harder than I have time for right
now:

* Sending TRACE logs up to the JS side in case it wants to print them would (a)
  increase overhead and (b) be an annoying breaking change in JsLogger

* Ideally we'd let the application configure its logging more precisely via an
  `EnvFilter` or something, but I'm not really sure what the API would look like
  for that.

So for now, we take the easy path.
2025-08-28 11:55:04 +01:00
Damir Jelić 527d001010 fix: Only report duplicate one-time key errors once
Since the server will reject any duplicate one-time keys forever,
clients which encounter such an error will spam sentry with such
reports.

This patch ensures that we only send the sentry report once.
2025-08-28 12:48:30 +02:00
Stefan Ceriu 759eeeb27f feat(test): add event factory methods for creating space rooms and populating parent-children relationships. 2025-08-28 10:55:00 +03:00
Stefan Ceriu 97d6f57aee feat(base): add SyncResponse::RoomUpdates::is_empty method to check if there were any room updates 2025-08-28 10:45:02 +03:00
Stefan Ceriu 6622a3ac93 fix(room): fix event types in matrix_sdk::Room::parent_spaces method comments 2025-08-28 09:37:07 +02:00
Stefan Ceriu 39730173d4 chore(ffi): expose the SyncService.expire_sessions so client can expire Sliding Sync positions on demand
This is useful when changing the required sliding sync state in between position resets which Synapse doesn't handle well at the moment as per https://github.com/element-hq/synapse/issues/18844
2025-08-28 10:32:32 +03:00
Stefan Ceriu 763314645b feat(sdk): add a Client::joined_space_rooms method that allows retrieving the list of joined spaces. 2025-08-28 09:28:04 +02:00
Ivan Enderlin b2fee72d79 refactor(sdk): Revisit the assert_latest_event_content macro.
This patch revisits the `assert_latest_event_content` macro to not take
a `true` or `false` value. It feels a bit weird to read. Instead, `with
|factory| { … }, true` becomes `event |factory| { … } is a candidate`.
Same for the `false`case which becomes `is not a candidate`. No more
comma, it feels a bit more like a sentence.
2025-08-27 16:45:48 +02:00
Ivan Enderlin 9803d2bcca refactor(sdk): Use SerializableEventContent.
This patch replaces `LocalLatestEventValue::content` and `…::event_type`
fields by using the existing `SerializableEventContent`. It does exactly
the same thing.
2025-08-27 16:45:48 +02:00
Ivan Enderlin 296867d2ac feat(sdk): LatestEventValue implements Serialize and Deserialize.
This patch implements `Serialize` and `Deserialize` on
`LatestEventValue`.
2025-08-27 16:45:48 +02:00
Ivan Enderlin 710b57e035 refactor(sdk): Remove LatestEventContent.
The problem is: `LatestEventContent` cannot be serialized. It's annoying
because it means we can't store a `LatestEventValue` (that wraps a
`LatestEventContent`) in the database.

This patch revisits `LatestEventValue`. Before we got:

```rust
pub enum LatestEventValue {
     None,
     Remote(LatestEventContent),
     LocalIsSending(LatestEventContent),
     LocalCannotBeSent(LatestEventContent),
}

pub enum LatestEventContent {
    RoomMessage(RoomMessageEventContent),
    Sticker(StickerEventContent),
    Poll(UnstablePollStartEventContent),
    CallInvite(CallInviteEventContent),
    CallNotify(CallNotifyEventContent),
    KnockedStateEvent(RoomMemberEventContent),
    Redacted(AnySyncMessageLikeEvent),
}
```

`LatestEventContent::Redacted` contains an `AnySyncMessageLikeEvent`.
That's the part that is not serializable.

It appears that `LatestEventContent` isn't necessary! The only thing we
need is to _filter_ the events by their type, no need to _find and
map_. The `LatestEventValue` can contain the entry event directly (e.g.
a `TimelineEvent` for the event cache). Okay, let's do that.

```rust
pub enum LatestEventValue {
    None,
    Remote(RemoteLatestEventValue),
    LocalIsSending(???),
    LocalCannotBeSent(???),
}

type RemoteLatestEventValue = TimelineEvent;
```

What about the `Local*` variants? We can't use a `TimelineEvent`. We
need a new type for that:

```rust
pub enum LatestEventValue {
    None,
    Remote(RemoteLatestEventValue),
    LocalIsSending(LocalLatestEventValue),
    LocalCannotBeSent(LocalLatestEventValue),
}

pub struct LocalLatestEventValue {
    pub timestamp: MilliSecondsSinceUnixEpoch,
    pub content: Raw<AnyMessageLikeEventContent>,
    pub event_type: String,
}
```

We don't need the event ID nor the transaction ID in
`LocalLatestEventValue`.

That's the only change. All the other changes are about the tests.
2025-08-27 16:45:48 +02:00
Skye Elliot baa75368d6 ci: Add feature matrix for integration testing
This will resolve a number of transitive dependency issues when testing
crates that do not enable the `experimental-encrypted-state-events` feature
flag by default.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-27 14:55:45 +02:00
Damir Jelić feb264e899 test: Add a test to check that we don't create event cache reference cycles 2025-08-27 13:03:44 +02:00
Skye Elliot 8e5075569e feat: Add top-level support for decrypting state events (#5552)
Implements support for decryption of state events

- [ ] Introduce a case for `AnySyncStateEvent::RoomEncrypted` to the
`state_events` sync response processor.
- [ ] Introduce modified `Room::decrypt_event` and
`::try_decrypt_room_event`.
- [ ] Introduce testing macro
`assert_let_decrypted_state_event_content`.
- [ ] Add casts and explicit type hints where necessary.

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-27 10:53:55 +01:00
Michael Goldenberg 33c11d08f0 test(indexeddb): add tests for media retention policy related fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg 9714ac8e10 refactor(indexeddb): add IndexedDB-backed impl for media retention policy fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg e1d136aa6e refactor(indexeddb): add indexed type to represent media retention policy
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg 8018753332 refactor(indexeddb): add primary key to core object store in event cache database
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg 61824f866c refactor(indexeddb): delegate media-related queries via media service to EventCacheStoreMedia implementation
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg 6919444e98 feat(indexeddb): add MemoryStore-backed impl of EventCacheStoreMedia
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg c5097cf07e refactor(indexeddb): remove macros for implementing EventCacheStore
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Michael Goldenberg b77c6c65cc feat(indexeddb): derive Clone for IndexeddbEventCacheStore
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-27 09:56:50 +02:00
Benjamin Bouvier f4ce4356ab refactor(event cache): only process threaded linked chunks if thread support has been globally enabled 2025-08-26 16:25:56 +02:00
Benjamin Bouvier d66733052a feat(event cache): add indexes for finding related events 2025-08-26 16:25:56 +02:00
Benjamin Bouvier 001dadffe1 bench: add a benchmark for finding related events (#5578)
Does what it says on the tin. Split from the performance fix, so we can
get some initial numbers on the CI bench runs too.

Part of investigating
https://github.com/matrix-org/matrix-rust-sdk/issues/5572
2025-08-26 15:57:06 +02:00
Shrey Patel b2387bf3a9 test(search): Add tests for RoomIndex::contains and indexing idempotency 2025-08-26 13:25:49 +02:00
Shrey Patel d43858ecb2 fix(search): Make indexing idempotent 2025-08-26 13:25:49 +02:00
dependabot[bot] 8804966094 chore(deps): bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 10:04:28 +02:00
dependabot[bot] a3ee011b61 chore(deps): bump crate-ci/typos from 1.35.4 to 1.35.5
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.35.4 to 1.35.5.
- [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.35.4...v1.35.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 10:04:06 +02:00
dependabot[bot] f586172f3e chore(deps): bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 10:03:51 +02:00
Jorge Martín 85d52586b6 fix(media): Use the right Option<Duration> value to get no timeout 2025-08-25 13:50:53 +02:00
Benjamin Bouvier 40d3dd57db tests(threads): add an exhaustive test to check for all notification mode combinations 2025-08-25 10:09:55 +02:00
Skye Elliot 4e2655aa1b feat(sdk): Use clearer fields for Span in SendRawStateEvent 2025-08-22 14:27:05 +01:00
Skye Elliot 41fcebbcb0 chore(sdk): Move ensure_room_encryption_ready to end of file 2025-08-22 14:27:05 +01:00
Skye Elliot 13b86a3f5d tests: Add test_room_encrypted_state_event_send 2025-08-22 14:27:05 +01:00
Skye Elliot dba23b66fa feat(sdk): Use SendStateEvent future in Room send_state methods
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-22 14:27:05 +01:00
Skye Elliot 132d0eb34a feat(sdk): Add SendStateEvent future
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-22 14:27:05 +01:00
Skye Elliot e2e70448ca feat(sdk): Modify Room::send_state_event_raw to return SendRawStateEvent
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-22 14:27:05 +01:00
Skye Elliot adaccbab2c feat(sdk): Add SendRawStateEvent future and deduplicate
Deduplicates common code that would be shared between
SendRawMessageLikeEvent and SendRawStateEvent to a helper method,
`ensure_room_encryption_ready`.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-22 14:27:05 +01:00
Richard van der Hoff 7a09ca0bbd Merge pull request #5545 from matrix-org/kaylendog/msc3414/json-castable
feat(sdk): Support room key downloading using `JsonCastable<EncryptedEvent>`
2025-08-21 18:59:23 +01:00
Richard van der Hoff fffdd34ebd Merge pull request #5559 from matrix-org/kaylendog/encrypt-poll-sync
This fixes a small issue encountered while developing the integration test for encrypted state events. If the first sync response received from the server after enabling encryption does not contain the m.room.encryption event, the client will report the encryption state as unknown, even if the next sync response does contain the event.

 - [x] Modify Room::enable_encryption_inner to spin on the room encryption state, rather than bailing after the first sync response is received.

Signed-off-by: Skye Elliot
2025-08-21 17:07:22 +01:00
Skye Elliot 24502d2706 fix(sdk): Check correct encryption state for encrypted state events 2025-08-21 15:43:35 +01:00
dragonfly1033 b6433dea27 refactor(sdk): Move index handling to EventCache and use linked_chunk_update_sender.
Remove previous code that got updates in `RoomEventCacheState::post_process_new_events`.
Add new task in `EventCache` that subscribes to `linked_chunk_update_sender` and forwards new events to `client::search::SearchIndex` same as before.

Signed-off-by: Shrey Patel shreyp@element.io
2025-08-21 14:17:45 +00:00
dragonfly1033 385f1a824f feat(multiverse): Add search feature to multiverse 2025-08-21 14:50:11 +01:00
dragonfly1033 dfc0ef8b35 refactor(multiverse): Create generic PopupInput widget 2025-08-21 14:50:11 +01:00
Benjamin Bouvier d2feeaac30 refactor(sdk): avoid an explicit dependency on ruma-common
The `ruma-common` crate is reexported by `ruma`, which is in our
dependency tree anyways. This change makes it so that qrcode, the only
crate that was making use of `ruma-common`, now depends on `ruma`. It
may move it further down the line in the compilation pipeline, but this
is unlikely to affect compile times in a crazy way. The benefit is that
this avoids the burden of having to specify the ruma commit hash twice
in the top-level Cargo.toml, as well as replacing it twice when
overriding it.
2025-08-21 15:42:25 +02:00
Benjamin Bouvier 25a81876a0 tests: allow conversion from EventBuilder into Raw<AnyGlobalAccountDataEvent>
This avoids a few explicit `.into_raw()` calls here and there.
2025-08-21 15:21:14 +02:00
Benjamin Bouvier e388fe6522 tests: streamline the SyncResponseBuilder global account methods
Only keep two: the one that uses the output of the event factory, and
one using custom JSON data.
2025-08-21 15:21:14 +02:00
Benjamin Bouvier ef20342ddf tests: use the new global account data methods a bit more 2025-08-21 15:21:14 +02:00
Benjamin Bouvier e6b1ffba99 tests: add support for the global account PushRules event in the event factory 2025-08-21 15:21:14 +02:00
Benjamin Bouvier 9a3ceb8be6 tests: add support for the global account IgnoredUserList event in the event factory 2025-08-21 15:21:14 +02:00
Benjamin Bouvier faee647c3a tests: get rid of GlobalAccountDataTestEvent::Direct 2025-08-21 15:21:14 +02:00
Benjamin Bouvier 1866143456 tests: add support for the global account Direct event in the event factory 2025-08-21 15:21:14 +02:00
Benjamin Bouvier be8e322ad6 doc(sdk): add comments around the NotificationSettings data structure 2025-08-21 15:21:14 +02:00
Benjamin Bouvier 838f607b32 refactor(benchmark): slightly change the benchmark to make it not use iter_custom
Instead of creating a fresh client every single time, this creates a
single client with a single event cache store, that's cleared between
runs of the benchmark.

So the tradeoff is:
- we don't have to create a new client anymore, which means no more
async setup code, which means we can avoid using `iter_custom`; a
benefit of this is that this can be benchmarked on CI now.
- but we're also measuring the time it takes to clear the database,
which isn't trivial in itself.
2025-08-21 15:19:26 +02:00
Benjamin Bouvier 6965004812 fix(benchmark): make the event cache benchmark compute what it's supposed to 2025-08-21 15:19:26 +02:00
Benjamin Bouvier 8ec23a95d5 fix(event cache): avoid cycle of EventCacheInner with the auto_shrink_linked_chunk_task
The task would stop when the receiver is closed; for the receiver to be
closed, the sender ought to be closed too.

Because the sender lives in `EventCacheInner`, and the task would hold
onto an `EventCacheInner` struct, the sender would never be dropped, so
the full `EventCacheInner` would leak, as a result.
2025-08-21 15:19:26 +02:00
Skye Elliot 7880ec5b01 fix(sdk): Return from poll when encryption state known
Fixes Room::enable_encryption_inner to break out of polling the
encryption state when it becomes known, rather than just encrypted.
2025-08-20 13:56:05 +01:00
Skye Elliot 36428564fc feat(sdk): Poll encryption state on sync event for up to 3 seconds. 2025-08-20 12:45:04 +01:00
Skye Elliot 7adaf7be73 feat(sdk): Rename state encryption methods for improved clarity
Changes `Room::enable_encryption_with_state` to
`Room::enable_encryption_with_encrypted_state_events`, and updates the
respective unit test and testing utilities.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-20 12:28:03 +02:00
Skye Elliot 7920723bb4 docs(sdk): Update CHANGELOG.md
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-20 12:28:03 +02:00
Skye Elliot b73163aa45 feat(sdk): Add Room::enable_encryption_with_state
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-20 12:28:03 +02:00
Ivan Enderlin feeeb53f19 refactor(sdk): Simplify Client::(joined|invited|left)_rooms.
This patch updates `Client::joined_rooms`, `invited_rooms` and
`left_rooms` to re-use `Client::rooms_filtered`.
2025-08-20 11:27:03 +02:00
Ivan Enderlin 1ac876db98 doc(sdk): Rephrase a couple of comments. 2025-08-20 08:37:27 +02:00
Ivan Enderlin afaf2cc036 test(sdk): Test that local latest event values has priority over remote's.
This patch moves the `test_update_ignores_none_value`
test in a new (correct) test module, and creates the new
`test_local_has_priority_over_remote` test.
2025-08-20 08:37:27 +02:00
Ivan Enderlin 5a3bb0a86d feat(sdk): Ensure the send queue has the priority over the event cache for computing a LatestEventValue. 2025-08-20 08:37:27 +02:00
Ivan Enderlin 2640aa1e23 fix(sdk): Compute the LatestEventValue on SentEvent before removing it from the buffer.
This patch changes the order in which the `LatestEventValue` is removed
from the buffer and re-created in case of a `SentEvent`. Previously,
the value from the buffer was removed before re-creating one, now it's
the opposite.

Why this order? Because once the local event is sent, it's not yet
received by the sync and consequently not stored in the event cache. So
once the local event is sent, it won't show up as a `LatestEventValue`
as it will immediately be replaced by an event from the event cache. By
computing the new value before removing it from the buffer, we ensure
the `LatestEventValue` represents the just sent local event.

See the comment in the code to learn more.
2025-08-20 08:37:27 +02:00
Ivan Enderlin d1a8392ce7 refactor(sdk): Rename LocalIsWedged to LocalCannotBeSent.
This patch renames `LocalIsWedged` to `LocalCannotBeSent` to avoid
confusion with the wedged/unwedged state of the `send_queue`. The
semantics is different in `latest_events`.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 4d14dd3692 doc(sdk): Add a TODO marker for later. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 63defca8af chore(sdk): Add mark_ prefix. 2025-08-19 16:35:41 +02:00
Ivan Enderlin fd83904b4d feat(sdk): Handle replacing a local event by a non-suitable latest event value.
This patch handles the case where a local event is replaced by another
local event which isn't suitable for being a latest event value. In this
case, the previous existing latest event value should be removed from
the buffer.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 9254c38a8d chore(sdk): Log all deserialize errors in latest_events. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 63d9dd5c6e refactor(sdk): Rename find_and_map_any_message… to extract_content_from_any_message_like. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 23dacc329e refactor(sdk): Rename LatestEventKind to LatestEventContent. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 5896a438f5 chore(sdk): Change error! to warn! when channels have been closed.
This patch reduces the level of logs when channels have been closed in
`LatestEvents`' tasks from `error` to `warn`. Indeed, when the `Client`
shutdowns, the channels will be closed, but it's not an error at all.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 31a3d76436 feat(sdk): Introduce LatestEventKind::Redacted.
This patch introduces `LatestEventKind::Redacted` to handle the case
where an event is supposed to be a latest event but has been redacted.
2025-08-19 16:35:41 +02:00
Ivan Enderlin dfaaf323ad test(sdk): Test that a None latest event value is ignored. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 94439d8913 test(sdk): Write tests for LatestEvent and SendQueue. 2025-08-19 16:35:41 +02:00
Ivan Enderlin 9cc29d7c65 feat(sdk): Implement LatestEvent::update_with_send_queue.
This patch implements `LatestEvent::update_with_send_queue`.
It introduces an intermediate type, for the sake of clarity,
`LatestEventValuesForLocalEvents`.

The difficulty here is to keep a buffer of `LatestEventValue`s requested
by the `SendQueue`. Why? Because we want the latest event value, but we
only receive `RoomSendQueueUpdate`s, we can't iterate over local events
in the `SendQueue` like we do for the `EventCache` to re-compute the
latest event if a local event has been cancelled or updated.

A particular care must also be applied when a local event is wedged:
this local event and all its followings must be marked as wedged too,
so that the `LatestEventValue` is `LocalIsWedged`. Same when the local
event is unwedged.
2025-08-19 16:35:41 +02:00
Ivan Enderlin f2fbdfbac2 chore(sdk): Rename and split a couple of types in latest_event.
This patch splits the `LatestEventValue` type into `LatestEventValue`
+ `LatestEventKind`. Basically, all variants in `LatestEventValue` are
moved inside the new `LatestEventKind` enum. `LatestEventValue` keeps
`None`, and see the new `Remote`, `LocalIsSending` and `LocalIsWedged`
variants.

This patch also extracts the message-like handling of `find_and_map`
(now renamed `find_and_map_timeline_event`) into its own function:
`find_and_map_any_message_like_event_content`. This is going to be
handful for the send queue part.
2025-08-19 16:35:41 +02:00
Ivan Enderlin fd34927f61 feat(sdk): compute_latest_events broadcasts the update to LatestEvent.
This patch updates `compute_latest_events` to broadcast a
`RoomSendQueueUpdate` onto `LatestEvent`. It introduces the new
`update_with_send_queue` method.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 0190d3556d chore(sdk): Rename update to update_with_event_cache in latest_events.
This patch renames the `update` methods to `update_with_event_cache`
in the `latest_events` module. It frees the road to introduce
`update_with_send_queue`.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 2d657fe908 feat(sdk): LatestEvents listens to the SendQueue.
This patch updates `LatestEvents` to listen to the updates from the
`SendQueue`. The `listen_to_event_cache_and_send_queue_updates` function
contains the important change. A new `LatestEventQueueUpdate` enum is
added to represent either an update from the event cache, or from the
send queue.

So far, `compute_latest_events` does nothing in particular, apart from
panicking with a `todo!()` when a send queue update is met.
2025-08-19 16:35:41 +02:00
Ivan Enderlin 5e43177d3a chore(sdk): Simplify code in listen_to_event_cache_and_send_queue_updates_task.
This patch removes the intermediate `rooms` variable in a new block. The
read-lock can be used immediately.
2025-08-19 16:35:41 +02:00
Skye Elliot e44b01cbe5 feat(sdk): Support room key downloading using JsonCastable<EncryptedEvent>
Allows `Backups::maybe_download_room_key` to accept any T:
JsonCastable<EncryptedEvent>, which will be required for state events to
trigger fetching the room key.

Implements JsonCastable<EncryptedEvent> for
OriginalSyncStateRoomEncryptedEventContent.

Implements JsonCastable<AnyStateEvent> for RoomEncryptedEventContent.
2025-08-19 14:47:44 +01:00
Michael Goldenberg 4882c98f99 refactor(indexeddb): allow IndexeddbSerializer::hash_key to be unused until event cache store is a default feature
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 4bf0187310 style(indexeddb): format event cache store impl by temporarily removing enclosing macro
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 10ca400d4d docs(indexeddb): correct key docs to express that keys are hashed, not encrypted
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg cc61e123b7 docs(indexeddb): remove references to room where relevant in transaction docs
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 6f23981268 refactor(indexeddb): log linked chunk id rather than room id in handle_linked_chunk_updates
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 859044285a test(indexeddb): use event cache store integration tests from matrix_sdk_base
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 6c1134006e fix(indexeddb): integrate linked chunk id into relevant chunk- and gap-related types and fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 6d1cdbc613 fix(indexeddb): integrate linked chunk id into relevant event-related types and fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 6160c15103 refactor(indexeddb): re-organize type synonyms in event_cache_store::serializer
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 100cbde526 refactor(indexeddb): use room-based queries in event-related fns that don't use linked chunk ids
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 6ff8a26cca refactor(indexeddb): add room-based index to event object store in preparation for linked chunk id as primary key
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg a1c484fb6e refactor(indexeddb): expose hash_key fn in serializer for keys represented as bytes rather than strings
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg d2ecc77014 feat(linked chunk): derive ser/de traits for OwnedLinkedChunkId
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 2e86fbc234 feat(linked chunk): add display impl for LinkedChunkId
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg 64698eaf1a feat(linked chunk): add trait-based conversions between owned and borrowed linked chunk id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Michael Goldenberg f180a14c88 feat(linked chunk): expose OwnedLinkedChunkId::as_ref for use in other crates
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-19 14:46:05 +02:00
Benjamin Bouvier bb0d480f24 fix(ci): clean more space in the CI runner for the codecov space
An idea courtesy from the gentle folks at apache/arrow.
2025-08-19 14:25:13 +02:00
Richard van der Hoff 7af1d3ab0e Merge pull request #5539 from matrix-org/kaylendog/msc3414/crypto
feat(crypto): Add support for encrypted state events to `matrix-sdk-crypto`
2025-08-19 10:55:38 +01:00
Skye Elliot 13ee4c8098 tests(crypto): Document introduced tests and helper
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-19 10:17:14 +01:00
dependabot[bot] 7bbd02ca73 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 10:33:03 +02:00
dependabot[bot] e22a7a2ed5 chore(deps): bump bnjbvr/cargo-machete from 0.8.0 to 0.9.1
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 0.8.0 to 0.9.1.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/v0.8.0...v0.9.1)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 10:16:04 +02:00
dependabot[bot] 4aad2c6b07 chore(deps): bump crate-ci/typos from 1.35.0 to 1.35.4
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.35.0 to 1.35.4.
- [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.35.0...v1.35.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 10:14:26 +02:00
Skye Elliot 960162453c feat(base): Add EncryptionState::StateEncrypted
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-18 17:46:46 +02:00
Benjamin Bouvier 1ea2162012 chore(ci): add a search feature to try out the experimental-search branch in CI 2025-08-18 17:33:34 +02:00
Benjamin Bouvier 1f0151705a fix(search): make the experimental-search feature compile
And simplify the code for parsing in the event cache.
2025-08-18 17:33:34 +02:00
Skye Elliot 84ebbd913c feat: Add naive state key verification to OlmMachine
Modifies `OlmMachine::decrypt_room_event_inner` to call a new method
`OlmMachine::verify_packed_state_key` which, if the event is a state
event, verifies that the original event's state key, when unpacked,
matches the state key and event type in the decrypted event content.

Introduces MegolmError::StateKeyVerificationFailed and
UnableToDecryptReason::StateKeyVerificationFailed which are thrown when
the verification fails.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-18 15:56:48 +01:00
Skye Elliot 756d50737e feat(crypto): Add state event encryption methods to OlmMachine
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-18 15:56:45 +01:00
Skye Elliot c32877284c feat(crypto): Add GroupSessionManager::encrypt_state
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-18 15:56:44 +01:00
Skye Elliot 6260811ea5 feat(crypto): Add OutboundGroupSession::encrypt_state
This commit also refactors out what would be common code between
::encrypt and ::encrypt_state to a helper ::encrypt_inner.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-08-18 15:56:38 +01:00
dragonfly1033 4f0415d4d2 add .envrc to .gitignore
Signed-off-by: dragonfly1033 <42915850+dragonfly1033@users.noreply.github.com>
2025-08-18 15:20:31 +02:00
Benjamin Bouvier b43aac129b chore: address review comments 2025-08-18 15:10:50 +02:00
Benjamin Bouvier c019009d00 refactor(event cache): avoid deserializing the full event content to be sent, for extracting its thread root 2025-08-18 15:10:50 +02:00
Benjamin Bouvier a88d6b37dc feat(event cache): also subscribe to a thread if we've sent a message into it 2025-08-18 15:10:50 +02:00
Benjamin Bouvier 705d6f870e refactor(event cache): move the listening to linked chunk updates to its own function, and introduce a select! at the top level 2025-08-18 15:10:50 +02:00
Benjamin Bouvier 4fc28c4701 feat(multiverse): enable threading support for multiverse with subscriptions 2025-08-18 15:10:50 +02:00
Benjamin Bouvier 64eecd0aee test(event cache): add tests for automatic thread subscriptions 2025-08-18 15:10:50 +02:00
Benjamin Bouvier c25be8b070 feat(event cache): automatically subscribe to threads according to msc4306 semantics 2025-08-18 15:10:50 +02:00
Benjamin Bouvier 1554c9d8fa feat(room): add a new function that will only subscribe to a thread if needed 2025-08-18 15:10:50 +02:00
Benjamin Bouvier d568c07489 feat(event cache): add a way to subscribe to any room's linked chunk updates 2025-08-18 15:10:50 +02:00
dragonfly1033 13c30f6691 feat(sdk): Add creation of indexes and indexing of messages (#5505)
Integrate matrix-sdk-search into matrix-sdk.
When a room is joined, a corresponding index is created.
When a message is received via sync or via a back-pagination, it is
added to the corresponding room's index.

Signed-off-by: Shrey Patel shreyp@element.io
2025-08-18 14:52:09 +02:00
Benjamin Bouvier 0e70a2fdfb test(sdk): add a regression test for the thread relation forwarding for poll start events
This got fixed in Ruma, and the Ruma's bump in the parent commit
includes this fix.
2025-08-18 13:04:35 +02:00
Benjamin Bouvier d70d758861 chore: bump Ruma 2025-08-18 13:04:35 +02:00
fkwp eefa9ff556 added comments including reference to MSCs 2025-08-15 15:58:28 +01:00
fkwp 28a8603f42 Allow new state key string-packing format for widget mode 2025-08-15 15:58:28 +01:00
Skye Elliot ae7f0fe022 feat: Experimental encrypted state feature flag with CI support (#5537)
This PR makes some non-domain-specific changes across multiple crates
that are required for proper testing of features implemented for #5397.

* Adds a `experimental-encrypted-state-events` feature flag across the
SDK.
* Introduces a feature set into xtask to ensure feature-gated tests are
run during CI.
* Minor fix to a test that would otherwise fail with the newly
introduced CI.
2025-08-15 12:54:41 +00:00
Skye Elliot d9f4e7c426 Merge pull request #5511 from kaylendog/kaylendog/room-settings
feat(crypto): Add RoomSettings::encrypt_state_events
2025-08-14 15:51:19 +01:00
Benjamin Bouvier 247ec1dcd2 refactor(event cache): shorten the name of room_event_cache_generic_update
We're in the inner workings of the event cache, so the prefix is
redundant, in the ambient context.
2025-08-14 13:05:57 +02:00
Benjamin Bouvier 558d7b56f9 refactor(event cache): get rid of RoomEventCacheGenericUpdate::Clear
The semantics of this variant are unclear: sometimes a timeline could be
cleared, which would result in a `UpdateTimeline` (and if we looked at
the vector diffs, it would include a `Clear`), but in this case the
`Clear` variant would not be emitted.

It was only emitted in a few adhoc spots, but it was missing the whole
picture. Also, the current observer was only interested in getting *a*
room update with the room id, and didn't react particularly to clears.
So, there's apparently little reason in having this variant, and as a
result we should get rid of it.
2025-08-14 13:05:57 +02:00
dragonfly1033 1201be484a fix!(sdk): Client::sync_once defaults to reuse previous token
Introduces a new `SyncToken` enum for the `SyncSettings::token` field.
The enum has 3 variants: ReusePrevious (default), NoToken, Specific(String).

Some tests were changed to use the old default (NoToken).
2025-08-14 12:14:06 +02:00
Benjamin Bouvier 1ffc014621 chore(tests): make some tests less flaky
When I run these locally, they may now take more than 100ms to run, when
being run in parallel. Increase the timeout duration to 1s.
2025-08-14 09:18:15 +02:00
Benjamin Bouvier 9491757cad chore: check in the correct version of Ruma for ruma-signatures 2025-08-14 09:18:15 +02:00
Kévin Commaille 33df0422e8 Upgrade Ruma: profile response
Handle the changes to the Response of the get_profile endpoint. The
content of the response is private and fields must be accessed with
methods.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-14 08:57:52 +02:00
Kévin Commaille a3a239f999 Upgrade Ruma: revert StrippedState
Handle the previous breaking change that was reverted: `StrippedState`
was removed and `AnyStrippedStateEvent` is used again.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-14 08:57:52 +02:00
Skye Elliot ca8b64e041 feat: Change type of DecryptedRoomEvent::event to Raw<AnyTimelineEvent> (#5512)
- [x] Change `DecryptedRoomEvent::event` to `Raw<AnyTimelineEvent>`
- [x] Update usages to pattern match on `AnyTimelineEvent::MessageLike`
where necessary

---------

Signed-off-by: kaylendog <actuallyori@gmail.com>
2025-08-14 08:53:56 +02:00
Stefan Ceriu 140e751af0 feat(timeline): consider unthreaded read receipts for ReceiptThread::Main timelines when computing timeline items states.
This patch updates the Timeline Controller's `handle_explicit_read_receipts` method to also consider unthreaded read receipts on **threaded** **main** timelines when calculating timeline items states and adds a test for it.

This picks up from #5442 and fixes https://github.com/matrix-org/matrix-rust-sdk/issues/5440
2025-08-14 05:24:20 +00:00
multisme a66b2c5123 feat(test): add a test utils crate to make log initialization possible everywhere
This PR allows `init_tracing_for_test` to be called by any other crate in the sdk

Signed-off-by: multi [multiestunhappydev@gmail.com](mailto:multiestunhappydev@gmail.com)
2025-08-14 05:24:03 +00:00
Copilot 69bef9a76a feat(sdk,ffi): Add server_vendor_info method to matrix-sdk with automatic logging in FFI
Add a new `server_vendor_info` method on the `matrix-sdk` `Client` that calls the `/_matrix/federation/v1/version` endpoint to retrieve the server's software name and version information.

Also add it to the bindings + log it when initializing the logs.
2025-08-14 05:14:25 +00:00
Michael Goldenberg b3c53dd08f test(indexeddb): run time-based integration tests on event cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Michael Goldenberg c8bffa26a4 test(event-cache-store): make time-based integration tests compatible with wasm targets
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Michael Goldenberg b4ef6cef55 refactor(indexeddb): add IndexedDB-backed impl of EventCacheStore::try_take_leased_lock
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Michael Goldenberg c6854a5c22 refactor(indexeddb): add support for indexing time-based locks in event cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Michael Goldenberg fb563953c9 refactor(indexeddb): add object store for tracking time-based lock on event cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Michael Goldenberg bc0018aecb refactor(indexeddb): add type to represent time-based lock on event cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-13 16:00:48 +02:00
Johannes Marbach 12292c5375 feat(ffi): allow specifying thumbnails using UploadSource
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-08-13 15:50:06 +02:00
Johannes Marbach cf9d058265 feat(ffi): allow specifying gallery items using UploadSource
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-08-13 15:30:34 +02:00
Benjamin Bouvier 4da13e1096 refactor!(ffi): use the send queue by default to upload medias
We do consider it stable now, after months of running it in production,
so let's use it by default to simplify the `UploadParameters`.
2025-08-13 15:16:16 +02:00
Benjamin Bouvier 333d4563ce refactor!(ffi): remove legacy progress upload tracking
This can now be achieved by using the send queue's global progress
support, i.e. `Client::enable_send_queue_upload_progress()`.
2025-08-13 15:16:16 +02:00
Andy Balaam 01059ef26c refactor(timeline): Make RoomDataProvider provide Decryptor to simplify redecryption 2025-08-13 12:40:07 +01:00
Kévin Commaille 7724271508 doc(test): Fix method auto-link
Rustdoc doesn't support passing parameters for auto-links.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Kévin Commaille 8dfe732cce doc(sdk): Fix method auto-link
Rustdoc doesn't do multi-level auto-link, so we have to provide the link
manually.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Kévin Commaille 1cf3477ada feat(crypto): Implement Default for SecretStorageKey
For the new_without_default clippy lint.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Kévin Commaille 0a2205f540 refactor(ffi): Remove dead code
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Kévin Commaille c586812159 refactor(crypto): Remove dead code
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Kévin Commaille c6210cad21 ci: Upgrade the version of Rust nightly
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-13 12:31:22 +01:00
Ivan Enderlin a9ce1c6e58 doc(sdk): Fix CHANGELOG.md entry.
The link for an entry was wrong. It's #5439, not #5442.
2025-08-13 10:08:50 +02:00
Kévin Commaille 1eb8f6ac16 Fix shared history test
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-12 17:25:22 +03:00
Kévin Commaille e0feebdb2b fix(sdk): Support unauthenticated media endpoint in Client::load_or_fetch_max_upload_size
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-12 17:25:22 +03:00
Kévin Commaille 0fee716c1e fix(sdk): Override timeout of media downloads for unauthenticated media endpoints too
There is no reason to treat them differently.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-12 17:25:22 +03:00
Kévin Commaille c41ed8a78a refactor(sdk): Use Ruma support for (un)stable feature flags for authenticated media
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-12 17:25:22 +03:00
Benjamin Bouvier 53f02c9f2d chore: bump Ruma
So as to get some changes for Element Call:
https://github.com/ruma/ruma/pull/2176
2025-08-12 16:06:26 +02:00
Johannes Marbach e2f0b4f3fd feat(ffi): expose media upload progress through EventSendState::NotSentYet
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-08-12 12:57:18 +02:00
Johannes Marbach 0a796cb468 feat(timeline): communicate media upload progress through EventSendState::NotSentYet
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-08-12 12:57:18 +02:00
copilot-swe-agent[bot] e3390c17ec docs: Add changelog entries for LowPriority and NonLowPriority filters
Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com>
2025-08-12 13:17:53 +03:00
Doug c6dc070c31 chore: Refactor non_space.rs filter to space.rs, using new_filter_not in the FFI. 2025-08-12 13:17:53 +03:00
copilot-swe-agent[bot] 486befc7fb feat: Add NonLowPriority to the FFI bindings
Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com>
2025-08-12 13:17:53 +03:00
copilot-swe-agent[bot] 9848d1472e feat: Add LowPriority filter implementation and FFI bindings
Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com>
2025-08-12 13:17:53 +03:00
Damir Jelić 6c944a9b39 Add a changelog entry for the sender data check when accepting historic room keys 2025-08-08 15:56:13 +02:00
Damir Jelić b4b010f9fe fix(crypto): Do a keys query before we accept historic room key bundles 2025-08-08 15:56:13 +02:00
Damir Jelić 536ba518bb feat(crypto): Check sender data before accepting room key bundles 2025-08-08 15:56:13 +02:00
Damir Jelić 917c46b570 chore: Remove a stale TODO comment 2025-08-08 15:56:13 +02:00
Damir Jelić b29886c0df test(crypto): Add a test that we refuse bundles if the sender isn't trusted enough 2025-08-08 15:56:13 +02:00
Damir Jelić 360c2d7f32 refactor(crypto): Turn should_recalculate function into an associated function for SenderData
This allows us to use the function in more places where SenderData is
used.
2025-08-08 15:56:13 +02:00
dragonfly1033 683f0f4027 feat(multiverse): Add room creation to multiverse 2025-08-08 12:33:48 +01:00
Damir Jelić c783ed8a6f Log a special error when try to upload duplicate one-time keys 2025-08-08 10:35:52 +02:00
Damir Jelić 139673810f Remember the public Curve25519 key of the sender of the historic room key bundle 2025-08-08 09:19:19 +02:00
Kévin Commaille 669ebf2408 refactor(base): Don't take room ID in Notification::push_notification_from_event_if
It is already in the `PushConditionRoomCtx`, so we don't need an extra
argument for it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-07 16:02:56 +02:00
dragonfly1033 992774b8b5 Add the matrix-sdk-search crate
A new crate with a basic API for a creating, populating and searching a
room message index.

Signed-off-by: Shrey Patel shreyp@element.io
2025-08-07 16:01:41 +02:00
Kévin Commaille 9d90a92b4c doc(base): Use different Result type in doc example
`anyhow::Ok(())` doesn't work under WASM because it requires the error
types to be `Send + Sync`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-07 15:11:30 +02:00
Michael Goldenberg d79975e0e3 refactor(indexeddb): simplify IndexeddbEventCacheStoreTransaction::get_events_by_related_event
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 2e598c0532 refactor(indexeddb): remove unnecessary fn IndexedEventRelationKey::with_related_event_id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 5a3ef30fdc refactor(indexeddb): remove redundant room id arg on relevant serializer and transaction fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 05178ccaf9 refactor(indexeddb): make room id a key component rather than fixed arg to IndexedKey::encode
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 65b9bd20a8 refactor(indexeddb): express IndexedKeyRange::All as IndexedKeyRange::Bound to loosen constraints on various functions
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 35505f9130 refactor(indexeddb): remove room id argument from Indexed::to_indexed
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg a6d630216d refactor(indexeddb): add room id to event_cache_store::types::Gap
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 159c9b4547 refactor(indexeddb): add room id to event_cache_store::types::GenericEvent
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg aead1a4489 refactor(indexeddb): add room id to event_cache_store::types::Chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 7fee1c7fd7 refactor(indexeddb): add traits for constructing prefix key bounds
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg ab9bfb2d61 refactor(indexeddb): add reusable const and static values to construct key component bounds
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg de5f00fd33 refactor(indexeddb): use references in IndexedEventRelationKey::KeyComponents
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg 33c16b2979 refactor(indexeddb): use references in IndexedEventIdKey::KeyComponents
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Michael Goldenberg e9dcdb7176 refactor(indexeddb): add lifetime to IndexedKey::KeyComponents
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-08-07 12:53:32 +02:00
Benjamin Bouvier 0a3fe939c5 refactor(event cache): don't panic when loading a linked chunk's latest event fails
This can happen because of an OS-level error, so let's try to handle it
in a slightly cleaner way. At the moment, it will bubble up and only be
logged, but we might try to find a better way to handle this at the
top-level.
2025-08-07 12:17:15 +02:00
Benjamin Bouvier 37e07ea331 refactor(test): use the matrix mock server in test_notification_client_with_context 2025-08-07 11:59:36 +02:00
Benjamin Bouvier e4e3ff63f5 refactor(notification client): extract the common filtering out of events in a common helper 2025-08-07 11:59:36 +02:00
Benjamin Bouvier 8409e52654 doc(base): tweak some doc comment around notifications 2025-08-07 11:59:36 +02:00
Benjamin Bouvier e8096ee518 refactor(notification client): simplify get_notification_with_sliding_sync 2025-08-07 11:59:36 +02:00
Jonas Platte 6814e70aa4 refactor: Simplify some methods of FailuresCache
Follow-up to #5490.
2025-08-07 10:00:53 +02:00
multisme efa4539a91 refactor(client): have upload_encrypted_file own the Client instance (#5470)
Signed-off-by: multi multiestunhappydev@gmail.com
2025-08-07 09:51:23 +02:00
Jonas Platte 42d2b93489 refactor: Introduce TestResult and use it in a couple random places 2025-08-06 22:21:39 +00:00
Kévin Commaille 872713c4bc Add setting to ignore the timeout sync setting on first sync (#5481)
The `timeout` setting on the `/sync` endpoint is the maximum allowed
time for the server to send its response, because this is a poll-based
API. It means that if there is no new data to show, the server will wait
until the end of `timeout` before returning a response.

It can be an undesirable behavior when starting a client and informing
the user that we are "catching up" while waiting for the first response.

By not setting a `timeout` on the first request to `/sync`, the
homeserver should reply immediately, whether the response is empty or
not.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
Signed-off-by: Ivan Enderlin <ivan@mnt.io>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2025-08-06 16:48:44 +02:00
Kévin Commaille feb22d4370 Move serde functions to module and use #[serde(with = "")]
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-06 16:44:51 +02:00
Kévin Commaille 6520c9b16e Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-06 16:44:51 +02:00
Kévin Commaille cd6fe271ba refactor(crypto): Make deprecated sender_key and device_id optional in RoomEncryptedEventContent and RoomKeyRequestContent
They were deprecated in Matrix 1.3 and are now optional.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-06 16:44:51 +02:00
Jonas Platte 5f447bbb17 chore: Fix new clippy lints 2025-08-06 16:38:08 +02:00
Jonas Platte 94e7ddd1ab chore: Upgrade matrix-sdk to edition 2024 and format 2025-08-06 16:38:08 +02:00
Jonas Platte 6ac4a8431d chore: Prepare matrix-sdk-common for edition 2024
Cherry-picked some changes from cargo fix --edition.
2025-08-06 16:38:08 +02:00
Benjamin Bouvier b585963abb test(notification client): check msc4306 behavior in the notification client too 2025-08-06 15:28:43 +02:00
Benjamin Bouvier 5719fde701 feat(client): add a global toggle for enabling thread subscriptions support 2025-08-06 15:28:43 +02:00
Benjamin Bouvier 2914d7a727 feat(threads): provide has_thread_subscription_fn to push condition context 2025-08-06 15:28:43 +02:00
Benjamin Bouvier 0cdec9d912 refactor(threads): flatten the ThreadStatus enum 2025-08-06 15:28:43 +02:00
Benjamin Bouvier d180d49c07 refactor(threads): do not store the unsubscribed state in the DB 2025-08-06 15:28:43 +02:00
Benjamin Bouvier bcee5badae refactor(threads): adapt to Ruma API changes related to async evaluation of push rules 2025-08-06 15:28:43 +02:00
Benjamin Bouvier ebb7059d55 refactor(threads): adapt to Ruma API changes for thread subscriptions 2025-08-06 15:28:43 +02:00
Benjamin Bouvier 8d3b1d3c7e chore: update Ruma 2025-08-06 15:28:43 +02:00
Kévin Commaille 056e90db25 feat(sdk): Use state_after in sync v2 (#5488)
It is supposed to be an improvement over `state`, since it allows the
server to send updates to the state that might not be reflected in the
timeline.

This is also the same behavior as in Simplified Sliding Sync.

This is MSC4222 that was accepted and is about to get merged in the
spec.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-06 13:42:49 +01:00
Kévin Commaille 787861eb35 fix: Upgrade Ruma
This brings 2 important bug fixes:

- Make deprecated fields of `m.room.encrypted` optional: it seems that there are events without these fields in the wild.
- Fix deserialization of `RedactedRoomJoinRulesEventContent`. This was found by a bug report in Fractal that caused the same error as #3557 when restoring the client. So maybe we could consider that this bug is fixed? It is still possible that there is another deserialization error. 

There is also a breaking change in the format of the `state` field in response to `GET /v3/sync`.
2025-08-05 16:04:34 +02:00
dependabot[bot] f081416baa chore(deps): bump crate-ci/typos from 1.34.0 to 1.35.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.34.0 to 1.35.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.34.0...v1.35.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-05 10:02:48 +02:00
Jorge Martín 8a6cc7bc22 refactor(sdk): Only log the power levels computing error for joined rooms 2025-08-05 09:54:26 +02:00
Jorge Martín 61258e823f test: Fix and add tests for computing the push conditions requiring the room creation event 2025-08-05 09:54:26 +02:00
Jorge Martín e86aab68b4 fix(sdk): Make sure we log the error when we can't compute the power levels of a room 2025-08-05 09:54:26 +02:00
Jorge Martín 48f1bc0780 fix(notification_client): Add m.room.create to the required sliding sync state
With this missing, power levels couldn't be computed, and some push rules didn't take effect.
2025-08-05 09:54:26 +02:00
Stefan Ceriu 1fe71acbcb change(room_list): request space rooms through sliding sync and expose a room list filter for them (#5479)
This is a breaking change as spaces are now requested through sliding sync and they need to manually be excluded from the room list by using the newly introduced non-space filter.
2025-08-04 16:39:02 +02:00
Kévin Commaille 0e054deb19 fix(sdk): Allow legacy push rules to be missing when changing NotificationSettings
They are being removed from the spec with MSC4210, so we can't error if they are not found.
2025-08-04 13:40:01 +00:00
Doug d2b7dc6116 ffi: Export TimelineDiff as uniffi:Enum to match RoomDirectorySearchEntryUpdate & RoomListEntriesUpdate.
The difference in API shape has been weird for long enough.
2025-08-04 12:01:24 +02:00
Ivan Enderlin 1089a25588 fix(xtask): Use --package instead of -p on cargo ndk.
We use `cargo ndk -p {package_name}`, where `-p` is short for
`--package`. However, `cargo ndk` has introduced the `-p` option (see
https://github.com/bbqsrc/cargo-ndk/commit/c6b93a89a2723ff0fac99b5ac86ae6636a6cf54a),
short for `--platform`. It creates a confusion and the command line
doesn't execute properly. Let's use the long option `--package` to
clarify everything.
2025-08-04 10:41:17 +02:00
Kévin Commaille 3276bc87ad refactor(base): Don't drop whole m.room.create if predecessor is invalid
The `m.room.create` event contains at lot of important information for a
room, like its type (i.e. whether it is a space), its version and its
creator(s) (which are important in room version 12). So ignoring the
event completely might break a room.

Instead what we do here is simply ignore the `predecessor` field if it
is considered invalid, allowing us to access the other fields.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-04 10:09:52 +02:00
Hubert Chathi a4da6ba7c8 Exclude insecure devices on Olm encryption (#5457)
Fixes the encrypting part of
https://github.com/matrix-org/matrix-rust-sdk/issues/4147

Probably easiest to review commit-by-commit

<!-- description of the changes in this PR -->

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

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:
2025-08-04 08:50:32 +01:00
Kévin Commaille 033c6bd6a4 Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-01 14:07:28 +02:00
Kévin Commaille b02e1da471 Upgrade Ruma
This brings in a new breaking change from Ruma, because not all events
are stripped in a room's stripped state. For simplicity, this still
considers the events as stripped during deserialization for now, since
this format is compatible with the other possible formats.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-08-01 14:07:28 +02:00
Damir Jelić 5ad477ac96 tests: Rename some more benchmarks 2025-08-01 11:33:08 +02:00
Damir Jelić 975432565d tests: Enable timers for the runtime of the store bench 2025-08-01 11:33:08 +02:00
Damir Jelić 46b0113765 tests: Rename some crypto benchmarks
This is mostly due to codspeed not showing the group name, so we're
repeating this info in the benchmark ID.
2025-08-01 11:33:08 +02:00
Damir Jelić 09eff8c6bd ci: Lower the amount of events we benchmark against on the CI 2025-08-01 11:33:08 +02:00
Damir Jelić 7ee546a3d9 ci: Enable more benchmarks 2025-08-01 11:33:08 +02:00
Benjamin Bouvier b164cd6a51 refactor(timeline): remove a few unused Self in some read-receipt related methods 2025-07-31 14:14:27 +02:00
Benjamin Bouvier 6d95abfb36 refactor(send queue): move the AbstractProgress to the new progress module 2025-07-31 13:04:28 +02:00
Benjamin Bouvier 33f09d6d26 refactor(send queue): move upload progress functionality to its own file 2025-07-31 13:04:28 +02:00
Benjamin Bouvier 8c01e99144 refactor(send queue): misc improvements to media upload progress reporting
Notable changes: don't send a global update whenever a media upload
progress happened, since this doesn't really matter for global
listeners.
2025-07-31 13:04:28 +02:00
Benjamin Bouvier 277cb7ac49 refactor(send queue): rename variables around the thumbnail file size cache 2025-07-31 11:16:48 +02:00
Johannes Marbach fc7124fd1a feat(send_queue): communicate media upload progress in RoomSendQueueUpdate::MediaUpload
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach 30c0420f83 chore(send_queue): rename RoomSendQueueUpdate::UploadedMedia to MediaUpload to prepare it for communicating upload progress
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach cb13c345ad feat(sdk): introduce AbstractProgress for tracking media progress in pseudo units
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach cd26973082 feat(send_queue): enable progress monitoring in RoomSendQueue::handle_request
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach 0edcdd33b2 chore(send_queue): Make parent_is_thumbnail_upload available outside of unstable-msc4274 to use it during media progress reporting later
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach c191eb7cd1 feat(send_queue): cache thumbnail sizes to use them in progress reporting later
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach fa6f270812 chore(send_queue): collect thumbnail-related metadata in a dedicated QueueThumbnailInfo struct for easier extension
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Johannes Marbach d4e96595d9 feat(send_queue): add global setting for sending media progress updates
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-31 11:16:48 +02:00
Jakob Lachermeier 540a11e7a8 fix(sqlite): made open_with_pool public again. 2025-07-31 09:15:47 +02:00
Damir Jelić 92192c549b fix(timeline): Remove UTD items that have been decrypted into unsupported events 2025-07-30 12:46:04 +02:00
Damir Jelić 88360040fb test(timeline): Add some integration tests for the redecryption logic
These tests now fully mock all the end-to-end encryption server
endpoints to test the redecryption and UTD item replacement logic of the
timeline without any manual room key insertions.

We test that the item replacement correctly handles supported event
types as well as unsupported ones.
2025-07-30 12:46:04 +02:00
Damir Jelić 4184e245a4 ci: Attempt to free up even more space for the coverage job 2025-07-30 12:10:12 +02:00
Benjamin Bouvier f37bf2f5d1 feat(store): also delete thread subscriptions when deleting a room in db 2025-07-30 12:07:07 +02:00
Benjamin Bouvier d57d3c4124 feat(sdk): save the unsubscribed status in the store, and use it to return something more precise than unknown when fetching a subscription 2025-07-30 12:07:07 +02:00
Benjamin Bouvier 1a5cb2beb8 feat(stores): allow saving thread subscriptions 2025-07-30 12:07:07 +02:00
Benjamin Bouvier b645c1101f refactor(test): avoid proliferation of builder submethods in the MockClientBuilder
Instead of having one static method duplicating an underlying
`ClientBuilder` method, we can pass the builder directly to a closure,
that will replace it. Call sites are a bit more verbose, but that would
avoid having to add duplicate `MockClientBuilder` methods for each
`ClientBuilder` method.
2025-07-30 11:56:31 +02:00
Robin 8091094bbc refactor(room): Remove methods for sending call notifications
As noted in the changelog entry, the event type they send is outdated, and Client is not actually supposed to be able to join MatrixRTC sessions at this time. A MatrixRTC client implementation which actually participates in sessions should be able to send these notifications without the SDK's help.
2025-07-29 15:04:25 +02:00
Robin feadfde1b5 feat(element-call): Support the sendNotificationType URL parameter
This URL parameter allows us to request Element Call widgets to send a notification when starting a call.
2025-07-29 15:04:25 +02:00
Damir Jelić e2ad07881c Merge pull request #5443 from matrix-org/poljar/ci/benchmarks
Enable benchmarks on the CI
2025-07-28 13:07:00 +02:00
Jorge Martín 1be8b42d03 refactor(ffi): expose privileged_creators_role in the FFI RoomInfo 2025-07-28 12:05:48 +02:00
Jorge Martín 7a5f83f6ec refactor(ffi): expose room_version in the FFI RoomInfo 2025-07-28 12:05:48 +02:00
Jorge Martín 88bb7a366f feat(ffi): enable unstable-hydra feature for the SDK bindings 2025-07-28 11:42:21 +02:00
Benjamin Bouvier 7d9bf56581 feat(ffi): add support for MSC4036 thread subscriptions 2025-07-28 10:39:38 +02:00
Benjamin Bouvier 770f65ede0 feat(multiverse): add support for subscribing/unsubscribing/showing the current sub status 2025-07-28 10:39:38 +02:00
Benjamin Bouvier 1f33e0f4d1 test(threads): add test for the thread subscription endpoints 2025-07-28 10:39:38 +02:00
Benjamin Bouvier 117f76102d test(threads): add mocks for the thread subscription endpoint 2025-07-28 10:39:38 +02:00
Benjamin Bouvier fd04ebfaba feat(sdk): add support for threads subscription CRUD (msc4306)
This doesn't store the subscriptions locally, nor does it implement the
automatic thread subscription.
2025-07-28 10:39:38 +02:00
Benjamin Bouvier afe9f7a979 chore: upgrade ruma for msc4306 2025-07-28 10:39:38 +02:00
Benjamin Bouvier 27a002c8e2 chore: add changelog entry for the breaking change in RelationalLinkedChunk::items 2025-07-28 10:34:56 +02:00
Benjamin Bouvier b8ab0972b3 fix(event cache store): return events from all linked chunks in a room, in find_event and find_event_with_relations of the memory store 2025-07-28 10:34:56 +02:00
Benjamin Bouvier d3419ea4ac test(event cache store): add tests for a thread's vs a room's linked chunk id 2025-07-28 10:34:56 +02:00
Benjamin Bouvier 019adb9a56 feat(common): implement the thread variants for LinkedChunkId and OwnedLinkedChunkId 2025-07-28 10:34:56 +02:00
Kévin Commaille ca89700dfe refactor(ffi): Fix clippy lint
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-25 17:55:31 +02:00
Kévin Commaille a0c87cfe4f Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-25 17:36:19 +02:00
Kévin Commaille 9ddc892aa0 fix(sdk): Support event types with variable suffix for event handlers
Previously they would just not work as we were trying to match a full
event type with a prefix.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-25 17:36:19 +02:00
Kévin Commaille 0a7ac18d9f refactor: Add IsPrefix = False bound on StaticEventContent bounds
Since those APIs only support a full event type, not an event type prefix.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-25 17:36:19 +02:00
Robin d8b6966c0a Allow Element Call to send call notifications
For clients which integrate Element Call: Currently matrix-rust-sdk is responsible for sending the call notification event when joining MatrixRTC sessions, but this is planned to be changed soon. As of the upcoming Element Call 0.14.0 release, it will request the capability to send call notifications itself, and we should auto-approve this capability.
2025-07-25 09:20:39 +03:00
Damir Jelić d40f04e32c chore: Remove the criterion function in the benchmarks
It's now equivalent to Criterion::default().
2025-07-24 17:11:03 +02:00
Damir Jelić d8294a0788 Fix the compilation of the crypto bench 2025-07-24 17:09:59 +02:00
Damir Jelić 06a4476e7f ci: Disable a benchmark that panics on the CI 2025-07-24 16:48:28 +02:00
Andy Balaam def1fedea3 feat(crypto): Refuse to decrypt to-device messages from unverified devices (when in exclude insecure mode) 2025-07-24 15:08:13 +01:00
Andy Balaam d061e7a5b2 refactor(tests): Pass decryption_settings in to send_and_receive_encrypted_to_device_test_helper
To allow passing in different values in future tests.
2025-07-24 15:08:13 +01:00
Andy Balaam f4619c91d3 refactor(tests): Re-use send_and_receive_encrypted_to_device_test_helper in 2 more tests 2025-07-24 15:08:13 +01:00
Andy Balaam 227f6eab85 refactor(tests): Take a reference to content in send_and_receive_encrypted_to_device_test_helper
This will allow us to re-use it in more tests.
2025-07-24 15:08:13 +01:00
Andy Balaam 16d7c3c094 refactor(crypto): Extract a method to check for to-device events from dehydrated devices 2025-07-24 15:08:13 +01:00
Andy Balaam c238a0edb8 refactor(crypto): Pass DecryptionSettings in to OlmMachine::decrypt_to_device_event
This will be used in the next commit, but it was very noisy, so I
separated it out into this commit to make the next one easier to read.
2025-07-24 15:08:13 +01:00
Damir Jelić 06bf487512 chore: Attempt to get rid of a crash on CI where the runtime isn't used for a drop 2025-07-24 15:40:41 +02:00
Damir Jelić c636ec63f4 chore: Inherit the release profile for the bench profile 2025-07-24 15:07:48 +02:00
Damir Jelić ffe239d620 Actually respect the benchmarks matrix on CI 2025-07-24 14:40:14 +02:00
dragonfly1033 822b709107 feat(sdk): Leaving a room now leaves its predecessors as well
Signed-off-by: Shrey Patel shreyp@element.io
2025-07-24 13:56:05 +02:00
Damir Jelić d75d7973b2 ci: Enable benchmarks on the CI 2025-07-24 13:37:45 +02:00
Damir Jelić cfe3adce48 chore: Bump criterion 2025-07-24 13:37:45 +02:00
Damir Jelić b478ae65f7 tests: Remove pprof 2025-07-24 13:37:45 +02:00
Michael Goldenberg ada68e1114 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::find_event_relations
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-23 18:03:06 +02:00
Michael Goldenberg c9137f0cad fix(indexeddb): Updates::PushItems performs an update if any provided item already exists
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-23 18:03:06 +02:00
Michael Goldenberg 4e0dab959a feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::save_event
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-23 18:03:06 +02:00
Michael Goldenberg e862ded147 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::find_event
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-23 18:03:06 +02:00
Michael Goldenberg 5cb033ad91 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::filter_duplicated_events
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-23 18:03:06 +02:00
Kévin Commaille 0e622cc5a1 Upgrade Ruma (phase 3)
This upgrade introduces support for room version 12[1].

[1]: https://matrix.org/blog/2025/07/security-predisclosure/)

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-23 14:32:05 +00:00
Jorge Martín 6d562eff2f fix: Restore the previous behaviour for calculating the timeout for syncs 2025-07-23 15:55:31 +02:00
Jorge Martín af2e15e02f refactor(ffi): Add the default read_timeout value to the FFI layer 2025-07-23 15:55:31 +02:00
Jorge Martín 79aa5aaf16 refactor: Remove the default timeout when downloading media, rely on read_timeout instead
Users with poor network connectivity complained their downloads were cancelled for no good reason after 30s before (the default `timeout` value).
2025-07-23 15:55:31 +02:00
Jorge Martín 0833ffdef2 refactor: Add RequestConfig::read_timeout to cancel network connections that have been stale for longer than the specified timeout 2025-07-23 15:55:31 +02:00
Jorge Martín 16f7239215 refactor: Make RequestConfig::timeout optional
This allows us to remove the default timeout on a per-request basis
2025-07-23 15:55:31 +02:00
Doug da946e51dd ffi: Update Client::get_url to return raw data and to throw on HTTP errors
Client::get_url is there for SDK consumers to be able to use the
existing HTTP stack (configuration and all) however in practice it was a
bit weird:
- Responses came back as strings limiting the types of resource that
could be fetched (as well as requiring the string to be converted back
to data before handed to a JSON decoder).
- HTTP errors weren't being raised and instead you would find the (e.g.
404) error response in the Ok case.

This patch fixes both of these issues.
2025-07-23 15:24:49 +02:00
Benjamin Bouvier cba711dbdf feat(timeline): add support for sending sticker/polls in thread automatically too 2025-07-23 15:05:23 +02:00
Benjamin Bouvier e87e9331c1 feat!(timeline): infer the reply type automatically when sending a media gallery 2025-07-23 15:05:23 +02:00
Benjamin Bouvier 6e9fc70d13 refactor(timeline): homogeneize naming of replied_to vs in_reply_to 2025-07-23 15:05:23 +02:00
Benjamin Bouvier e2148e46bc feat!(timeline): infer the reply type automatically when sending an attachment 2025-07-23 15:05:23 +02:00
Benjamin Bouvier d1163b75bf feat!(timeline): Timeline::send_reply() automatically includes the thread relationship too 2025-07-23 15:05:23 +02:00
Benjamin Bouvier 5ae7d0f60f feat(timeline): Timeline::send() automatically includes the thread relationship for thread foci 2025-07-23 15:05:23 +02:00
Benjamin Bouvier 6f067d5510 doc(event cache): add a code comment indicating why room updates processing isn't happening concurrently 2025-07-23 11:21:09 +02:00
Benjamin Bouvier d6fe654814 bench: add a benchmark for measuring the time it takes to handle a sync update in the event cache 2025-07-23 11:21:09 +02:00
Benjamin Bouvier 2710510786 refactor(base): use let chains where the comment said to do so \o/ 2025-07-23 09:57:50 +02:00
Benjamin Bouvier 35a8528712 chore(base): clippy fixes 2025-07-23 09:57:50 +02:00
Benjamin Bouvier f9735c75d3 chore(base): rustfmt 2024 edition 2025-07-23 09:57:50 +02:00
Benjamin Bouvier 15e6b81835 chore(base): upgrade matrix-rust-sdk-base to edition 2024 2025-07-23 09:57:50 +02:00
Damir Jelić bcb4ab4b10 contrib: Set our check command to check instead of clippy
Clippy is a bit too heavy to be used as the check on save command in
this repo.

Let's set a better default.
2025-07-22 15:20:37 +02:00
Kévin Commaille 4931c0749e Upgrade Ruma again
This patch updates our `Raw` API usage since the newly added `JsonCastable` that disallows Raw casts that are known to fail deserialization. 

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-22 12:59:26 +00:00
Kévin Commaille 37626b5ad9 Bump Ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-22 14:00:53 +02:00
Benjamin Bouvier d19616da03 chore!: bump the MSRV to 1.88
let-chains ftw
2025-07-22 12:15:33 +02:00
Ivan Enderlin 7c8f870d16 Revert "fix(sdk): Disable OrderTracker for the moment."
This reverts commit c5f2460e02.
2025-07-22 10:24:27 +02:00
Benjamin Bouvier b482ccd318 feat(sqlite): make sqlite's implementation of load_all_chunks_metadata even faster
See the updated code comment.
2025-07-21 17:41:06 +02:00
Ivan Enderlin 165ec9db1b bench: Add a benchmark for EventCacheStore::load_all_chunks_metadata. 2025-07-21 10:31:44 +02:00
Ivan Enderlin 6f42210d6a feat(sqlite): Improve throughput of load_all_chunks_metadata by 1140%.
This patch changes the query used by
`SqliteEventCacheStore::load_all_chunks_metadata`. It was the cause of
severe slowness. The new query improves the throughput by +1140% and the
time by -91.916%. The benchmark will follow in the next patch.

Metrics for 10'000 events (with 1 gap every 80 events).

- Before:
  - throughput: 20.686 Kelem/s,
  - time: 483.43 ms.
- After:
  - throughput: 253.52 Kelem/s,
  - time: 39.478 ms.

This query will visit all chunks of a linked chunk with ID
`hashed_linked_chunk_id`. For each chunk, it collects its ID
(`ChunkIdentifier`), previous chunk, next chunk, and number of
events (`num_events`). If it's a gap, `num_events` is equal to 0,
otherwise it counts the number of events in `event_chunks` where
`event_chunks.chunk_id = linked_chunks.id`.

Why not using a `(LEFT) JOIN` + `COUNT`? Because for gaps, the entire
`event_chunks` will be traversed every time. It's extremely inefficient.
To speed that up, we could use an `INDEX` but it will consume more
storage space. Finally, traversing an `INDEX` boils down to traverse a
B-tree, which is O(log n), whilst this `CASE` approach is O(1). This
solution is nice trade-off and offers great performance.
2025-07-21 10:31:44 +02:00
Kévin Commaille 6125580275 feat: Add Account::fetch_account_data_static
It uses the statically-known type from the `StaticEventContent`
implementation to call `fetch_account_data()`.

This is the equivalent of `Account::account_data()` but for fetching
from the server. It avoids the need to cast to the proper type after.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-18 16:01:07 +02:00
Kévin Commaille 8b3e295429 refactor(test): Use EventBuilder::into_raw_timeline rather than into_event
We need the full event so its better to build it an cast it to a sync
event than the opposite.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-18 16:01:07 +02:00
Kévin Commaille 1e568efbb5 refactor: Remove unnecessary Raw casting
The types are already correct so there is no need for casting.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-18 16:01:07 +02:00
Andy Balaam f89ced3ded task(sliding_sync): Avoid logging the entire sliding sync response at the DEBUG level
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2025-07-18 13:18:04 +00:00
Damir Jelić a5fbcf1491 Merge pull request #5322 from matrix-org/poljar/shared-history/out-of-order
feat(sdk): Add a tasks that listens for historic room keys if they arrive out of order
2025-07-17 16:52:13 +02:00
Yorusaka Miyabi 8923e58ee3 feat(ffi): Expose legacy SSO support infomation
Currently Element X can't distinguish the cases where a homeserver only
supports legacy SSO without OIDC (and password login isn't avaliable),
and other server unreachable scenarios.

This patch exposes legacy SSO support infomation so that Element X side
can give a dedicated error message when it encounters a homeserver that
can only support legacy SSO.

Signed-off-by: Yorusaka Miyabi <23130178+ShadowRZ@users.noreply.github.com>
2025-07-17 16:42:27 +02:00
Damir Jelić f14994baa9 test(sdk): Test if we accept historic room key bundles arriving out of order 2025-07-17 16:39:31 +02:00
Damir Jelić d42d0f3e17 test(sdk): Add a bunch more useful mock helpers 2025-07-17 16:39:17 +02:00
Damir Jelić e4849d5cab test(sdk): Don't expect a default access token in a bunch of methods
The mocks can be configured to expect a default access token separately,
this seems to have been a copy/paste error.
2025-07-17 16:39:17 +02:00
Damir Jelić 65aec7ee7f test(sdk): Add a method for two test clients to exchange E2EE identities 2025-07-17 16:39:17 +02:00
Damir Jelić a6868386d0 test(sdk): Allow the test client to enable shared history 2025-07-17 16:39:17 +02:00
Damir Jelić b3c1ca1577 Use the invite details when deciding if we should accept a bundle 2025-07-17 16:39:17 +02:00
Damir Jelić ce66ee4a16 test(sdk): Test the conditions under which we accept a historic room key bundle 2025-07-17 13:41:30 +02:00
Damir Jelić 2bbf6fc711 feat(sdk): Add a tasks that listens for historic room keys if they arrive out of order
Historic room key bundles are uploaded as an encrypted file to the media
repo and the key to decrypt the file is sent as a to-device message to
the recipient device.

In the nominal case, the invite and this to-device message should arrive
at the same time and accepting the invite would download and import the
bundle.

If the to-device message arrives after the invite has already been
accepted we would never download and import the bundle.

To mitigate this problem, this patch introduces a task that listens for
bundles that arrive. If the bundle is for a room that we have joined we
will consider importing the bundle.
2025-07-17 13:41:30 +02:00
Kévin Commaille 1a9e5b904b Move changelog entry to the top
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-17 12:42:05 +02:00
Kévin Commaille 4c43b06445 Add changelogs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-17 12:42:05 +02:00
Kévin Commaille 49a0765880 refactor(base): Remove the event_id field of PredecessorRoom
It is going away in MSC4291, which should be accepted next week.
Removing it now makes sure that no one uses it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-17 12:42:05 +02:00
Florian 39cf8b325d Allow requesting additional scopes for OAuth2 authorization code flow
For custom integrations it might be necessary to allow the SDK to
request additional scopes for the OAuth2 authorization code flow.
Currently, only the MSC2967 client API and client device scopes are
requested statically.


Signed-off-by: fl0lli <github@fl0lli.de>
2025-07-16 10:23:02 +02:00
Jonas Platte bb67150d6b chore: Upgrade matrix-sdk-store-encryption to Rust Edition 2024 2025-07-16 09:36:10 +02:00
Michael Goldenberg 471e3c340c refactor(indexeddb): add timers to all EventCacheStore functions for easy performance tracking
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-15 16:02:10 +02:00
Michael Goldenberg 74972d8db7 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::load_all_chunks_metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-15 16:02:10 +02:00
Michael Goldenberg 03a76fbaf5 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::clear_all_linked_chunks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-15 16:02:10 +02:00
Michael Goldenberg 392a1ef47b feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::load_previous_chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-15 16:02:10 +02:00
Ivan Enderlin c5f2460e02 fix(sdk): Disable OrderTracker for the moment.
This patch removes the use of `OrderTracker` because the
implementation of `EventCacheStore::load_all_chunks_metadata` for
`SqliteEventCacheStore` is the cause of severe slownesses (up to 100s
for some account).

We are going to undo this patch once the problem has been solved.
2025-07-15 12:17:28 +02:00
fl0lli 8ad785a117 docs(ffi): move entry for device_id change of url_for_oidc to top
Signed-off-by: fl0lli <github@fl0lli.de>
2025-07-15 10:14:17 +02:00
fl0lli a5b936d0b6 docs(ffi): add breaking change entry for Client::url_for_oidc
Signed-off-by: fl0lli <github@fl0lli.de>
2025-07-15 10:14:17 +02:00
fl0lli 1a0544c8eb chore(ffi): formatting
Signed-off-by: fl0lli <github@fl0lli.de>
2025-07-15 10:14:17 +02:00
fl0lli 232c23e8df feat(ffi): allow setting existing device id for Client.url_for_oidc
Signed-off-by: fl0lli <github@fl0lli.de>
2025-07-15 10:14:17 +02:00
Jonas Platte 7d9d5bf3b4 refactor: Use if-let chain 2025-07-15 08:41:44 +02:00
Jonas Platte ea076b3d76 chore: Upgrade testing crates to Rust Edition 2024 2025-07-15 08:41:44 +02:00
Jonas Platte 8aa6f97f7c chore: Upgrade benchmarks to Rust Edition 2024 2025-07-15 08:39:27 +02:00
Jonas Platte 679aa07115 chore(ui): Upgrade to Rust edition 2024 2025-07-14 19:50:36 +02:00
Jonas Platte 0c66d8a53f refactor(ui): Cherry-pick some edition-fix changes
Automated with `cargo fix --edition -p matrix-sdk-ui`, reverting
unnecessary changes.
2025-07-14 19:50:36 +02:00
Michael Goldenberg 25ed7eef2b doc(indexeddb): add explanation of error types in EventCacheStore::load_last_chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-14 18:46:20 +02:00
Michael Goldenberg a399840dff refactor(indexeddb): separate transaction and event cache error conversions
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-14 18:46:20 +02:00
Michael Goldenberg 8d4e7f0478 test(indexeddb): add IndexedDB-specific integration tests for loading last chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-14 18:46:20 +02:00
Michael Goldenberg 3bd93130c5 feat(indexeddb): add IndexedDB-backed impl for EventCacheStore::load_last_chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-14 18:46:20 +02:00
Michael Goldenberg 153618b77c refactor(indexeddb): add helper fns for EventCacheStore::load_last_chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-07-14 18:46:20 +02:00
Kévin Commaille dd871ef9ac refactor(base): Store RoomPowerLevels in RoomMember
Avoids to carry around the event content only to convert it when we
want to use it. Avoids also to carry around the room creator when we
might not need it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-14 18:04:33 +02:00
Kévin Commaille 8a29f17d1d refactor(base): Call Room::power_levels instead of loading event from the store
To reduce code duplication.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-14 18:04:33 +02:00
Kévin Commaille fab520ab33 refactor(base): Add methods on StateChanges to get a member or power level event
To reduce duplication.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-14 18:04:33 +02:00
Ivan Enderlin 435553c3d1 feat(sdk): Add more logs in generate_sync_request. 2025-07-14 17:48:07 +02:00
Ivan Enderlin 610ecd218c feat(sdk): Log the time spent waiting on the position lock. 2025-07-14 17:48:07 +02:00
Kévin Commaille fa300d1f33 refactor(tests): Prefer EventBuilder::into_raw to into_raw_(sync/timeline) then Raw::cast
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-07-14 17:46:02 +02:00
Ivan Enderlin af2a483158 feat(sdk): Add debug! log when updating the sliding sync pos. 2025-07-14 16:52:36 +02:00
Ivan Enderlin 753b0d8584 doc(sdk): Update doc of SlidingSyncPositionMarkers::pos.
With `SlidingSync::share_pos`, the `pos` can be persisted. This comment
is outdated.
2025-07-14 16:40:06 +02:00
Ivan Enderlin 36713adbdb feat(sdk): Add more logs around the sync_lock and response_processor in SlidingSync. 2025-07-14 14:58:09 +02:00
Ivan Enderlin d73a02c608 feat(sqlite): Add more timer! logs in each EventCacheStore methods.
This patch adds `timer!` logs in each method from `EventCacheStore` for
`SqliteEventCacheStore`. It will help to know the execution duration of
each of these methods.
2025-07-14 10:34:17 +02:00
Ivan Enderlin f73199b472 feat(sqlite): Instrument SqliteEventCacheStore::open_with_config. 2025-07-14 10:34:17 +02:00
Ivan Enderlin 420d373144 feat(sqlite): Add #[instrument] around all SqliteEventCacheStore methods. 2025-07-14 10:34:17 +02:00
Ivan Enderlin a79e9130e6 feat(sqlite): Add timer! tracings in read and write's SqliteEventCacheStore. 2025-07-14 10:34:17 +02:00
Ivan Enderlin 355b5327f8 refator(common): Rename TracingTimer::new_debug.
This patch renames `TracingTiming::new_debug` to `new`. The
documentation claims it sets the log level to `debug` while the `level`
is actually an argument of the constructor. It's then wrong, and the
constructor must be renamed.
2025-07-14 10:34:17 +02:00
Ivan Enderlin fa77852001 feat(common): TracingTimer uses the Debug impl of Duration.
This changes the `TracingTimer` message to use the `Debug` impl of
`Duration` instead of displaying it as milliseconds. It can help spotting
seconds without counting all the digits.
2025-07-14 10:34:17 +02:00
Ivan Enderlin 7b73311de5 feat(sqlite): Add logs around read and write. 2025-07-14 10:34:17 +02:00
Ivan Enderlin f03934bc4f feat(sqlite): SqliteEventCacheStore has 1 write connection.
Until now, `SqliteEventCacheStore` manages a pool of connections. A
connection is fetched from this pool and operations are executed on it,
regardless whether these are read operations or write operations.

We are seeing more and more _database is busy_ errors. We believe this
is because too many write operations are executed concurrently.

The solution to solve this is to use multiple connections for read
operations, and a single connection for write operations. That way,
concurrent writings are no longer a thing, and we hope it will reduce
the number of _database is busy_ errors to zero. That's our guess.

This patch does that. When the pool of connections is created, a
connection is elected as the `write_connection`. To get a connection for
read operations, one has to use the new `SqliteEventCacheStore::read`
method (it replaces the `acquire` method). To get a connection for
write operations, one has to use the new `SQliteEventCacheStore::write`
method. It returns a `OwnedMutexGuard` from an async `Mutex`. All
callers that want to do write operations on this store have to wait
their turn, this `Mutex` is fair, and the first to wait on the lock is
the first that will take the lock (FIFO). It guarantees the execution
ordering the code expects.

The rest of the patch updates all spots where `acquire` was used and
replaces them by `read()` or `write()`. A particular care was made to
see if other places are using `SqliteEventCacheStore::pool` directly. No
place remains except in `read()` and `write()`.
2025-07-14 10:34:17 +02:00
Ivan Enderlin 014ee98fb7 feat(sqlite): SqliteStoreConfig::pool_size sets a minimum to 2.
This patch updates `SqliteStoreConfig::pool_size` to be at least 2. We
need 2 connections: one for write operations, one for read operations.
This behaviour is coming in the next patches.
2025-07-14 10:34:17 +02:00
Ivan Enderlin fbcf9fce7c feat(sdk): Add more logs in EventCache.
This patch adds more logs inside `EventCache` around the
`multiple_room_updates_lock` and around the `listen_task`, just to be
sure if everything is listened and works as expected.
2025-07-14 09:24:38 +02:00
Damir Jelić 6de403276a feat(base): Remember the inviter if we accept an invite 2025-07-12 10:57:48 +02:00
Richard van der Hoff 6209bc942c indexeddb: Remove incorrect line from changelog 2025-07-11 16:35:50 +02:00
Doug edd371b570 ffi: Refactor ClientBuilder::build_with_qr_code into Client::login_with_qr_code
The FFI's API now matches the SDK and allows for checks to be made on the Client before logging in.
2025-07-11 15:56:46 +02:00
dragonfly1033 817f32e15b test(sdk): added configurable login response builders for mock login endpoint. 2025-07-11 14:14:24 +02:00
dragonfly1033 30eb12ed2d test(sdk): change test_login_username_refresh_token to use MatrixMockServer 2025-07-11 14:14:24 +02:00
Damir Jelić 900697bc3b chore: Add a missing changelog entry for PR #5250 2025-07-10 17:23:21 +02:00
Damir Jelić 18b169ca7e chore: Release matrix-sdk version 0.13.0 2025-07-10 15:15:04 +02:00
Damir Jelić b9ce4059fb refactor(sqlite): Move the transaction in find_event_relations into a function 2025-07-10 14:23:10 +02:00
Damir Jelić e8dcb5d250 chore: Add a changelog entry for the fix for GHSA-275g-g844-73jh
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-07-10 13:55:02 +02:00
Damir Jelić d0c01006e4 fix(sqlite): Fix a SQL injection issue in the find_event_relations function
The SQLite implementation for the
EventCache::find_event_with_relations() the relation type list isn't
inserted using SQL placeholders.

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This requires a fix for the dev profile, that consists in being able to
set the iOS deployment target, and set it to a high value in CI
settings. Production builds *don't* have to set it, though.
2025-07-02 14:20:53 +02:00
Benjamin Bouvier be3af5e0d4 refactor(event cache): push down absence of gap handling in back-paginations
This will be useful to do for threaded paginations too.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 38bbdf0547 doc(event cache): tweak outdated comment 2025-07-02 12:43:42 +02:00
Benjamin Bouvier b188a157af refactor(event cache): inline most uses of EventLinkedChunk::push_events
The only remaining uses outside the `EventLinkedChunk` are in tests of
deduplication, so keep the method as a test-only method for now.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 9ccbac0c0e refactor(event cache): inline and remove from public API remove_empty_chunk_at 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 137fc9cfbb refactor(event cache): remove EventLinkedChunk::push_gap from the public API
it's only used internally (and in tests), and it's not doing much, so we
can inline it as call sites instead.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 40a4c9a7e1 refactor(event cache): move pushing the live events into its own method in EventLinkedChunk 2025-07-02 12:43:42 +02:00
Benjamin Bouvier ad358955fd refactor(linked chunk): simplify remove_item_at 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 0ee89c86ab refactor(event cache): remove methods that are plain private wrappers
These two methods could be only made private now, since they're only
used in `finish_back_pagination` (and in tests). But they only call
inside other methods of the underlying linked chunk without any extra
value, so they can be inlined instead. This reduces the public API and
removes tests for other APIs that were tested some place else.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 216f0df945 refactor(linked chunk): invert the position of insert_items_at parameters
Same logic as `replace_gap_at`.
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 129e9e173e refactor(event cache): move finishing a network paginations in a EventLinkedChunk method 2025-07-02 12:43:42 +02:00
Benjamin Bouvier f7df0ebf97 refactor(event cache): change order of parameters in replace_gap
"Replace *this* gap with *these* events" read more natural to me than
"replace with *these* events *this* gap".
2025-07-02 12:43:42 +02:00
Benjamin Bouvier 2f78701374 refactor(event cache): move order of parameters in filter_duplicate_events 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 872bded711 refactor(event cache): rename room_event names to linked_chunk or room_linked_chunk where it makes sense 2025-07-02 12:43:42 +02:00
Benjamin Bouvier 5196298e5f refactor(event cache): rename RoomEvents to EventLinkedChunk 2025-07-02 12:43:42 +02:00
Johannes Marbach 308526a6bc feat(ffi): expose timestamp and identifier on EmbeddedEventDetails
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-02 12:38:53 +02:00
Johannes Marbach 12f94a3fd2 feat(ui): expose timestamp and identifier on EmbeddedEvent
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-07-02 12:38:53 +02:00
509 changed files with 36789 additions and 10527 deletions
+2 -12
View File
@@ -17,6 +17,7 @@ version = 2
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"CDLA-Permissive-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
@@ -28,15 +29,6 @@ allow = [
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
{ allow = ["CDDL-1.0"], crate = "inferno" },
{ allow = ["LicenseRef-ring"], crate = "ring" },
]
[[licenses.clarify]]
name = "ring"
expression = "LicenseRef-ring"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[bans]
@@ -51,9 +43,7 @@ unknown-git = "deny"
allow-git = [
# A patch override for the bindings fixing a bug for Android before upstream
# releases a new version.
"https://github.com/element-hq/tracing.git",
# Same as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
"https://github.com/tokio-rs/tracing.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
+26 -40
View File
@@ -1,54 +1,40 @@
name: Benchmarks
on:
push:
branches:
- "main"
pull_request:
workflow_dispatch:
jobs:
benchmarks:
name: Run Benchmarks
runs-on: ubuntu-latest
environment: matrix-rust-bot
if: github.event_name == 'push'
strategy:
matrix:
benchmark:
- crypto_bench
- event_cache
- linked_chunk
- store_bench
- timeline
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-06-27
components: rustfmt
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Run Benchmarks
run: cargo bench | tee benchmark-output.txt
- name: Build the benchmark target(s)
run: cargo codspeed build -p benchmarks ${{ matrix.benchmark }} --features codspeed
- name: Check benchmark result for PR
if: github.event_name == 'pull_request'
uses: benchmark-action/github-action-benchmark@v1
with:
name: Rust Benchmark
tool: 'cargo'
output-file-path: benchmark-output.txt
auto-push: false
# comment to alert the user this has gone bad
github-token: ${{ secrets.MRB_ACCESS_TOKEN }}
alert-threshold: '120%'
comment-on-alert: true
fail-threshold: '150%'
fail-on-alert: true
- name: Store benchmark result
if: github.event_name != 'pull_request'
uses: benchmark-action/github-action-benchmark@v1
with:
name: Rust Benchmark
tool: 'cargo'
output-file-path: benchmark-output.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
# Show alert with commit comment on detecting possible performance regression
alert-threshold: '150%'
comment-on-alert: true
fail-on-alert: true
alert-comment-cc-users: '@gnunicornBen,@jplatte,@poljar'
- name: Run the benchmarks
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
+7 -7
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -69,17 +69,17 @@ jobs:
steps:
- name: Checkout Rust SDK
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
ref: main
- name: Use JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
@@ -136,7 +136,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
@@ -175,7 +175,7 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev --ios-deployment-target=18.0
complement-crypto:
name: "Run Complement Crypto tests"
@@ -191,7 +191,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
+23 -15
View File
@@ -34,14 +34,16 @@ jobs:
- no-sqlite
- no-encryption-and-sqlite
- sqlite-cryptostore
- experimental-encrypted-state-events
- rustls-tls
- markdown
- socks
- sso-login
- search
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -83,7 +85,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -114,7 +116,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install libsqlite
run: |
@@ -165,7 +167,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -237,7 +239,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -249,7 +251,7 @@ jobs:
uses: qmaru/wasm-pack-action@v0.5.1
if: '!matrix.check_only'
with:
version: v0.10.3
version: v0.13.1
- name: Load cache
uses: Swatinem/rust-cache@v2
@@ -287,10 +289,10 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.34.0
uses: crate-ci/typos@v1.35.7
lint:
name: Lint
@@ -299,7 +301,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -309,7 +311,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-06-27
toolchain: nightly-2025-08-08
components: clippy, rustfmt
- name: Load cache
@@ -333,8 +335,7 @@ jobs:
target/debug/xtask ci clippy
integration-tests:
name: Integration test
name: 'Integration test (features: ${{ matrix.feature }})'
runs-on: ubuntu-latest
# run several docker containers with the same networking stack so the hostname 'synapse'
@@ -350,9 +351,16 @@ jobs:
ports:
- 8008:8008
strategy:
fail-fast: true
matrix:
feature:
- "default"
- "experimental-encrypted-state-events"
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install libsqlite
run: |
@@ -376,7 +384,7 @@ jobs:
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
run: |
cargo nextest run -p matrix-sdk-integration-testing
cargo nextest run -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
compile-bench:
name: 🚄 Compile benchmarks
@@ -384,7 +392,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
+60 -2
View File
@@ -42,10 +42,62 @@ jobs:
# This CI workflow can run into space issue, so we're cleaning up some
# space here.
- name: Create some more space
run: rm -rf /opt/hostedtoolcache
run: |
echo "Disk space before cleanup"
df -h
cd /opt
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
rm -rf /opt/hostedtoolcache
# Get rid of binaries and libs we're not interested in.
sudo rm -rf \
/usr/local/julia* \
/usr/local/aws*
sudo rm -rf \
/usr/local/bin/minikube \
/usr/local/bin/node \
/usr/local/bin/stack \
/usr/local/bin/bicep \
/usr/local/bin/pulumi* \
/usr/local/bin/helm \
/usr/local/bin/azcopy \
/usr/local/bin/packer \
/usr/local/bin/cmake-gui \
/usr/local/bin/cpack
sudo rm -rf \
/usr/local/share/powershell \
/usr/local/share/chromium
sudo rm -rf /usr/local/lib/android
echo "::group::/usr/local/bin/*"
du -hsc /usr/local/bin/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/share/*"
du -hsc /usr/local/share/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/*"
du -hsc /usr/local/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/lib/*"
du -hsc /usr/local/lib/* | sort -h
echo "::endgroup::"
echo "::group::/opt/*"
du -hsc /opt/* | sort -h
echo "::endgroup::"
echo "Disk space after cleanup"
df -h
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -53,6 +105,8 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -81,6 +135,10 @@ jobs:
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Check total disk space before running
run: |
df -h
- name: Create the coverage report
run: |
target/debug/xtask ci coverage -o codecov
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: EmbarkStudios/cargo-deny-action@v2
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Machete
uses: bnjbvr/cargo-machete@v0.8.0
uses: bnjbvr/cargo-machete@v0.9.1
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -31,7 +31,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-06-27
toolchain: nightly-2025-08-08
- name: Install Node.js
uses: actions/setup-node@v4
@@ -52,7 +52,7 @@ jobs:
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: './target/doc/'
+1 -1
View File
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+1 -1
View File
@@ -11,6 +11,6 @@ jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Calculate cache key
id: cachekey
+1
View File
@@ -4,6 +4,7 @@ master.zip
emsdk-*
.idea/
.env
.envrc
.build
.swiftpm
/Package.swift
Generated
+1263 -658
View File
File diff suppressed because it is too large Load Diff
+64 -49
View File
@@ -16,23 +16,28 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.85"
rust-version = "1.88"
[workspace.dependencies]
anyhow = "1.0.95"
anyhow = "1.0.99"
aquamarine = "0.6.0"
as_variant = "1.3.0"
assert-json-diff = "2.0.2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.2"
async-compat = "0.2.4"
async-compat = "0.2.5"
async-rx = "0.1.3"
# Bumping this to 0.3.6 produces a test failure because the semantic between the
# versions changed subtly.
async-stream = "0.3.5"
async-trait = "0.1.85"
async-trait = "0.1.89"
base64 = "0.22.1"
bitflags = "2.8.0"
bitflags = "2.9.3"
byteorder = "1.5.0"
chrono = "0.4.39"
cfg-if = "1.0.3"
clap = "4.5.46"
chrono = "0.4.41"
dirs = "6.0.0"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.7.0", features = ["tracing"] }
eyeball-im-util = "0.9.0"
@@ -44,79 +49,84 @@ gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.2.0"
http = "1.3.1"
imbl = "5.0.0"
indexmap = "2.7.1"
insta = { version = "1.42.1", features = ["json", "redactions"] }
indexmap = "2.11.0"
insta = { version = "1.43.1", features = ["json", "redactions"] }
itertools = "0.14.0"
js-sys = "0.3.69"
js-sys = "0.3.77"
mime = "0.3.17"
once_cell = "1.20.2"
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
once_cell = "1.21.3"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
rand = "0.8.5"
reqwest = { version = "0.12.12", default-features = false }
reqwest = { version = "0.12.23", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { git = "https://github.com/ruma/ruma", rev = "d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a", features = [
ruma = { version = "0.13.0", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"compat-encrypted-stickers",
"compat-lax-room-create-deser",
"compat-lax-room-topic-deser",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4143",
"unstable-msc4171",
"unstable-msc4222",
"unstable-msc4278",
"unstable-msc4286",
"unstable-msc4306",
"unstable-msc4308"
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "d1d53e2b7aaf9190f11a5465b9edf6a19fc5b59a" }
sentry = "0.36.0"
sentry-tracing = "0.36.0"
serde = { version = "1.0.217", features = ["rc"] }
sentry = { version = "0.42.0", default-features = false }
sentry-tracing = "0.42.0"
serde = { version = "1.0.219", features = ["rc"] }
serde_html_form = "0.2.7"
serde_json = "1.0.138"
sha2 = "0.10.8"
similar-asserts = "1.6.1"
serde_json = "1.0.143"
sha2 = "0.10.9"
similar-asserts = "1.7.0"
stream_assert = "0.1.1"
tempfile = "3.16.0"
thiserror = "2.0.11"
tokio = { version = "1.43.1", default-features = false, features = ["sync"] }
tempfile = "3.21.0"
thiserror = "2.0.16"
tokio = { version = "1.47.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
tracing-subscriber = "0.3.18"
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
tracing-appender = "0.2.3"
tracing-core = "0.1.34"
tracing-subscriber = "0.3.20"
unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.4"
uuid = "1.12.1"
url = "2.5.7"
uuid = "1.18.0"
vergen-gitcl = "1.0.8"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.69"
wiremock = "0.6.2"
wiremock = "0.6.5"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.12.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.12.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.12.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.12.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.14.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.12.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.12.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.12.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.12.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.12.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.12.0", default-features = false }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.14.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.0" }
[workspace.lints.rust]
rust_2018_idioms = "warn"
@@ -138,13 +148,15 @@ cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
manual_let_else = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
redundant_clone = "warn"
str_to_string = "warn"
todo = "warn"
unnecessary_semicolon = "warn"
unused_async = "warn"
redundant_clone = "warn"
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
@@ -179,12 +191,15 @@ lto = false
# Get symbol names for profiling purposes.
debug = true
[profile.bench]
inherits = "release"
lto = false
[patch.crates-io]
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
tracing-appender = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
+48 -27
View File
@@ -1,38 +1,55 @@
<h1 align="center">Matrix Rust SDK</h1>
<div align="center">
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
<br/><br/>
<img src="contrib/logo.svg">
<br>
<hr>
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
<a href="https://crates.io/crates/matrix-sdk/">
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
<br>
<a href="https://docs.rs/matrix-sdk/">
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
<br>
<br>
<em>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</em>
<br />
<img src="contrib/logo.svg">
<hr />
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
<a href="https://crates.io/crates/matrix-sdk/">
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
<br />
<a href="https://docs.rs/matrix-sdk/">
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
</div>
<div align="center">
The Matrix Rust SDK is a collection of libraries that make it easier to build
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
The Matrix Rust SDK is a collection of libraries that make it easier to build [Matrix] clients in [Rust].
<br />
<br />
<picture>
<source srcset="contrib/element-logo-light.png" media="(prefers-color-scheme: dark)">
<source srcset="contrib/element-logo-dark.png" media="(prefers-color-scheme: light)">
<img src="contrib/element-logo-fallback.png" alt="Element logo">
</picture>
<br />
<br />
Development of the SDK is proudly sponsored and maintained by [Element](https://element.io). Element uses the SDK in their next-generation mobile apps Element X on [iOS](https://github.com/element-hq/element-x-ios) and [Android](https://github.com/element-hq/element-x-android) and has plans to introduce it to the web and desktop clients as well.
The SDK is also the basis for multiple Matrix projects and we welcome contributions from all.
</div>
## Purpose
The SDK takes care of the low-level details like encryption,
syncing, and room state, so you can focus on your app's logic and UI. Whether
you're writing a small bot, a desktop client, or something in between, the SDK
is designed to be flexible, async-friendly, and ready to use out of the box.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
## Project structure
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
The Matrix Rust SDK is made up of several crates that build on top of each
other. The following crates are expected to be usable as direct dependencies:
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) A high-level client library that makes it easy to build
full-featured UI clients with minimal setup. Check out our reference client,
@@ -45,6 +62,9 @@ The Matrix Rust SDK is made up of several crates that build on top of each other
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
for a step-by-step introduction.
All other crates are effectively internal-only and only structured as crates
for organizational purposes and to improve compilation times. Direct usage of them is discouraged.
## Status
The library is considered production ready and backs multiple client
@@ -54,9 +74,6 @@ implementations such as Element X
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
confident to build upon it.
Development of the SDK has been primarily sponsored by Element though accepts
contributions from all.
## Bindings
The higher-level crates of the Matrix Rust SDK can be embedded in other
@@ -67,3 +84,7 @@ into your language of choice.
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
+10 -6
View File
@@ -1,7 +1,7 @@
[package]
name = "benchmarks"
description = "Matrix SDK benchmarks"
edition = "2021"
edition = "2024"
license = "Apache-2.0"
rust-version.workspace = true
version = "1.0.0"
@@ -10,8 +10,11 @@ publish = false
[package.metadata.release]
release = false
[features]
codspeed = []
[dependencies]
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
@@ -21,13 +24,10 @@ matrix-sdk-ui.workspace = true
ruma.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile = "3.3.0"
tempfile.workspace = true
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
wiremock.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
harness = false
@@ -47,3 +47,7 @@ harness = false
[[bench]]
name = "timeline"
harness = false
[[bench]]
name = "event_cache"
harness = false
+93 -69
View File
@@ -1,15 +1,16 @@
use std::{ops::Deref, sync::Arc};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
use matrix_sdk_sqlite::SqliteCryptoStore;
use matrix_sdk_test::ruma_response_from_json;
use ruma::{
DeviceId, OwnedUserId, TransactionId, UserId,
api::client::{
keys::{claim_keys, get_keys},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
device_id, room_id, user_id,
};
use serde_json::Value;
use tokio::runtime::Builder;
@@ -58,10 +59,14 @@ pub fn keys_query(c: &mut Criterion) {
// Benchmark memory store.
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
});
group.bench_with_input(
BenchmarkId::new("Device keys query [memory]", &name),
&response,
|b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
},
);
// Benchmark sqlite store.
@@ -71,10 +76,14 @@ pub fn keys_query(c: &mut Criterion) {
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
});
group.bench_with_input(
BenchmarkId::new("Device keys query [SQLite]", &name),
&response,
|b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
},
);
{
let _guard = runtime.enter();
@@ -84,6 +93,8 @@ pub fn keys_query(c: &mut Criterion) {
group.finish()
}
/// This test panics on the CI, not sure why so we're disabling it for now.
#[cfg(not(feature = "codspeed"))]
pub fn keys_claiming(c: &mut Criterion) {
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
@@ -99,49 +110,65 @@ pub fn keys_claiming(c: &mut Criterion) {
let name = format!("{count} one-time keys");
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.iter_batched(
|| {
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
runtime
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
.unwrap();
(machine, &runtime, &txn_id)
},
move |(machine, runtime, txn_id)| {
runtime.block_on(async {
machine.mark_request_as_sent(txn_id, response).await.unwrap();
group.bench_with_input(
BenchmarkId::new("One-time keys claiming [memory]", &name),
&response,
|b, response| {
b.iter_batched(
|| {
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
runtime
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
.unwrap();
(machine, &runtime, &txn_id)
},
move |(machine, runtime, txn_id)| {
runtime.block_on(async {
machine.mark_request_as_sent(txn_id, response).await.unwrap();
drop(machine);
})
},
criterion::BatchSize::SmallInput,
)
},
);
group.bench_with_input(
BenchmarkId::new("One-time keys claiming [SQLite]", &name),
&response,
|b, response| {
b.iter_batched(
|| {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(
runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap(),
);
let machine = runtime
.block_on(OlmMachine::with_store(
alice_id(),
alice_device_id(),
store,
None,
))
.unwrap();
runtime
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
.unwrap();
(machine, &runtime, &txn_id)
},
move |(machine, runtime, txn_id)| {
runtime.block_on(async {
machine.mark_request_as_sent(txn_id, response).await.unwrap();
});
let _ = runtime.enter();
drop(machine);
})
},
BatchSize::SmallInput,
)
});
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
b.iter_batched(
|| {
let dir = tempfile::tempdir().unwrap();
let store =
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
runtime
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
.unwrap();
(machine, &runtime, &txn_id)
},
move |(machine, runtime, txn_id)| {
runtime.block_on(async {
machine.mark_request_as_sent(txn_id, response).await.unwrap();
drop(machine)
})
},
BatchSize::SmallInput,
)
});
},
criterion::BatchSize::SmallInput,
)
},
);
group.finish()
}
@@ -169,7 +196,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
// Benchmark memory store.
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
group.bench_function(BenchmarkId::new("Room key sharing [memory]", &name), |b| {
b.to_async(&runtime).iter(|| async {
let requests = machine
.share_room_key(
@@ -201,7 +228,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
group.bench_function(BenchmarkId::new("Room key sharing [SQLite]", &name), |b| {
b.to_async(&runtime).iter(|| async {
let requests = machine
.share_room_key(
@@ -249,7 +276,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
// Benchmark memory store.
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
group.bench_function(BenchmarkId::new("Devices collecting [memory]", &name), |b| {
b.to_async(&runtime).iter_with_large_drop(|| async {
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
})
@@ -266,7 +293,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
group.bench_function(BenchmarkId::new("Devices collecting [SQLite]", &name), |b| {
b.to_async(&runtime).iter(|| async {
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
})
@@ -280,21 +307,18 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
group.finish()
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
criterion
}
#[cfg(not(feature = "codspeed"))]
criterion_group! {
name = benches;
config = criterion();
config = Criterion::default();
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
}
#[cfg(feature = "codspeed")]
criterion_group! {
name = benches;
config = Criterion::default();
targets = keys_query, room_key_sharing, devices_missing_sessions_collecting,
}
criterion_main!(benches);
+353
View File
@@ -0,0 +1,353 @@
use std::{pin::Pin, sync::Arc};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
store::StoreConfig,
sync::{JoinedRoomUpdate, RoomUpdates},
test_utils::client::MockClientBuilder,
};
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{
EventId, RoomId, event_id,
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
room_id,
};
use tempfile::tempdir;
use tokio::runtime::Builder;
type StoreBuilder = Box<dyn Fn() -> Pin<Box<dyn Future<Output = Arc<DynEventCacheStore>>>>>;
fn handle_room_updates(c: &mut Criterion) {
// Create a new asynchronous runtime.
let runtime = Builder::new_multi_thread()
.enable_time()
.enable_io()
.build()
.expect("Failed to create an asynchronous runtime");
let mut group = c.benchmark_group("Event cache room updates");
group.sample_size(10);
const NUM_EVENTS: usize = 1000;
for num_rooms in [1, 10, 100] {
// Add some joined rooms, each with NUM_EVENTS in it, to the sync response.
let mut room_updates = RoomUpdates::default();
let mut changes = matrix_sdk::StateChanges::default();
for i in 0..num_rooms {
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap();
let event_factory = EventFactory::new().room(&room_id).sender(&ALICE);
let mut joined_room_update = JoinedRoomUpdate::default();
for j in 0..NUM_EVENTS {
let event_id = EventId::parse(format!("$ev{i}_{j}")).unwrap();
let event =
event_factory.text_msg(format!("Message {j}")).event_id(&event_id).into();
joined_room_update.timeline.events.push(event);
}
room_updates.joined.insert(room_id.clone(), joined_room_update);
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
}
// Declare new stores for this set of events.
let temp_dir = Arc::new(tempdir().unwrap());
let store_builders: Vec<(_, StoreBuilder)> = vec![
(
"memory",
Box::new(|| Box::pin(async { MemoryStore::default().into_event_cache_store() })),
),
(
"SQLite",
Box::new(move || {
let temp_dir = temp_dir.clone();
Box::pin(async move {
// Remove all the files in the temp_dir, to reset the event cache state.
for entry in temp_dir.path().read_dir().unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
// If it's a directory, remove it recursively.
std::fs::remove_dir_all(path).unwrap();
} else {
std::fs::remove_file(path).unwrap();
}
}
// Recreate a new store.
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
.await
.unwrap()
.into_event_cache_store()
})
}),
),
];
let state_store = runtime.block_on(async {
let state_store = matrix_sdk::MemoryStore::new();
state_store.save_changes(&changes).await.unwrap();
Arc::new(state_store)
});
for (store_name, store_builder) in &store_builders {
let client = runtime.block_on(async {
let event_cache_store = store_builder().await;
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(state_store.clone())
.event_cache_store(event_cache_store.clone()),
)
})
.build()
.await;
client.event_cache().subscribe().unwrap();
client
});
// Define a state store with all rooms known in it.
// Define the throughput.
group.throughput(Throughput::Elements(num_rooms));
// Bench the handling of room updates.
group.bench_function(
BenchmarkId::new(
format!("Event cache room updates[{store_name}]"),
format!("room count: {num_rooms}"),
),
|bencher| {
bencher.to_async(&runtime).iter(
// The routine itself.
|| {
let room_updates = room_updates.clone();
let client = client.clone();
async move {
client.event_cache().clear_all_rooms().await.unwrap();
client
.event_cache()
.handle_room_updates(room_updates.clone())
.await
.unwrap();
}
},
)
},
);
}
}
group.finish()
}
fn find_event_relations(c: &mut Criterion) {
// Number of other events to saturate the DB, but that will not be affected by
// the benchmark. A small multiple of this number will be added.
// When running locally, run with more events than in Codespeed CI.
#[cfg(feature = "codspeed")]
const NUM_OTHER_EVENTS: usize = 100;
#[cfg(not(feature = "codspeed"))]
const NUM_OTHER_EVENTS: usize = 1000;
// Create a new asynchronous runtime.
let runtime = Builder::new_multi_thread()
.enable_time()
.enable_io()
.build()
.expect("Failed to create an asynchronous runtime");
let mut group = c.benchmark_group("Event cache room updates");
group.sample_size(10);
let room_id = room_id!("!room:ben.ch");
let other_room_id = room_id!("!other-room:ben.ch");
// Make the state store aware of the room, so that `client.get_room()` works
// with it.
let mut changes = matrix_sdk::StateChanges::default();
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
changes.add_room(RoomInfo::new(other_room_id, RoomState::Joined));
let state_store = runtime.block_on(async {
let state_store = matrix_sdk::MemoryStore::new();
state_store.save_changes(&changes).await.unwrap();
Arc::new(state_store)
});
for num_related_events in [10, 100, 1000] {
// Prefill the event cache store with one event and N related events.
let mut room_updates = RoomUpdates::default();
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
let mut joined_room_update = JoinedRoomUpdate::default();
// Add the target event.
let target_event_id = event_id!("$target");
let target_event =
event_factory.text_msg("hello world").event_id(target_event_id).into_event();
joined_room_update.timeline.events.push(target_event);
// Add the numerous edits.
for i in 0..num_related_events {
let event_id = EventId::parse(format!("$edit{i}")).unwrap();
let event = event_factory
.text_msg(format!("* edit {i}"))
.edit(
target_event_id,
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
)
.event_id(&event_id)
.into();
joined_room_update.timeline.events.push(event);
}
// Add other events, in the same room, without a relation.
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$msg{i}")).unwrap();
let event =
event_factory.text_msg(format!("unrelated message {i}")).event_id(&event_id).into();
joined_room_update.timeline.events.push(event);
}
// Add other events, in the same room, related to other events.
let other_target_event_id = event_id!("$other_target");
let other_target_event =
event_factory.text_msg("hello world").event_id(other_target_event_id).into_event();
joined_room_update.timeline.events.push(other_target_event);
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$unrelated{i}")).unwrap();
let event =
event_factory.reaction(other_target_event_id, "👍").event_id(&event_id).into();
joined_room_update.timeline.events.push(event);
}
room_updates.joined.insert(room_id.to_owned(), joined_room_update);
// Add other events, in another room.
let mut other_joined_room_update = JoinedRoomUpdate::default();
let event_factory = event_factory.room(other_room_id);
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$other_room{i}")).unwrap();
let event = event_factory.text_msg(format!("hi {i}")).event_id(&event_id).into();
other_joined_room_update.timeline.events.push(event);
}
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
// Declare new stores for this set of events.
let temp_dir = Arc::new(tempdir().unwrap());
let stores = vec![
("memory", MemoryStore::default().into_event_cache_store()),
(
"SQLite",
runtime.block_on(async {
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
.await
.unwrap()
.into_event_cache_store()
}),
),
];
for (store_name, event_cache_store) in stores {
let (client, room_event_cache, _drop_handles) = runtime.block_on(async {
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(state_store.clone())
.event_cache_store(event_cache_store),
)
})
.build()
.await;
client.event_cache().subscribe().unwrap();
// Sync the updates before starting the benchmark.
let mut update_recv = client.event_cache().subscribe_to_room_generic_updates();
client.event_cache().handle_room_updates(room_updates.clone()).await.unwrap();
// Wait for the event cache to notify us of the room updates.
let update = update_recv.recv().await.unwrap();
assert!(update.room_id == room_id || update.room_id == other_room_id);
let update = update_recv.recv().await.unwrap();
assert!(update.room_id == room_id || update.room_id == other_room_id);
let room = client.get_room(room_id).unwrap();
let room_event_cache = room.event_cache().await.unwrap();
(client, room_event_cache.0, room_event_cache.1)
});
// Define the throughput.
group.throughput(Throughput::Elements(num_related_events));
for filter in [None, Some(vec![RelationType::Replacement])] {
group.bench_function(
BenchmarkId::new(
format!("Event cache find_event_relations[{store_name}]"),
format!(
"{num_related_events} events, {} filter",
if filter.is_some() { "edits" } else { "#no" },
),
),
|bencher| {
bencher.to_async(&runtime).iter_batched(
// The setup.
|| (room_event_cache.clone(), filter.clone()),
// The routine itself.
|(room_event_cache, filter)| async move {
let (target, relations) = room_event_cache
.find_event_with_relations(target_event_id, filter)
.await
.unwrap();
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
assert_eq!(relations.len(), num_related_events as usize);
},
criterion::BatchSize::PerIteration,
)
},
);
}
{
let _guard = runtime.enter();
drop(room_event_cache);
drop(client);
drop(_drop_handles);
}
}
}
{
let _guard = runtime.enter();
drop(state_store);
}
group.finish()
}
criterion_group! {
name = event_cache;
config = Criterion::default();
targets = handle_room_updates, find_event_relations,
}
criterion_main!(event_cache);
+58 -45
View File
@@ -1,16 +1,16 @@
use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
SqliteEventCacheStore,
linked_chunk::{LinkedChunk, LinkedChunkId, Update, lazy_loader},
};
use matrix_sdk_base::event_cache::{
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
Event, Gap,
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
use ruma::{room_id, EventId};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{EventId, room_id};
use tempfile::tempdir;
use tokio::runtime::Builder;
@@ -20,6 +20,11 @@ enum Operation {
PushGapBack(Gap),
}
#[cfg(not(feature = "codspeed"))]
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000, 10_000, 100_000];
#[cfg(feature = "codspeed")]
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000];
fn writing(c: &mut Criterion) {
// Create a new asynchronous runtime.
let runtime = Builder::new_multi_thread()
@@ -32,10 +37,10 @@ fn writing(c: &mut Criterion) {
let linked_chunk_id = LinkedChunkId::Room(room_id);
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
let mut group = c.benchmark_group("writing");
let mut group = c.benchmark_group("Linked chunk writing");
group.sample_size(10).measurement_time(Duration::from_secs(30));
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
for &number_of_events in NUMBER_OF_EVENTS {
let sqlite_temp_dir = tempdir().unwrap();
// Declare new stores for this set of events.
@@ -96,7 +101,7 @@ fn writing(c: &mut Criterion) {
// Get a bencher.
group.bench_with_input(
BenchmarkId::new(store_name, number_of_events),
BenchmarkId::new(format!("Linked chunk writing [{store_name}]"), number_of_events),
&operations,
|bencher, operations| {
// Bench the routine.
@@ -149,10 +154,10 @@ fn reading(c: &mut Criterion) {
let linked_chunk_id = LinkedChunkId::Room(room_id);
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
let mut group = c.benchmark_group("reading");
let mut group = c.benchmark_group("Linked chunk reading");
group.sample_size(10);
for num_events in [10, 100, 1000, 10_000, 100_000] {
for &num_events in NUMBER_OF_EVENTS {
let sqlite_temp_dir = tempdir().unwrap();
// Declare new stores for this set of events.
@@ -187,11 +192,14 @@ fn reading(c: &mut Criterion) {
while events.peek().is_some() {
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
if events_chunk.is_empty() {
break;
}
lc.push_items_back(events_chunk);
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
num_gaps += 1;
}
@@ -205,30 +213,47 @@ fn reading(c: &mut Criterion) {
// Define the throughput.
group.throughput(Throughput::Elements(num_events));
// Get a bencher.
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
// Bench the routine.
bencher.to_async(&runtime).iter(|| async {
// Load the last chunk first,
let (last_chunk, chunk_id_gen) =
store.load_last_chunk(linked_chunk_id).await.unwrap();
// Bench the lazy loader.
group.bench_function(
BenchmarkId::new(format!("Linked chunk lazy loader[{store_name}]"), num_events),
|bencher| {
// Bench the routine.
bencher.to_async(&runtime).iter(|| async {
// Load the last chunk first,
let (last_chunk, chunk_id_gen) =
store.load_last_chunk(linked_chunk_id).await.unwrap();
let mut lc =
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
.expect("no error when reconstructing the linked chunk")
.expect("there is a linked chunk in the store");
let mut lc =
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
.expect("no error when reconstructing the linked chunk")
.expect("there is a linked chunk in the store");
// Then load until the start of the linked chunk.
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
while let Some(prev) =
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
{
cur_chunk_id = prev.identifier;
lazy_loader::insert_new_first_chunk(&mut lc, prev)
.expect("no error when linking the previous lazy-loaded chunk");
}
})
});
// Then load until the start of the linked chunk.
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
while let Some(prev) =
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
{
cur_chunk_id = prev.identifier;
lazy_loader::insert_new_first_chunk(&mut lc, prev)
.expect("no error when linking the previous lazy-loaded chunk");
}
})
},
);
// Bench the metadata loader.
group.bench_function(
BenchmarkId::new(format!("Linked chunk metadata loader[{store_name}]"), num_events),
|bencher| {
// Bench the routine.
bencher.to_async(&runtime).iter(|| async {
let _metadata = store
.load_all_chunks_metadata(linked_chunk_id)
.await
.expect("metadata must load");
})
},
);
{
let _guard = runtime.enter();
@@ -240,21 +265,9 @@ fn reading(c: &mut Criterion) {
group.finish()
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
criterion
}
criterion_group! {
name = event_cache;
config = criterion();
config = Criterion::default();
targets = writing, reading,
}
+11 -23
View File
@@ -1,20 +1,22 @@
use std::time::Duration;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
store::StoreConfig,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
api::client::membership::get_member_events,
device_id,
events::room::member::{MembershipState, RoomMemberEvent},
mxc_uri, owned_room_id, owned_user_id,
serde::Raw,
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
user_id,
};
use serde_json::json;
use tokio::runtime::Builder;
@@ -58,6 +60,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
let base_client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
ThreadingSupport::Disabled,
);
runtime
@@ -82,7 +85,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
group.throughput(Throughput::Elements(count as u64));
group.sample_size(50);
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
group.bench_function(BenchmarkId::new("Handle /members request [SQLite]", name), |b| {
b.to_async(&runtime).iter(|| async {
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
});
@@ -162,11 +165,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
let count = PINNED_EVENTS_COUNT;
let name = format!("{count} pinned events");
let mut group = c.benchmark_group("Test");
let mut group = c.benchmark_group("Load pinned events");
group.throughput(Throughput::Elements(count as u64));
group.sample_size(10);
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
group.bench_function(BenchmarkId::new("Load pinned events [memory]", name), |b| {
b.to_async(&runtime).iter(|| async {
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
@@ -205,24 +208,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
group.finish();
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
{
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
))
}
#[cfg(not(target_os = "linux"))]
{
Criterion::default()
}
}
criterion_group! {
name = room;
config = criterion();
config = Criterion::default();
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
}
criterion_main!(room);
+9 -24
View File
@@ -1,28 +1,15 @@
use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
SessionTokens, StateChanges,
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
authentication::matrix::MatrixSession, config::StoreConfig,
};
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
use matrix_sdk_sqlite::SqliteStateStore;
use ruma::{device_id, user_id, RoomId};
use ruma::{RoomId, device_id, user_id};
use tokio::runtime::Builder;
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
criterion
}
/// Number of joined rooms in the benchmark.
const NUM_JOINED_ROOMS: usize = 10000;
@@ -30,7 +17,7 @@ const NUM_JOINED_ROOMS: usize = 10000;
const NUM_STRIPPED_JOINED_ROOMS: usize = 10000;
pub fn restore_session(c: &mut Criterion) {
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let runtime = Builder::new_multi_thread().enable_time().build().expect("Can't create runtime");
// Create a fake list of changes, and a session to recover from.
let mut changes = StateChanges::default();
@@ -58,13 +45,11 @@ pub fn restore_session(c: &mut Criterion) {
let mut group = c.benchmark_group("Client reload");
group.throughput(Throughput::Elements(100));
const NAME: &str = "restore a session";
// Memory
let mem_store = Arc::new(MemoryStore::new());
runtime.block_on(mem_store.save_changes(&changes)).expect("initial filling of mem failed");
group.bench_with_input(BenchmarkId::new("memory store", NAME), &mem_store, |b, store| {
group.bench_with_input("Restore session [memory store]", &mem_store, |b, store| {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
@@ -92,7 +77,7 @@ pub fn restore_session(c: &mut Criterion) {
.expect("initial filling of sqlite failed");
group.bench_with_input(
BenchmarkId::new(format!("sqlite store {encrypted_suffix}"), NAME),
BenchmarkId::new("Restore session [SQLite]", encrypted_suffix),
&sqlite_store,
|b, store| {
b.to_async(&runtime).iter(|| async {
@@ -124,7 +109,7 @@ pub fn restore_session(c: &mut Criterion) {
criterion_group! {
name = benches;
config = criterion();
config = Criterion::default();
targets = restore_session
}
criterion_main!(benches);
+7 -22
View File
@@ -1,10 +1,10 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_ui::timeline::TimelineBuilder;
use ruma::{
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
EventId,
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
owned_user_id,
};
use tokio::runtime::Builder;
@@ -94,12 +94,12 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
room
});
let mut group = c.benchmark_group("Test");
let mut group = c.benchmark_group("Create a timeline");
group.throughput(Throughput::Elements(NUM_EVENTS as _));
group.sample_size(10);
group.bench_function(
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
BenchmarkId::new("Create a timeline with initial events", format!("{NUM_EVENTS} events")),
|b| {
b.to_async(&runtime).iter(|| async {
let timeline = TimelineBuilder::new(&room)
@@ -117,24 +117,9 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
group.finish();
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
{
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
))
}
#[cfg(not(target_os = "linux"))]
{
Criterion::default()
}
}
criterion_group! {
name = room;
config = criterion();
config = Criterion::default();
targets = create_timeline_with_initial_events
}
criterion_main!(room);
+11 -5
View File
@@ -23,14 +23,20 @@ path = "uniffi-bindgen.rs"
default = ["bundled-sqlite"]
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
# Enable experimental support for encrypting state events; see
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
experimental-encrypted-state-events = [
"matrix-sdk-crypto/experimental-encrypted-state-events",
]
[dependencies]
anyhow.workspace = true
futures-util.workspace = true
hmac = "0.12.1"
hmac.workspace = true
http.workspace = true
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
matrix-sdk-ffi-macros.workspace = true
pbkdf2 = "0.12.2"
pbkdf2.workspace = true
rand.workspace = true
ruma.workspace = true
serde.workspace = true
@@ -56,17 +62,17 @@ workspace = true
features = ["crypto-store"]
[dependencies.tokio]
version = "1.43.1"
workspace = true
default-features = false
features = ["rt-multi-thread"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
vergen-gitcl = { workspace = true, features = ["build"] }
[dev-dependencies]
assert_matches2.workspace = true
tempfile = "3.8.0"
tempfile.workspace = true
[lints]
workspace = true
+3 -2
View File
@@ -5,7 +5,7 @@ use std::{
process::Command,
};
use vergen::EmitBuilder;
use vergen_gitcl::{Emitter, GitclBuilder};
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
@@ -59,7 +59,8 @@ fn get_clang_major_version(clang_path: &Path) -> String {
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
let git_config = GitclBuilder::default().sha(true).describe(true, false, None).build()?;
Emitter::default().add_instructions(&git_config)?.emit()?;
Ok(())
}
@@ -7,6 +7,7 @@ use matrix_sdk_crypto::{
RehydratedDevice as InnerRehydratedDevice,
},
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
DecryptionSettings,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
@@ -154,9 +155,13 @@ impl Drop for RehydratedDevice {
#[matrix_sdk_ffi_macros::export]
impl RehydratedDevice {
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
pub fn receive_events(
&self,
events: String,
decryption_settings: &DecryptionSettings,
) -> Result<(), crate::CryptoStoreError> {
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
self.runtime.block_on(self.inner.receive_events(events))?;
self.runtime.block_on(self.inner.receive_events(events, decryption_settings))?;
Ok(())
}
+19 -1
View File
@@ -665,6 +665,9 @@ impl From<HistoryVisibility> for RustHistoryVisibility {
pub struct EncryptionSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// Whether state event encryption is enabled.
#[cfg(feature = "experimental-encrypted-state-events")]
pub encrypt_state_events: bool,
/// How long can the room key be used before it should be rotated. Time in
/// seconds.
pub rotation_period: u64,
@@ -694,6 +697,8 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
RustEncryptionSettings {
algorithm: v.algorithm.into(),
#[cfg(feature = "experimental-encrypted-state-events")]
encrypt_state_events: false,
rotation_period: Duration::from_secs(v.rotation_period),
rotation_period_msgs: v.rotation_period_msgs,
history_visibility: v.history_visibility.into(),
@@ -910,6 +915,10 @@ impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
pub struct RoomSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// Whether state event encryption is enabled.
#[cfg(feature = "experimental-encrypted-state-events")]
#[serde(default)]
pub encrypt_state_events: bool,
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
pub only_allow_trusted_devices: bool,
@@ -920,7 +929,12 @@ impl TryFrom<RustRoomSettings> for RoomSettings {
fn try_from(value: RustRoomSettings) -> Result<Self, Self::Error> {
let algorithm = value.algorithm.try_into()?;
Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices })
Ok(Self {
algorithm,
#[cfg(feature = "experimental-encrypted-state-events")]
encrypt_state_events: value.encrypt_state_events,
only_allow_trusted_devices: value.only_allow_trusted_devices,
})
}
}
@@ -1173,6 +1187,8 @@ mod tests {
assert_eq!(
Some(RoomSettings {
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
#[cfg(feature = "experimental-encrypted-state-events")]
encrypt_state_events: false,
only_allow_trusted_devices: true
}),
settings1
@@ -1182,6 +1198,8 @@ mod tests {
assert_eq!(
Some(RoomSettings {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
#[cfg(feature = "experimental-encrypted-state-events")]
encrypt_state_events: false,
only_allow_trusted_devices: false
}),
settings2
+22 -14
View File
@@ -18,7 +18,8 @@ use matrix_sdk_crypto::{
olm::ExportedRoomKey,
store::types::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
};
use ruma::{
api::{
@@ -38,7 +39,7 @@ use ruma::{
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, MessageLikeEvent,
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
@@ -526,6 +527,7 @@ impl OlmMachine {
key_counts: HashMap<String, i32>,
unused_fallback_keys: Option<Vec<String>>,
next_batch_token: String,
decryption_settings: &DecryptionSettings,
) -> Result<SyncChangesResult, CryptoStoreError> {
let to_device: ToDevice = serde_json::from_str(&events)?;
let device_changes: RumaDeviceLists = device_changes.into();
@@ -544,15 +546,17 @@ impl OlmMachine {
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 {
to_device_events: to_device.events,
changed_devices: &device_changes,
one_time_keys_counts: &key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
}),
)?;
let (to_device_events, room_key_infos) =
self.runtime.block_on(self.inner.receive_sync_changes(
matrix_sdk_crypto::EncryptionSyncChanges {
to_device_events: to_device.events,
changed_devices: &device_changes,
one_time_keys_counts: &key_counts,
unused_fallback_keys: unused_fallback_keys.as_deref(),
next_batch_token: Some(next_batch_token),
},
decryption_settings,
))?;
let to_device_events = to_device_events
.into_iter()
@@ -829,6 +833,7 @@ impl OlmMachine {
device_id: String,
event_type: String,
content: String,
share_strategy: CollectStrategy,
) -> Result<Option<Request>, CryptoStoreError> {
let user_id = parse_user_id(&user_id)?;
let device_id = device_id.as_str().into();
@@ -837,8 +842,11 @@ impl OlmMachine {
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
if let Some(device) = device {
let encrypted_content =
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
let encrypted_content = self.runtime.block_on(device.encrypt_event_raw(
&event_type,
&content,
share_strategy,
))?;
let request = ToDeviceRequest::new(
user_id.as_ref(),
@@ -894,7 +902,7 @@ impl OlmMachine {
))?;
if handle_verification_events {
if let Ok(e) = decrypted.event.deserialize() {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
@@ -27,7 +27,7 @@ use ruma::{
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
assign,
events::EventContent,
events::MessageLikeEventContent,
OwnedTransactionId, UserId,
};
use serde_json::json;
+98 -4
View File
@@ -6,11 +6,101 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.14.0] - 2025-09-04
### Features:
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
rooms based on their low priority status. These filters allow clients to show only low priority rooms
or exclude low priority rooms from the room list.
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
- The [`unstable-hydra`] feature has been enabled, which enables room v12 changes in the SDK.
([#5450](https://github.com/matrix-org/matrix-rust-sdk/pull/5450)).
- Add experimental support for
[MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the
`Room::fetch_thread_subscription()` and `Room::set_thread_subscription()` methods.
([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442))
- [**breaking**] [`GalleryUploadParameters::reply`] and [`UploadParameters::reply`] have been both
replaced with a new optional `in_reply_to` field, that's a string which will be parsed into an
`OwnedEventId` when sending the event. The thread relationship will be automatically filled in,
based on the timeline focus.
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
- [**breaking**] [`Timeline::send_reply()`] now automatically fills in the thread relationship,
based on the timeline focus. As a result, it only takes an `OwnedEventId` parameter, instead of
the `Reply` type. The proper way to start a thread is now thus to create a threaded-focused
timeline, and then use `Timeline::send()`.
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
- Add `HomeserverLoginDetails::supports_sso_login` for legacy SSO support information.
This is primarily for Element X to give a dedicated error message in case
it connects a homeserver with only this method available.
([#5222](https://github.com/matrix-org/matrix-rust-sdk/pull/5222))
### Breaking changes:
- The timeline will now always use the send queue to upload medias, so the
`UploadParameters::use_send_queue` bool has been removed. Make sure to listen to the send queue's
error updates, and to handle send queue restarts.
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
- Support for the legacy media upload progress has been disabled. Media upload progress is
available through the send queue, and can be enabled thanks to
`Client::enable_send_queue_upload_progress()`.
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
- `TimelineDiff` is now exported as a true `uniffi::Enum` instead of the weird `uniffi::Object` hybrid. This matches
both `RoomDirectorySearchEntryUpdate` and `RoomListEntriesUpdate` and can be used in the same way.
([#5474](https://github.com/matrix-org/matrix-rust-sdk/pull/5474))
- The `creator` field of `RoomInfo` has been renamed to `creators` and can now contain a list of
user IDs, to reflect that a room can now have several creators, as introduced in room version 12.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- The `PowerLevel` type was introduced to represent power levels instead of `i64` to differentiate
the infinite power level of creators, as introduced in room version 12. It is used in
`suggested_role_for_power_level`, `suggested_power_level_for_role` and `RoomMember`.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- `Client::get_url` now returns a `Vec<u8>` instead of a `String`. It also throws an error when the
response isn't status code 200 OK, instead of providing the error in the response body.
([#5438](https://github.com/matrix-org/matrix-rust-sdk/pull/5438))
- `RoomPreview::info()` doesn't return a result anymore. All unknown join rules are handled in the
`JoinRule::Custom` variant.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- The `reason` argument of `Room::report_room` is now required, do to a clarification in the spec.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- `PublicRoomJoinRule` has more variants, supporting all the known values from the spec.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- The fields of `MediaPreviewConfig` are both optional, allowing to use the type for room account
data as well as global account data.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- The `event_id` field of `PredecessorRoom` was removed, due to its removal in the Matrix
specification with MSC4291.
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
- `Client::url_for_oidc` now allows requesting additional scopes for the OAuth2 authorization code grant.
([#5395](https://github.com/matrix-org/matrix-rust-sdk/pull/5395))
- `Client::url_for_oidc` now allows passing an optional existing device id from a previous login call.
([#5394](https://github.com/matrix-org/matrix-rust-sdk/pull/5394))
- `ClientBuilder::build_with_qr_code` has been removed. Instead, the Client should be built by passing
`QrCodeData::server_name` to `ClientBuilder::server_name_or_homeserver_url`, after which QR login can be performed by
calling `Client::login_with_qr_code`. ([#5388](https://github.com/matrix-org/matrix-rust-sdk/pull/5388))
- The MSRV has been bumped to Rust 1.88.
([#5431](https://github.com/matrix-org/matrix-rust-sdk/pull/5431))
- `Room::send_call_notification` and `Room::send_call_notification_if_needed` have been removed, since the event type they send is outdated, and `Client` is not actually supposed to be able to join MatrixRTC sessions (yet). In practice, users of these methods probably already rely on another MatrixRTC implementation to participate in sessions, and such an implementation should be capable of sending notifications itself.
- The `GalleryItemInfo` variants now take an `UploadSource` rather than a `String` path to enable uploading
from bytes directly.
([#5529](https://github.com/matrix-org/matrix-rust-sdk/pull/5529))
- Media and gallery uploads now use `UploadSource` to specify the thumbnail.
([#5530](https://github.com/matrix-org/matrix-rust-sdk/pull/5530))
## [0.13.0] - 2025-07-10
### Features
- Add `NotificationRoomInfo::topic` to the `NotificationRoomInfo` struct, which
contains the topic of the room. This is useful for displaying the room topic
in notifications. ([#5300](https://github.com/matrix-org/matrix-rust-sdk/pull/5300))
- Add `EmbeddedEventDetails::timestamp` and `EmbeddedEventDetails::event_or_transaction_id`
which are already available in regular timeline items.
([#5331](https://github.com/matrix-org/matrix-rust-sdk/pull/5331))
- `RoomListService::subscribe_to_rooms` becomes `async` and automatically calls
`matrix_sdk::latest_events::LatestEvents::listen_to_room`
([#5369](https://github.com/matrix-org/matrix-rust-sdk/pull/5369))
### Refactor
@@ -64,7 +154,8 @@ Additions:
we can automatically update the UI.
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
uploads by compressing/transcoding the media.
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history
on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
([#5141](https://github.com/matrix-org/matrix-rust-sdk/pull/5141))
- Support for adding a Sentry layer to the FFI bindings has been added. Only `tracing` statements with
the field `sentry=true` will be forwarded to Sentry, in addition to default Sentry filters.
@@ -157,7 +248,8 @@ Breaking changes:
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
Clients are supposed to re-register with the homeserver for every login.
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an
`Option<RoomMemberWithSenderInfo>`.
Additions:
@@ -172,9 +264,11 @@ Additions:
- Add `Timeline::send_thread_reply` for clients that need to start threads
themselves.
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the
stores configuration, especially their memory consumption
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
has less memory available than the current standard
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member`
event the `RoomMember` is based on.
+10 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-ffi"
version = "0.12.0"
version = "0.14.0"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
@@ -38,7 +38,6 @@ sentry = ["dep:sentry", "dep:sentry-tracing"]
[dependencies]
anyhow.workspace = true
as_variant.workspace = true
extension-trait = "1.0.1"
eyeball-im.workspace = true
futures-util.workspace = true
@@ -58,10 +57,10 @@ matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.16"
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
serde.workspace = true
serde_json.workspace = true
sentry = { version = "0.36.0", optional = true, default-features = false, features = [
sentry = { workspace = true, optional = true, default-features = false, features = [
# Most default features enabled otherwise.
"backtrace",
"contexts",
@@ -69,15 +68,16 @@ sentry = { version = "0.36.0", optional = true, default-features = false, featur
"reqwest",
"sentry-debug-images",
] }
sentry-tracing = { version = "0.36.0", optional = true }
sentry-tracing = { workspace = true, optional = true }
thiserror.workspace = true
tracing.workspace = true
tracing-appender = { version = "0.2.2" }
tracing-appender.workspace = true
tracing-core.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url.workspace = true
uuid = { version = "1.4.1", features = ["v4"] }
zeroize.workspace = true
oauth2.workspace = true
[target.'cfg(target_family = "wasm")'.dependencies]
console_error_panic_hook = "0.1.7"
@@ -92,9 +92,12 @@ uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.1"
[dev-dependencies]
similar-asserts.workspace = true
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
vergen-gitcl = { workspace = true, features = ["build"] }
[lints]
workspace = true
+5 -2
View File
@@ -5,7 +5,7 @@ use std::{
process::Command,
};
use vergen::EmitBuilder;
use vergen_gitcl::{Emitter, GitclBuilder};
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
@@ -59,6 +59,9 @@ fn get_clang_major_version(clang_path: &Path) -> String {
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
EmitBuilder::builder().git_sha(true).emit()?;
let git_config = GitclBuilder::default().sha(true).build()?;
Emitter::default().add_instructions(&git_config)?.emit()?;
Ok(())
}
@@ -23,6 +23,7 @@ pub struct HomeserverLoginDetails {
pub(crate) sliding_sync_version: SlidingSyncVersion,
pub(crate) supports_oidc_login: bool,
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
pub(crate) supports_sso_login: bool,
pub(crate) supports_password_login: bool,
}
@@ -43,6 +44,11 @@ impl HomeserverLoginDetails {
self.supports_oidc_login
}
/// Whether the current homeserver supports login using legacy SSO.
pub fn supports_sso_login(&self) -> bool {
self.supports_sso_login
}
/// The prompts advertised by the authentication issuer for use in the login
/// URL.
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
+154 -45
View File
@@ -20,7 +20,7 @@ use matrix_sdk::{
api::client::{
discovery::{
discover_homeserver::RtcFocusInfo,
get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
},
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
@@ -39,7 +39,7 @@ use matrix_sdk::{
},
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
Account, AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
STATE_STORE_DATABASE_NAME,
};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
@@ -48,11 +48,18 @@ use matrix_sdk_ui::{
NotificationClient as MatrixNotificationClient,
NotificationProcessSetup as MatrixNotificationProcessSetup,
},
spaces::SpaceService as UISpaceService,
unable_to_decrypt_hook::UtdHookManager,
};
use mime::Mime;
use oauth2::Scope;
use ruma::{
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
api::client::{
alias::get_alias,
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
uiaa::UserIdentifier,
},
events::{
direct::DirectEventContent,
fully_read::FullyReadEventContent,
@@ -74,11 +81,11 @@ use ruma::{
},
tag::TagEventContent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
room_version_rules::AuthorizationRules,
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -96,6 +103,7 @@ use crate::{
encryption::Encryption,
notification::NotificationClient,
notification_settings::NotificationSettings,
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
room::{RoomHistoryVisibility, RoomInfoListener},
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
@@ -104,6 +112,7 @@ use crate::{
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
},
runtime::get_runtime_handle,
spaces::SpaceService,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utd::{UnableToDecryptDelegate, UtdHook},
@@ -339,7 +348,24 @@ impl Client {
}
};
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
let login_types = self.inner.matrix_auth().get_login_types().await.ok();
let supports_password_login = login_types
.as_ref()
.map(|login_types| {
login_types.flows.iter().any(|login_type| {
matches!(login_type, get_login_types::v3::LoginType::Password(_))
})
})
.unwrap_or(false);
let supports_sso_login = login_types
.as_ref()
.map(|login_types| {
login_types
.flows
.iter()
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Sso(_)))
})
.unwrap_or(false);
let sliding_sync_version = self.sliding_sync_version();
Arc::new(HomeserverLoginDetails {
@@ -347,6 +373,7 @@ impl Client {
sliding_sync_version,
supports_oidc_login,
supported_oidc_prompts,
supports_sso_login,
supports_password_login,
})
}
@@ -456,16 +483,39 @@ impl Client {
/// However, it should be noted that when providing a user ID as a hint
/// for MAS (with no upstream provider), then the format to use is defined
/// by [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
///
/// * `device_id` - The unique ID that will be associated with the session.
/// If not set, a random one will be generated. It can be an existing
/// device ID from a previous login call. Note that this should be done
/// only if the client also holds the corresponding encryption keys.
///
/// * `additional_scopes` - Additional scopes to request from the
/// authorization server, e.g. "urn:matrix:client:com.example.msc9999.foo".
/// The scopes for API access and the device ID according to the
/// [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
/// are always requested.
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: Option<OidcPrompt>,
login_hint: Option<String>,
device_id: Option<String>,
additional_scopes: Option<Vec<String>>,
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
let registration_data = oidc_configuration.registration_data()?;
let redirect_uri = oidc_configuration.redirect_uri()?;
let mut url_builder = self.inner.oauth().login(redirect_uri, None, Some(registration_data));
let device_id = device_id.map(OwnedDeviceId::from);
let additional_scopes =
additional_scopes.map(|scopes| scopes.into_iter().map(Scope::new).collect::<Vec<_>>());
let mut url_builder = self.inner.oauth().login(
redirect_uri,
device_id,
Some(registration_data),
additional_scopes,
);
if let Some(prompt) = prompt {
url_builder = url_builder.prompt(vec![prompt.into()]);
@@ -494,6 +544,45 @@ impl Client {
Ok(())
}
/// Log in using the provided [`QrCodeData`]. The `Client` must be built
/// by providing [`QrCodeData::server_name`] as the server name for this
/// login to succeed.
///
/// This method uses the login mechanism described in [MSC4108]. As such
/// this method requires OAuth 2.0 support as well as sliding sync support.
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn login_with_qr_code(
self: Arc<Self>,
qr_code_data: &QrCodeData,
oidc_configuration: &OidcConfiguration,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<(), HumanQrLoginError> {
let registration_data = oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let oauth = self.inner.oauth();
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(&registration_data));
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(())
}
/// Restores the client from a `Session`.
///
/// It reloads the entire set of rooms from the previous session.
@@ -539,6 +628,12 @@ impl Client {
self.inner.send_queue().set_enabled(enable).await;
}
/// Enables or disables progress reporting for media uploads in the send
/// queue.
pub fn enable_send_queue_upload_progress(&self, enable: bool) {
self.inner.send_queue().enable_upload_progress(enable);
}
/// Subscribe to the global enablement status of the send queue, at the
/// client-wide level.
///
@@ -694,11 +789,24 @@ impl Client {
}
}
/// Allows generic GET requests to be made through the SDKs internal HTTP
/// client
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
let http_client = self.inner.http_client();
Ok(http_client.get(url).send().await?.text().await?)
/// Allows generic GET requests to be made through the SDK's internal HTTP
/// client. This is useful when the caller's native HTTP client wouldn't
/// have the same configuration (such as certificates, proxies, etc.) This
/// method returns the raw bytes of the response, so that any kind of
/// resource can be fetched including images, files, etc.
///
/// Note: When an HTTP error occurs, the error response can be found in the
/// `ClientError::Generic`'s `details` field.
pub async fn get_url(&self, url: String) -> Result<Vec<u8>, ClientError> {
let response = self.inner.http_client().get(url).send().await?;
if response.status().is_success() {
Ok(response.bytes().await?.into())
} else {
Err(ClientError::Generic {
msg: response.status().to_string(),
details: response.text().await.ok(),
})
}
}
/// Empty the server version and unstable features cache.
@@ -744,18 +852,6 @@ impl Client {
}
}
impl Client {
/// Whether or not the client's homeserver supports the password login flow.
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
let login_types = self.inner.matrix_auth().get_login_types().await?;
let supports_password = login_types
.flows
.iter()
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_)));
Ok(supports_password)
}
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
@@ -1144,15 +1240,8 @@ impl Client {
}
pub async fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
let owned_user_id = UserId::parse(user_id.clone())?;
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
Ok(UserProfile {
user_id,
display_name: response.displayname.clone(),
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
})
let user_id = <&UserId>::try_from(user_id.as_str())?;
UserProfile::fetch(&self.inner.account(), user_id).await
}
pub async fn notification_client(
@@ -1170,6 +1259,11 @@ impl Client {
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
}
pub fn space_service(&self) -> Arc<SpaceService> {
let inner = UISpaceService::new((*self.inner).clone());
Arc::new(SpaceService::new(inner))
}
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
let inner = self.inner.notification_settings().await;
@@ -1183,13 +1277,10 @@ impl Client {
// Ignored users
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
if let Some(raw_content) = self
.inner
.account()
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
.await?
if let Some(raw_content) =
self.inner.account().fetch_account_data_static::<IgnoredUserListEventContent>().await?
{
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
let content = raw_content.deserialize()?;
let user_ids: Vec<String> =
content.ignored_users.keys().map(|id| id.to_string()).collect();
@@ -1532,6 +1623,14 @@ impl Client {
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
}
/// Get server vendor information from the federation API.
///
/// This method retrieves information about the server's name and version
/// by calling the `/_matrix/federation/v1/version` endpoint.
pub async fn server_vendor_info(&self) -> Result<matrix_sdk::ServerVendorInfo, ClientError> {
Ok(self.inner.server_vendor_info(None).await?)
}
/// Subscribe to changes in the media preview configuration.
pub async fn subscribe_to_media_preview_config(
&self,
@@ -1565,7 +1664,7 @@ impl Client {
) -> Result<Option<MediaPreviews>, ClientError> {
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
match configuration {
Some(configuration) => Ok(Some(configuration.media_previews.into())),
Some(configuration) => Ok(configuration.media_previews.map(Into::into)),
None => Ok(None),
}
}
@@ -1586,7 +1685,7 @@ impl Client {
) -> Result<Option<InviteAvatars>, ClientError> {
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
match configuration {
Some(configuration) => Ok(Some(configuration.invite_avatars.into())),
Some(configuration) => Ok(configuration.invite_avatars.map(Into::into)),
None => Ok(None),
}
}
@@ -1718,6 +1817,18 @@ pub struct UserProfile {
pub avatar_url: Option<String>,
}
impl UserProfile {
/// Fetch the profile for the given user ID, using the given [`Account`]
/// API.
pub(crate) async fn fetch(account: &Account, user_id: &UserId) -> Result<Self, ClientError> {
let response = account.fetch_user_profile_of(user_id).await?;
let display_name = response.get_static::<DisplayName>()?;
let avatar_url = response.get_static::<AvatarUrl>()?.map(|url| url.to_string());
Ok(UserProfile { user_id: user_id.to_string(), display_name, avatar_url })
}
}
impl From<&search_users::v3::User> for UserProfile {
fn from(value: &search_users::v3::User) -> Self {
UserProfile {
@@ -1832,7 +1943,7 @@ pub struct PowerLevels {
impl From<PowerLevels> for RoomPowerLevelsEventContent {
fn from(value: PowerLevels) -> Self {
let mut power_levels = RoomPowerLevelsEventContent::new();
let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
if let Some(users_default) = value.users_default {
power_levels.users_default = users_default.into();
@@ -2402,9 +2513,7 @@ impl TryFrom<AllowRule> for RumaAllowRule {
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,
)))
Ok(Self::RoomMembership(ruma::room::RoomMembership::new(room_id)))
}
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
}
+46 -68
View File
@@ -1,12 +1,9 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use futures_util::StreamExt;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
crypto::{
types::qr_login::QrCodeModeData, CollectStrategy, DecryptionSettings, TrustRequirement,
},
crypto::{CollectStrategy, DecryptionSettings, TrustRequirement},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
@@ -15,22 +12,14 @@ use matrix_sdk::{
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, SqliteStoreConfig,
RumaApiError, SqliteStoreConfig, ThreadingSupport,
};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use zeroize::Zeroizing;
use super::client::Client;
use crate::{
authentication::OidcConfiguration,
client::ClientSessionDelegate,
error::ClientError,
helpers::unwrap_or_clone_arc,
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
runtime::get_runtime_handle,
task_handle::TaskHandle,
};
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
/// A list of bytes containing a certificate in DER or PEM form.
pub type CertificateBytes = Vec<u8>;
@@ -140,8 +129,15 @@ pub struct ClientBuilder {
disable_built_in_root_certificates: bool,
#[cfg(not(target_family = "wasm"))]
additional_root_certificates: Vec<Vec<u8>>,
threading_support: ThreadingSupport,
}
/// The timeout applies to each read operation, and resets after a successful
/// read. This is more appropriate for detecting stalled connections when the
/// size isnt known beforehand.
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
#[uniffi::constructor]
@@ -177,6 +173,7 @@ impl ClientBuilder {
},
enable_share_history_on_invite: false,
request_config: Default::default(),
threading_support: ThreadingSupport::Disabled,
})
}
@@ -390,6 +387,23 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Whether the client should support threads client-side or not, and enable
/// experimental support for MSC4306 (threads subscriptions) or not.
pub fn threads_enabled(
self: Arc<Self>,
enabled: bool,
thread_subscriptions: bool,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
let support = if enabled {
ThreadingSupport::Enabled { with_subscriptions: thread_subscriptions }
} else {
ThreadingSupport::Disabled
};
builder.threading_support = support;
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
@@ -541,6 +555,7 @@ impl ClientBuilder {
if let Some(timeout) = config.timeout {
updated_config = updated_config.timeout(Duration::from_millis(timeout));
}
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
if max_concurrent_requests > 0 {
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
@@ -555,8 +570,25 @@ impl ClientBuilder {
inner_builder = inner_builder.request_config(updated_config);
}
inner_builder = inner_builder.with_threading_support(builder.threading_support);
let sdk_client = inner_builder.build().await?;
// Disable retries for this request to prevent it from being retried
// indefinitely
let config = sdk_client.request_config().disable_retry();
// Log server version information at info level.
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
tracing::info!(
server_name = %server_info.server_name,
version = %server_info.version,
"Connected to Matrix server"
);
} else {
tracing::warn!("Could not retrieve server version information");
}
Ok(Arc::new(
Client::new(
sdk_client,
@@ -567,60 +599,6 @@ impl ClientBuilder {
.await?,
))
}
/// Finish the building of the client and attempt to log in using the
/// provided [`QrCodeData`].
///
/// This method will build the client and immediately attempt to log the
/// client in using the provided [`QrCodeData`] using the login
/// mechanism described in [MSC4108]. As such this methods requires OAuth
/// 2.0 support as well as sliding sync support.
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn build_with_qr_code(
self: Arc<Self>,
qr_code_data: &QrCodeData,
oidc_configuration: &OidcConfiguration,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<Arc<Client>, HumanQrLoginError> {
let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data else {
return Err(HumanQrLoginError::OtherDeviceNotSignedIn);
};
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
let client = builder.build().await.map_err(|e| match e {
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
_ => {
error!("Couldn't build the client {e:?}");
HumanQrLoginError::Unknown
}
})?;
let registration_data = oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let oauth = client.inner.oauth();
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(&registration_data));
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(client)
}
}
#[cfg(not(target_family = "wasm"))]
+1 -1
View File
@@ -442,7 +442,7 @@ impl Encryption {
Err(error) => {
error!("Failed fetching identity from the store: {error}");
}
};
}
info!("Requesting identity from the server.");
+1
View File
@@ -25,6 +25,7 @@ mod room_preview;
mod ruma;
mod runtime;
mod session_verification;
mod spaces;
mod sync_service;
mod task_handle;
mod timeline;
+67 -31
View File
@@ -1,10 +1,10 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
};
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
use tracing::error;
use crate::{
client::{Client, JoinRule},
@@ -54,12 +54,12 @@ pub struct NotificationItem {
}
impl NotificationItem {
fn from_inner(item: MatrixNotificationItem) -> Self {
fn from_inner(item: SdkNotificationItem) -> Self {
let event = match item.event {
matrix_sdk_ui::notification_client::NotificationEvent::Timeline(event) => {
SdkNotificationEvent::Timeline(event) => {
NotificationEvent::Timeline { event: Arc::new(TimelineEvent(event)) }
}
matrix_sdk_ui::notification_client::NotificationEvent::Invite(event) => {
SdkNotificationEvent::Invite(event) => {
NotificationEvent::Invite { sender: event.sender.to_string() }
}
};
@@ -87,9 +87,46 @@ impl NotificationItem {
}
}
#[allow(clippy::large_enum_variant)]
#[derive(uniffi::Enum)]
pub enum NotificationStatus {
/// The event has been found and was not filtered out.
Event { item: NotificationItem },
/// The event couldn't be found in the network queries used to find it.
EventNotFound,
/// The event has been filtered out, either because of the user's push
/// rules, or because the user which triggered it is ignored by the
/// current user.
EventFilteredOut,
}
impl From<SdkNotificationStatus> for NotificationStatus {
fn from(item: SdkNotificationStatus) -> Self {
match item {
SdkNotificationStatus::Event(item) => {
NotificationStatus::Event { item: NotificationItem::from_inner(*item) }
}
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(uniffi::Enum)]
pub enum BatchNotificationResult {
/// We have more detailed information about the notification.
Ok { status: NotificationStatus },
/// An error occurred while trying to fetch the notification.
Error {
/// The error message observed while handling a specific notification.
message: String,
},
}
#[derive(uniffi::Object)]
pub struct NotificationClient {
pub(crate) inner: MatrixNotificationClient,
pub(crate) inner: SdkNotificationClient,
/// A reference to the FFI client.
///
@@ -113,55 +150,54 @@ impl NotificationClient {
Ok(room)
}
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
/// Fetches the content of a notification.
///
/// This will first try to get the notification using a short-lived sliding
/// sync, and if the sliding-sync can't find the event, then it'll use a
/// `/context` query to find the event with associated member information.
///
/// An error result means that we couldn't resolve the notification; in that
/// case, a dummy notification may be displayed instead.
pub async fn get_notification(
&self,
room_id: String,
event_id: String,
) -> Result<Option<NotificationItem>, ClientError> {
) -> Result<NotificationStatus, ClientError> {
let room_id = RoomId::parse(room_id)?;
let event_id = EventId::parse(event_id)?;
let item =
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
if let Some(item) = item {
Ok(Some(NotificationItem::from_inner(item)))
} else {
Ok(None)
}
Ok(item.into())
}
/// Get several notification items in a single batch.
///
/// Returns an error if the flow failed when preparing to fetch the
/// notifications, and a [`HashMap`] containing either a
/// [`NotificationItem`] or no entry for it if it failed to fetch a
/// notification for the provided [`EventId`].
/// [`BatchNotificationResult`], that indicates if the notification was
/// successfully fetched (in which case, it's a [`NotificationStatus`]), or
/// an error message if it couldn't be fetched.
pub async fn get_notifications(
&self,
requests: Vec<NotificationItemsRequest>,
) -> Result<HashMap<String, NotificationItem>, ClientError> {
) -> Result<HashMap<String, BatchNotificationResult>, ClientError> {
let requests =
requests.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?;
let items = self.inner.get_notifications(&requests).await?;
let mut result = HashMap::new();
let mut batch_result = HashMap::new();
for (key, value) in items.into_iter() {
match value {
Ok(item) => {
result.insert(key.to_string(), NotificationItem::from_inner(item));
}
Err(error) => {
// TODO This error should actually be returned so the clients can handle the
// error as they see fit, but it's failing when creating
// bindings for Go, i.e.
// (https://github.com/NordSecurity/uniffi-bindgen-go/issues/62)
error!("Could not fetch notification {key}, an error happened: {error}");
}
}
let result = match value {
Ok(status) => BatchNotificationResult::Ok { status: status.into() },
Err(error) => BatchNotificationResult::Error { message: error.to_string() },
};
batch_result.insert(key.to_string(), result);
}
Ok(result)
Ok(batch_result)
}
}
@@ -172,7 +172,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
}
SdkPushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key }
Self::SenderNotificationPermission { key: key.to_string() }
}
SdkPushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
@@ -197,7 +197,7 @@ impl From<PushCondition> for SdkPushCondition {
},
},
PushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key }
Self::SenderNotificationPermission { key: key.into() }
}
PushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
+206 -128
View File
@@ -14,164 +14,158 @@ use tracing_subscriber::{
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::SubscriberExt as _,
layer::{Layered, SubscriberExt as _},
registry::LookupSpan,
reload::{self, Handle},
util::SubscriberInitExt as _,
Layer,
EnvFilter, Layer, Registry,
};
use crate::{error::ClientError, tracing::LogLevel};
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
display_timestamp: bool,
display_level: bool,
}
impl EventFormatter {
fn new() -> Self {
Self { display_timestamp: true, display_level: true }
}
#[cfg(target_os = "android")]
fn for_logcat() -> Self {
// Level and time are already captured by logcat separately
Self { display_timestamp: false, display_level: false }
}
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
if fmt::time::SystemTime.format_time(writer).is_err() {
writer.write_str("<unknown time>")?;
}
Ok(())
}
fn write_filename(
&self,
writer: &mut fmt::format::Writer<'_>,
filename: &str,
) -> std::fmt::Result {
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
let crates_io_filename = filename
.split_once(CRATES_IO_PATH_MATCHER)
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
if let Some(filename) = crates_io_filename {
writer.write_str("<crates.io>/")?;
writer.write_str(filename)
} else {
writer.write_str(filename)
}
}
}
impl<S, N> FormatEvent<S, N> for EventFormatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
display_timestamp: bool,
display_level: bool,
}
fn format_event(
&self,
ctx: &fmt::FmtContext<'_, S, N>,
mut writer: fmt::format::Writer<'_>,
event: &tracing_core::Event<'_>,
) -> std::fmt::Result {
let meta = event.metadata();
impl EventFormatter {
fn new() -> Self {
Self { display_timestamp: true, display_level: true }
if self.display_timestamp {
self.format_timestamp(&mut writer)?;
writer.write_char(' ')?;
}
#[cfg(target_os = "android")]
fn for_logcat() -> Self {
// Level and time are already captured by logcat separately
Self { display_timestamp: false, display_level: false }
if self.display_level {
// For info and warn, add a padding space to the left
write!(writer, "{:>5} ", meta.level())?;
}
fn format_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> std::fmt::Result {
if fmt::time::SystemTime.format_time(writer).is_err() {
writer.write_str("<unknown time>")?;
}
Ok(())
}
write!(writer, "{}: ", meta.target())?;
fn write_filename(
&self,
writer: &mut fmt::format::Writer<'_>,
filename: &str,
) -> std::fmt::Result {
const CRATES_IO_PATH_MATCHER: &str = ".cargo/registry/src/index.crates.io";
let crates_io_filename = filename
.split_once(CRATES_IO_PATH_MATCHER)
.and_then(|(_, rest)| rest.split_once('/').map(|(_, rest)| rest));
ctx.format_fields(writer.by_ref(), event)?;
if let Some(filename) = crates_io_filename {
writer.write_str("<crates.io>/")?;
writer.write_str(filename)
} else {
writer.write_str(filename)
if let Some(filename) = meta.file() {
writer.write_str(" | ")?;
self.write_filename(&mut writer, filename)?;
if let Some(line_number) = meta.line() {
write!(writer, ":{line_number}")?;
}
}
}
impl<S, N> FormatEvent<S, N> for EventFormatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &fmt::FmtContext<'_, S, N>,
mut writer: fmt::format::Writer<'_>,
event: &tracing_core::Event<'_>,
) -> std::fmt::Result {
let meta = event.metadata();
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
if self.display_timestamp {
self.format_timestamp(&mut writer)?;
writer.write_char(' ')?;
}
let mut first = true;
if self.display_level {
// For info and warn, add a padding space to the left
write!(writer, "{:>5} ", meta.level())?;
}
write!(writer, "{}: ", meta.target())?;
ctx.format_fields(writer.by_ref(), event)?;
if let Some(filename) = meta.file() {
writer.write_str(" | ")?;
self.write_filename(&mut writer, filename)?;
if let Some(line_number) = meta.line() {
write!(writer, ":{line_number}")?;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
}
if let Some(scope) = ctx.event_scope() {
writer.write_str(" | spans: ")?;
first = false;
let mut first = true;
write!(writer, "{}", span.name())?;
for span in scope.from_root() {
if !first {
writer.write_str(" > ")?;
}
first = false;
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
}
}
writeln!(writer)
}
writeln!(writer)
}
}
let file_layer = config.write_to_files.map(|c| {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&c.file_prefix);
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterForFiles(DefaultFields);
if let Some(max_files) = c.max_files {
builder = builder.max_log_files(max_files as usize)
};
if let Some(file_suffix) = c.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
type ReloadHandle = Handle<
tracing_subscriber::fmt::Layer<
Layered<EnvFilter, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
>,
Layered<EnvFilter, Registry>,
>;
// Another fields formatter is necessary because of this bug
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
// formatter for the fields forces to record them in different span
// extensions, and thus remove the duplicated fields in the span.
#[derive(Default)]
struct FieldsFormatterForFiles(DefaultFields);
fn text_layers(
config: TracingConfiguration,
) -> (impl Layer<Layered<EnvFilter, Registry>>, Option<ReloadHandle>) {
let (file_layer, reload_handle) = config
.write_to_files
.map(|c| {
let layer = make_file_layer(c);
reload::Layer::new(layer)
})
.unzip();
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
fn format_fields<R: RecordFields>(
&self,
writer: Writer<'writer>,
fields: R,
) -> std::fmt::Result {
self.0.format_fields(writer, fields)
}
}
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(writer)
});
Layer::and_then(
let layers = Layer::and_then(
file_layer,
config.write_to_stdout_or_system.then(|| {
// Another fields formatter is necessary because of this bug
@@ -209,7 +203,41 @@ where
"org.matrix.rust.sdk".to_owned(),
));
}),
)
);
(layers, reload_handle)
}
fn make_file_layer(
file_configuration: TracingFileConfiguration,
) -> tracing_subscriber::fmt::Layer<
Layered<EnvFilter, Registry, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
> {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&file_configuration.file_prefix);
if let Some(max_files) = file_configuration.max_files {
builder = builder.max_log_files(max_files as usize)
}
if let Some(file_suffix) = file_configuration.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
let writer =
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(writer)
}
/// Configuration to save logs to (rotated) log-files.
@@ -243,6 +271,7 @@ enum LogTarget {
MatrixSdkBaseEventCache,
MatrixSdkBaseSlidingSync,
MatrixSdkBaseStoreAmbiguityMap,
MatrixSdkBaseResponseProcessors,
// SDK common modules.
MatrixSdkCommonStoreLocks,
@@ -272,6 +301,7 @@ impl LogTarget {
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
LogTarget::MatrixSdk => "matrix_sdk",
LogTarget::MatrixSdkClient => "matrix_sdk::client",
@@ -308,6 +338,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
];
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
@@ -330,6 +361,8 @@ pub enum TraceLogPacks {
Timeline,
/// Enables all the logs relevant to the notification client.
NotificationClient,
/// Enables all the logs relevant to sync profiling.
SyncProfiling,
}
impl TraceLogPacks {
@@ -345,6 +378,12 @@ impl TraceLogPacks {
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
TraceLogPacks::SyncProfiling => &[
LogTarget::MatrixSdkSlidingSync,
LogTarget::MatrixSdkBaseSlidingSync,
LogTarget::MatrixSdkBaseResponseProcessors,
LogTarget::MatrixSdkCrypto,
],
}
}
}
@@ -359,6 +398,7 @@ struct SentryLoggingCtx {
}
struct LoggingCtx {
reload_handle: Option<ReloadHandle>,
#[cfg(feature = "sentry")]
sentry: Option<SentryLoggingCtx>,
}
@@ -463,20 +503,23 @@ impl TracingConfiguration {
} else {
(None, None)
};
let (text_layers, reload_handle) = crate::platform::text_layers(self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(crate::platform::text_layers(self))
.with(text_layers)
.with(sentry_layer)
.init();
logging_ctx = LoggingCtx { sentry: sentry_logging_ctx };
logging_ctx = LoggingCtx { reload_handle, sentry: sentry_logging_ctx };
}
#[cfg(not(feature = "sentry"))]
{
let (text_layers, reload_handle) = crate::platform::text_layers(self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(crate::platform::text_layers(self))
.with(text_layers)
.init();
logging_ctx = LoggingCtx {};
logging_ctx = LoggingCtx { reload_handle };
}
// Log the log levels 🧠.
@@ -570,6 +613,36 @@ pub fn enable_sentry_logging(enabled: bool) {
};
}
/// Updates the tracing subscriber with a new file writer based on the provided
/// configuration.
///
/// This method will throw if `init_platform` hasn't been called, or if it was
/// called with `write_to_files` set to `None`.
#[matrix_sdk_ffi_macros::export]
pub fn reload_tracing_file_writer(
configuration: TracingFileConfiguration,
) -> Result<(), ClientError> {
let Some(logging_context) = LOGGING.get() else {
return Err(ClientError::Generic {
msg: "Logging hasn't been initialized yet".to_owned(),
details: None,
});
};
let Some(reload_handle) = logging_context.reload_handle.as_ref() else {
return Err(ClientError::Generic {
msg: "Logging wasn't initialized with a file config".to_owned(),
details: None,
});
};
let layer = make_file_layer(configuration);
reload_handle.reload(layer).map_err(|error| ClientError::Generic {
msg: format!("Failed to reload file config: {error}"),
details: None,
})
}
#[cfg(not(target_family = "wasm"))]
fn setup_multithreaded_tokio_runtime() {
async_compat::set_runtime_builder(Box::new(|| {
@@ -613,6 +686,8 @@ fn setup_lightweight_tokio_runtime() {
#[cfg(test)]
mod tests {
use similar_asserts::assert_eq;
use super::build_tracing_filter;
use crate::platform::TraceLogPacks;
@@ -651,6 +726,7 @@ mod tests {
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
matrix_sdk_base::response_processors=debug,
super_duper_app=error"#
.split('\n')
.map(|s| s.trim())
@@ -694,6 +770,7 @@ mod tests {
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=trace,
matrix_sdk_base::response_processors=trace,
super_duper_app=trace,
some_other_span=trace"#
.split('\n')
@@ -738,6 +815,7 @@ mod tests {
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
matrix_sdk_base::response_processors=debug,
super_duper_app=info"#
.split('\n')
.map(|s| s.trim())
+61 -54
View File
@@ -44,11 +44,11 @@ use crate::{
live_location_share::{LastLocation, LiveLocationShare},
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
ruma::{ImageInfo, LocationContent},
runtime::get_runtime_handle,
timeline::{
configuration::{TimelineConfiguration, TimelineFilter},
EventTimelineItem, ReceiptType, SendHandle, Timeline,
EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
},
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
@@ -304,6 +304,10 @@ impl Room {
self.inner.latest_event_item().await.map(Into::into)
}
async fn new_latest_event(&self) -> LatestEventValue {
self.inner.new_latest_event().await.into()
}
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
Ok(self.inner.latest_encryption_state().await?)
}
@@ -484,7 +488,7 @@ impl Room {
/// # Errors
///
/// Returns an error if the room is not found or on rate limit
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
pub async fn report_room(&self, reason: String) -> Result<(), ClientError> {
self.inner.report_room(reason).await?;
Ok(())
@@ -720,53 +724,6 @@ impl Room {
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
}
/// This will only send a call notification event if appropriate.
///
/// This function is supposed to be called whenever the user creates a room
/// call. It will send a `m.call.notify` event if:
/// - there is not yet a running call.
///
/// It will configure the notify type: ring or notify based on:
/// - is this a DM room -> ring
/// - is this a group with more than one other member -> notify
///
/// Returns:
/// - `Ok(true)` if the event was successfully sent.
/// - `Ok(false)` if we didn't send it because it was unnecessary.
/// - `Err(_)` if sending the event failed.
pub async fn send_call_notification_if_needed(&self) -> Result<bool, ClientError> {
Ok(self.inner.send_call_notification_if_needed().await?)
}
/// Send a call notification event in the current room.
///
/// This is only supposed to be used in **custom** situations where the user
/// explicitly chooses to send a `m.call.notify` event to invite/notify
/// someone explicitly in unusual conditions. The default should be to
/// use `send_call_notification_if_necessary` just before a new room call is
/// created/joined.
///
/// One example could be that the UI allows to start a call with a subset of
/// users of the room members first. And then later on the user can
/// invite more users to the call.
pub async fn send_call_notification(
&self,
call_id: String,
application: RtcApplicationType,
notify_type: NotifyType,
mentions: Mentions,
) -> Result<(), ClientError> {
self.inner
.send_call_notification(
call_id,
application.into(),
notify_type.into(),
mentions.into(),
)
.await?;
Ok(())
}
/// Returns whether the send queue for that particular room is enabled or
/// not.
pub fn is_send_queue_enabled(&self) -> bool {
@@ -1139,6 +1096,59 @@ impl Room {
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
/// Set a MSC4306 subscription to a thread in this room, based on the thread
/// root event id.
///
/// If `subscribed` is `true`, it will subscribe to the thread, with a
/// precision that the subscription was manually requested by the user
/// (i.e. not automatic).
///
/// If the thread was already subscribed to (resp. unsubscribed from), while
/// trying to subscribe to it (resp. unsubscribe from it), it will do
/// nothing, i.e. subscribing (resp. unsubscribing) to a thread is an
/// idempotent operation.
pub async fn set_thread_subscription(
&self,
thread_root_event_id: String,
subscribed: bool,
) -> Result<(), ClientError> {
let thread_root = EventId::parse(thread_root_event_id)?;
if subscribed {
// This is a manual subscription.
let automatic = None;
self.inner.subscribe_thread(thread_root, automatic).await?;
} else {
self.inner.unsubscribe_thread(thread_root).await?;
}
Ok(())
}
/// Return the current MSC4306 thread subscription for the given thread root
/// in this room.
///
/// Returns `None` if the thread doesn't exist, or isn't subscribed to, or
/// the server can't handle MSC4306; otherwise, returns the thread
/// subscription status.
pub async fn fetch_thread_subscription(
&self,
thread_root_event_id: String,
) -> Result<Option<ThreadSubscription>, ClientError> {
let thread_root = EventId::parse(thread_root_event_id)?;
Ok(self
.inner
.fetch_thread_subscription(thread_root)
.await?
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
}
}
/// A thread subscription (MSC4306).
#[derive(uniffi::Record)]
pub struct ThreadSubscription {
/// Whether the thread subscription happened automatically (e.g. after a
/// mention) or if it was manually requested by the user.
automatic: bool,
}
/// A listener for receiving new live location shares in a room.
@@ -1504,13 +1514,10 @@ impl From<SdkSuccessorRoom> for SuccessorRoom {
pub struct PredecessorRoom {
/// The ID of the replacement room.
pub room_id: String,
/// The event ID of the last known event in the predecesssor room.
pub last_event_id: String,
}
impl From<SdkPredecessorRoom> for PredecessorRoom {
fn from(value: SdkPredecessorRoom) -> Self {
Self { room_id: value.room_id.to_string(), last_event_id: value.last_event_id.to_string() }
Self { room_id: value.room_id.to_string() }
}
}
+15 -2
View File
@@ -17,7 +17,7 @@ use crate::{
pub struct RoomInfo {
id: String,
encryption_state: EncryptionState,
creator: Option<String>,
creators: Option<Vec<String>>,
/// The room's name from the room state event if received from sync, or one
/// that's been computed otherwise.
display_name: Option<String>,
@@ -74,6 +74,11 @@ pub struct RoomInfo {
///
/// Can be missing if the room power levels event is missing from the store.
power_levels: Option<Arc<RoomPowerLevels>>,
/// This room's version.
room_version: Option<String>,
/// Whether creators are privileged over every other user (have infinite
/// power level).
privileged_creators_role: bool,
}
impl RoomInfo {
@@ -102,7 +107,9 @@ impl RoomInfo {
Ok(Self {
id: room.room_id().to_string(),
encryption_state: room.encryption_state(),
creator: room.creator().as_ref().map(ToString::to_string),
creators: room
.creators()
.map(|creators| creators.into_iter().map(Into::into).collect()),
display_name: room.cached_display_name().map(|name| name.to_string()),
raw_name: room.name(),
topic: room.topic(),
@@ -150,6 +157,12 @@ impl RoomInfo {
join_rule,
history_visibility: room.history_visibility_or_default().try_into()?,
power_levels: power_levels.map(Arc::new),
room_version: room.version().map(|version| version.to_string()),
privileged_creators_role: room
.version()
.and_then(|version| version.rules())
.map(|rules| rules.authorization.explicitly_privilege_room_creators)
.unwrap_or_default(),
})
}
}
@@ -28,15 +28,21 @@ use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHa
pub enum PublicRoomJoinRule {
Public,
Knock,
Restricted,
KnockRestricted,
Invite,
}
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
impl TryFrom<ruma::room::JoinRuleKind> for PublicRoomJoinRule {
type Error = String;
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
fn try_from(value: ruma::room::JoinRuleKind) -> Result<Self, Self::Error> {
match value {
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
ruma::room::JoinRuleKind::Public => Ok(Self::Public),
ruma::room::JoinRuleKind::Knock => Ok(Self::Knock),
ruma::room::JoinRuleKind::Restricted => Ok(Self::Restricted),
ruma::room::JoinRuleKind::KnockRestricted => Ok(Self::KnockRestricted),
ruma::room::JoinRuleKind::Invite => Ok(Self::Invite),
rule => Err(format!("unsupported join rule: {rule:?}")),
}
}
@@ -149,11 +155,6 @@ impl RoomDirectorySearch {
}
}
#[derive(uniffi::Record)]
pub struct RoomDirectorySearchEntriesResult {
pub entries_stream: Arc<TaskHandle>,
}
#[derive(uniffi::Enum)]
pub enum RoomDirectorySearchEntryUpdate {
Append { values: Vec<RoomDescription> },
+15 -4
View File
@@ -16,8 +16,9 @@ use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
new_filter_joined, new_filter_non_left, new_filter_none,
new_filter_normalized_match_room_name, new_filter_unread, BoxedFilterFn, RoomCategory,
new_filter_joined, new_filter_low_priority, new_filter_non_left, new_filter_none,
new_filter_normalized_match_room_name, new_filter_not, new_filter_space, new_filter_unread,
BoxedFilterFn, RoomCategory,
},
unable_to_decrypt_hook::UtdHookManager,
};
@@ -121,7 +122,7 @@ impl RoomListService {
})))
}
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
async fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
let room_ids = room_ids
.into_iter()
.map(|room_id| {
@@ -129,7 +130,9 @@ impl RoomListService {
})
.collect::<Result<Vec<_>, _>>()?;
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
self.inner
.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>())
.await;
Ok(())
}
@@ -452,10 +455,15 @@ impl RoomListDynamicEntriesController {
pub enum RoomListEntriesDynamicFilterKind {
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonSpace,
NonLeft,
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
Joined,
Unread,
Favourite,
LowPriority,
NonLowPriority,
Invite,
Category { expect: RoomListFilterCategory },
None,
@@ -491,9 +499,12 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
)),
Kind::NonLeft => Box::new(new_filter_non_left()),
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
Kind::Joined => Box::new(new_filter_joined()),
Kind::Unread => Box::new(new_filter_unread()),
Kind::Favourite => Box::new(new_filter_favourite()),
Kind::LowPriority => Box::new(new_filter_low_priority()),
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
Kind::Invite => Box::new(new_filter_invite()),
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
Kind::None => Box::new(new_filter_none()),
+57 -9
View File
@@ -1,5 +1,5 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::UserId;
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
use crate::error::{ClientError, NotYetImplemented};
@@ -57,16 +57,25 @@ impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for Member
}
}
/// Get the suggested role for the given power level.
///
/// Returns an error if the value of the power level is out of range for numbers
/// accepted in canonical JSON.
#[matrix_sdk_ffi_macros::export]
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
pub fn suggested_role_for_power_level(
power_level: PowerLevel,
) -> Result<RoomMemberRole, ClientError> {
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
RoomMemberRole::suggested_role_for_power_level(power_level)
Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?))
}
/// Get the suggested power level for the given role.
///
/// Returns an error if the value of the power level is unsupported.
#[matrix_sdk_ffi_macros::export]
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> Result<PowerLevel, ClientError> {
// It's not possible to expose methods on an Enum through Uniffi ☹️
role.suggested_power_level()
Ok(role.suggested_power_level().try_into()?)
}
/// Generates a `matrix.to` permalink to the given userID.
@@ -83,8 +92,8 @@ pub struct RoomMember {
pub avatar_url: Option<String>,
pub membership: MembershipState,
pub is_name_ambiguous: bool,
pub power_level: i64,
pub normalized_power_level: i64,
pub power_level: PowerLevel,
pub normalized_power_level: PowerLevel,
pub is_ignored: bool,
pub suggested_role_for_power_level: RoomMemberRole,
pub membership_change_reason: Option<String>,
@@ -100,8 +109,8 @@ impl TryFrom<SdkRoomMember> for RoomMember {
avatar_url: m.avatar_url().map(|a| a.to_string()),
membership: m.membership().clone().try_into()?,
is_name_ambiguous: m.name_ambiguous(),
power_level: m.power_level(),
normalized_power_level: m.normalized_power_level(),
power_level: m.power_level().try_into()?,
normalized_power_level: m.normalized_power_level().try_into()?,
is_ignored: m.is_ignored(),
suggested_role_for_power_level: m.suggested_role_for_power_level(),
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
@@ -130,3 +139,42 @@ impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSende
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum PowerLevel {
/// The user is a room creator and has infinite power level.
///
/// This power level was introduced in room version 12.
Infinite,
/// The user has the given power level.
Value { value: i64 },
}
impl TryFrom<UserPowerLevel> for PowerLevel {
type Error = NotYetImplemented;
fn try_from(value: UserPowerLevel) -> Result<Self, Self::Error> {
match value {
UserPowerLevel::Infinite => Ok(Self::Infinite),
UserPowerLevel::Int(value) => Ok(Self::Value { value: value.into() }),
_ => Err(NotYetImplemented),
}
}
}
impl TryFrom<PowerLevel> for UserPowerLevel {
type Error = ClientError;
fn try_from(value: PowerLevel) -> Result<Self, Self::Error> {
Ok(match value {
PowerLevel::Infinite => Self::Infinite,
PowerLevel::Value { value } => {
Self::Int(value.try_into().map_err(|err| ClientError::Generic {
msg: "Power level is out of range".to_owned(),
details: Some(format!("{err:?}")),
})?)
}
})
}
}
+32 -32
View File
@@ -1,10 +1,9 @@
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 ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
use crate::{
client::JoinRule,
client::{AllowRule, JoinRule},
error::ClientError,
room::{Membership, RoomHero},
room_member::{RoomMember, RoomMemberWithSenderInfo},
@@ -22,9 +21,9 @@ pub struct RoomPreview {
#[matrix_sdk_ffi_macros::export]
impl RoomPreview {
/// Returns the room info the preview contains.
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
pub fn info(&self) -> RoomPreviewInfo {
let info = &self.inner;
Ok(RoomPreviewInfo {
RoomPreviewInfo {
room_id: info.room_id.to_string(),
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
name: info.name.clone(),
@@ -32,21 +31,16 @@ impl RoomPreview {
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(),
room_type: info.room_type.clone().into(),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.as_ref()
.map(TryInto::try_into)
.transpose()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
join_rule: info.join_rule.clone().map(Into::into),
is_direct: info.is_direct,
heroes: info
.heroes
.as_ref()
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
})
}
}
/// Leave the room if the room preview state is either joined, invited or
@@ -122,23 +116,29 @@ pub struct RoomPreviewInfo {
pub heroes: Option<Vec<RoomHero>>,
}
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(());
}
})
impl From<JoinRuleSummary> for JoinRule {
fn from(join_rule: JoinRuleSummary) -> Self {
match join_rule {
JoinRuleSummary::Invite => JoinRule::Invite,
JoinRuleSummary::Knock => JoinRule::Knock,
JoinRuleSummary::Private => JoinRule::Private,
JoinRuleSummary::Restricted(summary) => JoinRule::Restricted {
rules: summary
.allowed_room_ids
.iter()
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
.collect(),
},
JoinRuleSummary::KnockRestricted(summary) => JoinRule::KnockRestricted {
rules: summary
.allowed_room_ids
.iter()
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
.collect(),
},
JoinRuleSummary::Public => JoinRule::Public,
_ => JoinRule::Custom { repr: join_rule.as_str().to_owned() },
}
}
}
@@ -153,8 +153,8 @@ pub enum RoomType {
Custom { value: String },
}
impl From<Option<&RumaRoomType>> for RoomType {
fn from(value: Option<&RumaRoomType>) -> Self {
impl From<Option<RumaRoomType>> for RoomType {
fn from(value: Option<RumaRoomType>) -> Self {
match value {
Some(RumaRoomType::Space) => RoomType::Space,
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
+4 -4
View File
@@ -1486,17 +1486,17 @@ impl From<RumaSecretStorageV1AesHmacSha2Properties> for SecretStorageV1AesHmacSh
#[derive(Clone, uniffi::Record, Default)]
pub struct MediaPreviewConfig {
/// The media previews setting for the user.
pub media_previews: MediaPreviews,
pub media_previews: Option<MediaPreviews>,
/// The invite avatars setting for the user.
pub invite_avatars: InviteAvatars,
pub invite_avatars: Option<InviteAvatars>,
}
impl From<MediaPreviewConfigEventContent> for MediaPreviewConfig {
fn from(value: MediaPreviewConfigEventContent) -> Self {
Self {
media_previews: value.media_previews.into(),
invite_avatars: value.invite_avatars.into(),
media_previews: value.media_previews.map(Into::into),
invite_avatars: value.invite_avatars.map(Into::into),
}
}
}
@@ -254,18 +254,14 @@ impl SessionVerificationController {
return;
};
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
let Ok(sender_profile) = UserProfile::fetch(&self.account, sender).await else {
error!("Failed fetching user profile for verification request");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_profile: UserProfile {
user_id: request.other_user_id().to_string(),
display_name: sender_profile.displayname,
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
},
sender_profile,
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
device_display_name: other_device_data.display_name().map(str::to_string),
+276
View File
@@ -0,0 +1,276 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
};
use ruma::RoomId;
use crate::{
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_preview::RoomType,
runtime::get_runtime_handle,
TaskHandle,
};
/// The main entry point into the Spaces facilities.
///
/// The spaces service is responsible for retrieving one's joined rooms,
/// building a graph out of their `m.space.parent` and `m.space.child` state
/// events, and providing access to the top-level spaces and their children.
#[derive(uniffi::Object)]
pub struct SpaceService {
inner: UISpaceService,
}
impl SpaceService {
/// Creates a new `SpaceService` instance.
pub(crate) fn new(inner: UISpaceService) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl SpaceService {
/// Returns a list of all the top-level joined spaces. It will eagerly
/// compute the latest version and also notify subscribers if there were
/// any changes.
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
}
/// Subscribes to updates on the joined spaces list. If space rooms are
/// joined or left, the stream will yield diffs that reflect the changes.
pub async fn subscribe_to_joined_spaces(
&self,
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
listener.on_update(vec![SpaceListUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(Into::into).collect());
}
})))
}
/// Returns a `SpaceRoomList` for the given space ID.
#[allow(clippy::unused_async)]
// This method doesn't need to be async but if its not the FFI layer panics
// with "there is no no reactor running, must be called from the context
// of a Tokio 1.x runtime" error because the underlying constructor spawns
// an async task.
pub async fn space_room_list(
&self,
space_id: String,
) -> Result<Arc<SpaceRoomList>, ClientError> {
let space_id = RoomId::parse(space_id)?;
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
}
}
/// The `SpaceRoomList`represents a paginated list of direct rooms
/// that belong to a particular space.
///
/// It can be used to paginate through the list (and have live updates on the
/// pagination state) as well as subscribe to changes as rooms are joined or
/// left.
///
/// The `SpaceRoomList` also automatically subscribes to client room changes
/// and updates the list accordingly as rooms are joined or left.
#[derive(uniffi::Object)]
pub struct SpaceRoomList {
inner: UISpaceRoomList,
}
impl SpaceRoomList {
/// Creates a new `SpaceRoomList` for the underlying UI crate room list.
fn new(inner: UISpaceRoomList) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl SpaceRoomList {
/// Returns if the room list is currently paginating or not.
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
self.inner.pagination_state()
}
/// Subscribe to pagination updates.
pub fn subscribe_to_pagination_state_updates(
&self,
listener: Box<dyn SpaceRoomListPaginationStateListener>,
) -> Arc<TaskHandle> {
let pagination_state = self.inner.subscribe_to_pagination_state_updates();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(pagination_state);
while let Some(state) = pagination_state.next().await {
listener.on_update(state);
}
})))
}
/// Return the current list of rooms.
pub fn rooms(&self) -> Vec<SpaceRoom> {
self.inner.rooms().into_iter().map(Into::into).collect()
}
/// Subscribes to room list updates.
pub fn subscribe_to_room_update(
&self,
listener: Box<dyn SpaceRoomListEntriesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_room_updates();
listener.on_update(vec![SpaceListUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(Into::into).collect());
}
})))
}
/// Ask the list to retrieve the next page if the end hasn't been reached
/// yet. Otherwise it no-ops.
pub async fn paginate(&self) -> Result<(), ClientError> {
self.inner.paginate().await.map_err(ClientError::from)
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceRoomListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, pagination_state: SpaceRoomListPaginationState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, rooms: Vec<SpaceListUpdate>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
}
/// Structure representing a room in a space and aggregated information
/// relevant to the UI layer.
#[derive(uniffi::Record)]
pub struct SpaceRoom {
/// The ID of the room.
pub room_id: String,
/// The canonical alias of the room, if any.
pub canonical_alias: Option<String>,
/// The name of the room, if any.
pub name: Option<String>,
/// The topic of the room, if any.
pub topic: Option<String>,
/// The URL for the room's avatar, if one is set.
pub avatar_url: Option<String>,
/// The type of room from `m.room.create`, if any.
pub room_type: RoomType,
/// The number of members joined to the room.
pub num_joined_members: u64,
/// The join rule of the room.
pub join_rule: Option<JoinRule>,
/// Whether the room may be viewed by users without joining.
pub world_readable: Option<bool>,
/// Whether guest users may join the room and participate in it.
pub guest_can_join: bool,
/// The number of children room this has, if a space.
pub children_count: u64,
/// Whether this room is joined, left etc.
pub state: Option<Membership>,
/// A list of room members considered to be heroes.
pub heroes: Option<Vec<RoomHero>>,
}
impl From<UISpaceRoom> for SpaceRoom {
fn from(room: UISpaceRoom) -> Self {
Self {
room_id: room.room_id.into(),
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
name: room.name,
topic: room.topic,
avatar_url: room.avatar_url.map(|url| url.into()),
room_type: room.room_type.into(),
num_joined_members: room.num_joined_members,
join_rule: room.join_rule.map(Into::into),
world_readable: room.world_readable,
guest_can_join: room.guest_can_join,
children_count: room.children_count,
state: room.state.map(Into::into),
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
}
}
}
#[derive(uniffi::Enum)]
pub enum SpaceListUpdate {
Append { values: Vec<SpaceRoom> },
Clear,
PushFront { value: SpaceRoom },
PushBack { value: SpaceRoom },
PopFront,
PopBack,
Insert { index: u32, value: SpaceRoom },
Set { index: u32, value: SpaceRoom },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<SpaceRoom> },
}
impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
fn from(diff: VectorDiff<UISpaceRoom>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: value.into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
}
}
}
}
@@ -90,6 +90,15 @@ impl SyncService {
}
})))
}
/// Force expiring both sliding sync sessions.
///
/// This ensures that the sync service is stopped before expiring both
/// sessions. It should be used sparingly, as it will cause a restart of
/// the sessions on the server as well.
pub async fn expire_sessions(&self) {
self.inner.expire_sessions().await;
}
}
#[derive(Clone, uniffi::Object)]
@@ -119,6 +128,12 @@ impl SyncServiceBuilder {
Arc::new(Self { builder, ..this })
}
pub fn with_share_pos(self: Arc<Self>, enable: bool) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_share_pos(enable);
Arc::new(Self { builder, ..this })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService {
@@ -79,7 +79,6 @@ pub enum TimelineFocus {
Thread {
/// The thread root event ID to focus on.
root_event_id: String,
num_events: u16,
},
PinnedEvents {
max_events_to_load: u16,
@@ -108,7 +107,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
hide_threaded_events,
})
}
TimelineFocus::Thread { root_event_id, num_events } => {
TimelineFocus::Thread { root_event_id } => {
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
FocusEventError::InvalidEventId {
event_id: root_event_id.clone(),
@@ -116,7 +115,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
}
})?;
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
Ok(Self::Thread { root_event_id: parsed_root_event_id })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
+194 -239
View File
@@ -15,28 +15,24 @@
use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use eyeball_im::VectorDiff;
use futures_util::pin_mut;
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseVideoInfo, Thumbnail,
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
event_cache::RoomPaginationStatus,
room::{
edit::EditedContent as SdkEditedContent,
reply::{EnforceThread, Reply},
},
room::edit::EditedContent as SdkEditedContent,
};
use matrix_sdk_common::{
executor::{AbortHandle, JoinHandle},
stream::StreamExt,
};
use matrix_sdk_ui::timeline::{
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
TimelineUniqueId as SdkTimelineUniqueId,
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
use reply::{EmbeddedEventDetails, InReplyToDetails};
@@ -51,10 +47,8 @@ use ruma::{
UnstablePollStartContentBlock,
},
},
receipt::ReceiptThread,
room::message::{
LocationMessageEventContent, MessageType, ReplyWithinThread,
RoomMessageEventContentWithoutRelation,
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
},
AnyMessageLikeEventContent,
},
@@ -67,10 +61,8 @@ use uuid::Uuid;
use self::content::TimelineItemContent;
pub use self::msg_like::MessageContent;
use crate::{
client::ProgressWatcher,
error::{ClientError, RoomError},
event::EventOrTransactionId,
helpers::unwrap_or_clone_arc,
ruma::{
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
ThumbnailInfo, VideoInfo,
@@ -106,45 +98,40 @@ impl Timeline {
params: UploadParameters,
attachment_info: AttachmentInfo,
mime_type: Option<String>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
thumbnail: Option<Thumbnail>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let in_reply_to_event_id = params
.in_reply_to
.map(EventId::parse)
.transpose()
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
let formatted_caption = formatted_body_from(
params.caption.as_deref(),
params.formatted_caption.map(Into::into),
);
let attachment_config = AttachmentConfig::new()
.thumbnail(thumbnail)
.info(attachment_info)
.caption(params.caption)
.formatted_caption(formatted_caption)
.mentions(params.mentions.map(Into::into))
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
let attachment_config = AttachmentConfig {
info: Some(attachment_info),
thumbnail,
caption: params.caption,
formatted_caption,
mentions: params.mentions.map(Into::into),
in_reply_to: in_reply_to_event_id,
..Default::default()
};
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
let mut request =
self.inner.send_attachment(params.source, mime_type, attachment_config);
if params.use_send_queue {
request = request.use_send_queue();
}
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
get_runtime_handle().spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
});
}
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
Ok(())
self.inner
.send_attachment(params.source, mime_type, attachment_config)
.use_send_queue()
.await
.map_err(|_| RoomError::FailedSendingAttachment)
}));
Ok(handle)
@@ -152,15 +139,19 @@ impl Timeline {
}
fn build_thumbnail_info(
thumbnail_path: Option<String>,
thumbnail_source: Option<UploadSource>,
thumbnail_info: Option<ThumbnailInfo>,
) -> Result<Option<Thumbnail>, RoomError> {
match (thumbnail_path, thumbnail_info) {
match (thumbnail_source, thumbnail_info) {
(None, None) => Ok(None),
(Some(thumbnail_path), Some(thumbnail_info)) => {
let thumbnail_data =
fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?;
(Some(thumbnail_source), Some(thumbnail_info)) => {
let thumbnail_data = match thumbnail_source {
UploadSource::File { filename } => {
fs::read(filename).map_err(|_| RoomError::InvalidThumbnailData)?
}
UploadSource::Data { bytes, .. } => bytes,
};
let height = thumbnail_info
.height
@@ -190,7 +181,7 @@ fn build_thumbnail_info(
}
_ => {
warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined");
warn!("Ignoring thumbnail because either the thumbnail source or info isn't defined");
Ok(None)
}
}
@@ -206,16 +197,12 @@ pub struct UploadParameters {
formatted_caption: Option<FormattedBody>,
/// Optional intentional mentions to be sent with the media.
mentions: Option<Mentions>,
/// Optional parameters for sending the media as (threaded) reply.
reply_params: Option<ReplyParameters>,
/// Should the media be sent with the send queue, or synchronously?
///
/// Watching progress only works with the synchronous method, at the moment.
use_send_queue: bool,
/// Optional Event ID to reply to.
in_reply_to: Option<String>,
}
/// A source for uploading a file
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum UploadSource {
/// Upload source is a file on disk
File {
@@ -240,34 +227,47 @@ impl From<UploadSource> for AttachmentSource {
}
}
#[derive(uniffi::Record)]
pub struct ReplyParameters {
/// The ID of the event to reply to.
event_id: String,
/// Whether to enforce a thread relation.
enforce_thread: bool,
/// If enforcing a threaded relation, whether the message is a reply on a
/// thread.
reply_within_thread: bool,
/// This type represents the progress of a media (consisting of a file and
/// possibly a thumbnail) being uploaded.
#[derive(Clone, Copy, uniffi::Record)]
pub struct MediaUploadProgress {
/// The index of the media within the transaction. A file and its
/// thumbnail share the same index. Will always be 0 for non-gallery
/// media uploads.
pub index: u64,
/// The current combined upload progress for both the file and,
/// if it exists, its thumbnail.
pub progress: AbstractProgress,
}
impl TryInto<Reply> for ReplyParameters {
type Error = RoomError;
impl From<SdkMediaUploadProgress> for MediaUploadProgress {
fn from(value: SdkMediaUploadProgress) -> Self {
Self { index: value.index, progress: value.progress.into() }
}
}
fn try_into(self) -> Result<Reply, Self::Error> {
let event_id =
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
let enforce_thread = if self.enforce_thread {
EnforceThread::Threaded(if self.reply_within_thread {
ReplyWithinThread::Yes
} else {
ReplyWithinThread::No
})
} else {
EnforceThread::MaybeThreaded
};
/// Progress of an operation in abstract units.
///
/// Contrary to [`TransmissionProgress`], this allows tracking the progress
/// of sending or receiving a payload in estimated pseudo units representing a
/// percentage. This is helpful in cases where the exact progress in bytes isn't
/// known, for instance, because encryption (which changes the size) happens on
/// the fly.
#[derive(Clone, Copy, uniffi::Record)]
pub struct AbstractProgress {
/// How many units were already transferred.
pub current: u64,
/// How many units there are in total.
pub total: u64,
}
Ok(Reply { event_id, enforce_thread })
impl From<matrix_sdk::send_queue::AbstractProgress> for AbstractProgress {
fn from(value: matrix_sdk::send_queue::AbstractProgress) -> Self {
Self {
current: value.current.try_into().unwrap_or(u64::MAX),
total: value.total.try_into().unwrap_or(u64::MAX),
}
}
}
@@ -282,17 +282,14 @@ impl Timeline {
// handled by the caller. See #3535 for details.
// First, pass all the items as a reset update.
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
values: timeline_items,
}))]);
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(timeline_stream);
// Then forward new items.
while let Some(diffs) = timeline_stream.next().await {
listener
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
listener.on_update(diffs.into_iter().map(TimelineDiff::new).collect());
}
})))
}
@@ -351,9 +348,7 @@ impl Timeline {
event_id: String,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
self.inner
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
.await?;
self.inner.send_single_receipt(receipt_type.into(), event_id).await?;
Ok(())
}
@@ -389,53 +384,38 @@ impl Timeline {
pub fn send_image(
self: Arc<Self>,
params: UploadParameters,
thumbnail_path: Option<String>,
thumbnail_source: Option<UploadSource>,
image_info: ImageInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::Image(
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
);
let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?;
self.send_attachment(
params,
attachment_info,
image_info.mimetype,
progress_watcher,
thumbnail,
)
let thumbnail = build_thumbnail_info(thumbnail_source, image_info.thumbnail_info)?;
self.send_attachment(params, attachment_info, image_info.mimetype, thumbnail)
}
pub fn send_video(
self: Arc<Self>,
params: UploadParameters,
thumbnail_path: Option<String>,
thumbnail_source: Option<UploadSource>,
video_info: VideoInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::Video(
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
);
let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?;
self.send_attachment(
params,
attachment_info,
video_info.mimetype,
progress_watcher,
thumbnail,
)
let thumbnail = build_thumbnail_info(thumbnail_source, video_info.thumbnail_info)?;
self.send_attachment(params, attachment_info, video_info.mimetype, thumbnail)
}
pub fn send_audio(
self: Arc<Self>,
params: UploadParameters,
audio_info: AudioInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::Audio(
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
);
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
}
pub fn send_voice_message(
@@ -443,26 +423,24 @@ impl Timeline {
params: UploadParameters,
audio_info: AudioInfo,
waveform: Vec<u16>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::Voice {
audio_info: BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
waveform: Some(waveform),
};
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
}
pub fn send_file(
self: Arc<Self>,
params: UploadParameters,
file_info: FileInfo,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::File(
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
);
self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None)
self.send_attachment(params, attachment_info, file_info.mimetype, None)
}
pub async fn create_poll(
@@ -532,9 +510,10 @@ impl Timeline {
pub async fn send_reply(
&self,
msg: Arc<RoomMessageEventContentWithoutRelation>,
reply_params: ReplyParameters,
event_id: String,
) -> Result<(), ClientError> {
self.inner.send_reply((*msg).clone(), reply_params.try_into()?).await?;
let event_id = EventId::parse(&event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
self.inner.send_reply((*msg).clone(), event_id).await?;
Ok(())
}
@@ -588,7 +567,7 @@ impl Timeline {
description: Option<String>,
zoom_level: Option<u8>,
asset_type: Option<AssetType>,
reply_params: Option<ReplyParameters>,
replied_to_event_id: Option<String>,
) -> Result<(), ClientError> {
let mut location_event_message_content =
LocationMessageEventContent::new(body, geo_uri.clone());
@@ -607,8 +586,8 @@ impl Timeline {
MessageType::Location(location_event_message_content),
);
if let Some(reply_params) = reply_params {
self.send_reply(Arc::new(room_message_event_content), reply_params).await
if let Some(replied_to_event_id) = replied_to_event_id {
self.send_reply(Arc::new(room_message_event_content), replied_to_event_id).await
} else {
self.send(Arc::new(room_message_event_content)).await?;
Ok(())
@@ -704,6 +683,8 @@ impl Timeline {
content: replied_to.content.clone().into(),
sender: replied_to.sender.to_string(),
sender_profile: replied_to.sender_profile.into(),
timestamp: replied_to.timestamp.into(),
event_or_transaction_id: replied_to.identifier.into(),
},
))),
@@ -819,7 +800,7 @@ pub enum FocusEventError {
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
fn on_update(&self, diff: Vec<TimelineDiff>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
@@ -827,7 +808,7 @@ pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: RoomPaginationStatus);
}
#[derive(Clone, uniffi::Object)]
#[derive(Clone, uniffi::Enum)]
pub enum TimelineDiff {
Append { values: Vec<Arc<TimelineItem>> },
Clear,
@@ -835,10 +816,10 @@ pub enum TimelineDiff {
PushBack { value: Arc<TimelineItem> },
PopFront,
PopBack,
Insert { index: usize, value: Arc<TimelineItem> },
Set { index: usize, value: Arc<TimelineItem> },
Remove { index: usize },
Truncate { length: usize },
Insert { index: u32, value: Arc<TimelineItem> },
Set { index: u32, value: Arc<TimelineItem> },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<Arc<TimelineItem>> },
}
@@ -849,14 +830,18 @@ impl TimelineDiff {
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::Insert { index, value } => {
Self::Insert { index, value: TimelineItem::from_arc(value) }
VectorDiff::Insert { index, value } => Self::Insert {
index: u32::try_from(index).unwrap(),
value: TimelineItem::from_arc(value),
},
VectorDiff::Set { index, value } => Self::Set {
index: u32::try_from(index).unwrap(),
value: TimelineItem::from_arc(value),
},
VectorDiff::Truncate { length } => {
Self::Truncate { length: u32::try_from(length).unwrap() }
}
VectorDiff::Set { index, value } => {
Self::Set { index, value: TimelineItem::from_arc(value) }
}
VectorDiff::Truncate { length } => Self::Truncate { length },
VectorDiff::Remove { index } => Self::Remove { index },
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
VectorDiff::PushBack { value } => {
Self::PushBack { value: TimelineItem::from_arc(value) }
}
@@ -872,94 +857,6 @@ impl TimelineDiff {
}
}
#[matrix_sdk_ffi_macros::export]
impl TimelineDiff {
pub fn change(&self) -> TimelineChange {
match self {
Self::Append { .. } => TimelineChange::Append,
Self::Insert { .. } => TimelineChange::Insert,
Self::Set { .. } => TimelineChange::Set,
Self::Remove { .. } => TimelineChange::Remove,
Self::PushBack { .. } => TimelineChange::PushBack,
Self::PushFront { .. } => TimelineChange::PushFront,
Self::PopBack => TimelineChange::PopBack,
Self::PopFront => TimelineChange::PopFront,
Self::Clear => TimelineChange::Clear,
Self::Truncate { .. } => TimelineChange::Truncate,
Self::Reset { .. } => TimelineChange::Reset,
}
}
pub fn append(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Append { values } => values)
}
pub fn insert(self: Arc<Self>) -> Option<InsertData> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Insert { index, value } => {
InsertData { index: index.try_into().unwrap(), item: value }
})
}
pub fn set(self: Arc<Self>) -> Option<SetData> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Set { index, value } => {
SetData { index: index.try_into().unwrap(), item: value }
})
}
pub fn remove(&self) -> Option<u32> {
as_variant!(self, Self::Remove { index } => (*index).try_into().unwrap())
}
pub fn push_back(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::PushBack { value } => value)
}
pub fn push_front(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::PushFront { value } => value)
}
pub fn reset(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
let this = unwrap_or_clone_arc(self);
as_variant!(this, Self::Reset { values } => values)
}
pub fn truncate(&self) -> Option<u32> {
as_variant!(self, Self::Truncate { length } => (*length).try_into().unwrap())
}
}
#[derive(uniffi::Record)]
pub struct InsertData {
pub index: u32,
pub item: Arc<TimelineItem>,
}
#[derive(uniffi::Record)]
pub struct SetData {
pub index: u32,
pub item: Arc<TimelineItem>,
}
#[derive(Clone, Copy, uniffi::Enum)]
pub enum TimelineChange {
Append,
Clear,
Insert,
Set,
Remove,
PushBack,
PushFront,
PopBack,
PopFront,
Truncate,
Reset,
}
#[derive(Clone, uniffi::Record)]
pub struct TimelineUniqueId {
id: String,
@@ -1019,7 +916,11 @@ impl TimelineItem {
#[derive(Clone, uniffi::Enum)]
pub enum EventSendState {
/// The local event has not been sent yet.
NotSentYet,
NotSentYet {
/// The progress of the sending operation, if the event involves a media
/// upload.
progress: Option<MediaUploadProgress>,
},
/// The local event has been sent to the server, but unsuccessfully: The
/// sending has failed.
@@ -1044,7 +945,9 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
use matrix_sdk_ui::timeline::EventSendState::*;
match value {
NotSentYet => Self::NotSentYet,
NotSentYet { progress } => {
Self::NotSentYet { progress: progress.clone().map(|p| p.into()) }
}
SendingFailed { error, is_recoverable } => {
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
Self::SendingFailed {
@@ -1382,6 +1285,44 @@ impl LazyTimelineItemProvider {
}
}
/// Mimic the [`UiLatestEventValue`] type.
#[derive(Clone, uniffi::Enum)]
pub enum LatestEventValue {
None,
Remote {
timestamp: Timestamp,
sender: String,
is_own: bool,
profile: ProfileDetails,
content: TimelineItemContent,
},
Local {
timestamp: Timestamp,
content: TimelineItemContent,
is_sending: bool,
},
}
impl From<UiLatestEventValue> for LatestEventValue {
fn from(value: UiLatestEventValue) -> Self {
match value {
UiLatestEventValue::None => Self::None,
UiLatestEventValue::Remote { timestamp, sender, is_own, profile, content } => {
Self::Remote {
timestamp: timestamp.into(),
sender: sender.to_string(),
is_own,
profile: profile.into(),
content: content.into(),
}
}
UiLatestEventValue::Local { timestamp, content, is_sending } => {
Self::Local { timestamp: timestamp.into(), content: content.into(), is_sending }
}
}
}
}
#[cfg(feature = "unstable-msc4274")]
mod galleries {
use std::{panic, sync::Arc};
@@ -1395,6 +1336,7 @@ mod galleries {
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use ruma::EventId;
use tokio::sync::Mutex;
use tracing::error;
@@ -1402,7 +1344,7 @@ mod galleries {
error::RoomError,
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
runtime::get_runtime_handle,
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
timeline::{build_thumbnail_info, Timeline, UploadSource},
};
#[derive(uniffi::Record)]
@@ -1413,37 +1355,37 @@ mod galleries {
formatted_caption: Option<FormattedBody>,
/// Optional intentional mentions to be sent with the gallery.
mentions: Option<Mentions>,
/// Optional parameters for sending the media as (threaded) reply.
reply_params: Option<ReplyParameters>,
/// Optional Event ID to reply to.
in_reply_to: Option<String>,
}
#[derive(uniffi::Enum)]
pub enum GalleryItemInfo {
Audio {
audio_info: AudioInfo,
filename: String,
source: UploadSource,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
},
File {
file_info: FileInfo,
filename: String,
source: UploadSource,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
},
Image {
image_info: ImageInfo,
filename: String,
source: UploadSource,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
thumbnail_path: Option<String>,
thumbnail_source: Option<UploadSource>,
},
Video {
video_info: VideoInfo,
filename: String,
source: UploadSource,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
thumbnail_path: Option<String>,
thumbnail_source: Option<UploadSource>,
},
}
@@ -1457,12 +1399,12 @@ mod galleries {
}
}
fn filename(&self) -> &String {
fn source(&self) -> &UploadSource {
match self {
GalleryItemInfo::Audio { filename, .. } => filename,
GalleryItemInfo::File { filename, .. } => filename,
GalleryItemInfo::Image { filename, .. } => filename,
GalleryItemInfo::Video { filename, .. } => filename,
GalleryItemInfo::File { source, .. } => source,
GalleryItemInfo::Audio { source, .. } => source,
GalleryItemInfo::Image { source, .. } => source,
GalleryItemInfo::Video { source, .. } => source,
}
}
@@ -1508,11 +1450,17 @@ mod galleries {
fn thumbnail(&self) -> Result<Option<Thumbnail>, RoomError> {
match self {
GalleryItemInfo::Audio { .. } | GalleryItemInfo::File { .. } => Ok(None),
GalleryItemInfo::Image { image_info, thumbnail_path, .. } => {
build_thumbnail_info(thumbnail_path.clone(), image_info.thumbnail_info.clone())
GalleryItemInfo::Image { image_info, thumbnail_source, .. } => {
build_thumbnail_info(
thumbnail_source.as_ref().cloned(),
image_info.thumbnail_info.clone(),
)
}
GalleryItemInfo::Video { video_info, thumbnail_path, .. } => {
build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone())
GalleryItemInfo::Video { video_info, thumbnail_source, .. } => {
build_thumbnail_info(
thumbnail_source.as_ref().cloned(),
video_info.thumbnail_info.clone(),
)
}
}
}
@@ -1528,7 +1476,7 @@ mod galleries {
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
source: self.filename().into(),
source: self.source().clone().into(),
content_type: mime_type,
attachment_info: self.attachment_info()?,
caption: self.caption().clone(),
@@ -1599,11 +1547,18 @@ mod galleries {
params.formatted_caption.map(Into::into),
);
let in_reply_to = params
.in_reply_to
.as_ref()
.map(EventId::parse)
.transpose()
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
let mut gallery_config = GalleryConfig::new()
.caption(params.caption)
.formatted_caption(formatted_caption)
.mentions(params.mentions.map(Into::into))
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
.in_reply_to(in_reply_to);
for item_info in item_infos {
gallery_config = gallery_config.add_item(item_info.try_into()?);
@@ -15,7 +15,7 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
use super::{
content::Reaction,
+13 -2
View File
@@ -15,6 +15,7 @@
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
use crate::{event::EventOrTransactionId, utils::Timestamp};
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
@@ -50,8 +51,16 @@ impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
pub enum EmbeddedEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
Ready {
content: TimelineItemContent,
sender: String,
sender_profile: ProfileDetails,
timestamp: Timestamp,
event_or_transaction_id: EventOrTransactionId,
},
Error {
message: String,
},
}
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
@@ -63,6 +72,8 @@ impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
content: event.content.into(),
sender: event.sender.to_string(),
sender_profile: event.sender_profile.into(),
timestamp: event.timestamp.into(),
event_or_transaction_id: event.identifier.into(),
},
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
}
+26
View File
@@ -197,6 +197,15 @@ pub fn get_element_call_required_permissions(
.chain(read_send.clone())
.collect(),
send: vec![
// To notify other users that a call has started.
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
},
// Also for call notifications, except this is the deprecated fallback type which
// Element Call still sends.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::CallNotify.to_string(),
},
// To send the call participation state event (main MatrixRTC event).
// This is required for legacy state events (using only one event for all devices with
// a membership array). TODO: remove once legacy call member events are
@@ -211,6 +220,12 @@ pub fn get_element_call_required_permissions(
event_type: StateEventType::CallMember.to_string(),
state_key: format!("{own_user_id}_{own_device_id}"),
},
// Same as above for [MSC3779] and [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
// with application suffix
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("{own_user_id}_{own_device_id}_m.call"),
},
// The same as above but with an underscore.
// To work around the issue that state events starting with `@` have to be Matrix id's
// but we use mxId+deviceId.
@@ -218,6 +233,11 @@ pub fn get_element_call_required_permissions(
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}"),
},
// Same as above for [MSC4143], with application suffix
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}_m.call"),
},
]
.into_iter()
.chain(read_send)
@@ -497,9 +517,15 @@ mod tests {
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
);
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI_m.call",
);
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
);
cap_assert(
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI_m.call",
);
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
{
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.checkOnSave.command": "check",
"rust-analyzer.rustfmt.extraArgs": ["+nightly"]
}
+84 -2
View File
@@ -6,6 +6,88 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.14.0] - 2025-09-04
### Features
- Add `SyncResponse::RoomUpdates::is_empty` to check if there were any room updates.
([#5593](https://github.com/matrix-org/matrix-rust-sdk/pull/5593))
- Add `EncryptionState::StateEncrypted` to represent rooms supporting encrypted
state events. Feature-gated behind `experimental-encrypted-state-events`.
([#5523](https://github.com/matrix-org/matrix-rust-sdk/pull/5523))
- [**breaking**] The `state` field of `JoinedRoomUpdate` and `LeftRoomUpdate`
now uses the `State` enum, depending on whether the state changes were
received in the `state` field or the `state_after` field.
([#5488](https://github.com/matrix-org/matrix-rust-sdk/pull/5488))
- [**breaking**] `RoomCreateWithCreatorEventContent` has a new field
`additional_creators` that allows to specify additional room creators beside
the user sending the `m.room.create` event, introduced with room version 12.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- [**breaking**] The `RoomInfo` method now remembers the inviter at the time
when the `BaseClient::room_joined()` method was called. The caller is
responsible to remember the inviter before a server request to join the room
is made. The `RoomInfo::invite_accepted_at` method was removed, the
`RoomInfo::invite_details` method returns both the timestamp and the
inviter.
([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390))
### Refactor
- [**breaking**] The `Stripped` variants of `RawAnySyncOrStrippedTimelineEvent`,
`RawAnySyncOrStrippedState` and `AnySyncOrStrippedState` use `StrippedState`
instead of `AnyStrippedStateEvent`.
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
- [**breaking**] The `stripped_state` field of `StateChanges` uses
`StrippedState` instead of `AnyStrippedStateEvent`.
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
- [**breaking**] `RelationalLinkedChunk::items` now takes a `RoomId` instead of an
`&OwnedLinkedChunkId` parameter.
([#5445](https://github.com/matrix-org/matrix-rust-sdk/pull/5445))
- [**breaking**] Add an `IsPrefix = False` bound to the
`get_state_event_static()`, `get_state_event_static_for_key()` and
`get_state_events_static()`, `get_account_data_event_static()` and
`get_room_account_data_event_static` methods of `StateStoreExt`. These methods
only worked for events where the full event type is statically-known, and this
is now enforced at compile-time. The matching non-`static` methods of
`StateStore` can be used instead for event types with a variable suffix.
([#5444](https://github.com/matrix-org/matrix-rust-sdk/pull/5444))
- [**breaking**] `SyncOrStrippedState<RoomPowerLevelsEventContent>::power_levels()`
takes `AuthorizationRules` and a list of creators, because creators can have
infinite power levels, as introduced in room version 12.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- [**breaking**] `RoomMember::power_level()` and
`RoomMember::normalized_power_level()` now use `UserPowerLevel` to represent
power levels instead of `i64` to differentiate the infinite power level of
creators, as introduced in room version 12.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- [**breaking**] The `creator()` methods of `Room` and `RoomInfo` have been
renamed to `creators()` and can now return a list of user IDs, to reflect that
a room can have several creators, as introduced in room version 12.
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
- [**breaking**] `RoomInfo::room_version_or_default()` was replaced with
`room_version_rules_or_default()`. The room version should only be used for
display purposes. The rules contain flags for all the differences in behavior
between all known room versions.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- [**breaking**] `MinimalStateEvent::redact()` takes `RedactionRules` instead of
a `RoomVersionId`.
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
- [**breaking**] The `event_id` field of `PredecessorRoom` was removed, due to
its removal in the Matrix specification with MSC4291.
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
## [0.13.0] - 2025-07-10
### Features
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
`BaseClient::room_joined()` method was called. A new getter for this
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
timestamp.
([#5333](https://github.com/matrix-org/matrix-rust-sdk/pull/5333))
- [**breaking**] The `BaseClient::new()` method now takes an additional `ThreadingSupport`
parameter controlling whether the client is supposed to do extra processing for threads. Right
now, it controls whether to exclude in-thread events from the room unread counts, but it may be
expanded in the future to support more threading-related features.
([#5325](https://github.com/matrix-org/matrix-rust-sdk/pull/5325))
### Refactor
- The cached `ServerCapabilities` has been renamed to `ServerInfo` and
@@ -36,8 +118,8 @@ No notable changes in this release.
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
- There are new `'static` bounds in `MediaService` for the media cache stores
- `event_cache::store::MemoryStore` implements `Clone`.
- `BaseClient` now has a `handle_verification_events` field which is `true` by
default and can be negated so the `NotificationClient` won't handle received
- `BaseClient` now has a `handle_verification_events` field which is `true` by
default and can be negated so the `NotificationClient` won't handle received
verification events too, causing errors in the `VerificationMachine`.
- [**breaking**] `Room::is_encryption_state_synced` has been removed
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
+22 -6
View File
@@ -1,7 +1,7 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "The base component to build a Matrix client library."
edition = "2021"
edition = "2024"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version.workspace = true
version = "0.12.0"
version = "0.14.0"
[package.metadata.docs.rs]
all-features = true
@@ -25,8 +25,21 @@ js = [
"matrix-sdk-store-encryption/js",
]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
automatic-room-key-forwarding = [
"matrix-sdk-crypto?/automatic-room-key-forwarding",
]
experimental-send-custom-to-device = [
"matrix-sdk-crypto?/experimental-send-custom-to-device",
]
# Enable experimental support for encrypting state events; see
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
experimental-encrypted-state-events = [
"e2e-encryption",
"ruma/unstable-msc3414",
"matrix-sdk-crypto?/experimental-encrypted-state-events"
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
@@ -58,7 +71,7 @@ assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait.workspace = true
bitflags = { workspace = true, features = ["serde"] }
decancer = "3.3.0"
decancer = "3.3.3"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im.workspace = true
futures-util.workspace = true
@@ -69,7 +82,7 @@ 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.1"
regex = "1.11.2"
ruma = { workspace = true, features = [
"canonical-json",
"unstable-msc2867",
@@ -86,12 +99,14 @@ unicode-normalization.workspace = true
uniffi = { workspace = true, optional = true }
[dev-dependencies]
anyhow.workspace = true
assert_matches.workspace = true
assert_matches2.workspace = true
assign = "1.1.1"
futures-executor.workspace = true
http.workspace = true
matrix-sdk-test.workspace = true
matrix-sdk-test-utils.workspace = true
similar-asserts.workspace = true
stream_assert.workspace = true
@@ -100,6 +115,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
wasm-bindgen-test.workspace = true
gloo-timers = { workspace = true, features = ["futures"] }
[lints]
workspace = true
+236 -87
View File
@@ -24,35 +24,37 @@ use std::{
use eyeball::{SharedObservable, Subscriber};
use eyeball_im::{Vector, VectorDiff};
use futures_util::Stream;
use matrix_sdk_common::timer;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
EncryptionSettings, OlmError, OlmMachine, TrustRequirement,
CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
};
#[cfg(feature = "e2e-encryption")]
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
#[cfg(doc)]
use ruma::DeviceId;
#[cfg(feature = "e2e-encryption")]
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{self as api, sync::sync_events::v5},
events::{
StateEvent, StateEventType,
ignored_user_list::IgnoredUserListEventContent,
push_rules::{PushRulesEvent, PushRulesEventContent},
room::member::SyncRoomMemberEvent,
StateEvent, StateEventType,
},
push::Ruleset,
time::Instant,
OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tokio::sync::{broadcast, Mutex};
use tokio::sync::{Mutex, broadcast};
#[cfg(feature = "e2e-encryption")]
use tokio::sync::{RwLock, RwLockReadGuard};
use tracing::{debug, enabled, info, instrument, warn, Level};
use tracing::{Level, debug, enabled, info, instrument, warn};
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
deserialized_responses::DisplayName,
error::{Error, Result},
event_cache::store::EventCacheStoreLock,
@@ -61,12 +63,11 @@ use crate::{
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
},
store::{
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
StateStoreDataValue, StateStoreExt, StoreConfig,
BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult, RoomLoadSettings,
StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt, StoreConfig,
ambiguity_map::AmbiguityCache,
},
sync::{RoomUpdates, SyncResponse},
RoomStateFilter, SessionMeta,
};
/// A no (network) IO client implementation.
@@ -76,11 +77,12 @@ use crate::{
/// rather through `matrix_sdk::Client`.
///
/// ```rust
/// use matrix_sdk_base::{store::StoreConfig, BaseClient};
/// use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
///
/// let client = BaseClient::new(StoreConfig::new(
/// "cross-process-holder-name".to_owned(),
/// ));
/// let client = BaseClient::new(
/// StoreConfig::new("cross-process-holder-name".to_owned()),
/// ThreadingSupport::Disabled,
/// );
/// ```
#[derive(Clone)]
pub struct BaseClient {
@@ -122,6 +124,9 @@ pub struct BaseClient {
/// If the client should handle verification events received when syncing.
#[cfg(feature = "e2e-encryption")]
pub handle_verification_events: bool,
/// Whether the client supports threads or not.
pub threading_support: ThreadingSupport,
}
#[cfg(not(tarpaulin_include))]
@@ -134,6 +139,32 @@ impl fmt::Debug for BaseClient {
}
}
/// Whether this client instance supports threading or not. Currently used to
/// determine how the client handles read receipts and unread count computations
/// on the base SDK level.
///
/// Timelines on the other hand have a separate `TimelineFocus`
/// `hide_threaded_events` associated value that can be used to hide threaded
/// events but also to enable threaded read receipt sending. This is because
/// certain timeline instances should ignore threading no matter what's defined
/// at the client level. One such example are media filtered timelines which
/// should contain all the room's media no matter what thread its in (unless
/// explicitly opted into).
#[derive(Clone, Copy, Debug)]
pub enum ThreadingSupport {
/// Threading enabled.
Enabled {
/// Enable client-wide thread subscriptions support (MSC4306 / MSC4308).
///
/// This may cause filtering out of thread subscriptions, and loading
/// the thread subscriptions via the sliding sync extension,
/// when the room list service is being used.
with_subscriptions: bool,
},
/// Threading disabled.
Disabled,
}
impl BaseClient {
/// Create a new client.
///
@@ -141,7 +172,7 @@ impl BaseClient {
///
/// * `config` - the configuration for the stores (state store, event cache
/// store and crypto store).
pub fn new(config: StoreConfig) -> Self {
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
let store = BaseStateStore::new(config.state_store);
// Create the channel to receive `RoomInfoNotableUpdate`.
@@ -173,6 +204,7 @@ impl BaseClient {
},
#[cfg(feature = "e2e-encryption")]
handle_verification_events: true,
threading_support,
}
}
@@ -204,6 +236,7 @@ impl BaseClient {
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
decryption_settings: self.decryption_settings.clone(),
handle_verification_events,
threading_support: self.threading_support,
};
copy.state_store
@@ -224,7 +257,7 @@ impl BaseClient {
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
.state_store(MemoryStore::new());
Ok(Self::new(config))
Ok(Self::new(config, ThreadingSupport::Disabled))
}
/// Get the session meta information.
@@ -248,7 +281,9 @@ impl BaseClient {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
pub fn rooms_stream(
&self,
) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
self.state_store.rooms_stream()
}
@@ -394,26 +429,92 @@ impl BaseClient {
Ok(room)
}
/// User has joined a room.
/// The user has joined a room using this specific client.
///
/// This method should be called if the user accepts an invite or if they
/// join a public room.
///
/// The method will create a [`Room`] object if one does not exist yet and
/// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`]
/// object will be persisted in the cache. Please note that the [`Room`]
/// will be a stub until a sync has been received with the full room
/// state using [`BaseClient::receive_sync_response`].
///
/// Update the internal and cached state accordingly. Return the final Room.
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
///
/// # Arguments
///
/// * `room_id` - The unique ID identifying the joined room.
/// * `inviter` - When joining this room in response to an invitation, the
/// inviter should be recorded before sending the join request to the
/// server. Providing the inviter here ensures that the
/// [`InviteAcceptanceDetails`] are stored for this room.
///
/// # Examples
///
/// ```rust
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
/// # async {
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
/// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
/// # let room_id: &RoomId = todo!();
/// let maybe_inviter = maybe_get_inviter(room_id).await?;
/// let room_id = send_join_request().await?;
/// let room = client.room_joined(&room_id, maybe_inviter).await?;
///
/// assert_eq!(room.state(), RoomState::Joined);
/// # matrix_sdk_test::TestResult::Ok(()) };
/// ```
pub async fn room_joined(
&self,
room_id: &RoomId,
inviter: Option<OwnedUserId>,
) -> Result<Room> {
let room = self.state_store.get_or_create_room(
room_id,
RoomState::Joined,
self.room_info_notable_update_sender.clone(),
);
// If the state isn't `RoomState::Joined` then this means that we knew about
// this room before. Let's modify the existing state now.
if room.state() != RoomState::Joined {
let _sync_lock = self.sync_lock().lock().await;
let mut room_info = room.clone_info();
let previous_state = room.state();
room_info.mark_as_joined();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
// If our previous state was an invite and we're now in the joined state, this
// means that the user has explicitly accepted an invite. Let's
// remember some details about the invite.
//
// This is somewhat of a workaround for our lack of cryptographic membership.
// Later on we will decide if historic room keys should be accepted
// based on this info. If a user has accepted an invite and we receive a room
// key bundle shortly after, we might accept it. If we don't do
// this, the homeserver could trick us into accepting any historic room key
// bundle.
if previous_state == RoomState::Invited
&& let Some(inviter) = inviter
{
let details = InviteAcceptanceDetails {
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
inviter,
};
room_info.set_invite_acceptance_details(details);
}
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.state_store.save_changes(&changes).await?; // Update the store
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
@@ -500,7 +601,12 @@ impl BaseClient {
let processors::e2ee::to_device::Output {
processed_to_device_events: to_device,
room_key_updates,
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
} = processors::e2ee::to_device::from_sync_v2(
&response,
olm_machine.as_ref(),
&self.decryption_settings,
)
.await?;
processors::latest_event::decrypt_from_rooms(
&mut context,
@@ -526,14 +632,25 @@ impl BaseClient {
.events
.into_iter()
.map(|raw| {
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
ToDeviceUnableToDecryptReason,
};
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
ProcessedToDeviceEvent::UnableToDecrypt {
encrypted_event: raw,
utd_info: ToDeviceUnableToDecryptInfo {
reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
},
}
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
ProcessedToDeviceEvent::PlainText(raw)
}
} else {
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
// Exclude events with no type
ProcessedToDeviceEvent::Invalid(raw)
}
})
.collect();
@@ -768,14 +885,11 @@ impl BaseClient {
_ => (),
}
if let StateEvent::Original(e) = &member {
if let Some(d) = &e.content.displayname {
let display_name = DisplayName::new(d);
ambiguity_map
.entry(display_name)
.or_default()
.insert(member.state_key().clone());
}
if let StateEvent::Original(e) = &member
&& let Some(d) = &e.content.displayname
{
let display_name = DisplayName::new(d);
ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
}
let sync_member: SyncRoomMemberEvent = member.clone().into();
@@ -947,9 +1061,10 @@ impl BaseClient {
&self,
global_account_data_processor: &processors::account_data::Global,
) -> Result<Ruleset> {
let _timer = timer!(Level::TRACE, "get_push_rules");
if let Some(event) = global_account_data_processor
.push_rules()
.and_then(|ev| ev.deserialize_as::<PushRulesEvent>().ok())
.and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
{
Ok(event.content.global)
} else if let Some(event) = self
@@ -1062,16 +1177,16 @@ impl From<&v5::Request> for RequestedRequiredStates {
mod tests {
use std::collections::HashMap;
use assert_matches2::assert_let;
use assert_matches2::{assert_let, assert_matches};
use futures_util::FutureExt as _;
use matrix_sdk_test::{
async_test, event_factory::EventFactory, ruma_response_from_json, InvitedRoomBuilder,
LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, BOB,
BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
};
use ruma::{
api::client::{self as api, sync::sync_events::v5},
event_id,
events::{room::member::MembershipState, StateEventType},
events::{StateEventType, room::member::MembershipState},
room_id,
serde::Raw,
user_id,
@@ -1080,9 +1195,10 @@ mod tests {
use super::{BaseClient, RequestedRequiredStates};
use crate::{
RoomDisplayName, RoomState, SessionMeta,
client::ThreadingSupport,
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
test_utils::logged_in_base_client,
RoomDisplayName, RoomState, SessionMeta,
};
#[test]
@@ -1374,8 +1490,10 @@ mod tests {
let user_id = user_id!("@alice:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1434,8 +1552,10 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1496,8 +1616,10 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -1568,8 +1690,10 @@ mod tests {
#[async_test]
async fn test_ignored_user_list_changes() {
let user_id = user_id!("@alice:example.org");
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
@@ -1584,18 +1708,10 @@ mod tests {
let mut subscriber = client.subscribe_to_ignore_user_list_changes();
assert!(subscriber.next().now_or_never().is_none());
let f = EventFactory::new();
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
json!({
"content": {
"ignored_users": {
*BOB: {}
}
},
"type": "m.ignored_user_list",
}),
))
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
@@ -1604,16 +1720,7 @@ mod tests {
// Receive the same response.
let response = sync_builder
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
json!({
"content": {
"ignored_users": {
*BOB: {}
}
},
"type": "m.ignored_user_list",
}),
))
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
@@ -1621,16 +1728,8 @@ mod tests {
assert!(subscriber.next().now_or_never().is_none());
// Now remove Bob from the ignored list.
let response = sync_builder
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
json!({
"content": {
"ignored_users": {}
},
"type": "m.ignored_user_list",
}),
))
.build_sync_response();
let response =
sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert_let!(Some(ignored) = subscriber.next().await);
@@ -1643,20 +1742,70 @@ mod tests {
let client = logged_in_base_client(None).await;
let mut sync_builder = SyncResponseBuilder::new();
let f = EventFactory::new();
let response = sync_builder
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
json!({
"content": {
"ignored_users": {
ignored_user_id: {}
}
},
"type": "m.ignored_user_list",
}),
))
.add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(client.is_user_ignored(ignored_user_id).await);
}
#[async_test]
async fn test_invite_details_are_set() {
let user_id = user_id!("@alice:localhost");
let client = logged_in_base_client(Some(user_id)).await;
let invited_room_id = room_id!("!invited:localhost");
let unknown_room_id = room_id!("!unknown:localhost");
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
// Let us first check the initial state, we should have a room in the invite
// state.
let invited_room = client
.get_room(invited_room_id)
.expect("The sync should have created a room in the invited state");
assert_eq!(invited_room.state(), RoomState::Invited);
assert!(invited_room.invite_acceptance_details().is_none());
// Now we join the room.
let joined_room = client
.room_joined(invited_room_id, Some(user_id.to_owned()))
.await
.expect("We should be able to mark a room as joined");
// Yup, we now have some invite details.
assert_eq!(joined_room.state(), RoomState::Joined);
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
assert_eq!(details.inviter, user_id);
// If we didn't know about the room before the join, we assume that there wasn't
// an invite and we don't record the timestamp.
assert!(client.get_room(unknown_room_id).is_none());
let unknown_room = client
.room_joined(unknown_room_id, Some(user_id.to_owned()))
.await
.expect("We should be able to mark a room as joined");
assert_eq!(unknown_room.state(), RoomState::Joined);
assert!(unknown_room.invite_acceptance_details().is_none());
sync_builder.clear();
let response =
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
client.receive_sync_response(response).await.unwrap();
// Now that we left the room, we shouldn't have any details anymore.
let left_room = client
.get_room(invited_room_id)
.expect("The sync should have created a room in the invited state");
assert_eq!(left_room.state(), RoomState::Left);
assert!(left_room.invite_acceptance_details().is_none());
}
}
@@ -20,17 +20,18 @@ pub use matrix_sdk_common::deserialized_responses::*;
use once_cell::sync::Lazy;
use regex::Regex;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
events::{
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
room::{
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
},
room_version_rules::AuthorizationRules,
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
@@ -304,8 +305,8 @@ impl RawAnySyncOrStrippedState {
C::Redacted: RedactedStateEventContent,
{
match self {
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast()),
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast()),
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast_unchecked()),
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast_unchecked()),
}
}
}
@@ -517,10 +518,14 @@ impl MemberEvent {
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
/// The power levels of the event.
pub fn power_levels(&self) -> RoomPowerLevels {
pub fn power_levels(
&self,
rules: &AuthorizationRules,
creators: Vec<OwnedUserId>,
) -> RoomPowerLevels {
match self {
Self::Sync(e) => e.power_levels(),
Self::Stripped(e) => e.power_levels(),
Self::Sync(e) => e.power_levels(rules, creators),
Self::Stripped(e) => e.power_levels(rules, creators),
}
}
}
@@ -17,31 +17,34 @@
use std::{collections::BTreeMap, sync::Arc};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use matrix_sdk_common::{
deserialized_responses::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
VerificationState,
},
linked_chunk::{
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update, lazy_loader,
},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory};
use ruma::{
EventId, RoomId,
api::client::media::get_content_thumbnail::v3::Method,
event_id,
events::{
AnyMessageLikeEvent, AnyTimelineEvent,
relation::RelationType,
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
room::{MediaSource, message::RoomMessageEventContentWithoutRelation},
},
mxc_uri,
push::Action,
room_id, uint, EventId, RoomId,
room_id, uint,
};
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
use super::{DynEventCacheStore, media::IgnoreMediaRetentionPolicy};
use crate::{
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
event_cache::{Gap, store::DEFAULT_CHUNK_CAPACITY},
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
};
@@ -74,7 +77,7 @@ pub fn make_test_event_with_event_id(
if let Some(event_id) = event_id {
builder = builder.event_id(event_id);
}
let event = builder.into_raw_timeline().cast();
let event = builder.into_raw();
TimelineEvent::from_decrypted(
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
@@ -103,7 +106,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
// Check event.
let deserialized = d.event.deserialize().unwrap();
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
assert_matches!(deserialized, AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => {
assert_eq!(msg.as_original().unwrap().content.body(), text);
});
});
@@ -153,6 +156,10 @@ pub trait EventCacheStoreIntegrationTests {
/// Test that saving an event works as expected.
async fn test_save_event(&self);
/// Test multiple things related to distinguishing a thread linked chunk
/// from a room linked chunk.
async fn test_thread_vs_room_linked_chunk(&self);
}
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
@@ -760,31 +767,39 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();
// Sanity check: both linked chunks can be reloaded.
assert!(lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id0).await.unwrap()
)
.unwrap()
.is_some());
assert!(lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id1).await.unwrap()
)
.unwrap()
.is_some());
assert!(
lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id0).await.unwrap()
)
.unwrap()
.is_some()
);
assert!(
lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id1).await.unwrap()
)
.unwrap()
.is_some()
);
// Clear the chunks.
self.clear_all_linked_chunks().await.unwrap();
// Both rooms now have no linked chunk.
assert!(lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id0).await.unwrap()
)
.unwrap()
.is_none());
assert!(lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id1).await.unwrap()
)
.unwrap()
.is_none());
assert!(
lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id0).await.unwrap()
)
.unwrap()
.is_none()
);
assert!(
lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id1).await.unwrap()
)
.unwrap()
.is_none()
);
}
async fn test_remove_room(&self) {
@@ -970,19 +985,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(event.event_id(), event_comte.event_id());
// Now let's try to find an event that exists, but not in the expected room.
assert!(self
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
assert!(
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none()
);
// Clearing the rooms also clears the event's storage.
self.clear_all_linked_chunks().await.expect("failed to clear all rooms chunks");
assert!(self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
assert!(
self.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none()
);
}
async fn test_find_event_relations(&self) {
@@ -1029,12 +1046,16 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
assert_eq!(relations.len(), 2);
// The position is `None` for items outside the linked chunk.
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none()));
assert!(
relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none())
);
assert!(
relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none())
);
// Finding relations with a filter only returns a subset.
let relations = self
@@ -1088,9 +1109,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
}));
// But it's still not set for the other related events.
assert!(relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
assert!(
relations
.iter()
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none())
);
}
async fn test_save_event(&self) {
@@ -1123,16 +1146,151 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(event.event_id(), event_gruyere.event_id());
// But they won't be returned when searching in the wrong room.
assert!(self
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
assert!(
self.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none()
);
assert!(
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none()
);
}
async fn test_thread_vs_room_linked_chunk(&self) {
let room_id = room_id!("!r0:matrix.org");
let event = |msg: &str| make_test_event(room_id, msg);
let thread1_ev = event("comté");
let thread2_ev = event("gruyère");
let thread2_ev2 = event("beaufort");
let room_ev = event("brillat savarin triple crème");
let thread_root1 = event("thread1");
let thread_root2 = event("thread2");
// Add one event in a thread linked chunk.
self.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![thread1_ev.clone()],
},
],
)
.await
.unwrap();
// Add one event in another thread linked chunk (same room).
self.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_root2.event_id().unwrap().as_ref()),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![thread2_ev.clone(), thread2_ev2.clone()],
},
],
)
.await
.unwrap();
// Add another event to the room linked chunk.
self.handle_linked_chunk_updates(
LinkedChunkId::Room(room_id),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![room_ev.clone()],
},
],
)
.await
.unwrap();
// All the events can be found with `find_event()` for the room.
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
assert!(self
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.expect("failed to find thread1_ev");
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
.expect("failed to find thread2_ev");
self.find_event(room_id, thread2_ev2.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find thread2_ev2");
self.find_event(room_id, room_ev.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find room_ev");
// Finding duplicates operates based on the linked chunk id.
let dups = self
.filter_duplicated_events(
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
vec![
thread1_ev.event_id().unwrap().to_owned(),
room_ev.event_id().unwrap().to_owned(),
],
)
.await
.unwrap();
assert_eq!(dups.len(), 1);
assert_eq!(dups[0].0, thread1_ev.event_id().unwrap());
// Loading all chunks operates based on the linked chunk id.
let all_chunks = self
.load_all_chunks(LinkedChunkId::Thread(
room_id,
thread_root2.event_id().unwrap().as_ref(),
))
.await
.unwrap();
assert_eq!(all_chunks.len(), 1);
assert_eq!(all_chunks[0].identifier, CId::new(0));
assert_let!(ChunkContent::Items(observed_items) = all_chunks[0].content.clone());
assert_eq!(observed_items.len(), 2);
assert_eq!(observed_items[0].event_id(), thread2_ev.event_id());
assert_eq!(observed_items[1].event_id(), thread2_ev2.event_id());
// Loading the metadata of all chunks operates based on the linked chunk
// id.
let metas = self
.load_all_chunks_metadata(LinkedChunkId::Thread(
room_id,
thread_root2.event_id().unwrap().as_ref(),
))
.await
.unwrap();
assert_eq!(metas.len(), 1);
assert_eq!(metas[0].identifier, CId::new(0));
assert_eq!(metas[0].num_items, 2);
// Loading the last chunk operates based on the linked chunk id.
let (last_chunk, _chunk_identifier_generator) = self
.load_last_chunk(LinkedChunkId::Thread(
room_id,
thread_root1.event_id().unwrap().as_ref(),
))
.await
.unwrap();
let last_chunk = last_chunk.unwrap();
assert_eq!(last_chunk.identifier, CId::new(0));
assert_let!(ChunkContent::Items(observed_items) = last_chunk.content);
assert_eq!(observed_items.len(), 1);
assert_eq!(observed_items[0].event_id(), thread1_ev.event_id());
}
}
@@ -1155,8 +1313,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
/// mod tests {
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
///
/// async fn get_event_cache_store(
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
/// async fn get_event_cache_store()
/// -> EventCacheStoreResult<impl EventCacheStore> {
/// Ok(MyStore::new())
/// }
///
@@ -1258,6 +1416,13 @@ macro_rules! event_cache_store_integration_tests {
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_save_event().await;
}
#[async_test]
async fn test_thread_vs_room_linked_chunk() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_thread_vs_room_linked_chunk().await;
}
}
};
}
@@ -1268,11 +1433,14 @@ macro_rules! event_cache_store_integration_tests {
#[macro_export]
macro_rules! event_cache_store_integration_tests_time {
() => {
#[cfg(not(target_family = "wasm"))]
mod event_cache_store_integration_tests_time {
use std::time::Duration;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use gloo_timers::future::sleep;
use matrix_sdk_test::async_test;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use tokio::time::sleep;
use $crate::event_cache::store::IntoEventCacheStore;
use super::get_event_cache_store;
@@ -1301,26 +1469,26 @@ macro_rules! event_cache_store_integration_tests_time {
assert!(!acquired5);
// That's a nice test we got here, go take a little nap.
tokio::time::sleep(Duration::from_millis(50)).await;
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;
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;
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;
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();
@@ -22,7 +22,7 @@ use ruma::{
};
use super::{
media_service::IgnoreMediaRetentionPolicy, EventCacheStoreMedia, MediaRetentionPolicy,
EventCacheStoreMedia, MediaRetentionPolicy, media_service::IgnoreMediaRetentionPolicy,
};
use crate::media::{MediaFormat, MediaRequestParameters};
@@ -16,11 +16,11 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
executor::{spawn, JoinHandle},
locks::Mutex,
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
executor::{JoinHandle, spawn},
locks::Mutex,
};
use ruma::{time::SystemTime, MxcUri};
use ruma::{MxcUri, time::SystemTime};
use tokio::sync::Mutex as AsyncMutex;
use tracing::error;
@@ -538,15 +538,15 @@ mod tests {
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_test::async_test;
use ruma::{
MxcUri, OwnedMxcUri,
events::room::MediaSource,
mxc_uri,
time::{Duration, SystemTime},
MxcUri, OwnedMxcUri,
};
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
use crate::{
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
event_cache::store::{EventCacheStoreError, media::MediaRetentionPolicy},
media::{MediaFormat, MediaRequestParameters, UniqueKey},
};
@@ -21,23 +21,22 @@ use std::{
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, RawChunk, Update,
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
RawChunk, Update, relational::RelationalLinkedChunk,
},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
events::relation::RelationType,
time::{Instant, SystemTime},
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
};
use tracing::error;
use super::{
compute_filters_string, extract_event_relation,
EventCacheStore, EventCacheStoreError, Result, compute_filters_string, extract_event_relation,
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
EventCacheStore, EventCacheStoreError, Result,
};
use crate::{
event_cache::{Event, Gap},
@@ -223,11 +222,9 @@ impl EventCacheStore for MemoryStore {
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let event = inner
.events
.items(&target_linked_chunk_id)
.items(room_id)
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
Ok(event)
@@ -241,13 +238,11 @@ impl EventCacheStore for MemoryStore {
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
let inner = self.inner.read().unwrap();
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
let filters = compute_filters_string(filters);
let related_events = inner
.events
.items(&target_linked_chunk_id)
.items(room_id)
.filter_map(|(event, pos)| {
// Must have a relation.
let (related_to, rel_type) = extract_event_relation(event.raw())?;
@@ -410,7 +405,7 @@ impl EventCacheStoreMedia for MemoryStore {
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
// Do not store it.
return Ok(());
};
}
// Now, let's add it.
let mut inner = self.inner.write().unwrap();
@@ -33,9 +33,9 @@ use matrix_sdk_common::store_locks::{
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use ruma::{
events::{relation::RelationType, AnySyncTimelineEvent},
serde::Raw,
OwnedEventId,
events::{AnySyncTimelineEvent, relation::RelationType},
serde::Raw,
};
use tracing::trace;
@@ -43,7 +43,7 @@ use tracing::trace;
pub use self::integration_tests::EventCacheStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
traits::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
};
/// The high-level public type to represent an `EventCacheStore` lock.
@@ -236,11 +236,7 @@ pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<St
filter
.iter()
.map(|f| {
if *f == RelationType::Replacement {
"m.replace".to_owned()
} else {
f.to_string()
}
if *f == RelationType::Replacement { "m.replace".to_owned() } else { f.to_string() }
})
.collect()
})
@@ -16,17 +16,17 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
AsyncTraitDeps,
linked_chunk::{
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
RawChunk, Update,
},
AsyncTraitDeps,
};
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
use ruma::{EventId, MxcUri, OwnedEventId, RoomId, events::relation::RelationType};
use super::{
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
EventCacheStoreError,
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
};
use crate::{
event_cache::{Event, Gap},
@@ -128,6 +128,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
/// Find an event by its ID in a room.
///
/// This method must return events saved either in any linked chunks, *or*
/// events saved "out-of-band" with the [`Self::save_event`] method.
async fn find_event(
&self,
room_id: &RoomId,
@@ -147,6 +150,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
///
/// An additional filter can be provided to only retrieve related events for
/// a certain relationship.
///
/// This method must return events saved either in any linked chunks, *or*
/// events saved "out-of-band" with the [`Self::save_event`] method.
async fn find_event_relations(
&self,
room_id: &RoomId,
@@ -238,7 +244,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
///
/// * `uri` - The `MxcUri` of the media file.
async fn get_media_content_for_uri(&self, uri: &MxcUri)
-> Result<Option<Vec<u8>>, Self::Error>;
-> Result<Option<Vec<u8>>, Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
@@ -464,6 +470,12 @@ pub trait IntoEventCacheStore {
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
}
impl IntoEventCacheStore for Arc<DynEventCacheStore> {
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
self
}
}
impl<T> IntoEventCacheStore for T
where
T: EventCacheStore + Sized + 'static,
+59 -28
View File
@@ -2,9 +2,12 @@
//! use as a [crate::Room::latest_event].
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
#[cfg(feature = "e2e-encryption")]
use ruma::{
UserId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
@@ -14,14 +17,43 @@ use ruma::{
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
},
UserId,
};
use ruma::{MxcUri, OwnedEventId};
use serde::{Deserialize, Serialize};
use crate::MinimalRoomMemberEvent;
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
/// A latest event value!
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub enum LatestEventValue {
/// No value has been computed yet, or no candidate value was found.
#[default]
None,
/// The latest event represents a remote event.
Remote(RemoteLatestEventValue),
/// The latest event represents a local event that is sending.
LocalIsSending(LocalLatestEventValue),
/// The latest event represents a local event that cannot be sent, either
/// because a previous local event, or this local event cannot be sent.
LocalCannotBeSent(LocalLatestEventValue),
}
/// Represents the value for [`LatestEventValue::Remote`].
pub type RemoteLatestEventValue = TimelineEvent;
/// Represents the value for [`LatestEventValue::LocalIsSending`] and
/// [`LatestEventValue::LocalCannotBeSent`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalLatestEventValue {
/// The time where the event has been created (by this module).
pub timestamp: MilliSecondsSinceUnixEpoch,
/// The content of the local event.
pub content: SerializableEventContent,
}
/// Represents a decision about whether an event could be stored as the latest
/// event in a room. Variants starting with Yes indicate that this message could
@@ -125,21 +157,21 @@ pub fn is_suitable_for_latest_event<'a>(
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);
if let AnySyncStateEvent::RoomMember(member) = state
&& 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
@@ -311,13 +343,17 @@ mod tests {
use ruma::serde::Raw;
#[cfg(feature = "e2e-encryption")]
use ruma::{
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
call::{
SessionDescription,
invite::{CallInviteEventContent, SyncCallInviteEvent},
notify::{
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
},
SessionDescription,
},
poll::{
unstable_response::{
@@ -330,6 +366,7 @@ mod tests {
},
relation::Replacement,
room::{
ImageInfo, MediaSource,
encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
SyncRoomEncryptedEvent,
@@ -339,22 +376,16 @@ mod tests {
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
},
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
ImageInfo, MediaSource,
},
sticker::{StickerEventContent, SyncStickerEvent},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
UnsignedRoomRedactionEvent,
},
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
VoipVersionId,
owned_event_id, owned_mxc_uri, owned_user_id,
};
use serde_json::json;
use super::LatestEvent;
#[cfg(feature = "e2e-encryption")]
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
#[cfg(feature = "e2e-encryption")]
#[test]
@@ -501,7 +532,7 @@ mod tests {
#[test]
fn test_redacted_messages_are_suitable() {
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
let room_redaction_event = serde_json::from_value(json!({
"content": {},
"event_id": "$redaction",
"sender": "@x:y.za",
+7 -6
View File
@@ -48,27 +48,28 @@ mod utils;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
pub use client::BaseClient;
pub use client::{BaseClient, ThreadingSupport};
#[cfg(any(test, feature = "testing"))]
pub use http;
#[cfg(feature = "e2e-encryption")]
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use room::{
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
RoomStateFilter, SuccessorRoom, apply_redaction,
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
};
#[cfg(test)]
matrix_sdk_test::init_tracing_for_tests!();
matrix_sdk_test_utils::init_tracing_for_tests!();
/// The Matrix user session info.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+2 -2
View File
@@ -1,18 +1,18 @@
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
use ruma::{
MxcUri, UInt,
api::client::media::get_content_thumbnail::v3::Method,
events::{
room::{
MediaSource,
message::{
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
LocationMessageEventContent, VideoMessageEventContent,
},
MediaSource,
},
sticker::StickerEventContent,
},
MxcUri, UInt,
};
use serde::{Deserialize, Serialize};
+161 -53
View File
@@ -122,21 +122,26 @@ use std::{
num::NonZeroUsize,
};
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_common::{
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
serde_helpers::extract_thread_root,
};
use ruma::{
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
events::{
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
SyncMessageLikeEvent,
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
room::message::Relation,
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
SyncMessageLikeEvent,
},
serde::Raw,
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, trace, warn};
use crate::ThreadingSupport;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct LatestReadReceipt {
/// The id of the event the read receipt is referring to. (Not the read
@@ -201,7 +206,18 @@ impl RoomReadReceipts {
///
/// Returns whether a new event triggered a new unread/notification/mention.
#[inline(always)]
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
fn process_event(
&mut self,
event: &TimelineEvent,
user_id: &UserId,
threading_support: ThreadingSupport,
) {
if matches!(threading_support, ThreadingSupport::Enabled { .. })
&& extract_thread_root(event.raw()).is_some()
{
return;
}
if marks_as_unread(event.raw(), user_id) {
self.num_unread += 1;
}
@@ -240,6 +256,7 @@ impl RoomReadReceipts {
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a TimelineEvent>,
threading_support: ThreadingSupport,
) -> bool {
let mut counting_receipts = false;
@@ -247,19 +264,19 @@ impl RoomReadReceipts {
// Sliding sync sometimes sends the same event multiple times, so it can be at
// the beginning and end of a batch, for instance. In that case, just reset
// every time we see the event matching the receipt.
if let Some(event_id) = event.event_id() {
if event_id == receipt_event_id {
// Bingo! Switch over to the counting state, after resetting the
// previous counts.
trace!("Found the event the receipt was referring to! Starting to count.");
self.reset();
counting_receipts = true;
continue;
}
if let Some(event_id) = event.event_id()
&& event_id == receipt_event_id
{
// Bingo! Switch over to the counting state, after resetting the
// previous counts.
trace!("Found the event the receipt was referring to! Starting to count.");
self.reset();
counting_receipts = true;
continue;
}
if counting_receipts {
self.process_event(event, user_id);
self.process_event(event, user_id, threading_support);
}
}
@@ -370,17 +387,17 @@ impl ReceiptSelector {
// Now consider new receipts.
for (event_id, receipts) in &receipt_event.0 {
for ty in [ReceiptType::Read, ReceiptType::ReadPrivate] {
if let Some(receipt) = receipts.get(&ty).and_then(|receipts| receipts.get(user_id))
if let Some(receipts) = receipts.get(&ty)
&& let Some(receipt) = receipts.get(user_id)
&& matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded)
{
if matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded) {
trace!(%event_id, "found new candidate");
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
self.try_select_later(event_id, *event_pos);
} else {
// It's a new pending receipt.
trace!(%event_id, "stashed as pending");
pending.push(event_id.clone());
}
trace!(%event_id, "found new candidate");
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
self.try_select_later(event_id, *event_pos);
} else {
// It's a new pending receipt.
trace!(%event_id, "stashed as pending");
pending.push(event_id.clone());
}
}
}
@@ -443,6 +460,7 @@ pub(crate) fn compute_unread_counts(
mut previous_events: Vec<TimelineEvent>,
new_events: &[TimelineEvent],
read_receipts: &mut RoomReadReceipts,
threading_support: ThreadingSupport,
) {
debug!(?read_receipts, "Starting");
@@ -489,7 +507,12 @@ pub(crate) fn compute_unread_counts(
// The event for the receipt is in `all_events`, so we'll find it and can count
// safely from here.
read_receipts.find_and_process_events(&event_id, user_id, all_events.iter());
read_receipts.find_and_process_events(
&event_id,
user_id,
all_events.iter(),
threading_support,
);
debug!(?read_receipts, "after finding a better receipt");
return;
@@ -503,7 +526,7 @@ pub(crate) fn compute_unread_counts(
// for the next receipt.
for event in new_events {
read_receipts.process_event(event, user_id);
read_receipts.process_event(event, user_id, threading_support);
}
debug!(?read_receipts, "no better receipt, {} new events", new_events.len());
@@ -608,18 +631,21 @@ mod tests {
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::event_factory::EventFactory;
use ruma::{
event_id,
EventId, UserId, event_id,
events::{
receipt::{ReceiptThread, ReceiptType},
room::{member::MembershipState, message::MessageType},
},
owned_event_id, owned_user_id,
push::Action,
room_id, user_id, EventId, UserId,
room_id, user_id,
};
use super::compute_unread_counts;
use crate::read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts};
use crate::{
ThreadingSupport,
read_receipts::{ReceiptSelector, RoomReadReceipts, marks_as_unread},
};
#[test]
fn test_room_message_marks_as_unread() {
@@ -720,7 +746,7 @@ mod tests {
// An interesting event from oneself doesn't count as a new unread message.
let event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
@@ -728,7 +754,7 @@ mod tests {
// An interesting event from someone else does count as a new unread message.
let event = make_event(user_id!("@bob:example.org"), Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
@@ -736,7 +762,7 @@ mod tests {
// Push actions computed beforehand are respected.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
@@ -746,7 +772,7 @@ mod tests {
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true))],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 0);
@@ -756,7 +782,7 @@ mod tests {
vec![Action::SetTweak(ruma::push::Tweak::Highlight(true)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 1);
@@ -765,7 +791,7 @@ mod tests {
// make sure to resist against it.
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id);
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
@@ -779,7 +805,9 @@ mod tests {
// When provided with no events, we report not finding the event to which the
// receipt relates.
let mut receipts = RoomReadReceipts::default();
assert!(receipts.find_and_process_events(ev0, user_id, &[]).not());
assert!(
receipts.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled).not()
);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
@@ -800,9 +828,16 @@ mod tests {
num_mentions: 37,
..Default::default()
};
assert!(receipts
.find_and_process_events(ev0, user_id, &[make_event(event_id!("$1"))],)
.not());
assert!(
receipts
.find_and_process_events(
ev0,
user_id,
&[make_event(event_id!("$1"))],
ThreadingSupport::Disabled
)
.not()
);
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
@@ -816,7 +851,12 @@ mod tests {
num_mentions: 37,
..Default::default()
};
assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)]));
assert!(receipts.find_and_process_events(
ev0,
user_id,
&[make_event(ev0)],
ThreadingSupport::Disabled
),);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
@@ -829,17 +869,20 @@ mod tests {
num_mentions: 37,
..Default::default()
};
assert!(receipts
.find_and_process_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
)
.not());
assert!(
receipts
.find_and_process_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
)
.not()
);
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
@@ -861,6 +904,7 @@ mod tests {
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
@@ -883,6 +927,7 @@ mod tests {
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
ThreadingSupport::Disabled
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
@@ -908,7 +953,7 @@ mod tests {
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = Default::default();
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
@@ -916,6 +961,7 @@ mod tests {
previous_events.clone(),
&[ev1.clone(), ev2.clone()],
&mut read_receipts,
ThreadingSupport::Disabled,
);
// It did find the receipt event (ev1).
@@ -934,6 +980,7 @@ mod tests {
previous_events,
&[new_event],
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Only the new event should be added.
@@ -1000,6 +1047,7 @@ mod tests {
all_events.clone(),
&[],
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
@@ -1021,6 +1069,7 @@ mod tests {
head_events.clone(),
&tail_events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
@@ -1065,6 +1114,7 @@ mod tests {
events,
&[], // no new events
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Then there are no unread events,
@@ -1102,6 +1152,7 @@ mod tests {
events,
&[ev0], // duplicate event!
&mut read_receipts,
ThreadingSupport::Disabled,
);
// All events are unread, and there's no pending receipt.
@@ -1536,6 +1587,7 @@ mod tests {
Vec::new(),
&events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
// Only the last two events sent by Bob count as unread.
@@ -1547,4 +1599,60 @@ mod tests {
// And the active receipt is the implicit one on my event.
assert_eq!(read_receipts.latest_active.unwrap().event_id, event_id!("$6"));
}
#[test]
fn test_compute_unread_counts_with_threading_enabled() {
fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
EventFactory::new()
.text_msg("A")
.sender(user_id)
.event_id(event_id!("$ida"))
.in_thread(thread_root, event_id!("$latest_event"))
.into_event()
}
let mut receipts = RoomReadReceipts::default();
let own_alice = user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
// Threaded messages from myself or other users shouldn't change the
// unread counts.
receipts.process_event(
&make_event(own_alice, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(own_alice, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(bob, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(bob, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
// Processing an unthreaded message should still count as unread.
receipts.process_event(
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
}
}
@@ -17,17 +17,18 @@ use std::{
mem,
};
use matrix_sdk_common::timer;
use ruma::{
RoomId,
events::{
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
AnyGlobalAccountDataEvent, GlobalAccountDataEventType, direct::OwnedDirectUserIdentifier,
},
serde::Raw,
RoomId,
};
use tracing::{debug, instrument, trace, warn};
use super::super::Context;
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
use crate::{RoomInfo, StateChanges, store::BaseStateStore};
/// Create the [`Global`] account data processor.
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
@@ -43,6 +44,8 @@ pub struct Global {
impl Global {
/// Creates a new processor for global account data.
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
let _timer = timer!(tracing::Level::TRACE, "Global::process (global account data)");
let mut raw_by_type = BTreeMap::new();
let mut parsed_events = Vec::new();
@@ -102,10 +105,10 @@ impl Global {
// Update the direct targets of rooms if they changed.
for (room_id, new_direct_targets) in new_dms {
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
if old_direct_targets == new_direct_targets {
continue;
}
if let Some(old_direct_targets) = old_dms.remove(&room_id)
&& old_direct_targets == new_direct_targets
{
continue;
}
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
map_info(room_id, state_changes, state_store, |info| {
@@ -125,6 +128,8 @@ impl Global {
/// Applies the processed data to the state changes and the state store.
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
let _timer = timer!(tracing::Level::TRACE, "Global::apply (global account data)");
// Fill in the content of `changes.account_data`.
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
@@ -15,5 +15,5 @@
mod global;
mod room;
pub use global::{global, Global};
pub use global::{Global, global};
pub use room::for_room;
@@ -13,20 +13,20 @@
// limitations under the License.
use ruma::{
events::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
serde::Raw,
RoomId,
events::{AnyRoomAccountDataEvent, marked_unread::MarkedUnreadEventContent},
serde::Raw,
};
use tracing::{instrument, warn};
use super::super::{Context, RoomInfoNotableUpdates};
use crate::{
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
StateChanges,
RoomInfo, RoomInfoNotableUpdateReasons, StateChanges, room::AccountDataSource,
store::BaseStateStore,
};
#[instrument(skip_all, fields(?room_id))]
pub async fn for_room(
pub fn for_room(
context: &mut Context,
room_id: &RoomId,
events: &[Raw<AnyRoomAccountDataEvent>],
@@ -13,22 +13,25 @@
// limitations under the License.
use eyeball::SharedObservable;
use matrix_sdk_common::timer;
use ruma::{
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
events::{GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent},
serde::Raw,
};
use tracing::{error, instrument, trace};
use super::Context;
use crate::{
store::{BaseStateStore, StateStoreExt as _},
Result,
store::{BaseStateStore, StateStoreExt as _},
};
/// Save the [`StateChanges`] from the [`Context`] inside the [`BaseStateStore`]
/// only! The changes aren't applied on the in-memory rooms.
#[instrument(skip_all)]
pub async fn save_only(context: Context, state_store: &BaseStateStore) -> Result<()> {
let _timer = timer!(tracing::Level::TRACE, "_method");
save_changes(&context, state_store, None).await?;
broadcast_room_info_notable_updates(&context, state_store);
@@ -44,6 +47,8 @@ pub async fn save_and_apply(
ignore_user_list_changes: &SharedObservable<Vec<String>>,
sync_token: Option<String>,
) -> Result<()> {
let _timer = timer!(tracing::Level::TRACE, "_method");
trace!("ready to submit changes to store");
let previous_ignored_user_list =
@@ -80,7 +85,7 @@ fn apply_changes(
if let Some(event) =
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
{
match event.deserialize_as::<IgnoredUserListEvent>() {
match event.deserialize_as_unchecked::<IgnoredUserListEvent>() {
Ok(event) => {
let user_ids: Vec<String> =
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
@@ -14,7 +14,7 @@
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
use super::{super::verification, E2EE};
use crate::Result;
@@ -35,7 +35,7 @@ pub async fn sync_timeline_event(
Ok(Some(
match olm
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
@@ -14,13 +14,17 @@
use std::collections::BTreeMap;
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
};
use matrix_sdk_crypto::{
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
};
use ruma::{
api::client::sync::sync_events::{v3, v5, DeviceLists},
OneTimeKeyAlgorithm, UInt,
api::client::sync::sync_events::{DeviceLists, v3, v5},
events::AnyToDeviceEvent,
serde::Raw,
OneTimeKeyAlgorithm, UInt,
};
use crate::Result;
@@ -34,6 +38,7 @@ pub async fn from_msc4186(
to_device: Option<&v5::response::ToDevice>,
e2ee: &v5::response::E2EE,
olm_machine: Option<&OlmMachine>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
process(
olm_machine,
@@ -42,6 +47,7 @@ pub async fn from_msc4186(
&e2ee.device_one_time_keys_count,
e2ee.device_unused_fallback_key_types.as_deref(),
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
decryption_settings,
)
.await
}
@@ -54,6 +60,7 @@ pub async fn from_msc4186(
pub async fn from_sync_v2(
response: &v3::Response,
olm_machine: Option<&OlmMachine>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
process(
olm_machine,
@@ -62,6 +69,7 @@ pub async fn from_sync_v2(
&response.device_one_time_keys_count,
response.device_unused_fallback_key_types.as_deref(),
Some(response.next_batch.clone()),
decryption_settings,
)
.await
}
@@ -77,6 +85,7 @@ async fn process(
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
next_batch_token: Option<String>,
decryption_settings: &DecryptionSettings,
) -> Result<Output> {
let encryption_sync_changes = EncryptionSyncChanges {
to_device_events,
@@ -92,7 +101,7 @@ async fn process(
// This makes sure that we have the decryption keys for the room
// events at hand.
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
@@ -107,7 +116,12 @@ async fn process(
.map(|raw| {
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
if event_type == "m.room.encrypted" {
ProcessedToDeviceEvent::UnableToDecrypt(raw)
ProcessedToDeviceEvent::UnableToDecrypt {
encrypted_event: raw,
utd_info: ToDeviceUnableToDecryptInfo {
reason: ToDeviceUnableToDecryptReason::NoOlmMachine,
},
}
} else {
ProcessedToDeviceEvent::PlainText(raw)
}
@@ -14,10 +14,11 @@
use std::collections::BTreeSet;
use matrix_sdk_common::timer;
use matrix_sdk_crypto::OlmMachine;
use ruma::{OwnedUserId, RoomId};
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
use crate::{EncryptionState, Result, RoomMemberships, store::BaseStateStore};
/// Update tracked users, if the room is encrypted.
pub async fn update(
@@ -25,12 +26,11 @@ pub async fn update(
room_encryption_state: EncryptionState,
user_ids_to_track: &BTreeSet<OwnedUserId>,
) -> Result<()> {
if room_encryption_state.is_encrypted() {
if let Some(olm) = olm_machine {
if !user_ids_to_track.is_empty() {
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
}
}
if room_encryption_state.is_encrypted()
&& let Some(olm) = olm_machine
&& !user_ids_to_track.is_empty()
{
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
}
Ok(())
@@ -46,19 +46,21 @@ pub async fn update_or_set_if_room_is_newly_encrypted(
room_id: &RoomId,
state_store: &BaseStateStore,
) -> Result<()> {
if new_room_encryption_state.is_encrypted() {
if let Some(olm) = olm_machine {
if !previous_room_encryption_state.is_encrypted() {
// The room turned on encryption in this sync, we need
// to also get all the existing users and mark them for
// tracking.
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
}
let _timer = timer!(tracing::Level::TRACE, "update_or_set_if_room_is_newly_encrypted");
if !user_ids_to_track.is_empty() {
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
}
if new_room_encryption_state.is_encrypted()
&& let Some(olm) = olm_machine
{
if !previous_room_encryption_state.is_encrypted() {
// The room turned on encryption in this sync, we need
// to also get all the existing users and mark them for
// tracking.
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
}
if !user_ids_to_track.is_empty() {
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
use ruma::{RoomId, events::AnySyncEphemeralRoomEvent, serde::Raw};
use tracing::info;
use super::Context;
@@ -14,12 +14,12 @@
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
use super::{e2ee::E2EE, verification, Context};
use super::{Context, e2ee::E2EE, verification};
use crate::{
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
Result, Room,
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
};
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
@@ -111,7 +111,7 @@ async fn decrypt_sync_room_event(
let event = match e2ee
.olm_machine
.expect("An `OlmMachine` is expected")
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
@@ -137,11 +137,11 @@ async fn decrypt_sync_room_event(
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
};
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
use super::{decrypt_from_rooms, Context, E2EE};
use super::{Context, E2EE, decrypt_from_rooms};
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
#[async_test]
@@ -192,11 +192,13 @@ mod tests {
assert!(room.latest_encrypted_events().is_empty());
assert!(room.latest_event().is_none());
assert!(context.state_changes.room_infos.is_empty());
assert!(!context
.room_info_notable_updates
.get(room_id)
.copied()
.unwrap_or_default()
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
assert!(
!context
.room_info_notable_updates
.get(room_id)
.copied()
.unwrap_or_default()
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)
);
}
}
@@ -15,9 +15,9 @@
use std::collections::BTreeMap;
use ruma::{
OwnedRoomId,
push::{Action, PushConditionRoomCtx, Ruleset},
serde::Raw,
OwnedRoomId, RoomId,
};
use crate::{
@@ -43,25 +43,21 @@ impl<'a> Notification<'a> {
fn push_notification(
&mut self,
room_id: &RoomId,
room_id: OwnedRoomId,
actions: Vec<Action>,
event: RawAnySyncOrStrippedTimelineEvent,
) {
self.notifications
.entry(room_id.to_owned())
.or_default()
.push(sync::Notification { actions, event });
self.notifications.entry(room_id).or_default().push(sync::Notification { actions, event });
}
/// Push a new [`sync::Notification`] in [`Self::notifications`] from
/// `event` if and only if `predicate` returns `true` for at least one of
/// the [`Action`]s associated to this event and this
/// `push_condition_room_ctx`. (based on `Self::push_rules`).
/// `push_condition_room_ctx`. (based on [`Self::push_rules`]).
///
/// This method returns the fetched [`Action`]s.
pub fn push_notification_from_event_if<E, P>(
pub async fn push_notification_from_event_if<E, P>(
&mut self,
room_id: &RoomId,
push_condition_room_ctx: &PushConditionRoomCtx,
event: &Raw<E>,
predicate: P,
@@ -70,10 +66,14 @@ impl<'a> Notification<'a> {
Raw<E>: Into<RawAnySyncOrStrippedTimelineEvent>,
P: Fn(&Action) -> bool,
{
let actions = self.push_rules.get_actions(event, push_condition_room_ctx);
let actions = self.push_rules.get_actions(event, push_condition_room_ctx).await;
if actions.iter().any(predicate) {
self.push_notification(room_id, actions.to_owned(), event.clone().into());
self.push_notification(
push_condition_room_ctx.room_id.clone(),
actions.to_owned(),
event.clone().into(),
);
}
actions
@@ -13,11 +13,11 @@
// limitations under the License.
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
SyncStateEvent,
},
RoomId,
events::{
SyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use super::Context;
@@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::timer;
use super::super::Context;
use crate::{
room::UpdatedRoomDisplayName, store::BaseStateStore, sync::RoomUpdates,
RoomInfoNotableUpdateReasons,
RoomInfoNotableUpdateReasons, room::UpdatedRoomDisplayName, store::BaseStateStore,
sync::RoomUpdates,
};
pub async fn update_for_rooms(
@@ -23,6 +25,8 @@ pub async fn update_for_rooms(
room_updates: &RoomUpdates,
state_store: &BaseStateStore,
) {
let _timer = timer!(tracing::Level::TRACE, "display_name::update_for_rooms");
for room in room_updates.iter_all_room_ids().filter_map(|room_id| state_store.room(room_id)) {
// Compute the display name. If it's different, let's register the `RoomInfo` in
// the `StateChanges`.
@@ -15,7 +15,7 @@
use ruma::RoomId;
use tokio::sync::broadcast::Sender;
use crate::{store::ambiguity_map::AmbiguityCache, RequestedRequiredStates, RoomInfoNotableUpdate};
use crate::{RequestedRequiredStates, RoomInfoNotableUpdate, store::ambiguity_map::AmbiguityCache};
pub mod display_name;
pub mod msc4186;
@@ -15,19 +15,19 @@
use std::collections::BTreeMap;
use ruma::{
api::client::sync::sync_events::v5 as http,
events::{receipt::ReceiptEventContent, AnySyncEphemeralRoomEvent, SyncEphemeralRoomEvent},
serde::Raw,
OwnedRoomId, RoomId,
api::client::sync::sync_events::v5 as http,
events::{AnySyncEphemeralRoomEvent, SyncEphemeralRoomEvent, receipt::ReceiptEventContent},
serde::Raw,
};
use super::super::super::{
account_data::for_room as account_data_for_room, ephemeral_events::dispatch_receipt, Context,
Context, account_data::for_room as account_data_for_room, ephemeral_events::dispatch_receipt,
};
use crate::{
RoomState,
store::BaseStateStore,
sync::{JoinedRoomUpdate, RoomUpdates},
RoomState,
};
/// Dispatch the ephemeral events in the `extensions.typing` part of the
@@ -51,22 +51,20 @@ pub fn dispatch_receipt_ephemeral_event_for_room(
context: &mut Context,
room_id: &RoomId,
receipt: &Raw<SyncEphemeralRoomEvent<ReceiptEventContent>>,
joined_room_update: &mut JoinedRoomUpdate,
) {
let receipt: Raw<AnySyncEphemeralRoomEvent> = receipt.cast_ref().clone();
dispatch_receipt(context, &receipt, room_id);
joined_room_update.ephemeral.push(receipt);
}
pub async fn room_account_data(
pub fn room_account_data(
context: &mut Context,
account_data: &http::response::AccountData,
room_updates: &mut RoomUpdates,
state_store: &BaseStateStore,
) {
for (room_id, raw) in &account_data.rooms {
account_data_for_room(context, room_id, raw, state_store).await;
account_data_for_room(context, room_id, raw, state_store);
if let Some(room) = state_store.room(room_id) {
match room.state() {
@@ -20,36 +20,35 @@ use std::collections::BTreeSet;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::StateEventType;
use matrix_sdk_common::timer;
use ruma::{
JsOption, OwnedRoomId, RoomId, UserId,
api::client::sync::sync_events::{
v3::{InviteState, InvitedRoom, KnockState, KnockedRoom},
v5 as http,
},
assign,
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
serde::Raw,
JsOption, OwnedRoomId, RoomId, UserId,
};
use tokio::sync::broadcast::Sender;
#[cfg(feature = "e2e-encryption")]
use super::super::e2ee;
use super::{
super::{notification, state_events, timeline, Context},
super::{Context, notification, state_events, timeline},
RoomCreationData,
};
#[cfg(feature = "e2e-encryption")]
use crate::StateChanges;
use crate::{
store::BaseStateStore,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomState,
store::BaseStateStore,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
};
/// Represent any kind of room updates.
@@ -69,6 +68,8 @@ pub async fn update_any_room(
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
notification: notification::Notification<'_>,
) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
let _timer = timer!(tracing::Level::TRACE, "update_any_room");
let RoomCreationData {
room_id,
room_info_notable_update_sender,
@@ -81,8 +82,8 @@ pub async fn update_any_room(
// 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`.
let (raw_state_events, state_events) =
state_events::sync::collect(&room_response.required_state);
let state = State::from_msc4186(room_response.required_state.clone());
let (raw_state_events, state_events) = state.collect(&[]);
let state_store = notification.state_store;
@@ -119,6 +120,8 @@ pub async fn update_any_room(
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
)
.await?;
@@ -191,7 +194,7 @@ pub async fn update_any_room(
room_info,
RoomUpdateKind::Joined(JoinedRoomUpdate::new(
timeline,
raw_state_events,
state,
room_account_data.cloned().unwrap_or_default(),
ephemeral,
notification_count,
@@ -204,7 +207,7 @@ pub async fn update_any_room(
room_info,
RoomUpdateKind::Left(LeftRoomUpdate::new(
timeline,
raw_state_events,
state,
room_account_data.cloned().unwrap_or_default(),
ambiguity_changes,
)),
@@ -243,10 +246,10 @@ fn membership(
// We need to find the membership event since it could be for either an invited
// or knocked room.
let membership_event = state_events.1.iter().find_map(|event| {
if let AnyStrippedStateEvent::RoomMember(membership_event) = event {
if membership_event.state_key == user_id {
return Some(membership_event.content.clone());
}
if let AnyStrippedStateEvent::RoomMember(membership_event) = event
&& membership_event.state_key == user_id
{
return Some(membership_event.content.clone());
}
None
});
@@ -434,26 +437,18 @@ pub(crate) async fn cache_latest_events(
use crate::{
deserialized_responses::DisplayName,
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
store::ambiguity_map::is_display_name_ambiguous,
};
let _timer = timer!(tracing::Level::TRACE, "cache_latest_events");
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() {
// Try to get room power levels from the current changes. If we didn't get any
// info, try getting it from local data.
let power_levels = match changes.and_then(|changes| changes.power_levels(room_info.room_id())) {
Some(power_levels) => Some(power_levels),
None => room.power_levels().await.ok(),
};
@@ -509,17 +504,17 @@ pub(crate) async fn cache_latest_events(
}
// Otherwise, look up the sender's profile from the `Store`.
if sender_profile.is_none() {
if let Some(store) = store {
sender_profile = store
.get_profile(room.room_id(), timeline_event.sender())
.await
.ok()
.flatten();
if sender_profile.is_none()
&& let Some(store) = store
{
sender_profile = store
.get_profile(room.room_id(), timeline_event.sender())
.await
.ok()
.flatten();
// TODO: need to update `sender_name_is_ambiguous`,
// but how?
}
// TODO: need to update `sender_name_is_ambiguous`,
// but how?
}
let latest_event = Box::new(LatestEvent::new_with_sender_details(
@@ -563,3 +558,11 @@ pub(crate) async fn cache_latest_events(
// the latest is last
room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
}
impl State {
/// Construct a [`State`] from the state changes for a joined or left room
/// from a response of the Simplified Sliding Sync endpoint.
fn from_msc4186(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
Self::After(events)
}
}
@@ -15,20 +15,23 @@
use std::collections::{BTreeMap, BTreeSet};
use ruma::{
api::client::sync::sync_events::v3::{InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom},
OwnedRoomId, OwnedUserId, RoomId,
api::client::sync::sync_events::v3::{
InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, State as RumaState,
},
};
use tokio::sync::broadcast::Sender;
use tracing::error;
#[cfg(feature = "e2e-encryption")]
use super::super::e2ee;
use super::{
super::{account_data, ephemeral_events, notification, state_events, timeline, Context},
super::{Context, account_data, ephemeral_events, notification, state_events, timeline},
RoomCreationData,
};
use crate::{
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
Result, RoomInfoNotableUpdate, RoomState,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
};
/// Process updates of a joined room.
@@ -61,10 +64,11 @@ pub async fn update_joined_room(
room_info.mark_state_fully_synced();
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
let (raw_state_events, state_events) = state_events::sync::collect(&joined_room.state.events);
let mut new_user_ids = BTreeSet::new();
let state = State::from_sync_v2(joined_room.state);
let (raw_state_events, state_events) = state.collect(&joined_room.timeline.events);
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
@@ -72,6 +76,8 @@ pub async fn update_joined_room(
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
)
.await?;
@@ -81,19 +87,6 @@ pub async fn update_joined_room(
room_info.mark_members_missing();
}
let (raw_state_events_from_timeline, state_events_from_timeline) =
state_events::sync::collect_from_timeline(&joined_room.timeline.events);
state_events::sync::dispatch(
context,
(&raw_state_events_from_timeline, &state_events_from_timeline),
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
)
.await?;
#[cfg(feature = "e2e-encryption")]
let olm_machine = e2ee.olm_machine;
@@ -111,7 +104,7 @@ pub async fn update_joined_room(
// Save the new `RoomInfo`.
context.state_changes.add_room(room_info);
account_data::for_room(context, room_id, &joined_room.account_data.events, state_store).await;
account_data::for_room(context, room_id, &joined_room.account_data.events, state_store);
// `processors::account_data::from_room` might have updated the `RoomInfo`.
// Let's fetch it again.
@@ -145,7 +138,7 @@ pub async fn update_joined_room(
Ok(JoinedRoomUpdate::new(
timeline,
joined_room.state.events,
state,
joined_room.account_data.events,
joined_room.ephemeral.events,
notification_count,
@@ -179,7 +172,8 @@ pub async fn update_left_room(
room_info.mark_state_partially_synced();
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
let (raw_state_events, state_events) = state_events::sync::collect(&left_room.state.events);
let state = State::from_sync_v2(left_room.state);
let (raw_state_events, state_events) = state.collect(&left_room.timeline.events);
state_events::sync::dispatch(
context,
@@ -188,19 +182,8 @@ pub async fn update_left_room(
ambiguity_cache,
&mut (),
state_store,
)
.await?;
let (raw_state_events_from_timeline, state_events_from_timeline) =
state_events::sync::collect_from_timeline(&left_room.timeline.events);
state_events::sync::dispatch(
context,
(&raw_state_events_from_timeline, &state_events_from_timeline),
&mut room_info,
ambiguity_cache,
&mut (),
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
)
.await?;
@@ -218,16 +201,11 @@ pub async fn update_left_room(
// Save the new `RoomInfo`.
context.state_changes.add_room(room_info);
account_data::for_room(context, room_id, &left_room.account_data.events, state_store).await;
account_data::for_room(context, room_id, &left_room.account_data.events, state_store);
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
Ok(LeftRoomUpdate::new(
timeline,
left_room.state.events,
left_room.account_data.events,
ambiguity_changes,
))
Ok(LeftRoomUpdate::new(timeline, state, left_room.account_data.events, ambiguity_changes))
}
/// Process updates of an invited room.
@@ -301,3 +279,19 @@ pub async fn update_knocked_room(
Ok(knocked_room)
}
impl State {
/// Construct a [`State`] from the state changes for a joined or left room
/// from a response of the sync v2 endpoint.
fn from_sync_v2(state: RumaState) -> Self {
match state {
RumaState::Before(state) => Self::Before(state.events),
RumaState::After(state) => Self::After(state.events),
// We shouldn't receive other variants because they are opt-in.
state => {
error!("Unsupported State variant received for joined room: {state:?}");
Self::default()
}
}
}
}
@@ -15,12 +15,12 @@
use std::collections::BTreeSet;
use ruma::{
RoomId,
events::{
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
AnySyncStateEvent, SyncStateEvent,
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
},
serde::Raw,
RoomId,
};
use serde::Deserialize;
use tracing::warn;
@@ -33,41 +33,45 @@ pub mod sync {
use std::{collections::BTreeSet, iter};
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnySyncTimelineEvent, SyncStateEvent,
},
OwnedUserId, RoomId, UserId,
events::{
AnySyncTimelineEvent, SyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use tracing::{error, instrument};
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
#[cfg(feature = "experimental-encrypted-state-events")]
use crate::response_processors::e2ee;
use crate::{
store::{ambiguity_map::AmbiguityCache, BaseStateStore, Result as StoreResult},
RoomInfo,
store::{BaseStateStore, Result as StoreResult, ambiguity_map::AmbiguityCache},
sync::State,
};
/// Collect [`AnySyncStateEvent`] to [`AnySyncStateEvent`].
pub fn collect(
raw_events: &[Raw<AnySyncStateEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events)
}
/// Collect [`AnySyncTimelineEvent`] to [`AnySyncStateEvent`].
///
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
/// state events. The message-like events are filtered out.
pub fn collect_from_timeline(
raw_events: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events.iter().filter_map(|raw_event| {
// Only state events have a `state_key` field.
match raw_event.get_field::<&str>("state_key") {
Ok(Some(_)) => Some(raw_event.cast_ref()),
_ => None,
impl State {
/// Collect all the state changes to update the local state, from this
/// [`State`] and from the given timeline, if necessary.
///
/// The events that fail to deserialize are logged and filtered out.
pub(crate) fn collect(
&self,
timeline: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
match self {
Self::Before(events) => {
super::collect(events.iter().chain(timeline.iter().filter_map(|raw_event| {
// Only state events have a `state_key` field.
match raw_event.get_field::<&str>("state_key") {
Ok(Some(_)) => Some(raw_event.cast_ref_unchecked()),
_ => None,
}
})))
}
Self::After(events) => super::collect(events),
}
}))
}
}
/// Dispatch the sync state events.
@@ -87,6 +91,7 @@ pub mod sync {
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
state_store: &BaseStateStore,
#[cfg(feature = "experimental-encrypted-state-events")] e2ee: e2ee::E2EE<'_>,
) -> StoreResult<()>
where
U: NewUsers,
@@ -107,24 +112,16 @@ pub mod sync {
}
AnySyncStateEvent::RoomCreate(create) => {
if super::is_create_event_valid(
let edited_create = super::validate_create_event_predecessor(
context,
room_info.room_id(),
create,
state_store,
) {
room_info.handle_state_event(event);
} else {
error!(
room_id = ?room_info.room_id(),
?create,
"`m.create.tombstone` event is invalid, it creates a loop"
);
);
// Do not add the event to `room_info`.
// Do not add the event to `context.state_changes.state`.
continue;
}
room_info.handle_state_event(
edited_create.map(Into::into).as_ref().unwrap_or(event),
);
}
AnySyncStateEvent::RoomTombstone(tombstone) => {
@@ -148,6 +145,55 @@ pub mod sync {
}
}
#[cfg(feature = "experimental-encrypted-state-events")]
AnySyncStateEvent::RoomEncrypted(SyncStateEvent::Original(outer)) => {
use matrix_sdk_crypto::RoomEventDecryptionResult;
use tracing::{trace, warn};
trace!(event_id = ?outer.event_id, "Received encrypted state event, attempting decryption...");
let Some(olm_machine) = e2ee.olm_machine else {
continue;
};
let decrypted_event = olm_machine
.try_decrypt_room_event(
raw_event.cast_ref_unchecked(),
&room_info.room_id,
e2ee.decryption_settings,
)
.await
.expect("OlmMachine was not started");
// Skip state events that failed to decrypt.
let RoomEventDecryptionResult::Decrypted(decrypted_event) = decrypted_event
else {
warn!(event_id = ?outer.event_id, "Failed to decrypt state event");
continue;
};
// Cast to `AnySyncTimelineEvent`, safe since this is a supertype of
// `AnyTimelineEvent`.
let deserialized_event = match decrypted_event
.event
.deserialize_as::<AnySyncTimelineEvent>()
{
Ok(event) => event,
Err(err) => {
warn!(event_id = ?outer.event_id, "Failed to decrypt state event: {err}");
continue;
}
};
// Ensure decrypted event is actually a state event.
let AnySyncTimelineEvent::State(event) = deserialized_event else {
continue;
};
trace!(event_id = ?outer.event_id, "Decrypted state event successfully.");
room_info.handle_state_event(&event);
}
_ => {
room_info.handle_state_event(event);
}
@@ -166,7 +212,7 @@ pub mod sync {
Ok(())
}
/// Dispatch a [`RoomMemberEventContent>`] state event.
/// Dispatch a [`RoomMemberEventContent`] state event.
async fn dispatch_room_member<U>(
context: &mut Context,
room_id: &RoomId,
@@ -192,7 +238,7 @@ pub mod sync {
}
/// A trait to collect new users in [`dispatch`].
trait NewUsers {
pub(crate) trait NewUsers {
/// Insert a new user in the collection of new users.
fn insert(&mut self, user_id: &UserId);
}
@@ -221,7 +267,7 @@ pub mod stripped {
};
use crate::{Result, Room, RoomInfo};
/// Collect [`AnyStrippedStateEvent`] to [`AnyStrippedStateEvent`].
/// Collect [`Raw<AnyStrippedStateEvent>`] to [`AnyStrippedStateEvent`].
pub fn collect(
raw_events: &[Raw<AnyStrippedStateEvent>],
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
@@ -270,19 +316,17 @@ pub mod stripped {
// We need to check for notifications after we have handled all state
// events, to make sure we have the full push context.
if let Some(push_condition_room_ctx) =
timeline::get_push_room_context(context, room, room_info, notification.state_store)
.await?
timeline::get_push_room_context(context, room, room_info).await?
{
let room_id = room.room_id();
// Check every event again for notification.
for event in state_events.values().flat_map(|map| map.values()) {
notification.push_notification_from_event_if(
room_id,
&push_condition_room_ctx,
event,
Action::should_notify,
);
notification
.push_notification_from_event_if(
&push_condition_room_ctx,
event,
Action::should_notify,
)
.await;
}
}
@@ -307,31 +351,47 @@ where
.unzip()
}
/// Check if `m.room.create` isn't creating a loop of rooms.
pub fn is_create_event_valid(
/// Check if the `predecessor` in `m.room.create` isn't creating a loop of
/// rooms.
///
/// If it is, we return a clone of the event with the predecessor removed.
pub fn validate_create_event_predecessor(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomCreateEventContent>,
state_store: &BaseStateStore,
) -> bool {
) -> Option<SyncStateEvent<RoomCreateEventContent>> {
let mut already_seen = BTreeSet::new();
already_seen.insert(room_id.to_owned());
let Some(mut predecessor_room_id) = event
.as_original()
.and_then(|event| Some(event.content.predecessor.as_ref()?.room_id.clone()))
// Redacted and non-redacted create events use the same content type.
let content = match event {
SyncStateEvent::Original(event) => &event.content,
SyncStateEvent::Redacted(event) => &event.content,
};
let Some(mut predecessor_room_id) =
content.predecessor.as_ref().map(|predecessor| predecessor.room_id.clone())
else {
// `true` means no problem. No predecessor = no problem here.
return true;
// No predecessor = no problem here.
return None;
};
loop {
// We must check immediately if the `predecessor_room_id` is in `already_seen`
// in case of a room is created and marks itself as its predecessor in a single
// sync.
if already_seen.contains(AsRef::<RoomId>::as_ref(&predecessor_room_id)) {
if already_seen.contains(&predecessor_room_id) {
// Ahhh, there is a loop with `m.room.create` events!
return false;
// We remove the predecessor so that we don't process it later.
let mut event = event.clone();
match &mut event {
SyncStateEvent::Original(event) => event.content.predecessor.take(),
SyncStateEvent::Redacted(event) => event.content.predecessor.take(),
};
return Some(event);
}
already_seen.insert(predecessor_room_id.clone());
@@ -356,7 +416,7 @@ pub fn is_create_event_valid(
predecessor_room_id = next_predecessor_room_id;
}
true
None
}
/// Check if `m.room.tombstone` isn't creating a loop of rooms.
@@ -410,16 +470,17 @@ pub fn is_tombstone_event_valid(
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
DEFAULT_TEST_ROOM_ID, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, TestResult,
async_test, event_factory::EventFactory,
};
use ruma::{event_id, room_id, user_id, RoomVersionId};
use ruma::{RoomVersionId, event_id, room_id, user_id};
use crate::test_utils::logged_in_base_client;
#[async_test]
async fn test_not_possible_to_overwrite_m_room_create() {
async fn test_not_possible_to_overwrite_m_room_create() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -437,14 +498,14 @@ mod tests {
.add_joined_room(
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("42").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("42")?),
)
.add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("43").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("43")?),
),
)
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("44").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("44")?),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_2))
.build_sync_response();
@@ -472,13 +533,13 @@ mod tests {
{
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("45").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("45")?),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("46").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("46")?),
))
.add_joined_room(JoinedRoomBuilder::new(room_id_2).add_timeline_event(
event_factory.create(sender, RoomVersionId::try_from("47").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("47")?),
))
.build_sync_response();
@@ -502,6 +563,8 @@ mod tests {
"47"
);
}
Ok(())
}
#[async_test]
@@ -519,7 +582,7 @@ mod tests {
}
#[async_test]
async fn test_check_room_upgrades_no_error() {
async fn test_check_room_upgrades_no_error() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -534,7 +597,7 @@ mod tests {
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_timeline_event(
// Room 0 has no predecessor.
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("41")?),
))
.build_sync_response();
@@ -558,8 +621,8 @@ mod tests {
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_0),
),
)
.build_sync_response();
@@ -597,8 +660,8 @@ mod tests {
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, tombstone_event_id),
.create(sender, RoomVersionId::try_from("43")?)
.predecessor(room_id_1),
),
)
.build_sync_response();
@@ -627,10 +690,12 @@ mod tests {
);
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_no_loop_within_misordered_rooms() {
async fn test_check_room_upgrades_no_loop_within_misordered_rooms() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -651,7 +716,7 @@ mod tests {
JoinedRoomBuilder::new(room_id_0)
.add_timeline_event(
// No predecessor for room 0.
event_factory.create(sender, RoomVersionId::try_from("41").unwrap()),
event_factory.create(sender, RoomVersionId::try_from("41")?),
)
.add_timeline_event(
// Successor of room 0 is room 1.
@@ -666,8 +731,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, event_id!("$ev0")),
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_0),
)
.add_timeline_event(
// Successor of room 1 is room 2.
@@ -681,8 +746,8 @@ mod tests {
JoinedRoomBuilder::new(room_id_2).add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, event_id!("$ev1")),
.create(sender, RoomVersionId::try_from("43")?)
.predecessor(room_id_1),
),
)
.build_sync_response();
@@ -732,10 +797,12 @@ mod tests {
);
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor");
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_shortest_invalid_successor() {
async fn test_check_room_upgrades_shortest_invalid_successor() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -767,10 +834,12 @@ mod tests {
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_invalid_successor() {
async fn test_check_room_upgrades_invalid_successor() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -792,8 +861,8 @@ mod tests {
JoinedRoomBuilder::new(room_id_1).add_timeline_event(
// Predecessor of room 1 is room 0.
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_0),
),
)
.build_sync_response();
@@ -832,8 +901,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 2 is room 1.
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_1, tombstone_event_id),
.create(sender, RoomVersionId::try_from("43")?)
.predecessor(room_id_1),
)
.add_timeline_event(
// Successor of room 2 is room 0.
@@ -880,10 +949,12 @@ mod tests {
// this state event is missing because it creates a loop
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_shortest_invalid_predecessor() {
async fn test_check_room_upgrades_shortest_invalid_predecessor() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -900,8 +971,8 @@ mod tests {
// No successor.
JoinedRoomBuilder::new(room_id_0).add_timeline_event(
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id)
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_0)
.event_id(tombstone_event_id),
),
)
@@ -910,16 +981,19 @@ mod tests {
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event has not been saved.
// … the predecessor has not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
assert_matches!(room_0.create_content(), Some(_), "room 0 must have a create content");
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_shortest_loop() {
async fn test_check_room_upgrades_shortest_loop() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -942,8 +1016,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 0 is room 0
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_0, tombstone_event_id),
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_0),
),
)
.build_sync_response();
@@ -951,16 +1025,19 @@ mod tests {
// The sync doesn't fail but…
assert!(client.receive_sync_response(response).await.is_ok());
// … the state event has not been saved.
// … the tombstone event and the predecessor have not been saved.
let room_0 = client.get_room(room_id_0).unwrap();
assert!(room_0.predecessor_room().is_none(), "room 0 must not have a predecessor");
assert!(room_0.successor_room().is_none(), "room 0 must not have a successor");
assert_matches!(room_0.create_content(), Some(_), "room 0 must have a create content");
}
Ok(())
}
#[async_test]
async fn test_check_room_upgrades_loop() {
async fn test_check_room_upgrades_loop() -> TestResult {
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new().sender(sender);
let mut response_builder = SyncResponseBuilder::new();
@@ -982,8 +1059,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 0 is room 2
event_factory
.create(sender, RoomVersionId::try_from("42").unwrap())
.predecessor(room_id_2, event_id!("$ev2")),
.create(sender, RoomVersionId::try_from("42")?)
.predecessor(room_id_2),
)
.add_timeline_event(
// Successor of room 0 is room 1
@@ -997,8 +1074,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 1 is room 0
event_factory
.create(sender, RoomVersionId::try_from("43").unwrap())
.predecessor(room_id_0, event_id!("$ev0")),
.create(sender, RoomVersionId::try_from("43")?)
.predecessor(room_id_0),
)
.add_timeline_event(
// Successor of room 1 is room 2
@@ -1012,8 +1089,8 @@ mod tests {
.add_timeline_event(
// Predecessor of room 2 is room 1
event_factory
.create(sender, RoomVersionId::try_from("44").unwrap())
.predecessor(room_id_1, event_id!("$ev1")),
.create(sender, RoomVersionId::try_from("44")?)
.predecessor(room_id_1),
)
.add_timeline_event(
// Successor of room 2 is room 0
@@ -1060,11 +1137,14 @@ mod tests {
// this state event is missing because it creates a loop
assert!(room_2.predecessor_room().is_none(), "room 2 must not have a predecessor");
assert!(room_2.successor_room().is_none(), "room 2 must not have a successor",);
assert_matches!(room_2.create_content(), Some(_), "room 2 must have a create content");
}
Ok(())
}
#[async_test]
async fn test_state_events_after_sync() {
async fn test_state_events_after_sync() -> TestResult {
// Given a room
let user_id = user_id!("@u:u.to");
@@ -1081,10 +1161,11 @@ mod tests {
.add_joined_room(
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
.add_timeline_event(room_name)
.add_state_event(StateTestEvent::Create)
.add_state_event(StateTestEvent::PowerLevels),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
client.receive_sync_response(response).await?;
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Just-created room not found!");
@@ -1093,5 +1174,7 @@ mod tests {
// ensure that we have the topic
assert_eq!(room.topic().unwrap(), "this is the test topic in the timeline");
Ok(())
}
}
@@ -12,30 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_common::{deserialized_responses::TimelineEvent, timer};
#[cfg(feature = "e2e-encryption")]
use ruma::events::SyncMessageLikeEvent;
use ruma::{
events::{
room::power_levels::{
RoomPowerLevelsEvent, RoomPowerLevelsEventContent, StrippedRoomPowerLevelsEvent,
},
AnyStrippedStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
StateEventType,
},
UInt, UserId, assign,
events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
push::{Action, PushConditionRoomCtx},
UInt, UserId,
};
use tracing::{instrument, trace, warn};
use super::{Context, notification};
#[cfg(feature = "e2e-encryption")]
use super::{e2ee, verification};
use super::{notification, Context};
use crate::{
store::{BaseStateStore, StateStoreExt as _},
sync::Timeline,
Result, Room, RoomInfo,
};
use crate::{Result, Room, RoomInfo, sync::Timeline};
/// Process a set of sync timeline event, and create a [`Timeline`].
///
@@ -44,6 +34,7 @@ use crate::{
/// - will process verification,
/// - will process redaction,
/// - will process notification.
#[allow(clippy::extra_unused_lifetimes)]
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn build<'notification, 'e2ee>(
context: &mut Context,
@@ -53,9 +44,10 @@ pub async fn build<'notification, 'e2ee>(
mut notification: notification::Notification<'notification>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
) -> Result<Timeline> {
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
let mut push_condition_room_ctx =
get_push_room_context(context, room, room_info, notification.state_store).await?;
let mut push_condition_room_ctx = get_push_room_context(context, room, room_info).await?;
let room_id = room.room_id();
for raw_event in timeline_inputs.raw_events {
@@ -76,16 +68,18 @@ pub async fn build<'notification, 'e2ee>(
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
redaction_event,
)) => {
let room_version = room_info.room_version_or_default();
let redaction_rules = room_info.room_version_rules_or_default().redaction;
if let Some(redacts) = redaction_event.redacts(&room_version) {
room_info
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
if let Some(redacts) = redaction_event.redacts(&redaction_rules) {
room_info.handle_redaction(
redaction_event,
timeline_event.raw().cast_ref_unchecked(),
);
context.state_changes.add_redaction(
room_id,
redacts,
timeline_event.raw().clone().cast(),
timeline_event.raw().clone().cast_unchecked(),
);
}
}
@@ -134,17 +128,17 @@ pub async fn build<'notification, 'e2ee>(
)
} else {
push_condition_room_ctx =
get_push_room_context(context, room, room_info, notification.state_store)
.await?;
get_push_room_context(context, room, room_info).await?;
}
if let Some(push_condition_room_ctx) = &push_condition_room_ctx {
let actions = notification.push_notification_from_event_if(
room_id,
push_condition_room_ctx,
timeline_event.raw(),
Action::should_notify,
);
let actions = notification
.push_notification_from_event_if(
push_condition_room_ctx,
timeline_event.raw(),
Action::should_notify,
)
.await;
timeline_event.set_push_actions(actions.to_owned());
}
@@ -207,23 +201,13 @@ fn update_push_room_context(
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
// TODO: Use if let chain once stable
if let Some(AnySyncStateEvent::RoomMember(member)) =
context.state_changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
})
{
push_rules.user_display_name = member
.as_original()
.and_then(|ev| ev.content.displayname.clone())
.unwrap_or_else(|| user_id.localpart().to_owned())
if let Some(member) = context.state_changes.member(room_id, user_id) {
push_rules.user_display_name =
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
}
if let Some(AnySyncStateEvent::RoomPowerLevels(event)) =
context.state_changes.state.get(room_id).and_then(|types| {
types.get(&StateEventType::RoomPowerLevels)?.get("")?.deserialize().ok()
})
{
push_rules.power_levels = Some(event.power_levels().into());
if let Some(power_levels) = context.state_changes.power_levels(room_id) {
push_rules.power_levels = Some(power_levels.into());
}
}
@@ -238,7 +222,6 @@ pub async fn get_push_room_context(
context: &Context,
room: &Room,
room_info: &RoomInfo,
state_store: &BaseStateStore,
) -> Result<Option<PushConditionRoomCtx>> {
let room_id = room.room_id();
let user_id = room.own_user_id();
@@ -246,19 +229,7 @@ pub async fn get_push_room_context(
let member_count = room_info.active_members_count();
// TODO: Use if let chain once stable
let user_display_name = if let Some(AnySyncStateEvent::RoomMember(member)) =
context.state_changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
}) {
member
.as_original()
.and_then(|ev| ev.content.displayname.clone())
.unwrap_or_else(|| user_id.localpart().to_owned())
} else if let Some(AnyStrippedStateEvent::RoomMember(member)) =
context.state_changes.stripped_state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
})
{
let user_display_name = if let Some(member) = context.state_changes.member(room_id, user_id) {
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
member.name().to_owned()
@@ -267,38 +238,19 @@ pub async fn get_push_room_context(
return Ok(None);
};
let power_levels = if let Some(event) =
context.state_changes.state.get(room_id).and_then(|types| {
types
.get(&StateEventType::RoomPowerLevels)?
.get("")?
.deserialize_as::<RoomPowerLevelsEvent>()
.ok()
}) {
Some(event.power_levels().into())
} else if let Some(event) =
context.state_changes.stripped_state.get(room_id).and_then(|types| {
types
.get(&StateEventType::RoomPowerLevels)?
.get("")?
.deserialize_as::<StrippedRoomPowerLevelsEvent>()
.ok()
})
{
Some(event.power_levels().into())
let power_levels = if let Some(power_levels) = context.state_changes.power_levels(room_id) {
Some(power_levels)
} else {
state_store
.get_state_event_static::<RoomPowerLevelsEventContent>(room_id)
.await?
.and_then(|e| e.deserialize().ok())
.map(|event| event.power_levels().into())
room.power_levels().await.ok()
};
Ok(Some(PushConditionRoomCtx {
user_id: user_id.to_owned(),
room_id: room_id.to_owned(),
member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
user_display_name,
power_levels,
}))
Ok(Some(assign!(
PushConditionRoomCtx::new(
room_id.to_owned(),
UInt::new(member_count).unwrap_or(UInt::MAX),
user_id.to_owned(),
user_display_name
),
{ power_levels: power_levels.map(Into::into) }
)))
}
@@ -13,11 +13,11 @@
// limitations under the License.
use ruma::{
events::{
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
SyncMessageLikeEvent,
},
RoomId,
events::{
AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent,
room::message::MessageType,
},
};
use super::e2ee::E2EE;
+19 -13
View File
@@ -43,18 +43,18 @@ mod tests {
use assign::assign;
use matrix_sdk_test::{ALICE, BOB, CAROL};
use ruma::{
device_id, event_id,
DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId, device_id, event_id,
events::{
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
call::member::{
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
},
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
},
room_id,
time::SystemTime,
user_id, DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
user_id,
};
use similar_asserts::assert_eq;
@@ -133,17 +133,23 @@ mod tests {
"https://lk.org".to_owned(),
))];
let focus_active = ActiveFocus::Livekit(ActiveLivekitFocus::new());
let (content, state_key) = match init_data {
Some(InitData { device_id, minutes_ago }) => (
CallMemberEventContent::new(
application,
device_id.to_owned(),
focus_active,
foci_preferred,
Some(timestamp(minutes_ago)),
),
CallMemberStateKey::new(user_id.to_owned(), Some(device_id.to_owned()), false),
),
Some(InitData { device_id, minutes_ago }) => {
let member_id = format!("{device_id}_m.call");
(
CallMemberEventContent::new(
application,
device_id.to_owned(),
focus_active,
foci_preferred,
Some(timestamp(minutes_ago)),
None,
),
CallMemberStateKey::new(user_id.to_owned(), Some(member_id), false),
)
}
None => (
CallMemberEventContent::new_empty(None),
CallMemberStateKey::new(user_id.to_owned(), None, false),
+48 -8
View File
@@ -12,15 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
use ruma::{
assign,
OwnedUserId, RoomVersionId, assign,
events::{
EmptyStateKey, RedactContent, RedactedStateEventContent, StateEventType,
macros::EventContent,
room::create::{PreviousRoom, RoomCreateEventContent},
EmptyStateKey, RedactContent, RedactedStateEventContent,
},
room::RoomType,
OwnedUserId, RoomVersionId,
room_version_rules::RedactionRules,
};
use serde::{Deserialize, Serialize};
@@ -69,18 +70,38 @@ pub struct RoomCreateWithCreatorEventContent {
/// This is currently only used for spaces.
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub room_type: Option<RoomType>,
/// Additional room creators, considered to have "infinite" power level, in
/// room versions 12 onwards.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub additional_creators: Vec<OwnedUserId>,
}
impl RoomCreateWithCreatorEventContent {
/// Constructs a `RoomCreateWithCreatorEventContent` with the given original
/// content and sender.
pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self {
let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content;
Self { creator: sender, federate, room_version, predecessor, room_type }
let RoomCreateEventContent {
federate,
room_version,
predecessor,
room_type,
additional_creators,
..
} = content;
Self {
creator: sender,
federate,
room_version,
predecessor,
room_type,
additional_creators,
}
}
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
let Self { creator, federate, room_version, predecessor, room_type } = self;
let Self { creator, federate, room_version, predecessor, room_type, additional_creators } =
self;
#[allow(deprecated)]
let content = assign!(RoomCreateEventContent::new_v11(), {
@@ -89,10 +110,25 @@ impl RoomCreateWithCreatorEventContent {
room_version,
predecessor,
room_type,
additional_creators,
});
(content, creator)
}
/// Get the creators of the room from this content, according to the room
/// version.
pub(crate) fn creators(&self) -> Vec<OwnedUserId> {
let rules = self.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK);
if rules.authorization.explicitly_privilege_room_creators {
std::iter::once(self.creator.clone())
.chain(self.additional_creators.iter().cloned())
.collect()
} else {
vec![self.creator.clone()]
}
}
}
/// Redacted form of [`RoomCreateWithCreatorEventContent`].
@@ -100,15 +136,19 @@ pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventC
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
type StateKey = EmptyStateKey;
fn event_type(&self) -> StateEventType {
StateEventType::RoomCreate
}
}
impl RedactContent for RoomCreateWithCreatorEventContent {
type Redacted = RedactedRoomCreateWithCreatorEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
fn redact(self, rules: &RedactionRules) -> Self::Redacted {
let (content, sender) = self.into_event_content();
// Use Ruma's redaction algorithm.
let content = content.redact(version);
let content = content.redact(rules);
Self::from_event_content(content, sender)
}
}
+10 -13
View File
@@ -17,17 +17,17 @@ use std::fmt;
use as_variant::as_variant;
use regex::Regex;
use ruma::{
events::{member_hints::MemberHintsEventContent, SyncStateEvent},
OwnedMxcUri, OwnedUserId, UserId,
events::{SyncStateEvent, member_hints::MemberHintsEventContent},
};
use serde::{Deserialize, Serialize};
use tracing::{debug, trace, warn};
use super::{Room, RoomMemberships};
use crate::{
RoomMember, RoomState,
deserialized_responses::SyncOrStrippedState,
store::{Result as StoreResult, StateStoreExt},
RoomMember, RoomState,
};
impl Room {
@@ -485,11 +485,7 @@ fn compute_display_name_from_heroes(
// User is alone.
if num_joined_invited <= 1 {
if names.is_empty() {
RoomDisplayName::Empty
} else {
RoomDisplayName::EmptyWas(names)
}
if names.is_empty() { RoomDisplayName::Empty } else { RoomDisplayName::EmptyWas(names) }
} else {
RoomDisplayName::Calculated(names)
}
@@ -513,26 +509,27 @@ mod tests {
use matrix_sdk_test::{async_test, event_factory::EventFactory};
use ruma::{
UserId,
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
assign,
events::{
StateEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
name::RoomNameEventContent,
},
StateEventType,
},
room_alias_id, room_id,
serde::Raw,
user_id, UserId,
user_id,
};
use serde_json::json;
use super::{compute_display_name_from_heroes, Room, RoomDisplayName};
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
use crate::{
store::MemoryStore, MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges,
StateStore,
MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges, StateStore,
store::MemoryStore,
};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
@@ -554,7 +551,7 @@ mod tests {
"state_key": user_id,
});
Raw::new(&ev_json).unwrap().cast()
Raw::new(&ev_json).unwrap().cast_unchecked()
}
fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
+35 -3
View File
@@ -36,6 +36,11 @@ pub enum EncryptionState {
/// The room is encrypted.
Encrypted,
/// The room is encrypted, additionally requiring state events to be
/// encrypted.
#[cfg(feature = "experimental-encrypted-state-events")]
StateEncrypted,
/// The room is not encrypted.
NotEncrypted,
@@ -46,10 +51,25 @@ pub enum EncryptionState {
impl EncryptionState {
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
#[cfg(not(feature = "experimental-encrypted-state-events"))]
pub fn is_encrypted(&self) -> bool {
matches!(self, Self::Encrypted)
}
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted] or
/// [`StateEncrypted`][Self::StateEncrypted].
#[cfg(feature = "experimental-encrypted-state-events")]
pub fn is_encrypted(&self) -> bool {
matches!(self, Self::Encrypted | Self::StateEncrypted)
}
/// Check whether `EncryptionState` is
/// [`StateEncrypted`][Self::StateEncrypted].
#[cfg(feature = "experimental-encrypted-state-events")]
pub fn is_state_encrypted(&self) -> bool {
matches!(self, Self::StateEncrypted)
}
/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
@@ -68,17 +88,18 @@ mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::ALICE;
use ruma::{
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
},
room_id,
time::SystemTime,
user_id, EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
user_id,
};
use super::{EncryptionState, Room};
use crate::{store::MemoryStore, RoomState};
use crate::{RoomState, store::MemoryStore};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -154,5 +175,16 @@ mod tests {
assert!(EncryptionState::Unknown.is_encrypted().not());
assert!(EncryptionState::Encrypted.is_encrypted());
assert!(EncryptionState::NotEncrypted.is_encrypted().not());
#[cfg(feature = "experimental-encrypted-state-events")]
{
assert!(EncryptionState::StateEncrypted.is_unknown().not());
assert!(EncryptionState::StateEncrypted.is_encrypted());
assert!(EncryptionState::Unknown.is_state_encrypted().not());
assert!(EncryptionState::Encrypted.is_state_encrypted().not());
assert!(EncryptionState::StateEncrypted.is_state_encrypted());
assert!(EncryptionState::NotEncrypted.is_state_encrypted().not());
}
}
}
+5 -5
View File
@@ -16,19 +16,19 @@ use std::collections::BTreeMap;
use eyeball::{AsyncLock, ObservableWriteGuard};
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
StateEventType, SyncStateEvent,
},
OwnedEventId, OwnedUserId,
events::{
StateEventType, SyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use tracing::warn;
use super::Room;
use crate::{
StateStoreDataKey, StateStoreDataValue, StoreError,
deserialized_responses::{MemberEvent, RawMemberEvent, SyncOrStrippedState},
store::{Result as StoreResult, StateStoreExt},
StateStoreDataKey, StateStoreDataValue, StoreError,
};
impl Room {
@@ -16,12 +16,12 @@
use std::{collections::BTreeMap, num::NonZeroUsize};
#[cfg(feature = "e2e-encryption")]
use ruma::{events::AnySyncTimelineEvent, serde::Raw, OwnedRoomId};
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
use super::Room;
#[cfg(feature = "e2e-encryption")]
use super::RoomInfoNotableUpdateReasons;
use crate::latest_event::LatestEvent;
use crate::latest_event::{LatestEvent, LatestEventValue};
impl Room {
/// The size of the latest_encrypted_events RingBuffer
@@ -34,6 +34,11 @@ impl Room {
self.inner.read().latest_event.as_deref().cloned()
}
/// Return the [`LatestEventValue`] of this room.
pub fn new_latest_event(&self) -> LatestEventValue {
self.inner.read().new_latest_event.clone()
}
/// Return the most recent few encrypted events. When the keys come through
/// to decrypt these, the most recent relevant one will replace
/// latest_event. (We can't tell which one is relevant until
@@ -88,11 +93,12 @@ mod tests_with_e2e_encryption {
use serde_json::json;
use crate::{
BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
SessionMeta, StateChanges,
client::ThreadingSupport,
latest_event::LatestEvent,
response_processors as processors,
store::{MemoryStore, RoomLoadSettings, StoreConfig},
BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
SessionMeta, StateChanges,
};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
@@ -107,8 +113,10 @@ mod tests_with_e2e_encryption {
#[async_test]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
+29 -47
View File
@@ -20,24 +20,24 @@ use std::{
use bitflags::bitflags;
use ruma::{
MxcUri, OwnedUserId, UserId,
events::{
MessageLikeEventType, StateEventType,
ignored_user_list::IgnoredUserListEventContent,
presence::PresenceEvent,
room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::{PowerLevelAction, RoomPowerLevels, RoomPowerLevelsEventContent},
power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel},
},
MessageLikeEventType, StateEventType,
},
MxcUri, OwnedUserId, UserId,
};
use tracing::debug;
use super::Room;
use crate::{
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
store::{ambiguity_map::is_display_name_ambiguous, Result as StoreResult, StateStoreExt},
MinimalRoomMemberEvent,
deserialized_responses::{DisplayName, MemberEvent},
store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
};
impl Room {
@@ -166,13 +166,7 @@ impl Room {
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);
let power_levels = self
.store
.get_state_event_static(self.room_id())
.await?
.and_then(|e| e.deserialize().ok());
let power_levels = self.power_levels_or_default().await;
let users_display_names =
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
@@ -188,7 +182,6 @@ impl Room {
Ok(MemberRoomInfo {
power_levels: power_levels.into(),
max_power_level,
room_creator,
users_display_names,
ignored_users,
})
@@ -205,9 +198,8 @@ pub struct RoomMember {
pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
#[allow(dead_code)]
pub(crate) presence: Arc<Option<PresenceEvent>>,
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
pub(crate) power_levels: Arc<RoomPowerLevels>,
pub(crate) max_power_level: i64,
pub(crate) is_room_creator: bool,
pub(crate) display_name_ambiguous: bool,
pub(crate) is_ignored: bool,
}
@@ -219,15 +211,9 @@ impl RoomMember {
presence: Option<PresenceEvent>,
room_info: &MemberRoomInfo<'_>,
) -> Self {
let MemberRoomInfo {
power_levels,
max_power_level,
room_creator,
users_display_names,
ignored_users,
} = room_info;
let MemberRoomInfo { power_levels, max_power_level, users_display_names, ignored_users } =
room_info;
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
let display_name = event.display_name();
let display_name_ambiguous = users_display_names
.get(&display_name)
@@ -240,7 +226,6 @@ impl RoomMember {
presence: presence.into(),
power_levels: power_levels.clone(),
max_power_level: *max_power_level,
is_room_creator,
display_name_ambiguous,
is_ignored,
}
@@ -270,11 +255,7 @@ impl RoomMember {
/// This returns either the display name or the local part of the user id if
/// the member didn't set a display name.
pub fn name(&self) -> &str {
if let Some(d) = self.display_name() {
d
} else {
self.user_id().localpart()
}
if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
}
/// Get the avatar url of the member, if there is one.
@@ -289,22 +270,27 @@ impl RoomMember {
/// Get the normalized power level of this member.
///
/// The normalized power level depends on the maximum power level that can
/// be found in a certain room, positive values are always in the range of
/// 0-100.
pub fn normalized_power_level(&self) -> i64 {
/// be found in a certain room, positive values that are not `Infinite` are
/// always in the range of 0-100.
pub fn normalized_power_level(&self) -> UserPowerLevel {
let UserPowerLevel::Int(power_level) = self.power_level() else {
return UserPowerLevel::Infinite;
};
let mut power_level = i64::from(power_level);
if self.max_power_level > 0 {
(self.power_level() * 100) / self.max_power_level
} else {
self.power_level()
power_level = (power_level * 100) / self.max_power_level;
}
UserPowerLevel::Int(
power_level.try_into().expect("normalized power level should fit in Int"),
)
}
/// Get the power level of this member.
pub fn power_level(&self) -> i64 {
(*self.power_levels)
.as_ref()
.map(|e| e.power_levels().for_user(self.user_id()).into())
.unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 })
pub fn power_level(&self) -> UserPowerLevel {
self.power_levels.for_user(self.user_id())
}
/// Whether this user can ban other users based on the power levels.
@@ -377,11 +363,8 @@ impl RoomMember {
self.can_do_impl(|pls| pls.user_can_do(self.user_id(), action))
}
fn can_do_impl(&self, f: impl FnOnce(RoomPowerLevels) -> bool) -> bool {
match &*self.power_levels {
Some(event) => f(event.power_levels()),
None => self.is_room_creator,
}
fn can_do_impl(&self, f: impl FnOnce(&RoomPowerLevels) -> bool) -> bool {
f(&self.power_levels)
}
/// Is the name that the member uses ambiguous in the room.
@@ -405,9 +388,8 @@ impl RoomMember {
// Information about the room a member is in.
pub(crate) struct MemberRoomInfo<'a> {
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
pub(crate) power_levels: Arc<RoomPowerLevels>,
pub(crate) max_power_level: i64,
pub(crate) room_creator: Option<OwnedUserId>,
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
}

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