Compare commits

..

615 Commits

Author SHA1 Message Date
Damir Jelić b41efb063e chore: Release matrix-sdk version 0.12.0 2025-06-10 13:33:01 +02:00
Damir Jelić 23db199262 Merge branch 'release-0.11' 2025-06-10 12:51:59 +02:00
Damir Jelić 76d1f8bd18 chore: Fix a PR link in the changelog file 2025-06-10 12:37:32 +02:00
Damir Jelić 550f4c5fde Update crates/matrix-sdk-crypto/CHANGELOG.md
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-06-10 12:37:32 +02:00
Damir Jelić b3f07f4587 chore: Release matrix-sdk version 0.11.1 2025-06-10 12:37:32 +02:00
Damir Jelić 56980745b4 chore: Add a changelog entry for GHSA-x958-rvg6-956w 2025-06-10 12:37:32 +02:00
Richard van der Hoff 13c1d20482 fix(crypto): Check the sender of an event matches owner of session
Having decrypted an event with a given megolm session, we need to check that
the owner of that session actually matches the sender of an event, otherwise
there is a danger of the sender being spoofed to make it look like it was sent
by another user.

Security-Impact: High
CVE: CVE-2025-48937
GitHub-Advisory: GHSA-x958-rvg6-956w
2025-06-10 12:07:10 +02:00
Richard van der Hoff 7f3e144cb3 refactor (crypto): clarify some comments 2025-06-10 12:07:10 +02:00
Richard van der Hoff fe8bd2fdf3 refactor(crypto): Break get_or_update_verification_state in two
Split this into `get_room_event_verification_state` and
`get_or_update_sender_data`, which I think is a bit clearer.
2025-06-10 12:07:10 +02:00
Benjamin Bouvier 7cdfb0d1c0 chore(sqlite): revert the busy_timeout pragmas
Internal Sentry reports tell us that enabling the busy_timeout seems to
have *increased* the number of "database is busy" errors, instead of
lowering those. As a result, we're going to disable the pragmas in all
the places where we enabled it before, and observe how the number of
"database is busy" errors evolves.
2025-06-10 11:13:28 +02:00
Benjamin Bouvier d8652d27f8 feat(sdk): forbid ignoring the current user 2025-06-10 10:50:34 +02:00
Damir Jelić aa67148247 refactor(xtask): Use a helper to append options for the release tasks 2025-06-10 09:47:42 +02:00
Damir Jelić 769fcdb1fb feat: Support releasing a specific package 2025-06-10 09:47:42 +02:00
Damir Jelić 0f4afb32f7 refactor(xtask): Use a helper to append options for the release tasks 2025-06-10 09:34:56 +02:00
Damir Jelić 398787253d feat: Support releasing a specific package 2025-06-10 09:34:56 +02:00
Benjamin Bouvier 7a6e29c347 feat(ui): don't mark each thread reply as an actual reply to the previous message, in threaded timelines
This correctly handles the reply fallback behavior:

- the behavior isn't changed for a live timeline,
- when a timeline is thread-focused, we will extract the `replied_to`
field if and only if the thread relation is *not* marked as behaving in
a fallback manner.

This makes it possible to distinguish actual in-thread replies.
2025-06-10 09:03:30 +02:00
Damir Jelić 6e628781c0 release: Add a changelog entry for the tracing-attributes issue 2025-06-09 20:29:09 +02:00
VerdeQuar a75a2b4113 fix(crypto): Remove wildcard enum variant import
Signed-off-by: VerdeQuar <verdequar@gmail.com>
2025-06-09 20:29:09 +02:00
Damir Jelić 216e878231 Revert "feat(base): Detecting invalid states in room upgrades."
This reverts commit c7f6190cff.
2025-06-09 18:08:03 +02:00
Damir Jelić e54b20fa68 Revert "doc(base): Improve documentation. "
This reverts commit 8e3ad22d92.
2025-06-09 18:08:03 +02:00
Damir Jelić a120057ec3 Revert "fix(base): Revisit check_tombstone entirely."
This reverts commit 0478037b57.
2025-06-09 18:08:03 +02:00
dependabot[bot] 737bda44a2 chore(deps): bump crate-ci/typos from 1.32.0 to 1.33.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.32.0 to 1.33.1.
- [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.32.0...v1.33.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 17:45:37 +02:00
Benjamin Bouvier df65c94974 chore(ui): apply review comments 2025-06-09 16:59:32 +02:00
Stefan Ceriu 39ee5112b4 chore(ui): add timeline threaded filtering tests 2025-06-09 16:59:32 +02:00
Stefan Ceriu 96afdeffad feat(ui): add TimelineFocus associated values for defining whether threaded events should be hidden on those particular timeline instances. 2025-06-09 16:59:32 +02:00
Stefan Ceriu 669d91e7e9 feat(ui): add thread based filtering for local event items 2025-06-09 16:59:32 +02:00
Stefan Ceriu dcec53ba00 feat(ui): filter threaded remote events based on the timeline focus and extracted relations 2025-06-09 16:59:32 +02:00
Stefan Ceriu 9d9ce4a68b chore(ui): remove relation processing when dealing with EmbeddedEvents 2025-06-09 16:59:32 +02:00
Stefan Ceriu dc24365ddf chore(ui): move event based relation handling to TimelineMetadata and out of the TimelineAction 2025-06-09 16:59:32 +02:00
Stefan Ceriu ba53390547 chore(ui): move timeline item relation handling from the TimelineAction to the TimelineMetadata 2025-06-09 16:59:32 +02:00
Stefan Ceriu c860e7fca7 fix(ui): load up reactions to threaded messages 2025-06-09 16:59:32 +02:00
Benjamin Bouvier ebcb74a86d refactor!(event cache): introduce LinkedChunkId in the backends (#5182)
In a "soon" future, threads have their own linked chunk. All our code
has been written with the fact that a linked chunk belong to *a room* in
mind, so it needs some biggish update. Fortunately, most of the changes
are mechanical, so they should be rather easy to review.

Part of #4869, namely #5122.
2025-06-09 13:26:46 +00:00
Jonas Platte 65bb20c965 refactor: Clean up tracing and formatting macro uses (#5192)
Signed-off-by: Jonas Platte <jplatte+matrix@posteo.de>
2025-06-09 12:15:59 +02:00
VerdeQuar bdda4abf56 fix(crypto): Remove wildcard enum variant import
Signed-off-by: VerdeQuar <verdequar@gmail.com>
2025-06-07 14:55:27 +02:00
Jorge Martín 47e81818bc test: Add extra test for the active call state
Also, adapt the other tests to the new return type
2025-06-06 15:02:17 +02:00
Jorge Martín fe015b7eda refactor: Expose the new return type of Client::send_call_notifications_if_needed in the FFI layer 2025-06-06 15:02:17 +02:00
Jorge Martín baf6824bc4 refactor: Add extra logs to Client::send_call_notifications_if_needed
Also make it return `Result<bool>` instead of `Result<()>` so we can check if the event was sent.
2025-06-06 15:02:17 +02:00
Jorge Martín c1ce92bf48 refactor(ffi): Use Client::room_info_notable_update_receiver instead of RoomUpdates
This allows us to also react to local changes that would take several extra seconds to be received in a new sync response.
2025-06-06 13:18:49 +02:00
Benjamin Bouvier 4eb7e0c845 feat(timeline): insert the timeline start item just after creating a live timeline
This is even better, as we don't need to look at the live pagination
status to know whether we've reached the start of the timeline or not.
2025-06-06 12:57:39 +02:00
Benjamin Bouvier 766ff3f8e9 test(timeline): add a test for the mysterious missing timeline start case 2025-06-06 12:57:39 +02:00
Benjamin Bouvier a855f1df2c fix(timeline): add the timeline start of the current pagination state too 2025-06-06 12:57:39 +02:00
Ivan Enderlin 0478037b57 fix(base): Revisit check_tombstone entirely.
This patch renames `check_tombstone` to `check_room_upgrades`.
Then it rewrites it **entirely** to remove all false-positives and
false-negatives. More importantly, the room versions are no longer
involved: they can't be compared or ordered, they must be treated as
opaque values.

This new version of `check_room_upgrades` does a first path to check
predecessor-successor consistency. Then it does a second version to
detect loops. This new algorithm is robust to absent `m.room.create`
events. Making them mandatory is left to another patch.

More tests are added, especially to ensure that `m.room.create` cannot
be overwritten, and to ensure loops or inconsistent predecessors and
successors are correctly detected.
2025-06-06 12:54:45 +02:00
Benjamin Bouvier caa07a8007 refactor(sdk): regroup bundled thread extraction in TimelineEvent ctors 2025-06-06 13:34:12 +03:00
Benjamin Bouvier 93f2c61447 feat(event cache): store a bundled thread's latest TimelineEvent if provided 2025-06-06 13:34:12 +03:00
Benjamin Bouvier c8a5c43232 feat(common): temporarily store a bundled thread's latest TimelineEvent 2025-06-06 13:34:12 +03:00
Benjamin Bouvier 2aeb1a0353 refactor(sdk): rename TimelineEvent::new to from_plaintext 2025-06-06 13:34:12 +03:00
Benjamin Bouvier a1ad772642 refactor(sdk): rename TimelineEvent::new_utd_event to from_utd 2025-06-06 13:34:12 +03:00
Benjamin Bouvier b8f850b6f2 refactor(sdk): make it clearer that Context isn't mutated in a few processor helpers
It isn't mutated by the function, so there's no need to pass a mutable
reference here.
2025-06-06 13:34:12 +03:00
Benjamin Bouvier c6ed2d1963 refactor(sdk): compute push actions before creating a decrypted TimelineEvent
This reduces the number of callers to `set_push_actions()`, which should
be minimally used.
2025-06-06 13:34:12 +03:00
Benjamin Bouvier c48a2d68d1 refactor(base): streamline the verification processor 2025-06-06 13:34:12 +03:00
Benjamin Bouvier 5a1909aab9 refactor(sdk): get rid of the implicit conversion from DecryptedEvent to TimelineEvent 2025-06-06 13:34:12 +03:00
Benjamin Bouvier fc81178504 refactor(sdk): make TimelineEvent::push_actions private
And add getters and setters. It makes it clear who are the external
readers/writers of the push actions, and it makes it impossible to
create a `TimelineEvent` out of thin air (since it now has a private
field).
2025-06-06 13:34:12 +03:00
Daniel Salinas d3f63e91d5 Enable subscribe_to_send_progress for Wasm 2025-06-06 10:08:04 +02:00
Benjamin Bouvier 70705f4e9d chore(ci): exclude some crate from codecov testing
There's been many segfaults happening in tests, while running the test
coverage for this specific crate. An issue has been opened on
cargo-tarpaulin's repository:

https://github.com/xd009642/tarpaulin/issues/1749

Until this is fixed or worked around, we'll disable coverage testing for
this specific crate.
2025-06-06 09:28:55 +02:00
Benjamin Bouvier 8c66e0ba2f chore(ci): remove unused SLIDING_SYNC_PROXY_URL env variable 2025-06-06 09:28:55 +02:00
Benjamin Bouvier f5e0c6f004 chore(ci): exclude multiverse from the codecov reports 2025-06-06 09:28:55 +02:00
Jonas Platte 2a140770a0 fix: Move runtime module from matrix-sdk-common to matrix-sdk-ffi (#5184)
This module only builds on non-wasm with the patched async-compat from
the workspace Cargo.toml's patch section, and it is only used by the ffi
crate. It is currently breaking the use of the SDK as a git dependency,
and would prevent the publishing of matrix-sdk-common (unless using
--no-verify, but then that would just break all users of the newly
published crates.io version).

This bug was introduced in
https://github.com/matrix-org/matrix-rust-sdk/pull/5089.

Signed-off-by: Jonas Platte <jplatte+matrix@posteo.de>
2025-06-05 19:30:15 +00:00
Jorge Martín becbb63ad7 feat(ffi): Subscribe to a room's RoomInfo through Client
This helps in the case we want to observe the membership state changes - or some other info - for a room that's still not known so we can't just use `Client::get_room` to fetch it.
2025-06-05 16:49:39 +02:00
Damir Jelić 34d3cd496b feat(multiverse): Show thread roots even if we can't find the latest message 2025-06-05 16:29:54 +02:00
Damir Jelić 1f9c3394c5 refactor(multiverse): Split out the timeline item formatting logic 2025-06-05 16:29:54 +02:00
Damir Jelić 005f002747 feat(multiverse): Start to render threads 2025-06-05 16:29:54 +02:00
Damir Jelić 80b8a6d8cc feat(multiverse): Allow timeline items to be selected 2025-06-05 16:29:54 +02:00
Valere f7265c39e0 cleanup: Reuse existing server.mock_sync instead of custom function 2025-06-05 14:29:50 +02:00
Valere 4468c36b14 review: extend existing MatrixMockServer instead of creating another 2025-06-05 14:29:50 +02:00
Valere 25841c787e refactor(test): Extract common crypto mock server helper 2025-06-05 14:29:50 +02:00
Damir Jelić 9461ef3a5a chore: Fix a typo in the cargo-deny file 2025-06-05 11:11:40 +02:00
Johannes Marbach b8ae210e4a feat(ffi): Add reply_params to GalleryUploadParameters (#5173)
Looks like I forgot adding this in
https://github.com/matrix-org/matrix-rust-sdk/pull/5163, sorry.
Everything below the FFI layer was already prepared for replies.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-06-05 10:07:32 +01:00
Daniel Salinas a3225e5cd7 feat(wasm): Wasm equivalent of get_runtime_handle and corresponding tokio types (#5089)
Adds a Wasm equivalent of the get_runtime_handle method provided by
tokio, as well as Handle/Runtime types that can be used on either Wasm
or non-Wasm platforms interchangeably.

Dependent on https://github.com/matrix-org/matrix-rust-sdk/pull/5088
<!-- description of the changes in this PR -->

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

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

---------

Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
Co-authored-by: Daniel Salinas <danielsalinas@Daniels-MacBook-Pro-2.local>
Co-authored-by: Daniel Salinas <danielsalinas@daniels-mbp-2.myfiosgateway.com>
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2025-06-05 08:07:01 +02:00
Daniel Salinas 0777e6e08a feat(wasm): Enable subscribe_to_send_progress on wasm platforms (#5170) 2025-06-04 18:13:47 +02:00
Daniel Salinas 8b2088fd61 feat(wasm): Eliminate some unecessary wasm removals from matrix-sdk crate (#5169) 2025-06-04 18:13:27 +02:00
Johannes Marbach d38f409351 feat(ffi): Expose method for sending galleries (#5163)
Addendum to https://github.com/matrix-org/matrix-rust-sdk/pull/5125 to
allow sending galleries from the FFI crate. This is the final PR for
galleries (apart from possible enhancements to report the upload status
in https://github.com/matrix-org/matrix-rust-sdk/pull/5008).

This is somewhat lengthy again, apologies. Most of the changes are just
wrappers and type mapping, however. So I was hoping that it's relatively
straightforward to review in bulk. Happy to try and elaborate on the
changes or break them up into smaller commits if that helps, however.

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-06-04 16:41:03 +02:00
Daniel Salinas 58b8a2560c feat(wasm): Comprehensive Send+Sync bound improvements for Wasm compatibility (#5082)
This commit systematically replaces Send+Sync trait bounds throughout
the matrix-rust-sdk codebase to enable Wasm compatibility while
maintaining thread safety on native targets.

Key changes:
- Use SendOutsideWasm/SyncOutsideWasm traits instead of Send/Sync for
trait bounds
- Apply conditional compilation for error types and trait objects
- Update FFI trait definitions to use Wasm-compatible bounds
- Fix event handler and type alias definitions for cross-platform
compatibility
- Maintain existing functionality while enabling WebAssembly target
support

The SendOutsideWasm/SyncOutsideWasm traits are empty on Wasm (allowing
all types) and alias to Send/Sync on native targets, ensuring zero-cost
abstraction.

Files updated:
- All FFI bindings (30+ trait definitions)
- Core SDK error types and type aliases
- Event handler infrastructure
- Store and crypto abstractions
- UI service filters and sorters
- Timeline and authentication modules

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

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

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

---------

Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
Co-authored-by: Daniel Salinas <danielsalinas@Daniels-MacBook-Pro-2.local>
Co-authored-by: Daniel Salinas <danielsalinas@daniels-mbp-2.myfiosgateway.com>
2025-06-04 15:59:37 +02:00
Daniel Salinas edb7a0f433 Add Wasm platform aware test to export proc-macro 2025-06-04 12:32:33 +02:00
Valere Fedronic 0f73ffde68 feat(crypto): Add the EncryptionInfo to the Decrypted ProcessedToDeviceEvent variant
The `ProcessedToDeviceEvent::Decrypted` variant now also have an
`EncryptionInfo` field.

The enum variant  changed from `Decrypted(Raw<AnyToDeviceEvent>)` to `Decrypted {
raw: Raw<AnyToDeviceEvent>, encryption_info: EncryptionInfo) }`
2025-06-04 11:54:38 +02:00
Daniel Salinas d3be744244 feat(wasm): Remove direct use of tokio::spawn in favor of matrix-sdk-common (#5159)
Mechanical move from tokio::spawn to matrix_sdk_common::executor::spawn
that has support for Wasm platforms. On non-Wasm, this shim defaults to
tokio::spawn.
2025-06-03 12:22:53 -04:00
Andy Balaam ca63d60068 doc(crypto): Add missing word 'verify' in 'verify_device' docs 2025-06-03 17:04:48 +02:00
Damir Jelić 8cc3b0fa33 refactor(multiverse): Add a common method to execute commands on rooms 2025-06-03 16:46:17 +02:00
Damir Jelić bf201e317e feat(multiverse): Add a /leave command 2025-06-03 16:46:17 +02:00
Benjamin Bouvier fe11fda832 refactor(timeline): extract the method to fetch a latest thread reply as an item
Do the ~~~loco~~ code motion 🎶
2025-06-03 16:28:25 +02:00
Benjamin Bouvier 281faa7a0b refactor(ffi): simplify From<TimelineDetails<Profile>> for ProfileDetails 2025-06-03 16:28:25 +02:00
Benjamin Bouvier 7962253ebd refactor(timeline): only use the publicly exposed content/sender/sender_profile on EmbeddedEvent 2025-06-03 16:28:25 +02:00
Benjamin Bouvier dd14df086e refactor(ffi): simplify FFI layer and use only EmbeddedEventDetails there for both thread latest event + replied-to event item 2025-06-03 16:28:25 +02:00
Benjamin Bouvier c426138971 refactor(timeline): rename RepliedToEvent to EmbeddedEvent and reuse it for the thread summary 2025-06-03 16:28:25 +02:00
Benjamin Bouvier ff4b7a8acc feat(event cache): add basic support for the latest event in the thread summary 2025-06-03 16:28:25 +02:00
Benjamin Bouvier a152f9c074 refactor(event cache): remove one caller of with_events_mut
The actual code useful in `with_events_mut` and used in that function
was to propagate the changes to the store and propagating diffs to
observers. It often striked me as hacky to use this method to do that,
so instead I'm proposing here to inline the useful bits. That way,
`with_events_mut` is now clearly called only in two cases: after a sync,
or after a successful network back-pagination.
2025-06-03 16:28:25 +02:00
Benjamin Bouvier 50be8a158c refactor(event cache): only send a thread summary update when a thread has changed 2025-06-03 16:28:25 +02:00
Benjamin Bouvier aa291079d0 feat(event cache): include the reply count in the ThreadSummary 2025-06-03 16:28:25 +02:00
Benjamin Bouvier 672bb9f460 feat: add the busy timeout pragma to the event cache store acquire() method too
It will tell us if this is sufficient to avoid locking the event cache
store database, now that we have some proof that this is happening in
the wild.
2025-06-03 16:17:36 +02:00
Kévin Commaille 9a75007535 Upgrade Ruma
Use the newly released version.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-06-03 15:52:15 +02:00
Johannes Marbach ee06965d2e feat(timeline): Expose method to send galleries in matrix-sdk-ui (#5125)
This was broken out of
https://github.com/matrix-org/matrix-rust-sdk/pull/4838 and is a step
towards implementing
https://github.com/matrix-org/matrix-spec-proposals/pull/4274. Building
upon https://github.com/matrix-org/matrix-rust-sdk/pull/4977, a new
method `Timeline::send_gallery` in matrix-sdk-ui for sending galleries.

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

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-06-03 15:24:02 +02:00
Ivan Enderlin 8e3ad22d92 doc(base): Improve documentation.
This patch improves an inline documentation.

Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2025-06-03 15:04:26 +02:00
Ivan Enderlin c7f6190cff feat(base): Detecting invalid states in room upgrades.
This patch adds the `check_tombstone` response processor to
detect invalid state with room upgrades. One can check the new
`InconsistentTombstonedRooms` error to learn more about the detected
patterns.
2025-06-03 15:04:26 +02:00
Ivan Enderlin 5ccb1c6fa8 fix(base): Fix the Debug implementation of RoomUpdates.
The field of `RoomUpdates` has been renamed, but the `Debug`
implementation does not reflect that change. This patch fixes that.
2025-06-03 15:04:26 +02:00
Ivan Enderlin b77a02662d feat(base): Add RoomUpdates::iter_all_room_ids.
This patch adds the `RoomUpdates::iter_all_room_ids` method, to iterate
over all IDs of rooms that have received an update.
2025-06-03 15:04:26 +02:00
Damir Jelić 13306be4ed fix(multiverse): Wait for the join task to finish before switching the view 2025-06-03 14:07:37 +02:00
Yousef Moazzam 491f81c376 test: remove unnecessary image info field values 2025-06-03 13:19:34 +02:00
Yousef Moazzam 703c01004c test: remove unnecessary server timestamp field on sticker event 2025-06-03 13:19:34 +02:00
Yousef Moazzam b1d34763d4 test: create timeline event with EventBuilder method 2025-06-03 13:19:34 +02:00
Yousef Moazzam c4aa200f19 test: create sticker event with EventFactory 2025-06-03 13:19:34 +02:00
Yousef Moazzam a099879563 test: add reply thread relation method to sticker event builder 2025-06-03 13:19:34 +02:00
Yousef Moazzam b6569762db test: add sticker event method to EventFactory 2025-06-03 13:19:34 +02:00
Jorge Martín edabb2362b refactor: revert some method removals in SendRequest 2025-06-03 12:59:00 +02:00
Jorge Martín 8a5d4f0d82 test: Increase delay to fix code coverage failure 2025-06-03 12:59:00 +02:00
Jorge Martín 12aed8dc67 test: fix tests after adding the media config check 2025-06-03 12:59:00 +02:00
Jorge Martín 7d03d4ce0d feat(ffi): Add Client::get_max_media_upload_size function
This allows the clients to know the max upload size for a media file and try to compress if it's too large for the homeserver.
2025-06-03 12:59:00 +02:00
Jorge Martín 2680dc65fd fix(sdk): check max_upload_size before attempting to upload any media 2025-06-03 12:59:00 +02:00
Richard van der Hoff a1e2eed467 sdk: Add ClientBuilder::with_enable_share_history_on_invite (#5141)
Replace `experimental-share-history-on-invite` feature flag with a
runtime flag on the `Client`.
2025-06-03 11:36:48 +01:00
Benjamin Bouvier 5600ce7a77 fix(event cache): after adding a thread summary in memory, also save the result in the store 2025-06-03 12:22:21 +02:00
Benjamin Bouvier f28a2b1cc3 fix(event cache): after redacting an in-memory event, also save the result in the store 2025-06-03 12:22:21 +02:00
Benjamin Bouvier 1a07ec22b8 test(event cache): move tests which require the cross process lock under a different test mod
This avoids a few `use` statements within the test functions themselves,
and helps finding out which tests are integration tests.
2025-06-03 12:22:21 +02:00
dependabot[bot] 7b8671d82c chore(deps): bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from c6634ca281a9fc05b03bee224ba00910cb78ab6e to 115870536a85eaf050e369291c7895748ff12aea.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/c6634ca281a9fc05b03bee224ba00910cb78ab6e...115870536a85eaf050e369291c7895748ff12aea)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 115870536a85eaf050e369291c7895748ff12aea
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-03 11:17:12 +02:00
Daniel Salinas 0f877a3e80 feat(wasm): Replace async_trait with wasm aware cfg conditional 2025-06-02 23:07:05 +02:00
Daniel Salinas c6e55c1a36 Mechanical move from target_arch="wasm32" to target_family="wasm" 2025-06-02 17:27:34 +02:00
Benjamin Bouvier 40648f6998 chore: formatting, clippy and review comments 2025-06-02 16:24:25 +02:00
Benjamin Bouvier 58b46813d6 feat(event cache): when we see a thread reply, add a thread summary to the thread root 2025-06-02 16:24:25 +02:00
Benjamin Bouvier f2a90cb921 test: use helpers to create a bundled edit and thread summary 2025-06-02 16:24:25 +02:00
Benjamin Bouvier efda26ef6f feat(timeline): forward the TimelineEvent::thread_summary to timeline items
Right now, we're not passing the latest event yet, but we could improve
that later!
2025-06-02 16:24:25 +02:00
Benjamin Bouvier 433209ca9b feat(common): always extract a bundled thread summary when creating or deserializing a timeline event 2025-06-02 16:24:25 +02:00
Benjamin Bouvier 1680891982 feat(common): add helper to extract a bundled thread summary 2025-06-02 16:24:25 +02:00
Benjamin Bouvier 3c965ed56e feat(common): add a ThreadSummary field to TimelineEvent
It can be either known to be there or not there, or in an unknown state,
hence a tiny enum making this very explicit.
2025-06-02 16:24:25 +02:00
Benjamin Bouvier 0bb2c44254 feat(common): don't serialize push actions if there's none
If the vec of actions is defined (Some) but empty, then it means we
could determine there are no push actions. It is a slight optimization
to not serialize them, in this case; otherwise, this can result in many
serialized empty action vector, in the event cache's persistent storage.
2025-06-02 16:24:25 +02:00
Benjamin Bouvier 23cdcaf5c8 feat(common): add helpers to extract only the thread root from a raw event 2025-06-02 16:24:25 +02:00
Benjamin Bouvier ebc7396334 refactor(sdk): remove unused TimelineEvent ctor
It was intended for test purposes, but it seems it's unused, so let's
get rid of it.
2025-06-02 16:24:25 +02:00
Benjamin Bouvier c7234b6f13 doc: make the self type clearer in doc comment of TimelineEvent 2025-06-02 16:24:25 +02:00
Richard van der Hoff 56be9dec59 multiverse: expand user names when inviting (#5146)
When the user does an `/invite`, if the target user doesn't start with
an `@`, try expanding it as a user on the local server.

This makes it much easier when repeatedly testing inviting!
2025-06-02 14:41:20 +01:00
Richard van der Hoff 2fa6a98052 multiverse: write logs to session dir (#5145)
Rather than always writing the logs to `/tmp`, write them to the session
directory. The session directory defaults to `/tmp` so by default this
will do the same as before, but if you override the session path on the
commandline, the logs will get stored alongside the stores and caches.

This is particularly useful when running two instances of multiverse,
and you want them to put their logs in different places.
2025-06-02 14:41:11 +01:00
Yousef Moazzam f03407fdb9 test: remove unnecessary server timestamp fields on events 2025-06-02 14:59:23 +02:00
Yousef Moazzam fd4fd3c4f9 test: add timeline events in bulk to room builder 2025-06-02 14:59:23 +02:00
Yousef Moazzam a9513e6f73 test: remove import of unused sync_timeline_event! macro in timeline subscribe test module 2025-06-02 14:59:23 +02:00
Yousef Moazzam 5f0bcef5ce test: create events with EventFactory in profile updates test 2025-06-02 14:59:23 +02:00
Yousef Moazzam 96049f41a5 test: create events with EventFactory in event filter test 2025-06-02 14:59:23 +02:00
Daniel Salinas ba8998623e Fix annotations on comments for new StreamExt
Hook up use of new extension in matrix-sdk-ffi crate
2025-06-02 12:26:52 +02:00
Daniel Salinas d6bf3019f9 StreamExt that supports wasm platforms
The .boxed() method requires a Send trait that makes it
unusable on Wasm platforms. This re-exports the existing StreamExt,
while providing a new extension for the wasm family of targets.
2025-06-02 12:26:52 +02:00
Damir Jelić ef037631a1 refactor(crypto): Simplify the checking of device keys when decrypting Olm events 2025-05-30 15:45:27 +02:00
Damir Jelić 42ade32bea feat(sdk): Fallback to the device keys in the Olm event for room key bundles
This makes it possible to correctly accept a historic room key bundle if
we previously didn't know about the device of the sender of the bundle.
2025-05-30 15:45:27 +02:00
Damir Jelić 37d7e26929 feat(sdk): Ensure that we have all the devices of a user we invite
This makes it possible to correctly share a historic room key bundle with the
invited user without sharing another room with the user.
2025-05-30 15:45:27 +02:00
Robin e51dceb399 refactor(widget): Improve wording of ApiVersion docs 2025-05-30 15:32:35 +02:00
Robin cee0129225 feat(widget): Distinguish room state and timeline events
This is an implementation of an update to MSC2762 (https://github.com/matrix-org/matrix-spec-proposals/pull/4237). It changes the widget driver to split state updates out into an `update_state` action and use the `send_event` action exclusively for forwarding timeline events. Accordingly, `read_events` now always reads from /messages, never the state store.
2025-05-30 15:32:35 +02:00
Robin ef36665d7d doc(widget): Note that capability renegotiation is unimplemented 2025-05-30 15:32:35 +02:00
Robin e45e357841 refactor(widget): Extract method for processing requested capabilities 2025-05-30 15:32:35 +02:00
Robin 6657501ef4 refactor(widget): Extract method for acquire capabilities response 2025-05-30 15:32:35 +02:00
Robin 69e8f1e86c refactor(widget): Extract method for message-like event reading response 2025-05-30 15:32:35 +02:00
Robin e613fc269f refactor(widget): Remove 'Matrix' from some identifiers
Just making these a bit less verbose and more consistent with surrounding identifiers.
2025-05-30 15:32:35 +02:00
Robin 5c7566c6c9 refactor(widget): Allow state events to be converted to filter inputs
So that when I need to do this (in later commits) I don't have to cast.
2025-05-30 15:32:35 +02:00
Robin aba0adf18d refactor(tests): Move some JSON into static items
I want to use this JSON in multiple tests.
2025-05-30 15:32:35 +02:00
Robin 2c8e71e560 refactor(tests): Allow converting EventBuilder to state 2025-05-30 15:32:35 +02:00
Robin 6f683d3cde refactor(tests): Accept more types for sync builder state events
Refactoring the test event implementation to use the From trait rather than ad-hoc methods along the way.
2025-05-30 15:32:35 +02:00
Jonas Platte 9242d1869a refactor: Use native async fn in traits for BackingStore 2025-05-30 10:48:06 +02:00
Daniel Salinas 59d632fd45 feat(wasm): Improve wasm join handle to implement more tokio methods (#5088)
This change adds support for the abort/abort_handle/is_finished methods
onto the JoinHandle shim for Wasm targets.

Signed-off-by: Daniel Salinas
2025-05-29 14:22:38 +00:00
Richard van der Hoff c55652d327 crypto: changelog fixes (#5136)
Put one change in the right place in the changelog, and add missing PR
links.
2025-05-29 14:22:00 +01:00
Jonas Platte 0220689964 refactor: Wrap EncryptionInfo in Arc
It's >100 bytes large and often optional, so it makes sense to put it
on the heap to reduce the size of structs with such optional fields,
and the stack size of functions with such optional parameters.
It's also cloned in a couple places in the UI crate, so it probably
makes sense to just always refcount it.

This started as a clippy suggestion to box PendingEdit inside
AggregationKind::Edit.
2025-05-29 14:08:10 +02:00
Jonas Platte d8969db30a refactor: Increase readability of WidgetMachine::process
Based on a clippy suggestion.
2025-05-29 13:19:59 +02:00
Jonas Platte 8eec683793 refactor: Use inline format arguments more
Automated with cargo clippy --fix --workspace --all-targets.
2025-05-29 13:19:59 +02:00
Jonas Platte 4705389ab7 refactor: Use native async fn in traits for testing traits 2025-05-29 11:45:16 +02:00
Damir Jelić db931c5d5c chore: Bump our decancer version
This removes the paste dependency decancer had. We still need to have a
denyc exception for paste because of rmp-serde and ratatui.
2025-05-29 11:40:37 +02:00
Jonas Platte 9a0b56ad1a refactor(ci): Don't rerun most CI jobs when un-drafting a PR
This only makes sense to do for workflows that branch off of
github.event.pull_request.draft, which only bindings_ci.yml does at this
point in time.
2025-05-29 11:05:03 +02:00
Jonas Platte 3c20ee41d6 chore: Fix clippy lints 2025-05-29 10:59:56 +02:00
Jorge Martín ed245a0cf0 refactor(ffi): When mapping ffi::StateEventContent, log any unsupported event types
The same was done for unsupported message-like event contents.
2025-05-29 09:54:39 +02:00
Jorge Martín 4626c4caaf refactor(ffi): When mapping ffi::MessageLikeEventContent, log any unsupported event types
At the moment, the logs just say 'Unsupported Event Type', which is not that helpful.
2025-05-29 09:54:39 +02:00
Ivan Enderlin 7cda6d2ea6 chore(sdk): Add more logs for Room::leave.
This patch adds a bit more logs in `Room::leave` to understand what's
happening for some users.
2025-05-28 14:03:05 +02:00
Jorge Martín 2ec15984a8 test: Improve NotificationClient tests using the modern test utils 2025-05-28 12:32:58 +02:00
Yousef Moazzam 01f035d574 Test: Replace sync_timeline_event! with EventFactory for events in event item tests (#5093)
Part of #3716

I did notice that the `sender` and `member` methods have some overlap in
what values get set for the "sender" and "state key", and I tried to
make sure that in my changes to use `EventFactory` the original event
configuration is being replicated, so please do double-check me on that
note in particular.

Signed-off-by: Yousef Moazzam <yousefmoazzam@hotmail.co.uk>
2025-05-28 10:48:25 +02:00
Damir Jelić 53b01fb8a5 test: Enable the historic room key bundle storage test, except on WASM
The test was ignored since the functionality was only implemented for
the memory and SQLite store. This caused a bug in the SQLite
implementation to go unnoticed.

Let's just disable it for WASM since this is the only place where we
didn't yet implement the necessary methods.
2025-05-27 18:15:36 +02:00
Jorge Martín 79c47b4470 fix(sdk): Handle 520 HTTP status code as a permanent error
The status code is usually returned by Cloudflare to indicate an unknown server error, so we should cancel the upload and let the user retry if they want to.
2025-05-27 17:56:44 +02:00
Damir Jelić 089abec866 fix(sdk): Don't require the invite details to be present when accepting an room invite 2025-05-27 17:46:10 +02:00
Damir Jelić 064fd6cb0b fix(sqlite): Use the correct column name for the sender of bundled room keys 2025-05-27 17:46:10 +02:00
Damir Jelić 60d3b3d56b test: Finish up the shared history integration test 2025-05-27 17:46:10 +02:00
Damir Jelić 995838d9d3 refactor(tests): Move the shared history test into its own module 2025-05-27 17:46:10 +02:00
Damir Jelić 1d4d4bc741 refactor(tests): Create a submodule for the end-to-end encryption integration tests 2025-05-27 17:46:10 +02:00
Damir Jelić 41a5fd90f4 feat(tests): Add a macro to assert that a TimelineEventKind was encrypted 2025-05-27 17:46:10 +02:00
Damir Jelić 3d9d619f8b feat(sdk): Import the room keys we download from the shared history bundle 2025-05-27 17:46:10 +02:00
Damir Jelić af38f0d1ee refactor(crypto): Move the room key bundle import method under the store 2025-05-27 17:46:10 +02:00
Damir Jelić 091a5fb354 refactor(crypto): Clarify some things in the room key bundle import logic 2025-05-27 17:46:10 +02:00
Richard van der Hoff 8a1d6ce0eb feat(sdk): Attempt to download room key bundles when we accept an invite 2025-05-27 17:46:10 +02:00
Ivan Enderlin 0f5f24527b chore(ffi): Remove Room::is_tombstoned.
This patch removes the `Room::is_tombstoned` method as using
`Room::successor_room` plays the same role and its returned value is
always useful.
2025-05-27 17:35:31 +02:00
Ivan Enderlin da60c1488e feat(ffi): Add the DeduplicateVersions room list filter.
This patch adds `RoomListEntriesDynamicFilterKind::DeduplicateVersions`
to use `new_filter_deduplicate_versions`.
2025-05-27 17:35:31 +02:00
Ivan Enderlin f65893d65e doc(base): Remove a useless link reference. 2025-05-27 17:35:31 +02:00
Ivan Enderlin 3e89b6b8f9 feat(ffi): Add SuccessorRoom and PredecessorRoom.
This patch removes `RoomTombstoneInfo` and replaces it by
`SuccessorRoom`. This patch renames `RoomInfo::tombstone` to
`RoomInfo::success_room`.

This patch also implements `Room::successor_room()` and
`Room::predecessor_room()`, and adds documentation.
2025-05-27 17:35:31 +02:00
Mauro 7531167824 feat: Let the media preview config return an optional
This allows applications to decide what they'd like to do if there isn't a value present. It allows the application to decide what the default should be.
2025-05-27 16:24:41 +02:00
Ivan Enderlin d24e269ecc doc(ui): Fix typos. 2025-05-27 14:02:21 +02:00
Ivan Enderlin 88c18c5499 feat(ui): New Room List filter: deduplicate_versions.
This patch adds the new `deduplicate_versions`. This new filter will
filter out room versions that are outdated. Only the “active” versions
are kept.

A room version is considered active if and only if:

* the room is joined and has no successor,
* the room is joined and has a successor room that is invited or knocked,
* the room is left, invited, banned or knocked.

All other rooms are filtered out.
2025-05-27 14:02:21 +02:00
Ivan Enderlin d0f1e6ce6d chore(base): Expose the new SuccessorRoom and PredecessorRoom types.
This patch makes the `SuccessorRoom` and `PredecessorRoom` types public.
2025-05-27 14:02:21 +02:00
Johannes Marbach 10668f20b0 feat(send_queue): Implement sending of MSC4274 galleries (#4977)
This was broken out of
https://github.com/matrix-org/matrix-rust-sdk/pull/4838 and is a step
towards implementing
[MSC4274](https://github.com/matrix-org/matrix-spec-proposals/pull/4274).

* The entry point for sending galleries via the send queue is a new
method
[`RoomSendQueue::send_gallery`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-8752e86459c22cff470d7ca617240dcbdf222c3c6c98be2af2e43ddec071154cR362)
which is a generalization of `RoomSendQueue::send_attachment`.
* `send_gallery` takes as input parameters a
[`GalleryConfig`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-d38d74cec6159cf769c32bed496199146175f05428fc23ab13bc2c629900da3eR283)
(containing info about the gallery itself, such as its caption) and a
vector of
[`GalleryItemInfo`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-d38d74cec6159cf769c32bed496199146175f05428fc23ab13bc2c629900da3eR355)s
(containing info about each image / file / etc. in the gallery).
* `send_gallery` creates the gallery event content via
[`Room:make_message_event`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-474f20e47fdcf60feac3f839a81f82fbb2c1fd0bb406388b0262adb60216ce3bR2281)
which was renamed from `make_attachment_event` to reflect the fact that
it creates general `msgtype` events now.
* `send_gallery` maps the passed item infos into
[`GalleryItemQueueInfo`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-d569f54c7901b5cd660aae77045c80dabbd3c251f0fa5891530469f35941327aR2008)s.
This additional struct allows grouping all the metadata for a single
gallery item together.
* Finally `send_gallery` invokes
[`QueueStorage::push_gallery`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-d569f54c7901b5cd660aae77045c80dabbd3c251f0fa5891530469f35941327aR1294)
which is a generalization of `QueueStorage::push_media`.
* `send_gallery` pushes upload requests for the media and thumbnails to
the queue in a "daisy chain" manner. The first thumbnail (or media if no
thumbnail exists) is pushed as a `QueuedRequestKind::MediaUpload`. The
remaining thumbnails and media are pushed as
`DependentQueuedRequestKind::UploadFileOrThumbnail`s while chaining each
request to the previous one.
* Finally a
[`DependentQueuedRequestKind::FinishGallery`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-42a1a7ebe2446d916c9e013293bafc5eb16fd89bfe89248e3877ef031cc83ef2R265)
is pushed to finalize the gallery upload (analogous to the existing
`FinishUpload` for single media uploads).
* The `FinishGallery` request is handled in
[`QueueStorage::handle_dependent_finish_gallery_upload`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-8752e86459c22cff470d7ca617240dcbdf222c3c6c98be2af2e43ddec071154cR628)
which was modeled after `handle_dependent_finish_upload`.
* To be able to map the temporary event source for each gallery item to
the final `AccumulatedSentMediaInfo`, a hash map is used.
* Using the hash map, the gallery event is then updated to use the
actual media sources via
[`update_gallery_event_after_upload`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-8752e86459c22cff470d7ca617240dcbdf222c3c6c98be2af2e43ddec071154cR104)
and a new method
[`Room::make_gallery_item_type`](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-474f20e47fdcf60feac3f839a81f82fbb2c1fd0bb406388b0262adb60216ce3bR2303)
* An [integration
test](https://github.com/matrix-org/matrix-rust-sdk/pull/4977/files#diff-21532ad5467a69489d7913b8da7afd4d618b7e357ce94f769e6e60e395b58055R2048)
has been added to demonstrate the functionality.

This is relatively large, unfortunately, but including everything needed
to actually send the event made it possible to also add a test for it.
It would be nice if the amount of new code could be reduced but I'm
struggling a bit to find ways to integrate galleries with the existing
media uploads further.

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

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-05-27 11:53:05 +02:00
Ivan Enderlin 2537f0a508 refactor(base): Create dispatch_room_member.
This patch extracts a bit of code from `dispatch` in a new response
processor: `dispatch_room_member`. The idea is to clarify the content of
`dispatch` itself.
2025-05-27 10:00:16 +02:00
Ivan Enderlin 1387772589 refactor(base): Remove a BTreeSet clone.
This patch moves the insertion of `new_user_ids` in
`updated_members_in_room` after the use of `new_user_ids` by
`update_or_set_if_room_is_newly_encrypted`. By moving it after, it saves
the need to clone it entirely, thus saving memory allocations.
2025-05-27 10:00:16 +02:00
Ivan Enderlin c4f9ef2e2e refactor(base): Improve dispatch_and_get_new_users.
This patch renames `dispatch_and_get_new_users` to `dispatch`. This
response processor no longer returns the new user IDs, but they are
written in a mutable reference argument. This argument is
typed by a new private trait: `NewUsers`. This trait is implemented on
`BTreeSet<OwnedUserId>` and on `()`. It helps to avoid allocations when
the new users are not needed.
2025-05-27 10:00:16 +02:00
Benjamin Bouvier acce75a6dc test: make the test for the previous commit more realistic
This reproduces the issue originally described, where it wasn't possible
to run a /members query in an event handler, because the sync lock was
taken at this point.
2025-05-27 09:09:41 +02:00
aeoncl 5e0704a7c7 fix(sliding sync): don't take the sync lock while handling events
Fixes #5091.
2025-05-27 09:09:41 +02:00
Benjamin Bouvier 206857bc9e feat(ffi): remove the state store when clearing caches
This is a dangerous operation, and requires that the sync service must
be stopped while we're touching this. Since this is an internal method,
that shouldn't be used in most clients anyways (as it usually papers
over actual issues happening elsewhere, on the server for instance), I
kept it simple with a scary doc comment explaining the preconditions,
but we could assert them at runtime, with a little bit more effort.
2025-05-27 08:59:59 +02:00
Benjamin Bouvier 9e1ea5d7d3 feat(sdk): expose the state store database name 2025-05-27 08:59:59 +02:00
Hugh Nimmo-Smith e90b105b36 feat(sdk): Add support for MSC4286
This patch updates Ruma to include support for the mx-external-payment-details span attribute from MSC4286.
2025-05-27 08:47:56 +02:00
dependabot[bot] 40ffd404e8 chore(deps): bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 480f49412651059a414a6a5c96887abb1877de8a to c6634ca281a9fc05b03bee224ba00910cb78ab6e.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/480f49412651059a414a6a5c96887abb1877de8a...c6634ca281a9fc05b03bee224ba00910cb78ab6e)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: c6634ca281a9fc05b03bee224ba00910cb78ab6e
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 17:05:29 +02:00
Benjamin Bouvier b6b9dc8c1a fix(test): make the test_monthly_divider_mode test pass all on timezones 2025-05-26 15:39:04 +02:00
Benjamin Bouvier 4a397f5ab4 refactor(ffi): pass the UTD hook when constructing a Room from the room list (again!) 2025-05-26 14:48:16 +02:00
Benjamin Bouvier be927730fa refactor(ffi): get rid of RoomListItem
It is simply a Room now.
2025-05-26 14:48:16 +02:00
Benjamin Bouvier 81036da44c refactor(ffi): rename the SDK Room import to SdkRoom 2025-05-26 14:48:16 +02:00
Benjamin Bouvier b016f60e46 refactor(ffi): move methods from RoomListItem to Room
Only code motion. Most methods already existed on the `Room` impl block.
2025-05-26 14:48:16 +02:00
Benjamin Bouvier cd76cec089 refactor(ffi): also pass the UTD hook to the notification client 2025-05-26 14:48:16 +02:00
Benjamin Bouvier 820c73dd2f refactor(ffi): get rid of the timeline stored in a Room 2025-05-26 14:48:16 +02:00
Benjamin Bouvier 42a4a6c1a8 refactor(ui): get rid of the RoomListService::Room wrapper
It's just a room, now! Since there were some users of the
`latest_event()` that returned an `EventTimelineItem`, I kept this
method as a method in the Room trait extension that's in the UI crate,
so it's still convenient to use.
2025-05-26 14:48:16 +02:00
Ivan Enderlin eec9a067be fix(sdk): Continue leaving a room if server replies with 403.
This patch updates `Room::leave` to not early return when the server
returns a 403 error code on `/leave`. Indeed, if the user doesn't have
the permissions to leave a room, it's impossible for they to leave it.
Let's consider it's fine to ignore this particular error and continue
the process of leaving the room.
2025-05-26 14:42:17 +02:00
Jorge Martín e53eaf4213 refactor: Fetch the ignored user list account data from the store for Client::is_user_ignored
Also move it to `BaseClient` instead.
2025-05-26 13:02:58 +02:00
Jorge Martín a3725b6b24 fix: Do not retrieve notification events sent by ignored users 2025-05-26 13:02:58 +02:00
Stefan Ceriu f0c7370637 feat(ffi): expose the inviter directly on the room to avoid having to fetch the room preview
The room preview doesn't actually contain the inviter but instead retrieves it from the room invite details.
2025-05-24 10:49:44 +03:00
Yousef Moazzam f108840e28 test: put repeated event field values into variables 2025-05-23 14:12:14 +01:00
Yousef Moazzam f4f63a7e41 test: create plain text room event with EventFactory 2025-05-23 14:12:14 +01:00
Ivan Enderlin 26afd890ce test(base): Use EventFactory instead of JSON hardcoded value.
This patch updates two tests to use the `EventFactory` to replace JSON
hardcoded values to represent the events.
2025-05-23 11:35:53 +02:00
Ivan Enderlin bfac815a5e test: EventBuilder<RoomCreateEventContent> can set predecessor.
This patch adds the `predecessor` and `no_predecessor` methods on
`EventBuilder<RoomCreateEventContent>`. This is helpful to configure the
`predecessor` field.
2025-05-23 11:35:53 +02:00
Ivan Enderlin 95e8d0589b test: Fix EventFactory::create.
This patch fixes `EventFactory::create` where the `m.room.create` wasn't
created as a state-event (the `state_key` field was missing).

Also, it uses the `creator_user_id` in the `sender` field if no sender
was given.
2025-05-23 11:35:53 +02:00
Ivan Enderlin 16a923edda test: Simplify EventBuilder::into_raw_timeline.
This patch simplifies `EventBuilder::into_raw_timeline`. It is exactly
like `EventBuilder::into_raw`, so let's use it.
2025-05-23 11:35:53 +02:00
Ivan Enderlin 9c227c2321 feat(base): Add Room::successor_room and Room::predecessor_room.
First off, this patch renames `Room::tombstone` to
`Room::tombstone_content` (to be consistent with other methods, such as
`Room::create_content`).

Second, this patch adds the `Room::successor_room` and
`Room::predecessor_room` methods, along with the `SuccessorRoom` and
`PredecessorRoom` types. This naming more or less comes from the Matrix
specification:

- the term _predecessor_ is part of the specification,
- the term _successor_ isn't present _per se_, the words _replacement
  room_ are used instead, but I prefer _successor_ as it brings a nice
  symmetry with _predecessor_.
2025-05-23 11:35:53 +02:00
Ivan Enderlin 5d617da74c chore(base): Move the Room::*tombstone* methods in new tombstone module.
This patch moves the `Room::is_tombstoned` and `Room::tombstone` methods
in the new `tombstone` module.
2025-05-23 11:35:53 +02:00
Jonas Platte 3aa356dcd6 chore: Use shorter syntax for workspace inheritance where possible 2025-05-23 10:23:36 +02:00
Jonas Platte 491f7cd529 chore: Clean up Cargo.toml formatting 2025-05-23 10:23:36 +02:00
Timo 6d5ad4eddc feat(widget-driver): Add to-device support
The widget Driver should be able to send and receive to-device events.
This is useful for element call encryption keys.

This PR focusses on the widget driver and machine logic. To
send/communicate the events from the widget to the driver.

It skips any encryption logic. Some of the encryption logic will be part
of crypto crate and the code in the widget driver crate should be kept
minimal once the crypto crate is ready.

---------

Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-05-22 13:38:28 +02:00
Yousef Moazzam ec638e017b test: remove import of unused sync_timeline_event! macro in shield test 2025-05-22 12:46:56 +02:00
Yousef Moazzam 8f693f4615 test: create plain text room event with EventFactory 2025-05-22 12:46:56 +02:00
Jorge Martín fe8f77ed93 refactor: when leaving an Invited room also forget it
This behaviour was added only at the `RoomPreview::leave` method, but since we're slowly moving away from it we should move the forget action to the `Room::leave` method instead
2025-05-22 12:07:19 +02:00
Benjamin Bouvier 5c6238f132 chore(deps): bump wasm-bindgen-test 2025-05-22 11:50:13 +02:00
Benjamin Bouvier a5b932d086 chore(base): rename config file to config.toml
There was a warning in the console about this, when running the
following: cargo xtask ci wasm-pack matrix-sdk-base
2025-05-22 11:50:13 +02:00
Benjamin Bouvier b0e8b8a532 chore(deps): bump web-sys 2025-05-22 11:50:13 +02:00
Benjamin Bouvier 2984030f90 chore(ffi): configure the sentry dependency differently on android
On Android, we should use rustls instead of native-tls; this requires
unsetting the default features of the `sentry` crate, and specifying
them by hand instead.

For consistency, I've done the same for the non-android sentry
dependency.
2025-05-22 11:50:13 +02:00
Benjamin Bouvier a3c82e9087 chore(deps): bump reqwest to 0.12.15 2025-05-22 11:50:13 +02:00
Benjamin Bouvier 56082f93d0 chore(sdk): forward "database is busy" errors to sentry 2025-05-22 11:50:13 +02:00
Benjamin Bouvier c8474511a7 feat(ffi): add support for sentry logging 2025-05-22 11:50:13 +02:00
Ivan Enderlin 1348525447 test(base): Fix a test. 2025-05-21 17:26:52 +02:00
Ivan Enderlin 56f9b2d9f6 chore(base): Rename rooms to room.
This patch renames the `rooms` module into `room`. It contains a single
kind of `Room`.
2025-05-21 17:26:52 +02:00
Ivan Enderlin b18680b853 test(base): Use SystemTime from web_time for Wasm. 2025-05-21 17:26:52 +02:00
Ivan Enderlin 72b2763dad chore(base): Run rustfmt from nightly. 2025-05-21 17:26:52 +02:00
Ivan Enderlin 7278a36704 chore(base): Move Room::get_member_hints in the display_name module.
This patch moves the `Room::get_member_hints` method inside the newly
created `display_name` module. That way it is isolated from the rest of
the codebase.
2025-05-21 17:26:52 +02:00
Ivan Enderlin bd9a895089 chore(base): Move Room::*room_call* methods in the new call module.
This patch moves the `Room::has_active_room_call` and
`Room::active_room_call_participants` methods into the new `call`
module. This patch also moves the associated tests in this new module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin a71e7923e0 chore(base): Move the Room::MAX_ENCRYPTED_EVENTS constant in latest_event.
This patch moves the declaration of the `Room::MAX_ENCRYPTED_TESTS`
constants in the `latest_event` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 2433e91a6c chore(base): Move the m.room.create wrapper types in the new create module.
This patch moves the `m.room.create` wrapper types, aka
`RoomCreateWithCreatorEventContent` type and siblings, in the new
`create` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 48d2a1543c chore(base): Move code inside the same module.
This patch puts tests at the end of the file.
2025-05-21 17:26:52 +02:00
Ivan Enderlin bcd75362f7 chore(base): Move Room::*knock* methods into the new knock module.
This patch moves the `Room::*knock*` methods into the new `knock`
module.

The idea is to group API by “theme” to get smaller modules and more
organised code.
2025-05-21 17:26:52 +02:00
Ivan Enderlin f4488e42a2 chore(base): Move Room::state and sibling types into new state module.
This patch moves the `Room::state` method, along with the ``RoomState`
and `RoomStateFilter` types into the new `state` module. This patch also
moves the tests in this new module.

The idea is to group API by “theme” to get smaller modules and more
organised code.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 3d13b60b68 chore(base): Merge rooms::normal into rooms.
This patch merges the `rooms/normal.rs` file into `rooms/mod.rs`.

The name `normal` is present for historical reasons that no longer stand
today. This is no needed anymore. Let's simplify the modules.
2025-05-21 17:26:52 +02:00
Ivan Enderlin f1c54d1e27 chore(base): Move tests in the correct module.
This patch move tests about the `RoomDisplayName` in the newly created
`display_name` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 27edb163e9 chore(base): Move tests in the correct module.
This patch moves tests about `RoomNotableTags` in the newly created
`tags` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 6d9d202701 chore(base): Move Room::*latest_event*() inside the new latest_event module.
This patch moves everything related to the `Room` latest event API
inside the new `latest_event` module. This patch also moves the tests.
The idea is to get a smaller `rooms::normal` module, and to clarify the
code by grouping `Room` APIs by “theme”.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 61d99ef709 chore(base): Move everything related to Room members in members.
This patch moves all types and methods used by or implemented on `Room`
inside the existing `members` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 6f031261d5 chore(base): Move Room::is_favourite and ::is_low_priority into new tags module.
This patch moves the  Room::is_favourite` and `::is_low_priority`, along
with the `RoomNotableTags` into the new `tags` module. This patch also
moves the tests in this new module.

The idea is to group API by “theme” to get smaller modules and more
organised code.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 4a57044680 chore(base): Move Room::encryption_* mtehods into new encryption module.
This patch moves the `Room::encryption_state()` and
`Room::encryption_settings()` methods, and the sibling types, into a new
`encryption` module. This patch also moves the tests in this new module.
The idea is to group `Room` API by “theme” in modules, to clarify the
code.
2025-05-21 17:26:52 +02:00
Ivan Enderlin d5c0e209fc chore(base): Move Room::display_name() and siblings into new display_name module.
This patch moves the `Room::display_name()` method, and all the
siblings, into a new `display_name` module. It includes types like
`RoomDisplayName`, `UpdatedRoomDisplayName`, `RoomSummary` etc. This
patch also moves the tests in this new module. The idea is to group
`Room` API by “theme” in modules, to clarify the code and to make the
big `rooms::normal` module smaller.
2025-05-21 17:26:52 +02:00
Ivan Enderlin 737654549b chore(base): Move the Room's room info related methods and tests in room_info.
This patch moves the `Room::subscribe_info`, `::clone_info` and
`::set_room_info` methods from the `rooms::normal` module to the
`rooms::room_info` module. This patch also moves the tests related to
room info inside `room_info`.
2025-05-21 17:26:52 +02:00
Ivan Enderlin cc34603864 chore(base): Move RoomInfoNotableUpdate* inside room_info.
This patch moves the `RoomInfoNotableUpdate` and
`RoomInfoNotableUpdateReasons` types inside the `room_info` module.
2025-05-21 17:26:52 +02:00
Ivan Enderlin bad82ccd42 doc(ui): Explain why m.room.create is necessary.
This patch explains why having `m.room.create` is necessary. It's not
only about the room previews, but also about the room versions, and thus
the tombstoned rooms.
2025-05-21 17:26:52 +02:00
Ivan Enderlin d71cd68a90 refactor(base): Rename RoomInfo::version to data_format_version.
This patch renames the `RoomInfo::version` field to
`data_format_version` to avoid all possible confusion with the
`room_version` (from `m.room.create`).
2025-05-21 17:26:52 +02:00
Benjamin Bouvier 726111b073 refactor(timeline): group all the parameters for a remote echo in TimelineAction::from_content 2025-05-21 16:59:41 +02:00
Benjamin Bouvier 298fa7d5d2 feat(timeline): pass the encryption info for a bundled edit when constructing the edit aggregation
This should properly update shields in case the bundled edit was
correctly decrypted.
2025-05-21 16:59:41 +02:00
Benjamin Bouvier 4a627baae8 feat(timeline): indicate when the replied-to event is not a standalone item
If the replied-to event is an aggregation, the
`RepliedToEvent::try_from_timeline_event` will now return `Ok(None)`,
and the caller may handle this as they please.

In the FFI layer, this will be filled with an error message indicating
that the event is unsupported.
2025-05-21 15:52:31 +02:00
Benjamin Bouvier 560e33c27b refactor(timeline): deduplicate parsing of events when making a RepliedToEvent 2025-05-21 15:52:31 +02:00
Stefan Ceriu d84cf0614d change(ffi): return errors if the client or utd delegates were already set 2025-05-21 16:38:20 +03:00
Stefan Ceriu 3f1bc2591e chore(ffi): report an error if the timeline is set to report UTDs but the hook manager isn't configured 2025-05-21 16:38:20 +03:00
Stefan Ceriu 8e83b724da chore(ffi): rename the utd_hook to utd_hook_manager 2025-05-21 16:38:20 +03:00
Stefan Ceriu 767b10f5e2 change(ffi): remove now unused ClientDelegate did_refresh_tokens callback (dropped in favor of the ClientSessionDelegate) 2025-05-21 16:38:20 +03:00
Stefan Ceriu 4e2b5562f1 change(ffi): use a OnceLock to guard against multiple settings of the ClientDelegate 2025-05-21 16:38:20 +03:00
Stefan Ceriu 8ef471b492 chore(multiverse): simplify marking a room as read 2025-05-21 16:38:20 +03:00
Stefan Ceriu de6998dbe0 change(ffi): move the UTD delegate directly to the client for ease of use 2025-05-21 16:38:20 +03:00
Stefan Ceriu e48b1f6056 change(ffi): stop retrieving room list last messages from through the timeline
As per the plan defined in #4718:

```
the room_list_service::room::RoomInner shouldn't make use of its inner timeline;
it's only used in a direct getter, or to compute the latest room event, but it's not working
as intended, since local echoes aren't properly displayed in the room list.
This non-working feature can be removed, in favor of #4112
```
2025-05-21 16:38:20 +03:00
Stefan Ceriu 7074110780 change(ffi): remove the UTD manager from the sync service, room list service and room list items 2025-05-21 16:38:20 +03:00
Stefan Ceriu c90d272374 change(ffi): add timeline configuration option for reporting UTDs 2025-05-21 16:38:20 +03:00
Stefan Ceriu e6dc203c4d change(ffi): pass the client utd manager down into the timeline builder 2025-05-21 16:38:20 +03:00
Stefan Ceriu 195ee35eea change(ffi): move the utd hook from the sync service to the client 2025-05-21 16:38:20 +03:00
Stefan Ceriu 70122a4407 change(ui): stop relying on a room's stored timeline for marking it as read 2025-05-21 16:38:20 +03:00
Stefan Ceriu f2ca0697af chore(ui): remove the timeline's builder method and make the builder's constructor public 2025-05-21 16:38:20 +03:00
Kévin Commaille 9f196be2f6 Fix doc link
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 254d86bc45 refactor(ui): Set room as variable instead of calling self.room() every time
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille a8dcea6931 refactor(sdk): Make Room::set_unread_flag() a no-op if the unread flag already has the wanted value
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille eed04ecdb3 chore: Add changelog entries for #5055
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille c131e0cfe5 test(ui): Add tests for unsetting the unread flag when sending receipts
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 5f01c72a48 refactor(ui): Use the new mock endpoints in tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille bf7d5e7841 feat(ui): Unset the unread flag when sending unthreaded receipts in Timeline
Updates the unread flag or the room in `Timeline::send_single_receipt()`
and `Timeline::send_multiple_receipts()` if the room is marked as unread
and the receipts are unthreaded.

Updates it also in `Timeline::mark_as_read()`, even if there is no
latest event ID.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille dff1886015 test(sdk): Add tests for unsetting the unread flag when sending receipts
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 14a73dc932 refactor(sdk): Use new mock endpoints for tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 51be581d48 feat(test): Add RoomAccountDataTestEvent::MarkedUnread
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 0a41febe15 test(sdk): Mock endpoints for sending receipts and room account data
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Kévin Commaille 63eb429843 feat(sdk): Unset the unread flag when sending unthreaded receipts in Room
Updates the unread flag or the room in `Room::send_single_receipt()` and
`Room::send_multiple_receipts()` if the room is marked as unread and the
receipts are unthreaded.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-21 13:51:48 +02:00
Mauro 91815ab678 feat(bindings): added APIs to get the media preview config from the store 2025-05-21 12:15:17 +02:00
Benjamin Bouvier a98f71ed0c feat(timeline): handle live aggregations on non-live timelines (#5060)
This makes it possible to handle reactions/redactions/edits/etc. on
non-live timelines. As a result, the pinned and focused timelines will
now get live reactions/redactions and so on. This makes it possible to
also have the thread timelines handle those live events, although it's
unclear how it will pane out in the end, when the event cache is also
involved.
2025-05-20 18:03:57 +02:00
Benjamin Bouvier 6f8b744c24 refactor(timeline): address review comments 2025-05-20 13:50:02 +02:00
Benjamin Bouvier 5e9c76f476 refactor(timeline): avoid cloning of relates_to for room messages
This can be done by splitting the handling of the msgtype/mentions from
the handling of the `relates_to` field, requiring a few API changes here
and there.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier 4fd0f2a32c refactor(timeline): slightly optimize flow for saving a bundled edit
We only need the edit_json if we're about to save the edit aggregation.
Likewise, if there's no current event id (i.e. the event being handled
is a local echo), then we don't need to even try to extract anything
from the bundle information.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier 74d5d6e265 refactor(timeline): group extracting the reply and thread root
The poll events's `related_to` field is a `RelationWithoutReplacement`,
while the two others are `Relation<C>`, where `C` is the event content
type (in case it was replacement). As a matter of fact, we try
converting the `Relation<C>` into a `RelationWithoutReplacement` (which
unfortunately requires cloning, which is wasteful if the relation was a
replacement indeed), and then we can use a single function to extract
the reply information and thread root info, for all three.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier caa53be00e optimize(timeline): find the position of an event by starting from the end 2025-05-20 13:50:02 +02:00
Benjamin Bouvier b3086accd5 refactor(timeline): hey, i can actually remove this pending_edits field now 2025-05-20 13:50:02 +02:00
Benjamin Bouvier f010587201 chore: make clippy happy
bleh.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier 2af751d8c6 optimize(timeline): avoid one clone if a new item has pending aggregations 2025-05-20 13:50:02 +02:00
Benjamin Bouvier f36f9915d1 refactor(timeline): simplify a few functions as a result of not providing the edit json ahead of time 2025-05-20 13:50:02 +02:00
Benjamin Bouvier e8f8e7bfd6 fix(timeline): properly update encryption info upon edit
To be fair, this is a regression from a previous commit in this PR.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier dbe23777a0 refactor(timeline): use the aggregations manager for handling edits 2025-05-20 13:50:02 +02:00
Benjamin Bouvier b7191e3dc2 refactor(timeline): have Aggregations::try_remove_aggregation also take care of updating the item 2025-05-20 13:50:02 +02:00
Benjamin Bouvier 56ce93ce72 refactor(timeline): simplify a bit mark_aggregation_as_sent by having it do more work
This avoids another struct definition, and items are going to be needed
for edits anyways.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier 9cbc674cf2 optimize(timeline): don't eagerly clone an EventTimelineItem before applying an aggregation onto it 2025-05-20 13:50:02 +02:00
Benjamin Bouvier 16fe53c40d refactor!(timeline): have TimelineItem::reactions() return an Option<&ReactionsByKeyBySender> instead of owned non-optional 2025-05-20 13:50:02 +02:00
Benjamin Bouvier 1b581d52e8 refactor(timeline): rename Aggregations::apply to Aggregations::apply_all
It applies *all* the stashed aggregations for a given event timeline
item.
2025-05-20 13:50:02 +02:00
Benjamin Bouvier dd6604e9f3 feat(timeline): handle redaction in the pending aggregations
This allows having a redaction event come before the related event, and
still handle it when the redacted event shows up.
2025-05-20 13:50:02 +02:00
Yousef Moazzam f173510482 test: Replace sync_timeline_event! with EventFactory for beacon events in room integration tests 2025-05-20 12:31:23 +02:00
Benjamin Bouvier 905d9b9aba refactor(timeline): don't reload a pinned event timeline that hasn't changed since the previous time 2025-05-20 10:49:08 +02:00
Benjamin Bouvier 810056cd4d refactor(timeline): add logging for the pinned events task 2025-05-20 10:49:08 +02:00
Richard van der Hoff 4d2c261ff8 crypto: rewrite check_sender_trust_requirement to use VerificationState (#5044)
There are two reasons for this.

Firstly. we've already done a bunch of work to map `SenderData` into a
`VerificationState`, and the decision tree from `VerificationState` to
allow/reject is simpler than going from `SenderData`, even if we have to
fudge it a bit to get the "legacy" flag. (Note that it allows us to get
rid of an `unreachable!` panic.)

Secondly, `VerficationState` represents the state of an *event*, whereas
`SenderData` is about the session as a whole. A session can be fine,
whilst events (claiming to be) encrypted with it can be suspect. What we
want here is to check a specific message. Currently, this doesn't make
any functional difference, but conceptually it's cleaner to check the
`VerificationState`.

Note that there are a bunch of tests for this method in
`matrix-sdk-crypto/src/machine/tests/decryption_verification_state.rs`,
called `test_decryption_trust_requirement`.
2025-05-19 18:35:21 +01:00
Ivan Enderlin d4626835bb chore(base): Fix formatting. 2025-05-19 16:09:32 +02:00
Ivan Enderlin f684883902 chore(base): Move RoomInfo inside another module.
This patch moves the `RoomInfo` type and its implementations inside the
`room_info` module.
2025-05-19 16:09:32 +02:00
Ivan Enderlin cf6316e290 chore(base): Move BaseRoomInfo into its own module.
This patch extracts the `BaseRoomInfo` type and its implementations
inside its own module.
2025-05-19 16:09:32 +02:00
Ivan Enderlin 12caf12d8a doc(base): Fix a typo. 2025-05-19 16:09:32 +02:00
Ivan Enderlin 9809e1b53c doc(sdk): Fix a typo in an inline comment. 2025-05-19 15:58:58 +02:00
Ivan Enderlin 80b7eed14b task(sdk): Fix warnings error without e2e-encryption. 2025-05-19 15:58:58 +02:00
Ivan Enderlin 6980dc5628 doc(sdk): Add #5047. 2025-05-19 15:58:58 +02:00
Ivan Enderlin 9366bc85e9 refactor(sdk): Remove FrozenSlidingSync.
This patch removes `FrozenSlidingSync`. Its unique field is supposed to
be stored in the crypto store.
2025-05-19 15:58:58 +02:00
Ivan Enderlin 0c193500d2 refactor(sdk): Remove SlidingSyncRoom \o/.
This patch FINALLY removes `SlidingSyncRoom`, youhou!
2025-05-19 15:58:58 +02:00
Ivan Enderlin 6c68cef6e0 refactor(sdk): Remove SlidingSync::rooms.
This patch removes the `SlidingSync::rooms` field. A cascade of removal
happens, and many part of the code is simplified. The most notable is
`FrozenSlidingSync`.
2025-05-19 15:58:58 +02:00
Ivan Enderlin 5a7a42cde3 refactor(sdk): Stop maintaining SlidingSync::rooms.
This patch stops maintaining/updating `SlidingSync::rooms`. The goal is
to remove `SlidingSyncRoom`.
2025-05-19 15:58:58 +02:00
Ivan Enderlin 0ad88842cc refactor(sdk): Remove SlidingSync::get_rooms and get_all_rooms.
This patch removes `SlidingSync::get_rooms` and `get_all_rooms`. The
goal is to remove `SlidingSyncRoom`.
2025-05-19 15:58:58 +02:00
Ivan Enderlin 7c0f7f4715 refactor(sdk): Remove SlidingSync::get_number_of_rooms.
This patch removes `SlidingSync::get_number_of_rooms`. The goal is to
remove `SlidingSyncRoom`.
2025-05-19 15:58:58 +02:00
Ivan Enderlin 52c38ec44d refactor(base): Remove SlidingSync::get_room.
This patch removes the `SlidingSync::get_room` method. The goal is to
remove `SlidingSyncRoom`.
2025-05-19 15:58:58 +02:00
Valere Fedronic 21de891ea5 feat(sdk): Add the encrypt_and_send_raw_to_device method
This method allows users to encrypt and send custom to-device events to a set of devices of their choosing.
2025-05-19 11:20:25 +00:00
Mauro 154f29e5a0 feat(sdk): implement and observe MSC4278 config value
This patch
- Updates Ruma to use the improved MediaPreviewConfig event type that
also supports a `Default` for the content type
- Implemented a way to observe the stable and unstable values of the
event and return the used one accordingly, if no one is present the
default will be used
- Set the value (will only use unstable type for now)
2025-05-19 12:35:50 +02:00
Ivan Enderlin 7f07731471 doc(changelog): Add #5054. 2025-05-19 11:42:09 +02:00
Ivan Enderlin 1a0a4d7905 chore(base): Remove RoomInfo::prev_room_state.
This patch removes the `RoomInfo::prev_room_state` field, along with the
`RoomInfo::prev_state` method.

This data was introduced during the knocking project but was never used,
and is not used nowadays. Let's remove it.
2025-05-19 11:42:09 +02:00
Jonas Platte e3bcd4d5b2 chore: Upgrade dirs to 6.0 in examples 2025-05-19 09:23:02 +02:00
Timo ea4c9a41f8 feat(widgets): Add the controlledMediaOutput url parameter to the VirtualElementCallWidgetOptions.
This is used to configure EC on devices that need to control media outputs on their own (android, ios).
If set, EC will display a list of devices provided by the app.
2025-05-16 15:48:49 +02:00
Ivan Enderlin ac2c7f431c feat(ui): Add m.room.tombstone to the room list required_state.
This patch adds the `m.room.tombstone` state event to the list of
events in `required_state` used by the `RoomListService`. The goal is to
offer the possibility for the consumers to know whether a room has been
tombstoned or not.
2025-05-16 15:11:11 +02:00
Ivan Enderlin c0d6e87c99 task(base): Fix conflicts with a previous patch. 2025-05-16 14:43:45 +02:00
Ivan Enderlin 6162600bda refactor(base): Remove &mut Context argument from response processors when unused.
This patch removes the `_context: &mut Context` argument from response
processors when it's unused.
2025-05-16 14:43:45 +02:00
Ivan Enderlin 1187539ea4 chore(base): Remove the useless PreviousEventsProvider. 2025-05-16 14:43:45 +02:00
Ivan Enderlin 2649587d2f refactor(sdk): Use the Event Cache for read_receipts::compute_unread_counts.
The `read_receipts::compute_unread_counts` function needs the _previous
events_ to compute the read receipt correctly. These previous events
were store in `SlidingSyncRoom::timeline_queue`. Since the removal of
`timeline_queue` in the previous patches, this patch uses the Event
Cache to fetch them. It only uses events that are loaded in memory.
This is as correct as the prior behaviour, even this is still incorrect
since it doesn't back-paginate to get a better view. This is for
later. The goal of this patch is to restore the same behaviour, without
`timeline_queue`.

The main problem is that read receipts are computed in
`matrix-sdk-base`, and that the Event Cache lives in `matrix-sdk`. Thus,
we change the `SlidingSyncResponseProcessor` to handle read receipt
in particular.

The
`matrix_sdk_base::response_processors::rooms::msc4186::extensions::dispa
tch_ephemeral_events` function has been split in
two methods `dispatch_typing_ephemeral_events`, and
`dispatch_receipt_ephemeral_event_for_room`. The workflow has been a
little bit redesigned to fit in the new `SlidingSyncResponseProcessor`
constraints.

This patch moves one test from `matrix-sdk-base` into `matrix-sdk`,
because to compute the read receipt, the Event Cache must be
enabled/listening to sync updates.
2025-05-16 14:43:45 +02:00
Ivan Enderlin 68651aac1f feat(sdk): Add RoomEventCache::events to avoid ::subscribe.
`RoomEventCache::subscribe` returns the set of events + the
`RoomEventCacheListener`. However, creating this listener isn't
cheap, especially dropping it. That's why this patch creates
`RoomEventCache::events` to replace `subscribe` when the listener is
not necessary.
2025-05-16 14:43:45 +02:00
Ivan Enderlin 2c8f48fabb doc(base): Fix inline comment typos. 2025-05-16 14:43:45 +02:00
Ivan Enderlin c426c03624 refactor(sdk): Remove timeline and prev_batch from SlidingSyncRoom. 2025-05-16 14:43:45 +02:00
Ivan Enderlin eeaa091024 chore(ffi): Justify the allow(clippy::large_enum_variant). 2025-05-16 14:27:49 +02:00
Ivan Enderlin 7ef962f931 chore(labs): Allow clippy::large_enum_variant in multiverse.
This is development-, debug-oriented tool. Let's allow
`clippy::large_enum_variant` for the moment.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 8480e0fc55 chore(ui): Allow clippy::large_enum_variant on TimelineAction.
This enum is large, but it's used in a short period of time, not
collected somewhere, so it's safe to accept a large size here.
2025-05-16 14:27:49 +02:00
Ivan Enderlin bf5e0124ab refactor(ui): Reduce the size of NotificationEvent.
This patch reduces the size of `NotificationEvent` from 576 bytes to
16 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin d56ad64cc2 refactor(ui): Reduce the size of NotificationStatus.
This patch reduces the size of `NotificationStatus` from 216 bytes to
16 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin ab3f22c212 refactor(sdk): Reduce the output size of get_header.
This patch reduces the size of the output's `Result::Err` variant of
`get_header` from 160 bytes to 8 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin e41dbd6300 refactor(sdk): Reduce size of HttpError.
This patch reduces the size of `HttpError` from 160 bytes to 24 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 29a8556f10 refactor(sdk): Reduce size of ReplyContent.
This patch reduces the size of `ReplyContent` from 448 bytes to
16 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 8d0d920808 refactor(sdk): Replace iter().any() by contains().
This is faster for scalars, but it falls back to a regualar
`iter().any()` for other types. It's the same, but at least Clippy
doesn't complain.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 1cdc9ff6e4 refactor(sdk): Use IoError::other.
This patch replaces `IoError::new(IoErrorKind::Other, …)` by
`IoError::other(…)`.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 9541123fcf refactor(crypto): Reduce the size of SasState.
This patch reduces the size of `SasState` from 288 bytes to 88 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin ed4b789f87 refactor(crypto): Reduce the size of OutgoingContent.
This patch reduces the size of `OutgoingContent` from 160 bytes to
24 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 18a3c37554 refactor(crypto): Reduce sizes of Verification and VerificationRequestState.
This patch reduces the sizes of `Verification` from 376 bytes to
16 bytes, and `VerificationRequestState` from 424 bytes to 96 bytes.

It also reduces the size of a couple of other types in the same vain.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 92b4b03a8d refactor(crypto): Reduce the size of OutgoingContent and RoomMessageRequest.
This patch reduces the sizes of `OutgoingContent` from 464 bytes to
160 bytes, and `RoomMessageRequest` from 480 bytes to 40 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 192c50dcad refactor(crypto): Reduce the size of OutgoingVerificationRequest.
This patch reduces the size of `OutgoingVerificationRequest` from
480 bytes to 64 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 7b34eaabe5 refactor(crypto): Reduce the size of AnyOutgoingRequest.
This patch reduces the size of `AnyOutgoingRequest` from 488 bytes to
72 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 2eb4278835 refactor(crypto): Reduce the size of RoomIdentityChange.
This patch reduces the size of `RoomIdentityChange` from 576 bytes to
72 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 293d4ee08c refactor(crypto): Reduce the size of MaybeEncryptedRoomKey.
This patch reduces the size of `MaybeEncryptedRoomKey` from 336 bytes
to 32 bytes.
2025-05-16 14:27:49 +02:00
Ivan Enderlin 87066a127e refactor(crypto): Use IoError::other.
This patch uses `IoError::other(…)` as a shortcut of
`IoError::new(ErrorKind::Other, …)`.
2025-05-16 14:27:49 +02:00
Kévin Commaille 4847a3135b feat(base-sdk): Ignore marked_unread room account data with unstable prefix after seeing one with stable prefix
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-16 09:55:02 +02:00
Kévin Commaille af02e0c472 feat(sdk): Send stable m.marked_unread room account data
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-16 09:55:02 +02:00
Stefan Ceriu da2dda0e45 fix(ui): populate the thread_root and in_reply_to fields for stickers and polls
They have never been set and there was no way of telling if stickers and polls belong on a thread or come in reply of any other message.

This patch also exposes methods for setting these relations on the event factory level.
2025-05-15 16:45:34 +03:00
Benjamin Bouvier 69b8878890 fix(sdk): mark rooms joined with join_room_by_id or join_room_by_id_or_alias as DMs if needs be
This moves the logic from `Room::join()` to these two methods. This is
isofunctional, because `Room::join()` does call into
`Client::join_room_by_id()` internally.
2025-05-15 14:19:31 +02:00
Damir Jelić 7893e55a8d refactor(crypto): Make the room key importing logic more generic 2025-05-15 12:23:23 +02:00
Damir Jelić c0e45a2e0f refactor(widgets): Rename the then() function into add_response_handler() (#5037)
The then() function can be used with booleans and futures to execute a
piece of code if the boolean is true or once the future is completed.

In contrast, the then() function in the widget driver is not executed
immediately. Instead it only adds a callback that is later on executed
by the widget driver.
2025-05-14 16:16:04 +02:00
Stefan Ceriu 13a65c8dfe feat(ui): add new Thread timeline focus mode and associated events loader (#5032)
… that allows building a timeline instance specific to a particular
thread root.

Creating a timeline in this mode will start by backpaginating root event
relations with `num_events` and automatically insert the thread root
event when reaching the end. It will include
`RelationsOfType(RelationType::Thread)` but also recurse over the
retrieved events to fetch reactions.
It will not however react to new events received over sync or that the
user sends (for now).

This patch will also help incrementally deliver the upstream client
support for creating such a timeline.

Part of #4833 (meta #4869).
2025-05-14 14:14:29 +00:00
Kévin Commaille 36667c1298 chore: Get rid of cargo-deny errors due to new advisories
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-05-14 15:42:46 +02:00
Benjamin Bouvier e4f2299785 refactor(timeline): get rid of FullEventMeta and replace it with EventMeta + function parameters 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 81a2679bb8 refactor(timeline): only compute the TimelineEventContext when it's needed 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 9087263da4 refactor(timeline): rewrite how failed-to-parse items are added 2025-05-14 13:30:24 +02:00
Benjamin Bouvier d58111fa04 refactor(timeline): isolate computation of should_add in its own function 2025-05-14 13:30:24 +02:00
Benjamin Bouvier ab87ea5770 refactor(timeline): simplify computation of should_add a bit 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 6b62b41a60 refactor(timeline): get rid of the return value of find_item_and_apply_aggregation
As it's now unused.
2025-05-14 13:30:24 +02:00
Benjamin Bouvier b1f088277d refactor(timeline): get rid of HandleEventResult 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 277eae75d9 refactor(timeline): make item_added a local variable instead of deeply-stored context 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 20559e3f2c refactor(timeline): get rid of unused HandleEventResult::items_updated 2025-05-14 13:30:24 +02:00
Benjamin Bouvier 341f9c267d feat(state store): enable the busy timeout to automatically retry operations on busy dbs
Under some very particular circumstances, the "database is locked" error
can still happen, even in WAL mode, even if the database connection is
not being upgraded from a read transaction to a write transaction.

We *think* this might be the reason behind errors like
github.com/element-hq/element-x-ios/issues/3582, so we're enabling the
sqlite busy_timeout, which will retry the operation after a short sleep,
until the busy timeout is being hit.
2025-05-14 13:27:54 +02:00
Timo af3039abde docs(WidgetDriver): Add module documentation
Co-authored-by: Robin <robin@robin.town>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-05-14 11:42:55 +02:00
Benjamin Bouvier 4d027ec405 doc: add a changelog entry for the persistent storage of the event cache 2025-05-13 15:50:21 +02:00
dependabot[bot] 3cd64ac03b chore(deps): Bump tokio from 1.43.0 to 1.43.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.43.0 to 1.43.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.43.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.43.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 14:58:30 +02:00
Jorge Martín 44e103c0e3 feat(ffi): expose ffi::RoomInfo::tombstone
This replaces `ffi::RoomInfo::is_tombstoned`, including the needed extra info for the migration UI.
2025-05-13 14:41:41 +02:00
dependabot[bot] 7d992d1af8 chore(deps): Bump ring from 0.17.8 to 0.17.14
Bumps [ring](https://github.com/briansmith/ring) from 0.17.8 to 0.17.14.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-version: 0.17.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 14:37:12 +02:00
Benjamin Bouvier ac42953524 doc(timeline): add extra documentation for HandleAggregationKind 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 06759359af refactor(timeline): simplify lightly computation of should_add_new_items for local events 2025-05-13 14:15:00 +02:00
Benjamin Bouvier a3a30885c0 refactor(timeline): rename TimelineEventKind into TimelineAction and add comments 2025-05-13 14:15:00 +02:00
Benjamin Bouvier b448c4ac39 refactor(timeline): get rid of TimelineEventKind::Message 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 6a874fec2d refactor(timeline): use TimelineEventKind::AddItem for most room messages 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 998019b8b8 refactor(timeline): use TimelineEventKind::AddItem for poll starts 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 5a0b33fcd1 refactor(timeline): use TimelineEventKind::AddItem for call invite/notify/sticker 2025-05-13 14:15:00 +02:00
Benjamin Bouvier ec55d7cb58 refactor(timeline): use TimelineEventKind::HandleAggregation for poll edits/responses/ends 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 706f78d5b3 refactor(timeline): use TimelineEventKind::HandleAggregation for edits 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 1218cb4c28 refactor(timeline): use TimelineEventKind::HandleAggregation for redactions 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 31bd8a3587 refactor(timeline): introduce TimelineEventKind::HandleAggregation for non-items
This variant will be used to cause side-effects to existing timeline
items, because the event they relate to is for an aggregation
(edit/reaction/etc.).

This is used here for reactions.
2025-05-13 14:15:00 +02:00
Benjamin Bouvier 9e70cc5dde refactor(timeline): add a small helper function to create TimelineEventKind::AddItem items 2025-05-13 14:15:00 +02:00
Benjamin Bouvier d78a4927fd refactor(timeline): use TimelineEventKind::AddItem for room member and state events 2025-05-13 14:15:00 +02:00
Benjamin Bouvier bf246b6c09 refactor(timeline): use TimelineEventKind::AddItem for redacted messages 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 5da2235973 refactor(timeline): use TimelineEventKind::AddItem for UTDs 2025-05-13 14:15:00 +02:00
Benjamin Bouvier a9de4709f9 refactor(timeline): use TimelineEventKind::AddItem to insert failed-to-parse items 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 3b8d7ffacd refactor(timeline): introduce a new TimelineEventKind::AddItem variant
The intent is that the `TimelineItemContent` can be constructed in a
single place, avoiding the need to create it in multiple places.
2025-05-13 14:15:00 +02:00
Benjamin Bouvier 3eee5a4a92 refactor(timeline): bury one use of TimelineEventKind 2025-05-13 14:15:00 +02:00
Benjamin Bouvier 6fed5747bb refactor(timeline): remove another spurious is_own_event
Once again, this information is available in the `TimelineMetadata`.
2025-05-13 14:15:00 +02:00
Benjamin Bouvier 442094a725 refactor(timeline): remove spurious FullEventMeta::is_own_event
This information can be accessed via the `TimelineMetadata::own_user_id`
field, which is instantiated only once.
2025-05-13 14:15:00 +02:00
Benjamin Bouvier de66047eee refactor(event cache): simplify Deduplicator so it only operates with the store impl 2025-05-13 10:17:21 +02:00
Benjamin Bouvier 48da03a148 refactor(event cache): don't make the store optional in the event cache 2025-05-13 10:17:21 +02:00
Benjamin Bouvier 13a2a8757e feat(event cache): enable storage by default \o/ 2025-05-13 10:17:21 +02:00
Benjamin Bouvier 1d901ec12a refactor(room list): get rid of the sliding sync in room_list_service::Room
This was only used to retrieve events cached in the timeline_queue().
2025-05-13 10:17:21 +02:00
Benjamin Bouvier 7115203a90 feat(event cache): get rid of add_initial_events() entirely 2025-05-13 10:17:21 +02:00
Jorge Martín 68fb60f223 test(ui): add more tests for fetching invite notifications in sliding sync 2025-05-13 09:10:44 +02:00
Jorge Martín 1f064fe474 test(ui): migrate notification fetching tests to use the batched methods
Also add one for the sliding sync + /context case
2025-05-13 09:10:44 +02:00
Jorge Martín 008c6f6d6c feat(ui): allow retrieving push notification events in batches 2025-05-13 09:10:44 +02:00
Yousef Moazzam 1afad3ab78 test: remove import of unused sync_timeline_event! macro 2025-05-13 09:04:55 +02:00
Yousef Moazzam d1802086ad test: create room canonical alias events with EventFactory 2025-05-13 09:04:55 +02:00
Yousef Moazzam cabe9632af test: add room canonical alias event method to EventFactory 2025-05-13 09:04:55 +02:00
Ivan Enderlin 08aa9c8614 doc(ui): Fix a typo in a comment. 2025-05-12 17:15:43 +02:00
Ivan Enderlin 831bba5cf0 test(ui): Add tests for push_local and push_date_divider. 2025-05-12 17:15:43 +02:00
Ivan Enderlin d727111a51 doc(ui): Add #5000 in the CHANGELOG.md. 2025-05-12 17:15:43 +02:00
Ivan Enderlin 8d785b762e chore(ui): Make Clippy happy. 2025-05-12 17:15:43 +02:00
Ivan Enderlin ad4ae230d5 refactor(ui): EventHandler uses regions to improve the code and avoid bugs.
This patch updates `EventHandler` to use the correct regions where
appropriate, thus reducing the complexity of the code, and removing
classes of bugs.

In the case of `Flow::Remote { position: TimelineItemPosition::At { …
}}`, we no longer need to skip the local timeline items, and to handle
the presence of the `TimelineStart` timeline item. The code is less
complex.

In the case of `Flow::Remote { position: TimelineItemPosition::End { …
}}`, that's exactly the same at the previous case.

In the case of `recycle_local_or_create_item`, the `try_fold` approach
is replaced entirely with a simple `iter_locals_region`, reducing the
size of the comments explaining the code, reducing the complexity of the
code, and reducing the surface of bugs.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 54f7963152 refactor(ui): TimelineStateTransaction works on _remotes_ and _all_ regions.
This patch updates `TimelineStateTransaction` to work on the correct
regions, _remotes_ in one place, and all regions in another place.
2025-05-12 17:15:43 +02:00
Ivan Enderlin b59c0b671e refactor(ui): TimelineMetadata works on the _remotes_ region.
This patch updates `TimelineMetadata` to work on the _remotes_ region
only, excluding the _start_ and the _locals_ regions. It helps to reduce
the risk of inserting items in an incorrect regions.

This patch also removes on more `rfind_event_by_id` usage, which is
nice.
2025-05-12 17:15:43 +02:00
Ivan Enderlin c6453a4cb3 refactor(ui): ReadReceiptTimelineUpdate works on _remotes_ region.
This patch updates `ReadReceiptTimelineUpdate` to work on the _remotes_
region only, excluding the _start_ and the _lcoals_ regions. It helps
to reduce the risk of inserting a `ReadMarker` inside the _start_ or the
_locals_ regions.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 74bf699615 refactor(ui): DateDividerAdjuster works on _remotes_ and _locals_ regions.
This patch updates `DateDividerAdjuster` to work on _remotes_ and
_locals_ regions only, excluding the _start_ region. It helps to reduce
the risk of inserting a `DateDivider` inside the _start_ region.

This patch also uses the new `push_date_divider` method, which provides
a couple of invariants.
2025-05-12 17:15:43 +02:00
Ivan Enderlin e1f94bf9c4 feat(ui): Define _regions_ in the Timeline.
This patch defines a new concept in the `Timeline`: Regions.

The `ObservableItems` holds all the invariants about the _position_ of the
items. It defines three regions where items can live:

1. the _start_ region, which can only contain a single `TimelineStart`,
2. the _remotes_ region, which can only contain many `Remote` timeline
   items with their decorations (only `DateDivider`s and `ReadMarker`s),
3. the _locals_ region, which can only contain many `Local` timeline items
   with their decorations (only `DateDivider`s).

The `iter_all_regions` method allows to iterate over all regions.
`iter_remotes_region` will restrict the iterator over the _remotes_
region, and so on. These iterators provide the absolute indices of the
items, so that it's harder to make mistakes when manipulating the indices of
items with operations like `insert`, `remove`, `replace` etc.

Other methods like `push_local` or `push_date_divider` insert the items
in the correct region, and check a couple of invariants.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 44a0745110 chore(base): Move the bitflags dependency in the workspace. 2025-05-12 17:15:43 +02:00
Ivan Enderlin 94b76168e8 refactor(ui): Add ObservableItemsTransaction::has_local.
This patch implements the `has_local` method on
`ObservableItemsTransaction`, which is way faster than the previous the
previous solution which was to iterate over all items to find at least
one local timeline item.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 55ea80b485 refactor(ui): Add ObservableItemsTransaction::push_local.
This patch adds the `push_local` method on `ObservableItemsTransaction`
to add semantics and hardcode the invariant in a single place for the
different timeline items.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 4e501e88ee refactor(ui): Add ObservableItemsTransaction::push_timeline_start_if_missing.
This patch adds the `push_timeline_start_if_missing` method on
`ObservableItemsTransaction` to add semantics and hardcode the
invariant in a single place for the different timeline items.
2025-05-12 17:15:43 +02:00
Ivan Enderlin afc02781e9 test(ui): Add a regression test.
This patch adds a regression test ensuring [this bug][4976] cannot
happen anymore.

[4976]: https://github.com/matrix-org/matrix-rust-sdk/issues/4976
2025-05-12 17:15:43 +02:00
Ivan Enderlin c2072e1cc2 test(ui): Support index [$nth] --- date divider --- in assert_timeline_stream!. 2025-05-12 17:15:43 +02:00
Ivan Enderlin eef99b2679 test: Add assert messages in the assert_timeline_stream macro.
This patch improves the `assert_timeline_stream` macro by adding a bunch
of assert messages in case it fails.
2025-05-12 17:15:43 +02:00
Ivan Enderlin 581d54f65f fix(ui): Offset the timeline index in the presence of a TimelineStart.
This patch fixes the insertion of a new `TimelineItem` in the presence
of a `TimelineStart` that shifts/offsets the timeline index of 1.
2025-05-12 17:15:43 +02:00
dependabot[bot] 5f5ea69a32 chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 4168bb487d5b82227665ab4ec90b67ce02691741 to 480f49412651059a414a6a5c96887abb1877de8a.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/4168bb487d5b82227665ab4ec90b67ce02691741...480f49412651059a414a6a5c96887abb1877de8a)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 480f49412651059a414a6a5c96887abb1877de8a
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 17:00:46 +02:00
Johannes Marbach 08800f7d60 Reduce boilerplate further 2025-05-12 10:56:57 +02:00
Johannes Marbach eb51a7f145 Use macros to reduce boilerplate 2025-05-12 10:56:57 +02:00
Johannes Marbach 8cf09217d6 Switch to structs in yet more places 2025-05-12 10:56:57 +02:00
Johannes Marbach f81945ad7e Fix build error 2025-05-12 10:56:57 +02:00
Johannes Marbach c21f97274c Switch to struct 2025-05-12 10:56:57 +02:00
Johannes Marbach da67bacfbf Add changelog 2025-05-12 10:56:57 +02:00
Johannes Marbach 175d854a9b feat(ffi): Add methods for observing account data changes
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-05-12 10:56:57 +02:00
Ivan Enderlin afabfb97b6 task(ui): Log when the room name is empty when filtering the room list. 2025-05-12 10:24:52 +02:00
Yousef Moazzam 8e4554d3c0 test: create room server ACL event with EventFactory 2025-05-12 09:30:36 +02:00
Yousef Moazzam f1ea47f0b6 test: add room server ACL event method to EventFactory 2025-05-12 09:30:36 +02:00
Yousef Moazzam 26f1282c6a test: create room power level events with EventFactory 2025-05-10 20:35:57 +02:00
Yousef Moazzam 4053321cd0 test: add room power levels event method to EventFactory 2025-05-10 20:35:57 +02:00
Robin 561158c7bb fix(tests): Avoid depending on the local time zone in tests
This test fails when run in the Americas, because over here the Unix epoch took place in 1969 =D
2025-05-09 20:47:48 +02:00
Mauro Romito c023745dcf crates: update Ruma to support MSC 4278
This will later be handled once the account data observation PR is ready
2025-05-09 14:41:17 +02:00
Yousef Moazzam 6ba68fe87e test: add timeline events in bulk to room builder 2025-05-09 11:51:45 +02:00
Yousef Moazzam 7afb46cc0c test: create room create event with EventFactory 2025-05-09 11:51:45 +02:00
Yousef Moazzam 93dcd07073 test: add room create event method to EventFactory 2025-05-09 11:51:45 +02:00
Yousef Moazzam cf2f507951 test: create member event with EventFactory 2025-05-09 11:51:45 +02:00
Damir Jelić 35a2ce97d8 refactor(widget): Use streams to streamline the action processing logic 2025-05-09 11:07:41 +02:00
Ivan Enderlin f042084bd2 doc: Generate doc with --generate-link-to-definition.
This patch adds the `--generate-link-to-definition`
argument to `rustdoc` for `docs.rs`. This is using
https://github.com/rust-lang/rust/pull/84176 to add links in the source
code page.
2025-05-08 13:08:32 +02:00
Doug be6d5f9bd9 ffi: Add support for the login hints with OIDC. 2025-05-08 12:10:16 +02:00
Doug 506060f23d sdk: Add support for generic OAuth login hints.
See https://github.com/element-hq/matrix-authentication-service/pull/4512
2025-05-08 12:10:16 +02:00
Richard van der Hoff 55d475df04 Merge pull request #4988 from matrix-org/rav/history_sharing/better_sender_data
crypto: improve SenderData stored with room key bundle data
2025-05-07 22:31:48 +01:00
Richard van der Hoff f349a66292 crypto: improve SenderData stored with room key bundle data
If we already have cross-signing details for the owner of the device at the
point we receive the to-device message, we should store that rather than just
the device info.
2025-05-07 22:16:57 +01:00
Richard van der Hoff 3742bdc7cf crypto: move some logic from SenderDataFinder to SenderData
create a new method `SenderData::from_device` which does the last few steps of
`SenderDataFinder`: turns out we want it elsewhere. Add some tests to test that
functionality in isolation.
2025-05-07 22:16:57 +01:00
Doug b5b2450eac ffi: Expose the QrCodeData server name. 2025-05-07 13:40:29 +02:00
Denis Kasak fc071bafb2 docs: Various fixes for store-related comments.
- Doc comment for the SQLite-based state store incorrectly referred to
  it as a "cryptostore".
- Consistent capitalisation of SQLite.
- Consistent use of indefinite article "an" before SQLite.
- Fix line length.
2025-05-06 13:55:03 +02:00
Yousef Moazzam e4ce1790cd test: replace sync_timeline_event! with EventFactory in notification test 2025-05-06 13:34:00 +02:00
Ivan Enderlin 3461b13ec7 doc(sqlite): Add entry in the CHANGELOG.md. 2025-05-06 09:17:54 +02:00
Ivan Enderlin 83e4314645 fix(sqlite): Fix a UNIQUE constraint violation with Update::RemoveItem.
Imagine we have the following events:

| event_id | room_id | chunk_id | position |
|----------|---------|----------|----------|
| $ev0     | !r0     | 42       | 0        |
| $ev1     | !r0     | 42       | 1        |
| $ev2     | !r0     | 42       | 2        |
| $ev3     | !r0     | 42       | 3        |
| $ev4     | !r0     | 42       | 4        |

`$ev2` has been removed, then we end up in this state:

| event_id | room_id | chunk_id | position |
|----------|---------|----------|----------|
| $ev0     | !r0     | 42       | 0        |
| $ev1     | !r0     | 42       | 1        |
|          |         |          |          | <- no more `$ev2`
| $ev3     | !r0     | 42       | 3        |
| $ev4     | !r0     | 42       | 4        |

We need to shift the `position` of `$ev3` and `$ev4` to `position - 1`,
like so:

| event_id | room_id | chunk_id | position |
|----------|---------|----------|----------|
| $ev0     | !r0     | 42       | 0        |
| $ev1     | !r0     | 42       | 1        |
| $ev3     | !r0     | 42       | 2        |
| $ev4     | !r0     | 42       | 3        |

Usually, it boils down to run the following query:

```sql
UPDATE event_chunks
SET position = position - 1
WHERE position > 2 AND …
```

Okay. But `UPDATE` runs on rows in no particular order. It means that
it can update `$ev4` before `$ev3` for example. What happens in this
particular case? The `position` of `$ev4` becomes `3`, however `$ev3`
already has `position = 3`. Because there is a `UNIQUE` constraint
on `(room_id, chunk_id, position)`, it will result in a constraint
violation.

There is **no way** to control the execution order of `UPDATE` in
SQLite. To persuade yourself, try:

```sql
UPDATE event_chunks
SET position = position - 1
FROM (
    SELECT event_id
    FROM event_chunks
    WHERE position > 2 AND …
    ORDER BY position ASC
) as ordered
WHERE event_chunks.event_id = ordered.event_id
```

It will fail the same way.

Thus, we have 2 solutions:

1. Remove the `UNIQUE` constraint,
2. Be creative.

The `UNIQUE` constraint is a safe belt. Normally, we have
`event_cache::Deduplicator` that is responsible to ensure there is no
duplicated event. However, relying on this is “fragile” in the sense it
can contain bugs. Relying on the `UNIQUE` constraint from SQLite is more
robust. It's “braces and belt” as we say here.

So. We need to be creative.

Many solutions exist. Amongst the most popular, we see _dropping and
re-creating the index_, which is no-go for us, it's too expensive. I
(@hywan) have adopted the following one:

- Do `position = position - 1` but in the negative space, so
 `position = -(position - 1)`. A position cannot be negative; we are
  sure it is unique!
- Once all candidate rows are updated, do `position = -position` to move
  back to the positive space.

'told you it's gonna be creative.

This solution is a hack, **but** it is a small number of operations, and
we can keep the `UNIQUE` constraint in place.

This patch updates the `test_linked_chunk_remove_item` to handle
6 events. On _my_ system, with _my_ SQLite version, it triggers the
`UNIQUE` constraint violation without the bug fix.
2025-05-06 09:17:54 +02:00
dependabot[bot] c726bc5904 chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 5426ecc3f5c2b10effaefbd374f0abdc6a571b2f to 4168bb487d5b82227665ab4ec90b67ce02691741.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/5426ecc3f5c2b10effaefbd374f0abdc6a571b2f...4168bb487d5b82227665ab4ec90b67ce02691741)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 4168bb487d5b82227665ab4ec90b67ce02691741
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 18:35:37 +03:00
dependabot[bot] 970af0de7c chore(deps): Bump crate-ci/typos from 1.31.2 to 1.32.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.31.2 to 1.32.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.31.2...v1.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 17:31:29 +02:00
mpeter50 8be0a7df95 Update logging of device verification request timestamp valdiation
In element-hq/element-web#29625 it was found to be useful to give more visibility to this kind of verification error.

Signed-off-by: mpeter50 <83356418+mpeter50@users.noreply.github.com>
2025-05-05 09:46:29 +03:00
Michael Goldenberg 8fd122c431 style(indexeddb): cargo fmt
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Michael Goldenberg f661b82f18 refactor(indexeddb): rename module (indexeddb_serializer -> serializer)
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Michael Goldenberg 77ee7f1d19 refactor(indexeddb): change indexeddb_serializer::Result to use IndexeddbSerializerError
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Michael Goldenberg af90b7ac4e refactor(indexeddb): add conversions into IndexeddbSerializerError
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Michael Goldenberg 3f3daef01c refactor(indexeddb): add conversion IndexeddbSerializerError -> IndexeddbCryptoStoreError
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Michael Goldenberg c2e859273d refactor(indexeddb): add enum for general IndexedDB serialization errors
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-05-05 08:44:54 +02:00
Richard van der Hoff 3b84b2c5e7 crypto-ffi: fix error message for MissingRoomKey (#4997)
This error does not necessarily mean that the session was *withheld*.
2025-05-02 15:52:15 +01:00
Stefan Ceriu 284db61540 feat(ffi): expose a new get_room method on the NotificationClient that will fetch it from its inner in-memory store backed client instead of the parent one.
This is necessary because the `NotificationClient` runs a sliding sync loop and the retrieved data isn't pushed back into the parent client stores (because of cross process locking shenanigans).
This will be used with the previously introduced `org.matrix.msc3401.call.member` required state to check whether a room still has an ongoing call before showing the ringing screen.
2025-04-30 12:48:41 +03:00
Stefan Ceriu 8e19a5eb33 change(notification_client): request the org.matrix.msc3401.call.member state events resolving notification payloads
- this will be used to check whether a room still has an active call (`has_active_room_call`) before showing the ringing screen
2025-04-30 12:48:41 +03:00
Jorge Martín ef4cb79cde fix(sdk): Upload encrypted media with application/octet-stream mime type
This is apparently the right way to do it, both because some HS expect only this mimetype and also so we don't leak the mime type of the encrypted media.
2025-04-30 09:10:27 +02:00
Timo 9fbb9cbe9b WidgetDriver: refactor Filter
This commit simplifies the filter public api.

Rethinking the public api we only need:
 - to know if events can be sent based on the capabilities
 - to know if events can be sent to the widget (read) based on the capabilities
 - if it even makes sense to sent a cs api read request or if all possibly returned events
   would not match the type.

To simplify the code in the machine it also made sense to add `From` implementation
to the FilterInputs instead of gathering the relevant data from all kinds of Raw events.

The new api is simpler:
All possible events we need to check can be converted into filter inputs (using `into()`).
`capabilites` has two allow_read/allow_send that consume filter inputs.
`capabilites` can be asked if there is any filter for specific event types
to allow not send unnecassary requests.
2025-04-29 18:15:07 +02:00
Timo 4e64f28318 WidgetDriver: filter event_type change from dedicated ...EventType -> String
This is (sadly) required since we cannot do `as_str()` for `TimlineEventType` and other `ruma` event types.
So we need to use `String`. We also never used them more specifically than strings.
2025-04-29 18:15:07 +02:00
Timo 5e2f775b2b WidgetDriver: rename EventFilter->Filter & MatrixEventFilterInput -> FilterInput
This is a simple devtool refactor rename. Nothing fancy here.
2025-04-29 18:15:07 +02:00
procr1337 0856f4e6b0 refactor(crypto): Properly encapsulate internal OutboundGroupSession state
Previously, the `share_strategy` was breaking the abstraction provided
by `OutboundGroupSession` by accessing its internal fields in an
inconsistent and adhoc way. Now all fields are private and a proper
abstraction was added to access the required state in a consistent API.
2025-04-29 17:39:21 +02:00
Benjamin Bouvier ae4cdda939 feat(sdk): add a room method to retrieve all related events 2025-04-29 15:01:31 +02:00
Benjamin Bouvier 0db273bf38 test(sdk): add a mocking endpoint for listing relations and test Room::relations 2025-04-29 15:01:31 +02:00
Benjamin Bouvier a912a7584f test(sdk): add a mocking endpoint for listing threads and test Room::list_threads() 2025-04-29 15:01:31 +02:00
Benjamin Bouvier fa1aa57581 feat(sdk): add a room method to retrieve a list of threads 2025-04-29 15:01:31 +02:00
Richard van der Hoff b22bb3fa86 crypto: Move some test helpers out from sender_data_finder 2025-04-29 12:36:32 +01:00
Michael Goldenberg c3ed8b9e7b docs(ffi): update changelog
Signed-of-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg 6e442d9046 feat(ffi): rename fields in UploadSource to match AttachmentSource
Signed-of-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg 79c5edd319 refactor(ffi): add conversion from UploadSource to AttachmentSource
Signed-of-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg bd6361e23a feat(ffi): replace file-related fields with UploadSource in UploadParameters
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg 02fdf8c0d3 feat(ffi): add UploadSource for representing upload data
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg 1e835b24fb feat(ffi): update changelog
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
Michael Goldenberg 1a12ba3ad4 feat(ffi): allow file data to be passed through bindings when sending attachment
Signed-of-by: Michael Goldenberg <m@mgoldenberg.net>
2025-04-29 12:35:31 +02:00
dependabot[bot] d9f2588561 chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from c34c1c13a740b06851baff92ab9a653d93ad6ce7 to 5426ecc3f5c2b10effaefbd374f0abdc6a571b2f.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/c34c1c13a740b06851baff92ab9a653d93ad6ce7...5426ecc3f5c2b10effaefbd374f0abdc6a571b2f)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 5426ecc3f5c2b10effaefbd374f0abdc6a571b2f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 12:31:49 +02:00
dependabot[bot] 5268bc35db chore(deps): Bump crate-ci/typos from 1.31.1 to 1.31.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.31.1 to 1.31.2.
- [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.31.1...v1.31.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 12:20:08 +02:00
Valere Fedronic ff32840387 refactor(crypto): Move session_id from EncryptionInfo to AlgorithmInfo as it is megolm specific
This patch moves the `session_id` field from EncryptionInfo to
AlgorithmInfo::MegolmV1AesSha2 as it is specific to Megolm. We provide
transparent migration of the serialized data from one format to the other.

In the future we plan to reuse `EncryptionInfo` for to_device decryption
(using olm not megolm). So megolm session_id should move to algorithm
specific data.
2025-04-29 08:07:03 +00:00
Mauro Romito b4afb91de5 feat(bindings): check if report room api is supported 2025-04-29 09:48:29 +03:00
Richard van der Hoff d800d3c324 crypto: clean up confusing method (#4983)
This method had a confusing name: it didn't receive a key bundle, but
rather the data *about* a key bundle.

Remove the unused `sender_key` parameter while we are at it: we use the
embedded (and already-checked) `event.sender_device_keys` here.
2025-04-28 13:55:51 +01:00
Richard van der Hoff 7c84ab2701 Merge pull request #4982 from matrix-org/rav/random_fix_1
sdk: remove redundant variable
2025-04-28 13:51:12 +01:00
procr1337 6e119c737c fix(crypto): Take into account pending to-device room key sharing requests when collecting devices that have already received a session
This avoids conditions where a key may be shared with a device only
after we decided that it is fine to reuse (and not rotate) the session
based on the wrong assumption that that particular device does not have
the keys.

Signed-off-by: Niklas Baumstark
[niklas.baumstark@gmail.com](mailto:niklas.baumstark@gmail.com)
2025-04-25 15:51:41 +02:00
Valere Fedronic 237c0256a2 fix(tests): tweak a flaky test to make it more stable + logs (#4968)
Tentative fix for
https://github.com/matrix-org/matrix-rust-sdk/issues/4832

Tweaked a bit the timings and added more logging in the UTD manager to
see what is happening exactly in case it is still flaky.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-04-25 10:42:55 +00:00
Benjamin Bouvier f763d3690d docs(common): add a comment explaining how to use the timer! macro 2025-04-24 18:21:00 +02:00
Andy Balaam 91d085c41b task(tests): Ignore test_new_users_first_messages_dont_warn_about_insecure_device_if_it_is_secure because it is flaky 2025-04-24 15:14:58 +01:00
procr1337 afb6627bef fix(crypto): Fixed a bug where room keys would be rotated unecessarily
Previously, `is_session_overshared_for_user` did not take into account
that `shared_with_set` also contains withheld device IDs who explicitly
have never received the session keys. This would lead to it mistakenly
determining oversharing for those devices for every event being sent in
the presence of blacklisted/withheld devices in the room, and rotating
the group session accordingly.

The fix is to correctly exclude devices with `ShareInfo::Withheld` from
the enumeration.

Signed-off-by: Niklas Baumstark niklas.baumstark@gmail.com
2025-04-24 14:39:02 +02:00
Benjamin Bouvier 8c3f55456f refactor(widget): reduce indent in a few places thanks to early returns 2025-04-24 14:07:27 +02:00
Benjamin Bouvier 03d9e9b368 refactor(widget): avoid complicated combinators and make decisions more local and explicit 2025-04-24 14:07:27 +02:00
Benjamin Bouvier 75c4af5f4e chore(widget): make some names more explicit 2025-04-24 14:07:27 +02:00
Benjamin Bouvier c9f6938cb7 refactor(widget): get rid of WidgetDriverRequestHandle::null too 2025-04-24 14:07:27 +02:00
Benjamin Bouvier 939af521f3 refactor(widget): simplify further the MatrixDriverRequestHandle 2025-04-24 14:07:27 +02:00
Benjamin Bouvier bb9d481d88 refactor(widget): get rid of the null MatrixDriverRequestHandle 2025-04-24 14:07:27 +02:00
Benjamin Bouvier 3df336ab1c refactor(widget): get rid of function used only once 2025-04-24 14:07:27 +02:00
Benjamin Kampmann 12e358a54f fix(sdk): Don't overwrite previously added state events in state_event processing
Fixes #4952 .

Signed-off-by: Benjamin Kampmann <ben@acter.global>
2025-04-24 13:49:22 +02:00
Richard van der Hoff 468e7c35f6 Merge pull request #4932 from matrix-org/rav/history_sharing/save_key_bundle_data
crypto: store received room key bundle data information

Add hooks to the memory store and sqlite store to stash the information about room key data.
2025-04-24 12:22:14 +01:00
Richard van der Hoff a3cb1cd6b5 Merge branch 'main' into rav/history_sharing/save_key_bundle_data 2025-04-24 12:07:21 +01:00
Johannes Marbach 1554e05d8a refactor(send_queue): generalize SentRequestKey::Media and DependentQueuedRequestKind::UploadFileWithThumbnail to prepare for MSC4274 gallery uploads (#4897)
This was broken out of
https://github.com/matrix-org/matrix-rust-sdk/pull/4838 and is a
preliminary step towards implementing
[MSC4274](https://github.com/matrix-org/matrix-spec-proposals/pull/4274).
`SentRequestKey::Media` and
`DependentQueuedRequestKind::UploadFileWithThumbnail` are generalized to
allow chaining dependent media uploads and accumulating sent media
sources.

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

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-04-24 09:52:33 +00:00
Richard van der Hoff 85e0626d5f indexeddb: fudge implementation of get_received_room_key_bundle_data 2025-04-23 19:59:24 +01:00
Richard van der Hoff e89c45ba42 sqlite: store data on received room key bundles 2025-04-23 19:59:24 +01:00
Richard van der Hoff 6173aef064 memorystore: store received room key bundle data 2025-04-23 19:59:24 +01:00
Richard van der Hoff 00364d95af crypto: add methods for room key bundles to store traits 2025-04-23 19:59:24 +01:00
Richard van der Hoff 3aa0983a5c crypto: add received room key bundles to store changes list
After we receive a to-device message holding room key bundle info, add the data
to the store's Changes structure
2025-04-23 19:59:24 +01:00
Richard van der Hoff 4be4d39851 crypto: add types to support decryption of RoomKeyHistoryBundle to-device
events
2025-04-23 19:59:24 +01:00
Benjamin Bouvier 884775086a chore: add an intermittent test failure policy 2025-04-23 15:34:26 +02:00
Damir Jelić a60e336f85 feat(crypto): Start using the stable identifier for the sender device keys
This patch updates the sending side of the `sender_device_keys` field
introduced in MSC4147.

Since the MSC got merged, we're switching from the unstable identifier
to the stable one.

A couple of snapshot tests were added modified to make this happen.
2025-04-23 15:25:11 +02:00
Benjamin Bouvier 426a4ff1bf chore(ci): make clippy happy on all configurations 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 9492614ea6 refactor(sdk): rename a few push_action_ctx variables back into push_ctx 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 234e0be337 refactor(timeline): reuse the same push context for all the events we're trying to re-decrypt 2025-04-23 14:49:49 +02:00
Benjamin Bouvier b6d71a3875 refactor(timeline): make use of PushContext in the RoomDataProvider trait 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 4c8e2fd4ae refactor(sdk): no need to recompute push actions from /messages, since try_decrypt_event does it for us 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 55342a84fa refactor(sdk): explicit the case where we can't compute the push actions 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 9950268164 refactor(sdk): rename Room::push_action_ctx to Room::push_context() (and associated type too) 2025-04-23 14:49:49 +02:00
Benjamin Bouvier f17c9fb2d5 refactor(sdk): rename Room::push_context() to Room::push_condition_room_ctx() 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 93c961d673 refactor(sdk): avoid recomputing the push context and ruleset for every single event when decrypting a batch 2025-04-23 14:49:49 +02:00
Benjamin Bouvier 6e786e0ede refactor(sdk): use Room::try_decrypt_event in an extra location
The previous code was wrong, in that it wouldn't properly compute push
actions for events that were not encrypted and received with /messages.
2025-04-23 14:49:49 +02:00
Timo 6c4a4382d7 WidgetDriver: Use matrix_sdk_common::executor::spawn instead of tokio::spawn to make it wasm compatible. (#4959)
This PR is a revived version of:
https://github.com/matrix-org/matrix-rust-sdk/pull/4707 since it is now
possible to easily add wasm support thanks to:
https://github.com/matrix-org/matrix-rust-sdk/pull/4572

Using the `executer::spawn` will select `tokio::spawn` or
`wasm_bindgen_futures::spawn_local` based on what platform we are on.

~~They behave differently in that `tokio` actually starts the async
block inside the spawn but the wasm `spawn` wrapper will only start the
part that is not inside the `async` block and the join handle needs to
be awaited for it to work.~~

This has now changed with:
https://github.com/matrix-org/matrix-rust-sdk/pull/4572.
Now they behave the same and this PR becomes a very simple change.
2025-04-23 14:39:34 +02:00
Richard van der Hoff 7272a347fa Merge pull request #4961 from matrix-org/rav/history_sharing/deflake_integ_test
test: attempt to deflake history-sharing integ test
2025-04-23 11:49:41 +01:00
Richard van der Hoff 6d1c24f6fb test: attempt to deflake history-sharing test
Rather than attempting to trigger Alice's encryption sync loop with an incoming
message from Bob, instead have Alice send a message once Bob has joined the
room.
2025-04-23 11:33:06 +01:00
Richard van der Hoff 4300148663 test: factour out new helper wait_until_some. 2025-04-23 11:33:06 +01:00
Richard van der Hoff 75cde02283 Merge pull request #4946 from matrix-org/rav/history_sharing/share_on_invite
sdk: share room history when we send an invite, subject to an experimental feature flag.
2025-04-23 11:15:57 +01:00
Damir Jelić 59ecb1edbd fix(multiverse): Add a shortcut to mark rooms as read back 2025-04-23 11:39:59 +02:00
Richard van der Hoff 6e963917d6 sdk: clean up imports 2025-04-23 09:51:01 +01:00
Valere 7adf60d2c6 fixup: Cleaner ProcessedToDevice snapshot serialization 2025-04-22 16:30:53 +02:00
Valere bd576c22c0 fixup: test, redact snapshot value that is dependent of feature flag 2025-04-22 16:30:53 +02:00
Valere 35023ceb0b fixup: invalid tag in doc 2025-04-22 16:30:53 +02:00
Valere f1e7894c01 fixup: insta use shorter names 2025-04-22 16:30:53 +02:00
Valere ef44631fc6 review: remove outdated changelog line 2025-04-22 16:30:53 +02:00
Valere 541586f6cc review: add snapshot test with proper redaction 2025-04-22 16:30:53 +02:00
Valere f89150d3ee review: quick doc improvements 2025-04-22 16:30:53 +02:00
Valere b27770801c review: refactor, rename NotProcessed variant to Invalid 2025-04-22 16:30:53 +02:00
Valere a49bffac4c review: refactors ProcessedToDeviceEvent to tuple variants
Simplifies the `ProcessedToDeviceEvent` enum by converting its variants to tuple variants.

This change improves code readability and conciseness by removing the need for named fields within the variants.
2025-04-22 16:30:53 +02:00
Valere d4a0c2882d review: Move ProcessedToDeviceEvent to crypto types mod 2025-04-22 16:30:53 +02:00
Valere 031f4ec329 review: Remove encryption_info. Will be part of another PR 2025-04-22 16:30:53 +02:00
Valere 4bf103db38 test: Add more olm decryption encryption_info tests 2025-04-22 16:30:53 +02:00
Valere 4363105976 crypto: Add variants for plain text and encrypted to-device events
fixup: post rebase
2025-04-22 16:30:53 +02:00
Doug 3b133865f0 chore: Remove unused contacts field from OidcConfiguration. 2025-04-22 16:25:07 +02:00
Richard van der Hoff 82a0708b4e SDK: rename confusing-named Room::query_keys_for_untracked_users
This also sends out the query for dirty users, so the name was misleading.
2025-04-22 15:19:51 +01:00
dependabot[bot] 3eafefcf37 chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 9b4bb2bedb217d3ede225b6b07ebde713177cd8f to c34c1c13a740b06851baff92ab9a653d93ad6ce7.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/9b4bb2bedb217d3ede225b6b07ebde713177cd8f...c34c1c13a740b06851baff92ab9a653d93ad6ce7)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: c34c1c13a740b06851baff92ab9a653d93ad6ce7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 10:02:57 +02:00
Andy Balaam d2874afb75 fix(integration-tests): Fixes #4871 (hopefully). Repeatedly sync in a test after other user cross-signs 2025-04-17 13:51:32 +02:00
Damir Jelić a848506669 feat(multiverse): Add a /invite command 2025-04-17 12:03:51 +02:00
Damir Jelić f0e49c2adf feat(multiverse): Render membership changes 2025-04-17 12:03:51 +02:00
Ivan Enderlin 74e2e767dd fix(base): Add RoomNotableUpdateReasons::NONE to… fix a possible regression.
This patch introduces a temporary hack.

So here is the thing. Ideally, we DO NOT want to emit this reason.
It does not makes sense. However, all notable update reasons
are not clearly identified so far. Why is it a problem? The
`matrix_sdk_ui::room_list_service::RoomList` is listening this stream
of [`RoomInfoNotableUpdate`], and emits an update on a room item if
it receives a notable reason. Because all reasons are not identified,
we are likely to miss particular updates, and it can feel broken.
Ultimately, we want to clearly identify all the notable update reasons,
and remove this one.
2025-04-17 09:13:41 +03:00
Ivan Enderlin 3ec831f5da test(base): Update tests after the previous patch.
This patch updates the tests, which now break because of the previous
batch that fixes a bug.
2025-04-17 09:13:41 +03:00
Ivan Enderlin ab34330e47 fix(base): Do not emit an RoomInfoNotableUpdate with empty reasons.
In case an empty `RoomInfoNotableUpdateReasons` is empty, let's not send
a `RoomInfoNotableUpdate`.
2025-04-17 09:13:41 +03:00
Ivan Enderlin 35e5cca3fb fix(base): room::display_name emits a RoomInfoNotableUpdateReasons::DISPLAY_NAME.
This patch updates the `room::display_name` response processor to emits
a `RoomInfoNotableUpdateReasons::DISPLAY_NAME`.
2025-04-17 09:13:41 +03:00
Ivan Enderlin 80a7aadf9f feat(base): changes::save_only will also broadcast room info notable updates.
This patch changes the `changes::save_only` and
`changes::save_and_apply` response processors to both broadcast the
`RoomInfoNotableUpdates`.
2025-04-17 09:13:41 +03:00
Ivan Enderlin 4837add55e test(base): Test that sliding sync persists the room (cached) display name. 2025-04-17 09:13:41 +03:00
Ivan Enderlin abf0bbb1a6 fix(base): Use the room::display_name processor for sync v2.
This patch uses `room::display_name` and `changes::save_only` to compute
the new display name for all rooms and save them, for sync v2 only.
2025-04-17 09:13:41 +03:00
Ivan Enderlin c1d885f913 fix(base): Create the room::display_name response processor.
This patch creates the `room::display_name::update_for_rooms` response
processor. It also creates the `changes::save_only` response processor.
Finally, this patch uses both to compute the new display name for all
rooms and save them, for sliding sync only.
2025-04-17 09:13:41 +03:00
Ivan Enderlin 568e60b434 refactor(base): Extract the “save” part of save_and_apply into its own function.
This patch extracts the “save” part of the `save_and_apply` response
processor into its own function. Thus we have `save_changes` (new) and
`apply_changes` that are used by `save_and_apply`.
2025-04-17 09:13:41 +03:00
Ivan Enderlin d05796d8cb task(base): Room::compute_display_name returns an UpdatedRoomDisplayName.
This patch updates `Room::compute_display_name` to return an
`UpdatedRoomDisplayName`. This is useful to know if the display name has
changed or not.
2025-04-17 09:13:41 +03:00
Ivan Enderlin adb7cd33d1 task(base): Introduce UpdatedRoomDisplayName.
This patch introduces a new enum: `UpdatedRoomDisplayName`, which
will be used to know if a room display name is different or not when
computing the room display name.
2025-04-17 09:13:41 +03:00
Richard van der Hoff 18f20a7e29 sdk: only share history if cross-signing is set up
... otherwise, it fails with an error, which makes the integ tests fail
2025-04-16 16:53:31 +01:00
Richard van der Hoff 96bdd91bad sdk: share room history when we send an invite
... subject to an experimental feature flag.
2025-04-16 16:53:27 +01:00
Damir Jelić bc50cae35f feat(multiverse): Add support to join rooms you've been invited to 2025-04-16 11:35:57 +02:00
Stefan Ceriu d36b2a6869 feat(ffi): introduce a ThreadSummary type within MsgLikeContent (#4933)
…that holds information on the thread the given item is the root of

- it holds the latest event content and sender at the moment but will
hold more information in the future e.g. number of replies, if it's
unread etc.
- the field is not currently being populate but is delivered earlier so
it can power shipping the UI side on the embedders
2025-04-16 09:11:31 +03:00
Richard van der Hoff ed232df0b6 Merge pull request #4864 from matrix-org/rav/history_sharing/upload_bundle
crypto: encrypt, upload and share keys for room history
2025-04-15 18:00:03 +01:00
Richard van der Hoff 1a4f6effda Merge branch 'main' into rav/history_sharing/upload_bundle
Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-04-15 17:43:01 +01:00
Richard van der Hoff dc6fe93d1e crypto: fix changelog for 0.10.0 and 0.11.0 (#4939)
PR #4670 didn't land until 0.11.0.
2025-04-15 17:12:32 +01:00
Benjamin Bouvier a5537a8f24 fix(event cache): don't ditch a previous-batch token when we didn't have initial events (#4936)
See #4891 that shows a case where we should have saved the
previous-batch token, and instead ditched it, in the previous version.

Changes include:
- moving the code deciding to keep or ditch the `previous-batch` token
into `append_events_locked`.
- tweak the condition to ditch, so that the `previous-batch` token is
ditched only if we didn't have events in the event cache in the first
place, in addition to having storage + the timeline not being marked as
limited explicitly.

Credits to @zecakeh for the test case.
2025-04-15 15:29:11 +00:00
Benjamin Bouvier a27d6e2655 multiverse: prefer rendering back-paginated events instead of timeline's tail
This is useful to observe the virtyual start of timeline item in manual
testing.
2025-04-15 16:31:20 +02:00
Richard van der Hoff bce6c19bba Merge branch 'main' into rav/history_sharing/upload_bundle
Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-04-15 14:32:42 +01:00
Ivan Enderlin 22d092b83c chore(base): Rename ephemeral_events to dispatch_ephemeral_events. 2025-04-15 14:17:07 +02:00
Ivan Enderlin ee5671bef5 chore(base): Rename Room to RoomCreationData. 2025-04-15 14:17:07 +02:00
Ivan Enderlin 0b58b9112d chore(base): Make Clippy happy. 2025-04-15 14:17:07 +02:00
Ivan Enderlin 0fed5147b9 chore(base): Use Entry::or_default() to simplify code. 2025-04-15 14:17:07 +02:00
Ivan Enderlin b06234149f chore(base): Fix imports and e2e-encryption. 2025-04-15 14:17:07 +02:00
Ivan Enderlin 739f306bf0 refactor(base): Create the room::msc4186::extensions::room_account_data response processor.
This patch is twofold:

1. it transforms the `&mut` to a `&` for the room account data in the
   sliding sync flow, which allows to remove one big clone!
2. it adds the `room_account_data` response processor.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 5f3c96607f refactor(base): Create the room::msc4186::extensions::ephemeral_events response processor.
This patch creates the new `room::msc4186::extensions::ephemeral_events`
response processor.

Ideally we would like to merge this with `ephemeral_events`, but they
are a bit different. Let's see how it evolves in the future.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 235facb793 refactor(base): room::sync_v2::update_(joined|left)_room use room::Room.
This patch uses the `room::Room` structure in `room::sync_v2` to
reduce the number of arguments of the `update_joined_room` and the
`update_left_room` processors.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 9adb0deaa5 refactor(base): room::msc4186::update_any_room uses room::Room.
This patch uses the `room::Room` structure in
`room::msc4186::update_any_room` to reduce its number of arguments. It
results in the removal of te `allow(clippy::too_many_arguments)`.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 61b711ce76 chore(base): Create the room::Room structure.
This patch creates the `room::Room` structure to group common arguments
in the `room` processors.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 9e6dc71609 refactor(base): Remove the BaseStateStore argument in room:sync_v2::* processors.
This patch removes the `BaseStateStore` argument in `room::sync_v2::*`
response processors as it can be fetched from the `Notification`
argument.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 33b1c02873 chore(base): Rename room_data to room_response.
This patch tries to clarify that the `room_data` _is_ the HTTP response.
2025-04-15 14:17:07 +02:00
Ivan Enderlin f4ad575090 refactor(base): Remove SessionMeta from update_any_room.
This patch removes the `SessionMeta` argument of
`room::msc4186::update_any_room`. Tracking its usage, it reveals that
providing a `UserId` is sufficient. Happily, we already provide a
`UserId`, hence making `SessionMeta` useless here.
2025-04-15 14:17:07 +02:00
Ivan Enderlin f55730716a chore(base): Rename a variable room_type to room_state. 2025-04-15 14:17:07 +02:00
Ivan Enderlin bad1c683f8 chore(base): Rename fields of RoomUpdate.
This patch renames the fields in `RoomUpdate`: `join` becomes `joined`,
`leave` becomes `left`, `invite` becomes `invited`, to match their
associate type.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 2479339c46 refactor(base): Simplify the flow of membership.
This patch simplifies the flow of `membership` when the room is an
invited room.

Previously, we were creating the room as `Invited`. Then, later
overriding it to `Knocked` if we found it was such a room. Otherwise, we
were overriding it again to `Invited`. Well. Now the flow is simpler.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 45d5d1a802 refactor(base): Remove the numerous Option<…> returned by update_any_room.
This patch removes the 4 `Option<…>` returned by `update_any_room`: they
are replaced by a single new `RoomUpdateKind` enum.
2025-04-15 14:17:07 +02:00
Ivan Enderlin a218105aca refactor(base): Create the room::msc4186::update_any_room response processor.
This patch extracts the `BaseClient::process_sliding_sync_room` method
into the new `msc4186::update_any_room` response processor. So far, this
is purely a code move, without any changes (modulo method-to-function
transformation).

With `BaseClient:process_sliding_sync_room` comes
`BaseClient::process_sliding_sync_room_membership`,
`BaseClient::handle_own_room_membership`, `process_room_properties` and
`cache_latest_events`.
2025-04-15 14:17:07 +02:00
Ivan Enderlin 6bbb7fb498 chore(base): Remove unnecessary comments.
This patch removes comments that are irrelevant today. There is nothing
to fix.
2025-04-15 14:17:07 +02:00
Richard van der Hoff 80db096be8 crypto: fix changelog 2025-04-15 12:55:55 +01:00
Richard van der Hoff 9ef1a040dd test: add the start of an integration test for room history sharing
This is only a partial test, since we haven't yet implemented the receiver side
of the history-sharing messages.
2025-04-15 12:55:55 +01:00
Richard van der Hoff f11158ab6c sdk: send out the to-device requests created by Room::share_history 2025-04-15 12:55:55 +01:00
Richard van der Hoff 84a030aed0 crypto: Support for encrypting and sending room key history bundle data
For each device belonging to the user, encrypt and send to-device messages
containing the bundle data
2025-04-15 12:55:55 +01:00
Richard van der Hoff 7b25a50a51 sdk: add Room::share_history
The next step in our work on sharing encrypted room history. Add a method to
`matrix_sdk::room::Room` which will upload an encrypted key bundle.
2025-04-15 12:55:55 +01:00
Ivan Enderlin f32d0099fc chore(base): Make Clippy happy. 2025-04-15 11:57:39 +02:00
Ivan Enderlin 6d9cf861f6 chore(base): Remove the re-exports in timeline::builder.
This patch removes the re-exports of `Notification` and `E2EE` in
`timeline::builder`. Let's make things simple now that they are used
outside the `timeline` processors.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 13e565a4dc refactor(base): Create the room::sync_v2::update_knocked_room response processor.
This patch creates the `update_knocked_room` processor, extracted from
the sync v2 flow.
2025-04-15 11:57:39 +02:00
Ivan Enderlin aeca1f1495 refactor(base): Use Notification wherever possible.
This patch groups arguments behind the `Notification` struct
if it makes sense. This patch also uses the new method
`Notification::push_notification_from_event_if` method to replace
duplicated code.
2025-04-15 11:57:39 +02:00
Ivan Enderlin c16bc6b435 refactor(base): Move and improve the Notification struct in response processors.
This patch moves the `timeline::builder::Notification` into
its own module, `notification`. This patch adds two methods:
`push_notification` and `push_notification_from_event_if`.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 846fcfb408 feat(base): Add From implementations to build a RawAnySyncOrStrippedTimelineEvent.
This patch adds two `From` implementations on
`RawAnySyncOrStrippedTimelineEvent` to create the correct variant of
this enum. This is useful when we get generic types and want to build
this type.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 1141b7db1a refactor(base): Create the room::sync_v2::update_invited_room response processor.
This patch creates the `update_invited_room` processor by extracting the
code for the sync v2 flow.
2025-04-15 11:57:39 +02:00
Ivan Enderlin b85a1a0998 refactor(base): Create the room::sync_v2::update_left_room response processor.
This patch creates the `update_left_room` processor, extracted from the
sync v2 flow.

I've noticed that `new_user_ids` is not used, so it has been put behind
a `_` variable for the moment, but some computations have been removed.
We need to clean this user ID flow.
2025-04-15 11:57:39 +02:00
Ivan Enderlin e62313d7ba reefactor(base): Create the room::sync_v2::update_joined_room response processor.
This patch extracts the logic to handle a `JoinedRoom` in a response
processor.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 71510de602 refactor(base): Extract timeline::builder::E2EE into e2ee, and use it more.
This patch moves the `timeline::builder::E2EE` type into the `e2ee`
module. Gathering these 3 E2EE values in the same type was a good idea,
and is now applied to more places in the response processors.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 28888a414b refactor(base): Compute now if and only if Level::INFO is enabled.
This patch computes the `now` variable if and only if it is required.
It is used by an `info!` log, so let's condition the creation of `now`
to that.
2025-04-15 11:57:39 +02:00
Ivan Enderlin 3538bb91e3 refactor(base): Context derives Default.
This patch makes `Context` to derive `Default`. The `new` constructor
also no longer takes a `RoomInfoNotableUpdates`, since it was always
used with a default value, it generates this value by itself.
2025-04-15 11:57:39 +02:00
Ivan Enderlin fdcc6dbeda chore(base): Replace an unwrap by an expect.
This patch updates a comment and replaces an `unwrap` by an `expect`.
2025-04-15 11:57:39 +02:00
dependabot[bot] f1cd8120a8 chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 6f67ee9ac810f0192ea7b3d2086406f97847bcf9 to 9b4bb2bedb217d3ede225b6b07ebde713177cd8f.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/6f67ee9ac810f0192ea7b3d2086406f97847bcf9...9b4bb2bedb217d3ede225b6b07ebde713177cd8f)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 9b4bb2bedb217d3ede225b6b07ebde713177cd8f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-15 11:31:54 +02:00
Richard van der Hoff 473852c7d5 Merge pull request #4922 from matrix-org/rav/check_sender_device_keys
crypto: check `sender_device_keys` on incoming Olm messages
2025-04-15 10:18:50 +01:00
Ivan Enderlin f19f8b25db fix(sdk): Remove include_heroes in sliding sync requests.
This patch removes all mentions of `include_heroes`. This field isn't
part of the MSC4186. We have the `test_compute_heroes_from_sliding_sync`
which continues to pass, everything fine on that side. There is no
mention of `include_heroes` in Synapse at the time of writing. It seems
to be an artifact from the sliding sync proxy experiment.
2025-04-14 18:26:27 +02:00
Jonas Richard Richter 4de202fc64 doc(changelog): add entry for room topic string in StateEventContent 2025-04-14 15:43:17 +02:00
Jonas Richard Richter 7b206d33f0 feat(ffi): update RoomTopic AnySyncStateEvent variant to include topic string 2025-04-14 15:43:17 +02:00
Ivan Enderlin 085de8bdae refactor(base): Create the ephemeral_events response processor.
This patch creates the `ephemeral_events` response processors:
`dispatch` and `dispatch_one`.

This remove duplicated code.
2025-04-14 08:48:18 +02:00
Ivan Enderlin fe0b954019 refactor(base): Create the dispatch_invite_and_knock response processor.
This patch creates the
`state_events::stripped::dispatch_invite_and_knock`, that is migrated
from the `BaseClient::handle_invite_state` method.
2025-04-14 08:48:18 +02:00
Ivan Enderlin e21ae2ae53 chore(base): Move dispatch_and_get_new_users inside the sync module.
This patch moves the `state_events::dispatch_and_get_new_users`
processor inside the `state_events::sync` module. Why? Because a similar
processor for `state_events::stripped` is about to be created.
2025-04-14 08:48:18 +02:00
Jorge Martín 0306683cbf feat(ffi): Add extra details to ClientError.
Also split `ClientError::new` into `ClientError::from_str` and `ClientError::from_err` so we can automatically get the details from the 2nd one.
2025-04-11 15:49:20 +02:00
Richard van der Hoff e020ba1023 crypto: check sender_device_keys on incoming Olm messages
MSC4147 added a `sender_device_keys` property to olm-encrypted to-device
messages, with recommendations about checking the values in that propety. We do
(most of?) those checks for `m.room_key` messages today, but not other types of
to-device message.
2025-04-11 12:00:18 +01:00
Richard van der Hoff 0697e0705b crypto: support MSC4147 device keys on custom to-device events
MSC4147 added a `sender_device_keys` property to the plaintext of *all*
olm-encrypted events. 03d4a30eb added the field to `DecryptedOlmV1Event`, but
due to Reasons, there is an almost-parallel struct `ToDeviceCustomEvent` which
is used for event types other than the 4 we have content types for.

To complete the set, let's add the field to `ToDeviceCustomEvent`.
2025-04-11 11:01:22 +01:00
447 changed files with 32994 additions and 17428 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ allow-git = [
# A patch override for the bindings fixing a bug for Android before upstream
# releases a new version.
"https://github.com/element-hq/tracing.git",
# Sam as for the tracing dependency.
# Same as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
+1 -7
View File
@@ -6,11 +6,6 @@ on:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -295,7 +290,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.31.1
uses: crate-ci/typos@v1.33.1
lint:
name: Lint
@@ -380,7 +375,6 @@ jobs:
RUST_LOG: "info,matrix_sdk=trace"
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
run: |
cargo nextest run -p matrix-sdk-integration-testing
-6
View File
@@ -5,11 +5,6 @@ on:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -83,7 +78,6 @@ jobs:
CARGO_PROFILE_COV_DEBUG: 1
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
+1 -6
View File
@@ -7,11 +7,6 @@ on:
workflow_dispatch:
pull_request: # focus on the changed files in current PR
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -25,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@6f67ee9ac810f0192ea7b3d2086406f97847bcf9 # v45
uses: tj-actions/changed-files@115870536a85eaf050e369291c7895748ff12aea
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
-5
View File
@@ -4,11 +4,6 @@ on:
push:
branches: [main]
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
-5
View File
@@ -6,11 +6,6 @@ on:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
msrv:
+17
View File
@@ -57,6 +57,23 @@ cargo insta test
cargo insta review
```
### Intermittent failure policy
While we strive to add test coverage for as many features as we can, it sometimes happens that the
tests will be intermittently failing in CI (such tests are sometimes called "flaky"). This can be
caused by race conditions of all sorts, either in the test code itself, but sometimes in the
underlying feature being tested too, and as such, it requires some investigation, usually from the
original author of the test.
Whenever such an intermittent failure happens, we try to open an issue to track the failures,
adding the
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
label to it, and commenting with links to CI runs where the failure happened.
If a test has been intermittently failing for **two weeks** or more, and no one is actively working
on fixing it, then we might decide to mark the test as `ignored` until it is fixed, to not cause
unrelated failures in other contributors' pull requests and pushes.
## Pull requests
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
Generated
+392 -151
View File
File diff suppressed because it is too large Load Diff
+52 -48
View File
@@ -4,15 +4,13 @@ members = [
"bindings/matrix-sdk-crypto-ffi",
"bindings/matrix-sdk-ffi",
"crates/*",
"testing/*",
"examples/*",
"labs/*",
"testing/*",
"uniffi-bindgen",
"xtask",
]
exclude = [
"testing/data",
]
exclude = ["testing/data"]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
@@ -23,14 +21,16 @@ rust-version = "1.85"
[workspace.dependencies]
anyhow = "1.0.95"
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-rx = "0.1.3"
async-stream = "0.3.5"
async-trait = "0.1.85"
as_variant = "1.3.0"
base64 = "0.22.1"
bitflags = "2.8.0"
byteorder = "1.5.0"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
@@ -60,7 +60,7 @@ reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.2", features = [
ruma = { version = "0.12.3", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -74,9 +74,13 @@ ruma = { version = "0.12.2", features = [
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4171",
] }
"unstable-msc4278",
"unstable-msc4286",
] }
ruma-common = "0.15.2"
serde = "1.0.217"
sentry = "0.36.0"
sentry-tracing = "0.36.0"
serde = { version = "1.0.217", features = ["rc"] }
serde_html_form = "0.2.7"
serde_json = "1.0.138"
sha2 = "0.10.8"
@@ -84,7 +88,7 @@ similar-asserts = "1.6.1"
stream_assert = "0.1.1"
tempfile = "3.16.0"
thiserror = "2.0.11"
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
tokio = { version = "1.43.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"
@@ -96,22 +100,50 @@ url = "2.5.4"
uuid = "1.12.1"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.33"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
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-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.11.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.11.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.11.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.11.0", default-features = false }
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 }
[workspace.lints.rust]
rust_2018_idioms = "warn"
semicolon_in_expressions_from_macros = "warn"
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
] }
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
[workspace.lints.clippy]
assigning_clones = "allow"
box_default = "allow"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
str_to_string = "warn"
todo = "warn"
unused_async = "warn"
redundant_clone = "warn"
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
@@ -155,31 +187,3 @@ tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca943
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
[workspace.lints.rust]
rust_2018_idioms = "warn"
semicolon_in_expressions_from_macros = "warn"
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
] }
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
[workspace.lints.clippy]
assigning_clones = "allow"
box_default = "allow"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
str_to_string = "warn"
todo = "warn"
unused_async = "warn"
redundant_clone = "warn"
+13 -13
View File
@@ -3,24 +3,27 @@ name = "benchmarks"
description = "Matrix SDK benchmarks"
edition = "2021"
license = "Apache-2.0"
rust-version = { workspace = true }
rust-version.workspace = true
version = "1.0.0"
publish = false
[package.metadata.release]
release = false
[dependencies]
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
matrix-sdk-base = { workspace = true }
matrix-sdk-crypto = { workspace = true }
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test = { workspace = true }
matrix-sdk-ui = { workspace = true }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test.workspace = true
matrix-sdk-ui.workspace = true
ruma.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile = "3.3.0"
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
wiremock = { workspace = true }
wiremock.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
@@ -44,6 +47,3 @@ harness = false
[[bench]]
name = "timeline"
harness = false
[package.metadata.release]
release = false
+11 -6
View File
@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
linked_chunk::{lazy_loader, LinkedChunk, Update},
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
SqliteEventCacheStore,
};
use matrix_sdk_base::event_cache::{
@@ -29,6 +29,7 @@ fn writing(c: &mut Criterion) {
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
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");
@@ -115,9 +116,9 @@ fn writing(c: &mut Criterion) {
if let Some(store) = &store {
let updates = linked_chunk.updates().unwrap().take();
store.handle_linked_chunk_updates(room_id, updates).await.unwrap();
store.handle_linked_chunk_updates(linked_chunk_id, updates).await.unwrap();
// Empty the store.
store.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await.unwrap();
store.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear]).await.unwrap();
}
},
@@ -145,6 +146,7 @@ fn reading(c: &mut Criterion) {
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
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");
@@ -195,7 +197,9 @@ fn reading(c: &mut Criterion) {
// Now persist the updates to recreate this full linked chunk.
let updates = lc.updates().unwrap().take();
runtime.block_on(store.handle_linked_chunk_updates(room_id, updates)).unwrap();
runtime
.block_on(store.handle_linked_chunk_updates(linked_chunk_id, updates))
.unwrap();
}
// Define the throughput.
@@ -206,7 +210,8 @@ fn reading(c: &mut Criterion) {
// Bench the routine.
bencher.to_async(&runtime).iter(|| async {
// Load the last chunk first,
let (last_chunk, chunk_id_gen) = store.load_last_chunk(room_id).await.unwrap();
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)
@@ -216,7 +221,7 @@ fn reading(c: &mut Criterion) {
// 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(room_id, cur_chunk_id).await.unwrap()
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)
+4 -4
View File
@@ -7,7 +7,7 @@ use matrix_sdk_base::{
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
use ruma::{
api::client::membership::get_member_events,
device_id,
@@ -29,7 +29,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
let f = EventFactory::new().room(&room_id);
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
for i in 0..MEMBERS_IN_ROOM {
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
let user_id = OwnedUserId::try_from(format!("@user_{i}:matrix.org")).unwrap();
let event = f
.member(&user_id)
.membership(MembershipState::Join)
@@ -178,11 +178,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
.lock()
.await
.unwrap()
.clear_all_rooms_chunks()
.clear_all_linked_chunks()
.await
.unwrap();
let timeline = Timeline::builder(&room)
let timeline = TimelineBuilder::new(&room)
.with_focus(TimelineFocus::PinnedEvents {
max_events_to_load: 100,
max_concurrent_requests: 10,
+2 -2
View File
@@ -1,7 +1,7 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_ui::Timeline;
use matrix_sdk_ui::timeline::TimelineBuilder;
use ruma::{
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
EventId,
@@ -102,7 +102,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|b| {
b.to_async(&runtime).iter(|| async {
let timeline = Timeline::builder(&room)
let timeline = TimelineBuilder::new(&room)
.track_read_marker_and_receipts()
.build()
.await
+18 -18
View File
@@ -3,12 +3,15 @@ name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
rust-version = { workspace = true }
rust-version.workspace = true
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"
publish = false
[package.metadata.release]
release = false
[lib]
crate-type = ["cdylib", "staticlib"]
@@ -21,23 +24,23 @@ default = ["bundled-sqlite"]
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
[dependencies]
anyhow = { workspace = true }
futures-util = { workspace = true }
anyhow.workspace = true
futures-util.workspace = true
hmac = "0.12.1"
http = { workspace = true }
http.workspace = true
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
matrix-sdk-ffi-macros = { workspace = true }
matrix-sdk-ffi-macros.workspace = true
pbkdf2 = "0.12.2"
rand = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
rand.workspace = true
ruma.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
uniffi = { workspace = true, features = ["cli"] }
vodozemac = { workspace = true }
vodozemac.workspace = true
zeroize = { workspace = true, features = ["zeroize_derive"] }
[dependencies.js_int]
@@ -53,20 +56,17 @@ workspace = true
features = ["crypto-store"]
[dependencies.tokio]
version = "1.33.0"
version = "1.43.1"
default-features = false
features = ["rt-multi-thread"]
[build-dependencies]
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
[dev-dependencies]
assert_matches2.workspace = true
tempfile = "3.8.0"
assert_matches2 = { workspace = true }
[lints]
workspace = true
[package.metadata.release]
release = false
+3 -3
View File
@@ -78,10 +78,10 @@ pub enum DecryptionError {
impl From<MegolmError> for DecryptionError {
fn from(value: MegolmError) -> Self {
match value {
match &value {
MegolmError::MissingRoomKey(withheld_code) => Self::MissingRoomKey {
error: "Withheld Inbound group session".to_owned(),
withheld_code: withheld_code.map(|w| w.as_str().to_owned()),
error: value.to_string(),
withheld_code: withheld_code.as_ref().map(|w| w.as_str().to_owned()),
},
_ => Self::Megolm { error: value.to_string() },
}
+26 -18
View File
@@ -354,7 +354,7 @@ impl OlmMachine {
.map(|d| d.into()))
}
/// Manually the device of the given user with the given device ID.
/// Manually verify the device of the given user with the given device ID.
///
/// This method will attempt to sign the device using our private cross
/// signing key.
@@ -554,8 +554,10 @@ impl OlmMachine {
}),
)?;
let to_device_events =
to_device_events.into_iter().map(|event| event.json().get().to_owned()).collect();
let to_device_events = to_device_events
.into_iter()
.map(|event| event.to_raw().json().get().to_owned())
.collect();
let room_key_infos = room_key_infos.into_iter().map(|info| info.into()).collect();
Ok(SyncChangesResult { to_device_events, room_key_infos })
@@ -915,20 +917,25 @@ impl OlmMachine {
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
Ok(match &encryption_info.algorithm_info {
AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, sender_claimed_keys } => {
DecryptedEvent {
clear_event: serde_json::to_string(&event_json)?,
sender_curve25519_key: curve25519_key.to_owned(),
claimed_ed25519_key: sender_claimed_keys
.get(&DeviceKeyAlgorithm::Ed25519)
.cloned(),
forwarding_curve25519_chain: vec![],
shield_state: if strict_shields {
encryption_info.verification_state.to_shield_state_strict().into()
} else {
encryption_info.verification_state.to_shield_state_lax().into()
},
}
AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key,
sender_claimed_keys,
session_id: _,
} => DecryptedEvent {
clear_event: serde_json::to_string(&event_json)?,
sender_curve25519_key: curve25519_key.to_owned(),
claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(),
forwarding_curve25519_chain: vec![],
shield_state: if strict_shields {
encryption_info.verification_state.to_shield_state_strict().into()
} else {
encryption_info.verification_state.to_shield_state_lax().into()
},
},
AlgorithmInfo::OlmV1Curve25519AesSha2 { .. } => {
// cannot happen because `decrypt_room_event` would have fail to decrypt olm for
// a room (EventError::UnsupportedAlgorithm)
panic!("Unsupported olm algorithm in room")
}
})
}
@@ -1335,7 +1342,8 @@ impl OlmMachine {
let (sas, request) = self.runtime.block_on(device.start_verification())?;
Some(StartSasResult {
sas: Sas { inner: sas, runtime: self.runtime.handle().to_owned() }.into(),
sas: Sas { inner: Box::new(sas), runtime: self.runtime.handle().to_owned() }
.into(),
request: request.into(),
})
} else {
@@ -224,8 +224,8 @@ impl From<&ToDeviceRequest> for Request {
}
}
impl From<&RoomMessageRequest> for Request {
fn from(r: &RoomMessageRequest) -> Self {
impl From<&Box<RoomMessageRequest>> for Request {
fn from(r: &Box<RoomMessageRequest>) -> Self {
Self::RoomMessage {
request_id: r.txn_id.to_string(),
room_id: r.room_id.to_string(),
@@ -88,7 +88,7 @@ impl Verification {
/// returns `None` if the verification is not a `Sas` verification.
pub fn as_sas(&self) -> Option<Arc<Sas>> {
if let InnerVerification::SasV1(sas) = &self.inner {
Some(Sas { inner: sas.to_owned(), runtime: self.runtime.to_owned() }.into())
Some(Sas { inner: sas.clone(), runtime: self.runtime.to_owned() }.into())
} else {
None
}
@@ -98,7 +98,7 @@ impl Verification {
/// returns `None` if the verification is not a `QrCode` verification.
pub fn as_qr(&self) -> Option<Arc<QrCode>> {
if let InnerVerification::QrV1(qr) = &self.inner {
Some(QrCode { inner: qr.to_owned(), runtime: self.runtime.to_owned() }.into())
Some(QrCode { inner: qr.clone(), runtime: self.runtime.to_owned() }.into())
} else {
None
}
@@ -108,7 +108,7 @@ impl Verification {
/// The `m.sas.v1` verification flow.
#[derive(uniffi::Object)]
pub struct Sas {
pub(crate) inner: InnerSas,
pub(crate) inner: Box<InnerSas>,
pub(crate) runtime: Handle,
}
@@ -324,7 +324,7 @@ impl From<QrVerificationState> for QrCodeState {
/// verification flow.
#[derive(uniffi::Object)]
pub struct QrCode {
pub(crate) inner: InnerQr,
pub(crate) inner: Box<InnerQr>,
pub(crate) runtime: Handle,
}
@@ -669,7 +669,7 @@ impl VerificationRequest {
/// verification flow.
pub fn start_sas_verification(&self) -> Result<Option<StartSasResult>, CryptoStoreError> {
Ok(self.runtime.block_on(self.inner.start_sas())?.map(|(sas, r)| StartSasResult {
sas: Arc::new(Sas { inner: sas, runtime: self.runtime.clone() }),
sas: Arc::new(Sas { inner: Box::new(sas), runtime: self.runtime.clone() }),
request: r.into(),
}))
}
@@ -690,7 +690,7 @@ impl VerificationRequest {
Ok(self
.runtime
.block_on(self.inner.generate_qr_code())?
.map(|qr| QrCode { inner: qr, runtime: self.runtime.clone() }.into()))
.map(|qr| QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into()))
}
/// Pass data from a scanned QR code to an active verification request and
@@ -717,7 +717,7 @@ impl VerificationRequest {
let request = qr.reciprocate()?;
Some(ScanResult {
qr: QrCode { inner: qr, runtime: self.runtime.clone() }.into(),
qr: QrCode { inner: Box::new(qr), runtime: self.runtime.clone() }.into(),
request: request.into(),
})
} else {
+1 -1
View File
@@ -7,7 +7,7 @@ license = "Apache-2.0"
name = "matrix-sdk-ffi-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
rust-version.workspace = true
version = "0.7.0"
publish = false
+6 -1
View File
@@ -51,7 +51,12 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
let res = match syn::parse(item) {
Ok(item) => match has_async_fn(item) {
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
true => {
quote! {
#[cfg_attr(target_family = "wasm", uniffi::export(#attr2))]
#[cfg_attr(not(target_family = "wasm"), uniffi::export(async_runtime = "tokio", #attr2))]
}
}
false => quote! { #[uniffi::export(#attr2)] },
},
Err(e) => e.into_compile_error(),
+43 -1
View File
@@ -6,6 +6,48 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.12.0] - 2025-06-10
Breaking changes:
- `Client::send_call_notification_if_needed` now returns `Result<bool>` instead of `Result<()>` so we can check if
the event was sent.
- `Client::upload_avatar` and `Timeline::send_attachment` now may fail if a file too large for the homeserver media
config is uploaded.
- `UploadParameters` replaces field `filename: String` with `source: UploadSource`.
`UploadSource` is an enum which may take a filename or a filename and bytes, which
allows a foreign language to read file contents natively and then pass those contents to
the foreign function when uploading a file through the `Timeline`.
([#4948](https://github.com/matrix-org/matrix-rust-sdk/pull/4948))
- `RoomInfo` replaces its field `is_tombstoned: bool` with `tombstone: Option<RoomTombstoneInfo>`,
containing the data needed to implement the room migration UI, a message and the replacement room id.
([#5027](https://github.com/matrix-org/matrix-rust-sdk/pull/5027))
Additions:
- `Client::subscribe_to_room_info` allows clients to subscribe to room info updates in rooms which may not be known yet.
This is useful when displaying a room preview for an unknown room, so when we receive any membership change for it,
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).
([#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.
- Add room topic string to `StateEventContent`
- Add `UploadSource` for representing upload data - this is analogous to `matrix_sdk_ui::timeline::AttachmentSource`
- Add `Client::observe_account_data_event` and `Client::observe_room_account_data_event` to
subscribe to global and room account data changes.
([#4994](https://github.com/matrix-org/matrix-rust-sdk/pull/4994))
- Add `Timeline::send_gallery` to send MSC4274-style galleries.
([#5163](https://github.com/matrix-org/matrix-rust-sdk/pull/5163))
- Add `reply_params` to `GalleryUploadParameters` to allow sending galleries as (threaded) replies.
([#5173](https://github.com/matrix-org/matrix-rust-sdk/pull/5173))
Breaking changes:
- `contacts` has been removed from `OidcConfiguration` (it was unused since the switch to OAuth).
## [0.11.0] - 2025-04-11
Breaking changes:
@@ -20,7 +62,7 @@ Breaking changes:
programs can set it to `true`.
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
original message and the associated error code and kind.
original message and the associated error code and kind.
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
`SendingFromUnverifiedDevice`. These indicate that your own device is not
+69 -38
View File
@@ -1,52 +1,81 @@
[package]
name = "matrix-sdk-ffi"
version = "0.11.0"
version = "0.12.0"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
license = "Apache-2.0"
readme = "README.md"
rust-version = { workspace = true }
rust-version.workspace = true
repository = "https://github.com/matrix-org/matrix-rust-sdk"
publish = false
[package.metadata.release]
release = true
[lib]
crate-type = ["cdylib", "staticlib"]
[features]
default = ["bundled-sqlite"]
default = ["bundled-sqlite", "unstable-msc4274"]
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
[dependencies]
anyhow = { workspace = true }
as_variant = { workspace = true }
anyhow.workspace = true
as_variant.workspace = true
async-compat = "0.2.4"
eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
eyeball-im.workspace = true
futures-util.workspace = true
language-tags = "0.3.2"
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk-ffi-macros = { workspace = true }
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.16"
once_cell = { workspace = true }
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-core = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
sentry-tracing = "0.36.0"
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tracing.workspace = true
tracing-appender = { version = "0.2.2" }
tracing-core.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
uniffi = { workspace = true, features = ["tokio"] }
url = { workspace = true }
zeroize = { workspace = true }
url.workspace = true
uuid = { version = "1.4.1", features = ["v4"] }
language-tags = "0.3.2"
zeroize.workspace = true
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
workspace = true
features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
# note: differ from block below
"native-tls",
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(not(target_os = "android"))'.dependencies.sentry]
version = "0.36.0"
default-features = false
features = [
# TLS lib used on non-Android platforms.
"native-tls",
# Most default features enabled otherwise.
"backtrace",
"contexts",
"panic",
"reqwest",
]
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.1"
@@ -58,27 +87,29 @@ features = [
"e2e-encryption",
"experimental-widgets",
"markdown",
"rustls-tls", # note: differ from block below
# note: differ from block above
"rustls-tls",
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
workspace = true
[target.'cfg(target_os = "android")'.dependencies.sentry]
version = "0.36.0"
default-features = false
features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
"native-tls", # note: differ from block above
"socks",
"sqlite",
"uniffi",
# TLS lib specific for Android.
"rustls",
# Most default features enabled otherwise.
"backtrace",
"contexts",
"panic",
"reqwest",
]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
[lints]
workspace = true
[package.metadata.release]
release = true
@@ -119,8 +119,6 @@ pub struct OidcConfiguration {
pub tos_uri: Option<String>,
/// A URI that contains the client's privacy policy.
pub policy_uri: Option<String>,
/// An array of e-mail addresses of people responsible for this client.
pub contacts: Option<Vec<String>>,
/// Pre-configured registrations for use with homeservers that don't support
/// dynamic client registration.
@@ -173,7 +171,7 @@ impl OidcConfiguration {
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
tracing::error!("Failed to parse {issuer:?}");
return None;
};
Some((issuer, ClientId::new(client_id.clone())))
+450 -44
View File
@@ -1,11 +1,13 @@
use std::{
collections::HashMap,
fmt::Debug,
sync::{Arc, RwLock},
path::PathBuf,
sync::{Arc, OnceLock, RwLock},
time::Duration,
};
use anyhow::{anyhow, Context as _};
use async_compat::get_runtime_handle;
use futures_util::pin_mut;
use matrix_sdk::{
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
@@ -36,17 +38,27 @@ use matrix_sdk::{
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
STATE_STORE_DATABASE_NAME,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
NotificationProcessSetup as MatrixNotificationProcessSetup,
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
notification_client::{
NotificationClient as MatrixNotificationClient,
NotificationProcessSetup as MatrixNotificationProcessSetup,
},
unable_to_decrypt_hook::UtdHookManager,
};
use mime::Mime;
use ruma::{
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
events::{
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
ignored_user_list::IgnoredUserListEventContent,
key::verification::request::ToDeviceKeyVerificationRequestEvent,
marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent},
push_rules::PushRulesEventContent,
room::{
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::{
@@ -55,7 +67,13 @@ use ruma::{
message::OriginalSyncRoomMessageEvent,
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
secret_storage::{
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
},
tag::TagEventContent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
@@ -73,12 +91,18 @@ use crate::{
encryption::Encryption,
notification::NotificationClient,
notification_settings::NotificationSettings,
room::RoomHistoryVisibility,
room::{RoomHistoryVisibility, RoomInfoListener},
room_directory_search::RoomDirectorySearch,
room_info::RoomInfo,
room_preview::RoomPreview,
ruma::{AuthData, MediaSource},
ruma::{
AccountDataEvent, AccountDataEventType, AuthData, InviteAvatars, MediaPreviewConfig,
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
},
runtime::get_runtime_handle,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utd::{UnableToDecryptDelegate, UtdHook},
utils::AsyncRuntimeDropped,
ClientError,
};
@@ -144,30 +168,43 @@ impl From<PushFormat> for RumaPushFormat {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientDelegate: Sync + Send {
pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
fn did_receive_auth_error(&self, is_soft_logout: bool);
fn did_refresh_tokens(&self);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientSessionDelegate: Sync + Send {
pub trait ClientSessionDelegate: SyncOutsideWasm + SendOutsideWasm {
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
fn save_session_in_keychain(&self, session: Session);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressWatcher: Send + Sync {
pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
fn transmission_progress(&self, progress: TransmissionProgress);
}
/// A listener to the global (client-wide) error reporter of the send queue.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomErrorListener: Sync + Send {
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
/// Called every time the send queue has ran into an error for a given room,
/// which will disable the send queue for that particular room.
fn on_error(&self, room_id: String, error: ClientError);
}
/// A listener for changes of global account data events.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
/// Called when a global account data event has changed.
fn on_change(&self, event: AccountDataEvent);
}
/// A listener for changes of room account data events.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
/// Called when a room account data event was changed.
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
}
#[derive(Clone, Copy, uniffi::Record)]
pub struct TransmissionProgress {
pub current: u64,
@@ -186,9 +223,14 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
delegate: OnceLock<Arc<dyn ClientDelegate>>,
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
/// The path to the directory where the state store and the crypto store are
/// located, if the `Client` instance has been built with a SQLite store
/// backend.
store_path: Option<PathBuf>,
}
impl Client {
@@ -196,6 +238,7 @@ impl Client {
sdk_client: MatrixClient,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
store_path: Option<PathBuf>,
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
@@ -229,9 +272,11 @@ impl Client {
sdk_client.cross_process_store_locks_holder_name().to_owned();
let client = Client {
inner: AsyncRuntimeDropped::new(sdk_client),
delegate: RwLock::new(None),
inner: AsyncRuntimeDropped::new(sdk_client.clone()),
delegate: OnceLock::new(),
utd_hook_manager: OnceLock::new(),
session_verification_controller,
store_path,
};
if enable_oidc_refresh_lock {
@@ -400,10 +445,18 @@ impl Client {
/// * `prompt` - The desired user experience in the web UI. No value means
/// that the user wishes to login into an existing account, and a value of
/// `Create` means that the user wishes to register a new account.
///
/// * `login_hint` - A generic login hint that an identity provider can use
/// to pre-fill the login form. The format of this hint is not restricted
/// by the spec as external providers all have their own way to handle the hint.
/// 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
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: Option<OidcPrompt>,
login_hint: Option<String>,
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
let registration_data = oidc_configuration.registration_data()?;
let redirect_uri = oidc_configuration.redirect_uri()?;
@@ -413,6 +466,9 @@ impl Client {
if let Some(prompt) = prompt {
url_builder = url_builder.prompt(vec![prompt.into()]);
}
if let Some(login_hint) = login_hint {
url_builder = url_builder.login_hint(login_hint);
}
let data = url_builder.build().await?;
@@ -486,7 +542,7 @@ impl Client {
auth_session,
room_load_settings
.try_into()
.map_err(|error| ClientError::Generic { msg: error })?,
.map_err(|error| ClientError::from_str(error, None))?,
)
.await?;
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
@@ -525,7 +581,7 @@ impl Client {
loop {
match subscriber.recv().await {
Ok(report) => listener
.on_error(report.room_id.to_string(), ClientError::new(report.error)),
.on_error(report.room_id.to_string(), ClientError::from_err(report.error)),
Err(err) => {
error!("error when listening to the send queue error reporter: {err}");
}
@@ -534,6 +590,132 @@ impl Client {
})))
}
/// Subscribe to updates of global account data events.
///
/// Be careful that only the most recent value can be observed. Subscribers
/// are notified when a new value is sent, but there is no guarantee that
/// they will see all values.
pub fn observe_account_data_event(
&self,
event_type: AccountDataEventType,
listener: Box<dyn AccountDataListener>,
) -> Arc<TaskHandle> {
macro_rules! observe {
($t:ty, $cb: expr) => {{
// Using an Arc here is mandatory or else the subscriber will never trigger
let observer =
Arc::new(self.inner.observe_events::<RumaGlobalAccountDataEvent<$t>, ()>());
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let mut subscriber = observer.subscribe();
loop {
if let Some(next) = subscriber.next().await {
$cb(next.0);
}
}
})))
}};
($t:ty) => {{
observe!($t, |event: RumaGlobalAccountDataEvent<$t>| {
listener.on_change(event.into());
})
}};
}
match event_type {
AccountDataEventType::Direct => {
observe!(DirectEventContent)
}
AccountDataEventType::IdentityServer => {
observe!(IdentityServerEventContent)
}
AccountDataEventType::IgnoredUserList => {
observe!(IgnoredUserListEventContent)
}
AccountDataEventType::PushRules => {
observe!(PushRulesEventContent, |event: RumaGlobalAccountDataEvent<
PushRulesEventContent,
>| {
if let Ok(event) = event.try_into() {
listener.on_change(event);
}
})
}
AccountDataEventType::SecretStorageDefaultKey => {
observe!(SecretStorageDefaultKeyEventContent)
}
AccountDataEventType::SecretStorageKey { key_id } => {
observe!(SecretStorageKeyEventContent, |event: RumaGlobalAccountDataEvent<
SecretStorageKeyEventContent,
>| {
if event.content.key_id != key_id {
return;
}
if let Ok(event) = event.try_into() {
listener.on_change(event);
}
})
}
}
}
/// Subscribe to updates of room account data events.
///
/// Be careful that only the most recent value can be observed. Subscribers
/// are notified when a new value is sent, but there is no guarantee that
/// they will see all values.
pub fn observe_room_account_data_event(
&self,
room_id: String,
event_type: RoomAccountDataEventType,
listener: Box<dyn RoomAccountDataListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
macro_rules! observe {
($t:ty, $cb: expr) => {{
// Using an Arc here is mandatory or else the subscriber will never trigger
let observer =
Arc::new(self.inner.observe_room_events::<RumaRoomAccountDataEvent<$t>, ()>(
&RoomId::parse(&room_id)?,
));
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let mut subscriber = observer.subscribe();
loop {
if let Some(next) = subscriber.next().await {
$cb(next.0);
}
}
}))))
}};
($t:ty) => {{
observe!($t, |event: RumaRoomAccountDataEvent<$t>| {
listener.on_change(event.into(), room_id.clone());
})
}};
}
match event_type {
RoomAccountDataEventType::FullyRead => {
observe!(FullyReadEventContent)
}
RoomAccountDataEventType::MarkedUnread => {
observe!(MarkedUnreadEventContent)
}
RoomAccountDataEventType::Tag => {
observe!(TagEventContent, |event: RumaRoomAccountDataEvent<TagEventContent>| {
if let Ok(event) = event.try_into() {
listener.on_change(event, room_id.clone());
}
})
}
RoomAccountDataEventType::UnstableMarkedUnread => {
observe!(UnstableMarkedUnreadEventContent)
}
}
}
/// Allows generic GET requests to be made through the SDKs internal HTTP
/// client
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
@@ -581,11 +763,20 @@ impl Client {
self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
}
/// Sets the [ClientDelegate] which will inform about authentication errors.
/// Returns an error if the delegate was already set.
pub fn set_delegate(
self: Arc<Self>,
delegate: Option<Box<dyn ClientDelegate>>,
) -> Option<Arc<TaskHandle>> {
delegate.map(|delegate| {
) -> Result<Option<Arc<TaskHandle>>, ClientError> {
if self.delegate.get().is_some() {
return Err(ClientError::Generic {
msg: "Delegate already initialized".to_owned(),
details: None,
});
}
Ok(delegate.map(|delegate| {
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
let client_clone = self.clone();
let session_change_task = get_runtime_handle().spawn(async move {
@@ -601,9 +792,44 @@ impl Client {
}
});
*self.delegate.write().unwrap() = Some(Arc::from(delegate));
self.delegate.get_or_init(|| Arc::from(delegate));
Arc::new(TaskHandle::new(session_change_task))
})
}))
}
/// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
/// Returns an error if the delegate was already set.
pub async fn set_utd_delegate(
self: Arc<Self>,
utd_delegate: Box<dyn UnableToDecryptDelegate>,
) -> Result<(), ClientError> {
if self.utd_hook_manager.get().is_some() {
return Err(ClientError::Generic {
msg: "UTD delegate already initialized".to_owned(),
details: None,
});
}
// UTDs detected before this duration may be reclassified as "late decryption"
// events (or discarded, if they get decrypted fast enough).
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
let mut utd_hook_manager = UtdHookManager::new(
Arc::new(UtdHook { delegate: utd_delegate.into() }),
(*self.inner).clone(),
)
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
if let Err(e) = utd_hook_manager.reload_from_store().await {
error!("Unable to reload UTD hook data from data store: {e}");
// Carry on with the setup anyway; we shouldn't fail setup just
// because the UTD hook failed to load its data.
}
self.utd_hook_manager.get_or_init(|| Arc::new(utd_hook_manager));
Ok(())
}
pub fn session(&self) -> Result<Session, ClientError> {
@@ -862,7 +1088,11 @@ impl Client {
}
pub fn rooms(&self) -> Vec<Arc<Room>> {
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
self.inner
.rooms()
.into_iter()
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
.collect()
}
/// Get a room by its ID.
@@ -879,14 +1109,17 @@ impl Client {
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let sdk_room = self.inner.get_room(&room_id);
let room = sdk_room.map(|room| Arc::new(Room::new(room)));
let room =
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
Ok(room)
}
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_room = self.inner.get_dm_room(&user_id);
let dm = sdk_room.map(|room| Arc::new(Room::new(room)));
let dm =
sdk_room.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())));
Ok(dm)
}
@@ -918,12 +1151,12 @@ impl Client {
Ok(Arc::new(NotificationClient {
inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
.await?,
_client: self.clone(),
client: self.clone(),
}))
}
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
SyncServiceBuilder::new((*self.inner).clone())
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
}
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
@@ -942,7 +1175,7 @@ impl Client {
if let Some(raw_content) = self
.inner
.account()
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
.await?
{
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
@@ -993,7 +1226,7 @@ impl Client {
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
Ok(Arc::new(Room::new(room)))
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
}
/// Join a room by its ID or alias.
@@ -1014,7 +1247,7 @@ impl Client {
.collect::<Result<Vec<_>, _>>()?;
let room =
self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
Ok(Arc::new(Room::new(room)))
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
}
/// Knock on a room to join it using its ID or alias.
@@ -1028,7 +1261,7 @@ impl Client {
let server_names =
server_names.iter().map(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
let room = self.inner.knock(room_id, reason, server_names).await?;
Ok(Arc::new(Room::new(room)))
Ok(Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
}
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
@@ -1122,7 +1355,10 @@ impl Client {
/// or an externally set timeout happens.**
pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
let room_id = RoomId::parse(room_id)?;
Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await)))
Ok(Arc::new(Room::new(
self.inner.await_room_remote_echo(&room_id).await,
self.utd_hook_manager.get().cloned(),
)))
}
/// Lets the user know whether this is an `m.login.password` based
@@ -1182,30 +1418,201 @@ impl Client {
/// Clear all the non-critical caches for this Client instance.
///
/// WARNING: This will clear all the caches, including the base store (state
/// store), so callers must make sure that any sync is inactive before
/// calling this method. In particular, the `SyncService` must not be
/// running. After the method returns, the Client will be in an unstable
/// state, and it is required that the caller reinstantiates a new
/// Client instance, be it via dropping the previous and re-creating it,
/// restarting their application, or any other similar means.
///
/// - This will get rid of the backing state store file, if provided.
/// - This will empty all the room's persisted event caches, so all rooms
/// will start as if they were empty.
/// - This will empty the media cache according to the current media
/// retention policy.
pub async fn clear_caches(&self) -> Result<(), ClientError> {
let closure = async || -> Result<_, EventCacheError> {
let closure = async || -> Result<_, ClientError> {
// Clean up the media cache according to the current media retention policy.
self.inner.event_cache_store().lock().await?.clean_up_media_cache().await?;
self.inner
.event_cache_store()
.lock()
.await
.map_err(EventCacheError::from)?
.clean_up_media_cache()
.await
.map_err(EventCacheError::from)?;
// Clear all the room chunks. It's important to *not* call
// `EventCacheStore::clear_all_rooms_chunks` here, because there might be live
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
// observers of the linked chunks, and that would cause some very bad state
// mismatch.
self.inner.event_cache().clear_all_rooms().await?;
// Delete the state store file, if it exists.
if let Some(store_path) = &self.store_path {
debug!("Removing the state store: {}", store_path.display());
// The state store and the crypto store both live in the same store path, so we
// can't blindly delete the directory.
//
// Delete the state store SQLite file, as well as the write-ahead log (WAL) and
// shared-memory (SHM) files, if they exist.
for file_name in [
PathBuf::from(STATE_STORE_DATABASE_NAME),
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.wal")),
PathBuf::from(format!("{STATE_STORE_DATABASE_NAME}.shm")),
] {
let file_path = store_path.join(file_name);
if file_path.exists() {
debug!("Removing file: {}", file_path.display());
std::fs::remove_file(&file_path).map_err(|err| ClientError::Generic {
msg: format!(
"couldn't delete the state store file {}: {err}",
file_path.display()
),
details: None,
})?;
}
}
}
Ok(())
};
Ok(closure().await?)
closure().await
}
/// Checks if the server supports the report room API.
pub async fn is_report_room_api_supported(&self) -> Result<bool, ClientError> {
Ok(self.inner.server_versions().await?.contains(&ruma::api::MatrixVersion::V1_13))
}
/// Subscribe to changes in the media preview configuration.
pub async fn subscribe_to_media_preview_config(
&self,
listener: Box<dyn MediaPreviewConfigListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let (initial_value, stream) = self.inner.account().observe_media_preview_config().await?;
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
// Send the initial value to the listener.
listener.on_change(initial_value.map(|config| config.into()));
// Listen for changes and notify the listener.
pin_mut!(stream);
while let Some(media_preview_config) = stream.next().await {
listener.on_change(Some(media_preview_config.into()));
}
}))))
}
/// Set the media previews timeline display policy
pub async fn set_media_preview_display_policy(
&self,
policy: MediaPreviews,
) -> Result<(), ClientError> {
self.inner.account().set_media_previews_display_policy(policy.into()).await?;
Ok(())
}
/// Get the media previews timeline display policy
/// currently stored in the cache.
pub async fn get_media_preview_display_policy(
&self,
) -> 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())),
None => Ok(None),
}
}
/// Set the invite request avatars display policy
pub async fn set_invite_avatars_display_policy(
&self,
policy: InviteAvatars,
) -> Result<(), ClientError> {
self.inner.account().set_invite_avatars_display_policy(policy.into()).await?;
Ok(())
}
/// Get the invite request avatars display policy
/// currently stored in the cache.
pub async fn get_invite_avatars_display_policy(
&self,
) -> 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())),
None => Ok(None),
}
}
/// Fetch the media preview configuration from the server.
pub async fn fetch_media_preview_config(
&self,
) -> Result<Option<MediaPreviewConfig>, ClientError> {
Ok(self.inner.account().fetch_media_preview_config_event_content().await?.map(Into::into))
}
/// Gets the `max_upload_size` value from the homeserver, which controls the
/// max size a media upload request can have.
pub async fn get_max_media_upload_size(&self) -> Result<u64, ClientError> {
let max_upload_size = self.inner.load_or_fetch_max_upload_size().await?;
Ok(max_upload_size.into())
}
/// Subscribe to [`RoomInfo`] updates given a provided [`RoomId`].
///
/// This works even for rooms we haven't received yet, so we can subscribe
/// to this and wait until we receive updates from them when sync responses
/// are processed.
///
/// Note this method should be used sparingly since using callback
/// interfaces is expensive, as well as keeping them alive for a long
/// time. Usages of this method should be short-lived and dropped as
/// soon as possible.
pub async fn subscribe_to_room_info(
&self,
room_id: String,
listener: Box<dyn RoomInfoListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let room_id = RoomId::parse(room_id)?;
// Emit the initial event, if present
if let Some(room) = self.inner.get_room(&room_id) {
if let Ok(room_info) = RoomInfo::new(&room).await {
listener.call(room_info);
}
}
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
let client = self.inner.clone();
let mut receiver = client.room_info_notable_update_receiver();
async move {
while let Ok(room_update) = receiver.recv().await {
if room_update.room_id != room_id {
continue;
}
if let Some(room) = client.get_room(&room_id) {
if let Ok(room_info) = RoomInfo::new(&room).await {
listener.call(room_info);
}
}
}
}
}))))
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IgnoredUsersListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, ignored_user_ids: Vec<String>);
}
@@ -1280,15 +1687,13 @@ impl From<&search_users::v3::User> for UserProfile {
impl Client {
fn process_session_change(&self, session_change: SessionChange) {
if let Some(delegate) = self.delegate.read().unwrap().clone() {
if let Some(delegate) = self.delegate.get().cloned() {
debug!("Applying session change: {session_change:?}");
get_runtime_handle().spawn_blocking(move || match session_change {
SessionChange::UnknownToken { soft_logout } => {
delegate.did_receive_auth_error(soft_logout);
}
SessionChange::TokensRefreshed => {
delegate.did_refresh_tokens();
}
SessionChange::TokensRefreshed => {}
});
} else {
debug!(
@@ -1519,7 +1924,8 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
Err(e) => {
return Err(ClientError::Generic {
msg: format!("Failed to serialize power levels, error: {e}"),
})
details: Some(format!("{e:?}")),
});
}
}
}
@@ -1984,7 +2390,7 @@ impl TryFrom<RumaJoinRule> for JoinRule {
}
RumaJoinRule::Invite => Ok(JoinRule::Invite),
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
_ => Err(format!("Unknown JoinRule: {:?}", value)),
_ => Err(format!("Unknown JoinRule: {value:?}")),
}
}
}
@@ -2001,7 +2407,7 @@ impl TryFrom<RumaAllowRule> for AllowRule {
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
Ok(Self::Custom { json })
}
_ => Err(format!("Invalid AllowRule: {:?}", value)),
_ => Err(format!("Invalid AllowRule: {value:?}")),
}
}
}
+48 -45
View File
@@ -1,6 +1,5 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
@@ -19,6 +18,7 @@ use matrix_sdk::{
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, SqliteStoreConfig,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use zeroize::Zeroizing;
@@ -26,7 +26,7 @@ use zeroize::Zeroizing;
use super::client::Client;
use crate::{
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
helpers::unwrap_or_clone_arc, runtime::get_runtime_handle, task_handle::TaskHandle,
};
/// A list of bytes containing a certificate in DER or PEM form.
@@ -57,6 +57,18 @@ impl QrCodeData {
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
/// The server name contained within the scanned QR code data.
///
/// Note: This value is only present when scanning a QR code the belongs to
/// a logged in client. The mode where the new client shows the QR code
/// will return `None`.
pub fn server_name(&self) -> Option<String> {
match &self.inner.mode_data {
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
QrCodeModeData::Login => None,
}
}
}
/// Error type for the decoding of the [`QrCodeData`].
@@ -161,7 +173,7 @@ pub enum QrLoginProgress {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: Sync + Send {
pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: QrLoginProgress);
}
@@ -275,11 +287,8 @@ pub struct ClientBuilder {
encryption_settings: EncryptionSettings,
room_key_recipient_strategy: CollectStrategy,
decryption_trust_requirement: TrustRequirement,
enable_share_history_on_invite: bool,
request_config: Option<RequestConfig>,
/// Whether to enable use of the event cache store, for reloading events
/// when building timelines et al.
use_event_cache_persistent_storage: bool,
}
#[matrix_sdk_ffi_macros::export]
@@ -313,28 +322,11 @@ impl ClientBuilder {
},
room_key_recipient_strategy: Default::default(),
decryption_trust_requirement: TrustRequirement::Untrusted,
enable_share_history_on_invite: false,
request_config: Default::default(),
use_event_cache_persistent_storage: false,
})
}
/// Whether to use the event cache persistent storage or not.
///
/// This is a temporary feature flag, for testing the event cache's
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
///
/// This is disabled by default. When disabled, a one-time cleanup is
/// performed when creating the client, and it will clear all the events
/// previously stored in the event cache.
///
/// When enabled, it will attempt to store events in the event cache as
/// they're received, and reuse them when reconstructing timelines.
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.use_event_cache_persistent_storage = value;
Arc::new(builder)
}
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
holder_name: String,
@@ -562,6 +554,19 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set whether to enable the experimental support for sending and receiving
/// encrypted room history on invite, per [MSC4268].
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
pub fn enable_share_history_on_invite(
self: Arc<Self>,
enable_share_history_on_invite: bool,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_share_history_on_invite = enable_share_history_on_invite;
Arc::new(builder)
}
/// Add a default request config to this client.
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
@@ -578,14 +583,16 @@ impl ClientBuilder {
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
if let Some(session_paths) = &builder.session_paths {
let store_path = if let Some(session_paths) = &builder.session_paths {
// This is the path where both the state store and the crypto store will live.
let data_path = Path::new(&session_paths.data_path);
// This is the path where the event cache store will live.
let cache_path = Path::new(&session_paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
cache_path = %cache_path.to_string_lossy(),
"Creating directories for data and cache stores.",
event_cache_path = %cache_path.to_string_lossy(),
"Creating directories for data (state and crypto) and cache stores.",
);
fs::create_dir_all(data_path)?;
@@ -614,9 +621,12 @@ impl ClientBuilder {
inner_builder = inner_builder
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
Some(data_path.to_owned())
} else {
debug!("Not using a store path.");
}
None
};
// Determine server either from URL, server name or user ID.
inner_builder = match builder.homeserver_cfg {
@@ -685,7 +695,8 @@ impl ClientBuilder {
inner_builder = inner_builder
.with_encryption_settings(builder.encryption_settings)
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
.with_decryption_trust_requirement(builder.decryption_trust_requirement);
.with_decryption_trust_requirement(builder.decryption_trust_requirement)
.with_enable_share_history_on_invite(builder.enable_share_history_on_invite);
match builder.sliding_sync_version_builder {
SlidingSyncVersionBuilder::None => {
@@ -727,22 +738,14 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
if builder.use_event_cache_persistent_storage {
// Enable the persistent storage \o/
sdk_client.event_cache().enable_storage()?;
} else {
// Get rid of all the previous events, if any.
let store = sdk_client
.event_cache_store()
.lock()
.await
.map_err(EventCacheError::LockingStorage)?;
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
}
Ok(Arc::new(
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
Client::new(
sdk_client,
builder.enable_oidc_refresh_lock,
builder.session_delegate,
store_path,
)
.await?,
))
}
+1 -1
View File
@@ -18,5 +18,5 @@ pub struct ElementWellKnown {
/// Helper function to parse a string into a ElementWellKnown struct
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
serde_json::from_str(&string).map_err(ClientError::from_err)
}
+15 -19
View File
@@ -1,16 +1,19 @@
use std::sync::Arc;
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
use crate::{
client::Client, error::ClientError, ruma::AuthData, runtime::get_runtime_handle,
task_handle::TaskHandle,
};
#[derive(uniffi::Object)]
pub struct Encryption {
@@ -25,22 +28,22 @@ pub struct Encryption {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupStateListener: Sync + Send {
pub trait BackupStateListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: BackupState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
pub trait BackupSteadyStateListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: BackupUploadState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RecoveryStateListener: Sync + Send {
pub trait RecoveryStateListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: RecoveryState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
pub trait VerificationStateListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: VerificationState);
}
@@ -164,7 +167,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
pub trait EnableRecoveryProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: EnableRecoveryProgress);
}
@@ -369,12 +372,8 @@ impl Encryption {
/// Completely reset the current user's crypto identity: reset the cross
/// signing keys, delete the existing backup and recovery key.
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
if let Some(reset_handle) = self
.inner
.recovery()
.reset_identity()
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
if let Some(reset_handle) =
self.inner.recovery().reset_identity().await.map_err(ClientError::from_err)?
{
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
}
@@ -441,7 +440,7 @@ impl Encryption {
info!("No identity found in the store.");
}
Err(error) => {
error!("Failed fetching identity from the store: {}", error);
error!("Failed fetching identity from the store: {error}");
}
};
@@ -541,12 +540,9 @@ impl IdentityResetHandle {
/// 4. Finally, re-enable key backups only if they were enabled before
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
if let Some(auth) = auth {
self.inner
.reset(Some(auth.into()))
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
} else {
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
self.inner.reset(None).await.map_err(ClientError::from_err)
}
}
+45 -32
View File
@@ -1,4 +1,4 @@
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
use std::{collections::HashMap, error::Error, fmt, fmt::Display, time::SystemTime};
use matrix_sdk::{
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
@@ -8,6 +8,7 @@ use matrix_sdk::{
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
use tracing::warn;
use uniffi::UnexpectedUniFFICallbackError;
use crate::{room_list::RoomListError, timeline::FocusEventError};
@@ -15,32 +16,39 @@ use crate::{room_list::RoomListError, timeline::FocusEventError};
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
Generic { msg: String },
Generic { msg: String, details: Option<String> },
#[error("api error {code}: {msg}")]
MatrixApi { kind: ErrorKind, code: String, msg: String },
MatrixApi { kind: ErrorKind, code: String, msg: String, details: Option<String> },
}
impl ClientError {
pub(crate) fn new<E: Display>(error: E) -> Self {
Self::Generic { msg: error.to_string() }
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
warn!("Error: {error}");
Self::Generic { msg: error.to_string(), details }
}
pub(crate) fn from_err<E: Error>(e: E) -> Self {
let details = Some(format!("{e:?}"));
Self::from_str(e, details)
}
}
impl From<anyhow::Error> for ClientError {
fn from(e: anyhow::Error) -> ClientError {
ClientError::Generic { msg: format!("{e:#}") }
let details = format!("{e:?}");
ClientError::Generic { msg: format!("{e:#}"), details: Some(details) }
}
}
impl From<reqwest::Error> for ClientError {
fn from(e: reqwest::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<UnexpectedUniFFICallbackError> for ClientError {
fn from(e: UnexpectedUniFFICallbackError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
@@ -53,135 +61,140 @@ impl From<matrix_sdk::Error> for ClientError {
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
return Self::Generic { msg: message.to_string() };
return (*http_error).into();
};
return Self::MatrixApi {
kind,
code,
msg: message.to_owned(),
details: Some(format!("{api_error:?}")),
};
return Self::MatrixApi { kind, code, msg: message.to_owned() };
}
}
Self::Generic { msg: http_error.to_string() }
(*http_error).into()
}
_ => Self::Generic { msg: e.to_string() },
_ => Self::from_err(e),
}
}
}
impl From<StoreError> for ClientError {
fn from(e: StoreError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<CryptoStoreError> for ClientError {
fn from(e: CryptoStoreError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<HttpError> for ClientError {
fn from(e: HttpError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<IdParseError> for ClientError {
fn from(e: IdParseError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<serde_json::Error> for ClientError {
fn from(e: serde_json::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<url::ParseError> for ClientError {
fn from(e: url::ParseError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<mime::FromStrError> for ClientError {
fn from(e: mime::FromStrError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<encryption_sync_service::Error> for ClientError {
fn from(e: encryption_sync_service::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<timeline::Error> for ClientError {
fn from(e: timeline::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<timeline::UnsupportedEditItem> for ClientError {
fn from(e: timeline::UnsupportedEditItem) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<notification_client::Error> for ClientError {
fn from(e: notification_client::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<sync_service::Error> for ClientError {
fn from(e: sync_service::Error) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<OAuthError> for ClientError {
fn from(e: OAuthError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<RoomError> for ClientError {
fn from(e: RoomError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<RoomListError> for ClientError {
fn from(e: RoomListError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<EditError> for ClientError {
fn from(e: EditError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<RoomSendQueueError> for ClientError {
fn from(e: RoomSendQueueError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
impl From<NotYetImplemented> for ClientError {
fn from(_: NotYetImplemented) -> Self {
Self::new("This functionality is not implemented yet.")
Self::from_str("This functionality is not implemented yet.", None)
}
}
impl From<FocusEventError> for ClientError {
fn from(e: FocusEventError) -> Self {
Self::new(e)
Self::from_err(e)
}
}
+33 -7
View File
@@ -1,3 +1,5 @@
use std::ops::Deref;
use anyhow::{bail, Context};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
@@ -22,7 +24,7 @@ use crate::{
};
#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
pub struct TimelineEvent(pub(crate) Box<AnySyncTimelineEvent>);
#[matrix_sdk_ffi_macros::export]
impl TimelineEvent {
@@ -39,7 +41,7 @@ impl TimelineEvent {
}
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
let event_type = match &self.0 {
let event_type = match self.0.deref() {
AnySyncTimelineEvent::MessageLike(event) => {
TimelineEventType::MessageLike { content: event.clone().try_into()? }
}
@@ -53,11 +55,19 @@ impl TimelineEvent {
impl From<AnyTimelineEvent> for TimelineEvent {
fn from(event: AnyTimelineEvent) -> Self {
Self(event.into())
Self(Box::new(event.into()))
}
}
#[derive(uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineEventType`, we would need to
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
// `uniffi::Object`, which is a radical change. It would simplify the memory
// usage, but it would slow down the performance around the FFI border. Thus,
// let's consider this is a false-positive lint in this particular case.
#[allow(clippy::large_enum_variant)]
pub enum TimelineEventType {
MessageLike { content: MessageLikeEventContent },
State { content: StateEventContent },
@@ -83,7 +93,7 @@ pub enum StateEventContent {
RoomServerAcl,
RoomThirdPartyInvite,
RoomTombstone,
RoomTopic,
RoomTopic { topic: String },
SpaceChild,
SpaceParent,
}
@@ -118,16 +128,28 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
AnySyncStateEvent::RoomServerAcl(_) => StateEventContent::RoomServerAcl,
AnySyncStateEvent::RoomThirdPartyInvite(_) => StateEventContent::RoomThirdPartyInvite,
AnySyncStateEvent::RoomTombstone(_) => StateEventContent::RoomTombstone,
AnySyncStateEvent::RoomTopic(_) => StateEventContent::RoomTopic,
AnySyncStateEvent::RoomTopic(content) => {
let content = get_state_event_original_content(content)?;
StateEventContent::RoomTopic { topic: content.topic }
}
AnySyncStateEvent::SpaceChild(_) => StateEventContent::SpaceChild,
AnySyncStateEvent::SpaceParent(_) => StateEventContent::SpaceParent,
_ => bail!("Unsupported state event"),
_ => bail!("Unsupported state event: {:?}", value.event_type()),
};
Ok(event)
}
}
#[derive(uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `MessageLineEventContent`, we would need to
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
// `uniffi::Object`, which is a radical change. It would simplify the memory
// usage, but it would slow down the performance around the FFI border. Thus,
// let's consider this is a false-positive lint in this particular case.
#[allow(clippy::large_enum_variant)]
pub enum MessageLikeEventContent {
CallAnswer,
CallInvite,
@@ -222,7 +244,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
MessageLikeEventContent::RoomRedaction { redacted_event_id, reason }
}
AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker,
_ => bail!("Unsupported Event Type"),
_ => bail!("Unsupported Event Type: {:?}", value.event_type()),
};
Ok(content)
}
@@ -365,6 +387,8 @@ pub enum RoomMessageEventMessageType {
Audio,
Emote,
File,
#[cfg(feature = "unstable-msc4274")]
Gallery,
Image,
Location,
Notice,
@@ -381,6 +405,8 @@ impl From<RumaMessageType> for RoomMessageEventMessageType {
RumaMessageType::Audio { .. } => Self::Audio,
RumaMessageType::Emote { .. } => Self::Emote,
RumaMessageType::File { .. } => Self::File,
#[cfg(feature = "unstable-msc4274")]
RumaMessageType::Gallery { .. } => Self::Gallery,
RumaMessageType::Image { .. } => Self::Image,
RumaMessageType::Location { .. } => Self::Location,
RumaMessageType::Notice { .. } => Self::Notice,
+2
View File
@@ -26,11 +26,13 @@ mod room_list;
mod room_member;
mod room_preview;
mod ruma;
mod runtime;
mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod tracing;
mod utd;
mod utils;
mod widget;
+79 -3
View File
@@ -1,14 +1,16 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
};
use ruma::{EventId, RoomId};
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
use tracing::error;
use crate::{
client::{Client, JoinRule},
error::ClientError,
event::TimelineEvent,
room::Room,
};
#[derive(uniffi::Enum)]
@@ -94,11 +96,23 @@ pub struct NotificationClient {
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
/// would need to repeat the hack done in the FFI `Client::drop` method.
pub(crate) _client: Arc<Client>,
pub(crate) client: Arc<Client>,
}
#[matrix_sdk_ffi_macros::export]
impl NotificationClient {
/// Fetches a room by its ID using the in-memory state store backed client.
///
/// Useful to retrieve room information after running the limited
/// notification client sliding sync loop.
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let sdk_room = self.inner.get_room(&room_id);
let room = sdk_room
.map(|room| Arc::new(Room::new(room, self.client.utd_hook_manager.get().cloned())));
Ok(room)
}
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
pub async fn get_notification(
@@ -118,4 +132,66 @@ impl NotificationClient {
Ok(None)
}
}
/// 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`].
pub async fn get_notifications(
&self,
requests: Vec<NotificationItemsRequest>,
) -> Result<HashMap<String, NotificationItem>, 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();
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}");
}
}
}
Ok(result)
}
}
/// A request for notification items grouped by their room.
#[derive(uniffi::Record)]
pub struct NotificationItemsRequest {
room_id: String,
event_ids: Vec<String>,
}
impl NotificationItemsRequest {
/// The parsed [`OwnedRoomId`] to use with the SDK crates.
pub fn room_id(&self) -> Result<OwnedRoomId, ClientError> {
RoomId::parse(&self.room_id).map_err(ClientError::from)
}
/// The parsed [`OwnedEventId`] list to use with the SDK crates.
pub fn event_ids(&self) -> Result<Vec<OwnedEventId>, ClientError> {
self.event_ids
.iter()
.map(|id| EventId::parse(id).map_err(ClientError::from))
.collect::<Result<Vec<_>, _>>()
}
}
impl TryFrom<NotificationItemsRequest>
for matrix_sdk_ui::notification_client::NotificationItemsRequest
{
type Error = ClientError;
fn try_from(value: NotificationItemsRequest) -> Result<Self, Self::Error> {
Ok(Self { room_id: value.room_id()?, event_ids: value.event_ids()? })
}
}
@@ -9,6 +9,7 @@ use matrix_sdk::{
ruma::events::push_rules::PushRulesEvent,
Client as MatrixClient,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::{
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
@@ -161,7 +162,7 @@ pub enum PushCondition {
}
impl TryFrom<SdkPushCondition> for PushCondition {
type Error = ();
type Error = String;
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
Ok(match value {
@@ -179,7 +180,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
SdkPushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
}
_ => return Err(()),
_ => return Err("Unsupported condition type".to_owned()),
})
}
}
@@ -290,7 +291,7 @@ impl TryFrom<SdkTweak> for Tweak {
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
SdkTweak::Custom { name, value } => {
let json_string = serde_json::to_string(&value)
.map_err(|e| format!("Failed to serialize custom tweak value: {}", e))?;
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
Self::Custom { name, value: json_string }
}
@@ -308,9 +309,9 @@ impl TryFrom<Tweak> for SdkTweak {
Tweak::Highlight { value } => Self::Highlight(value),
Tweak::Custom { name, value } => {
let json_value: serde_json::Value = serde_json::from_str(&value)
.map_err(|e| format!("Failed to deserialize custom tweak value: {}", e))?;
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
let value = serde_json::from_value(json_value)
.map_err(|e| format!("Failed to convert JSON value: {}", e))?;
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
Self::Custom { name, value }
}
@@ -334,7 +335,7 @@ impl TryFrom<SdkAction> for Action {
Ok(match value {
SdkAction::Notify => Self::Notify,
SdkAction::SetTweak(tweak) => Self::SetTweak {
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
},
_ => return Err("Unsupported action type".to_owned()),
})
@@ -348,7 +349,7 @@ impl TryFrom<Action> for SdkAction {
Ok(match value {
Action::Notify => Self::Notify,
Action::SetTweak { value } => Self::SetTweak(
value.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?,
),
})
}
@@ -387,7 +388,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
/// Delegate to notify of changes in push rules
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait NotificationSettingsDelegate: Sync + Send {
pub trait NotificationSettingsDelegate: SyncOutsideWasm + SendOutsideWasm {
fn settings_did_change(&self);
}
+133 -22
View File
@@ -1,3 +1,6 @@
use std::sync::{atomic::AtomicBool, Arc, OnceLock};
use tracing::warn;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_core::Subscriber;
use tracing_subscriber::{
@@ -8,19 +11,13 @@ use tracing_subscriber::{
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::SubscriberExt,
layer::SubscriberExt as _,
registry::LookupSpan,
util::SubscriberInitExt,
EnvFilter, Layer,
util::SubscriberInitExt as _,
Layer,
};
use crate::tracing::LogLevel;
pub fn log_panics() {
std::env::set_var("RUST_BACKTRACE", "1");
log_panics::init();
}
use crate::{error::ClientError, tracing::LogLevel};
fn text_layers<S>(config: TracingConfiguration) -> impl Layer<S>
where
@@ -343,6 +340,20 @@ impl TraceLogPacks {
}
}
struct SentryLoggingCtx {
/// The Sentry client guard, which keeps the Sentry context alive.
_guard: sentry::ClientInitGuard,
/// Whether the Sentry layer is enabled or not, at a global level.
enabled: Arc<AtomicBool>,
}
struct LoggingCtx {
sentry: Option<SentryLoggingCtx>,
}
static LOGGING: OnceLock<LoggingCtx> = OnceLock::new();
#[derive(uniffi::Record)]
pub struct TracingConfiguration {
/// The desired log level.
@@ -363,6 +374,89 @@ pub struct TracingConfiguration {
/// If set, configures rotated log files where to write additional logs.
write_to_files: Option<TracingFileConfiguration>,
/// If set, the Sentry DSN to use for error reporting.
sentry_dsn: Option<String>,
}
impl TracingConfiguration {
/// Sets up the tracing configuration and return a [`Logger`] instance
/// holding onto it.
fn build(mut self) -> LoggingCtx {
// Show full backtraces, if we run into panics.
std::env::set_var("RUST_BACKTRACE", "1");
// Log panics.
log_panics::init();
// Prepare the Sentry layer, if a DSN is provided.
let (sentry_layer, sentry_logging_ctx) = if let Some(sentry_dsn) = self.sentry_dsn.take() {
// Initialize the Sentry client with the given options.
let sentry_guard = sentry::init((
sentry_dsn,
sentry::ClientOptions {
traces_sample_rate: 0.0,
attach_stacktrace: true,
..sentry::ClientOptions::default()
},
));
let sentry_enabled = Arc::new(AtomicBool::new(true));
// Add a Sentry layer to the tracing subscriber.
//
// Pass custom event and span filters, which will ignore anything, if the Sentry
// support has been globally disabled, or if the statement doesn't include a
// `sentry` field set to `true`.
let sentry_layer = sentry_tracing::layer()
.event_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst)
&& metadata.fields().field("sentry").is_some()
{
sentry_tracing::default_event_filter(metadata)
} else {
// Ignore the event.
sentry_tracing::EventFilter::Ignore
}
}
})
.span_filter({
let enabled = sentry_enabled.clone();
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
sentry_tracing::default_span_filter(metadata)
} else {
// Ignore, if sentry is globally disabled.
false
}
}
});
(
Some(sentry_layer),
Some(SentryLoggingCtx { _guard: sentry_guard, enabled: sentry_enabled }),
)
} else {
(None, None)
};
let env_filter = build_tracing_filter(&self);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&env_filter))
.with(crate::platform::text_layers(self))
.with(sentry_layer)
.init();
// Log the log levels 🧠.
tracing::info!(env_filter, "Logging has been set up");
LoggingCtx { sentry: sentry_logging_ctx }
}
}
fn build_tracing_filter(config: &TracingConfiguration) -> String {
@@ -407,24 +501,38 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
/// multithreaded tokio runtime will be set up.
#[matrix_sdk_ffi_macros::export]
pub fn init_platform(config: TracingConfiguration, use_lightweight_tokio_runtime: bool) {
log_panics();
let env_filter = build_tracing_filter(&config);
tracing_subscriber::registry()
.with(EnvFilter::new(&env_filter))
.with(text_layers(config))
.init();
// Log the log levels 🧠.
tracing::info!(env_filter, "Logging has been set up");
pub fn init_platform(
config: TracingConfiguration,
use_lightweight_tokio_runtime: bool,
) -> Result<(), ClientError> {
LOGGING.set(config.build()).map_err(|_| ClientError::Generic {
msg: "logger already initialized".to_owned(),
details: None,
})?;
if use_lightweight_tokio_runtime {
setup_lightweight_tokio_runtime();
} else {
setup_multithreaded_tokio_runtime();
}
Ok(())
}
/// Set the global enablement level for the Sentry layer (after the logs have
/// been set up).
#[matrix_sdk_ffi_macros::export]
pub fn enable_sentry_logging(enabled: bool) {
if let Some(ctx) = LOGGING.get() {
if let Some(sentry_ctx) = &ctx.sentry {
sentry_ctx.enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
} else {
warn!("Sentry logging is not enabled");
}
} else {
// Can't use log statements here, since logging hasn't been enabled yet 🧠
eprintln!("Logging hasn't been enabled yet");
};
}
fn setup_multithreaded_tokio_runtime() {
@@ -479,6 +587,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
sentry_dsn: None,
};
let filter = build_tracing_filter(&config);
@@ -515,6 +624,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
sentry_dsn: None,
};
let filter = build_tracing_filter(&config);
@@ -552,6 +662,7 @@ mod tests {
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
sentry_dsn: None,
};
let filter = build_tracing_filter(&config);
+182 -47
View File
@@ -1,7 +1,6 @@
use std::{collections::HashMap, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use async_compat::get_runtime_handle;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
@@ -10,9 +9,14 @@ use matrix_sdk::{
TryFromReportedContentScoreError,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
SuccessorRoom as SdkSuccessorRoom,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
timeline::{default_event_filter, RoomExt, TimelineBuilder},
unable_to_decrypt_hook::UtdHookManager,
};
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
use mime::Mime;
use ruma::{
assign,
@@ -26,9 +30,9 @@ use ruma::{
},
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
},
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId,
};
use tokio::sync::RwLock;
use tracing::{error, warn};
use crate::{
@@ -40,12 +44,14 @@ use crate::{
live_location_share::{LastLocation, LiveLocationShare},
room_info::RoomInfo,
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
runtime::get_runtime_handle,
timeline::{
configuration::{TimelineConfiguration, TimelineFilter},
ReceiptType, SendHandle, Timeline,
EventTimelineItem, ReceiptType, SendHandle, Timeline,
},
utils::u64_to_uint,
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
};
@@ -70,21 +76,15 @@ impl From<RoomState> for Membership {
}
}
pub(crate) type TimelineLock = Arc<RwLock<Option<Arc<Timeline>>>>;
#[derive(uniffi::Object)]
pub struct Room {
pub(super) inner: SdkRoom,
timeline: TimelineLock,
utd_hook_manager: Option<Arc<UtdHookManager>>,
}
impl Room {
pub(crate) fn new(inner: SdkRoom) -> Self {
Room { inner, timeline: Default::default() }
}
pub(crate) fn with_timeline(inner: SdkRoom, timeline: TimelineLock) -> Self {
Room { inner, timeline }
pub(crate) fn new(inner: SdkRoom, utd_hook_manager: Option<Arc<UtdHookManager>>) -> Self {
Room { inner, utd_hook_manager }
}
}
@@ -122,8 +122,31 @@ impl Room {
self.inner.is_space()
}
pub fn is_tombstoned(&self) -> bool {
self.inner.is_tombstoned()
/// If this room is tombstoned, return the “reference” to the successor room
/// —i.e. the room replacing this one.
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
/// event.
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
pub fn successor_room(&self) -> Option<SuccessorRoom> {
self.inner.successor_room().map(Into::into)
}
/// If this room is the successor of a tombstoned room, return the
/// “reference” to the predecessor room.
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
/// event.
///
/// To determine if a room is the successor of a tombstoned room, the
/// [`m.room.create`] must have been received, **with** a `predecessor`
/// field.
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
pub fn predecessor_room(&self) -> Option<PredecessorRoom> {
self.inner.predecessor_room().map(Into::into)
}
pub fn canonical_alias(&self) -> Option<String> {
@@ -134,6 +157,17 @@ impl Room {
self.inner.alt_aliases().iter().map(|a| a.to_string()).collect()
}
/// Get the user who created the invite, if any.
pub async fn inviter(&self) -> Result<Option<RoomMember>, ClientError> {
let invite_details = self.inner.invite_details().await?;
match invite_details.inviter {
Some(inviter) => Ok(Some(inviter.try_into()?)),
None => Ok(None),
}
}
/// The room's current membership state.
pub fn membership(&self) -> Membership {
self.inner.state().into()
}
@@ -173,15 +207,10 @@ impl Room {
Ok(())
}
/// Create a timeline with a default configuration, i.e. a live timeline
/// with read receipts and read marker tracking.
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
Ok(timeline.clone())
} else {
let timeline = Timeline::new(self.inner.timeline().await?);
*write_guard = Some(timeline.clone());
Ok(timeline)
}
Ok(Timeline::new(self.inner.timeline().await?))
}
/// Build a new timeline instance with the given configuration.
@@ -189,7 +218,7 @@ impl Room {
&self,
configuration: TimelineConfiguration,
) -> Result<Arc<Timeline>, ClientError> {
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
let mut builder = matrix_sdk_ui::timeline::TimelineBuilder::new(&self.inner);
builder = builder
.with_focus(configuration.focus.try_into()?)
@@ -233,6 +262,14 @@ impl Room {
builder = builder.with_internal_id_prefix(internal_id_prefix);
}
if configuration.report_utds {
if let Some(utd_hook_manager) = self.utd_hook_manager.clone() {
builder = builder.with_unable_to_decrypt_hook(utd_hook_manager);
} else {
return Err(ClientError::Generic { msg: "Failed creating timeline because the configuration is set to report UTDs but no hook manager is set".to_owned(), details: None });
}
}
let timeline = builder.build().await?;
Ok(Timeline::new(timeline))
@@ -246,6 +283,22 @@ impl Room {
self.inner.encryption_state()
}
/// Checks whether the room is encrypted or not.
///
/// **Note**: this info may not be reliable if you don't set up
/// `m.room.encryption` as required state.
async fn is_encrypted(&self) -> bool {
self.inner
.latest_encryption_state()
.await
.map(|state| state.is_encrypted())
.unwrap_or(false)
}
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event_item().await.map(Into::into)
}
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
Ok(self.inner.latest_encryption_state().await?)
}
@@ -346,8 +399,11 @@ impl Room {
///
/// * `content` - The content of the event to send encoded as JSON string.
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
let content_json: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
let content_json: serde_json::Value =
serde_json::from_str(&content).map_err(|e| ClientError::Generic {
msg: format!("Failed to parse JSON: {e}"),
details: Some(format!("{e:?}")),
})?;
self.inner.send_raw(&event_type, content_json).await?;
@@ -404,9 +460,7 @@ impl Room {
.report_content(
EventId::parse(event_id)?,
score.map(TryFrom::try_from).transpose().map_err(
|error: TryFromReportedContentScoreError| ClientError::Generic {
msg: error.to_string(),
},
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
)?,
reason,
)
@@ -655,11 +709,11 @@ impl Room {
/// Mark a room as read, by attaching a read receipt on the latest event.
///
/// Note: this does NOT unset the unread flag; it's the caller's
/// responsibility to do so, if needs be.
/// responsibility to do so, if need be.
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
let timeline = self.timeline().await?;
let timeline = TimelineBuilder::new(&self.inner).build().await?;
timeline.mark_as_read(receipt_type).await?;
timeline.mark_as_read(receipt_type.into()).await?;
Ok(())
}
@@ -689,10 +743,7 @@ impl Room {
})
.collect::<Result<Vec<_>>>()?;
self.inner
.update_power_levels(updates)
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })?;
self.inner.update_power_levels(updates).await.map_err(ClientError::from_err)?;
Ok(())
}
@@ -726,9 +777,13 @@ impl Room {
/// 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
pub async fn send_call_notification_if_needed(&self) -> Result<(), ClientError> {
self.inner.send_call_notification_if_needed().await?;
Ok(())
///
/// 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.
@@ -1084,11 +1139,46 @@ impl Room {
self.inner.forget().await?;
Ok(())
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited, knocked or banned rooms.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
{
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.room_id().to_owned().into();
(room_or_alias_id, server_names)
};
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
}
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: Sync + Send {
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
}
@@ -1110,7 +1200,7 @@ impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
/// A listener for receiving new requests to a join a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait KnockRequestsListener: Send + Sync {
pub trait KnockRequestsListener: SendOutsideWasm + SyncOutsideWasm {
fn call(&self, join_requests: Vec<KnockRequest>);
}
@@ -1230,17 +1320,17 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomInfoListener: Sync + Send {
pub trait RoomInfoListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, room_info: RoomInfo);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TypingNotificationsListener: Sync + Send {
pub trait TypingNotificationsListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, typing_user_ids: Vec<String>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IdentityStatusChangeListener: Sync + Send {
pub trait IdentityStatusChangeListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
}
@@ -1462,3 +1552,48 @@ impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
}
}
}
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
/// predecessor of B, and B is the successor of A. This type holds information
/// about the successor room. See [`Room::successor_room`].
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
#[derive(uniffi::Record)]
pub struct SuccessorRoom {
/// The ID of the replacement room.
pub room_id: String,
/// The message explaining why the room has been tombstoned.
pub reason: Option<String>,
}
impl From<SdkSuccessorRoom> for SuccessorRoom {
fn from(value: SdkSuccessorRoom) -> Self {
Self { room_id: value.room_id.to_string(), reason: value.reason }
}
}
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
/// predecessor of B, and B is the successor of A. This type holds information
/// about the predecessor room. See [`Room::predecessor_room`].
///
/// To know the predecessor of a room, the [`m.room.create`] state event must
/// have been received.
///
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
#[derive(uniffi::Record)]
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() }
}
}
@@ -15,14 +15,14 @@
use std::{fmt::Debug, sync::Arc};
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::ServerName;
use tokio::sync::RwLock;
use crate::{error::ClientError, task_handle::TaskHandle};
use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHandle};
#[derive(uniffi::Enum)]
pub enum PublicRoomJoinRule {
@@ -198,6 +198,6 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
pub trait RoomDirectorySearchEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+5 -4
View File
@@ -7,7 +7,7 @@ use crate::{
client::JoinRule,
error::ClientError,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero, RoomHistoryVisibility},
room::{Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom},
room_member::RoomMember,
};
@@ -26,7 +26,8 @@ pub struct RoomInfo {
is_direct: bool,
is_public: bool,
is_space: bool,
is_tombstoned: bool,
/// If present, it means the room has been archived/upgraded.
successor_room: Option<SuccessorRoom>,
is_favourite: bool,
canonical_alias: Option<String>,
alternative_aliases: Vec<String>,
@@ -80,7 +81,7 @@ impl RoomInfo {
let join_rule = room.join_rule().try_into();
if let Err(e) = &join_rule {
warn!("Failed to parse join rule: {:?}", e);
warn!("Failed to parse join rule: {e:?}");
}
Ok(Self {
@@ -94,7 +95,7 @@ impl RoomInfo {
is_direct: room.is_direct().await?,
is_public: room.is_public(),
is_space: room.is_space(),
is_tombstoned: room.is_tombstoned(),
successor_room: room.successor_room().map(Into::into),
is_favourite: room.is_favourite(),
canonical_alias: room.canonical_alias().map(Into::into),
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
+38 -225
View File
@@ -2,33 +2,29 @@
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt, TryFutureExt};
use matrix_sdk::ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
},
Room as SdkRoom,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_unread, BoxedFilterFn, RoomCategory,
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,
},
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;
use crate::{
error::ClientError,
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
utils::AsyncRuntimeDropped,
runtime::get_runtime_handle,
TaskHandle,
};
@@ -44,12 +40,6 @@ pub enum RoomListError {
RoomNotFound { room_name: String },
#[error("invalid room ID: {error}")]
InvalidRoomId { error: String },
#[error("A timeline instance already exists for room {room_name}")]
TimelineAlreadyExists { room_name: String },
#[error("A timeline instance hasn't been initialized for room {room_name}")]
TimelineNotInitialized { room_name: String },
#[error("Timeline couldn't be initialized: {error}")]
InitializingTimeline { error: String },
#[error("Event cache ran into an error: {error}")]
EventCache { error: String },
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
@@ -64,12 +54,6 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
UnknownList(list_name) => Self::UnknownList { list_name },
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
TimelineAlreadyExists(room_id) => {
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
}
InitializingTimeline(source) => {
Self::InitializingTimeline { error: source.to_string() }
}
EventCache(error) => Self::EventCache { error: error.to_string() },
}
}
@@ -101,13 +85,10 @@ impl RoomListService {
})))
}
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
Ok(Arc::new(RoomListItem {
inner: Arc::new(self.inner.room(room_id)?),
utd_hook: self.utd_hook.clone(),
}))
Ok(Arc::new(Room::new(self.inner.room(room_id)?, self.utd_hook.clone())))
}
async fn all_rooms(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
@@ -182,8 +163,7 @@ impl RoomList {
page_size: u32,
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self.clone();
let utd_hook = self.room_list_service.utd_hook.clone();
let this = self;
// The following code deserves a bit of explanation.
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
@@ -237,6 +217,7 @@ impl RoomList {
let dynamic_entries_controller =
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
let utd_hook = this.room_list_service.utd_hook.clone();
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(entries_stream);
@@ -244,7 +225,7 @@ impl RoomList {
listener.on_update(
diffs
.into_iter()
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
.collect(),
);
}
@@ -271,7 +252,7 @@ impl RoomList {
Arc::new(unsafe { result.assume_init() })
}
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
fn room(&self, room_id: String) -> Result<Arc<Room>, RoomListError> {
self.room_list_service.room(room_id)
}
}
@@ -362,63 +343,60 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceStateListener: Send + Sync + Debug {
pub trait RoomListServiceStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, state: RoomListServiceState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
pub trait RoomListLoadingStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, state: RoomListLoadingState);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
pub trait RoomListServiceSyncIndicatorListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
}
#[derive(uniffi::Enum)]
pub enum RoomListEntriesUpdate {
Append { values: Vec<Arc<RoomListItem>> },
Append { values: Vec<Arc<Room>> },
Clear,
PushFront { value: Arc<RoomListItem> },
PushBack { value: Arc<RoomListItem> },
PushFront { value: Arc<Room> },
PushBack { value: Arc<Room> },
PopFront,
PopBack,
Insert { index: u32, value: Arc<RoomListItem> },
Set { index: u32, value: Arc<RoomListItem> },
Insert { index: u32, value: Arc<Room> },
Set { index: u32, value: Arc<Room> },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<Arc<RoomListItem>> },
Reset { values: Vec<Arc<Room>> },
}
impl RoomListEntriesUpdate {
fn from(
vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
fn from(utd_hook: Option<Arc<UtdHookManager>>, vector_diff: VectorDiff<SdkRoom>) -> Self {
match vector_diff {
VectorDiff::Append { values } => Self::Append {
values: values
.into_iter()
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
.collect(),
},
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => {
Self::PushFront { value: Arc::new(RoomListItem::from(value, utd_hook)) }
Self::PushFront { value: Arc::new(Room::new(value, utd_hook)) }
}
VectorDiff::PushBack { value } => {
Self::PushBack { value: Arc::new(RoomListItem::from(value, utd_hook)) }
Self::PushBack { value: Arc::new(Room::new(value, utd_hook)) }
}
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => Self::Insert {
index: u32::try_from(index).unwrap(),
value: Arc::new(RoomListItem::from(value, utd_hook)),
value: Arc::new(Room::new(value, utd_hook)),
},
VectorDiff::Set { index, value } => Self::Set {
index: u32::try_from(index).unwrap(),
value: Arc::new(RoomListItem::from(value, utd_hook)),
value: Arc::new(Room::new(value, utd_hook)),
},
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
VectorDiff::Truncate { length } => {
@@ -427,7 +405,7 @@ impl RoomListEntriesUpdate {
VectorDiff::Reset { values } => Self::Reset {
values: values
.into_iter()
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
.map(|value| Arc::new(Room::new(value, utd_hook.clone())))
.collect(),
},
}
@@ -435,7 +413,7 @@ impl RoomListEntriesUpdate {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListEntriesListener: Send + Sync + Debug {
pub trait RoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
}
@@ -480,6 +458,7 @@ pub enum RoomListEntriesDynamicFilterKind {
None,
NormalizedMatchRoomName { pattern: String },
FuzzyMatchRoomName { pattern: String },
DeduplicateVersions,
}
#[derive(uniffi::Enum)]
@@ -521,177 +500,11 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
Kind::FuzzyMatchRoomName { pattern } => {
Box::new(new_filter_fuzzy_match_room_name(&pattern))
}
Kind::DeduplicateVersions => Box::new(new_filter_deduplicate_versions()),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomListItem {
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl RoomListItem {
fn from(
value: matrix_sdk_ui::room_list_service::Room,
utd_hook: Option<Arc<UtdHookManager>>,
) -> Self {
Self { inner: Arc::new(value), utd_hook }
}
}
#[matrix_sdk_ffi_macros::export]
impl RoomListItem {
fn id(&self) -> String {
self.inner.id().to_string()
}
/// Returns the room's name from the state event if available, otherwise
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
fn display_name(&self) -> Option<String> {
self.inner.cached_display_name()
}
fn avatar_url(&self) -> Option<String> {
self.inner.avatar_url().map(|uri| uri.to_string())
}
async fn is_direct(&self) -> bool {
self.inner.inner_room().is_direct().await.unwrap_or(false)
}
fn canonical_alias(&self) -> Option<String> {
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
}
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
RoomInfo::new(self.inner.inner_room()).await
}
/// The room's current membership state.
fn membership(&self) -> Membership {
self.inner.inner_room().state().into()
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited, knocked or banned rooms.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
{
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
(room_or_alias_id, server_names)
};
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
}
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
/// Build a full `Room` FFI object, filling its associated timeline.
///
/// An error will be returned if the room is a state different than joined
/// or if its internal timeline hasn't been initialized.
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
if let Some(timeline) = self.inner.timeline() {
Ok(Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
)))
} else {
Err(RoomListError::TimelineNotInitialized {
room_name: self.inner.inner_room().room_id().to_string(),
})
}
}
/// Checks whether the Room's timeline has been initialized before.
fn is_timeline_initialized(&self) -> bool {
self.inner.is_timeline_initialized()
}
/// Initializes the timeline for this room using the provided parameters.
///
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
/// used to filter timeline events besides the default timeline filter. If
/// `None` is passed, only the default timeline filter will be used.
/// * `internal_id_prefix` - An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
/// distinguish different timeline instances from each other.
async fn init_timeline(
&self,
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
internal_id_prefix: Option<String>,
) -> Result<(), RoomListError> {
let mut timeline_builder = self
.inner
.default_room_timeline_builder()
.await
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
if let Some(event_type_filter) = event_type_filter {
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
});
}
if let Some(internal_id_prefix) = internal_id_prefix {
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
}
if let Some(utd_hook) = self.utd_hook.clone() {
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
}
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
}
/// Checks whether the room is encrypted or not.
///
/// **Note**: this info may not be reliable if you don't set up
/// `m.room.encryption` as required state.
async fn is_encrypted(&self) -> bool {
self.inner
.latest_encryption_state()
.await
.map(|state| state.is_encrypted())
.unwrap_or(false)
}
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event().await.map(Into::into)
}
}
#[derive(uniffi::Object)]
pub struct UnreadNotificationsCount {
highlight_count: u32,
+1 -9
View File
@@ -59,15 +59,7 @@ impl RoomPreview {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
room.leave().await.map_err(ClientError::from)?;
if should_forget {
_ = self.forget().await;
}
Ok(())
Ok(room.leave().await?)
}
/// Get the user who created the invite, if any.
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
// 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.
//! Runtime abstractions for cross-platform async execution. This provides
//! a stand-in for tokio's `get_runtime_handle` method that will work on
//! both Wasm and non-Wasm platforms. It also provides corresponding types
//! that can be used in place of tokio's `Handle` and `Runtime` types.
#[cfg(not(target_family = "wasm"))]
mod sys {
pub use tokio::runtime::Handle;
/// Get a runtime handle appropriate for the current target platform.
///
/// This function returns a unified `Handle` type that works across both
/// Wasm and non-Wasm platforms, allowing code to be written that is
/// agnostic to the platform-specific runtime implementation.
///
/// Returns:
/// - A `tokio::runtime::Handle` on non-Wasm platforms
/// - A `WasmRuntimeHandle` on Wasm platforms
pub fn get_runtime_handle() -> Handle {
async_compat::get_runtime_handle()
}
}
#[cfg(target_family = "wasm")]
mod sys {
use std::future::Future;
use crate::executor::{spawn, JoinHandle};
/// A dummy guard that does nothing when dropped.
/// This is used for the Wasm implementation to match
/// tokio::runtime::EnterGuard.
#[derive(Debug)]
pub struct RuntimeGuard;
/// A runtime handle implementation for WebAssembly targets.
///
/// This implements a minimal subset of the tokio::runtime::Handle API
/// that is needed for the matrix-rust-sdk to function on Wasm.
#[derive(Default, Debug)]
pub struct Handle;
pub type Runtime = Handle;
impl Handle {
/// Spawns a future in the wasm32 bindgen runtime.
#[track_caller]
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + 'static,
F::Output: 'static,
{
spawn(future)
}
/// Runs the provided function on an executor dedicated to blocking
/// operations.
#[track_caller]
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + 'static,
R: 'static,
{
spawn(async move { func() })
}
/// Runs a future to completion on the current thread.
pub fn block_on<F, T>(&self, future: F) -> T
where
F: Future<Output = T>,
{
futures_executor::block_on(future)
}
/// Enters the runtime context.
///
/// For WebAssembly, this is a no-op that returns a dummy guard.
pub fn enter(&self) -> RuntimeGuard {
RuntimeGuard
}
}
/// Get a runtime handle appropriate for the current target platform.
///
/// This function returns a unified `Handle` type that works across both
/// Wasm and non-Wasm platforms, allowing code to be written that is
/// agnostic to the platform-specific runtime implementation.
///
/// Returns:
/// - A `tokio::runtime::Handle` on non-Wasm platforms
/// - A `WasmRuntimeHandle` on Wasm platforms
pub fn get_runtime_handle() -> Handle {
Handle
}
}
pub use sys::*;
@@ -1,6 +1,5 @@
use std::sync::{Arc, RwLock};
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
@@ -11,10 +10,13 @@ use matrix_sdk::{
ruma::events::key::verification::VerificationMethod,
Account,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::UserId;
use tracing::{error, warn};
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
use crate::{
client::UserProfile, error::ClientError, runtime::get_runtime_handle, utils::Timestamp,
};
#[derive(uniffi::Object)]
pub struct SessionVerificationEmoji {
@@ -51,7 +53,7 @@ pub struct SessionVerificationRequestDetails {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
pub trait SessionVerificationControllerDelegate: SyncOutsideWasm + SendOutsideWasm {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
@@ -94,7 +96,7 @@ impl SessionVerificationController {
.encryption
.get_verification_request(&sender_id, flow_id)
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
.ok_or(ClientError::from_str("Unknown session verification request", None))?;
self.set_ongoing_verification_request(verification_request)
}
@@ -131,10 +133,10 @@ impl SessionVerificationController {
.encryption
.get_user_identity(&user_id)
.await?
.ok_or(ClientError::new("Unknown user identity"))?;
.ok_or(ClientError::from_str("Unknown user identity", None))?;
if user_identity.is_verified() {
return Err(ClientError::new("User is already verified"));
return Err(ClientError::from_str("User is already verified", None));
}
let methods = vec![VerificationMethod::SasV1];
@@ -153,7 +155,7 @@ impl SessionVerificationController {
let verification_request = self.verification_request.read().unwrap().clone();
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
return Err(ClientError::from_str("Verification request missing.", None));
};
match verification_request.start_sas().await {
@@ -183,7 +185,7 @@ impl SessionVerificationController {
let sas_verification = self.sas_verification.read().unwrap().clone();
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
return Err(ClientError::from_str("SAS verification missing", None));
};
Ok(sas_verification.confirm().await?)
@@ -194,7 +196,7 @@ impl SessionVerificationController {
let sas_verification = self.sas_verification.read().unwrap().clone();
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
return Err(ClientError::from_str("SAS verification missing", None));
};
Ok(sas_verification.mismatch().await?)
@@ -205,7 +207,7 @@ impl SessionVerificationController {
let verification_request = self.verification_request.read().unwrap().clone();
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
return Err(ClientError::from_str("Verification request missing.", None));
};
Ok(verification_request.cancel().await?)
@@ -239,7 +241,7 @@ impl SessionVerificationController {
if sender != self.user_identity.user_id() {
if let Some(status) = self.encryption.cross_signing_status().await {
if !status.is_complete() {
warn!("Cannot verify other users until our own device's cross-signing status is complete: {:?}", status);
warn!("Cannot verify other users until our own device's cross-signing status is complete: {status:?}");
return;
}
}
@@ -285,7 +287,10 @@ impl SessionVerificationController {
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::new("There is another verification flow ongoing."));
return Err(ClientError::from_str(
"There is another verification flow ongoing.",
None,
));
}
}
+11 -125
View File
@@ -12,24 +12,22 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc, time::Duration};
use std::{fmt::Debug, sync::Arc};
use async_compat::get_runtime_handle;
use futures_util::pin_mut;
use matrix_sdk::{crypto::types::events::UtdCause, Client};
use matrix_sdk::Client;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
},
unable_to_decrypt_hook::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
},
unable_to_decrypt_hook::UtdHookManager,
};
use tracing::error;
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle, TaskHandle,
};
#[derive(uniffi::Enum)]
@@ -54,7 +52,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
pub trait SyncServiceStateObserver: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, state: SyncServiceState);
}
@@ -96,19 +94,13 @@ impl SyncService {
#[derive(Clone, uniffi::Object)]
pub struct SyncServiceBuilder {
client: Client,
builder: MatrixSyncServiceBuilder,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl SyncServiceBuilder {
pub(crate) fn new(client: Client) -> Arc<Self> {
Arc::new(Self {
client: client.clone(),
builder: MatrixSyncService::builder(client),
utd_hook: None,
})
pub(crate) fn new(client: Client, utd_hook: Option<Arc<UtdHookManager>>) -> Arc<Self> {
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook })
}
}
@@ -117,40 +109,14 @@ impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
Arc::new(Self { builder, ..this })
}
/// Enable the "offline" mode for the [`SyncService`].
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_offline_mode();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
pub async fn with_utd_hook(
self: Arc<Self>,
delegate: Box<dyn UnableToDecryptDelegate>,
) -> Arc<Self> {
// UTDs detected before this duration may be reclassified as "late decryption"
// events (or discarded, if they get decrypted fast enough).
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
let this = unwrap_or_clone_arc(self);
let mut utd_hook = UtdHookManager::new(Arc::new(UtdHook { delegate }), this.client.clone())
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
if let Err(e) = utd_hook.reload_from_store().await {
error!("Unable to reload UTD hook data from data store: {}", e);
// Carry on with the setup anyway; we shouldn't fail setup just
// because the UTD hook failed to load its data.
}
Arc::new(Self {
client: this.client,
builder: this.builder,
utd_hook: Some(Arc::new(utd_hook)),
})
Arc::new(Self { builder, ..this })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
@@ -161,83 +127,3 @@ impl SyncServiceBuilder {
}))
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
struct UtdHook {
delegate: Box<dyn UnableToDecryptDelegate>,
}
impl std::fmt::Debug for UtdHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UtdHook").finish_non_exhaustive()
}
}
impl UnableToDecryptHook for UtdHook {
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
// not considered UTDs.
if let Some(duration) = &info.time_to_decrypt {
if *duration < IGNORE_UTD_PERIOD {
return;
}
}
// Report the UTD to the client.
self.delegate.on_utd(info.into());
}
}
#[derive(uniffi::Record)]
pub struct UnableToDecryptInfo {
/// The identifier of the event that couldn't get decrypted.
event_id: String,
/// If the event could be decrypted late (that is, the event was encrypted
/// at first, but could be decrypted later on), then this indicates the
/// time it took to decrypt the event. If it is not set, this is
/// considered a definite UTD.
///
/// If set, this is in milliseconds.
pub time_to_decrypt_ms: Option<u64>,
/// What we know about what caused this UTD. E.g. was this event sent when
/// we were not a member of this room?
pub cause: UtdCause,
/// The difference between the event creation time (`origin_server_ts`) and
/// the time our device was created. If negative, this event was sent
/// *before* our device was created.
pub event_local_age_millis: i64,
/// Whether the user had verified their own identity at the point they
/// received the UTD event.
pub user_trusts_own_identity: bool,
/// The homeserver of the user that sent the undecryptable event.
pub sender_homeserver: String,
/// Our local user's own homeserver, or `None` if the client is not logged
/// in.
pub own_homeserver: Option<String>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
fn from(value: SdkUnableToDecryptInfo) -> Self {
Self {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
cause: value.cause,
event_local_age_millis: value.event_local_age_millis,
user_trusts_own_identity: value.user_trusts_own_identity,
sender_homeserver: value.sender_homeserver.to_string(),
own_homeserver: value.own_homeserver.map(String::from),
}
}
}
@@ -63,9 +63,28 @@ impl From<FilterTimelineEventType> for TimelineEventType {
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
Live,
Event { event_id: String, num_context_events: u16 },
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
Live {
/// Whether to hide in-thread replies from the live timeline.
hide_threaded_events: bool,
},
Event {
/// The initial event to focus on. This is usually the target of a
/// permalink.
event_id: String,
/// The number of context events to load around the focused event.
num_context_events: u16,
/// Whether to hide in-thread replies from the live timeline.
hide_threaded_events: bool,
},
Thread {
/// The thread root event ID to focus on.
root_event_id: String,
num_events: u16,
},
PinnedEvents {
max_events_to_load: u16,
max_concurrent_requests: u16,
},
}
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
@@ -75,15 +94,29 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
value: TimelineFocus,
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
match value {
TimelineFocus::Live => Ok(Self::Live),
TimelineFocus::Event { event_id, num_context_events } => {
TimelineFocus::Live { hide_threaded_events } => Ok(Self::Live { hide_threaded_events }),
TimelineFocus::Event { event_id, num_context_events, hide_threaded_events } => {
let parsed_event_id =
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
event_id: event_id.clone(),
err: err.to_string(),
})?;
Ok(Self::Event { target: parsed_event_id, num_context_events })
Ok(Self::Event {
target: parsed_event_id,
num_context_events,
hide_threaded_events,
})
}
TimelineFocus::Thread { root_event_id, num_events } => {
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
FocusEventError::InvalidEventId {
event_id: root_event_id.clone(),
err: err.to_string(),
}
})?;
Ok(Self::Thread { root_event_id: parsed_root_event_id, num_events })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
@@ -146,4 +179,8 @@ pub struct TimelineConfiguration {
/// As this has a non negligible performance impact, make sure to enable it
/// only when you need it.
pub track_read_receipts: bool,
/// Whether this timeline instance should report UTDs through the client's
/// delegate.
pub report_utds: bool,
}
@@ -96,6 +96,14 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
}
#[derive(Clone, uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineItemContent`, we would need to
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
// `uniffi::Object`, which is a radical change. It would simplify the memory
// usage, but it would slow down the performance around the FFI border. Thus,
// let's consider this is a false-positive lint in this particular case.
#[allow(clippy::large_enum_variant)]
pub enum TimelineItemContent {
MsgLike {
content: MsgLikeContent,
+296 -34
View File
@@ -16,7 +16,6 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt as _};
use matrix_sdk::{
@@ -32,11 +31,11 @@ use matrix_sdk::{
},
};
use matrix_sdk_ui::timeline::{
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
use reply::{InReplyToDetails, RepliedToEventDetails};
use reply::{EmbeddedEventDetails, InReplyToDetails};
use ruma::{
events::{
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
@@ -75,6 +74,7 @@ use crate::{
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
ThumbnailInfo, VideoInfo,
},
runtime::get_runtime_handle,
task_handle::TaskHandle,
utils::Timestamp,
};
@@ -85,6 +85,7 @@ mod msg_like;
mod reply;
use matrix_sdk::utils::formatted_body_from;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use crate::error::QueueWedgeError;
@@ -99,11 +100,6 @@ impl Timeline {
Arc::new(Self { inner })
}
pub(crate) fn from_arc(inner: Arc<matrix_sdk_ui::timeline::Timeline>) -> Arc<Self> {
// SAFETY: repr(transparent) means transmuting the arc this way is allowed
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
}
fn send_attachment(
self: Arc<Self>,
params: UploadParameters,
@@ -131,7 +127,7 @@ impl Timeline {
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
let mut request =
self.inner.send_attachment(params.filename, mime_type, attachment_config);
self.inner.send_attachment(params.source, mime_type, attachment_config);
if params.use_send_queue {
request = request.use_send_queue();
@@ -201,8 +197,8 @@ fn build_thumbnail_info(
#[derive(uniffi::Record)]
pub struct UploadParameters {
/// Filename (previously called "url") for the media to be sent.
filename: String,
/// Source from which to upload data
source: UploadSource,
/// Optional non-formatted caption, for clients that support it.
caption: Option<String>,
/// Optional HTML-formatted caption, for clients that support it.
@@ -217,6 +213,32 @@ pub struct UploadParameters {
use_send_queue: bool,
}
/// A source for uploading a file
#[derive(uniffi::Enum)]
pub enum UploadSource {
/// Upload source is a file on disk
File {
/// Path to file
filename: String,
},
/// Upload source is data in memory
Data {
/// Bytes being uploaded
bytes: Vec<u8>,
/// Filename to associate with bytes
filename: String,
},
}
impl From<UploadSource> for AttachmentSource {
fn from(value: UploadSource) -> Self {
match value {
UploadSource::File { filename } => Self::File(filename.into()),
UploadSource::Data { bytes, filename } => Self::Data { bytes, filename },
}
}
}
#[derive(uniffi::Record)]
pub struct ReplyParameters {
/// The ID of the event to reply to.
@@ -661,25 +683,28 @@ impl Timeline {
let event_id = EventId::parse(&event_id_str)?;
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
Ok(event) => RepliedToEvent::try_from_timeline_event_for_room(event, self.inner.room())
.await
.map_err(ClientError::from),
Ok(event) => self.inner.make_replied_to(event).await.map_err(ClientError::from),
Err(e) => Err(ClientError::from(e)),
};
match replied_to {
Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new(
Ok(Some(replied_to)) => Ok(Arc::new(InReplyToDetails::new(
event_id_str,
RepliedToEventDetails::Ready {
content: replied_to.content().clone().into(),
sender: replied_to.sender().to_string(),
sender_profile: replied_to.sender_profile().into(),
EmbeddedEventDetails::Ready {
content: replied_to.content.clone().into(),
sender: replied_to.sender.to_string(),
sender_profile: replied_to.sender_profile.into(),
},
))),
Ok(None) => Ok(Arc::new(InReplyToDetails::new(
event_id_str,
EmbeddedEventDetails::Error { message: "unsupported event".to_owned() },
))),
Err(e) => Ok(Arc::new(InReplyToDetails::new(
event_id_str,
RepliedToEventDetails::Error { message: e.to_string() },
EmbeddedEventDetails::Error { message: e.to_string() },
))),
}
}
@@ -783,12 +808,12 @@ pub enum FocusEventError {
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TimelineListener: Sync + Send {
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait PaginationStatusListener: Sync + Send {
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: RoomPaginationStatus);
}
@@ -1079,7 +1104,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
is_remote: !item.is_local_echo(),
event_or_transaction_id: item.identifier().into(),
sender: item.sender().to_string(),
sender_profile: item.sender_profile().into(),
sender_profile: item.sender_profile().clone().into(),
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
@@ -1120,13 +1145,13 @@ pub enum ProfileDetails {
Error { message: String },
}
impl From<&TimelineDetails<Profile>> for ProfileDetails {
fn from(details: &TimelineDetails<Profile>) -> Self {
impl From<TimelineDetails<Profile>> for ProfileDetails {
fn from(details: TimelineDetails<Profile>) -> Self {
match details {
TimelineDetails::Unavailable => Self::Unavailable,
TimelineDetails::Pending => Self::Pending,
TimelineDetails::Ready(profile) => Self::Ready {
display_name: profile.display_name.clone(),
display_name: profile.display_name,
display_name_ambiguous: profile.display_name_ambiguous,
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
},
@@ -1176,15 +1201,15 @@ impl TryFrom<PollData> for UnstablePollStartContentBlock {
#[derive(uniffi::Object)]
pub struct SendAttachmentJoinHandle {
join_hdl: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
abort_hdl: AbortHandle,
join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
abort_handle: AbortHandle,
}
impl SendAttachmentJoinHandle {
fn new(join_hdl: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
let abort_hdl = join_hdl.abort_handle();
let join_hdl = Arc::new(Mutex::new(join_hdl));
Arc::new(Self { join_hdl, abort_hdl })
fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
let abort_handle = join_handle.abort_handle();
let join_handle = Arc::new(Mutex::new(join_handle));
Arc::new(Self { join_handle, abort_handle })
}
}
@@ -1194,7 +1219,7 @@ impl SendAttachmentJoinHandle {
///
/// If the sending had been cancelled, will return immediately.
pub async fn join(&self) -> Result<(), RoomError> {
let handle = self.join_hdl.clone();
let handle = self.join_handle.clone();
let mut locked_handle = handle.lock().await;
let join_result = (&mut *locked_handle).await;
match join_result {
@@ -1213,7 +1238,7 @@ impl SendAttachmentJoinHandle {
///
/// A subsequent call to [`Self::join`] will return immediately.
pub fn cancel(&self) {
self.abort_hdl.abort();
self.abort_handle.abort();
}
}
@@ -1343,3 +1368,240 @@ impl LazyTimelineItemProvider {
self.0.contains_only_emojis()
}
}
#[cfg(feature = "unstable-msc4274")]
mod galleries {
use std::{panic, sync::Arc};
use async_compat::get_runtime_handle;
use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
utils::formatted_body_from,
};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use tokio::{
sync::Mutex,
task::{AbortHandle, JoinHandle},
};
use tracing::error;
use crate::{
error::RoomError,
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
};
#[derive(uniffi::Record)]
pub struct GalleryUploadParameters {
/// Optional non-formatted caption, for clients that support it.
caption: Option<String>,
/// Optional HTML-formatted caption, for clients that support it.
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>,
}
#[derive(uniffi::Enum)]
pub enum GalleryItemInfo {
Audio {
audio_info: AudioInfo,
filename: String,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
},
File {
file_info: FileInfo,
filename: String,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
},
Image {
image_info: ImageInfo,
filename: String,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
thumbnail_path: Option<String>,
},
Video {
video_info: VideoInfo,
filename: String,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
thumbnail_path: Option<String>,
},
}
impl GalleryItemInfo {
fn mimetype(&self) -> &Option<String> {
match self {
GalleryItemInfo::Audio { audio_info, .. } => &audio_info.mimetype,
GalleryItemInfo::File { file_info, .. } => &file_info.mimetype,
GalleryItemInfo::Image { image_info, .. } => &image_info.mimetype,
GalleryItemInfo::Video { video_info, .. } => &video_info.mimetype,
}
}
fn filename(&self) -> &String {
match self {
GalleryItemInfo::Audio { filename, .. } => filename,
GalleryItemInfo::File { filename, .. } => filename,
GalleryItemInfo::Image { filename, .. } => filename,
GalleryItemInfo::Video { filename, .. } => filename,
}
}
fn caption(&self) -> &Option<String> {
match self {
GalleryItemInfo::Audio { caption, .. } => caption,
GalleryItemInfo::File { caption, .. } => caption,
GalleryItemInfo::Image { caption, .. } => caption,
GalleryItemInfo::Video { caption, .. } => caption,
}
}
fn formatted_caption(&self) -> &Option<FormattedBody> {
match self {
GalleryItemInfo::Audio { formatted_caption, .. } => formatted_caption,
GalleryItemInfo::File { formatted_caption, .. } => formatted_caption,
GalleryItemInfo::Image { formatted_caption, .. } => formatted_caption,
GalleryItemInfo::Video { formatted_caption, .. } => formatted_caption,
}
}
fn attachment_info(&self) -> Result<AttachmentInfo, RoomError> {
match self {
GalleryItemInfo::Audio { audio_info, .. } => Ok(AttachmentInfo::Audio(
BaseAudioInfo::try_from(audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
)),
GalleryItemInfo::File { file_info, .. } => Ok(AttachmentInfo::File(
BaseFileInfo::try_from(file_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
)),
GalleryItemInfo::Image { image_info, .. } => Ok(AttachmentInfo::Image(
BaseImageInfo::try_from(image_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
)),
GalleryItemInfo::Video { video_info, .. } => Ok(AttachmentInfo::Video(
BaseVideoInfo::try_from(video_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
)),
}
}
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::Video { video_info, thumbnail_path, .. } => {
build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone())
}
}
}
}
impl TryInto<matrix_sdk_ui::timeline::GalleryItemInfo> for GalleryItemInfo {
type Error = RoomError;
fn try_into(
self,
) -> std::result::Result<matrix_sdk_ui::timeline::GalleryItemInfo, Self::Error> {
let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
source: self.filename().into(),
content_type: mime_type,
attachment_info: self.attachment_info()?,
caption: self.caption().clone(),
formatted_caption: self
.formatted_caption()
.clone()
.map(ruma::events::room::message::FormattedBody::from),
thumbnail: self.thumbnail()?,
})
}
}
#[derive(uniffi::Object)]
pub struct SendGalleryJoinHandle {
join_handle: Arc<Mutex<JoinHandle<Result<(), RoomError>>>>,
abort_handle: AbortHandle,
}
impl SendGalleryJoinHandle {
fn new(join_handle: JoinHandle<Result<(), RoomError>>) -> Arc<Self> {
let abort_handle = join_handle.abort_handle();
let join_handle = Arc::new(Mutex::new(join_handle));
Arc::new(Self { join_handle, abort_handle })
}
}
#[matrix_sdk_ffi_macros::export]
impl SendGalleryJoinHandle {
/// Wait until the gallery has been sent.
///
/// If the sending had been cancelled, will return immediately.
pub async fn join(&self) -> Result<(), RoomError> {
let handle = self.join_handle.clone();
let mut locked_handle = handle.lock().await;
let join_result = (&mut *locked_handle).await;
match join_result {
Ok(res) => res,
Err(err) => {
if err.is_cancelled() {
return Ok(());
}
error!("task panicked! resuming panic from here.");
panic::resume_unwind(err.into_panic());
}
}
}
/// Cancel the current sending task.
///
/// A subsequent call to [`Self::join`] will return immediately.
pub fn cancel(&self) {
self.abort_handle.abort();
}
}
#[matrix_sdk_ffi_macros::export]
impl Timeline {
pub fn send_gallery(
self: Arc<Self>,
params: GalleryUploadParameters,
item_infos: Vec<GalleryItemInfo>,
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
let formatted_caption = formatted_body_from(
params.caption.as_deref(),
params.formatted_caption.map(Into::into),
);
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()?);
for item_info in item_infos {
gallery_config = gallery_config.add_item(item_info.try_into()?);
}
let handle = SendGalleryJoinHandle::new(get_runtime_handle().spawn(async move {
let request = self.inner.send_gallery(gallery_config);
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
Ok(())
}));
Ok(handle)
}
}
}
@@ -17,7 +17,10 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
use super::{content::Reaction, reply::InReplyToDetails};
use super::{
content::Reaction,
reply::{EmbeddedEventDetails, InReplyToDetails},
};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
@@ -56,10 +59,12 @@ pub enum MsgLikeKind {
pub struct MsgLikeContent {
pub kind: MsgLikeKind,
pub reactions: Vec<Reaction>,
/// Event ID of the thread root, if this is a threaded message.
pub thread_root: Option<String>,
/// The event this message is replying to, if any.
pub in_reply_to: Option<Arc<InReplyToDetails>>,
/// Event ID of the thread root, if this is a message in a thread.
pub thread_root: Option<String>,
/// Details about the thread this message is the root of.
pub thread_summary: Option<Arc<ThreadSummary>>,
}
#[derive(Clone, uniffi::Record)]
@@ -95,6 +100,8 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
let thread_root = value.thread_root.map(|id| id.to_string());
let thread_summary = value.thread_summary.map(|t| Arc::new(t.into()));
Ok(match value.kind {
Kind::Message(message) => {
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
@@ -112,6 +119,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Sticker(sticker) => {
@@ -134,6 +142,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Poll(poll_state) => {
@@ -156,16 +165,22 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Redacted => {
Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root }
}
Kind::Redacted => Self {
kind: MsgLikeKind::Redacted,
reactions,
in_reply_to,
thread_root,
thread_summary,
},
Kind::UnableToDecrypt(msg) => Self {
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
reactions,
in_reply_to,
thread_root,
thread_summary,
},
})
}
@@ -222,3 +237,25 @@ pub struct PollAnswer {
pub id: String,
pub text: String,
}
#[derive(Clone, uniffi::Object)]
pub struct ThreadSummary {
pub latest_event: EmbeddedEventDetails,
pub num_replies: usize,
}
#[matrix_sdk_ffi_macros::export]
impl ThreadSummary {
pub fn latest_event(&self) -> EmbeddedEventDetails {
self.latest_event.clone()
}
}
impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self {
Self {
latest_event: EmbeddedEventDetails::from(value.latest_event),
num_replies: value.num_replies,
}
}
}
+22 -20
View File
@@ -12,18 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_ui::timeline::TimelineDetails;
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
event: EmbeddedEventDetails,
}
impl InReplyToDetails {
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
pub(crate) fn new(event_id: String, event: EmbeddedEventDetails) -> Self {
Self { event_id, event }
}
}
@@ -34,35 +34,37 @@ impl InReplyToDetails {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
pub fn event(&self) -> EmbeddedEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: event.content().clone().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
TimelineDetails::Error(err) => {
RepliedToEventDetails::Error { message: err.to_string() }
}
};
Self { event_id, event }
Self { event_id: inner.event_id.to_string(), event: inner.event.into() }
}
}
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
#[allow(clippy::large_enum_variant)]
pub enum EmbeddedEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
impl From<TimelineDetails<Box<EmbeddedEvent>>> for EmbeddedEventDetails {
fn from(event: TimelineDetails<Box<EmbeddedEvent>>) -> Self {
match event {
TimelineDetails::Unavailable => EmbeddedEventDetails::Unavailable,
TimelineDetails::Pending => EmbeddedEventDetails::Pending,
TimelineDetails::Ready(event) => EmbeddedEventDetails::Ready {
content: event.content.into(),
sender: event.sender.to_string(),
sender_profile: event.sender_profile.into(),
},
TimelineDetails::Error(err) => EmbeddedEventDetails::Error { message: err.to_string() },
}
}
}
+101
View File
@@ -0,0 +1,101 @@
// 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, time::Duration};
use matrix_sdk::crypto::types::events::UtdCause;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::unable_to_decrypt_hook::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
};
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: SyncOutsideWasm + SendOutsideWasm {
fn on_utd(&self, info: UnableToDecryptInfo);
}
pub struct UtdHook {
pub delegate: Arc<dyn UnableToDecryptDelegate>,
}
impl std::fmt::Debug for UtdHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UtdHook").finish_non_exhaustive()
}
}
impl UnableToDecryptHook for UtdHook {
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
// not considered UTDs.
if let Some(duration) = &info.time_to_decrypt {
if *duration < IGNORE_UTD_PERIOD {
return;
}
}
// Report the UTD to the client.
self.delegate.on_utd(info.into());
}
}
#[derive(uniffi::Record)]
pub struct UnableToDecryptInfo {
/// The identifier of the event that couldn't get decrypted.
event_id: String,
/// If the event could be decrypted late (that is, the event was encrypted
/// at first, but could be decrypted later on), then this indicates the
/// time it took to decrypt the event. If it is not set, this is
/// considered a definite UTD.
///
/// If set, this is in milliseconds.
pub time_to_decrypt_ms: Option<u64>,
/// What we know about what caused this UTD. E.g. was this event sent when
/// we were not a member of this room?
pub cause: UtdCause,
/// The difference between the event creation time (`origin_server_ts`) and
/// the time our device was created. If negative, this event was sent
/// *before* our device was created.
pub event_local_age_millis: i64,
/// Whether the user had verified their own identity at the point they
/// received the UTD event.
pub user_trusts_own_identity: bool,
/// The homeserver of the user that sent the undecryptable event.
pub sender_homeserver: String,
/// Our local user's own homeserver, or `None` if the client is not logged
/// in.
pub own_homeserver: Option<String>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
fn from(value: SdkUnableToDecryptInfo) -> Self {
Self {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
cause: value.cause,
event_local_age_millis: value.event_local_age_millis,
user_trusts_own_identity: value.user_trusts_own_identity,
sender_homeserver: value.sender_homeserver.to_string(),
own_homeserver: value.own_homeserver.map(String::from),
}
}
}
+2 -1
View File
@@ -14,10 +14,11 @@
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::get_runtime_handle;
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
use tracing::warn;
use crate::runtime::get_runtime_handle;
#[derive(Debug, Clone)]
pub struct Timestamp(u64);
+30 -14
View File
@@ -1,15 +1,15 @@
use std::sync::{Arc, Mutex};
use async_compat::get_runtime_handle;
use language_tags::LanguageTag;
use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter},
widget::{MessageLikeEventFilter, StateEventFilter, ToDeviceEventFilter},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::room::Room;
use crate::{room::Room, runtime::get_runtime_handle};
#[derive(uniffi::Record)]
pub struct WidgetDriverAndHandle {
@@ -95,7 +95,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
///
/// # Arguments
/// * `widget_settings` - The widget settings to generate the url for.
/// * `room` - A matrix room which is used to query the logged in username
/// * `room` - A Matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to adapt
/// to the client. e.g. language, font-scale...
#[matrix_sdk_ffi_macros::export]
@@ -248,6 +248,11 @@ pub struct VirtualElementCallWidgetOptions {
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub sentry_environment: Option<String>,
//// - `true`: The webview should show the list of media devices it detects using
//// `enumerateDevices`.
/// - `false`: the webview shows a a list of devices injected by the
/// client. (used on ios & android)
pub controlled_media_devices: bool,
}
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
@@ -271,6 +276,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
rageshake_submit_url: value.rageshake_submit_url,
sentry_dsn: value.sentry_dsn,
sentry_environment: value.sentry_environment,
controlled_media_devices: value.controlled_media_devices,
}
}
}
@@ -321,7 +327,9 @@ pub fn get_element_call_required_permissions(
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read and send encryption keys
WidgetEventFilter::ToDevice { event_type: "io.element.call.encryption_keys".to_owned() },
// TODO change this to the appropriate to-device version once ready
// remove this once all matrixRTC call apps supports to-device encryption.
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
@@ -373,7 +381,7 @@ pub fn get_element_call_required_permissions(
state_key: format!("{own_user_id}_{own_device_id}"),
},
// The same as above but with an underscore.
// To work around the issue that state events starting with `@` have to be matrix id's
// To work around the issue that state events starting with `@` have to be Matrix id's
// but we use mxId+deviceId.
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
@@ -442,7 +450,7 @@ pub struct WidgetCapabilities {
/// Types of the messages that a widget wants to be able to send.
pub send: Vec<WidgetEventFilter>,
/// If this capability is requested by the widget, it can not operate
/// separately from the matrix client.
/// separately from the Matrix client.
///
/// This means clients should not offer to open the widget in a separate
/// browser/tab/webview that is not connected to the postmessage widget-api.
@@ -488,9 +496,11 @@ pub enum WidgetEventFilter {
StateWithType { event_type: String },
/// Matches state events with the given `type` and `state_key`.
StateWithTypeAndStateKey { event_type: String, state_key: String },
/// Matches to-device events with the given `event_type`.
ToDevice { event_type: String },
}
impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
impl From<WidgetEventFilter> for matrix_sdk::widget::Filter {
fn from(value: WidgetEventFilter) -> Self {
match value {
WidgetEventFilter::MessageLikeWithType { event_type } => {
@@ -505,13 +515,16 @@ impl From<WidgetEventFilter> for matrix_sdk::widget::EventFilter {
WidgetEventFilter::StateWithTypeAndStateKey { event_type, state_key } => {
Self::State(StateEventFilter::WithTypeAndStateKey(event_type.into(), state_key))
}
WidgetEventFilter::ToDevice { event_type } => {
Self::ToDevice(ToDeviceEventFilter { event_type: event_type.into() })
}
}
}
}
impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
fn from(value: matrix_sdk::widget::EventFilter) -> Self {
use matrix_sdk::widget::EventFilter as F;
impl From<matrix_sdk::widget::Filter> for WidgetEventFilter {
fn from(value: matrix_sdk::widget::Filter) -> Self {
use matrix_sdk::widget::Filter as F;
match value {
F::MessageLike(MessageLikeEventFilter::WithType(event_type)) => {
@@ -526,18 +539,22 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
F::State(StateEventFilter::WithTypeAndStateKey(event_type, state_key)) => {
Self::StateWithTypeAndStateKey { event_type: event_type.to_string(), state_key }
}
F::ToDevice(ToDeviceEventFilter { event_type }) => {
Self::ToDevice { event_type: event_type.to_string() }
}
}
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait WidgetCapabilitiesProvider: Send + Sync {
pub trait WidgetCapabilitiesProvider: SendOutsideWasm + SyncOutsideWasm {
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
}
struct CapabilitiesProviderWrap(Arc<dyn WidgetCapabilitiesProvider>);
#[async_trait]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
async fn acquire_capabilities(
&self,
@@ -631,8 +648,7 @@ mod tests {
let cap_assert = |capability: &str| {
assert!(
permission_array.contains(&capability.to_owned()),
"The \"{}\" capability was missing from the element call capability list.",
capability
"The \"{capability}\" capability was missing from the element call capability list."
);
};
+12
View File
@@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.12.0] - 2025-06-10
No notable changes in this release.
## [0.11.0] - 2025-04-11
### Features
@@ -40,6 +44,14 @@ All notable changes to this project will be documented in this file.
- [**breaking**] `BaseClient::set_session_metadata` is renamed
`activate`, and `BaseClient::logged_in` is renamed `is_activated`
([#4850](https://github.com/matrix-org/matrix-rust-sdk/pull/4850))
- [**breaking] `DependentQueuedRequestKind::UploadFileWithThumbnail`
was renamed to `DependentQueuedRequestKind::UploadFileOrThumbnail`.
Under the `unstable-msc4274` feature, `DependentQueuedRequestKind::UploadFileOrThumbnail`
and `SentMediaInfo` were generalized to allow chaining multiple dependent
file / thumbnail uploads.
([#4897](https://github.com/matrix-org/matrix-rust-sdk/pull/4897))
- [**breaking**] `RoomInfo::prev_state` has been removed due to being useless.
([#5054](https://github.com/matrix-org/matrix-rust-sdk/pull/5054))
## [0.10.0] - 2025-02-04
+38 -29
View File
@@ -8,19 +8,25 @@ license = "Apache-2.0"
name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.11.0"
rust-version.workspace = true
version = "0.12.0"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]
[features]
default = []
e2e-encryption = ["dep:matrix-sdk-crypto"]
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
js = [
"matrix-sdk-common/js",
"matrix-sdk-crypto?/js",
"ruma/js",
"matrix-sdk-store-encryption/js",
]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
@@ -43,23 +49,26 @@ testing = [
"matrix-sdk-crypto?/testing",
]
# Add support for inline media galleries via msgtypes
unstable-msc4274 = []
[dependencies]
as_variant = { workspace = true }
as_variant.workspace = true
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.8.0", features = ["serde"] }
decancer = "3.2.8"
async-trait.workspace = true
bitflags = { workspace = true, features = ["serde"] }
decancer = "3.3.0"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
growable-bloom-filter = { workspace = true }
eyeball-im.workspace = true
futures-util.workspace = true
growable-bloom-filter.workspace = true
http = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
matrix-sdk-common.workspace = true
matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-store-encryption.workspace = true
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
once_cell.workspace = true
regex = "1.11.1"
ruma = { workspace = true, features = [
"canonical-json",
@@ -68,29 +77,29 @@ ruma = { workspace = true, features = [
"unstable-msc4186",
"rand",
] }
unicode-normalization = { workspace = true }
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
unicode-normalization.workspace = true
uniffi = { workspace = true, optional = true }
[dev-dependencies]
assert_matches = { workspace = true }
assert_matches2 = { 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 }
stream_assert = { workspace = true }
similar-asserts = { workspace = true }
futures-executor.workspace = true
http.workspace = true
matrix-sdk-test.workspace = true
similar-asserts.workspace = true
stream_assert.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = { workspace = true }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
wasm-bindgen-test.workspace = true
[lints]
workspace = true
+126 -324
View File
@@ -17,7 +17,7 @@
use std::sync::Arc;
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
fmt, iter,
fmt,
ops::Deref,
};
@@ -36,37 +36,36 @@ use ruma::DeviceId;
use ruma::{
api::client::{self as api, sync::sync_events::v5},
events::{
ignored_user_list::IgnoredUserListEventContent,
push_rules::{PushRulesEvent, PushRulesEventContent},
room::member::SyncRoomMemberEvent,
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, StateEvent, StateEventType,
StateEvent, StateEventType,
},
push::{Action, Ruleset},
serde::Raw,
push::Ruleset,
time::Instant,
OwnedRoomId, OwnedUserId, RoomId,
OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tokio::sync::{broadcast, Mutex};
#[cfg(feature = "e2e-encryption")]
use tokio::sync::{RwLock, RwLockReadGuard};
use tracing::{debug, info, instrument};
use tracing::{debug, enabled, info, instrument, warn, Level};
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent},
deserialized_responses::DisplayName,
error::{Error, Result},
event_cache::store::EventCacheStoreLock,
response_processors::{self as processors, Context},
rooms::{
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate},
Room, RoomInfo, RoomState,
room::{
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
},
store::{
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
StateStoreDataValue, StateStoreExt, StoreConfig,
},
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
sync::{RoomUpdates, SyncResponse},
RoomStateFilter, SessionMeta,
};
@@ -367,65 +366,6 @@ impl BaseClient {
self.state_store.sync_token.read().await.clone()
}
/// Handles the stripped state events in `invite_state`, modifying the
/// room's info and posting notifications as needed.
///
/// * `room` - The [`Room`] to modify.
/// * `events` - The contents of `invite_state` in the form of list of pairs
/// of raw stripped state events with their deserialized counterpart.
/// * `push_rules` - The push rules for this room.
/// * `room_info` - The current room's info.
/// * `changes` - The accumulated list of changes to apply once the
/// processing is finished.
/// * `notifications` - Notifications to post for the current room.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub(crate) async fn handle_invited_state(
&self,
context: &mut Context,
room: &Room,
events: (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>),
push_rules: &Ruleset,
room_info: &mut RoomInfo,
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
) -> Result<()> {
let mut state_events = BTreeMap::new();
for (raw_event, event) in iter::zip(events.0, events.1) {
room_info.handle_stripped_state_event(&event);
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event);
}
context
.state_changes
.stripped_state
.insert(room_info.room_id().to_owned(), state_events.clone());
// 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_context) =
processors::timeline::get_push_room_context(context, room, room_info, &self.state_store)
.await?
{
// Check every event again for notification.
for event in state_events.values().flat_map(|map| map.values()) {
let actions = push_rules.get_actions(event, &push_context);
if actions.iter().any(Action::should_notify) {
notifications.entry(room.room_id().to_owned()).or_default().push(
Notification {
actions: actions.to_owned(),
event: RawAnySyncOrStrippedTimelineEvent::Stripped(event.clone()),
},
);
}
}
}
Ok(())
}
/// User has knocked on a room.
///
/// Update the internal and cached state accordingly. Return the final Room.
@@ -546,25 +486,19 @@ impl BaseClient {
return Ok(SyncResponse::default());
}
let now = Instant::now();
let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
#[cfg(feature = "e2e-encryption")]
let olm_machine = self.olm_machine().await;
let mut context =
Context::new(StateChanges::new(response.next_batch.clone()), Default::default());
let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
#[cfg(feature = "e2e-encryption")]
let to_device = {
let processors::e2ee::to_device::Output {
decrypted_to_device_events: to_device,
room_key_updates,
} = processors::e2ee::to_device::from_sync_v2(
&mut context,
&response,
olm_machine.as_ref(),
)
.await?;
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
processors::latest_event::decrypt_from_rooms(
&mut context,
@@ -573,9 +507,11 @@ impl BaseClient {
.flatten()
.filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
.collect(),
olm_machine.as_ref(),
self.decryption_trust_requirement,
self.handle_verification_events,
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
self.handle_verification_events,
),
)
.await?;
@@ -592,87 +528,30 @@ impl BaseClient {
let push_rules = self.get_push_rules(&global_account_data_processor).await?;
let mut new_rooms = RoomUpdates::default();
let mut room_updates = RoomUpdates::default();
let mut notifications = Default::default();
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
BTreeMap::new();
for (room_id, new_info) in response.rooms.join {
let room = self.state_store.get_or_create_room(
&room_id,
RoomState::Joined,
self.room_info_notable_update_sender.clone(),
);
let mut room_info = room.clone_info();
room_info.mark_as_joined();
room_info.update_from_ruma_summary(&new_info.summary);
room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref());
room_info.mark_state_fully_synced();
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
let (raw_state_events, state_events) =
processors::state_events::sync::collect(&mut context, &new_info.state.events);
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
for (room_id, joined_room) in response.rooms.join {
let joined_room_update = processors::room::sync_v2::update_joined_room(
&mut context,
(&raw_state_events, &state_events),
&mut room_info,
&mut ambiguity_cache,
)
.await?;
for raw in &new_info.ephemeral.events {
match raw.deserialize() {
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
context.state_changes.add_receipts(&room_id, event.content);
}
Ok(_) => {}
Err(e) => {
let event_id: Option<String> = raw.get_field("event_id").ok().flatten();
#[rustfmt::skip]
info!(
?room_id, event_id,
"Failed to deserialize ephemeral room event: {e}"
);
}
}
}
if new_info.timeline.limited {
room_info.mark_members_missing();
}
let (raw_state_events_from_timeline, state_events_from_timeline) =
processors::state_events::sync::collect_from_timeline(
&mut context,
&new_info.timeline.events,
);
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
&mut context,
(&raw_state_events_from_timeline, &state_events_from_timeline),
&mut room_info,
&mut ambiguity_cache,
)
.await?;
new_user_ids.append(&mut other_new_user_ids);
updated_members_in_room.insert(room_id.to_owned(), new_user_ids.clone());
let timeline = processors::timeline::build(
&mut context,
&room,
&mut room_info,
processors::timeline::builder::Timeline::from(new_info.timeline),
processors::timeline::builder::Notification::new(
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
joined_room,
&mut updated_members_in_room,
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
processors::timeline::builder::E2EE::new(
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
self.handle_verification_events,
@@ -680,106 +559,26 @@ impl BaseClient {
)
.await?;
// Save the new `RoomInfo`.
context.state_changes.add_room(room_info);
processors::account_data::for_room(
&mut context,
&room_id,
&new_info.account_data.events,
&self.state_store,
)
.await;
// `Self::handle_room_account_data` might have updated the `RoomInfo`. Let's
// fetch it again.
//
// SAFETY: `unwrap` is safe because the `RoomInfo` has been inserted 2 lines
// above.
let mut room_info = context.state_changes.room_infos.get(&room_id).unwrap().clone();
#[cfg(feature = "e2e-encryption")]
processors::e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
&mut context,
olm_machine.as_ref(),
&new_user_ids,
room_info.encryption_state(),
room.encryption_state(),
&room_id,
&self.state_store,
)
.await?;
let notification_count = new_info.unread_notifications.into();
room_info.update_notification_count(notification_count);
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
new_rooms.join.insert(
room_id,
JoinedRoomUpdate::new(
timeline,
new_info.state.events,
new_info.account_data.events,
new_info.ephemeral.events,
notification_count,
ambiguity_changes,
),
);
context.state_changes.add_room(room_info);
room_updates.joined.insert(room_id, joined_room_update);
}
for (room_id, new_info) in response.rooms.leave {
let room = self.state_store.get_or_create_room(
&room_id,
RoomState::Left,
self.room_info_notable_update_sender.clone(),
);
let mut room_info = room.clone_info();
room_info.mark_as_left();
room_info.mark_state_partially_synced();
room_info.handle_encryption_state(requested_required_states.for_room(&room_id));
let (raw_state_events, state_events) =
processors::state_events::sync::collect(&mut context, &new_info.state.events);
let mut new_user_ids = processors::state_events::dispatch_and_get_new_users(
for (room_id, left_room) in response.rooms.leave {
let left_room_update = processors::room::sync_v2::update_left_room(
&mut context,
(&raw_state_events, &state_events),
&mut room_info,
&mut ambiguity_cache,
)
.await?;
let (raw_state_events_from_timeline, state_events_from_timeline) =
processors::state_events::sync::collect_from_timeline(
&mut context,
&new_info.timeline.events,
);
let mut other_new_user_ids = processors::state_events::dispatch_and_get_new_users(
&mut context,
(&raw_state_events_from_timeline, &state_events_from_timeline),
&mut room_info,
&mut ambiguity_cache,
)
.await?;
new_user_ids.append(&mut other_new_user_ids);
let timeline = processors::timeline::build(
&mut context,
&room,
&mut room_info,
processors::timeline::builder::Timeline::from(new_info.timeline),
processors::timeline::builder::Notification::new(
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
left_room,
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
processors::timeline::builder::E2EE::new(
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
self.decryption_trust_requirement,
self.handle_verification_events,
@@ -787,90 +586,41 @@ impl BaseClient {
)
.await?;
// Save the new `RoomInfo`.
context.state_changes.add_room(room_info);
room_updates.left.insert(room_id, left_room_update);
}
processors::account_data::for_room(
for (room_id, invited_room) in response.rooms.invite {
let invited_room_update = processors::room::sync_v2::update_invited_room(
&mut context,
&room_id,
&new_info.account_data.events,
&self.state_store,
)
.await;
let ambiguity_changes = ambiguity_cache.changes.remove(&room_id).unwrap_or_default();
new_rooms.leave.insert(
room_id,
LeftRoomUpdate::new(
timeline,
new_info.state.events,
new_info.account_data.events,
ambiguity_changes,
invited_room,
self.room_info_notable_update_sender.clone(),
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
);
}
for (room_id, new_info) in response.rooms.invite {
let room = self.state_store.get_or_create_room(
&room_id,
RoomState::Invited,
self.room_info_notable_update_sender.clone(),
);
let invite_state = processors::state_events::stripped::collect(
&mut context,
&new_info.invite_state.events,
);
let mut room_info = room.clone_info();
room_info.mark_as_invited();
room_info.mark_state_fully_synced();
self.handle_invited_state(
&mut context,
&room,
invite_state,
&push_rules,
&mut room_info,
&mut notifications,
)
.await?;
context.state_changes.add_room(room_info);
new_rooms.invite.insert(room_id, new_info);
room_updates.invited.insert(room_id, invited_room_update);
}
for (room_id, new_info) in response.rooms.knock {
let room = self.state_store.get_or_create_room(
for (room_id, knocked_room) in response.rooms.knock {
let knocked_room_update = processors::room::sync_v2::update_knocked_room(
&mut context,
&room_id,
RoomState::Knocked,
knocked_room,
self.room_info_notable_update_sender.clone(),
);
let knock_state = processors::state_events::stripped::collect(
&mut context,
&new_info.knock_state.events,
);
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
room_info.mark_state_fully_synced();
self.handle_invited_state(
&mut context,
&room,
knock_state,
&push_rules,
&mut room_info,
&mut notifications,
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
)
.await?;
context.state_changes.add_room(room_info);
new_rooms.knocked.insert(room_id, new_info);
room_updates.knocked.insert(room_id, knocked_room_update);
}
global_account_data_processor.apply(&mut context, &self.state_store).await;
@@ -899,12 +649,19 @@ impl BaseClient {
.await?;
}
let mut context = Context::default();
// Now that all the rooms information have been saved, update the display name
// cache (which relies on information stored in the database). This will
// live in memory, until the next sync which will saves the room info to
// disk; we do this to avoid saving that would be redundant with the
// above. Oh well.
new_rooms.update_in_memory_caches(&self.state_store).await;
// of the updated rooms (which relies on information stored in the database).
processors::room::display_name::update_for_rooms(
&mut context,
&room_updates,
&self.state_store,
)
.await;
// Save the new display name updates if any.
processors::changes::save_only(context, &self.state_store).await?;
for (room_id, member_ids) in updated_members_in_room {
if let Some(room) = self.get_room(&room_id) {
@@ -913,10 +670,12 @@ impl BaseClient {
}
}
info!("Processed a sync response in {:?}", now.elapsed());
if enabled!(Level::INFO) {
info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
}
let response = SyncResponse {
rooms: new_rooms,
rooms: room_updates,
presence: response.presence.events,
account_data: response.account_data.events,
to_device,
@@ -958,7 +717,7 @@ impl BaseClient {
};
let mut chunk = Vec::with_capacity(response.chunk.len());
let mut context = Context::new(StateChanges::default(), Default::default());
let mut context = Context::default();
#[cfg(feature = "e2e-encryption")]
let mut user_ids = BTreeSet::new();
@@ -1018,7 +777,6 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
processors::e2ee::tracked_users::update(
&mut context,
self.olm_machine().await.as_ref(),
room.encryption_state(),
&user_ids,
@@ -1203,6 +961,27 @@ impl BaseClient {
pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
self.room_info_notable_update_sender.subscribe()
}
/// Checks whether the provided `user_id` belongs to an ignored user.
pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
{
Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
Ok(current_ignored_user_list) => {
current_ignored_user_list.content.ignored_users.contains_key(user_id)
}
Err(error) => {
warn!(?error, "Failed to deserialize the ignored user list event");
false
}
},
Ok(None) => false,
Err(error) => {
warn!(?error, "Could not get the ignored user list from the state store");
false
}
}
}
}
/// Represent the `required_state` values sent by a sync request.
@@ -1568,7 +1347,7 @@ mod tests {
let room = client.get_room(room_id).expect("Room not found");
assert_eq!(room.state(), RoomState::Invited);
assert_eq!(
room.compute_display_name().await.expect("fetching display name failed"),
room.compute_display_name().await.expect("fetching display name failed").into_inner(),
RoomDisplayName::Calculated("Kyra".to_owned())
);
}
@@ -1840,4 +1619,27 @@ mod tests {
assert_let!(Some(ignored) = subscriber.next().await);
assert!(ignored.is_empty());
}
#[async_test]
async fn test_is_user_ignored() {
let ignored_user_id = user_id!("@alice:example.org");
let client = logged_in_base_client(None).await;
let mut sync_builder = SyncResponseBuilder::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",
}),
))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(client.is_user_ignored(ignored_user_id).await);
}
}
@@ -263,6 +263,18 @@ pub enum RawAnySyncOrStrippedTimelineEvent {
Stripped(Raw<AnyStrippedStateEvent>),
}
impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
Self::Sync(event)
}
}
impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
Self::Stripped(event)
}
}
/// Wrapper around both versions of any raw state event.
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
@@ -14,14 +14,17 @@
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use std::sync::Arc;
use assert_matches::assert_matches;
use async_trait::async_trait;
use matrix_sdk_common::{
deserialized_responses::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
VerificationState,
},
linked_chunk::{lazy_loader, ChunkContent, ChunkIdentifier as CId, Position, Update},
linked_chunk::{
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
@@ -56,16 +59,16 @@ pub fn make_test_event_with_event_id(
content: &str,
event_id: Option<&EventId>,
) -> TimelineEvent {
let encryption_info = EncryptionInfo {
let encryption_info = Arc::new(EncryptionInfo {
sender: (*ALICE).into(),
sender_device: None,
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "1337".to_owned(),
sender_claimed_keys: Default::default(),
session_id: Some("mysessionid9".to_owned()),
},
verification_state: VerificationState::Verified,
session_id: Some("mysessionid9".to_owned()),
};
});
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
if let Some(event_id) = event_id {
@@ -73,14 +76,10 @@ pub fn make_test_event_with_event_id(
}
let event = builder.into_raw_timeline().cast();
TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event,
encryption_info,
unsigned_encryption_info: None,
}),
push_actions: Some(vec![Action::Notify]),
}
TimelineEvent::from_decrypted(
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
Some(vec![Action::Notify]),
)
}
/// Check that an event created with [`make_test_event`] contains the expected
@@ -90,7 +89,7 @@ pub fn make_test_event_with_event_id(
#[track_caller]
pub fn check_test_event(event: &TimelineEvent, text: &str) {
// Check push actions.
let actions = event.push_actions.as_ref().unwrap();
let actions = event.push_actions().unwrap();
assert_eq!(actions.len(), 1);
assert_matches!(&actions[0], Action::Notify);
@@ -114,8 +113,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
///
/// This trait is not meant to be used directly, but will be used with the
/// `event_cache_store_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[allow(async_fn_in_trait)]
pub trait EventCacheStoreIntegrationTests {
/// Test media content storage.
async fn test_media_content(&self);
@@ -136,7 +134,7 @@ pub trait EventCacheStoreIntegrationTests {
async fn test_rebuild_empty_linked_chunk(&self);
/// Test that clear all the rooms' linked chunks works.
async fn test_clear_all_rooms_chunks(&self);
async fn test_clear_all_linked_chunks(&self);
/// Test that removing a room from storage empties all associated data.
async fn test_remove_room(&self);
@@ -154,8 +152,6 @@ pub trait EventCacheStoreIntegrationTests {
async fn test_save_event(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
@@ -343,9 +339,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
self.handle_linked_chunk_updates(
room_id,
linked_chunk_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -377,10 +374,11 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();
// The linked chunk is correctly reloaded.
let lc =
lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(room_id).await.unwrap())
.unwrap()
.unwrap();
let lc = lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(linked_chunk_id).await.unwrap(),
)
.unwrap()
.unwrap();
let mut chunks = lc.chunks();
@@ -421,19 +419,20 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_linked_chunk_incremental_loading(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
let event = |msg: &str| make_test_event(room_id, msg);
// Load the last chunk, but none exists yet.
{
let (last_chunk, chunk_identifier_generator) =
self.load_last_chunk(room_id).await.unwrap();
self.load_last_chunk(linked_chunk_id).await.unwrap();
assert!(last_chunk.is_none());
assert_eq!(chunk_identifier_generator.current(), 0);
}
self.handle_linked_chunk_updates(
room_id,
linked_chunk_id,
vec![
// new chunk for items
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -464,7 +463,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Load the last chunk.
let mut linked_chunk = {
let (last_chunk, chunk_identifier_generator) =
self.load_last_chunk(room_id).await.unwrap();
self.load_last_chunk(linked_chunk_id).await.unwrap();
assert_eq!(chunk_identifier_generator.current(), 2);
@@ -499,9 +498,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk =
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
let mut rchunks = linked_chunk.rchunks();
@@ -536,9 +535,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk =
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap().unwrap();
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
let mut rchunks = linked_chunk.rchunks();
@@ -585,7 +584,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Load the previous chunk: there is none.
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk = self.load_previous_chunk(room_id, first_chunk).await.unwrap();
let previous_chunk =
self.load_previous_chunk(linked_chunk_id, first_chunk).await.unwrap();
assert!(previous_chunk.is_none());
}
@@ -637,19 +637,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_rebuild_empty_linked_chunk(&self) {
// When I rebuild a linked chunk from an empty store, it's empty.
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(&DEFAULT_TEST_ROOM_ID).await.unwrap(),
self.load_all_chunks(LinkedChunkId::Room(&DEFAULT_TEST_ROOM_ID)).await.unwrap(),
)
.unwrap();
assert!(linked_chunk.is_none());
}
async fn test_clear_all_rooms_chunks(&self) {
async fn test_clear_all_linked_chunks(&self) {
let r0 = room_id!("!r0:matrix.org");
let linked_chunk_id0 = LinkedChunkId::Room(r0);
let r1 = room_id!("!r1:matrix.org");
let linked_chunk_id1 = LinkedChunkId::Room(r1);
// Add updates for the first room.
self.handle_linked_chunk_updates(
r0,
linked_chunk_id0,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -665,7 +667,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Add updates for the second room.
self.handle_linked_chunk_updates(
r1,
linked_chunk_id1,
vec![
// Empty items chunk.
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -689,32 +691,42 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();
// Sanity check: both linked chunks can be reloaded.
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
.unwrap()
.is_some());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).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_rooms_chunks().await.unwrap();
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(r0).await.unwrap())
.unwrap()
.is_none());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).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) {
let r0 = room_id!("!r0:matrix.org");
let linked_chunk_id0 = LinkedChunkId::Room(r0);
let r1 = room_id!("!r1:matrix.org");
let linked_chunk_id1 = LinkedChunkId::Room(r1);
// Add updates to the first room.
self.handle_linked_chunk_updates(
r0,
linked_chunk_id0,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -730,7 +742,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Add updates to the second room.
self.handle_linked_chunk_updates(
r1,
linked_chunk_id1,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
@@ -748,17 +760,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
self.remove_room(r0).await.unwrap();
// Check that r0 doesn't have a linked chunk anymore.
let r0_linked_chunk = self.load_all_chunks(r0).await.unwrap();
let r0_linked_chunk = self.load_all_chunks(linked_chunk_id0).await.unwrap();
assert!(r0_linked_chunk.is_empty());
// Check that r1 is unaffected.
let r1_linked_chunk = self.load_all_chunks(r1).await.unwrap();
let r1_linked_chunk = self.load_all_chunks(linked_chunk_id1).await.unwrap();
assert!(!r1_linked_chunk.is_empty());
}
async fn test_filter_duplicated_events(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
let another_room_id = room_id!("!r1:matrix.org");
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
@@ -770,7 +784,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
let event_mont_dor = event("mont d'or");
self.handle_linked_chunk_updates(
room_id,
linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
@@ -796,7 +810,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Add other events in another room, to ensure filtering take the `room_id` into
// account.
self.handle_linked_chunk_updates(
another_room_id,
another_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
@@ -810,7 +824,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
let duplicated_events = self
.filter_duplicated_events(
room_id,
linked_chunk_id,
vec![
event_comte.event_id().unwrap().to_owned(),
event_raclette.event_id().unwrap().to_owned(),
@@ -841,6 +855,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_find_event(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
@@ -848,7 +863,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Add one event in one room.
self.handle_linked_chunk_updates(
room_id,
LinkedChunkId::Room(room_id),
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
@@ -862,7 +877,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Add another event in another room.
self.handle_linked_chunk_updates(
another_room_id,
another_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
@@ -891,7 +906,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.is_none());
// Clearing the rooms also clears the event's storage.
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
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
@@ -1091,10 +1106,10 @@ macro_rules! event_cache_store_integration_tests {
}
#[async_test]
async fn test_clear_all_rooms_chunks() {
async fn test_clear_all_linked_chunks() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_clear_all_rooms_chunks().await;
event_cache_store.test_clear_all_linked_chunks().await;
}
#[async_test]
@@ -1141,7 +1156,7 @@ macro_rules! event_cache_store_integration_tests {
#[macro_export]
macro_rules! event_cache_store_integration_tests_time {
() => {
#[cfg(not(target_arch = "wasm32"))]
#[cfg(not(target_family = "wasm"))]
mod event_cache_store_integration_tests_time {
use std::time::Duration;
@@ -15,7 +15,6 @@
//! Trait and macro of integration tests for `EventCacheStoreMedia`
//! implementations.
use async_trait::async_trait;
use ruma::{
events::room::MediaSource,
mxc_uri, owned_mxc_uri,
@@ -31,8 +30,7 @@ use crate::media::{MediaFormat, MediaRequestParameters};
///
/// This trait is not meant to be used directly, but will be used with the
/// `event_cache_store_media_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[allow(async_fn_in_trait)]
pub trait EventCacheStoreMediaIntegrationTests {
/// Test media retention policy storage.
async fn test_store_media_retention_policy(&self);
@@ -58,8 +56,6 @@ pub trait EventCacheStoreMediaIntegrationTests {
async fn test_store_last_media_cleanup_time(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<Store> EventCacheStoreMediaIntegrationTests for Store
where
Store: EventCacheStoreMedia + std::fmt::Debug,
@@ -356,8 +356,8 @@ where
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
/// simplify the implementation of tests by being able to have complete control
/// over the `SystemTime`s provided to the store.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
/// The error type used by this media cache store.
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
@@ -630,8 +630,8 @@ mod tests {
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
type Error = MockEventCacheStoreMediaError;
@@ -21,8 +21,8 @@ use std::{
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator, Position,
RawChunk, Update,
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
LinkedChunkId, Position, RawChunk, Update,
},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
@@ -110,8 +110,8 @@ impl MemoryStore {
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl EventCacheStore for MemoryStore {
type Error = EventCacheStoreError;
@@ -128,57 +128,57 @@ impl EventCacheStore for MemoryStore {
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
inner.events.apply_updates(room_id, updates);
inner.events.apply_updates(linked_chunk_id, updates);
Ok(())
}
async fn load_all_chunks(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_all_chunks(room_id)
.load_all_chunks(linked_chunk_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_last_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_last_chunk(room_id)
.load_last_chunk(linked_chunk_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_previous_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_previous_chunk(room_id, before_chunk_identifier)
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
self.inner.write().unwrap().events.clear();
Ok(())
}
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
mut events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
// Collect all duplicated events.
@@ -186,7 +186,7 @@ impl EventCacheStore for MemoryStore {
let mut duplicated_events = Vec::new();
for (event, position) in inner.events.unordered_room_items(room_id) {
for (event, position) in inner.events.unordered_linked_chunk_items(linked_chunk_id) {
// If `events` is empty, we can short-circuit.
if events.is_empty() {
break;
@@ -212,8 +212,9 @@ impl EventCacheStore for MemoryStore {
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event = inner.events.items().find_map(|(event, this_room_id)| {
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
let event = inner.events.items().find_map(|(event, this_linked_chunk_id)| {
(room_id == this_linked_chunk_id.room_id() && event.event_id()? == event_id)
.then_some(event.clone())
});
Ok(event)
@@ -232,9 +233,9 @@ impl EventCacheStore for MemoryStore {
let related_events = inner
.events
.items()
.filter_map(|(event, this_room_id)| {
.filter_map(|(event, this_linked_chunk_id)| {
// Must be in the same room.
if room_id != this_room_id {
if room_id != this_linked_chunk_id.room_id() {
return None;
}
@@ -364,8 +365,8 @@ impl EventCacheStore for MemoryStore {
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl EventCacheStoreMedia for MemoryStore {
type Error = EventCacheStoreError;
@@ -126,8 +126,14 @@ impl Deref for EventCacheStoreLockGuard<'_> {
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
#[cfg(not(target_family = "wasm"))]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// An error happened in the underlying database backend.
#[error(transparent)]
#[cfg(target_family = "wasm")]
Backend(Box<dyn std::error::Error>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
#[error("The event cache store failed to be unlocked")]
@@ -169,12 +175,25 @@ impl EventCacheStoreError {
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
#[cfg(not(target_family = "wasm"))]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
#[cfg(target_family = "wasm")]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + 'static,
{
Self::Backend(Box::new(error))
}
}
/// An `EventCacheStore` specific result type.
@@ -185,8 +204,6 @@ pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
#[derive(Clone, Debug)]
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BackingStore for LockableEventCacheStore {
type LockError = EventCacheStoreError;
@@ -16,7 +16,9 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
linked_chunk::{
ChunkIdentifier, ChunkIdentifierGenerator, LinkedChunkId, Position, RawChunk, Update,
},
AsyncTraitDeps,
};
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
@@ -37,8 +39,8 @@ pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait EventCacheStore: AsyncTraitDeps {
/// The error type used by this event cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
@@ -56,7 +58,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// in-memory. This method aims at forwarding this update inside this store.
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error>;
@@ -64,7 +66,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
// Right now, this means removing all the linked chunk. If implementations
// override this behavior, they should *also* include this code.
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
self.handle_linked_chunk_updates(LinkedChunkId::Room(room_id), vec![Update::Clear]).await
}
/// Return all the raw components of a linked chunk, so the caller may
@@ -72,7 +74,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
#[doc(hidden)]
async fn load_all_chunks(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
/// Load the last chunk of the `LinkedChunk` holding all events of the room
@@ -81,7 +83,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// This is used to iteratively load events for the `EventCache`.
async fn load_last_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
/// Load the chunk before the chunk identified by `before_chunk_identifier`
@@ -91,7 +93,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// This is used to iteratively load events for the `EventCache`.
async fn load_previous_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
@@ -105,17 +107,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// ⚠ This is meant only for super specific use cases, where there shouldn't
/// be any live in-memory linked chunks. In general, prefer using
/// `EventCache::clear_all_rooms()` from the common SDK crate.
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error>;
/// Given a set of event IDs, return the duplicated events along with their
/// position if there are any.
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
/// Find an event by its ID.
/// Find an event by its ID in a room.
async fn find_event(
&self,
room_id: &RoomId,
@@ -124,6 +126,11 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// Find all the events that relate to a given event.
///
/// Note: it doesn't process relations recursively: for instance, if
/// requesting only thread events, it will NOT return the aggregated
/// events affecting the returned events. It is the responsibility of
/// the caller to do so, if needed.
///
/// An additional filter can be provided to only retrieve related events for
/// a certain relationship.
async fn find_event_relations(
@@ -277,8 +284,8 @@ impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
type Error = EventCacheStoreError;
@@ -293,44 +300,47 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
self.0.handle_linked_chunk_updates(linked_chunk_id, updates).await.map_err(Into::into)
}
async fn load_all_chunks(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
self.0.load_all_chunks(room_id).await.map_err(Into::into)
self.0.load_all_chunks(linked_chunk_id).await.map_err(Into::into)
}
async fn load_last_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
self.0.load_last_chunk(room_id).await.map_err(Into::into)
self.0.load_last_chunk(linked_chunk_id).await.map_err(Into::into)
}
async fn load_previous_chunk(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
self.0.load_previous_chunk(room_id, before_chunk_identifier).await.map_err(Into::into)
self.0
.load_previous_chunk(linked_chunk_id, before_chunk_identifier)
.await
.map_err(Into::into)
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
async fn clear_all_linked_chunks(&self) -> Result<(), Self::Error> {
self.0.clear_all_linked_chunks().await.map_err(Into::into)
}
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
linked_chunk_id: LinkedChunkId<'_>,
events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
self.0.filter_duplicated_events(room_id, events).await.map_err(Into::into)
self.0.filter_duplicated_events(linked_chunk_id, events).await.map_err(Into::into)
}
async fn find_event(
+4 -3
View File
@@ -629,7 +629,7 @@ mod tests {
latest_event: LatestEvent,
}
let event = TimelineEvent::new(
let event = TimelineEvent::from_plaintext(
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
);
@@ -654,8 +654,9 @@ mod tests {
"event_id": "$1"
}
}
}
},
},
"thread_summary": "None",
}
}
})
);
+6 -7
View File
@@ -15,7 +15,7 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
#![warn(missing_docs, missing_debug_implementations)]
pub use matrix_sdk_common::*;
@@ -34,10 +34,9 @@ pub mod latest_event;
pub mod media;
pub mod notification_settings;
mod response_processors;
mod rooms;
mod room;
pub mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
pub mod sliding_sync;
pub mod store;
@@ -55,10 +54,10 @@ pub use http;
#[cfg(feature = "e2e-encryption")]
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember,
RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter,
pub use room::{
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
+36 -54
View File
@@ -122,7 +122,6 @@ use std::{
num::NonZeroUsize,
};
use eyeball_im::Vector;
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use ruma::{
events::{
@@ -210,7 +209,7 @@ impl RoomReadReceipts {
let mut has_notify = false;
let mut has_mention = false;
let Some(actions) = event.push_actions.as_ref() else {
let Some(actions) = event.push_actions() else {
return;
};
@@ -268,19 +267,6 @@ impl RoomReadReceipts {
}
}
/// Provider for timeline events prior to the current sync.
pub trait PreviousEventsProvider: Send + Sync {
/// Returns the list of known timeline events, in sync order, for the given
/// room.
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
}
impl PreviousEventsProvider for () {
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
Vector::new()
}
}
/// Small helper to select the "best" receipt (that with the biggest sync
/// order).
struct ReceiptSelector {
@@ -294,10 +280,7 @@ struct ReceiptSelector {
}
impl ReceiptSelector {
fn new(
all_events: &Vector<TimelineEvent>,
latest_active_receipt_event: Option<&EventId>,
) -> Self {
fn new(all_events: &[TimelineEvent], latest_active_receipt_event: Option<&EventId>) -> Self {
let event_id_to_pos = Self::create_sync_index(all_events.iter());
let best_pos =
@@ -457,23 +440,22 @@ pub(crate) fn compute_unread_counts(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
previous_events: Vector<TimelineEvent>,
mut previous_events: Vec<TimelineEvent>,
new_events: &[TimelineEvent],
read_receipts: &mut RoomReadReceipts,
) {
debug!(?read_receipts, "Starting.");
debug!(?read_receipts, "Starting");
let all_events = if events_intersects(previous_events.iter(), new_events) {
// The previous and new events sets can intersect, for instance if we restored
// previous events from the disk cache, or a timeline was limited. This
// means the old events will be cleared, because we don't reconcile
// timelines in sliding sync (yet). As a result, forget
// timelines in the event cache (yet). As a result, forget
// about the previous events.
Vector::from_iter(new_events.iter().cloned())
new_events.to_owned()
} else {
let mut all_events = previous_events;
all_events.extend(new_events.iter().cloned());
all_events
previous_events.extend(new_events.iter().cloned());
previous_events
};
let new_receipt = {
@@ -481,6 +463,7 @@ pub(crate) fn compute_unread_counts(
&all_events,
read_receipts.latest_active.as_ref().map(|receipt| &*receipt.event_id),
);
selector.try_match_implicit(user_id, new_events);
selector.handle_pending_receipts(&mut read_receipts.pending);
if let Some(receipt_event) = receipt_event {
@@ -622,7 +605,6 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
mod tests {
use std::{num::NonZeroUsize, ops::Not as _};
use eyeball_im::Vector;
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::event_factory::EventFactory;
use ruma::{
@@ -729,7 +711,7 @@ mod tests {
.sender(user_id)
.event_id(event_id!("$ida"))
.into_event();
ev.push_actions = Some(push_actions);
ev.set_push_actions(push_actions);
ev
}
@@ -915,7 +897,7 @@ mod tests {
let room_id = room_id!("!room:example.org");
let receipt_event_id = event_id!("$1");
let mut previous_events = Vector::new();
let mut previous_events = Vec::new();
let f = EventFactory::new();
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
@@ -940,8 +922,8 @@ mod tests {
assert_eq!(read_receipts.num_unread, 1);
// Receive the same receipt event, with a new sync event.
previous_events.push_back(ev1);
previous_events.push_back(ev2);
previous_events.push(ev1);
previous_events.push(ev2);
let new_event =
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
@@ -958,7 +940,7 @@ mod tests {
assert_eq!(read_receipts.num_unread, 2);
}
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
fn make_test_events(user_id: &UserId) -> Vec<TimelineEvent> {
let f = EventFactory::new().sender(user_id);
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
@@ -976,7 +958,7 @@ mod tests {
let room_id = room_id!("!room:example.org");
let all_events = make_test_events(user_id!("@bob:example.org"));
let head_events: Vector<_> = all_events.iter().take(2).cloned().collect();
let head_events: Vec<_> = all_events.iter().take(2).cloned().collect();
let tail_events: Vec<_> = all_events.iter().skip(2).cloned().collect();
// Given a receipt event marking events 1-3 as read using a combination of
@@ -1165,7 +1147,7 @@ mod tests {
{
// No initial active receipt, so the first receipt we get *will* win.
let mut selector = ReceiptSelector::new(&vec![].into(), None);
let mut selector = ReceiptSelector::new(&[], None);
selector.try_select_later(event_id!("$1"), 0);
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
@@ -1203,11 +1185,11 @@ mod tests {
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
let events = &[ev1, ev2][..];
{
// No pending receipt => no better receipt.
let mut selector = ReceiptSelector::new(&events, None);
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
selector.handle_pending_receipts(&mut pending);
@@ -1221,7 +1203,7 @@ mod tests {
{
// No pending receipt, and there was an active last receipt => no better
// receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
selector.handle_pending_receipts(&mut pending);
@@ -1239,11 +1221,11 @@ mod tests {
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
let events = &[ev1, ev2][..];
{
// A pending receipt for an event that is still missing => no better receipt.
let mut selector = ReceiptSelector::new(&events, None);
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$3"));
@@ -1257,7 +1239,7 @@ mod tests {
{
// Ditto but there was an active receipt => no better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$3"));
@@ -1276,11 +1258,11 @@ mod tests {
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
let events = &[ev1, ev2][..];
{
// A pending receipt for an event that is present => better receipt.
let mut selector = ReceiptSelector::new(&events, None);
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$2"));
@@ -1296,7 +1278,7 @@ mod tests {
{
// Mixed found and not found receipt => better receipt.
let mut selector = ReceiptSelector::new(&events, None);
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$1"));
@@ -1318,12 +1300,12 @@ mod tests {
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
let events = &[ev1, ev2][..];
{
// Same, and there was an initial receipt that was less good than the one we
// selected => better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$2"));
@@ -1339,7 +1321,7 @@ mod tests {
{
// Same, but the previous receipt was better => no better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
let mut selector = ReceiptSelector::new(events, Some(event_id!("$2")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$1"));
@@ -1484,26 +1466,26 @@ mod tests {
// When the selector sees only other users' events,
let mut selector = ReceiptSelector::new(&events, None);
// And I search for my implicit read receipt,
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
selector.try_match_implicit(&myself, &events);
// Then I don't find any.
let best_receipt = selector.select();
assert!(best_receipt.is_none());
// Now, if there are events I've written too...
let f = EventFactory::new();
events.push_back(
events.push(
f.text_msg("A mulatto, an albino")
.sender(&myself)
.event_id(event_id!("$6"))
.into_event(),
);
events.push_back(
events.push(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
let mut selector = ReceiptSelector::new(&events, None);
// And I search for my implicit read receipt,
selector.try_match_implicit(&myself, &events.iter().cloned().collect::<Vec<_>>());
selector.try_match_implicit(&myself, &events);
// Then my last sent event counts as a read receipt.
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$6"));
@@ -1520,7 +1502,7 @@ mod tests {
// One by me,
let f = EventFactory::new();
events.push_back(
events.push(
f.text_msg("A mulatto, an albino")
.sender(user_id)
.event_id(event_id!("$6"))
@@ -1528,10 +1510,10 @@ mod tests {
);
// And others by Bob,
events.push_back(
events.push(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
events.push_back(
events.push(
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
);
@@ -1551,7 +1533,7 @@ mod tests {
user_id,
room_id,
Some(&receipt_event),
Vector::new(),
Vec::new(),
&events,
&mut read_receipts,
);
@@ -20,7 +20,10 @@ use ruma::{
use tracing::{instrument, warn};
use super::super::{Context, RoomInfoNotableUpdates};
use crate::{store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons, StateChanges};
use crate::{
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
StateChanges,
};
#[instrument(skip_all, fields(?room_id))]
pub async fn for_room(
@@ -49,6 +52,7 @@ pub async fn for_room(
on_unread_marker(
room_id,
&event.content,
AccountDataSource::Stable,
room_info,
&mut context.room_info_notable_updates,
);
@@ -64,6 +68,7 @@ pub async fn for_room(
on_unread_marker(
room_id,
&event.content.0,
AccountDataSource::Unstable,
room_info,
&mut context.room_info_notable_updates,
);
@@ -127,9 +132,17 @@ fn on_room_info<F>(
fn on_unread_marker(
room_id: &RoomId,
content: &MarkedUnreadEventContent,
source: AccountDataSource,
room_info: &mut RoomInfo,
room_info_notable_updates: &mut RoomInfoNotableUpdates,
) {
if room_info.base_info.is_marked_unread_source == AccountDataSource::Stable
&& source != AccountDataSource::Stable
{
// Ignore the unstable source if a stable source was used previously.
return;
}
if room_info.base_info.is_marked_unread != content.unread {
// Notify the room list about a manual read marker change if the
// value's changed.
@@ -140,4 +153,5 @@ fn on_unread_marker(
}
room_info.base_info.is_marked_unread = content.unread;
room_info.base_info.is_marked_unread_source = source;
}
@@ -25,6 +25,16 @@ use crate::{
Result,
};
/// 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<()> {
save_changes(&context, state_store, None).await?;
broadcast_room_info_notable_updates(&context, state_store);
Ok(())
}
/// Save the [`StateChanges`] from the [`Context`] inside the
/// [`BaseStateStore`], and apply them on the in-memory rooms.
#[instrument(skip_all)]
@@ -39,28 +49,36 @@ pub async fn save_and_apply(
let previous_ignored_user_list =
state_store.get_account_data_event_static().await.ok().flatten();
state_store.save_changes(&context.state_changes).await?;
if let Some(sync_token) = sync_token {
*state_store.sync_token.write().await = Some(sync_token);
}
apply_changes(context, state_store, ignore_user_list_changes, previous_ignored_user_list);
save_changes(&context, state_store, sync_token).await?;
apply_changes(&context, ignore_user_list_changes, previous_ignored_user_list);
broadcast_room_info_notable_updates(&context, state_store);
trace!("applied changes");
Ok(())
}
fn apply_changes(
context: Context,
async fn save_changes(
context: &Context,
state_store: &BaseStateStore,
sync_token: Option<String>,
) -> Result<()> {
state_store.save_changes(&context.state_changes).await?;
if let Some(sync_token) = sync_token {
*state_store.sync_token.write().await = Some(sync_token);
}
Ok(())
}
fn apply_changes(
context: &Context,
ignore_user_list_changes: &SharedObservable<Vec<String>>,
previous_ignored_user_list: Option<Raw<IgnoredUserListEvent>>,
) {
let (state_changes, room_info_notable_updates) = context.into_parts();
if let Some(event) =
state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
{
match event.deserialize_as::<IgnoredUserListEvent>() {
Ok(event) => {
@@ -93,11 +111,13 @@ fn apply_changes(
}
}
}
}
for (room_id, room_info) in &state_changes.room_infos {
fn broadcast_room_info_notable_updates(context: &Context, state_store: &BaseStateStore) {
for (room_id, room_info) in &context.state_changes.room_infos {
if let Some(room) = state_store.room(room_id) {
let room_info_notable_update_reasons =
room_info_notable_updates.get(room_id).copied().unwrap_or_default();
context.room_info_notable_updates.get(room_id).copied().unwrap_or_default();
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
}
@@ -13,12 +13,10 @@
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
};
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::super::{verification, Context};
use super::{super::verification, E2EE};
use crate::Result;
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
@@ -29,38 +27,29 @@ use crate::Result;
///
/// Returns `Ok(None)` if encryption is not configured.
pub async fn sync_timeline_event(
context: &mut Context,
olm_machine: Option<&OlmMachine>,
e2ee: E2EE<'_>,
event: &Raw<AnySyncTimelineEvent>,
room_id: &RoomId,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Result<Option<TimelineEvent>> {
let Some(olm) = olm_machine else { return Ok(None) };
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: decryption_trust_requirement };
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
Ok(Some(
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
RoomEventDecryptionResult::Decrypted(decrypted) => {
let timeline_event = TimelineEvent::from(decrypted);
// Note: the push actions are set by the caller.
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
verification::process_if_relevant(
context,
&sync_timeline_event,
verification_is_allowed,
olm_machine,
room_id,
)
.await?;
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
}
timeline_event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
TimelineEvent::new_utd_event(event.clone(), utd_info)
TimelineEvent::from_utd(event.clone(), utd_info)
}
},
))
@@ -12,6 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
pub mod decrypt;
pub mod to_device;
pub mod tracked_users;
/// A classical set of data used by some processors in this module.
#[derive(Clone)]
pub struct E2EE<'a> {
pub olm_machine: Option<&'a OlmMachine>,
pub decryption_trust_requirement: TrustRequirement,
pub verification_is_allowed: bool,
}
impl<'a> E2EE<'a> {
pub fn new(
olm_machine: Option<&'a OlmMachine>,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Self {
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
}
}
@@ -22,7 +22,6 @@ use ruma::{
OneTimeKeyAlgorithm, UInt,
};
use super::super::Context;
use crate::Result;
/// Process the to-device events and other related e2ee data based on a response
@@ -31,13 +30,11 @@ use crate::Result;
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
pub async fn from_msc4186(
context: &mut Context,
to_device: Option<&v5::response::ToDevice>,
e2ee: &v5::response::E2EE,
olm_machine: Option<&OlmMachine>,
) -> Result<Output> {
process(
context,
olm_machine,
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
&e2ee.device_lists,
@@ -54,12 +51,10 @@ pub async fn from_msc4186(
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
pub async fn from_sync_v2(
context: &mut Context,
response: &v3::Response,
olm_machine: Option<&OlmMachine>,
) -> Result<Output> {
process(
context,
olm_machine,
response.to_device.events.clone(),
&response.device_lists,
@@ -75,7 +70,6 @@ pub async fn from_sync_v2(
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
async fn process(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
device_lists: &DeviceLists,
@@ -99,6 +93,15 @@ async fn process(
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
let events = events
.iter()
// TODO: There is loss of information here, after calling `to_raw` it is not
// possible to make the difference between a successfully decrypted event and a plain
// text event. This information needs to be propagated to top layer at some point if
// clients relies on custom encrypted to device events.
.map(|p| p.to_raw())
.collect();
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
// If we have no `OlmMachine`, just return the events that were passed in.
@@ -17,12 +17,10 @@ use std::collections::BTreeSet;
use matrix_sdk_crypto::OlmMachine;
use ruma::{OwnedUserId, RoomId};
use super::super::Context;
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
/// Update tracked users, if the room is encrypted.
pub async fn update(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
room_encryption_state: EncryptionState,
user_ids_to_track: &BTreeSet<OwnedUserId>,
@@ -41,7 +39,6 @@ pub async fn update(
/// Update tracked users, if the room is encrypted, or if the room has become
/// encrypted.
pub async fn update_or_set_if_room_is_newly_encrypted(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
user_ids_to_track: &BTreeSet<OwnedUserId>,
new_room_encryption_state: EncryptionState,
@@ -0,0 +1,50 @@
// 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 ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
use tracing::info;
use super::Context;
/// Dispatch [`AnySyncEphemeralRoomEvent`]s on the [`Context`].
pub fn dispatch(
context: &mut Context,
raw_events: &[Raw<AnySyncEphemeralRoomEvent>],
room_id: &RoomId,
) {
for raw_event in raw_events {
dispatch_receipt(context, raw_event, room_id);
}
}
/// Dispatch the [`AnySyncEphemeralRoomEvent::Receipt`] on the [`Context`].
pub(super) fn dispatch_receipt(
context: &mut Context,
raw_event: &Raw<AnySyncEphemeralRoomEvent>,
room_id: &RoomId,
) {
match raw_event.deserialize() {
Ok(AnySyncEphemeralRoomEvent::Receipt(event)) => {
context.state_changes.add_receipts(room_id, event.content);
}
Ok(_) => {}
Err(e) => {
let event_id = raw_event.get_field::<String>("event_id").ok().flatten();
info!(?room_id, event_id, "Failed to deserialize ephemeral room event: {e}");
}
}
}
@@ -13,12 +13,10 @@
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
};
use matrix_sdk_crypto::{DecryptionSettings, RoomEventDecryptionResult};
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::{verification, Context};
use super::{e2ee::E2EE, verification, Context};
use crate::{
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
Result, Room,
@@ -33,27 +31,19 @@ use crate::{
pub async fn decrypt_from_rooms(
context: &mut Context,
rooms: Vec<Room>,
olm_machine: Option<&OlmMachine>,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
e2ee: E2EE<'_>,
) -> Result<()> {
let Some(olm_machine) = olm_machine else {
// All functions used by this one expect an `OlmMachine`. Return if there is
// none.
if e2ee.olm_machine.is_none() {
return Ok(());
};
}
for room in rooms {
// Try to find a message we can decrypt and is suitable for using as the latest
// event. If we found one, set it as the latest and delete any older
// encrypted events
if let Some((found, found_index)) = find_suitable_and_decrypt(
context,
olm_machine,
&room,
&decryption_trust_requirement,
verification_is_allowed,
)
.await
{
if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
room.on_latest_event_decrypted(
found,
found_index,
@@ -67,11 +57,8 @@ pub async fn decrypt_from_rooms(
}
async fn find_suitable_and_decrypt(
context: &mut Context,
olm_machine: &OlmMachine,
room: &Room,
decryption_trust_requirement: &TrustRequirement,
verification_is_allowed: bool,
e2ee: &E2EE<'_>,
) -> Option<(Box<LatestEvent>, usize)> {
let enc_events = room.latest_encrypted_events();
let power_levels = room.power_levels().await.ok();
@@ -82,14 +69,8 @@ async fn find_suitable_and_decrypt(
// Size of the `decrypt_sync_room_event` future should not impact this
// async fn since it is likely that there aren't even any encrypted
// events when calling it.
let decrypt_sync_room_event = Box::pin(decrypt_sync_room_event(
context,
olm_machine,
event,
room.room_id(),
decryption_trust_requirement,
verification_is_allowed,
));
let decrypt_sync_room_event =
Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
if let Ok(decrypted) = decrypt_sync_room_event.await {
// We found an event we can decrypt
@@ -119,41 +100,37 @@ async fn find_suitable_and_decrypt(
/// representing the decryption error; in the case of problems with our
/// application, returns `Err`.
///
/// Returns `Ok(None)` if encryption is not configured.
/// # Panics
///
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
async fn decrypt_sync_room_event(
context: &mut Context,
olm_machine: &OlmMachine,
event: &Raw<AnySyncTimelineEvent>,
e2ee: &E2EE<'_>,
room_id: &RoomId,
decryption_trust_requirement: &TrustRequirement,
verification_is_allowed: bool,
) -> Result<TimelineEvent> {
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: *decryption_trust_requirement };
DecryptionSettings { sender_device_trust_requirement: e2ee.decryption_trust_requirement };
let event = match olm_machine
let event = match e2ee
.olm_machine
.expect("An `OlmMachine` is expected")
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
let event: TimelineEvent = decrypted.into();
// We're fine not setting the push actions for the latest event.
let event = TimelineEvent::from_decrypted(decrypted, None);
if let Ok(sync_timeline_event) = event.raw().deserialize() {
verification::process_if_relevant(
context,
&sync_timeline_event,
verification_is_allowed,
Some(olm_machine),
room_id,
)
.await?;
verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
.await?;
}
event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
TimelineEvent::new_utd_event(event.clone(), utd_info)
TimelineEvent::from_utd(event.clone(), utd_info)
}
};
@@ -167,11 +144,8 @@ mod tests {
};
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
use super::{decrypt_from_rooms, Context};
use crate::{
rooms::normal::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client,
StateChanges,
};
use super::{decrypt_from_rooms, Context, E2EE};
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
#[async_test]
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
@@ -203,14 +177,16 @@ mod tests {
assert!(room.latest_event().is_none());
// When I tell it to do some decryption
let mut context = Context::new(StateChanges::default(), Default::default());
let mut context = Context::default();
decrypt_from_rooms(
&mut context,
vec![room.clone()],
client.olm_machine().await.as_ref(),
client.decryption_trust_requirement,
client.handle_verification_events,
E2EE::new(
client.olm_machine().await.as_ref(),
client.decryption_trust_requirement,
client.handle_verification_events,
),
)
.await
.unwrap();
@@ -16,9 +16,12 @@ pub mod account_data;
pub mod changes;
#[cfg(feature = "e2e-encryption")]
pub mod e2ee;
pub mod ephemeral_events;
#[cfg(feature = "e2e-encryption")]
pub mod latest_event;
pub mod notification;
pub mod profiles;
pub mod room;
pub mod state_events;
pub mod timeline;
#[cfg(feature = "e2e-encryption")]
@@ -33,22 +36,14 @@ use crate::{RoomInfoNotableUpdateReasons, StateChanges};
type RoomInfoNotableUpdates = BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>;
#[cfg_attr(test, derive(Clone))]
#[derive(Default)]
pub(crate) struct Context {
pub(super) state_changes: StateChanges,
pub(super) room_info_notable_updates: RoomInfoNotableUpdates,
}
impl Context {
pub fn new(
state_changes: StateChanges,
room_info_notable_updates: RoomInfoNotableUpdates,
) -> Self {
Self { state_changes, room_info_notable_updates }
}
pub fn into_parts(self) -> (StateChanges, RoomInfoNotableUpdates) {
let Self { state_changes, room_info_notable_updates } = self;
(state_changes, room_info_notable_updates)
pub fn new(state_changes: StateChanges) -> Self {
Self { state_changes, room_info_notable_updates: Default::default() }
}
}
@@ -0,0 +1,81 @@
// 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::collections::BTreeMap;
use ruma::{
push::{Action, PushConditionRoomCtx, Ruleset},
serde::Raw,
OwnedRoomId, RoomId,
};
use crate::{
deserialized_responses::RawAnySyncOrStrippedTimelineEvent, store::BaseStateStore, sync,
};
/// A classical set of data used by some processors dealing with notifications
/// and push rules.
pub struct Notification<'a> {
pub push_rules: &'a Ruleset,
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
pub state_store: &'a BaseStateStore,
}
impl<'a> Notification<'a> {
pub fn new(
push_rules: &'a Ruleset,
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
state_store: &'a BaseStateStore,
) -> Self {
Self { push_rules, notifications, state_store }
}
fn push_notification(
&mut self,
room_id: &RoomId,
actions: Vec<Action>,
event: RawAnySyncOrStrippedTimelineEvent,
) {
self.notifications
.entry(room_id.to_owned())
.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`).
///
/// This method returns the fetched [`Action`]s.
pub fn push_notification_from_event_if<E, P>(
&mut self,
room_id: &RoomId,
push_condition_room_ctx: &PushConditionRoomCtx,
event: &Raw<E>,
predicate: P,
) -> &[Action]
where
Raw<E>: Into<RawAnySyncOrStrippedTimelineEvent>,
P: Fn(&Action) -> bool,
{
let actions = self.push_rules.get_actions(event, push_condition_room_ctx);
if actions.iter().any(predicate) {
self.push_notification(room_id, actions.to_owned(), event.clone().into());
}
actions
}
}
@@ -0,0 +1,40 @@
// 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 super::super::Context;
use crate::{
room::UpdatedRoomDisplayName, store::BaseStateStore, sync::RoomUpdates,
RoomInfoNotableUpdateReasons,
};
pub async fn update_for_rooms(
context: &mut Context,
room_updates: &RoomUpdates,
state_store: &BaseStateStore,
) {
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`.
if let Ok(UpdatedRoomDisplayName::New(_)) = room.compute_display_name().await {
let room_id = room.room_id().to_owned();
context.state_changes.room_infos.insert(room_id.clone(), room.clone_info());
context
.room_info_notable_updates
.entry(room_id)
.or_default()
.insert(RoomInfoNotableUpdateReasons::DISPLAY_NAME);
}
}
}
@@ -0,0 +1,46 @@
// 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 ruma::RoomId;
use tokio::sync::broadcast::Sender;
use crate::{store::ambiguity_map::AmbiguityCache, RequestedRequiredStates, RoomInfoNotableUpdate};
pub mod display_name;
pub mod msc4186;
pub mod sync_v2;
/// A classical set of data used by some processors in this module.
pub struct RoomCreationData<'a> {
room_id: &'a RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
requested_required_states: &'a RequestedRequiredStates,
ambiguity_cache: &'a mut AmbiguityCache,
}
impl<'a> RoomCreationData<'a> {
pub fn new(
room_id: &'a RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
requested_required_states: &'a RequestedRequiredStates,
ambiguity_cache: &'a mut AmbiguityCache,
) -> Self {
Self {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
}
}
}
@@ -0,0 +1,89 @@
// 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::collections::BTreeMap;
use ruma::{
api::client::sync::sync_events::v5 as http,
events::{receipt::ReceiptEventContent, AnySyncEphemeralRoomEvent, SyncEphemeralRoomEvent},
serde::Raw,
OwnedRoomId, RoomId,
};
use super::super::super::{
account_data::for_room as account_data_for_room, ephemeral_events::dispatch_receipt, Context,
};
use crate::{
store::BaseStateStore,
sync::{JoinedRoomUpdate, RoomUpdates},
RoomState,
};
/// Dispatch the ephemeral events in the `extensions.typing` part of the
/// response.
pub fn dispatch_typing_ephemeral_events(
typing: &http::response::Typing,
joined_room_updates: &mut BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
) {
for (room_id, raw) in &typing.rooms {
joined_room_updates
.entry(room_id.to_owned())
.or_default()
.ephemeral
.push(raw.clone().cast());
}
}
/// Dispatch the ephemeral event in the `extensions.receipts` part of the
/// response for a particular room.
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(
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;
if let Some(room) = state_store.room(room_id) {
match room.state() {
RoomState::Joined => room_updates
.joined
.entry(room_id.to_owned())
.or_default()
.account_data
.append(&mut raw.to_vec()),
RoomState::Left | RoomState::Banned => room_updates
.left
.entry(room_id.to_owned())
.or_default()
.account_data
.append(&mut raw.to_vec()),
RoomState::Invited | RoomState::Knocked => {}
}
}
}
}
@@ -0,0 +1,564 @@
// 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.
pub mod extensions;
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeSet;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::StateEventType;
use ruma::{
api::client::sync::sync_events::{
v3::{InviteState, InvitedRoom, KnockState, KnockedRoom},
v5 as http,
},
assign,
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
},
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},
RoomCreationData,
};
#[cfg(feature = "e2e-encryption")]
use crate::StateChanges;
use crate::{
store::BaseStateStore,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomState,
};
/// Represent any kind of room updates.
pub enum RoomUpdateKind {
Joined(JoinedRoomUpdate),
Left(LeftRoomUpdate),
Invited(InvitedRoomUpdate),
Knocked(KnockedRoomUpdate),
}
pub async fn update_any_room(
context: &mut Context,
user_id: &UserId,
room_creation_data: RoomCreationData<'_>,
room_response: &http::response::Room,
rooms_account_data: &BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
notification: notification::Notification<'_>,
) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
// Read state events from the `required_state` field.
//
// 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_store = notification.state_store;
// Find or create the room in the store
let is_new_room = !state_store.room_exists(room_id);
let invite_state_events =
room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
#[allow(unused_mut)] // Required for some feature flag combinations
let (mut room, mut room_info, maybe_room_update_kind) = membership(
context,
&state_events,
&invite_state_events,
state_store,
user_id,
room_id,
room_info_notable_update_sender,
);
room_info.mark_state_partially_synced();
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
#[cfg(feature = "e2e-encryption")]
let mut new_user_ids = BTreeSet::new();
#[cfg(not(feature = "e2e-encryption"))]
let mut new_user_ids = ();
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
)
.await?;
// This will be used for both invited and knocked rooms.
if let Some((raw_events, events)) = invite_state_events {
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
&room,
&mut room_info,
notification::Notification::new(
notification.push_rules,
notification.notifications,
notification.state_store,
),
)
.await?;
}
properties(context, room_id, room_response, &mut room_info, is_new_room);
let timeline = timeline::build(
context,
&room,
&mut room_info,
timeline::builder::Timeline::from(room_response),
notification,
#[cfg(feature = "e2e-encryption")]
e2ee.clone(),
)
.await?;
// Cache the latest decrypted event in room_info, and also keep any later
// encrypted events, so we can slot them in when we get the keys.
#[cfg(feature = "e2e-encryption")]
cache_latest_events(
&room,
&mut room_info,
&timeline.events,
Some(&context.state_changes),
Some(state_store),
)
.await;
#[cfg(feature = "e2e-encryption")]
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
e2ee.olm_machine,
&new_user_ids,
room_info.encryption_state(),
room.encryption_state(),
room_id,
state_store,
)
.await?;
let notification_count = room_response.unread_notifications.clone().into();
room_info.update_notification_count(notification_count);
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
let room_account_data = rooms_account_data.get(room_id);
match (room_info.state(), maybe_room_update_kind) {
(RoomState::Joined, None) => {
// Ephemeral events are added separately, because we might not
// have a room subsection in the response, yet we may have receipts for
// that room.
let ephemeral = Vec::new();
Ok(Some((
room_info,
RoomUpdateKind::Joined(JoinedRoomUpdate::new(
timeline,
raw_state_events,
room_account_data.cloned().unwrap_or_default(),
ephemeral,
notification_count,
ambiguity_changes,
)),
)))
}
(RoomState::Left, None) | (RoomState::Banned, None) => Ok(Some((
room_info,
RoomUpdateKind::Left(LeftRoomUpdate::new(
timeline,
raw_state_events,
room_account_data.cloned().unwrap_or_default(),
ambiguity_changes,
)),
))),
(RoomState::Invited, Some(update @ RoomUpdateKind::Invited(_)))
| (RoomState::Knocked, Some(update @ RoomUpdateKind::Knocked(_))) => {
Ok(Some((room_info, update)))
}
_ => Ok(None),
}
}
/// Look through the sliding sync data for this room, find/create it in the
/// store, and process any invite information.
///
/// If there is any invite state events, the room can be considered an invited
/// or knocked room, depending of the membership event (if any).
fn membership(
context: &mut Context,
state_events: &[AnySyncStateEvent],
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
store: &BaseStateStore,
user_id: &UserId,
room_id: &RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
// There are invite state events. It means the room can be:
//
// 1. either an invited room,
// 2. or a knocked room.
//
// Let's find out.
if let Some(state_events) = invite_state_events {
// 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());
}
}
None
});
match membership_event {
// There is a membership event indicating it's a knocked room.
Some(RoomMemberEventContent { membership: MembershipState::Knock, .. }) => {
let room = store.get_or_create_room(
room_id,
RoomState::Knocked,
room_info_notable_update_sender,
);
let mut room_info = room.clone_info();
// Override the room state if the room already exists.
room_info.mark_as_knocked();
let raw_events = state_events.0.clone();
let knock_state = assign!(KnockState::default(), { events: raw_events });
let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
(room, room_info, Some(RoomUpdateKind::Knocked(knocked_room)))
}
// Otherwise, assume it's an invited room because there are invite state events.
_ => {
let room = store.get_or_create_room(
room_id,
RoomState::Invited,
room_info_notable_update_sender,
);
let mut room_info = room.clone_info();
// Override the room state if the room already exists.
room_info.mark_as_invited();
let raw_events = state_events.0.clone();
let invited_room = InvitedRoom::from(InviteState::from(raw_events));
(room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
}
}
}
// No invite state events. We assume this is a joined room for the moment. See this block to
// learn more.
else {
let room =
store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
let mut room_info = room.clone_info();
// We default to considering this room joined if it's not an invite. If it's
// actually left (and we remembered to request membership events in
// our sync request), then we can find this out from the events in
// required_state by calling handle_own_room_membership.
room_info.mark_as_joined();
// We don't need to do this in a v2 sync, because the membership of a room can
// be figured out by whether the room is in the "join", "leave" etc.
// property. In sliding sync we only have invite_state,
// required_state and timeline, so we must process required_state and timeline
// looking for relevant membership events.
own_membership(context, user_id, state_events, &mut room_info);
(room, room_info, None)
}
}
/// Find any `m.room.member` events that refer to the current user, and update
/// the state in room_info to reflect the "membership" property.
fn own_membership(
context: &mut Context,
user_id: &UserId,
state_events: &[AnySyncStateEvent],
room_info: &mut RoomInfo,
) {
// Start from the last event; the first membership event we see in that order is
// the last in the regular order, so that's the only one we need to
// consider.
for event in state_events.iter().rev() {
if let AnySyncStateEvent::RoomMember(member) = &event {
// If this event updates the current user's membership, record that in the
// room_info.
if member.state_key() == user_id.as_str() {
let new_state: RoomState = member.membership().into();
if new_state != room_info.state() {
room_info.set_state(new_state);
// Update an existing notable update entry or create a new one
context
.room_info_notable_updates
.entry(room_info.room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
break;
}
}
}
}
fn properties(
context: &mut Context,
room_id: &RoomId,
room_response: &http::response::Room,
room_info: &mut RoomInfo,
is_new_room: bool,
) {
// Handle the room's avatar.
//
// It can be updated via the state events, or via the
// [`http::ResponseRoom::avatar`] field. This part of the code handles the
// latter case. The former case is handled by [`BaseClient::handle_state`].
match &room_response.avatar {
// A new avatar!
JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
// Avatar must be removed.
JsOption::Null => room_info.update_avatar(None),
// Nothing to do.
JsOption::Undefined => {}
}
// Sliding sync doesn't have a room summary, nevertheless it contains the joined
// and invited member counts, in addition to the heroes.
if let Some(count) = room_response.joined_count {
room_info.update_joined_member_count(count.into());
}
if let Some(count) = room_response.invited_count {
room_info.update_invited_member_count(count.into());
}
if let Some(heroes) = &room_response.heroes {
room_info.update_heroes(
heroes
.iter()
.map(|hero| RoomHero {
user_id: hero.user_id.clone(),
display_name: hero.name.clone(),
avatar_url: hero.avatar.clone(),
})
.collect(),
);
}
room_info.set_prev_batch(room_response.prev_batch.as_deref());
if room_response.limited {
room_info.mark_members_missing();
}
if let Some(recency_stamp) = &room_response.bump_stamp {
let recency_stamp: u64 = (*recency_stamp).into();
if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
room_info.update_recency_stamp(recency_stamp);
// If it's not a new room, let's emit a `RECENCY_STAMP` update.
// For a new room, the room will appear as new, so we don't care about this
// update.
if !is_new_room {
context
.room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::RECENCY_STAMP);
}
}
}
}
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
///
/// If any encrypted events are found after that one, store them in the RoomInfo
/// too so we can use them when we get the relevant keys.
///
/// It is the responsibility of the caller to update the `RoomInfo` instance
/// stored in the `Room`.
#[cfg(feature = "e2e-encryption")]
pub(crate) async fn cache_latest_events(
room: &Room,
room_info: &mut RoomInfo,
events: &[TimelineEvent],
changes: Option<&StateChanges>,
store: Option<&BaseStateStore>,
) {
use tracing::warn;
use crate::{
deserialized_responses::DisplayName,
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
store::ambiguity_map::is_display_name_ambiguous,
};
let mut encrypted_events =
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
// Try to get room power levels from the current changes
let power_levels_from_changes = || {
let state_changes = changes?.state.get(room_info.room_id())?;
let room_power_levels_state =
state_changes.get(&StateEventType::RoomPowerLevels)?.values().next()?;
match room_power_levels_state.deserialize().ok()? {
AnySyncStateEvent::RoomPowerLevels(ev) => Some(ev.power_levels()),
_ => None,
}
};
// If we didn't get any info, try getting it from local data
let power_levels = match power_levels_from_changes() {
Some(power_levels) => Some(power_levels),
None => room.power_levels().await.ok(),
};
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
for event in events.iter().rev() {
if let Ok(timeline_event) = event.raw().deserialize() {
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesCallNotify(_)
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
// We found a suitable latest event. Store it.
// In order to make the latest event fast to read, we want to keep the
// associated sender in cache. This is a best-effort to gather enough
// information for creating a user profile as fast as possible. If information
// are missing, let's go back on the “slow” path.
let mut sender_profile = None;
let mut sender_name_is_ambiguous = None;
// First off, look up the sender's profile from the `StateChanges`, they are
// likely to be the most recent information.
if let Some(changes) = changes {
sender_profile = changes
.profiles
.get(room.room_id())
.and_then(|profiles_by_user| {
profiles_by_user.get(timeline_event.sender())
})
.cloned();
if let Some(sender_profile) = sender_profile.as_ref() {
sender_name_is_ambiguous = sender_profile
.as_original()
.and_then(|profile| profile.content.displayname.as_ref())
.and_then(|display_name| {
let display_name = DisplayName::new(display_name);
changes.ambiguity_maps.get(room.room_id()).and_then(
|map_for_room| {
map_for_room.get(&display_name).map(|users| {
is_display_name_ambiguous(&display_name, users)
})
},
)
});
}
}
// 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();
// TODO: need to update `sender_name_is_ambiguous`,
// but how?
}
}
let latest_event = Box::new(LatestEvent::new_with_sender_details(
event.clone(),
sender_profile,
sender_name_is_ambiguous,
));
// Store it in the return RoomInfo (it will be saved for us in the room later).
room_info.latest_event = Some(latest_event);
// We don't need any of the older encrypted events because we have a new
// decrypted one.
room.latest_encrypted_events.write().unwrap().clear();
// We can stop looking through the timeline now because everything else is
// older.
break;
}
PossibleLatestEvent::NoEncrypted => {
// m.room.encrypted - this might be the latest event later - we can't tell until
// we are able to decrypt it, so store it for now
//
// Check how many encrypted events we have seen. Only store another if we
// haven't already stored the maximum number.
if encrypted_events.len() < encrypted_events.capacity() {
encrypted_events.push(event.raw().clone());
}
}
_ => {
// Ignore unsuitable events
}
}
} else {
warn!(
"Failed to deserialize event as AnySyncTimelineEvent. ID={}",
event.event_id().expect("Event has no ID!")
);
}
}
// Push the encrypted events we found into the Room, in reverse order, so
// the latest is last
room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
}
@@ -0,0 +1,299 @@
// 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::collections::{BTreeMap, BTreeSet};
use ruma::{
api::client::sync::sync_events::v3::{InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom},
OwnedRoomId, OwnedUserId, RoomId,
};
use tokio::sync::broadcast::Sender;
#[cfg(feature = "e2e-encryption")]
use super::super::e2ee;
use super::{
super::{account_data, ephemeral_events, notification, state_events, timeline, Context},
RoomCreationData,
};
use crate::{
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate},
Result, RoomInfoNotableUpdate, RoomState,
};
/// Process updates of a joined room.
#[allow(clippy::too_many_arguments)]
pub async fn update_joined_room(
context: &mut Context,
room_creation_data: RoomCreationData<'_>,
joined_room: JoinedRoom,
updated_members_in_room: &mut BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>>,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
) -> Result<JoinedRoomUpdate> {
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
let state_store = notification.state_store;
let room =
state_store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
let mut room_info = room.clone_info();
room_info.mark_as_joined();
room_info.update_from_ruma_summary(&joined_room.summary);
room_info.set_prev_batch(joined_room.timeline.prev_batch.as_deref());
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();
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
)
.await?;
ephemeral_events::dispatch(context, &joined_room.ephemeral.events, room_id);
if joined_room.timeline.limited {
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,
)
.await?;
#[cfg(feature = "e2e-encryption")]
let olm_machine = e2ee.olm_machine;
let timeline = timeline::build(
context,
&room,
&mut room_info,
timeline::builder::Timeline::from(joined_room.timeline),
notification,
#[cfg(feature = "e2e-encryption")]
e2ee,
)
.await?;
// 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;
// `processors::account_data::from_room` might have updated the `RoomInfo`.
// Let's fetch it again.
//
// SAFETY: `expect` is safe because the `RoomInfo` has been inserted 2 lines
// above.
let mut room_info = context
.state_changes
.room_infos
.get(room_id)
.expect("`RoomInfo` must exist in `StateChanges` at this point")
.clone();
#[cfg(feature = "e2e-encryption")]
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
olm_machine,
&new_user_ids,
room_info.encryption_state(),
room.encryption_state(),
room_id,
state_store,
)
.await?;
updated_members_in_room.insert(room_id.to_owned(), new_user_ids);
let notification_count = joined_room.unread_notifications.into();
room_info.update_notification_count(notification_count);
context.state_changes.add_room(room_info);
Ok(JoinedRoomUpdate::new(
timeline,
joined_room.state.events,
joined_room.account_data.events,
joined_room.ephemeral.events,
notification_count,
ambiguity_cache.changes.remove(room_id).unwrap_or_default(),
))
}
/// Process historical updates of a left room.
#[allow(clippy::too_many_arguments)]
pub async fn update_left_room(
context: &mut Context,
room_creation_data: RoomCreationData<'_>,
left_room: LeftRoom,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
) -> Result<LeftRoomUpdate> {
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
let state_store = notification.state_store;
let room =
state_store.get_or_create_room(room_id, RoomState::Left, room_info_notable_update_sender);
let mut room_info = room.clone_info();
room_info.mark_as_left();
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);
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
&mut room_info,
ambiguity_cache,
&mut (),
)
.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 (),
)
.await?;
let timeline = timeline::build(
context,
&room,
&mut room_info,
timeline::builder::Timeline::from(left_room.timeline),
notification,
#[cfg(feature = "e2e-encryption")]
e2ee,
)
.await?;
// 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;
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,
))
}
/// Process updates of an invited room.
pub async fn update_invited_room(
context: &mut Context,
room_id: &RoomId,
invited_room: InvitedRoom,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
notification: notification::Notification<'_>,
) -> Result<InvitedRoomUpdate> {
let state_store = notification.state_store;
let room = state_store.get_or_create_room(
room_id,
RoomState::Invited,
room_info_notable_update_sender,
);
let (raw_events, events) = state_events::stripped::collect(&invited_room.invite_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_invited();
room_info.mark_state_fully_synced();
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
&room,
&mut room_info,
notification,
)
.await?;
context.state_changes.add_room(room_info);
Ok(invited_room)
}
/// Process updates of a knocked room.
pub async fn update_knocked_room(
context: &mut Context,
room_id: &RoomId,
knocked_room: KnockedRoom,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
notification: notification::Notification<'_>,
) -> Result<KnockedRoomUpdate> {
let state_store = notification.state_store;
let room = state_store.get_or_create_room(
room_id,
RoomState::Knocked,
room_info_notable_update_sender,
);
let (raw_events, events) = state_events::stripped::collect(&knocked_room.knock_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
room_info.mark_state_fully_synced();
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
&room,
&mut room_info,
notification,
)
.await?;
context.state_changes.add_room(room_info);
Ok(knocked_room)
}
@@ -12,34 +12,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
iter,
};
use ruma::{
events::{room::member::MembershipState, AnySyncStateEvent},
serde::Raw,
OwnedUserId,
};
use ruma::{events::AnySyncStateEvent, serde::Raw};
use serde::Deserialize;
use tracing::{instrument, warn};
use tracing::warn;
use super::{profiles, Context};
use crate::{
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
RoomInfo,
};
use super::Context;
/// Collect [`AnySyncStateEvent`].
pub mod sync {
use ruma::events::AnySyncTimelineEvent;
use std::{collections::BTreeSet, iter};
use super::{AnySyncStateEvent, Context, Raw};
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnySyncTimelineEvent, SyncStateEvent,
},
OwnedUserId, RoomId, UserId,
};
use tracing::instrument;
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
use crate::{
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
RoomInfo,
};
/// Collect [`AnySyncStateEvent`] to [`AnySyncStateEvent`].
pub fn collect(
_context: &mut Context,
raw_events: &[Raw<AnySyncStateEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events)
@@ -50,7 +49,6 @@ pub mod sync {
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
/// state events. The message-like events are filtered out.
pub fn collect_from_timeline(
_context: &mut Context,
raw_events: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events.iter().filter_map(|raw_event| {
@@ -61,21 +59,176 @@ pub mod sync {
}
}))
}
/// Dispatch the sync state events.
///
/// `raw_events` and `events` must be generated from [`collect`].
/// Events must be exactly the same list of events that are in
/// `raw_events`, but deserialised. We demand them here to avoid
/// deserialising multiple times.
///
/// The `new_users` mutable reference allows to collect the new users for
/// this room.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn dispatch<U>(
context: &mut Context,
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
) -> StoreResult<()>
where
U: NewUsers,
{
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_state_event(event);
if let AnySyncStateEvent::RoomMember(member) = event {
dispatch_room_member(
context,
&room_info.room_id,
member,
ambiguity_cache,
new_users,
)
.await?;
}
context
.state_changes
.state
.entry(room_info.room_id.to_owned())
.or_default()
.entry(event.event_type())
.or_default()
.insert(event.state_key().to_owned(), raw_event.clone());
}
Ok(())
}
/// Dispatch a [`RoomMemberEventContent>`] state event.
async fn dispatch_room_member<U>(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
) -> StoreResult<()>
where
U: NewUsers,
{
ambiguity_cache.handle_event(&context.state_changes, room_id, event).await?;
match event.membership() {
MembershipState::Join | MembershipState::Invite => {
new_users.insert(event.state_key());
}
_ => (),
}
profiles::upsert_or_delete(context, room_id, event);
Ok(())
}
/// A trait to collect new users in [`dispatch`].
trait NewUsers {
/// Insert a new user in the collection of new users.
fn insert(&mut self, user_id: &UserId);
}
impl NewUsers for BTreeSet<OwnedUserId> {
fn insert(&mut self, user_id: &UserId) {
self.insert(user_id.to_owned());
}
}
impl NewUsers for () {
fn insert(&mut self, _user_id: &UserId) {}
}
}
/// Collect [`AnyStrippedStateEvent`].
pub mod stripped {
use ruma::events::AnyStrippedStateEvent;
use std::{collections::BTreeMap, iter};
use super::{Context, Raw};
use ruma::{events::AnyStrippedStateEvent, push::Action};
use tracing::instrument;
use super::{
super::{notification, timeline},
Context, Raw,
};
use crate::{Result, Room, RoomInfo};
/// Collect [`AnyStrippedStateEvent`] to [`AnyStrippedStateEvent`].
pub fn collect(
_context: &mut Context,
raw_events: &[Raw<AnyStrippedStateEvent>],
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
super::collect(raw_events)
}
/// Dispatch the stripped state events.
///
/// `raw_events` and `events` must be generated from [`collect`].
/// Events must be exactly the same list of events that are in
/// `raw_events`, but deserialised. We demand them here to avoid
/// deserialising multiple times.
///
/// Dispatch the stripped state events in `invite_state` or `knock_state`,
/// modifying the room's info and posting notifications as needed.
///
/// * `raw_events` and `events` - The contents of `invite_state` in the form
/// of list of pairs of raw stripped state events with their deserialized
/// counterpart.
/// * `room` - The [`Room`] to modify.
/// * `room_info` - The current room's info.
/// * `notifications` - Notifications to post for the current room.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub(crate) async fn dispatch_invite_or_knock(
context: &mut Context,
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[AnyStrippedStateEvent]),
room: &Room,
room_info: &mut RoomInfo,
mut notification: notification::Notification<'_>,
) -> Result<()> {
let mut state_events = BTreeMap::new();
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_stripped_state_event(event);
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
}
context
.state_changes
.stripped_state
.insert(room_info.room_id().to_owned(), state_events.clone());
// 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?
{
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,
);
}
}
Ok(())
}
}
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
@@ -95,51 +248,45 @@ where
.unzip()
}
/// Dispatch the state events and return the new users for this room.
///
/// `raw_events` and `events` must be generated from [`collect_sync`]. Events
/// must be exactly the same list of events that are in raw_events, but
/// deserialised. We demand them here to avoid deserialising multiple times.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn dispatch_and_get_new_users(
context: &mut Context,
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
) -> StoreResult<BTreeSet<OwnedUserId>> {
let mut user_ids = BTreeSet::new();
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
};
use ruma::{event_id, user_id};
if raw_events.is_empty() {
return Ok(user_ids);
use crate::test_utils::logged_in_base_client;
#[async_test]
async fn test_state_events_after_sync() {
// Given a room
let user_id = user_id!("@u:u.to");
let client = logged_in_base_client(Some(user_id)).await;
let mut sync_builder = SyncResponseBuilder::new();
let room_name = EventFactory::new()
.sender(user_id)
.room_topic("this is the test topic in the timeline")
.event_id(event_id!("$2"))
.into_raw_sync();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
.add_timeline_event(room_name)
.add_state_event(StateTestEvent::PowerLevels),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).expect("Just-created room not found!");
// ensure that we have the power levels
assert!(room.power_levels().await.is_ok());
// ensure that we have the topic
assert_eq!(room.topic().unwrap(), "this is the test topic in the timeline");
}
let mut state_events = BTreeMap::new();
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_state_event(event);
if let AnySyncStateEvent::RoomMember(member) = event {
ambiguity_cache
.handle_event(&context.state_changes, &room_info.room_id, member)
.await?;
match member.membership() {
MembershipState::Join | MembershipState::Invite => {
user_ids.insert(member.state_key().to_owned());
}
_ => (),
}
profiles::upsert_or_delete(context, &room_info.room_id, member);
}
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
}
context.state_changes.state.insert(room_info.room_id.clone(), state_events);
Ok(user_ids)
}
@@ -28,13 +28,12 @@ use ruma::{
};
use tracing::{instrument, trace, warn};
use super::Context;
#[cfg(feature = "e2e-encryption")]
use super::{e2ee, verification};
use super::{notification, Context};
use crate::{
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
store::{BaseStateStore, StateStoreExt as _},
sync::{Notification, Timeline},
sync::Timeline,
Result, Room, RoomInfo,
};
@@ -51,18 +50,18 @@ pub async fn build<'notification, 'e2ee>(
room: &Room,
room_info: &mut RoomInfo,
timeline_inputs: builder::Timeline,
notification_inputs: builder::Notification<'notification>,
#[cfg(feature = "e2e-encryption")] e2ee: builder::E2EE<'e2ee>,
mut notification: notification::Notification<'notification>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
) -> Result<Timeline> {
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
let mut push_context =
get_push_room_context(context, room, room_info, notification_inputs.state_store).await?;
let mut push_condition_room_ctx =
get_push_room_context(context, room, room_info, notification.state_store).await?;
let room_id = room.room_id();
for raw_event in timeline_inputs.raw_events {
// Start by assuming we have a plaintext event. We'll replace it with a
// decrypted or UTD event below if necessary.
let mut timeline_event = TimelineEvent::new(raw_event);
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
// Do some special stuff on the `timeline_event` before collecting it.
match timeline_event.raw().deserialize() {
@@ -100,12 +99,9 @@ pub async fn build<'notification, 'e2ee>(
) => {
if let Some(decrypted_timeline_event) =
Box::pin(e2ee::decrypt::sync_timeline_event(
context,
e2ee.olm_machine,
e2ee.clone(),
timeline_event.raw(),
room_id,
e2ee.decryption_trust_requirement,
e2ee.verification_is_allowed,
))
.await?
{
@@ -115,10 +111,8 @@ pub async fn build<'notification, 'e2ee>(
_ => {
Box::pin(verification::process_if_relevant(
context,
&sync_timeline_event,
e2ee.verification_is_allowed,
e2ee.olm_machine,
e2ee.clone(),
room_id,
))
.await?;
@@ -131,36 +125,28 @@ pub async fn build<'notification, 'e2ee>(
AnySyncTimelineEvent::MessageLike(_) => (),
}
if let Some(push_context) = &mut push_context {
update_push_room_context(context, push_context, room.own_user_id(), room_info)
} else {
push_context = get_push_room_context(
if let Some(push_condition_room_ctx) = &mut push_condition_room_ctx {
update_push_room_context(
context,
room,
push_condition_room_ctx,
room.own_user_id(),
room_info,
notification_inputs.state_store,
)
.await?;
} else {
push_condition_room_ctx =
get_push_room_context(context, room, room_info, notification.state_store)
.await?;
}
if let Some(context) = &push_context {
let actions =
notification_inputs.push_rules.get_actions(timeline_event.raw(), context);
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,
);
if actions.iter().any(Action::should_notify) {
notification_inputs
.notifications
.entry(room_id.to_owned())
.or_default()
.push(Notification {
actions: actions.to_owned(),
event: RawAnySyncOrStrippedTimelineEvent::Sync(
timeline_event.raw().clone(),
),
});
}
timeline_event.push_actions = Some(actions.to_owned());
timeline_event.set_push_actions(actions.to_owned());
}
}
Err(error) => {
@@ -178,20 +164,12 @@ pub async fn build<'notification, 'e2ee>(
/// Set of types used by [`build`] to reduce the number of arguments by grouping
/// them by thematics.
pub mod builder {
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
use ruma::{
api::client::sync::sync_events::{v3, v5},
events::AnySyncTimelineEvent,
push::Ruleset,
serde::Raw,
OwnedRoomId,
};
use crate::{store::BaseStateStore, sync};
pub struct Timeline {
pub limited: bool,
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
@@ -213,47 +191,13 @@ pub mod builder {
}
}
}
pub struct Notification<'a> {
pub push_rules: &'a Ruleset,
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
pub state_store: &'a BaseStateStore,
}
impl<'a> Notification<'a> {
pub fn new(
push_rules: &'a Ruleset,
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
state_store: &'a BaseStateStore,
) -> Self {
Self { push_rules, notifications, state_store }
}
}
#[cfg(feature = "e2e-encryption")]
pub struct E2EE<'a> {
pub olm_machine: Option<&'a OlmMachine>,
pub decryption_trust_requirement: TrustRequirement,
pub verification_is_allowed: bool,
}
#[cfg(feature = "e2e-encryption")]
impl<'a> E2EE<'a> {
pub fn new(
olm_machine: Option<&'a OlmMachine>,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Self {
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
}
}
}
/// Update the push context for the given room.
///
/// Updates the context data from `context.state_changes` or `room_info`.
fn update_push_room_context(
context: &mut Context,
context: &Context,
push_rules: &mut PushConditionRoomCtx,
user_id: &UserId,
room_info: &RoomInfo,
@@ -291,7 +235,7 @@ fn update_push_room_context(
/// Returns `None` if some data couldn't be found. This should only happen
/// in brand new rooms, while we process its state.
pub async fn get_push_room_context(
context: &mut Context,
context: &Context,
room: &Room,
room_info: &RoomInfo,
state_store: &BaseStateStore,
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_crypto::OlmMachine;
use ruma::{
events::{
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
@@ -21,64 +20,48 @@ use ruma::{
RoomId,
};
use super::Context;
use super::e2ee::E2EE;
use crate::Result;
/// Process the given event as a verification event if it is a candidate. The
/// event must be decrypted.
pub async fn process_if_relevant(
context: &mut Context,
event: &AnySyncTimelineEvent,
verification_is_allowed: bool,
olm_machine: Option<&OlmMachine>,
e2ee: E2EE<'_>,
room_id: &RoomId,
) -> Result<()> {
if let AnySyncTimelineEvent::MessageLike(event) = event {
// That's it, we are good, the event has been decrypted successfully.
// However, let's run an additional action. Check if this is a verification
// event (`m.key.verification.*`), and call `verification` accordingly.
if match &event {
// This is an original (i.e. non-redacted) `m.room.message` event and its
// content is a verification request…
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
original_event,
)) => {
matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_))
}
// … or this is verification request event
AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
_ => false,
} {
verification(context, verification_is_allowed, olm_machine, event, room_id).await?;
}
}
Ok(())
}
async fn verification(
_context: &mut Context,
verification_is_allowed: bool,
olm_machine: Option<&OlmMachine>,
event: &AnySyncMessageLikeEvent,
room_id: &RoomId,
) -> Result<()> {
if !verification_is_allowed {
if !e2ee.verification_is_allowed {
return Ok(());
}
if let Some(olm) = olm_machine {
olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?;
let Some(olm) = e2ee.olm_machine else {
return Ok(());
};
let AnySyncTimelineEvent::MessageLike(event) = event else {
return Ok(());
};
match event {
// This is an original (i.e. non-redacted) `m.room.message` event and its
// content is a verification request…
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(original_event))
if matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_)) => {}
// … or this is verification request event.
AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => {}
_ => {
// No need to handle those other event types.
return Ok(());
}
}
Ok(())
Ok(olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?)
}
+278
View File
@@ -0,0 +1,278 @@
// 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 ruma::OwnedUserId;
use super::Room;
impl Room {
/// Is there a non expired membership with application `m.call` and scope
/// `m.room` in this room.
pub fn has_active_room_call(&self) -> bool {
self.inner.read().has_active_room_call()
}
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
///
/// MatrixRTC memberships with application `m.call` and scope `m.room` are
/// considered. A user can occur twice if they join with two devices.
/// Convert to a set depending if the different users are required or the
/// amount of sessions.
///
/// The vector is ordered by oldest membership user to newest.
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.inner.read().active_room_call_participants()
}
}
#[cfg(test)]
mod tests {
use std::{ops::Sub, sync::Arc, time::Duration};
use assign::assign;
use matrix_sdk_test::{ALICE, BOB, CAROL};
use ruma::{
device_id, event_id,
events::{
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,
};
use similar_asserts::assert_eq;
use super::super::{Room, RoomState};
use crate::store::MemoryStore;
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
let user_id = user_id!("@me:example.org");
let room_id = room_id!("!test:localhost");
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
}
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch::from_system_time(
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
)
.expect("date out of range")
}
fn legacy_membership_for_my_call(
device_id: &DeviceId,
membership_id: &str,
minutes_ago: u32,
) -> LegacyMembershipData {
let (application, foci) = foci_and_application();
assign!(
LegacyMembershipData::from(LegacyMembershipDataInit {
application,
device_id: device_id.to_owned(),
expires: Duration::from_millis(3_600_000),
foci_active: foci,
membership_id: membership_id.to_owned(),
}),
{ created_ts: Some(timestamp(minutes_ago)) }
)
}
fn legacy_member_state_event(
memberships: Vec<LegacyMembershipData>,
ev_id: &EventId,
user_id: &UserId,
) -> AnySyncStateEvent {
let content = CallMemberEventContent::new_legacy(memberships);
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
content,
event_id: ev_id.to_owned(),
sender: user_id.to_owned(),
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
origin_server_ts: timestamp(0),
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
unsigned: StateUnsigned::new(),
}))
}
struct InitData<'a> {
device_id: &'a DeviceId,
minutes_ago: u32,
}
fn session_member_state_event(
ev_id: &EventId,
user_id: &UserId,
init_data: Option<InitData<'_>>,
) -> AnySyncStateEvent {
let application = Application::Call(CallApplicationContent::new(
"my_call_id_1".to_owned(),
ruma::events::call::member::CallScope::Room,
));
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
"my_call_foci_alias".to_owned(),
"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),
),
None => (
CallMemberEventContent::new_empty(None),
CallMemberStateKey::new(user_id.to_owned(), None, false),
),
};
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
content,
event_id: ev_id.to_owned(),
sender: user_id.to_owned(),
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
origin_server_ts: timestamp(0),
state_key,
unsigned: StateUnsigned::new(),
}))
}
fn foci_and_application() -> (Application, Vec<Focus>) {
(
Application::Call(CallApplicationContent::new(
"my_call_id_1".to_owned(),
ruma::events::call::member::CallScope::Room,
)),
vec![Focus::Livekit(LivekitFocus::new(
"my_call_foci_alias".to_owned(),
"https://lk.org".to_owned(),
))],
)
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
room.inner.update_if(|info| {
let mut res = false;
for ev in events {
res |= info.handle_state_event(ev);
}
res
});
}
/// `user_a`: empty memberships
/// `user_b`: one membership
/// `user_c`: two memberships (two devices)
fn legacy_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
let (_, room) = make_room_test_helper(RoomState::Joined);
let a_empty = legacy_member_state_event(Vec::new(), event_id!("$1234"), a);
// make b 10min old
let m_init_b = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 1);
let b_one = legacy_member_state_event(vec![m_init_b], event_id!("$12345"), b);
// c1 1min old
let m_init_c1 = legacy_membership_for_my_call(device_id!("DEVICE_0"), "0", 10);
// c2 20min old
let m_init_c2 = legacy_membership_for_my_call(device_id!("DEVICE_1"), "0", 20);
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
// Intentionally use a non time sorted receive order.
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
room
}
/// `user_a`: empty memberships
/// `user_b`: one membership
/// `user_c`: two memberships (two devices)
fn session_create_call_with_member_events_for_user(a: &UserId, b: &UserId, c: &UserId) -> Room {
let (_, room) = make_room_test_helper(RoomState::Joined);
let a_empty = session_member_state_event(event_id!("$1234"), a, None);
// make b 10min old
let b_one = session_member_state_event(
event_id!("$12345"),
b,
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 1 }),
);
let m_c1 = session_member_state_event(
event_id!("$123456_0"),
c,
Some(InitData { device_id: "DEVICE_0".into(), minutes_ago: 10 }),
);
let m_c2 = session_member_state_event(
event_id!("$123456_1"),
c,
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
);
// Intentionally use a non time sorted receive order1
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
room
}
#[test]
fn test_show_correct_active_call_state() {
let room_legacy = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
// This check also tests the ordering.
// We want older events to be in the front.
// user_b (Bob) is 1min old, c1 (CAROL) 10min old, c2 (CAROL) 20min old
assert_eq!(
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
room_legacy.active_room_call_participants()
);
assert!(room_legacy.has_active_room_call());
let room_session = session_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
assert_eq!(
vec![CAROL.to_owned(), CAROL.to_owned(), BOB.to_owned()],
room_session.active_room_call_participants()
);
assert!(room_session.has_active_room_call());
}
#[test]
fn test_active_call_is_false_when_everyone_left() {
let room = legacy_create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
let b_empty_membership = legacy_member_state_event(Vec::new(), event_id!("$1234_1"), &BOB);
let c_empty_membership =
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
// We have no active call anymore after emptying the memberships
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
assert!(!room.has_active_room_call());
}
}
+118
View File
@@ -0,0 +1,118 @@
// 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 ruma::{
assign,
events::{
macros::EventContent,
room::create::{PreviousRoom, RoomCreateEventContent},
EmptyStateKey, RedactContent, RedactedStateEventContent,
},
room::RoomType,
OwnedUserId, RoomVersionId,
};
use serde::{Deserialize, Serialize};
/// The content of an `m.room.create` event, with a required `creator` field.
///
/// Starting with room version 11, the `creator` field should be removed and the
/// `sender` field of the event should be used instead. This is reflected on
/// [`RoomCreateEventContent`].
///
/// This type was created as an alternative for ease of use. When it is used in
/// the SDK, it is constructed by copying the `sender` of the original event as
/// the `creator`.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
pub struct RoomCreateWithCreatorEventContent {
/// The `user_id` of the room creator.
///
/// This is set by the homeserver.
///
/// While this should be optional since room version 11, we copy the sender
/// of the event so we can still access it.
pub creator: OwnedUserId,
/// Whether or not this room's data should be transferred to other
/// homeservers.
#[serde(
rename = "m.federate",
default = "ruma::serde::default_true",
skip_serializing_if = "ruma::serde::is_true"
)]
pub federate: bool,
/// The version of the room.
///
/// Defaults to `RoomVersionId::V1`.
#[serde(default = "default_create_room_version_id")]
pub room_version: RoomVersionId,
/// A reference to the room this room replaces, if the previous room was
/// upgraded.
#[serde(skip_serializing_if = "Option::is_none")]
pub predecessor: Option<PreviousRoom>,
/// The room type.
///
/// This is currently only used for spaces.
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub room_type: Option<RoomType>,
}
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 }
}
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
let Self { creator, federate, room_version, predecessor, room_type } = self;
#[allow(deprecated)]
let content = assign!(RoomCreateEventContent::new_v11(), {
creator: Some(creator.clone()),
federate,
room_version,
predecessor,
room_type,
});
(content, creator)
}
}
/// Redacted form of [`RoomCreateWithCreatorEventContent`].
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
type StateKey = EmptyStateKey;
}
impl RedactContent for RoomCreateWithCreatorEventContent {
type Redacted = RedactedRoomCreateWithCreatorEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
let (content, sender) = self.into_event_content();
// Use Ruma's redaction algorithm.
let content = content.redact(version);
Self::from_event_content(content, sender)
}
}
fn default_create_room_version_id() -> RoomVersionId {
RoomVersionId::V1
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,158 @@
// 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 ruma::events::room::encryption::RoomEncryptionEventContent;
use super::Room;
impl Room {
/// Get the encryption state of this room.
pub fn encryption_state(&self) -> EncryptionState {
self.inner.read().encryption_state()
}
/// Get the `m.room.encryption` content that enabled end to end encryption
/// in the room.
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
self.inner.read().base_info.encryption.clone()
}
}
/// Represents the state of a room encryption.
#[derive(Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum EncryptionState {
/// The room is encrypted.
Encrypted,
/// The room is not encrypted.
NotEncrypted,
/// The state of the room encryption is unknown, probably because the
/// `/sync` did not provide all data needed to decide.
Unknown,
}
impl EncryptionState {
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
pub fn is_encrypted(&self) -> bool {
matches!(self, Self::Encrypted)
}
/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
#[cfg(test)]
mod tests {
use std::{
ops::{Not, Sub},
str::FromStr,
sync::Arc,
time::Duration,
};
use assert_matches::assert_matches;
use matrix_sdk_test::ALICE;
use ruma::{
events::{
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
},
room_id,
time::SystemTime,
user_id, EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
};
use super::{EncryptionState, Room};
use crate::{store::MemoryStore, RoomState};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
let user_id = user_id!("@me:example.org");
let room_id = room_id!("!test:localhost");
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
}
fn timestamp(minutes_ago: u32) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch::from_system_time(
SystemTime::now().sub(Duration::from_secs((60 * minutes_ago).into())),
)
.expect("date out of range")
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
room.inner.update_if(|info| {
let mut res = false;
for ev in events {
res |= info.handle_state_event(ev);
}
res
});
}
#[test]
fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
let (_store, room) = make_room_test_helper(RoomState::Joined);
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
let encryption_content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
OriginalSyncRoomEncryptionEvent {
content: encryption_content,
event_id: OwnedEventId::from_str("$1234_1").unwrap(),
sender: ALICE.to_owned(),
// we can simply use now here since this will be dropped when using a
// MinimalStateEvent in the roomInfo
origin_server_ts: timestamp(0),
state_key: EmptyStateKey,
unsigned: StateUnsigned::new(),
},
));
receive_state_events(&room, vec![&encryption_event]);
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
}
#[test]
fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
let (_store, room) = make_room_test_helper(RoomState::Joined);
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
room.inner.update_if(|info| {
info.mark_encryption_state_synced();
false
});
assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
}
#[test]
fn test_encryption_state() {
assert!(EncryptionState::Unknown.is_unknown());
assert!(EncryptionState::Encrypted.is_unknown().not());
assert!(EncryptionState::NotEncrypted.is_unknown().not());
assert!(EncryptionState::Unknown.is_encrypted().not());
assert!(EncryptionState::Encrypted.is_encrypted());
assert!(EncryptionState::NotEncrypted.is_encrypted().not());
}
}
+168
View File
@@ -0,0 +1,168 @@
// 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::collections::BTreeMap;
use eyeball::{AsyncLock, ObservableWriteGuard};
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
StateEventType, SyncStateEvent,
},
OwnedEventId, OwnedUserId,
};
use tracing::warn;
use super::Room;
use crate::{
deserialized_responses::{MemberEvent, RawMemberEvent, SyncOrStrippedState},
store::{Result as StoreResult, StateStoreExt},
StateStoreDataKey, StateStoreDataValue, StoreError,
};
impl Room {
/// Mark a list of requests to join the room as seen, given their state
/// event ids.
pub async fn mark_knock_requests_as_seen(&self, user_ids: &[OwnedUserId]) -> StoreResult<()> {
let raw_user_ids: Vec<&str> = user_ids.iter().map(|id| id.as_str()).collect();
let member_raw_events = self
.store
.get_state_events_for_keys(self.room_id(), StateEventType::RoomMember, &raw_user_ids)
.await?;
let mut event_to_user_ids = Vec::with_capacity(member_raw_events.len());
// Map the list of events ids to their user ids, if they are event ids for knock
// membership events. Log an error and continue otherwise.
for raw_event in member_raw_events {
let event = raw_event.cast::<RoomMemberEventContent>().deserialize()?;
match event {
SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => {
if event.content.membership == MembershipState::Knock {
event_to_user_ids.push((event.event_id, event.state_key))
} else {
warn!("Could not mark knock event as seen: event {} for user {} is not in Knock membership state.", event.event_id, event.state_key);
}
}
_ => warn!(
"Could not mark knock event as seen: event for user {} is not valid.",
event.state_key()
),
}
}
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
current_seen_events.extend(event_to_user_ids);
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
Ok(())
}
/// Removes the seen knock request ids that are no longer valid given the
/// current room members.
pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
// Get and deserialize the member events for the seen knock requests
let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
let raw_member_events: Vec<RawMemberEvent> =
self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
let member_events = raw_member_events
.into_iter()
.map(|raw| raw.deserialize())
.collect::<Result<Vec<MemberEvent>, _>>()?;
let mut ids_to_remove = Vec::new();
for (event_id, user_id) in current_seen_events.iter() {
// Check the seen knock request ids against the current room member events for
// the room members associated to them
let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
if let Some(member) = matching_member {
let member_event_id = member.event_id();
// If the member event is not a knock or it's different knock, it's outdated
if *member.membership() != MembershipState::Knock
|| member_event_id.is_some_and(|id| id != event_id)
{
ids_to_remove.push(event_id.to_owned());
}
} else {
ids_to_remove.push(event_id.to_owned());
}
}
// If there are no ids to remove, do nothing
if ids_to_remove.is_empty() {
return Ok(());
}
for event_id in ids_to_remove {
current_seen_events.remove(&event_id);
}
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
Ok(())
}
/// Get the list of seen knock request event ids in this room.
pub async fn get_seen_knock_request_ids(
&self,
) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
}
async fn get_write_guarded_current_knock_request_ids(
&self,
) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
{
let mut guard = self.seen_knock_request_ids_map.write().await;
// If there are no loaded request ids yet
if guard.is_none() {
// Load the values from the store and update the shared observable contents
let updated_seen_ids = self
.store
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
.await?
.and_then(|v| v.into_seen_knock_requests())
.unwrap_or_default();
ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
}
Ok(guard)
}
async fn update_seen_knock_request_ids(
&self,
mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
new_value: BTreeMap<OwnedEventId, OwnedUserId>,
) -> StoreResult<()> {
// Save the new values to the shared observable
ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
// Save them into the store too
self.store
.set_kv_data(
StateStoreDataKey::SeenKnockRequests(self.room_id()),
StateStoreDataValue::SeenKnockRequests(new_value),
)
.await?;
Ok(())
}
}
@@ -0,0 +1,286 @@
// 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.
#[cfg(feature = "e2e-encryption")]
use std::{collections::BTreeMap, num::NonZeroUsize};
#[cfg(feature = "e2e-encryption")]
use ruma::{events::AnySyncTimelineEvent, serde::Raw, OwnedRoomId};
use super::Room;
#[cfg(feature = "e2e-encryption")]
use super::RoomInfoNotableUpdateReasons;
use crate::latest_event::LatestEvent;
impl Room {
/// The size of the latest_encrypted_events RingBuffer
#[cfg(feature = "e2e-encryption")]
pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap();
/// Return the last event in this room, if one has been cached during
/// sliding sync.
pub fn latest_event(&self) -> Option<LatestEvent> {
self.inner.read().latest_event.as_deref().cloned()
}
/// 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
/// they are decrypted.)
#[cfg(feature = "e2e-encryption")]
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
}
/// Replace our latest_event with the supplied event, and delete it and all
/// older encrypted events from latest_encrypted_events, given that the
/// new event was at the supplied index in the latest_encrypted_events
/// list.
///
/// Panics if index is not a valid index in the latest_encrypted_events
/// list.
///
/// It is the responsibility of the caller to apply the changes into the
/// state store after calling this function.
#[cfg(feature = "e2e-encryption")]
pub(crate) fn on_latest_event_decrypted(
&self,
latest_event: Box<LatestEvent>,
index: usize,
changes: &mut crate::StateChanges,
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
) {
self.latest_encrypted_events.write().unwrap().drain(0..=index);
let room_info = changes
.room_infos
.entry(self.room_id().to_owned())
.or_insert_with(|| self.clone_info());
room_info.latest_event = Some(latest_event);
room_info_notable_updates
.entry(self.room_id().to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
}
}
#[cfg(all(test, feature = "e2e-encryption"))]
mod tests_with_e2e_encryption {
use std::sync::Arc;
use assert_matches::assert_matches;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_test::async_test;
use ruma::{room_id, serde::Raw, user_id};
use serde_json::json;
use crate::{
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) {
let store = Arc::new(MemoryStore::new());
let user_id = user_id!("@me:example.org");
let room_id = room_id!("!test:localhost");
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
}
#[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()));
client
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
None,
)
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// That has an encrypted event,
add_encrypted_event(&room, "$A");
// Sanity: it has no latest_event
assert!(room.latest_event().is_none());
// When I set up an observer on the latest_event,
let mut room_info_notable_update = client.room_info_notable_update_receiver();
// And I provide a decrypted event to replace the encrypted one,
let event = make_latest_event("$A");
let mut context = processors::Context::default();
room.on_latest_event_decrypted(
event.clone(),
0,
&mut context.state_changes,
&mut context.room_info_notable_updates,
);
assert!(context.room_info_notable_updates.contains_key(room_id));
// The subscriber isn't notified at this point.
assert!(room_info_notable_update.is_empty());
// Then updating the room info will store the event,
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
// And wake up the subscriber.
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
}
);
}
#[async_test]
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
use std::collections::BTreeMap;
// Given a room with an encrypted event
let (_store, room) = make_room_test_helper(RoomState::Joined);
add_encrypted_event(&room, "$A");
// Sanity: it has no latest_event
assert!(room.latest_event().is_none());
// When I provide a decrypted event to replace the encrypted one
let event = make_latest_event("$A");
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
event.clone(),
0,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then is it stored
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
// Given a room with some encrypted events and a latest event
use std::collections::BTreeMap;
let (_store, room) = make_room_test_helper(RoomState::Joined);
room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
add_encrypted_event(&room, "$0");
add_encrypted_event(&room, "$1");
add_encrypted_event(&room, "$2");
add_encrypted_event(&room, "$3");
// When I provide a latest event
let new_event = make_latest_event("$1");
let new_event_index = 1;
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
new_event.clone(),
new_event_index,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then the encrypted events list is shortened to only newer events
let enc_evs = room.latest_encrypted_events();
assert_eq!(enc_evs.len(), 2);
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
// And the event is stored
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
}
#[async_test]
async fn test_replacing_the_newest_event_leaves_none_left() {
use std::collections::BTreeMap;
// Given a room with some encrypted events
let (_store, room) = make_room_test_helper(RoomState::Joined);
add_encrypted_event(&room, "$0");
add_encrypted_event(&room, "$1");
add_encrypted_event(&room, "$2");
add_encrypted_event(&room, "$3");
// When I provide a latest event and say it was the very latest
let new_event = make_latest_event("$3");
let new_event_index = 3;
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
new_event,
new_event_index,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then the encrypted events list ie empty
let enc_evs = room.latest_encrypted_events();
assert_eq!(enc_evs.len(), 0);
}
fn add_encrypted_event(room: &Room, event_id: &str) {
room.latest_encrypted_events
.write()
.unwrap()
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
}
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
)))
}
}
@@ -13,28 +13,188 @@
// limitations under the License.
use std::{
collections::{BTreeSet, HashMap},
collections::{BTreeMap, BTreeSet, HashMap},
mem,
sync::Arc,
};
use bitflags::bitflags;
use ruma::{
events::{
ignored_user_list::IgnoredUserListEventContent,
presence::PresenceEvent,
room::{
member::MembershipState,
member::{MembershipState, RoomMemberEventContent},
power_levels::{PowerLevelAction, RoomPowerLevels, RoomPowerLevelsEventContent},
},
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,
store::{ambiguity_map::is_display_name_ambiguous, Result as StoreResult, StateStoreExt},
MinimalRoomMemberEvent,
};
impl Room {
/// Check if the room has its members fully synced.
///
/// Members might be missing if lazy member loading was enabled for the
/// sync.
///
/// Returns true if no members are missing, false otherwise.
pub fn are_members_synced(&self) -> bool {
self.inner.read().members_synced
}
/// Mark this Room as holding all member information.
///
/// Useful in tests if we want to persuade the Room not to sync when asked
/// about its members.
#[cfg(feature = "testing")]
pub fn mark_members_synced(&self) {
self.inner.update(|info| {
info.members_synced = true;
});
}
/// Mark this Room as still missing member information.
pub fn mark_members_missing(&self) {
self.inner.update_if(|info| {
// notify observable subscribers only if the previous value was false
mem::replace(&mut info.members_synced, false)
})
}
/// Get the `RoomMember`s of this room that are known to the store, with the
/// given memberships.
pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
if user_ids.is_empty() {
return Ok(Vec::new());
}
let member_events = self
.store
.get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
self.room_id(),
&user_ids,
)
.await?
.into_iter()
.map(|raw_event| raw_event.deserialize())
.collect::<Result<Vec<_>, _>>()?;
let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
let mut presences = self
.store
.get_presence_events(&user_ids)
.await?
.into_iter()
.filter_map(|e| {
e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
})
.collect::<BTreeMap<_, _>>();
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
let room_info = self.member_room_info(&display_names).await?;
let mut members = Vec::new();
for event in member_events {
let profile = profiles.remove(event.user_id());
let presence = presences.remove(event.user_id());
members.push(RoomMember::from_parts(event, profile, presence, &room_info))
}
Ok(members)
}
/// Returns the number of members who have joined or been invited to the
/// room.
pub fn active_members_count(&self) -> u64 {
self.inner.read().active_members_count()
}
/// Returns the number of members who have been invited to the room.
pub fn invited_members_count(&self) -> u64 {
self.inner.read().invited_members_count()
}
/// Returns the number of members who have joined the room.
pub fn joined_members_count(&self) -> u64 {
self.inner.read().joined_members_count()
}
/// Get the `RoomMember` with the given `user_id`.
///
/// Returns `None` if the member was never part of this room, otherwise
/// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
/// banned state.
///
/// Async because it can read from storage.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
debug!(%user_id, "Member event not found in state store");
return Ok(None);
};
let event = raw_event.deserialize()?;
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let display_names = [event.display_name()];
let room_info = self.member_room_info(&display_names).await?;
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
}
/// The current `MemberRoomInfo` for this room.
///
/// Async because it can read from storage.
async fn member_room_info<'a>(
&self,
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 users_display_names =
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
let ignored_users = self
.store
.get_account_data_event_static::<IgnoredUserListEventContent>()
.await?
.map(|c| c.deserialize())
.transpose()?
.map(|e| e.content.ignored_users.into_keys().collect());
Ok(MemberRoomInfo {
power_levels: power_levels.into(),
max_power_level,
room_creator,
users_display_names,
ignored_users,
})
}
}
/// A member of a room.
#[derive(Clone, Debug)]
pub struct RoomMember {
@@ -251,3 +411,78 @@ pub(crate) struct MemberRoomInfo<'a> {
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
}
/// The kind of room member updates that just happened.
#[derive(Debug, Clone)]
pub enum RoomMembersUpdate {
/// The whole list room members was reloaded.
FullReload,
/// A few members were updated, their user ids are included.
Partial(BTreeSet<OwnedUserId>),
}
bitflags! {
/// Room membership filter as a bitset.
///
/// Note that [`RoomMemberships::empty()`] doesn't filter the results and
/// [`RoomMemberships::all()`] filters out unknown memberships.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RoomMemberships: u16 {
/// The member joined the room.
const JOIN = 0b00000001;
/// The member was invited to the room.
const INVITE = 0b00000010;
/// The member requested to join the room.
const KNOCK = 0b00000100;
/// The member left the room.
const LEAVE = 0b00001000;
/// The member was banned.
const BAN = 0b00010000;
/// The member is active in the room (i.e. joined or invited).
const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
}
}
impl RoomMemberships {
/// Whether the given membership matches this `RoomMemberships`.
pub fn matches(&self, membership: &MembershipState) -> bool {
if self.is_empty() {
return true;
}
let membership = match membership {
MembershipState::Ban => Self::BAN,
MembershipState::Invite => Self::INVITE,
MembershipState::Join => Self::JOIN,
MembershipState::Knock => Self::KNOCK,
MembershipState::Leave => Self::LEAVE,
_ => return false,
};
self.contains(membership)
}
/// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
pub fn as_vec(&self) -> Vec<MembershipState> {
let mut memberships = Vec::new();
if self.contains(Self::JOIN) {
memberships.push(MembershipState::Join);
}
if self.contains(Self::INVITE) {
memberships.push(MembershipState::Invite);
}
if self.contains(Self::KNOCK) {
memberships.push(MembershipState::Knock);
}
if self.contains(Self::LEAVE) {
memberships.push(MembershipState::Leave);
}
if self.contains(Self::BAN) {
memberships.push(MembershipState::Ban);
}
memberships
}
}
+501
View File
@@ -0,0 +1,501 @@
// 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.
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
mod call;
mod create;
mod display_name;
mod encryption;
mod knock;
mod latest_event;
mod members;
mod room_info;
mod state;
mod tags;
mod tombstone;
#[cfg(feature = "e2e-encryption")]
use std::sync::RwLock as SyncRwLock;
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
pub use create::*;
pub use display_name::{RoomDisplayName, RoomHero};
pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
pub use encryption::EncryptionState;
use eyeball::{AsyncLock, SharedObservable};
use futures_util::{Stream, StreamExt};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::ring_buffer::RingBuffer;
pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
pub(crate) use room_info::SyncInfo;
pub use room_info::{
apply_redaction, BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
};
#[cfg(feature = "e2e-encryption")]
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
use ruma::{
events::{
direct::OwnedDirectUserIdentifier,
receipt::{Receipt, ReceiptThread, ReceiptType},
room::{
avatar::{self},
guest_access::GuestAccess,
history_visibility::HistoryVisibility,
join_rules::JoinRule,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
},
room::RoomType,
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
pub use state::{RoomState, RoomStateFilter};
pub(crate) use tags::RoomNotableTags;
use tokio::sync::broadcast;
pub use tombstone::{PredecessorRoom, SuccessorRoom};
use tracing::{info, instrument, warn};
use crate::{
deserialized_responses::MemberEvent,
notification_settings::RoomNotificationMode,
read_receipts::RoomReadReceipts,
store::{DynStateStore, Result as StoreResult, StateStoreExt},
sync::UnreadNotificationsCount,
Error, MinimalStateEvent,
};
/// The underlying room data structure collecting state for joined, left and
/// invited rooms.
#[derive(Debug, Clone)]
pub struct Room {
/// The room ID.
pub(super) room_id: OwnedRoomId,
/// Our own user ID.
pub(super) own_user_id: OwnedUserId,
pub(super) inner: SharedObservable<RoomInfo>,
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
pub(super) store: Arc<DynStateStore>,
/// 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
/// they are decrypted.)
///
/// Currently, these are held in Room rather than RoomInfo, because we were
/// not sure whether holding too many of them might make the cache too
/// slow to load on startup. Keeping them here means they are not cached
/// to disk but held in memory.
#[cfg(feature = "e2e-encryption")]
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
/// A map for ids of room membership events in the knocking state linked to
/// the user id of the user affected by the member event, that the current
/// user has marked as seen so they can be ignored.
pub seen_knock_request_ids_map:
SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
/// A sender that will notify receivers when room member updates happen.
pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
}
impl Room {
pub(crate) fn new(
own_user_id: &UserId,
store: Arc<DynStateStore>,
room_id: &RoomId,
room_state: RoomState,
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
) -> Self {
let room_info = RoomInfo::new(room_id, room_state);
Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
}
pub(crate) fn restore(
own_user_id: &UserId,
store: Arc<DynStateStore>,
room_info: RoomInfo,
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
) -> Self {
let (room_member_updates_sender, _) = broadcast::channel(10);
Self {
own_user_id: own_user_id.into(),
room_id: room_info.room_id.clone(),
store,
inner: SharedObservable::new(room_info),
#[cfg(feature = "e2e-encryption")]
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
Self::MAX_ENCRYPTED_EVENTS,
))),
room_info_notable_update_sender,
seen_knock_request_ids_map: SharedObservable::new_async(None),
room_member_updates_sender,
}
}
/// Get the unique room id of the room.
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
/// Get a copy of the room creator.
pub fn creator(&self) -> Option<OwnedUserId> {
self.inner.read().creator().map(ToOwned::to_owned)
}
/// Get our own user id.
pub fn own_user_id(&self) -> &UserId {
&self.own_user_id
}
/// Whether this room's [`RoomType`] is `m.space`.
pub fn is_space(&self) -> bool {
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
}
/// Returns the room's type as defined in its creation event
/// (`m.room.create`).
pub fn room_type(&self) -> Option<RoomType> {
self.inner.read().room_type().map(ToOwned::to_owned)
}
/// Get the unread notification counts.
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
self.inner.read().notification_counts
}
/// Get the number of unread messages (computed client-side).
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_messages(&self) -> u64 {
self.inner.read().read_receipts.num_unread
}
/// Get the detailed information about read receipts for the room.
pub fn read_receipts(&self) -> RoomReadReceipts {
self.inner.read().read_receipts.clone()
}
/// Get the number of unread notifications (computed client-side).
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_notifications(&self) -> u64 {
self.inner.read().read_receipts.num_notifications
}
/// Get the number of unread mentions (computed client-side), that is,
/// messages causing a highlight in a room.
///
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_mentions(&self) -> u64 {
self.inner.read().read_receipts.num_mentions
}
/// Check if the room states have been synced
///
/// States might be missing if we have only seen the room_id of this Room
/// so far, for example as the response for a `create_room` request without
/// being synced yet.
///
/// Returns true if the state is fully synced, false otherwise.
pub fn is_state_fully_synced(&self) -> bool {
self.inner.read().sync_info == SyncInfo::FullySynced
}
/// Check if the room state has been at least partially synced.
///
/// See [`Room::is_state_fully_synced`] for more info.
pub fn is_state_partially_or_fully_synced(&self) -> bool {
self.inner.read().sync_info != SyncInfo::NoState
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
self.inner.read().last_prev_batch.clone()
}
/// Get the avatar url of this room.
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
self.inner.read().avatar_url().map(ToOwned::to_owned)
}
/// Get information about the avatar of this room.
pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
self.inner.read().avatar_info().map(ToOwned::to_owned)
}
/// Get the canonical alias of this room.
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
self.inner.read().canonical_alias().map(ToOwned::to_owned)
}
/// Get the canonical alias of this room.
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
self.inner.read().alt_aliases().to_owned()
}
/// Get the `m.room.create` content of this room.
///
/// This usually isn't optional but some servers might not send an
/// `m.room.create` event as the first event for a given room, thus this can
/// be optional.
///
/// For room versions earlier than room version 11, if the event is
/// redacted, all fields except `creator` will be set to their default
/// value.
pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
match self.inner.read().base_info.create.as_ref()? {
MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
}
}
/// Is this room considered a direct message.
///
/// Async because it can read room info from storage.
#[instrument(skip_all, fields(room_id = ?self.room_id))]
pub async fn is_direct(&self) -> StoreResult<bool> {
match self.state() {
RoomState::Joined | RoomState::Left | RoomState::Banned => {
Ok(!self.inner.read().base_info.dm_targets.is_empty())
}
RoomState::Invited => {
let member = self.get_member(self.own_user_id()).await?;
match member {
None => {
info!("RoomMember not found for the user's own id");
Ok(false)
}
Some(member) => match member.event.as_ref() {
MemberEvent::Sync(_) => {
warn!("Got MemberEvent::Sync in an invited room");
Ok(false)
}
MemberEvent::Stripped(event) => {
Ok(event.content.is_direct.unwrap_or(false))
}
},
}
}
// TODO: implement logic once we have the stripped events as we'd have with an Invite
RoomState::Knocked => Ok(false),
}
}
/// If this room is a direct message, get the members that we're sharing the
/// room with.
///
/// *Note*: The member list might have been modified in the meantime and
/// the targets might not even be in the room anymore. This setting should
/// only be considered as guidance. We leave members in this list to allow
/// us to re-find a DM with a user even if they have left, since we may
/// want to re-invite them.
pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
self.inner.read().base_info.dm_targets.clone()
}
/// If this room is a direct message, returns the number of members that
/// we're sharing the room with.
pub fn direct_targets_length(&self) -> usize {
self.inner.read().base_info.dm_targets.len()
}
/// Get the guest access policy of this room.
pub fn guest_access(&self) -> GuestAccess {
self.inner.read().guest_access().clone()
}
/// Get the history visibility policy of this room.
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
self.inner.read().history_visibility().cloned()
}
/// Get the history visibility policy of this room, or a sensible default if
/// the event is missing.
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
self.inner.read().history_visibility_or_default().clone()
}
/// Is the room considered to be public.
pub fn is_public(&self) -> bool {
matches!(self.join_rule(), JoinRule::Public)
}
/// Get the join rule policy of this room.
pub fn join_rule(&self) -> JoinRule {
self.inner.read().join_rule().clone()
}
/// Get the maximum power level that this room contains.
///
/// This is useful if one wishes to normalize the power levels, e.g. from
/// 0-100 where 100 would be the max power level.
pub fn max_power_level(&self) -> i64 {
self.inner.read().base_info.max_power_level
}
/// Get the current power levels of this room.
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
Ok(self
.store
.get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
.await?
.ok_or(Error::InsufficientData)?
.deserialize()?
.power_levels())
}
/// Get the `m.room.name` of this room.
///
/// The returned string may be empty if the event has been redacted, or it's
/// missing from storage.
pub fn name(&self) -> Option<String> {
self.inner.read().name().map(ToOwned::to_owned)
}
/// Get the topic of the room.
pub fn topic(&self) -> Option<String> {
self.inner.read().topic().map(ToOwned::to_owned)
}
/// Update the cached user defined notification mode.
///
/// This is automatically recomputed on every successful sync, and the
/// cached result can be retrieved in
/// [`Self::cached_user_defined_notification_mode`].
pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
self.inner.update_if(|info| {
if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
info.cached_user_defined_notification_mode = Some(mode);
true
} else {
false
}
});
}
/// Returns the cached user defined notification mode, if available.
///
/// This cache is refilled every time we call
/// [`Self::update_cached_user_defined_notification_mode`].
pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
self.inner.read().cached_user_defined_notification_mode
}
/// Get the list of users ids that are considered to be joined members of
/// this room.
pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
}
/// Get the heroes for this room.
pub fn heroes(&self) -> Vec<RoomHero> {
self.inner.read().heroes().to_vec()
}
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
/// `receipt_type`, `thread` and `user_id` in this room.
pub async fn load_user_receipt(
&self,
receipt_type: ReceiptType,
thread: ReceiptThread,
user_id: &UserId,
) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
}
/// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
/// tuples for the given `receipt_type`, `thread` and `event_id` in this
/// room.
pub async fn load_event_receipts(
&self,
receipt_type: ReceiptType,
thread: ReceiptThread,
event_id: &EventId,
) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
self.store
.get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
.await
}
/// Returns a boolean indicating if this room has been manually marked as
/// unread
pub fn is_marked_unread(&self) -> bool {
self.inner.read().base_info.is_marked_unread
}
/// Returns the recency stamp of the room.
///
/// Please read `RoomInfo::recency_stamp` to learn more.
pub fn recency_stamp(&self) -> Option<u64> {
self.inner.read().recency_stamp
}
/// Get a `Stream` of loaded pinned events for this room.
/// If no pinned events are found a single empty `Vec` will be returned.
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> {
self.inner
.subscribe()
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
}
/// Returns the current pinned event ids for this room.
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
self.inner.read().pinned_event_ids()
}
}
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Send for Room {}
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Sync for Room {}
#[cfg(feature = "test-send-sync")]
#[test]
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
fn test_send_sync_for_room() {
fn assert_send_sync<
T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
>() {
}
assert_send_sync::<Room>();
}
/// The possible sources of an account data type.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum AccountDataSource {
/// The source is account data with the stable prefix.
Stable,
/// The source is account data with the unstable prefix.
#[default]
Unstable,
}
File diff suppressed because it is too large Load Diff
+192
View File
@@ -0,0 +1,192 @@
// 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 bitflags::bitflags;
use ruma::events::room::member::MembershipState;
use serde::{Deserialize, Serialize};
use super::Room;
impl Room {
/// Get the state of the room.
pub fn state(&self) -> RoomState {
self.inner.read().room_state
}
}
/// Enum keeping track in which state the room is, e.g. if our own user is
/// joined, RoomState::Invited, or has left the room.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomState {
/// The room is in a joined state.
Joined,
/// The room is in a left state.
Left,
/// The room is in an invited state.
Invited,
/// The room is in a knocked state.
Knocked,
/// The room is in a banned state.
Banned,
}
impl From<&MembershipState> for RoomState {
fn from(membership_state: &MembershipState) -> Self {
match membership_state {
MembershipState::Ban => Self::Banned,
MembershipState::Invite => Self::Invited,
MembershipState::Join => Self::Joined,
MembershipState::Knock => Self::Knocked,
MembershipState::Leave => Self::Left,
_ => panic!("Unexpected MembershipState: {membership_state}"),
}
}
}
bitflags! {
/// Room state filter as a bitset.
///
/// Note that [`RoomStateFilter::empty()`] doesn't filter the results and
/// is equivalent to [`RoomStateFilter::all()`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RoomStateFilter: u16 {
/// The room is in a joined state.
const JOINED = 0b00000001;
/// The room is in an invited state.
const INVITED = 0b00000010;
/// The room is in a left state.
const LEFT = 0b00000100;
/// The room is in a knocked state.
const KNOCKED = 0b00001000;
/// The room is in a banned state.
const BANNED = 0b00010000;
}
}
impl RoomStateFilter {
/// Whether the given room state matches this `RoomStateFilter`.
pub fn matches(&self, state: RoomState) -> bool {
if self.is_empty() {
return true;
}
let bit_state = match state {
RoomState::Joined => Self::JOINED,
RoomState::Left => Self::LEFT,
RoomState::Invited => Self::INVITED,
RoomState::Knocked => Self::KNOCKED,
RoomState::Banned => Self::BANNED,
};
self.contains(bit_state)
}
/// Get this `RoomStateFilter` as a list of matching [`RoomState`]s.
pub fn as_vec(&self) -> Vec<RoomState> {
let mut states = Vec::new();
if self.contains(Self::JOINED) {
states.push(RoomState::Joined);
}
if self.contains(Self::LEFT) {
states.push(RoomState::Left);
}
if self.contains(Self::INVITED) {
states.push(RoomState::Invited);
}
if self.contains(Self::KNOCKED) {
states.push(RoomState::Knocked);
}
if self.contains(Self::BANNED) {
states.push(RoomState::Banned);
}
states
}
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::async_test;
use ruma::owned_room_id;
use super::{RoomState, RoomStateFilter};
use crate::test_utils::logged_in_base_client;
#[async_test]
async fn test_room_state_filters() {
let client = logged_in_base_client(None).await;
let joined_room_id = owned_room_id!("!joined:example.org");
client.get_or_create_room(&joined_room_id, RoomState::Joined);
let invited_room_id = owned_room_id!("!invited:example.org");
client.get_or_create_room(&invited_room_id, RoomState::Invited);
let left_room_id = owned_room_id!("!left:example.org");
client.get_or_create_room(&left_room_id, RoomState::Left);
let knocked_room_id = owned_room_id!("!knocked:example.org");
client.get_or_create_room(&knocked_room_id, RoomState::Knocked);
let banned_room_id = owned_room_id!("!banned:example.org");
client.get_or_create_room(&banned_room_id, RoomState::Banned);
let joined_rooms = client.rooms_filtered(RoomStateFilter::JOINED);
assert_eq!(joined_rooms.len(), 1);
assert_eq!(joined_rooms[0].state(), RoomState::Joined);
assert_eq!(joined_rooms[0].room_id, joined_room_id);
let invited_rooms = client.rooms_filtered(RoomStateFilter::INVITED);
assert_eq!(invited_rooms.len(), 1);
assert_eq!(invited_rooms[0].state(), RoomState::Invited);
assert_eq!(invited_rooms[0].room_id, invited_room_id);
let left_rooms = client.rooms_filtered(RoomStateFilter::LEFT);
assert_eq!(left_rooms.len(), 1);
assert_eq!(left_rooms[0].state(), RoomState::Left);
assert_eq!(left_rooms[0].room_id, left_room_id);
let knocked_rooms = client.rooms_filtered(RoomStateFilter::KNOCKED);
assert_eq!(knocked_rooms.len(), 1);
assert_eq!(knocked_rooms[0].state(), RoomState::Knocked);
assert_eq!(knocked_rooms[0].room_id, knocked_room_id);
let banned_rooms = client.rooms_filtered(RoomStateFilter::BANNED);
assert_eq!(banned_rooms.len(), 1);
assert_eq!(banned_rooms[0].state(), RoomState::Banned);
assert_eq!(banned_rooms[0].room_id, banned_room_id);
}
#[test]
fn test_room_state_filters_as_vec() {
assert_eq!(RoomStateFilter::JOINED.as_vec(), vec![RoomState::Joined]);
assert_eq!(RoomStateFilter::LEFT.as_vec(), vec![RoomState::Left]);
assert_eq!(RoomStateFilter::INVITED.as_vec(), vec![RoomState::Invited]);
assert_eq!(RoomStateFilter::KNOCKED.as_vec(), vec![RoomState::Knocked]);
assert_eq!(RoomStateFilter::BANNED.as_vec(), vec![RoomState::Banned]);
// Check all filters are taken into account
assert_eq!(
RoomStateFilter::all().as_vec(),
vec![
RoomState::Joined,
RoomState::Left,
RoomState::Invited,
RoomState::Knocked,
RoomState::Banned
]
);
}
}
+311
View File
@@ -0,0 +1,311 @@
// 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 bitflags::bitflags;
use ruma::events::{tag::Tags, AnyRoomAccountDataEvent, RoomAccountDataEventType};
use serde::{Deserialize, Serialize};
use super::Room;
use crate::store::Result as StoreResult;
impl Room {
/// Get the `Tags` for this room.
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
.store
.get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
.await?
.and_then(|raw| raw.deserialize().ok())
{
Ok(Some(event.content.tags))
} else {
Ok(None)
}
}
/// Check whether the room is marked as favourite.
///
/// A room is considered favourite if it has received the `m.favourite` tag.
pub fn is_favourite(&self) -> bool {
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
}
/// Check whether the room is marked as low priority.
///
/// A room is considered low priority if it has received the `m.lowpriority`
/// tag.
pub fn is_low_priority(&self) -> bool {
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
}
}
bitflags! {
/// Notable tags, i.e. subset of tags that we are more interested by.
///
/// We are not interested by all the tags. Some tags are more important than
/// others, and this struct describes them.
#[repr(transparent)]
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub(crate) struct RoomNotableTags: u8 {
/// The `m.favourite` tag.
const FAVOURITE = 0b0000_0001;
/// THe `m.lowpriority` tag.
const LOW_PRIORITY = 0b0000_0010;
}
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use matrix_sdk_test::async_test;
use ruma::{
events::tag::{TagInfo, TagName, Tags},
room_id,
serde::Raw,
user_id,
};
use serde_json::json;
use stream_assert::{assert_pending, assert_ready};
use super::{super::BaseRoomInfo, RoomNotableTags};
use crate::{
response_processors as processors,
store::{RoomLoadSettings, StoreConfig},
BaseClient, RoomState, SessionMeta,
};
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
client
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// Sanity checks to ensure the room isn't marked as favourite.
assert!(room.is_favourite().not());
// Subscribe to the `RoomInfo`.
let mut room_info_subscriber = room.subscribe_info();
assert_pending!(room_info_subscriber);
// Create the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {
"m.favourite": {
"order": 0.0
},
},
},
"type": "m.tag",
}))
.unwrap()
.cast();
// When the new tag is handled and applied.
let mut context = processors::Context::default();
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
processors::changes::save_and_apply(
context.clone(),
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as favourite.
assert!(room.is_favourite());
// Now, let's remove the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {},
},
"type": "m.tag"
}))
.unwrap()
.cast();
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as _not_ favourite.
assert!(room.is_favourite().not());
}
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
client
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// Sanity checks to ensure the room isn't marked as low priority.
assert!(!room.is_low_priority());
// Subscribe to the `RoomInfo`.
let mut room_info_subscriber = room.subscribe_info();
assert_pending!(room_info_subscriber);
// Create the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {
"m.lowpriority": {
"order": 0.0
},
}
},
"type": "m.tag"
}))
.unwrap()
.cast();
// When the new tag is handled and applied.
let mut context = processors::Context::default();
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
processors::changes::save_and_apply(
context.clone(),
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as low priority.
assert!(room.is_low_priority());
// Now, let's remove the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {},
},
"type": "m.tag"
}))
.unwrap()
.cast();
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as _not_ low priority.
assert!(room.is_low_priority().not());
}
#[test]
fn test_handle_notable_tags_favourite() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::Favorite, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
}
#[test]
fn test_handle_notable_tags_low_priority() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::LowPriority, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
}
}
@@ -0,0 +1,258 @@
// 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::ops::Not;
use ruma::{events::room::tombstone::RoomTombstoneEventContent, OwnedEventId, OwnedRoomId};
use super::Room;
impl Room {
/// Has the room been tombstoned.
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
/// event; see [`Room::tombstone_content`].
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
pub fn is_tombstoned(&self) -> bool {
self.inner.read().base_info.tombstone.is_some()
}
/// Get the [`m.room.tombstone`] state event's content of this room if one
/// has been received.
///
/// Also see [`Room::is_tombstoned`] to check if the [`m.room.tombstone`]
/// event has been received. It's faster than using this method.
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
pub fn tombstone_content(&self) -> Option<RoomTombstoneEventContent> {
self.inner.read().tombstone().cloned()
}
/// If this room is tombstoned, return the “reference” to the successor room
/// —i.e. the room replacing this one.
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
/// event; see [`Room::tombstone_content`].
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
pub fn successor_room(&self) -> Option<SuccessorRoom> {
self.tombstone_content().map(|tombstone_event| SuccessorRoom {
room_id: tombstone_event.replacement_room,
reason: tombstone_event.body.is_empty().not().then_some(tombstone_event.body),
})
}
/// If this room is the successor of a tombstoned room, return the
/// “reference” to the predecessor room.
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state
/// event; see [`Room::tombstone_content`].
///
/// To determine if a room is the successor of a tombstoned room, the
/// [`m.room.create`] must have been received, **with** a `predecessor`
/// field. See [`Room::create_content`].
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
pub fn predecessor_room(&self) -> Option<PredecessorRoom> {
self.create_content().and_then(|content_event| content_event.predecessor).map(
|predecessor| PredecessorRoom {
room_id: predecessor.room_id,
last_event_id: predecessor.event_id,
},
)
}
}
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
/// predecessor of B, and B is the successor of A. This type holds information
/// about the successor room. See [`Room::successor_room`].
///
/// A room is tombstoned if it has received a [`m.room.tombstone`] state event.
///
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
#[derive(Debug)]
pub struct SuccessorRoom {
/// The ID of the next room replacing this (tombstoned) room.
pub room_id: OwnedRoomId,
/// The reason why the room has been tombstoned.
pub reason: Option<String>,
}
/// When a room A is tombstoned, it is replaced by a room B. The room A is the
/// predecessor of B, and B is the successor of A. This type holds information
/// about the predecessor room. See [`Room::predecessor_room`].
///
/// To know the predecessor of a room, the [`m.room.create`] state event must
/// have been received.
///
/// [`m.room.create`]: https://spec.matrix.org/v1.14/client-server-api/#mroomcreate
#[derive(Debug)]
pub struct PredecessorRoom {
/// The ID of the old room.
pub room_id: OwnedRoomId,
/// The event ID of the last known event in the predecesssor room.
pub last_event_id: OwnedEventId,
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use assert_matches::assert_matches;
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
};
use ruma::{event_id, room_id, user_id, RoomVersionId};
use crate::{test_utils::logged_in_base_client, RoomState};
#[async_test]
async fn test_no_successor_room() {
let client = logged_in_base_client(None).await;
let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
assert!(room.is_tombstoned().not());
assert!(room.tombstone_content().is_none());
assert!(room.successor_room().is_none());
}
#[async_test]
async fn test_successor_room() {
let client = logged_in_base_client(None).await;
let sender = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!r0");
let successor_room_id = room_id!("!r1");
let room = client.get_or_create_room(room_id, RoomState::Joined);
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new()
.sender(sender)
.room_tombstone("traces of you", successor_room_id),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(room.is_tombstoned());
assert!(room.tombstone_content().is_some());
assert_matches!(room.successor_room(), Some(successor_room) => {
assert_eq!(successor_room.room_id, successor_room_id);
assert_matches!(successor_room.reason, Some(reason) => {
assert_eq!(reason, "traces of you");
});
});
}
#[async_test]
async fn test_successor_room_no_reason() {
let client = logged_in_base_client(None).await;
let sender = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!r0");
let successor_room_id = room_id!("!r1");
let room = client.get_or_create_room(room_id, RoomState::Joined);
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new().sender(sender).room_tombstone(
// An empty reason will result in `None` in `SuccessorRoom::reason`.
"",
successor_room_id,
),
))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(room.is_tombstoned());
assert!(room.tombstone_content().is_some());
assert_matches!(room.successor_room(), Some(successor_room) => {
assert_eq!(successor_room.room_id, successor_room_id);
assert!(successor_room.reason.is_none());
});
}
#[async_test]
async fn test_no_predecessor_room() {
let client = logged_in_base_client(None).await;
let room = client.get_or_create_room(room_id!("!r0"), RoomState::Joined);
assert!(room.create_content().is_none());
assert!(room.predecessor_room().is_none());
}
#[async_test]
async fn test_no_predecessor_room_with_create_event() {
let client = logged_in_base_client(None).await;
let sender = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!r1");
let room = client.get_or_create_room(room_id, RoomState::Joined);
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new()
.create(sender, RoomVersionId::V11)
// No `predecessor` field!
.no_predecessor()
.into_raw_sync(),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(room.create_content().is_some());
assert!(room.predecessor_room().is_none());
}
#[async_test]
async fn test_predecessor_room() {
let client = logged_in_base_client(None).await;
let sender = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!r1");
let predecessor_room_id = room_id!("!r0");
let predecessor_last_event_id = event_id!("$ev42");
let room = client.get_or_create_room(room_id, RoomState::Joined);
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new()
.create(sender, RoomVersionId::V11)
.predecessor(predecessor_room_id, predecessor_last_event_id)
.into_raw_sync(),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert!(room.create_content().is_some());
assert_matches!(room.predecessor_room(), Some(predecessor_room) => {
assert_eq!(predecessor_room.room_id, predecessor_room_id);
assert_eq!(predecessor_room.last_event_id, predecessor_last_event_id);
});
}
}
-643
View File
@@ -1,643 +0,0 @@
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
mod members;
pub(crate) mod normal;
use std::{
collections::{BTreeMap, HashSet},
fmt,
hash::Hash,
};
use bitflags::bitflags;
pub use members::RoomMember;
pub use normal::{
apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter,
};
use regex::Regex;
use ruma::{
assign,
events::{
beacon_info::BeaconInfoEventContent,
call::member::{CallMemberEventContent, CallMemberStateKey},
direct::OwnedDirectUserIdentifier,
macros::EventContent,
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::{PreviousRoom, RoomCreateEventContent},
encryption::RoomEncryptionEventContent,
guest_access::RoomGuestAccessEventContent,
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::RoomJoinRulesEventContent,
member::MembershipState,
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
tag::{TagName, Tags},
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
},
room::RoomType,
EventId, OwnedUserId, RoomVersionId,
};
use serde::{Deserialize, Serialize};
use crate::MinimalStateEvent;
/// The name of the room, either from the metadata or calculated
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomDisplayName {
/// The room has been named explicitly as
Named(String),
/// The room has a canonical alias that should be used
Aliased(String),
/// The room has not given an explicit name but a name could be
/// calculated
Calculated(String),
/// The room doesn't have a name right now, but used to have one
/// e.g. because it was a DM and everyone has left the room
EmptyWas(String),
/// No useful name could be calculated or ever found
Empty,
}
const WHITESPACE_REGEX: &str = r"\s+";
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
impl RoomDisplayName {
/// Transforms the current display name into the name part of a
/// `RoomAliasId`.
pub fn to_room_alias_name(&self) -> String {
let room_name = match self {
Self::Named(name) => name,
Self::Aliased(name) => name,
Self::Calculated(name) => name,
Self::EmptyWas(name) => name,
Self::Empty => "",
};
let whitespace_regex =
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
let symbol_regex =
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
// Replace whitespaces with `-`
let sanitised = whitespace_regex.replace_all(room_name, "-");
// Remove non-ASCII characters and ASCII control characters
let sanitised =
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
// Remove other problematic ASCII symbols
let sanitised = symbol_regex.replace_all(&sanitised, "");
// Lowercased
sanitised.to_lowercase()
}
}
impl fmt::Display for RoomDisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoomDisplayName::Named(s)
| RoomDisplayName::Calculated(s)
| RoomDisplayName::Aliased(s) => {
write!(f, "{s}")
}
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
RoomDisplayName::Empty => write!(f, "Empty Room"),
}
}
}
/// A base room info struct that is the backbone of normal as well as stripped
/// rooms. Holds all the state events that are important to present a room to
/// users.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BaseRoomInfo {
/// The avatar URL of this room.
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
/// All shared live location beacons of this room.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
/// The canonical alias of this room.
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
/// The `m.room.create` event content of this room.
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
/// A list of user ids this room is considered as direct message, if this
/// room is a DM.
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
/// The `m.room.encryption` event content that enabled E2EE in this room.
pub(crate) encryption: Option<RoomEncryptionEventContent>,
/// The guest access policy of this room.
pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
/// The history visibility policy of this room.
pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
/// The join rule policy of this room.
pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
/// The maximal power level that can be found in this room.
pub(crate) max_power_level: i64,
/// The `m.room.name` of this room.
pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
/// The `m.room.tombstone` event content of this room.
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
/// The topic of this room.
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
/// All minimal state events that containing one or more running matrixRTC
/// memberships.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member_events:
BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
/// Whether this room has been manually marked as unread.
#[serde(default)]
pub(crate) is_marked_unread: bool,
/// Some notable tags.
///
/// We are not interested by all the tags. Some tags are more important than
/// others, and this field collects them.
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
pub(crate) notable_tags: RoomNotableTags,
/// The `m.room.pinned_events` of this room.
pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
}
impl BaseRoomInfo {
/// Create a new, empty base room info.
pub fn new() -> Self {
Self::default()
}
/// Get the room version of this room.
///
/// For room versions earlier than room version 11, if the event is
/// redacted, this will return the default of [`RoomVersionId::V1`].
pub fn room_version(&self) -> Option<&RoomVersionId> {
match self.create.as_ref()? {
MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
}
}
/// Handle a state event for this room and update our info accordingly.
///
/// Returns true if the event modified the info, false otherwise.
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
match ev {
AnySyncStateEvent::BeaconInfo(b) => {
self.beacons.insert(b.state_key().clone(), b.into());
}
// No redacted branch - enabling encryption cannot be undone.
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
self.encryption = Some(encryption.content.clone());
}
AnySyncStateEvent::RoomAvatar(a) => {
self.avatar = Some(a.into());
}
AnySyncStateEvent::RoomName(n) => {
self.name = Some(n.into());
}
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
self.create = Some(c.into());
}
AnySyncStateEvent::RoomHistoryVisibility(h) => {
self.history_visibility = Some(h.into());
}
AnySyncStateEvent::RoomGuestAccess(g) => {
self.guest_access = Some(g.into());
}
AnySyncStateEvent::RoomJoinRules(c) => {
self.join_rules = Some(c.into());
}
AnySyncStateEvent::RoomCanonicalAlias(a) => {
self.canonical_alias = Some(a.into());
}
AnySyncStateEvent::RoomTopic(t) => {
self.topic = Some(t.into());
}
AnySyncStateEvent::RoomTombstone(t) => {
self.tombstone = Some(t.into());
}
AnySyncStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnySyncStateEvent::CallMember(m) => {
let Some(o_ev) = m.as_original() else {
return false;
};
// we modify the event so that `origin_sever_ts` gets copied into
// `content.created_ts`
let mut o_ev = o_ev.clone();
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
// Add the new event.
self.rtc_member_events
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
// Remove all events that don't contain any memberships anymore.
self.rtc_member_events.retain(|_, ev| {
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
});
}
AnySyncStateEvent::RoomPinnedEvents(p) => {
self.pinned_events = p.as_original().map(|p| p.content.clone());
}
_ => return false,
}
true
}
/// Handle a stripped state event for this room and update our info
/// accordingly.
///
/// Returns true if the event modified the info, false otherwise.
pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
match ev {
AnyStrippedStateEvent::RoomEncryption(encryption) => {
if let Some(algorithm) = &encryption.content.algorithm {
let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
rotation_period_ms: encryption.content.rotation_period_ms,
rotation_period_msgs: encryption.content.rotation_period_msgs,
});
self.encryption = Some(content);
}
// If encryption event is redacted, we don't care much. When
// entering the room, we will fetch the proper event before
// sending any messages.
}
AnyStrippedStateEvent::RoomAvatar(a) => {
self.avatar = Some(a.into());
}
AnyStrippedStateEvent::RoomName(n) => {
self.name = Some(n.into());
}
AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
self.create = Some(c.into());
}
AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
self.history_visibility = Some(h.into());
}
AnyStrippedStateEvent::RoomGuestAccess(g) => {
self.guest_access = Some(g.into());
}
AnyStrippedStateEvent::RoomJoinRules(c) => {
self.join_rules = Some(c.into());
}
AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
self.canonical_alias = Some(a.into());
}
AnyStrippedStateEvent::RoomTopic(t) => {
self.topic = Some(t.into());
}
AnyStrippedStateEvent::RoomTombstone(t) => {
self.tombstone = Some(t.into());
}
AnyStrippedStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnyStrippedStateEvent::CallMember(_) => {
// Ignore stripped call state events. Rooms that are not in Joined or Left state
// wont have call information.
return false;
}
AnyStrippedStateEvent::RoomPinnedEvents(p) => {
if let Some(pinned) = p.content.pinned.clone() {
self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
}
}
_ => return false,
}
true
}
fn handle_redaction(&mut self, redacts: &EventId) {
let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
// FIXME: Use let chains once available to get rid of unwrap()s
if self.avatar.has_event_id(redacts) {
self.avatar.as_mut().unwrap().redact(&room_version);
} else if self.canonical_alias.has_event_id(redacts) {
self.canonical_alias.as_mut().unwrap().redact(&room_version);
} else if self.create.has_event_id(redacts) {
self.create.as_mut().unwrap().redact(&room_version);
} else if self.guest_access.has_event_id(redacts) {
self.guest_access.as_mut().unwrap().redact(&room_version);
} else if self.history_visibility.has_event_id(redacts) {
self.history_visibility.as_mut().unwrap().redact(&room_version);
} else if self.join_rules.has_event_id(redacts) {
self.join_rules.as_mut().unwrap().redact(&room_version);
} else if self.name.has_event_id(redacts) {
self.name.as_mut().unwrap().redact(&room_version);
} else if self.tombstone.has_event_id(redacts) {
self.tombstone.as_mut().unwrap().redact(&room_version);
} else if self.topic.has_event_id(redacts) {
self.topic.as_mut().unwrap().redact(&room_version);
} else {
self.rtc_member_events
.retain(|_, member_event| member_event.event_id() != Some(redacts));
}
}
pub fn handle_notable_tags(&mut self, tags: &Tags) {
let mut notable_tags = RoomNotableTags::empty();
if tags.contains_key(&TagName::Favorite) {
notable_tags.insert(RoomNotableTags::FAVOURITE);
}
if tags.contains_key(&TagName::LowPriority) {
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
}
self.notable_tags = notable_tags;
}
}
bitflags! {
/// Notable tags, i.e. subset of tags that we are more interested by.
///
/// We are not interested by all the tags. Some tags are more important than
/// others, and this struct describes them.
#[repr(transparent)]
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub(crate) struct RoomNotableTags: u8 {
/// The `m.favourite` tag.
const FAVOURITE = 0b0000_0001;
/// THe `m.lowpriority` tag.
const LOW_PRIORITY = 0b0000_0010;
}
}
trait OptionExt {
fn has_event_id(&self, ev_id: &EventId) -> bool;
}
impl<C> OptionExt for Option<MinimalStateEvent<C>>
where
C: StaticStateEventContent + RedactContent,
C::Redacted: RedactedStateEventContent,
{
fn has_event_id(&self, ev_id: &EventId) -> bool {
self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
}
}
impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar: None,
beacons: BTreeMap::new(),
canonical_alias: None,
create: None,
dm_targets: Default::default(),
encryption: None,
guest_access: None,
history_visibility: None,
join_rules: None,
max_power_level: 100,
name: None,
tombstone: None,
topic: None,
rtc_member_events: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
pinned_events: None,
}
}
}
/// The content of an `m.room.create` event, with a required `creator` field.
///
/// Starting with room version 11, the `creator` field should be removed and the
/// `sender` field of the event should be used instead. This is reflected on
/// [`RoomCreateEventContent`].
///
/// This type was created as an alternative for ease of use. When it is used in
/// the SDK, it is constructed by copying the `sender` of the original event as
/// the `creator`.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
pub struct RoomCreateWithCreatorEventContent {
/// The `user_id` of the room creator.
///
/// This is set by the homeserver.
///
/// While this should be optional since room version 11, we copy the sender
/// of the event so we can still access it.
pub creator: OwnedUserId,
/// Whether or not this room's data should be transferred to other
/// homeservers.
#[serde(
rename = "m.federate",
default = "ruma::serde::default_true",
skip_serializing_if = "ruma::serde::is_true"
)]
pub federate: bool,
/// The version of the room.
///
/// Defaults to `RoomVersionId::V1`.
#[serde(default = "default_create_room_version_id")]
pub room_version: RoomVersionId,
/// A reference to the room this room replaces, if the previous room was
/// upgraded.
#[serde(skip_serializing_if = "Option::is_none")]
pub predecessor: Option<PreviousRoom>,
/// The room type.
///
/// This is currently only used for spaces.
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub room_type: Option<RoomType>,
}
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 }
}
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
let Self { creator, federate, room_version, predecessor, room_type } = self;
#[allow(deprecated)]
let content = assign!(RoomCreateEventContent::new_v11(), {
creator: Some(creator.clone()),
federate,
room_version,
predecessor,
room_type,
});
(content, creator)
}
}
/// Redacted form of [`RoomCreateWithCreatorEventContent`].
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
type StateKey = EmptyStateKey;
}
impl RedactContent for RoomCreateWithCreatorEventContent {
type Redacted = RedactedRoomCreateWithCreatorEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
let (content, sender) = self.into_event_content();
// Use Ruma's redaction algorithm.
let content = content.redact(version);
Self::from_event_content(content, sender)
}
}
fn default_create_room_version_id() -> RoomVersionId {
RoomVersionId::V1
}
bitflags! {
/// Room membership filter as a bitset.
///
/// Note that [`RoomMemberships::empty()`] doesn't filter the results and
/// [`RoomMemberships::all()`] filters out unknown memberships.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RoomMemberships: u16 {
/// The member joined the room.
const JOIN = 0b00000001;
/// The member was invited to the room.
const INVITE = 0b00000010;
/// The member requested to join the room.
const KNOCK = 0b00000100;
/// The member left the room.
const LEAVE = 0b00001000;
/// The member was banned.
const BAN = 0b00010000;
/// The member is active in the room (i.e. joined or invited).
const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
}
}
impl RoomMemberships {
/// Whether the given membership matches this `RoomMemberships`.
pub fn matches(&self, membership: &MembershipState) -> bool {
if self.is_empty() {
return true;
}
let membership = match membership {
MembershipState::Ban => Self::BAN,
MembershipState::Invite => Self::INVITE,
MembershipState::Join => Self::JOIN,
MembershipState::Knock => Self::KNOCK,
MembershipState::Leave => Self::LEAVE,
_ => return false,
};
self.contains(membership)
}
/// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
pub fn as_vec(&self) -> Vec<MembershipState> {
let mut memberships = Vec::new();
if self.contains(Self::JOIN) {
memberships.push(MembershipState::Join);
}
if self.contains(Self::INVITE) {
memberships.push(MembershipState::Invite);
}
if self.contains(Self::KNOCK) {
memberships.push(MembershipState::Knock);
}
if self.contains(Self::LEAVE) {
memberships.push(MembershipState::Leave);
}
if self.contains(Self::BAN) {
memberships.push(MembershipState::Ban);
}
memberships
}
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use ruma::events::tag::{TagInfo, TagName, Tags};
use super::{BaseRoomInfo, RoomNotableTags};
use crate::RoomDisplayName;
#[test]
fn test_handle_notable_tags_favourite() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::Favorite, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
}
#[test]
fn test_handle_notable_tags_low_priority() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::LowPriority, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
}
#[test]
fn test_room_alias_from_room_display_name_lowercases() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_whitespace() {
assert_eq!(
"room-alias",
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
assert_eq!(
"roomalias",
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_test::{event_factory::EventFactory, test_json};
use ruma::{
@@ -47,8 +46,7 @@ use crate::{
///
/// This trait is not meant to be used directly, but will be used with the
/// `statestore_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[allow(async_fn_in_trait)]
pub trait StateStoreIntegrationTests {
/// Populate the given `StateStore`.
async fn populate(&self) -> Result<()>;
@@ -98,8 +96,6 @@ pub trait StateStoreIntegrationTests {
async fn test_get_room_infos(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl StateStoreIntegrationTests for DynStateStore {
async fn populate(&self) -> Result<()> {
let mut changes = StateChanges::default();
@@ -138,8 +138,8 @@ impl MemoryStore {
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl StateStore for MemoryStore {
type Error = StoreError;
@@ -44,10 +44,7 @@ use serde::{Deserialize, Serialize};
use crate::{
deserialized_responses::SyncOrStrippedState,
latest_event::LatestEvent,
rooms::{
normal::{RoomSummary, SyncInfo},
BaseRoomInfo, RoomNotableTags,
},
room::{BaseRoomInfo, RoomSummary, SyncInfo},
sync::UnreadNotificationsCount,
MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
};
@@ -108,10 +105,9 @@ impl RoomInfoV1 {
} = self;
RoomInfo {
version: 0,
data_format_version: 0,
room_id,
room_state: room_type,
prev_room_state: None,
notification_counts,
summary,
members_synced,
@@ -214,10 +210,7 @@ impl BaseRoomInfoV1 {
name,
tombstone,
topic,
rtc_member_events: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
pinned_events: None,
..Default::default()
})
}
}

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