Compare commits

...

231 Commits

Author SHA1 Message Date
ganfra f92ccd8d40 feat(ffi): expose BeaconError from sdk to ffi
Benchmarks / Run Benchmarks (crypto_bench) (push) Failing after 1m29s
Benchmarks / Run Benchmarks (event_cache) (push) Failing after 6s
Benchmarks / Run Benchmarks (linked_chunk) (push) Failing after 6s
Benchmarks / Run Benchmarks (room_list) (push) Failing after 7s
Benchmarks / Run Benchmarks (store_bench) (push) Failing after 7s
Benchmarks / Run Benchmarks (timeline) (push) Failing after 7s
Bindings tests / xtask (push) Failing after 2m6s
Bindings tests / Run Complement Crypto tests (push) Failing after 0s
Rust tests / xtask (push) Failing after 42s
Rust tests / 🐧 [m], experimental-encrypted-state-events (push) Has been skipped
Rust tests / 🐧 [m], markdown (push) Has been skipped
Rust tests / 🐧 [m], no-encryption (push) Has been skipped
Rust tests / 🐧 [m], no-encryption-and-sqlite (push) Has been skipped
Rust tests / 🐧 [m], no-sqlite (push) Has been skipped
Rust tests / 🐧 [m], search (push) Has been skipped
Rust tests / 🐧 [m], socks (push) Has been skipped
Rust tests / 🐧 [m], sqlite-cryptostore (push) Has been skipped
Rust tests / 🐧 [m], sso-login (push) Has been skipped
Rust tests / 🐧 [m]-examples (push) Has been skipped
Rust tests / 🐧 [m]-crypto (push) Has been skipped
Rust tests / 🕸️ [m]-indexeddb (push) Has been skipped
Rust tests / 🕸️ [m]-base (push) Has been skipped
Rust tests / 🕸️ [m]-common (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores, no crypto (push) Has been skipped
Rust tests / 🕸️ [m], no-default (push) Has been skipped
Rust tests / 🕸️ [m]-qrcode (push) Has been skipped
Rust tests / 🕸️ [m]-ui (push) Has been skipped
Rust tests / Lint (push) Has been skipped
Rust tests / 🐧 all crates, 🦀 beta (push) Failing after 1m4s
Rust tests / 🐧 all crates, 🦀 stable (push) Failing after 31s
Rust tests / Spell Check with Typos (push) Failing after 1m43s
Rust tests / Integration test (features: default) (push) Failing after 1m20s
Rust tests / Integration test (features: experimental-encrypted-state-events) (push) Failing after 36s
Code Coverage / xtask (push) Failing after 29s
Code Coverage / Code Coverage (push) Has been skipped
Documentation / All crates (push) Failing after 2m1s
Rust version / msrv (push) Failing after 40s
Lint GHA workflows with zizmor / Run zizmor (push) Failing after 9m53s
Bindings tests / Test UniFFI bindings generation (push) Has been skipped
Bindings tests / matrix-rust-components-kotlin (push) Has been skipped
Bindings tests / Generate Crypto FFI Apple XCFramework (push) Has been cancelled
Rust tests / 🍏 all crates, 🦀 stable (push) Has been cancelled
Bindings tests / matrix-rust-components-swift (push) Has been cancelled
2026-04-23 11:25:24 +02:00
Jorge Martín 2c2e0f1b15 doc: Add doc comments and changelog entries 2026-04-22 17:26:51 +02:00
Jorge Martín f314578df5 feat(ffi): Add Client::get_dm_rooms
Expose the new function in the FFI layer
2026-04-22 17:26:51 +02:00
Jorge Martín 739289cd82 feat(ui): Add Client::get_dm_rooms
This function returns an iterator with all the available rooms that match the behaviour previously in `Client::get_dm`. `Client::get_dm` now just calls this function and returns the first item.
2026-04-22 17:26:51 +02:00
Kévin Commaille 1d7d6c943b feat(sdk): Add an expiry to the cached homeserver capabilities
I believe that it is a footgun to cache the data indefinitely by default
so this copies the same behavior as for the supported versions in the
ClientCaches: it sets an expiry duration of 1 day and refreshes the data
in the background when it has expired.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 14:21:34 +03:00
Damir Jelić 917f9ee298 chore: Bump vodozemac 2026-04-22 12:30:38 +02:00
Andy Balaam c7d44ddad3 Change in to into
Signed-off-by: Andy Balaam <mail@artificialworlds.net>
2026-04-22 10:52:25 +01:00
Kévin Commaille 1a170ddf90 fix(common): The expiry duration of TtlValue is 1 day
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille 696d1cacbe refactor(common): Don't check expiry for TtlValue::into_data()
This API is no longer in use so we can remove it. We also rename
`into_data_unchecked()` to `into_data()`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille c6d1cf20b6 feat(sdk): Refresh well-known cache in the background
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Jonas Platte 38f34e66eb Collapse an else { if ... }
as suggested by clippy.
2026-04-22 08:52:04 +02:00
Jonas Platte 8fa411aaa0 Clean up a lint 2026-04-22 08:52:04 +02:00
Jonas Platte b4b3b5258c Clean up some imports 2026-04-22 08:52:04 +02:00
Andy Balaam fd6cb6647f Support stable prefix for MSC4287 m.key_backup 2026-04-21 14:17:26 +01:00
Andy Balaam 103ff7d3ec Use stable identifier for m.shared_history 2026-04-21 13:47:19 +01:00
Andy Balaam efa28a1ffd Use stable identifiers for m.room_key_bundle and m.history_not_shared 2026-04-21 13:47:19 +01:00
Andy Balaam c4640897d4 Test for serialization and deserialization of a room key bundle 2026-04-21 13:47:19 +01:00
Benjamin Bouvier 0487a143ed test(integration): workaround non-working subscription to make the test pass in CI 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 7ab97d59b8 test(integration): add an integration test for computing latest events in a few rooms 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 940862ebed chore: bump the integration testing synapse version to v1.151.0 2026-04-21 14:43:55 +02:00
Jorge Martín e507eaabf6 feat(ffi): Expose ffi::NotificationRoomInfo::service_members
This is needed in some clients to know if a direct room is a DM or not
2026-04-21 13:22:58 +02:00
Jorge Martín a2bbc33920 doc(ffi): Add changelog entry 2026-04-21 10:38:14 +01:00
Jorge Martín db787c28b8 refactor(sdk-crypto): Fix several issues related to the newly enabled experimental-push-secrets feature 2026-04-21 10:38:14 +01:00
Jorge Martín a3d6114ddd refactor(ffi): Use the shared default features for the Kotlin bindings
This should align them with iOS.
2026-04-21 10:38:14 +01:00
Jorge Martín 58a139bf96 feat(ffi): Enable experimental-push-secrets feature by default 2026-04-21 10:38:14 +01:00
Kévin Commaille 012bc03014 refactor: Use TaskMonitor::spawn_finite_task
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille 38adf952bd chore: Fix typo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille 99d675d85a feat(sdk): Refresh supported versions cache in the background
The previous behavior was making the data unavailable as soon as it is
expired, forcing the user to make a request to access fresh data. This
change keeps the data after it is expired, allowing the function to
return quickly, and triggers a background task to refresh it so that
later calls will be able to use the updated data.

This is a step towards #6090. While it's not clear in the issue what
should be the trigger for the background refreshes, this implements at
least the background refresh while keeping the expiry time as a trigger.

The next steps are to apply this to other cached values and shorten the
expiry time to something like 1 day, which should be more helpful.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille be5eebaa35 refactor(base): Move and rename TtlStoreValue
Its is now available as `matrix_sdk_common::ttl_cache::TtlValue`,
allowing to use it outside of a store.

It also supports deserializing data without a timestamp, to allow to
add an expiration time to data that was already persisted.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Benjamin Bouvier cf4d3cb492 doc(ffi): clarify when to call Client::enable_automatic_backpagination
Since the boolean is read only once when subscribing the event cache,
let's make it clear when the FFI function should be called (based on
most prominent types used at the FFI layer).
2026-04-21 10:14:52 +02:00
dependabot[bot] ad5241b2e5 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from e82b6af0d3610e62ddf49bf7c62e662d7e31db0d to ac30a525c0a8d163a92d727b3ff079ee3f6ecb08.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/e82b6af0d3610e62ddf49bf7c62e662d7e31db0d...e82b6af0d3610e62ddf49bf7c62e662d7e31db0d)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: e82b6af0d3610e62ddf49bf7c62e662d7e31db0d
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:13:59 +02:00
dependabot[bot] d083c56ccb chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/671740ac38dd9b0130fbe1cec585b89eea48d3de...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:02:08 +02:00
dependabot[bot] eb493de1b8 chore(deps): bump actions/cache from 5.0.4 to 5.0.5
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:01:44 +02:00
dependabot[bot] 6aa4fdaed4 chore(deps): bump actions/upload-pages-artifact from 4.0.0 to 5.0.0
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/7b1f4a764d45c48632c6b24a0339c27f5614fb0b...fc324d3547104276b827a68afc52ff2a11cc49c9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:59:22 +02:00
dependabot[bot] 9c6bbd98a7 chore(deps): bump taiki-e/install-action from 2.68.26 to 2.75.10
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.26 to 2.75.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.68.26...85b24a67ef0c632dfefad70b9d5ce8fddb040754)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.10
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:47:28 +02:00
Benjamin Bouvier 8b14a8920c doc(sdk): add a module doc code example for search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 98b36fe5e0 chore: update changelogs 2026-04-20 12:18:52 +02:00
Benjamin Bouvier a1f32d741f refactor(ffi): remove unused struct 2026-04-20 12:18:52 +02:00
Benjamin Bouvier ce01854078 refactor(sdk): Room::search_messages and Client::search_messages 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 7f8c9b0244 refactor(search): pass the number of results per batch in the ctors 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 3621acb445 refactor(sdk): move the high-level search helpers back to the SDK crate 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 3789374951 refactor(sdk): move search related functions to a message_search module 2026-04-20 12:18:52 +02:00
Benjamin Bouvier e54a36a1ca refactor(search): simplify global search more by avoiding is_done tracking 2026-04-20 12:18:52 +02:00
Benjamin Bouvier cf95c3f4cc refactor(search): avoid using a HashMap for linear iteration 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 6ccfc92139 refactor(search): simplify code around global search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 4ee20befa3 chore: address first batch of review comments 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 4348d9de23 chore: add changelog entries for search! 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 477ee70e1c feat(ffi): bindings for search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 83989c030a feat(ffi): allow enabling search with a search index store 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 717b6371f8 feat(multiverse): add support for global search (ctrl+g) 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 6fa832cfc8 feat(search): higher-level Search API 2026-04-20 12:18:52 +02:00
Ivan Enderlin 1bff8cbe5d chore(ffi): Do not log errors in ClientError::from_str.
This patch stops logging errors in `ClientError::from_str` as it was an
artifact of some old debugging sessions.
2026-04-20 11:33:15 +02:00
Ivan Enderlin 9bb67e8422 fix(ffi): Use the ERROR log level for errors.
This patch changes a `warn!` to an `error!` in `ClientError`. There are
errors, let's use the proper log level here.
2026-04-20 11:33:15 +02:00
Damir Jelić c962a52f73 ci: Add a zizmor action 2026-04-20 09:49:55 +02:00
Damir Jelić 4d20b481c1 ci: Upload the coverage directly as part of the coverage workflow 2026-04-20 09:49:55 +02:00
Damir Jelić a5136c7384 ci: Add a zizmor config and use correct hashes for our github actions 2026-04-20 09:49:55 +02:00
Andy Balaam 0e120549ce Remove unneeded TODO in RoomKeyBundleContent
Quoting @poljar:

> If you follow the stack till you get where the secret key is, you end up here:
> https://github.com/ruma/ruma/blob/6282fd8838fd2c84f252f85973d8a43577ced91a/crates/ruma-events/src/room.rs#L269-L273
> And that is now zeroized.
> So yeah, the TODO was resolved in Ruma.
2026-04-17 16:18:36 +02:00
Damir Jelić 1e74c48351 fix(thread): Only apply valid edits in the thread summary 2026-04-17 12:34:47 +02:00
Damir Jelić 9e242abda9 test(latest-event): Check that we don't apply invalid edits in the latest event 2026-04-17 12:34:47 +02:00
Damir Jelić 62ff895ce8 fix(latest-event): Don't apply invalid edits on the latest event 2026-04-17 12:34:47 +02:00
Damir Jelić d0d22ece78 refactor(latest-event): Ensure we have access to the event JSON when deciding on a latest event 2026-04-17 12:34:47 +02:00
Damir Jelić d49d86e8b0 fix(ui): Follow the spec more closely in how we reject invalid edits 2026-04-17 12:34:47 +02:00
Damir Jelić b226191bf4 test(common): Add some tests to validate the edit validation logic 2026-04-17 12:34:47 +02:00
Damir Jelić 21d7cd1b9d feat(common): Add a function to validate edits 2026-04-17 12:34:47 +02:00
Ivan Enderlin b066260554 doc(ui): Add #6459 in the `CHANGELOG.md. 2026-04-17 11:02:21 +02:00
Ivan Enderlin dbc6e3c3aa fix(ui): Fix a possible panic in Switch by updating async-rx.
This patch updates `async-rx` to fix a possible panic in `Switch`. It
now requires the outer stream to implement `FusedStream` (for the moment).
2026-04-17 11:02:21 +02:00
Benoit Marty 3293c94451 feat(ffi): Expose Client.request_openid_token() in order to let sdk clients request an openId token 2026-04-17 08:29:43 +00:00
Andy Balaam 53385798f6 Changelog update for #6457 2026-04-17 09:15:58 +01:00
Andy Balaam d49736455b Discard room key if someone leaves a room for a non-leave reason e.g. ban 2026-04-17 09:15:58 +01:00
Andy Balaam e278a99a20 Factor out parts of test_history_share_on_invite_room_key_rotation 2026-04-17 09:15:58 +01:00
Andy Balaam d5c7c886a1 Add a note helping to understand that an existing test is sufficient to cover e.g. Ban events as well as Leave 2026-04-17 09:15:58 +01:00
Ivan Enderlin b9b40c0ef2 doc(sdk): Add #6453 in the `CHANGELOG.md. 2026-04-15 17:49:46 +02:00
Ivan Enderlin 99d60bcc70 fix(sdk): Fix an infinite loop when loading pinned events from the storage.
This patch fixes a critical infinite loop when loading pinned events
from the storage if and only if there is more than one chunk for the
pinned events.

The previous implementation was broken in two ways:

1. if there was no previous chunk, then `prev` was set to `None`, but
   `previous` was never updated, so it was loop forever,
2. `load_previous_chunk` was called with the `last_chunk.previous` chunk
   identifier, which is incorrect: it must be called with the last chunk
   identifier, not the previous': otherwise it skips one chunk every time.

This patch adds a test to ensure everything works as expected.
2026-04-15 17:49:46 +02:00
Damir Jelić 64d602141a chore: Bump rustls and rustls-webpki
There appears to be a security issue.
2026-04-15 15:04:02 +02:00
Floriane TUERNAL SABOTINOV 59d2f8d4af refactor(examples): update get-profiles example to use Account::fetch_user_profile
Fixes #5902

## What this PR does

Keeps the existing `Client::send()` demo but extends it with:

- A doc comment on `get_profile` explaining the spec behaviour and the 401 
condition on  (Synapse's `require_auth_for_profile_requests`)
- A `get_profile_authenticated()` fallback using `account().fetch_user_profile()` 
which internally uses `force_auth()`
2026-04-15 13:00:41 +00:00
Kévin Commaille 80a253ada9 test(sdk): Add tests for profile capabilities
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-15 13:14:27 +02:00
Kévin Commaille ec5082bb9c fix(client): Fix logic for profile fields capabilities
The current implementation assumes that if the `m.profile_fields`
capability is missing, it means that the capability is not enabled, but
this is not what the spec says. However the spec says that it depends on
the Matrix versions advertised by the homeserver. So this adds a method
that properly computes the capability depending on the supported
versions too.

Similarly, for the display name and avatar URL fields the implementation
assumes that if the `m.profile_fields` capability is present but
disabled, we should fallback to the legacy capabilities. This behavior
is not present in the spec, so the code is changed to always use the new
capability when it is present, whether it is enabled or not. The
legacy capabilities are only used if the new capability is missing and
the homeserver doesn't advertise support for extended profile fields.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-15 13:14:27 +02:00
Johannes Marbach 63f5be6935 feat(sync): Allow setting a custom Sliding Sync connection ID and timeline limit on RoomListService
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-04-15 11:06:57 +02:00
dependabot[bot] dab87b7731 chore(deps): bump CodSpeedHQ/action from 4.12.1 to 4.13.0
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.12.1 to 4.13.0.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/1c8ae4843586d3ba879736b7f6b7b0c990757fab...db35df748deb45fdef0960669f57d627c1956c30)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 16:03:20 +02:00
dependabot[bot] 63f33a081f chore(deps): bump taiki-e/install-action from 2.68.26 to 2.74.0
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.26 to 2.74.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.68.26...v2.74.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.74.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 15:42:51 +02:00
Damir Jelić 8b762b04df ci: Install cargo-llvm-cov explicitly for the coverage action as well 2026-04-14 15:05:28 +02:00
Ivan Enderlin a9ef675b1c chore(sdk): Move `insert_sent_event_from_send_queue. 2026-04-14 14:45:14 +02:00
Ivan Enderlin e69bb96144 test(integration): Fix the test_local_echo_to_send_event_has_encryption_info test.
This test forgot to subscribe to the Event Cache, hence the result
was partially okay. It was working by luck before. With the previous
commits, it was not possible to work without the Event Cache being
listening to the sync.
2026-04-14 14:45:14 +02:00
Ivan Enderlin 63b8fc8b47 test: Adjust tests according to previous patches.
This patch adjust tests since the Event Cache ignores events from the
Send Queue if the cache is empty. It mostly impacts the tests as this
scenario is pretty rare in real world use cases.
2026-04-14 14:45:14 +02:00
Ivan Enderlin d68879d0aa test(sdk): Test Send Queue inserts in the Event Cache as expected. 2026-04-14 14:45:14 +02:00
Ivan Enderlin 1afebd6d4e fix(sdk): Events from the Send Queue are inserted in the Event Cache if and only if it is no empty.
This patch fixes a bug where inserting an event from the Send Queue in
an empty Event Cache will break the back-pagination logic. Indeed, the
detection of the start of the timeline during the back-pagination is
conditioned to the emptiness of the cache:

- if there is no gap, and if the cache is not empty, then we consider
  the start of the timeline has been reached.

However, if an event from the Send Queue has been inserted, with no
previous batch token (because none can be computed at this step), no gap
is present and the cache won't be empty, so… this is wrongly assumed to
be the start of the timeline.

The solution to this problem is to insert the event from the Send Queue
if the cache is not empty.
2026-04-14 14:45:14 +02:00
Damir Jelić db4c1f8dbd ci: Explicitly state that we want cargo-hack installed 2026-04-14 14:44:58 +02:00
dependabot[bot] c1b89a2751 chore(deps): bump crate-ci/typos from 1.44.0 to 1.45.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.44.0 to 1.45.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/631208b7aac2daa8b707f55e7331f9112b0e062d...cf5f1c29a8ac336af8568821ec41919923b05a83)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 14:43:37 +02:00
Damir Jelić 9d39e4bae8 ci: Explicitly state which tool we want to install, namely nextest 2026-04-14 13:55:26 +02:00
Ivan Enderlin 936f173e6f doc(sdk): Add #6396 in the CHANGELOG.md. 2026-04-14 12:51:02 +02:00
Ivan Enderlin 9eb4a5a24c test(sdk): Add a test to ensure a redacted latest event is removed. 2026-04-14 12:51:02 +02:00
Ivan Enderlin 49bba4d471 test(sdk): A TODO has been resolved. 2026-04-14 12:51:02 +02:00
Ivan Enderlin f23deacec4 fix(sdk): Do not update the LatestEventValue if its event ID matches the previous value.
This patch updates the logic to update a `LatestEventValue`. We still
can't compare two `LatestEventValue` because the type doesn't implement
`PartialEq`. However, we can use the `EventId` in some case.

It fixes https://github.com/matrix-org/matrix-rust-sdk/issues/6381.
2026-04-14 12:51:02 +02:00
Jorge Martín 9c28ee7041 feat(ffi): Add ffi::Client::set_avatar_url
This method uses `Account::set_avatar_url` under the hood to update the user's avatar to the provided MXC url
2026-04-14 12:08:47 +02:00
dependabot[bot] 5d420b683f chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from b81ce1560c5fbd0210cb66d88bf210329ff04266 to e82b6af0d3610e62ddf49bf7c62e662d7e31db0d.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/b81ce1560c5fbd0210cb66d88bf210329ff04266...e82b6af0d3610e62ddf49bf7c62e662d7e31db0d)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: e82b6af0d3610e62ddf49bf7c62e662d7e31db0d
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 11:12:08 +02:00
dependabot[bot] 2d5b95e45e chore(deps): bump rand from 0.10.0 to 0.10.1
Bumps [rand](https://github.com/rust-random/rand) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 09:49:29 +02:00
Benjamin Bouvier 3d29db5ebb refactor(tests): simplify timeline/reactions tests to remove custom respond_with 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 8e041d6958 refactor(tests): use MatrixMockServer even more in room_list_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier c02d0f1d01 refactor(tests): make more use of the MatrixMockServer in ui tests 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 2da262e56c refactor(tests): make use of MatrixMockServer in other room_list_service tests 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 31e4657718 refactor(tests): make use of MatrixMockServer in room_list_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 5b2fe3cf47 refactor(tests): make use of MatrixMockServer in notification_client 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 49d7d4528d refactor(tests): make use of MatrixMockServer in sync_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 3d7e505530 refactor(tests): make use of MatrixMockServer in encryption_sync_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 56d4163982 refactor(tests): make use of MatrixMockServer in timeline/pagination 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 4cafc48cc4 refactor(tests): make use of MatrixMockServer in timeline/focus_event 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 8ea55b30bc refactor(tests): make use of MatrixMockServer in timeline/sliding_sync
We could do better, and have even a specialized sliding sync endpoint
helper, but that'll be for another day :)
2026-04-13 17:20:57 +02:00
Benjamin Bouvier d438ee2ca0 refactor(tests): add a test helper to send with a delay, use it in timeline/queue 2026-04-13 17:20:57 +02:00
Benjamin Bouvier a3fe9eef12 refactor(tests): make use of MatrixMockServer in timeline/queue 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 60056ba205 refactor(tests): make use of MatrixMockServer in timeline/subscribe 2026-04-13 17:20:57 +02:00
Benjamin Bouvier ff62ed667c refactor(tests): make use of MatrixMockServer in timeline/pinned_event 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 4168362912 refactor(tests): make use of MatrixMockServer in the timeline/profiles tests 2026-04-13 17:20:57 +02:00
ganfra 96d63b7c8d doc: update changelogs 2026-04-13 16:57:57 +02:00
ganfra 260eaeea2b feat (live location): rewrite live location shares subscription logic 2026-04-13 16:57:57 +02:00
ganfra f7442fa279 fix(live location): include BeaconInfo in required state 2026-04-13 16:57:57 +02:00
ganfra da3cf3d54b doc: update changelog 2026-04-13 16:57:06 +02:00
ganfra 872861e90e Handle beacon stop aggregation in latest event
Support BeaconStop aggregation to properly display stopped
live location sharing in the latest event timeline content.
2026-04-13 16:57:06 +02:00
Hubert Chathi 36cca4e444 add comment 2026-04-10 14:52:39 +01:00
Hubert Chathi bb6202c76b fix lint 2026-04-10 14:52:39 +01:00
Hubert Chathi 5458a5afd2 add changelog 2026-04-10 14:52:39 +01:00
Hubert Chathi 45fa860b6b add secret pushing feature to FFI 2026-04-10 14:52:39 +01:00
Hubert Chathi 4ad25e32af handle the backup key being pushed from other devices 2026-04-10 14:52:39 +01:00
Hubert Chathi 423ebf9502 push the backup key to our other devices when we create a new backup 2026-04-10 14:52:39 +01:00
Damir Jelić 7fbb8cf9e5 fix(spaces): Ensure space children ordering is transitive
This patch fixes the SpaceRoom::compare_rooms method to be transitive
(If A ≤ B and B ≤ C then A ≤ C). The transitive property of a comparison
function is required by the sorting functions we are using.

If the property doesn't hold, sorting will panic.

The previous logic violated strict weak ordering by falling back towards
room ID comparison as soon as one of the values doesn't have a
SpaceRoomChildState.

This introduced inconsistent ordering paths where:
    - A and B would be compared using the SpaceRoomChildState
    - B and C would be compared using only the room ID
    - A and C would be compared using only the room ID

Leading to the case where:
    - A < B due to the state
    - B < C due to the room ID
    - and finally A > C due to the room ID.

As a result, transitivity could be broken (A < B, B < C, but C < A),
leading to a panic during the sorting.
2026-04-10 15:49:26 +02:00
Damir Jelić e0cac4eb7a test(spaces): Check that space children ordering is total 2026-04-10 15:49:26 +02:00
Damir Jelić aa9cc67f32 refactor(spaces): Make the space children comparison function simpler 2026-04-10 15:49:26 +02:00
Damir Jelić 7d6569ff27 test(spaces): Test that the top-level space ordering is total 2026-04-10 15:49:26 +02:00
Damir Jelić 4d7b19999b refactor(space): Move the top-level space comparison logic into a separate function 2026-04-10 15:49:26 +02:00
Daniel Salinas d47306045f Add cfg around inadvertent use of sqlite in matrix-sdk-ffi crate
Several new functions implicitly rely on the sqlite config.
sqlite might not be present if you are using the indexedb store, in which case
these functions are likely irrelevant.

This change conditionalizes their compilation.
2026-04-10 14:11:36 +02:00
Philippe Bertin ba461598b2 feat(ffi): expose latest json
Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-04-09 15:57:29 +02:00
Stanislav Skobelkin 0233d03d4e fix(ffi): Export Eq and Hash traits in enums used as HashMap keys
matrix_sdk_ffi::ruma::TagName and matrix_sdk_ffi::event::TimelineEventType enums
that are used as HashMap keys by matrix_sdk_ffi::ruma::Tag and
matrix_sdk_ffi::room::power_levels::RoomPowerLevels respectively should probably
export Eq and Hash traits, so that we can generate bindings for languages that
implement uniffi's record/HashMap type using a hash table (e.g.
std::unordered_map in C++)

Signed-off-by: Stanislav Skobelkin <stanislav@skobelk.in>
2026-04-09 12:12:57 +02:00
Damir Jelić 725f74cbe8 fix(ffi): Correctly return that the backup is complete if the backup key is valid 2026-04-09 12:01:04 +02:00
Benjamin Bouvier 98aceb40c2 refactor(timeline): make sure all the timeline background tasks are aborted on drop
Otherwise, we might run into leaks!
2026-04-08 16:19:09 +02:00
Damir Jelić 34a35ad32a feat(ffi): Allow passing in a SecretsBundle after logging in (#6212)
This allows people to get a secrets bundle out of band or out of a database and import it
after logging in a new client.

Mainly targeted to support the Element Classic -> Element X migration.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Benoit Marty <benoitm@matrix.org>
2026-04-08 14:07:57 +00:00
Benjamin Bouvier 99971a2347 chore(sdk): add a changelog entry for the task monitor 2026-04-08 15:14:44 +02:00
Benjamin Bouvier b5f5a9c90e refactor(sdk): rename functions to spawn background tasks 2026-04-08 15:14:44 +02:00
Benjamin Bouvier ec4e228620 feat(sdk): use the new method in the context of two background jobs 2026-04-08 15:14:44 +02:00
Benjamin Bouvier a2c254d7da feat(sdk): add a way to spawn one-off jobs 2026-04-08 15:14:44 +02:00
Kevin Boos 557c0698ba Spaces: add suggested field to SpaceRoom (#6417)
Previously there wasn't a way to determine whether a given sub-space or
room within a given space had been marked as "suggested".
This was one of the things missing in support for spaces (#227) even
though it wasn't explicitly mentioned.
   
I tested that this fix does work properly in
[Robrix](https://github.com/project-robius/robrix).

Signed-off-by: Kevin Boos kevinaboos@gmail.com
2026-04-08 13:58:56 +02:00
Bryant Mairs 99a5568dbf feat: add missing APIs to HomeserverCapabilities
Add `m.room_versions` & `m.account_moderation` that were missed in the
first implementation.

Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-04-08 13:48:20 +02:00
Jonas Platte 94d8674709 refactor(ffi): Deduplicate thumbnail conversion code 2026-04-08 07:58:36 +02:00
Jonas Platte dfc4e03d5b refactor(ffi): Replace nested match with if-let chain 2026-04-08 07:58:36 +02:00
Jonas Platte 14b8248e4b refactor(ffi): Extract closure as fn to reduce indentation 2026-04-08 07:58:36 +02:00
Jonas Platte 447c89aed4 refactor(ffi): Merge separate impl blocks 2026-04-08 07:58:36 +02:00
Jonas Platte 894f94c383 Finish Rust Edition 2024 migration 2026-04-08 07:56:03 +02:00
Johannes Marbach 1f3dea778b feat(send_queue): send redactions via the send queue (#6250)
This is a first step towards
https://github.com/matrix-org/matrix-rust-sdk/issues/4162 and adds a way
to send redactions (including their local echoes) via the send queue.

I had to introduce new variants for `SentRequestKey` and
`LocalEchoContent` because in some room versions the redacted event ID
sits at the top-level of the event rather than in `content`.

At the timeline level redactions are handled via a new boolean flag in
`AggregationKind::Redaction`. Local echoes of redactions merely set a
flag on the timeline event whereas remote echoes of redactions lead to
actual redactions as before.

The FFI bindings will be updated in a follow-up PR.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-04-07 18:02:59 +02:00
Damir Jelić b07069170f Merge pull request #6409 from mgoldenberg/remove-native-tls
Remove support for `native-tls`
2026-04-07 16:03:09 +02:00
Michael Goldenberg f685feed40 doc(sdk): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:45:29 -04:00
Michael Goldenberg 610d4ca4e5 doc(ui): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:45:27 -04:00
Michael Goldenberg 41edb08283 doc(ffi): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:34 -04:00
Michael Goldenberg 44aad14732 doc: update references to tls features
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:34 -04:00
Michael Goldenberg b1fbf31c2e feat(sdk): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 2bcab72dbb ci: remove reference to matrix-sdk/rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 3bac5d250d refactor(benchmarks): remove reference to matrix-sdk/rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 4ec75b5b5c feat(ui): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg c8c9a94995 feat(ffi): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 08b80c533a feat(sdk): enforce use of rustls + remove extraneous build checks
Note that the rustls-tls feature flag is temporarily left in place
to keep other crates from breaking, but it becomes a trivial flag.
It will be removed altogether in a later commit.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 0e679904e3 feat(sdk): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 89c50c7ecd feat(ffi): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg d2857ee20a feat(ui): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Hubert Chathi c1274cea14 Add support for pushing secrets and receiving secret pushes (#6164)
see MSC4385

Pushing secrets allow devices to send secrets to other devices without waiting for a secret request.
2026-04-07 12:26:17 +01:00
Jonas Platte 12cd1effdc refactor(ffi): Fix new clippy lints 2026-04-07 12:39:12 +02:00
Jonas Platte 3a14b73258 chore(ffi): Rerun cargo fmt 2026-04-07 12:39:12 +02:00
Jonas Platte d97f4ab8b3 chore(ffi): Upgrade to Rust edition 2024 2026-04-07 12:39:12 +02:00
Bryant Mairs 3539487c49 feat: expose call_intent for m.rtc.notification
This allows for notifications to use the intent for deciding how to
render the message (e.g. whether to call it a "call" or a "video call").

Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-04-07 10:26:26 +02:00
dependabot[bot] 526f35e3d3 chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/671740ac38dd9b0130fbe1cec585b89eea48d3de...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:18:38 +02:00
dependabot[bot] 6b4e2655cc chore(deps): bump actions/deploy-pages from 4.0.5 to 5.0.0
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4.0.5 to 5.0.0.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e...cd2ce8fcbc39b97be8ca5fce6e763baed58fa128)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:18:11 +02:00
Kévin Commaille 30de5372eb Upgrade Ruma after api::Error breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-07 10:17:42 +02:00
Kévin Commaille ba451b67c9 Upgrade Ruma after RoomAliases removal
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-07 10:17:42 +02:00
Benjamin Bouvier a65fec3175 refactor(event cache): use a const array for receipt types 2026-04-02 14:29:09 +02:00
Benjamin Bouvier 6298b5c176 feat(event cache): include read receipts that were dormant in the state store 2026-04-02 14:29:09 +02:00
Benjamin Bouvier 0283ae14d9 doc(base): clarify comments around read receipts related store methods 2026-04-02 14:29:09 +02:00
Benjamin Bouvier a1157d2c38 refactor(event cache): clarify warning when a pinned event couldn't be loaded
The previous warning could trigger with abnormal numbers, since there
could be related events applying to a pinned event, the `loaded_events`
list could be bigger than the list of `pinned_event_ids`, due to the
eager flattening. This patch makes it display the correct number, by
postponing the flattening after the warning.
2026-04-02 12:39:18 +02:00
Valere Fedronic fcce02fc11 feat(widget): Support for avatars in calls via msc4039 (#6354)
Allows to get the avatars in Element Call widget !
Using
[MSC4039](https://github.com/nordeck/matrix-spec-proposals/blob/nic/feat/widgetapi-upload-files/proposals/4039-widget-api-media.md)

Counter part of web support:
- https://github.com/element-hq/element-web/pull/32755
- https://github.com/element-hq/element-call/pull/3780

Needs a EC PR to support passing data as base64 via the widget json API
- https://github.com/element-hq/element-call/pull/3818
2026-04-02 13:35:22 +03:00
Benjamin Bouvier 7cea5be9e2 refactor(timeline): allow creating a TimelineItemContent from a sdk::TimelineEvent 2026-04-02 12:34:01 +02:00
Bertin Philippe 32e9626e0f feat(ffi): expose event types and content (#6387)
Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-04-02 09:14:57 +02:00
Jonas Platte e376670af3 refactor(xtask): Fix clippy lints 2026-04-02 09:10:51 +02:00
Jonas Platte 7f08793360 chore(base): Delete outdated comments 2026-04-02 09:09:55 +02:00
Benjamin Bouvier 1fc7b34016 test: add an integration test reflecting the "bouncing" behavior of the timeline after sending an event 2026-04-01 14:59:33 +02:00
Benjamin Bouvier 37447e16e5 refactor(sdk): use the TimelineEvent::sender() helper in more places
This new helper has been added recently, and it can be reused in many
more places which were doing the same thing more verbously in many
places.
2026-04-01 14:59:13 +02:00
Benjamin Bouvier 782355b556 fix(timeline): fix timeline initialization races
For non-live timelines, the recurring pattern was the following:

- first, fill the timeline with initial events, with a first call to
some form of subscribe() method,
- then, subscribe to updates in a second time later, using a second call
to the same subscribe() method.

Unfortunately, this is wrong, and opens a window for a very small race:

- first call to subscribe() to get the initial events
- new updates are emitted in the receiver, that lead to new events
- second call to subscribe() to get the receiver, and subscribe to it

In this case, the new updates would be lost by an observer.

The patch consists in refactoring timeline initialization, such that the
initial events and receiver are created at the same time; if there are
some updates happening after the now single subscribe() call, the
updates are accumulated and will be observable by external users.

This also has the nice benefit of tidying up the number of background
task handles for non-live timelines: there should be at most one such
focus task, which is now cleanly reflected in the timeline drop handles.
2026-04-01 14:44:43 +02:00
Damir Jelić eb51c862ce chore: Add missing changelog entries for changes caused by the rand bump 2026-04-01 12:48:09 +02:00
Kévin Commaille 676c81ca80 Upgrade Ruma to latest commit
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 644c5e8a4c Upgrade Ruma to commit after PushCondition breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 34d5e6da2a Upgrade Ruma to commit after UserIdentifier breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 5936a7285f Upgrade Ruma to commit after push Action breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Mauro 62d58a21ab fix: include also stopped live location events in latest_event (#6373)
The issue is that when the most recent message was an live location event that started as live and later transitioned to a stopped state, the room list summary would ignore it entirely, even though the timeline still showed it as the latest item.

That behaviour feels incorrect. At most, the client should update the displayed description based on whether isLive is true or not.

This patch prevents the stopped live location events from being filtered out in the latest events.
2026-04-01 10:56:56 +02:00
Benjamin Bouvier 2e334fafa2 refactor(multiverse): reuse load_or_fetch_event instead of implementing it manually 2026-04-01 10:43:01 +02:00
Benjamin Bouvier e66967c46f refactor(multiverse): simplify code around search 2026-04-01 10:43:01 +02:00
Benjamin Bouvier c237659aca feat(multiverse): don't display "no results found" if the search input is empty 2026-04-01 10:43:01 +02:00
Benjamin Bouvier 8cb6f74996 chore: replace incorrect trace message in search indexing task 2026-04-01 10:43:01 +02:00
Doug 0afd7c9528 fix: Update the iOS platform version to match the deployment target. 2026-04-01 11:12:18 +03:00
Benjamin Bouvier 388ced09a6 Merge pull request #6344 from matrix-org/bnjbvr/automatic-backpagination-unread-counts
feat(event cache): automatic back-pagination for unread counts, MVP
2026-04-01 09:26:55 +02:00
Doug 4071a55cd7 chore: Use a single Swift module when building the bindings.
This fixes support for Xcode 26.4.
2026-04-01 09:20:38 +02:00
Michael Goldenberg c04a97f706 feat(common): ensure cross-process lock generation is opaque
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 5b0c6bbafc doc: update change logs
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 1fb0f7f56a refactor(indexeddb): remove old crypto store generation key-value
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg ee9a05defe refactor(sqlite): remove old crypto store generation key-value
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg c0f746f88f refactor(crypto): remove old store generation logic
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 839786f810 refactor(sdk): generalize client encryption store lock fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 4e4b508c38 refactor(sdk): use consistent errors for client encryption store locks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 313e634996 refactor(sdk): clean up errors in client encryption
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg ae72fcf663 refactor(sdk): rely on dirtiness reported by crypto store's cross-process lock
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 12e9a2a159 feat(common): expose generation in cross-process lock
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Damir Jelić 16c1b9b57f chore: Bump rand 2026-03-31 16:57:50 +02:00
Benjamin Bouvier 06e6dd05c3 chore(docs): publicly expose the AutomaticPagination API object for external users 2026-03-31 16:36:13 +02:00
Benjamin Bouvier 1cf84e203e chore(event cache): make clippy pass on automatic pagination 2026-03-31 15:57:36 +02:00
Benjamin Bouvier 9913cce946 Merge branch 'main' into bnjbvr/automatic-backpagination-unread-counts 2026-03-31 15:55:44 +02:00
Benjamin Bouvier 1ee88b176c refactor(event cache): create a standalone AutomaticPagination API object 2026-03-31 15:46:46 +02:00
Daniel Salinas f0193cc8ac Fix indexedb migration to clean up gaps + chunks properly 2026-03-31 15:37:36 +02:00
Jorge Martín 5d2eab119d feat: Improve getting homeserver capabilities
This extracts the `/capabilities` logic to its own `HomeserverCapabilities` component in the SDK that can be manually asked to fetch, cache locally and return these capabilities.
2026-03-31 15:25:37 +02:00
Benjamin Bouvier ca91b6a278 fix(event cache): handle read receipt after a duplicate-only sync response 2026-03-31 12:52:18 +02:00
Benjamin Bouvier 260fec8750 test(event cache): regression test for lack of unread count updates after a duplicate-only sync response 2026-03-31 12:52:18 +02:00
Benjamin Bouvier 950fcd289d refactor(ffi): only start with a reset diff if the timeline isn't empty 2026-03-31 11:17:34 +02:00
Benjamin Bouvier bdd0162831 tests(event cache): make tests more resilient to the order of event cache updates 2026-03-30 11:40:19 +02:00
Benjamin Bouvier 752695c6ff chore: rename background request to automatic pagination request
And associated vocabulary and fields.
2026-03-30 11:28:05 +02:00
Benjamin Bouvier 552639763e chore: move the BackgroundRequest type, tests and task to its own file 2026-03-30 11:19:01 +02:00
Benjamin Bouvier d151340882 chore(review): use an unbounded sender for BackgroundRequest 2026-03-30 11:12:39 +02:00
Benjamin Bouvier 27415dc64d chore(review): Use assert_matches and remove PartialEq/Eq impls for EventsOrigin 2026-03-30 11:04:41 +02:00
Benjamin Bouvier 4feeaf0cf5 chore(review): address first review comments 2026-03-30 11:01:57 +02:00
Benjamin Bouvier 87a2f8fab7 feat(multiverse): enable automatic back pagination in multiverse 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 871cb2221b test(event cache): add an integration test for automatically paginating a room with missing receipts 2026-03-30 10:40:31 +02:00
Benjamin Bouvier d1d174d137 test(event cache): test the credit system of automatic pagination 2026-03-30 10:40:31 +02:00
Benjamin Bouvier bd7dca45ef feat(event cache): make some background pagination parameters configurable 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 856b2be992 test(event cache): unit test automatic background pagination 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 161750f6d0 refactor(event cache): add PartialEq to EventsOrigin 2026-03-30 10:40:31 +02:00
Benjamin Bouvier c4a95f9bbe feat(event cache): automatically request paginations when no receipt has been found when computing unread counts 2026-03-30 10:40:31 +02:00
Benjamin Bouvier ac5552807b refactor(event cache): have select_best_receipt return a result struct
This makes it simpler to test its expected behavior, and will make it
trivial to add a system to request automatic paginations in the
background.
2026-03-30 10:40:31 +02:00
Benjamin Bouvier ff8b27f99b feat(event cache): add an automatic background requests task 2026-03-30 10:40:31 +02:00
Benjamin Bouvier bf9ffd7d09 refactor(event cache): make EventCache::config/config_mut sync by using a sync lock
There wasn't good reason to use an async lock, as this lock is always
super short-lived, it can be sync, which avoids complications in the
subsequent commit when calling sync init code.
2026-03-30 10:40:31 +02:00
Benjamin Bouvier 9f18d76355 feat(event cache): add a feature toggle to enable automatic back-pagination 2026-03-30 10:40:30 +02:00
312 changed files with 12856 additions and 5278 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
- name: Run the benchmarks
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30
with:
run: cargo codspeed run
mode: simulation
+22 -14
View File
@@ -38,12 +38,14 @@ jobs:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -51,12 +53,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -101,7 +103,9 @@ jobs:
ndk-version: r27
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -109,12 +113,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -149,12 +153,14 @@ jobs:
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Install aarch64-apple-ios target
run: rustup target install aarch64-apple-ios
@@ -165,12 +171,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -206,12 +212,14 @@ jobs:
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Add rust targets
run: |
@@ -223,7 +231,7 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
+48 -29
View File
@@ -37,7 +37,6 @@ jobs:
- no-encryption-and-sqlite
- sqlite-cryptostore
- experimental-encrypted-state-events
- native-tls
- markdown
- socks
- sso-login
@@ -50,7 +49,9 @@ jobs:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Install libsqlite
run: |
@@ -58,7 +59,7 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -69,10 +70,12 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -94,18 +97,22 @@ jobs:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -132,20 +139,23 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -182,7 +192,7 @@ jobs:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
@@ -193,17 +203,19 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ matrix.rust }}
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Test
run: |
@@ -256,8 +268,9 @@ jobs:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: wasm32-unknown-unknown
components: clippy
@@ -268,7 +281,7 @@ jobs:
version: v0.13.1
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -279,10 +292,12 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -308,7 +323,7 @@ jobs:
persist-credentials: false
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
lint:
name: Lint
@@ -322,23 +337,23 @@ jobs:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: nightly-2026-02-26
components: clippy, rustfmt
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -388,15 +403,19 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Test
env:
+21 -38
View File
@@ -26,6 +26,9 @@ jobs:
name: Code Coverage
needs: xtask
runs-on: "ubuntu-latest"
permissions:
contents: read
id-token: write
# run several docker containers with the same networking stack so the hostname 'synapse'
# maps to the synapse container, etc.
@@ -112,7 +115,9 @@ jobs:
sudo rm -rf /var/lib/apt/lists/*
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -120,19 +125,18 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
prefix-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@8a8ecf49de4feac160d13668bc406fe17413c1bf # cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Install nextest and llvm-cov
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest,cargo-llvm-cov
- name: Get xtask
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -151,35 +155,14 @@ jobs:
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
- name: Get PR number and commit SHA
run: |
echo "Storing PR number ${{ github.event.number }}"
echo "${{ github.event.number }}" > pr_number.txt
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
- name: Move the JUnit file into the root directory
shell: bash
run: |
mv target/nextest/ci/junit.xml ./junit.xml
# This stores the coverage report and metadata in artifacts.
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
- name: Store coverage report in artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- name: Upload coverage to Codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: codecov_report
path: |
coverage.xml
junit.xml
pr_number.txt
commit_sha.txt
if-no-files-found: error
use_oidc: true
- run: |
echo 'The coverage report was stored in Github artifacts.'
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
use_oidc: true
report_type: "test_results"
+1 -1
View File
@@ -16,4 +16,4 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
@@ -13,4 +13,4 @@ jobs:
with:
persist-credentials: false
- name: Machete
uses: bnjbvr/cargo-machete@b81ce1560c5fbd0210cb66d88bf210329ff04266
uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08
+5 -5
View File
@@ -26,12 +26,12 @@ jobs:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: nightly-2026-02-26
@@ -41,7 +41,7 @@ jobs:
node-version: 20
- name: Load cache
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -54,11 +54,11 @@ jobs:
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: './target/doc/'
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
+3 -1
View File
@@ -16,5 +16,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: taiki-e/install-action@4146cc238a18f9b3f3427572fd4585d2d2d78c88 # cargo-hack
- uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # cargo-hack
with:
tool: cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets
-99
View File
@@ -1,99 +0,0 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
# of `coverage` workflow.
workflow_run:
workflows: ["Code Coverage"]
types:
- completed
permissions: {}
jobs:
coverage:
name: Upload coverage report
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
var fs = require('fs');
// List artifacts of the workflow run that triggered this workflow
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "codecov_report";
});
if (codecovReport.length != 1) {
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
}
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: codecovReport[0].id,
archive_format: 'zip',
});
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
- id: parse_previous_artifacts
run: |
unzip codecov_report.zip
echo "Detected PR is: $(<pr_number.txt)"
echo "Detected commit_sha is: $(<commit_sha.txt)"
# Make the params available as step output
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
persist-credentials: false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
+4 -2
View File
@@ -57,7 +57,7 @@ jobs:
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: xtask-cache
with:
path: target/debug/xtask
@@ -68,7 +68,9 @@ jobs:
- name: Install Rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
+24
View File
@@ -0,0 +1,24 @@
name: Lint GHA workflows with zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
+1 -1
View File
@@ -7,4 +7,4 @@ group_imports = "StdExternalCrate"
format_code_in_doc_comments = true
doc_comment_code_block_width = 80
# Workaround for https://github.com/rust-lang/rust.vim/issues/464
edition = "2021"
edition = "2024"
+3
View File
@@ -0,0 +1,3 @@
rules:
unpinned-uses:
severity: high
+15 -25
View File
@@ -81,10 +81,10 @@ contributors' pull requests and pushes.
## Pull requests
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
each commit should have a *good commit message*.
Ideally, a PR should have a _proper title_, with _atomic logical commits_, and
each commit should have a _good commit message_.
A *proper PR title* would be a one-liner summary of the changes in the PR,
A _proper PR title_ would be a one-liner summary of the changes in the PR,
following the same guidelines of a good commit message, including the
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
be a good PR title.
@@ -126,10 +126,10 @@ A good example of a changelog entry could look like the following:
For security-related changelog entries, please include the following additional
details alongside the pull request number:
* Impact: Clearly describe the issue's potential impact on users or systems.
* CVE Number: If available, include the CVE (Common Vulnerabilities and
- Impact: Clearly describe the issue's potential impact on users or systems.
- CVE Number: If available, include the CVE (Common Vulnerabilities and
Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security
- GitHub Advisory Link: Provide a link to the corresponding GitHub security
advisory for further context.
```markdown
@@ -156,12 +156,12 @@ Conventional Commits are structured as follows:
The type of changes which will be included in changelogs is one of the
following:
* `feat`: A new feature
* `fix`: A bugfix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
* `ci`: Changes to CI configuration files and scripts
- `feat`: A new feature
- `fix`: A bugfix
- `doc`: Documentation changes
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `ci`: Changes to CI configuration files and scripts
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
@@ -174,10 +174,10 @@ changelog entry.
The metadata must be included in the following git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e.
- `Security-Impact`: The magnitude of harm that can be expected, i.e.
low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
- `CVE`: The CVE that was assigned to this issue.
- `GitHub-Advisory`: The GitHub advisory identifier.
Please include all the fields that are available.
@@ -334,16 +334,6 @@ on Git 2.17+ you can mass signoff using rebase:
git rebase --signoff origin/main
```
## Tips for working on the `matrix-rust-sdk` with specific IDEs
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
with all features enabled, causing an error in `matrix-sdk` ("only one of the
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
`native-tls` or `rustls-tls` feature definitions:
![Screenshot of RustRover](.img/rustrover-disable-feature.png)
## AI policy
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
Generated
+359 -388
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -28,7 +28,7 @@ assert_matches2 = { version = "0.1.2", default-features = false }
async_cell = { version = "0.2.3", default-features = false }
async-compat = { version = "0.2.5", default-features = false }
async-once-cell = { version = "0.5.4", default-features = false }
async-rx = { version = "0.1.3", default-features = false }
async-rx = { version = "0.2.0", default-features = false }
# Bumping this to 0.3.6 produces a test failure because the semantic between the
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
async-stream = { version = "0.3.6", default-features = false }
@@ -46,7 +46,7 @@ eyeball-im-util = { version = "0.10.0", default-features = false }
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.3.4", default-features = false }
getrandom = { version = "0.4.2", default-features = false }
gloo-timers = { version = "0.3.0", default-features = false }
gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] }
growable-bloom-filter = { version = "2.1.1", default-features = false }
@@ -67,11 +67,11 @@ pin-project-lite = { version = "0.2.16", default-features = false }
proc-macro2 = { version = "1.0.106", default-features = false }
proptest = { version = "1.9.0", default-features = false, features = ["std"] }
quote = { version = "1.0.37", default-features = false }
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
rand = { version = "0.10.1", default-features = false, features = ["std", "std_rng", "thread_rng"] }
regex = { version = "1.12.2", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "9666e207f2cd1a8b26e49bbdf656ad32e4639c7b", features = [
ruma = { git = "https://github.com/ruma/ruma", rev = "7680eebd9586669e1a4e5b1fd1c2c691221369d4", features = [
"client-api-c",
"compat-unset-avatar",
"compat-upload-signatures",
@@ -119,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca
url = { version = "2.5.7", default-features = false }
uuid = { version = "1.18.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
vodozemac = { version = "0.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
wasm-bindgen = { version = "0.2.105", default-features = false }
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
web-sys = { version = "0.3.82", default-features = false }
+1 -1
View File
@@ -17,7 +17,7 @@ codspeed = []
assert_matches.workspace = true
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
futures-util.workspace = true
matrix-sdk = { workspace = true, features = ["rustls-tls", "e2e-encryption", "sqlite", "testing"] }
matrix-sdk = { workspace = true, features = ["e2e-encryption", "sqlite", "testing"] }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
+3 -3
View File
@@ -6,7 +6,7 @@ use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::Even
use matrix_sdk_ui::{
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
};
use rand::{distributions::Uniform, prelude::Distribution};
use rand::{distr::Uniform, prelude::Distribution};
use ruma::{OwnedRoomId, RoomId, owned_user_id};
use tokio::runtime::Builder;
@@ -26,8 +26,8 @@ pub fn create(c: &mut Criterion) {
});
let sender_id = owned_user_id!("@mnt_io:matrix.org");
let mut rand = rand::thread_rng();
let server_ts_range = Uniform::from(100..1000);
let mut rand = rand::rng();
let server_ts_range = Uniform::try_from(100..1000).unwrap();
for room_nth in 0..NUMBER_OF_ROOMS {
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
+2 -2
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.7
// A package manifest for local development. This file will be copied
// into the root of the repo when generating an XCFramework.
@@ -8,7 +8,7 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.iOS(.v16),
.macOS(.v12)
],
products: [
+2 -2
View File
@@ -1,4 +1,4 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.iOS(.v16),
.macOS(.v12)
],
products: [
+1 -1
View File
@@ -2,7 +2,7 @@
name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
edition = "2024"
rust-version.workspace = true
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -3,10 +3,10 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
use hmac::Hmac;
use matrix_sdk_crypto::{
backups::DecryptionError,
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
store::{CryptoStoreError as InnerStoreError, types::BackupDecryptionKey},
};
use pbkdf2::pbkdf2;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::{RngExt, distr::Alphanumeric, rng};
use sha2::Sha512;
use thiserror::Error;
use zeroize::Zeroize;
@@ -75,11 +75,7 @@ impl BackupRecoveryKey {
#[allow(clippy::new_without_default)]
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
inner: BackupDecryptionKey::new()
.expect("Can't gather enough randomness to create a recovery key"),
passphrase_info: None,
})
Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
}
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
@@ -97,7 +93,7 @@ impl BackupRecoveryKey {
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
#[uniffi::constructor]
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
let mut rng = thread_rng();
let mut rng = rng();
let salt: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
@@ -2,14 +2,14 @@ use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
DecryptionSettings,
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
DecryptionSettings,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use ruma::{OwnedDeviceId, api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw};
use serde_json::json;
use crate::{CryptoStoreError, DehydratedDeviceKey};
@@ -29,8 +29,6 @@ pub enum DehydrationError {
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
#[error(transparent)]
Rand(#[from] rand::Error),
}
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
@@ -227,13 +225,11 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
#[cfg(test)]
mod tests {
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
use crate::{DehydratedDeviceKey, dehydrated_devices::DehydrationError};
#[test]
fn test_creating_dehydrated_key() {
let result = DehydratedDeviceKey::new();
assert!(result.is_ok());
let dehydrated_device_key = result.unwrap();
let dehydrated_device_key = DehydratedDeviceKey::new();
let base_64 = dehydrated_device_key.to_base64();
let inner_bytes = dehydrated_device_key.inner;
+1 -1
View File
@@ -1,9 +1,9 @@
#![allow(missing_docs)]
use matrix_sdk_crypto::{
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
SignatureError as InnerSignatureError,
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
};
use matrix_sdk_sqlite::OpenStoreError;
use ruma::{IdParseError, OwnedUserId};
+13 -13
View File
@@ -31,22 +31,22 @@ pub use error::{
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
};
use js_int::UInt;
pub use logger::{set_logger, Logger};
pub use logger::{Logger, set_logger};
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
use matrix_sdk_crypto::{
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{
CryptoStore,
types::{
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
CryptoStore,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
pub use responses::{
@@ -54,9 +54,9 @@ pub use responses::{
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
RoomId, SecondsSinceUnixEpoch, UserId,
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
};
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
@@ -850,9 +850,9 @@ pub struct DehydratedDeviceKey {
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
InnerDehydratedDeviceKey::new().into()
}
/// Creates a new dehydration pickle key from the given slice.
@@ -1015,18 +1015,18 @@ impl PkEncryption {
}
/// Encrypt a message using this [`PkEncryption`] object.
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
use vodozemac::base64_encode;
let message = self.inner.encrypt(plaintext.as_ref());
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
PkMessage {
Some(PkMessage {
ciphertext: base64_encode(ciphertext),
mac: base64_encode(mac),
ephemeral_key: ephemeral_key.to_base64(),
}
})
}
}
@@ -1049,11 +1049,11 @@ uniffi::setup_scaffolding!();
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde_json::{json, Value};
use serde_json::{Value, json};
use tempfile::tempdir;
use super::MigrationData;
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
use crate::{EventEncryptionAlgorithm, OlmMachine, RoomSettings, migrate};
#[test]
fn android_migration() -> Result<()> {
+1 -1
View File
@@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex},
};
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};
/// Trait that can be used to forward Rust logs over FFI to a language specific
/// logger.
+25 -28
View File
@@ -10,6 +10,8 @@ use std::{
use js_int::UInt;
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
use matrix_sdk_crypto::{
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
backups::{
MegolmV1BackupKey as RustBackupKey, SignatureState,
SignatureVerification as RustSignatureCheckResult,
@@ -18,11 +20,12 @@ use matrix_sdk_crypto::{
olm::ExportedRoomKey,
store::types::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
};
use ruma::{
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
api::{
IncomingResponse,
client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
keys::{
@@ -32,38 +35,35 @@ use ruma::{
upload_signatures::v3::Response as SignatureUploadResponse,
},
message::send_message_event::v3::Response as RoomMessageResponse,
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
sync::sync_events::{DeviceLists as RumaDeviceLists, v3::ToDevice},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
key::verification::VerificationMethod, room::message::MessageType,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
use serde_json::{Value, value::RawValue};
use tokio::runtime::Runtime;
use zeroize::Zeroize;
use crate::{
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
dehydrated_devices::DehydratedDevices,
error::{
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
SignatureError,
},
parse_user_id,
responses::{response_from_string, OwnedResponse},
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
responses::{OwnedResponse, response_from_string},
};
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
@@ -913,22 +913,19 @@ impl OlmMachine {
&decryption_settings,
))?;
if handle_verification_events {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
)) => {
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype
{
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
if handle_verification_events
&& let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize()
{
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(original_event)) => {
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
_ => (),
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
_ => (),
}
}
@@ -4,14 +4,15 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
CrossSigningBootstrapRequests,
types::requests::{
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
},
CrossSigningBootstrapRequests,
};
use ruma::{
OwnedTransactionId, UserId,
api::client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
keys::{
@@ -28,7 +29,6 @@ use ruma::{
},
assign,
events::MessageLikeEventContent,
OwnedTransactionId, UserId,
};
use serde_json::json;
+1 -1
View File
@@ -1,4 +1,4 @@
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
use matrix_sdk_crypto::{UserIdentity as SdkUserIdentity, types::CrossSigningKey};
use crate::CryptoStoreError;
@@ -3,10 +3,11 @@ use std::sync::Arc;
use futures_util::{Stream, StreamExt};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
CancelInfo as RustCancelInfo, QrVerification as InnerQr, QrVerificationState, Sas as InnerSas,
SasState as RustSasState, Verification as InnerVerification,
VerificationRequest as InnerVerificationRequest,
VerificationRequestState as RustVerificationRequestState,
matrix_sdk_qrcode::QrVerificationData,
};
use ruma::events::key::verification::VerificationMethod;
use vodozemac::{base64_decode, base64_encode};
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
edition = "2024"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
+8 -8
View File
@@ -27,18 +27,18 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
}
} else if let Item::Impl(blk) = &item {
for item in &blk.items {
if let ImplItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
if let ImplItem::Fn(fun) = item
&& fun.sig.asyncness.is_some()
{
return true;
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
if let TraitItem::Fn(fun) = item
&& fun.sig.asyncness.is_some()
{
return true;
}
}
}
+41
View File
@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
### Bug Fixes
- Add `Client::set_avatar_url` to manually set the avatar URL of the user to a provided MXC one.
- Allow setting a custom Sliding Sync connection ID and timeline limit on `RoomListService`.
([#6289](https://github.com/matrix-org/matrix-rust-sdk/pull/6289))
- Fix devices on Android 11 crashing because the SDK could not be initialized using `libloading`
to get a reference to the JVM. Replaced `libloading` with `jvm-getter`, which works like a
compatibility layer. ([#6370](https://github.com/matrix-org/matrix-rust-sdk/pull/6370))
@@ -41,12 +44,37 @@ All notable changes to this project will be documented in this file.
### Features
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
members to calculate if a room is a DM from the notification info.
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
- Enable `experimental-push-secrets` feature by default.
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
searches for messages in a room or across all rooms.
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Added the `Client.request_openid_token()` method.
([#6458](https://github.com/matrix-org/matrix-rust-sdk/pull/6458))
- Added the `Client::import_secrets_bundle` method.
([#6212](https://github.com/matrix-org/matrix-rust-sdk/pull/6212))
- [**breaking**] Remove support for `native-tls` and remove all feature
flags for selecting TLS backend, as `rustls` is the now the only supported
TLS backend.
([#6409](https://github.com/matrix-org/matrix-rust-sdk/pull/6409))
- Expose `event_type_raw` and `latest_json()` on `EventTimelineItem`,
allowing clients to access the raw event type string and full event JSON for
custom event handling without pattern-matching through nested enums.
([#6387](https://github.com/matrix-org/matrix-rust-sdk/pull/6387))
([#6424](https://github.com/matrix-org/matrix-rust-sdk/pull/6424))
- Expose sync v2 API through FFI via `Client.sync_v2()` and
`Client.sync_once_v2()`, enabling mobile clients to sync without
requiring Sliding Sync support on the homeserver. `Client.sync_v2()`
accepts a `SyncListenerV2` callback that receives a `SyncResponseV2`
after each successful sync.
([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359))
- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities
of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371))
- Expose `Room.send_state_event_raw()` for sending arbitrary state events
through the FFI layer.
([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350))
@@ -133,6 +161,19 @@ All notable changes to this project will be documented in this file.
### Refactor
- [**breaking**] `Room::observe_live_location_shares` has been replaced by
`Room::live_location_shares`. Call [`LiveLocationShares::subscribe`] on it to
receive an initial snapshot and a stream of incremental updates.The stream is seeded from the event cache
on creation and includes the own user's shares (previously excluded). `LiveLocationShare.is_live`
has been removed; instead `ts` (start timestamp) and `timeout` (duration in milliseconds) are now
exposed so clients can compute liveness themselves via `current_time < ts + timeout`. Non-live
shares are automatically removed from the list. A new `LiveLocationShareListener` callback
interface must be implemented and passed to the method.
([#6385](https://github.com/matrix-org/matrix-rust-sdk/pull/6385))
- [**breaking**] The `RoomAliases` variants of `StateEventContent`, `StateEventType` and
`OtherState` was removed. This state event type was removed from the Matrix specification a while
ago, and support for it has been removed in Ruma.
([#6414](https://github.com/matrix-org/matrix-rust-sdk/pull/6414))
- `Client::new` no longer unnecessarily instantiates an `OAuth` component if `CrossProcessLockConfig::SingleProcess`
is used. ([#6293](https://github.com/matrix-org/matrix-rust-sdk/pull/6293))
- [**breaking**] `Room::report_content()` no longer takes a `score` argument, because it was
+4 -6
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-ffi"
version = "0.16.0"
edition = "2021"
edition = "2024"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
license = "Apache-2.0"
@@ -24,7 +24,7 @@ crate-type = [
]
[features]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "rustls-tls"]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
# Use an embedded version of SQLite.
@@ -34,14 +34,11 @@ indexeddb = ["matrix-sdk/indexeddb"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
# Use Rustls as the TLS implementation, necessary on Android platforms.
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
sentry = ["dep:sentry", "dep:sentry-tracing"]
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
experimental-push-secrets = ["matrix-sdk/experimental-push-secrets"]
[dependencies]
anyhow.workspace = true
@@ -59,6 +56,7 @@ matrix-sdk = { workspace = true, features = [
"socks",
"uniffi",
"federation-api",
"experimental-search"
] }
matrix-sdk-base.workspace = true
matrix-sdk-common.workspace = true
+4 -9
View File
@@ -6,11 +6,6 @@ This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build
Given the number of platforms targeted, we have broken out a number of features
### Platform specific
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
### Functionality
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
@@ -24,11 +19,11 @@ Given the number of platforms targeted, we have broken out a number of features
## Platforms
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
Each supported target should use features to build the relevant system. Here are some suggested feature flags for the major platforms:
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
- Android: `"bundled-sqlite,unstable-msc4274,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,sentry"`
- JavaScript/Wasm: `"indexeddb,unstable-msc4274"`
### Swift/iOS sync
@@ -19,12 +19,12 @@ use std::{
};
use matrix_sdk::{
Error,
authentication::oauth::{
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
error::OAuthAuthorizationCodeError,
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
},
Error,
};
use ruma::serde::Raw;
use url::Url;
@@ -101,6 +101,7 @@ impl SsoHandler {
let builder =
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
Ok(())
}
}
+325 -181
View File
@@ -20,45 +20,47 @@ use std::{
time::Duration,
};
use anyhow::{anyhow, Context as _};
use anyhow::{Context as _, anyhow};
use futures_util::pin_mut;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
#[cfg(feature = "sqlite")]
use matrix_sdk::STATE_STORE_DATABASE_NAME;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
use matrix_sdk::{
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
executor::AbortOnDrop,
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
ruma::{
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
api::client::{
account::request_openid_token,
discovery::{
discover_homeserver::RtcFocusInfo,
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
},
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
room::{Visibility, create_room},
session::get_login_types,
user_directory::search_users,
},
events::{
AnyInitialStateEvent, InitialStateEvent,
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
message::MessageType,
},
AnyInitialStateEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
sync::Notification,
task_monitor::BackgroundTaskFailureReason,
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
};
use matrix_sdk_common::{
cross_process_lock::CrossProcessLockConfig, stream::StreamExt, SendOutsideWasm, SyncOutsideWasm,
SendOutsideWasm, SyncOutsideWasm, cross_process_lock::CrossProcessLockConfig, stream::StreamExt,
};
use matrix_sdk_ui::{
notification_client::{
@@ -71,17 +73,23 @@ use matrix_sdk_ui::{
use mime::Mime;
use oauth2::Scope;
use ruma::{
api::client::{
alias::get_alias,
discovery::get_authorization_server_metadata::v1::{
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
api::{
client::{
alias::get_alias,
discovery::get_authorization_server_metadata::v1::{
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
},
profile::{AvatarUrl, DisplayName},
room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent},
uiaa::{EmailUserIdentifier, UserIdentifier},
},
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
uiaa::UserIdentifier,
},
events::{
AnyMessageLikeEventContent, AnySyncTimelineEvent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
@@ -100,25 +108,22 @@ use ruma::{
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
},
tag::TagEventContent,
AnyMessageLikeEventContent, AnySyncTimelineEvent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
room::RoomType,
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::{Value, json};
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
use super::{
room::{room_info::RoomInfo, Room},
room::{Room, room_info::RoomInfo},
session_verification::SessionVerificationController,
};
use crate::{
ClientError,
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
encryption::Encryption,
@@ -142,7 +147,6 @@ use crate::{
task_handle::TaskHandle,
utd::{UnableToDecryptDelegate, UtdHook},
utils::AsyncRuntimeDropped,
ClientError,
};
#[derive(Clone, uniffi::Record)]
@@ -313,6 +317,28 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
}
}
#[derive(Clone, uniffi::Record)]
pub struct OpenIdToken {
pub access_token: String,
pub token_type: String,
pub matrix_server_name: String,
pub expires_in_seconds: u64,
}
impl From<request_openid_token::v3::Response> for OpenIdToken {
fn from(value: request_openid_token::v3::Response) -> Self {
Self {
access_token: value.access_token,
token_type: serde_json::to_value(&value.token_type)
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.unwrap_or_else(|| format!("{:?}", value.token_type)),
matrix_server_name: value.matrix_server_name.to_string(),
expires_in_seconds: value.expires_in.as_secs(),
}
}
}
struct ClientDelegateData {
/// The delegate itself, that will receive the callbacks.
delegate: Arc<dyn ClientDelegate>,
@@ -365,12 +391,12 @@ impl Client {
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
if let MessageType::VerificationRequest(_) = &event.content.msgtype
&& let Some(session_verification_controller) = &*controller.clone().read().await
{
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
});
@@ -488,13 +514,17 @@ impl Client {
device_id: Option<String>,
) -> Result<(), ClientError> {
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
}
@@ -520,6 +550,7 @@ impl Client {
}
builder.send().await?;
Ok(())
}
@@ -534,7 +565,7 @@ impl Client {
let mut builder = self
.inner
.matrix_auth()
.login_identifier(UserIdentifier::Email { address: email }, &password);
.login_identifier(UserIdentifier::Email(EmailUserIdentifier::new(email)), &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
@@ -951,139 +982,7 @@ impl Client {
let listener = Arc::new(listener);
self.inner
.register_notification_handler(move |notification, room, _client| {
let listener = listener.clone();
let room_id = room.room_id().to_string();
async move {
// Extract information about the actions
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
// Convert SDK actions to FFI type
let actions: Vec<crate::notification_settings::Action> = notification
.actions
.into_iter()
.filter_map(|action| action.try_into().ok())
.collect();
// Convert SDK event to FFI type
let (sender, event, thread_id, raw_event) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let thread_id = match &deserialized {
AnySyncTimelineEvent::MessageLike(event) => {
match event.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(
content,
)) => match content.relates_to {
Some(Relation::Thread(thread)) => {
Some(thread.event_id.to_string())
}
_ => None,
},
_ => None,
}
}
_ => None,
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(
deserialized,
))),
};
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize timeline event: {err}");
return;
}
}
}
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let event =
NotificationEvent::Invite { sender: sender.to_string() };
let thread_id = None;
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!(
"Failed to deserialize stripped state event: {err}"
);
return;
}
}
}
};
// Compile sender info
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
let sender_info = if let Some(sender) = sender.as_ref() {
NotificationSenderInfo {
display_name: sender.display_name().map(|name| name.to_owned()),
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
is_name_ambiguous: sender.name_ambiguous(),
}
} else {
NotificationSenderInfo {
display_name: None,
avatar_url: None,
is_name_ambiguous: false,
}
};
// Compile room info
let display_name = match room.display_name().await {
Ok(name) => name.to_string(),
Err(err) => {
tracing::warn!("Failed to calculate the room's display name: {err}");
return;
}
};
let is_direct = match room.is_direct().await {
Ok(is_direct) => is_direct,
Err(err) => {
tracing::warn!("Failed to determine if room is direct or not: {err}");
return;
}
};
let room_info = NotificationRoomInfo {
display_name,
avatar_url: room.avatar_url().map(Into::into),
canonical_alias: room.canonical_alias().map(Into::into),
topic: room.topic(),
join_rule: room
.join_rule()
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
joined_members_count: room.joined_members_count(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
};
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
has_mention: Some(has_mention),
thread_id,
actions: Some(actions),
},
room_id,
);
}
notification_handler(notification, room, listener.clone())
})
.await;
}
@@ -1125,10 +1024,7 @@ impl Client {
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_well_known().await?)
}
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Retrieves a media file from the media source
///
/// Not available on Wasm platforms, due to lack of accessible file system.
@@ -1195,10 +1091,7 @@ impl Client {
Ok(())
}
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
self.inner.sliding_sync_version().into()
@@ -1345,12 +1238,28 @@ impl Client {
Ok(display_name)
}
pub async fn request_openid_token(&self) -> Result<OpenIdToken, ClientError> {
Ok(self.inner.account().request_openid_token().await?.into())
}
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
let mime: Mime = mime_type.parse()?;
self.inner.account().upload_avatar(&mime, data).await?;
Ok(())
}
/// Updates the user's avatar using the provided MXC url.
pub async fn set_avatar_url(&self, url: String) -> Result<(), ClientError> {
// MxcUri can't just be instantiated, serde deserialization seems to be the only
// way
let mxc = serde_json::from_str::<OwnedMxcUri>(&url)?;
// Validate the newly generated MxcUri
mxc.validate().map_err(ClientError::from_err)?;
self.inner.account().set_avatar_url(Some(&mxc)).await?;
Ok(())
}
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
self.inner.account().set_avatar_url(None).await?;
Ok(())
@@ -1574,6 +1483,7 @@ impl Client {
Ok(room)
}
/// Get the first existing DM room with the given user, if any.
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);
@@ -1582,6 +1492,16 @@ impl Client {
Ok(dm)
}
/// Get an iterator with the existing DM rooms for the given user.
pub fn get_dm_rooms(&self, user_id: String) -> Result<Vec<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_rooms = self.inner.get_dm_rooms(&user_id);
let dms = sdk_rooms
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
.collect();
Ok(dms)
}
pub async fn search_users(
&self,
search_term: String,
@@ -2130,10 +2050,10 @@ impl Client {
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);
}
if let Some(room) = self.inner.get_room(&room_id)
&& let Ok(room_info) = RoomInfo::new(&room).await
{
listener.call(room_info);
}
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
@@ -2145,15 +2065,150 @@ impl Client {
continue;
}
if let Some(room) = client.get_room(&room_id) {
if let Ok(room_info) = RoomInfo::new(&room).await {
listener.call(room_info);
}
if let Some(room) = client.get_room(&room_id)
&& let Ok(room_info) = RoomInfo::new(&room).await
{
listener.call(room_info);
}
}
}
}))))
}
/// Whether to enable automatic backpagination under certain conditions
/// (e.g. when processing read receipts).
///
/// This is an experimental feature, and might cause performance issues on
/// large accounts. Use with caution.
///
/// This must be called after creating a client, but before subscribing to
/// the event cache (so, before spawning a sync service or a timeline).
pub fn enable_automatic_backpagination(&self) {
self.inner.event_cache().config_mut().experimental_auto_backpagination = true;
}
pub fn homeserver_capabilities(&self) -> HomeserverCapabilities {
HomeserverCapabilities::new(self.inner.homeserver_capabilities())
}
}
async fn notification_handler(
notification: Notification,
room: matrix_sdk::Room,
listener: Arc<Box<dyn SyncNotificationListener>>,
) {
let room_id = room.room_id().to_string();
// Extract information about the actions
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
// Convert SDK actions to FFI type
let actions: Vec<crate::notification_settings::Action> =
notification.actions.into_iter().filter_map(|action| action.try_into().ok()).collect();
// Convert SDK event to FFI type
let (sender, event, thread_id, raw_event) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let thread_id = if let AnySyncTimelineEvent::MessageLike(event) = &deserialized
&& let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
event.original_content()
&& let Some(Relation::Thread(thread)) = content.relates_to
{
Some(thread.event_id.to_string())
} else {
None
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(deserialized))),
};
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize timeline event: {err}");
return;
}
}
}
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let event = NotificationEvent::Invite { sender: sender.to_string() };
let thread_id = None;
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize stripped state event: {err}");
return;
}
}
}
};
// Compile sender info
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
let sender_info = if let Some(sender) = sender.as_ref() {
NotificationSenderInfo {
display_name: sender.display_name().map(|name| name.to_owned()),
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
is_name_ambiguous: sender.name_ambiguous(),
}
} else {
NotificationSenderInfo { display_name: None, avatar_url: None, is_name_ambiguous: false }
};
// Compile room info
let display_name = match room.display_name().await {
Ok(name) => name.to_string(),
Err(err) => {
tracing::warn!("Failed to calculate the room's display name: {err}");
return;
}
};
let is_direct = match room.is_direct().await {
Ok(is_direct) => is_direct,
Err(err) => {
tracing::warn!("Failed to determine if room is direct or not: {err}");
return;
}
};
let room_info = NotificationRoomInfo {
display_name,
avatar_url: room.avatar_url().map(Into::into),
canonical_alias: room.canonical_alias().map(Into::into),
topic: room.topic(),
join_rule: room.join_rule().map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: room.joined_members_count(),
service_members: room
.service_members()
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
};
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
has_mention: Some(has_mention),
thread_id,
actions: Some(actions),
},
room_id,
);
}
#[cfg(feature = "experimental-element-recent-emojis")]
@@ -3039,16 +3094,88 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
}
}
#[derive(uniffi::Object)]
pub struct HomeserverCapabilities {
inner: matrix_sdk::HomeserverCapabilities,
}
impl HomeserverCapabilities {
pub(crate) fn new(capabilities: matrix_sdk::HomeserverCapabilities) -> Self {
Self { inner: capabilities }
}
}
#[matrix_sdk_ffi_macros::export]
impl HomeserverCapabilities {
pub async fn refresh(&self) -> Result<(), ClientError> {
Ok(self.inner.refresh().await?)
}
pub async fn can_change_password(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_password().await?)
}
pub async fn can_change_displayname(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_displayname().await?)
}
pub async fn can_change_avatar(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_avatar().await?)
}
pub async fn can_change_thirdparty_ids(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_thirdparty_ids().await?)
}
pub async fn can_get_login_token(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_get_login_token().await?)
}
pub async fn extended_profile_fields(&self) -> Result<ExtendedProfileFields, ClientError> {
let profile_fields = self.inner.extended_profile_fields().await?;
Ok(ExtendedProfileFields {
enabled: profile_fields.enabled,
allowed: profile_fields
.allowed
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
disallowed: profile_fields
.disallowed
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
})
}
pub async fn forgets_room_when_leaving(&self) -> Result<bool, ClientError> {
Ok(self.inner.forgets_room_when_leaving().await?)
}
}
#[derive(uniffi::Record)]
pub struct ExtendedProfileFields {
pub enabled: bool,
pub allowed: Vec<String>,
pub disallowed: Vec<String>,
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use ruma::{
api::client::room::{create_room, Visibility},
ServerName,
api::client::room::{Visibility, create_room},
authentication::TokenType,
events::StateEventType,
room::RoomType,
};
use crate::{
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
client::{CreateRoomParameters, JoinRule, OpenIdToken, RoomPreset, RoomVisibility},
room::RoomHistoryVisibility,
};
@@ -3087,9 +3214,9 @@ mod tests {
assert_eq!(request.invite.len(), 1);
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomAvatar));
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomJoinRules));
assert!(initial_state
.iter()
.any(|e| e.event_type() == StateEventType::RoomHistoryVisibility));
assert!(
initial_state.iter().any(|e| e.event_type() == StateEventType::RoomHistoryVisibility)
);
assert_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
let room_type = request
@@ -3100,4 +3227,21 @@ mod tests {
.room_type;
assert_eq!(room_type, Some(RoomType::Space));
}
#[test]
fn test_openid_token_mapping() {
let response = ruma::api::client::account::request_openid_token::v3::Response::new(
"open-id-token".to_owned(),
TokenType::Bearer,
ServerName::parse("example.com").expect("valid server name"),
Duration::from_secs(3_600),
);
let token: OpenIdToken = response.into();
assert_eq!(token.access_token, "open-id-token");
assert_eq!(token.token_type, "Bearer");
assert_eq!(token.matrix_server_name, "example.com");
assert_eq!(token.expires_in_seconds, 3_600);
}
}
+56 -9
View File
@@ -15,21 +15,22 @@
// Allow UniFFI to use methods marked as `#[deprecated]`.
#![allow(deprecated)]
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
#[cfg(not(any(target_family = "wasm", target_os = "android")))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, ThreadingSupport,
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
search_index::SearchIndexStoreKind,
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, ThreadingSupport,
};
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
@@ -137,6 +138,7 @@ pub struct ClientBuilder {
decryption_settings: DecryptionSettings,
enable_share_history_on_invite: bool,
request_config: Option<RequestConfig>,
search_index_store: Option<SearchIndexStoreKind>,
#[cfg(not(target_family = "wasm"))]
user_agent: Option<String>,
@@ -193,6 +195,7 @@ impl ClientBuilder {
enable_share_history_on_invite: false,
request_config: Default::default(),
threading_support: ThreadingSupport::Disabled,
search_index_store: None,
})
}
@@ -357,6 +360,37 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set up the search index store for this client, which is used to store
/// the message search index locally.
///
/// As soon as this is enabled, messages will start to be indexed, and can
/// be later queried for search.
///
/// `path` is the directory where the search index will be stored. It must
/// be unique per session.
///
/// `password` is an optional password to encrypt the search index at rest.
/// If `None`, the search index will be stored unencrypted.
pub fn with_search_index_store(
self: Arc<Self>,
path: String,
password: Option<String>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
// Note: creation of the path is deferred to later.
let path = PathBuf::from(path);
let kind = if let Some(password) = password {
SearchIndexStoreKind::EncryptedDirectory(path, password)
} else {
SearchIndexStoreKind::UnencryptedDirectory(path)
};
builder.search_index_store = Some(kind);
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder()
@@ -385,6 +419,20 @@ impl ClientBuilder {
None
};
if let Some(search_index_store) = builder.search_index_store {
// Create the search index directory.
match search_index_store {
SearchIndexStoreKind::UnencryptedDirectory(ref path)
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
fs::create_dir_all(path)?;
}
_ => {}
}
// Configure the inner builder to use the search index store.
inner_builder = inner_builder.search_index_store(search_index_store);
}
// Determine server either from URL, server name or user ID.
inner_builder = match builder.homeserver_cfg {
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
@@ -490,12 +538,11 @@ impl ClientBuilder {
updated_config = updated_config.timeout(Duration::from_millis(timeout));
}
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
if max_concurrent_requests > 0 {
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
max_concurrent_requests as usize,
));
}
if let Some(max_concurrent_requests) = config.max_concurrent_requests
&& max_concurrent_requests > 0
{
updated_config = updated_config
.max_concurrent_requests(NonZeroUsize::new(max_concurrent_requests as usize));
}
if let Some(max_retry_time) = config.max_retry_time {
updated_config =
+261 -5
View File
@@ -12,14 +12,14 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::{str::FromStr, sync::Arc};
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk::encryption::{self, backups, recovery};
use matrix_sdk_base::crypto::types::{BackupSecrets, RoomKeyBackupInfo};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::OwnedUserId;
use serde::de::Error;
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
@@ -238,6 +238,225 @@ impl From<encryption::VerificationState> for VerificationState {
}
}
/// Struct containing the bundle of secrets to fully activate a new device for
/// end-to-end encryption.
#[derive(uniffi::Object)]
pub struct SecretsBundleWithUserId {
user_id: OwnedUserId,
inner: matrix_sdk_base::crypto::types::SecretsBundle,
}
/// Result for the check if a store has a valid secrets bundle.
#[derive(uniffi::Enum)]
pub enum DetectedSecretsBundle {
/// The store doesn't contain a secrets bundle at all.
None,
/// The store contains a bundle without a backup.
WithoutBackup,
/// The store contains a bundle with an unused backup, the backup key in the
/// bundle isn't used on the homeserver.
UnusedBackup,
/// The store contains a complete secrets bundle.
Complete,
}
/// Error type describing failures that can happen while exporting a
/// [`SecretsBundle`] from a SQLite store.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum BundleExportError {
/// The SQLite store couldn't be opened.
#[error("the store couldn't be opened: {msg}")]
OpenStoreError { msg: String },
/// Data from the SQLite store couldn't be exported.
#[error("the bundle couldn't be exported due to a storage error: {msg}")]
StoreError { msg: String },
/// The store doesn't contain a secrets bundle or it couldn't be read from
/// the store.
#[error("the bundle couldn't be exported: {msg}")]
SecretError { msg: String },
/// The store is empty and doesn't contain a secrets bundle.
#[error("the store is completely empty")]
StoreEmpty,
/// A JSON object couldn't be deserialized while the secrets bundle was
/// exported.
#[error("Couldn't deserialize a JSON value: {msg}")]
Json { msg: String },
/// Error returned when the secrets bundle is missing a backup key or
/// includes one that doesnt match the key configured for the active backup
/// version.
#[error(
"The bundle is missing a backup key or has one that isn't the one that's currently used"
)]
InvalidBackup,
}
#[cfg(feature = "sqlite")]
impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
match value {
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
BundleExportError::OpenStoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
BundleExportError::StoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
BundleExportError::SecretError { msg: e.to_string() }
}
}
}
}
impl From<serde_json::Error> for BundleExportError {
fn from(value: serde_json::Error) -> Self {
Self::Json { msg: value.to_string() }
}
}
#[cfg(feature = "sqlite")]
#[matrix_sdk_ffi_macros::export]
impl SecretsBundleWithUserId {
/// Attempt to export a [`SecretsBundle`] from a crypto store.
///
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
/// another [`Client`] instance.
///
/// This can be useful for migration purposes or to allow existing client
/// instances create new ones that will be fully verified.
#[uniffi::constructor]
pub async fn from_database(
database_path: &str,
mut passphrase: Option<String>,
backup_info: &str,
) -> Result<Arc<Self>, BundleExportError> {
let backup_info = serde_json::from_str(backup_info)?;
let ret = if let Some((user_id, bundle)) =
matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await?
{
let is_backup_ok =
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
if is_backup_ok {
Ok(SecretsBundleWithUserId { user_id, inner: bundle }.into())
} else {
Err(BundleExportError::InvalidBackup)
}
} else {
Err(BundleExportError::StoreEmpty)
};
passphrase.zeroize();
ret
}
}
#[matrix_sdk_ffi_macros::export]
impl SecretsBundleWithUserId {
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
/// bundle.
#[uniffi::constructor]
pub fn from_str(
user_id: &str,
bundle: &str,
backup_info: &str,
) -> Result<Arc<Self>, BundleExportError> {
let user_id =
OwnedUserId::from_str(user_id).map_err(|e| serde_json::Error::custom(e.to_string()))?;
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
let backup_info = serde_json::from_str(backup_info)?;
let is_backup_ok =
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
if is_backup_ok {
Ok(Self { user_id, inner: bundle }.into())
} else {
Err(BundleExportError::InvalidBackup)
}
}
/// Does the bundle contain a backup key.
///
/// Since enabling a backup is optional, the backup key might be missing
/// from the bundle. Returns `false` if the backup key is missing,
/// otherwise `true`.
pub fn contains_backup_key(&self) -> bool {
self.inner.backup.is_some()
}
}
fn is_valid_backup(secrets: &BackupSecrets, info: &RoomKeyBackupInfo) -> bool {
match secrets {
BackupSecrets::MegolmBackupV1Curve25519AesSha2(secrets) => {
secrets.key.backup_key_matches(info)
}
}
}
fn check_bundle_and_info(
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
info: Option<&RoomKeyBackupInfo>,
) -> DetectedSecretsBundle {
match (&bundle.backup, info) {
(None, None) => DetectedSecretsBundle::WithoutBackup,
(None, Some(_)) => DetectedSecretsBundle::WithoutBackup,
(Some(_), None) => DetectedSecretsBundle::UnusedBackup,
(Some(backup), Some(info)) => {
if is_valid_backup(backup, info) {
DetectedSecretsBundle::Complete
} else {
DetectedSecretsBundle::UnusedBackup
}
}
}
}
/// Check if a JSON encoded string contains a valid [`SecretsBundle`].
#[uniffi::export]
pub fn json_string_contains_secrets_bundle(
bundle: &str,
backup_info: Option<String>,
) -> Result<DetectedSecretsBundle, ClientError> {
let info: Option<RoomKeyBackupInfo> =
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
Ok(check_bundle_and_info(&bundle, info.as_ref()))
}
/// Check if a crypto store contains a valid [`SecretsBundle`].
#[cfg(feature = "sqlite")]
#[matrix_sdk_ffi_macros::export]
pub async fn database_contains_secrets_bundle(
database_path: &str,
mut passphrase: Option<String>,
backup_info: Option<String>,
) -> Result<DetectedSecretsBundle, BundleExportError> {
let info: Option<RoomKeyBackupInfo> =
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
let maybe_bundle = matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await?;
passphrase.zeroize();
Ok(match maybe_bundle {
Some((_, bundle)) => check_bundle_and_info(&bundle, info.as_ref()),
None => DetectedSecretsBundle::None,
})
}
#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
@@ -505,6 +724,43 @@ impl Encryption {
Ok(None)
}
}
/// This method will import all the private cross-signing keys and
/// the private part of a backup key and its accompanying version into the
/// store.
///
/// Importing all the secrets will mark the device as verified and enable
/// backups.
///
/// **Warning**: Only import this from a trusted source, i.e. if an existing
/// device is sharing this with a new device.
///
/// **Warning*: Only call this method right after logging in and before the
/// initial sync has been started.
pub async fn import_secrets_bundle(
&self,
secrets_bundle: &SecretsBundleWithUserId,
) -> Result<(), ClientError> {
let user_id = self._client.inner.user_id().expect(
"We should have a user ID available now, this is only called once we're logged in",
);
if user_id == secrets_bundle.user_id {
self.inner
.import_secrets_bundle(&secrets_bundle.inner)
.await
.map_err(ClientError::from_err)?;
self.inner.wait_for_e2ee_initialization_tasks().await;
Ok(())
} else {
Err(ClientError::Generic {
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
details: None,
})
}
}
}
/// The E2EE identity of a user.
+51 -21
View File
@@ -15,21 +15,20 @@
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
use matrix_sdk::{
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
authentication::oauth::OAuthError,
encryption::{identities::RequestVerificationError, CryptoStoreError},
encryption::{CryptoStoreError, identities::RequestVerificationError},
event_cache::EventCacheError,
reqwest,
room::{calls::CallError, edit::EditError},
send_queue::RoomSendQueueError,
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
MilliSecondsSinceUnixEpoch,
api::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
};
use tracing::warn;
use uniffi::UnexpectedUniFFICallbackError;
use crate::{room_list::RoomListError, timeline::FocusEventError};
@@ -44,7 +43,6 @@ pub enum ClientError {
impl ClientError {
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
warn!("Error: {error}");
Self::Generic { msg: error.to_string(), details }
}
@@ -77,22 +75,21 @@ impl From<matrix_sdk::Error> for ClientError {
fn from(e: matrix_sdk::Error) -> Self {
match e {
matrix_sdk::Error::Http(http_error) => {
if let Some(api_error) = http_error.as_client_api_error() {
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
if let Some(api_error) = http_error.as_client_api_error()
&& let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
&api_error.body
{
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 (*http_error).into();
};
return Self::MatrixApi {
kind,
code,
msg: message.to_owned(),
details: Some(format!("{api_error:?}")),
};
}
{
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 (*http_error).into();
};
return Self::MatrixApi {
kind,
code,
msg: message.to_owned(),
details: Some(format!("{api_error:?}")),
};
}
(*http_error).into()
}
@@ -348,6 +345,39 @@ pub enum RoomError {
FailedSendingAttachment,
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum LiveLocationError {
#[error("Network error")]
Network,
#[error("Existing beacon information not found")]
NotFound,
#[error("Beacon event is redacted and cannot be processed")]
Redacted,
#[error("Must join the room to access beacon information")]
Stripped,
#[error("The beacon event has expired")]
NotLive,
#[error("Deserialization error")]
Deserialization,
#[error("Other error: {msg}")]
Other { msg: String },
}
impl From<matrix_sdk::BeaconError> for LiveLocationError {
fn from(value: matrix_sdk::BeaconError) -> Self {
match value {
matrix_sdk::BeaconError::Network(_) => Self::Network,
matrix_sdk::BeaconError::NotFound => Self::NotFound,
matrix_sdk::BeaconError::Redacted => Self::Redacted,
matrix_sdk::BeaconError::Stripped => Self::Stripped,
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
matrix_sdk::BeaconError::NotLive => Self::NotLive,
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum MediaInfoError {
+8 -14
View File
@@ -12,29 +12,29 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use anyhow::{bail, Context};
use anyhow::{Context, bail};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
EventId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
room::{
encrypted,
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
},
EventId,
};
use crate::{
ClientError,
room_member::MembershipState,
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
utils::Timestamp,
ClientError,
};
#[derive(uniffi::Object)]
@@ -89,6 +89,7 @@ impl From<AnyTimelineEvent> for TimelineEvent {
/// The timeline event type.
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
#[uniffi::export(Eq, Hash)]
pub enum TimelineEventType {
/// The event is a message-like one and should be displayed as such.
MessageLike { value: MessageLikeEventType },
@@ -224,9 +225,6 @@ impl From<RumaTimelineEventType> for TimelineEventType {
RumaTimelineEventType::PolicyRuleUser => {
Self::State { value: StateEventType::PolicyRuleUser }
}
RumaTimelineEventType::RoomAliases => {
Self::State { value: StateEventType::RoomAliases }
}
RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar },
RumaTimelineEventType::RoomCanonicalAlias => {
Self::State { value: StateEventType::RoomCanonicalAlias }
@@ -306,7 +304,6 @@ pub enum StateEventContent {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
@@ -334,7 +331,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom,
AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer,
AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser,
AnySyncStateEvent::RoomAliases(_) => StateEventContent::RoomAliases,
AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar,
AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias,
AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate,
@@ -526,7 +522,6 @@ pub enum StateEventType {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
@@ -558,7 +553,6 @@ impl From<StateEventType> for ruma::events::StateEventType {
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
StateEventType::RoomAliases => Self::RoomAliases,
StateEventType::RoomAvatar => Self::RoomAvatar,
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
StateEventType::RoomCreate => Self::RoomCreate,
+1
View File
@@ -24,6 +24,7 @@ mod room_member;
mod room_preview;
mod ruma;
mod runtime;
mod search;
mod session_verification;
mod spaces;
mod store;
@@ -12,22 +12,153 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use crate::ruma::LocationContent;
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt as _;
use matrix_sdk::live_location_share::{
LiveLocationShare as SdkLiveLocationShare, LiveLocationShares as SdkLiveLocationShares,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use crate::{ruma::LocationContent, runtime::get_runtime_handle, task_handle::TaskHandle};
/// Details of the last known location beacon.
#[derive(uniffi::Record)]
pub struct LastLocation {
/// The most recent location content of the user.
/// The most recent location content shared for this asset.
pub location: LocationContent,
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
/// The timestamp of when the location was updated.
pub ts: u64,
}
/// Details of a users live location share.
/// Details of a user's live location share.
#[derive(uniffi::Record)]
pub struct LiveLocationShare {
/// The user's last known location.
pub last_location: LastLocation,
/// The live status of the live location share.
pub(crate) is_live: bool,
/// The asset's last known location.
pub last_location: Option<LastLocation>,
/// The user ID of the person sharing their live location.
pub user_id: String,
/// The time when location sharing started.
pub start_ts: u64,
/// The duration that the location sharing will be live.
/// Meaning that the location will stop being shared at ts + timeout.
pub timeout: u64,
}
/// An update to the list of active live location shares.
///
/// Corresponds to a [`VectorDiff`] on the underlying [`ObservableVector`].
///
/// [`ObservableVector`]: eyeball_im::ObservableVector
#[derive(uniffi::Enum)]
pub enum LiveLocationShareUpdate {
Append { values: Vec<LiveLocationShare> },
Clear,
PushFront { value: LiveLocationShare },
PushBack { value: LiveLocationShare },
PopFront,
PopBack,
Insert { index: u32, value: LiveLocationShare },
Set { index: u32, value: LiveLocationShare },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<LiveLocationShare> },
}
/// Listener for live location share updates.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SendOutsideWasm + SyncOutsideWasm + Debug {
/// Called with a batch of [`LiveLocationShareUpdate`]s whenever the list
/// of active shares changes.
fn on_update(&self, updates: Vec<LiveLocationShareUpdate>);
}
/// Tracks active live location shares in a room.
///
/// Holds the SDK [`SdkLiveLocationShares`] which keeps the beacon and
/// beacon_info event handlers registered for as long as this object is alive.
/// Call [`LiveLocationShares::subscribe`] to start receiving updates.
#[derive(uniffi::Object)]
pub struct LiveLocationShares {
inner: SdkLiveLocationShares,
}
impl LiveLocationShares {
pub fn new(inner: SdkLiveLocationShares) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl LiveLocationShares {
/// Subscribe to changes in the list of active live location shares.
///
/// Immediately calls `listener` with a `Reset` update containing the
/// current snapshot (if non-empty), then calls it again for every
/// subsequent change that arrives from sync.
///
/// Returns a [`TaskHandle`] that, when dropped, stops the listener.
/// The event handlers remain registered for as long as this
/// [`LiveLocationShares`] object is alive.
pub fn subscribe(&self, listener: Box<dyn LiveLocationShareListener>) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe();
if !initial_values.is_empty() {
listener.on_update(vec![LiveLocationShareUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
}
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(Into::into).collect());
}
})))
}
}
impl From<SdkLiveLocationShare> for LiveLocationShare {
fn from(share: SdkLiveLocationShare) -> Self {
let start_ts = share.beacon_info.ts.0.into();
let timeout = share.beacon_info.timeout.as_millis() as u64;
let asset = share.beacon_info.asset.type_.into();
let last_location = share.last_location.map(|l| LastLocation {
location: LocationContent {
body: "".to_owned(),
geo_uri: l.location.uri.to_string(),
description: None,
zoom_level: None,
asset,
},
ts: l.ts.0.into(),
});
LiveLocationShare { user_id: share.user_id.to_string(), last_location, start_ts, timeout }
}
}
impl From<VectorDiff<SdkLiveLocationShare>> for LiveLocationShareUpdate {
fn from(diff: VectorDiff<SdkLiveLocationShare>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(Into::into).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: value.into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(Into::into).collect() }
}
}
}
}
@@ -49,6 +49,7 @@ pub struct NotificationRoomInfo {
pub topic: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub service_members: Vec<String>,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_space: bool,
@@ -106,6 +107,7 @@ impl NotificationItem {
topic: item.room_topic,
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: item.joined_members_count,
service_members: item.service_members,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_space: item.is_space,
@@ -15,23 +15,26 @@
use std::sync::{Arc, RwLock};
use matrix_sdk::{
Client as MatrixClient,
event_handler::EventHandlerHandle,
notification_settings::{
NotificationSettings as SdkNotificationSettings,
RoomNotificationMode as SdkRoomNotificationMode,
},
ruma::events::push_rules::PushRulesEvent,
Client as MatrixClient,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::{
Int, RoomId, UInt,
events::push_rules::PushRulesEventContent,
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, EventMatchConditionData,
EventPropertyContainsConditionData, EventPropertyIsConditionData, HighlightTweakValue,
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition as SdkPushCondition,
RoomMemberCountConditionData, RoomMemberCountIs, RuleKind as SdkRuleKind,
ScalarJsonValue as SdkJsonValue, SenderNotificationPermissionConditionData,
Tweak as SdkTweak,
},
Int, RoomId, UInt,
};
use tokio::sync::RwLock as AsyncRwLock;
@@ -181,20 +184,22 @@ impl TryFrom<SdkPushCondition> for PushCondition {
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
Ok(match value {
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
SdkPushCondition::EventMatch(data) => {
Self::EventMatch { key: data.key, pattern: data.pattern }
}
#[allow(deprecated)]
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
SdkPushCondition::RoomMemberCount { is } => {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
SdkPushCondition::RoomMemberCount(data) => {
Self::RoomMemberCount { prefix: data.is.prefix.into(), count: data.is.count.into() }
}
SdkPushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.to_string() }
SdkPushCondition::SenderNotificationPermission(data) => {
Self::SenderNotificationPermission { key: data.key.to_string() }
}
SdkPushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
SdkPushCondition::EventPropertyIs(data) => {
Self::EventPropertyIs { key: data.key, value: data.value.into() }
}
SdkPushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
SdkPushCondition::EventPropertyContains(data) => {
Self::EventPropertyContains { key: data.key, value: data.value.into() }
}
_ => return Err("Unsupported condition type".to_owned()),
})
@@ -204,24 +209,28 @@ impl TryFrom<SdkPushCondition> for PushCondition {
impl From<PushCondition> for SdkPushCondition {
fn from(value: PushCondition) -> Self {
match value {
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
PushCondition::EventMatch { key, pattern } => {
Self::EventMatch(EventMatchConditionData::new(key, pattern))
}
#[allow(deprecated)]
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
is: RoomMemberCountIs {
PushCondition::RoomMemberCount { prefix, count } => {
Self::RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs {
prefix: prefix.into(),
count: UInt::new(count).unwrap_or_default(),
},
},
}))
}
PushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.into() }
Self::SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
key.into(),
))
}
PushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
PushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
Self::EventPropertyIs(EventPropertyIsConditionData::new(key, value.into()))
}
PushCondition::EventPropertyContains { key, value } => Self::EventPropertyContains(
EventPropertyContainsConditionData::new(key, value.into()),
),
}
}
}
@@ -303,16 +312,19 @@ impl TryFrom<SdkTweak> for Tweak {
type Error = String;
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
Ok(match value {
SdkTweak::Sound(sound) => Self::Sound { value: sound },
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}"))?;
Self::Custom { name, value: json_string }
Ok(match &value {
SdkTweak::Sound(sound) => Self::Sound { value: sound.to_string() },
SdkTweak::Highlight(highlight) => {
Self::Highlight { value: matches!(highlight, HighlightTweakValue::Yes) }
}
_ => {
let json_string = value
.custom_value()
.ok_or_else(|| "Unsupported tweak type".to_owned())?
.to_string();
Self::Custom { name: value.set_tweak().to_owned(), value: json_string }
}
_ => return Err("Unsupported tweak type".to_owned()),
})
}
}
@@ -322,16 +334,16 @@ impl TryFrom<Tweak> for SdkTweak {
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
Ok(match value {
Tweak::Sound { value } => Self::Sound(value),
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}"))?;
let value = serde_json::from_value(json_value)
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
Self::Custom { name, value }
}
Tweak::Sound { value } => Self::Sound(value.into()),
Tweak::Highlight { value } => Self::Highlight(value.into()),
Tweak::Custom { name, value } => Self::new(
name,
Some(
serde_json::value::RawValue::from_string(value)
.map_err(|e| format!("Failed to convert JSON value: {e}"))?,
),
)
.map_err(|e| format!("Failed to convert custom tweak: {e}"))?,
})
}
}
@@ -2,7 +2,7 @@ use std::{error::Error, mem::MaybeUninit};
use jni::{
errors::JniError,
sys::{JavaVM as RawJavaVM, JNI_OK},
sys::{JNI_OK, JavaVM as RawJavaVM},
};
use tracing::debug;
@@ -70,8 +70,8 @@ fn init_rustls_platform_verifier(env: &mut jni::JNIEnv<'_>) -> jni::errors::Resu
}
/// Attach the current thread to a JVM one.
pub(crate) fn android_attach_current_thread_permanently(
) -> jni::errors::Result<jni::JNIEnv<'static>> {
pub(crate) fn android_attach_current_thread_permanently()
-> jni::errors::Result<jni::JNIEnv<'static>> {
ANDROID_JVM
.get()
.ok_or_else(|| jni::errors::Error::JniCall(JniError::Unknown))?
+15 -15
View File
@@ -14,7 +14,7 @@
use std::sync::OnceLock;
#[cfg(feature = "sentry")]
use std::sync::{atomic::AtomicBool, Arc};
use std::sync::{Arc, atomic::AtomicBool};
use ::tracing::info;
#[cfg(feature = "sentry")]
@@ -24,18 +24,17 @@ use tracing_appender::rolling::Rotation;
use tracing_core::Level;
use tracing_core::Subscriber;
use tracing_subscriber::{
EnvFilter, Layer, Registry,
field::RecordFields,
fmt::{
self,
self, FormatEvent, FormatFields, FormattedFields,
format::{DefaultFields, Writer},
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::{Layered, SubscriberExt as _},
registry::LookupSpan,
reload::{self, Handle},
util::SubscriberInitExt as _,
EnvFilter, Layer, Registry,
};
use crate::error::ClientError;
@@ -54,9 +53,9 @@ mod android_platform;
use rolling_writer::SizeAndDateRollingWriter;
use self::tracing::LogLevel;
#[cfg(feature = "sentry")]
use self::tracing::BRIDGE_SPAN_NAME;
use self::tracing::LogLevel;
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
@@ -150,10 +149,10 @@ where
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>()
&& !fields.is_empty()
{
write!(writer, "{{{fields}}}")?;
}
}
}
@@ -514,7 +513,12 @@ impl TracingConfiguration {
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
fn build(mut self) -> LoggingCtx {
// Show full backtraces, if we run into panics.
std::env::set_var("RUST_BACKTRACE", "1");
//
// FIXME: Use safe API for this once stable. Tracking issue:
// https://github.com/rust-lang/rust/issues/93346
unsafe {
std::env::set_var("RUST_BACKTRACE", "1");
}
// Log panics.
log_panics::init();
@@ -533,11 +537,7 @@ impl TracingConfiguration {
sentry::ClientOptions {
traces_sampler: Some(Arc::new(|ctx| {
// Make sure bridge spans are always uploaded
if ctx.name() == BRIDGE_SPAN_NAME {
1.0
} else {
0.0
}
if ctx.name() == BRIDGE_SPAN_NAME { 1.0 } else { 0.0 }
})),
attach_stacktrace: true,
release: Some(env!("VERGEN_GIT_SHA").into()),
@@ -237,12 +237,11 @@ impl SizeAndDateRollingWriter {
check_conditions: bool,
) -> io::Result<()> {
// Check if rotation is needed (skip for uninitialized state)
if check_conditions {
if let Some(state) = state.as_ref() {
if !Self::should_rotate_by_time(config, &state.current_path) {
return Ok(());
}
}
if check_conditions
&& let Some(state) = state.as_ref()
&& !Self::should_rotate_by_time(config, &state.current_path)
{
return Ok(());
}
let time_str = Self::format_rotation_timestamp(config);
@@ -312,10 +311,10 @@ impl SizeAndDateRollingWriter {
}
// Check if file is older than max age
if let Ok(duration) = now.duration_since(modified) {
if duration.as_secs() > config.max_age_seconds {
let _ = fs::remove_file(path);
}
if let Ok(duration) = now.duration_since(modified)
&& duration.as_secs() > config.max_age_seconds
{
let _ = fs::remove_file(path);
}
}
@@ -403,11 +402,7 @@ impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
fn flush(&mut self) -> io::Result<()> {
let mut state = self.state.lock().unwrap();
if let Some(s) = state.as_mut() {
s.current_file.flush()
} else {
Ok(())
}
if let Some(s) = state.as_mut() { s.current_file.flush() } else { Ok(()) }
}
}
@@ -658,11 +653,7 @@ mod tests {
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() {
path.file_name()?.to_str().map(|s| s.to_owned())
} else {
None
}
if path.is_file() { path.file_name()?.to_str().map(|s| s.to_owned()) } else { None }
})
.collect();
@@ -876,10 +867,10 @@ mod tests {
std::fs::read_dir(log_path)
.unwrap()
.filter(|entry| {
if let Ok(entry) = entry {
if let Some(name) = entry.file_name().to_str() {
return name.starts_with("multi");
}
if let Ok(entry) = entry
&& let Some(name) = entry.file_name().to_str()
{
return name.starts_with("multi");
}
false
})
@@ -19,7 +19,7 @@ use std::{
sync::{Arc, Mutex, OnceLock},
};
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
use tracing::{Callsite, callsite::DefaultCallsite, debug, error, field::FieldSet};
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// Log an event.
+2 -2
View File
@@ -15,14 +15,14 @@
use std::sync::Arc;
use matrix_sdk::authentication::oauth::{
OAuth,
qrcode::{
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
},
OAuth,
};
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt};
use crate::{
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
+62 -110
View File
@@ -15,62 +15,61 @@
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk::{
encryption::LocalTrust,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
},
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
SuccessorRoom as SdkSuccessorRoom,
encryption::LocalTrust,
room::{
Room as SdkRoom, RoomMemberRole, edit::EditedContent, power_levels::RoomPowerLevelChanges,
},
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
timeline::{default_event_filter, RoomExt, TimelineBuilder},
timeline::{RoomExt, TimelineBuilder, default_event_filter},
unable_to_decrypt_hook::UtdHookManager,
};
use mime::Mime;
use ruma::{
assign,
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId, assign,
events::{
AnyMessageLikeEventContent, AnySyncTimelineEvent,
receipt::ReceiptThread,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
MediaSource as RumaMediaSource, avatar::ImageInfo as RumaAvatarImageInfo,
history_visibility::HistoryVisibility as RumaHistoryVisibility,
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
MediaSource as RumaMediaSource,
},
AnyMessageLikeEventContent, AnySyncTimelineEvent,
},
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId,
};
use tracing::{error, warn};
use tracing::error;
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
use crate::{
TaskHandle,
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
error::{
ClientError, LiveLocationError, MediaInfoError, NotYetImplemented, QueueWedgeError,
RoomError,
},
event::TimelineEvent,
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
live_location_share::LiveLocationShares,
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
},
ruma::{AudioInfo, FileInfo, ImageInfo, MediaSource, ThumbnailInfo, VideoInfo},
runtime::get_runtime_handle,
timeline::{
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
configuration::{TimelineConfiguration, TimelineFilter},
threads::{ThreadListService, ThreadSubscription},
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
},
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
utils::{AsyncRuntimeDropped, u64_to_uint},
};
mod power_levels;
@@ -1088,17 +1087,14 @@ impl Room {
}
/// Stop the current users live location share in the room.
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
pub async fn stop_live_location_share(&self) -> Result<(), LiveLocationError> {
self.inner.stop_live_location_share().await?;
Ok(())
}
/// Send the current users live location beacon in the room.
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
self.inner
.send_location_beacon(geo_uri)
.await
.expect("Unable to send live location beacon");
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), LiveLocationError> {
self.inner.send_location_beacon(geo_uri).await?;
Ok(())
}
@@ -1140,46 +1136,16 @@ impl Room {
}))))
}
/// Subscribes to live location shares in this room, using a `listener` to
/// be notified of the changes.
/// Returns the active live location shares for this room.
///
/// The current live location shares will be emitted immediately when
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
pub fn subscribe_to_live_location_shares(
self: Arc<Self>,
listener: Box<dyn LiveLocationShareListener>,
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let subscription = room.observe_live_location_shares();
let stream = subscription.subscribe();
let mut pinned_stream = pin!(stream);
while let Some(event) = pinned_stream.next().await {
let last_location = LocationContent {
body: "".to_owned(),
geo_uri: event.last_location.location.uri.clone().to_string(),
description: None,
zoom_level: None,
asset: event.beacon_info.as_ref().map(|b| b.asset.type_.clone()).into(),
};
let Some(beacon_info) = event.beacon_info else {
warn!("Live location share is missing the associated beacon_info state, skipping event.");
continue;
};
listener.call(vec![LiveLocationShare {
last_location: LastLocation {
location: last_location,
ts: event.last_location.ts.0.into(),
},
is_live: beacon_info.is_live(),
user_id: event.user_id.to_string(),
}])
}
})))
/// The returned [`LiveLocationShares`] object tracks which users are
/// currently sharing their live location. It keeps the underlying event
/// handlers registered — and therefore the share list up-to-date — for as
/// long as it is alive. Call [`LiveLocationShares::subscribe`] on it to
/// receive an initial snapshot and a stream of incremental updates.
pub async fn live_location_shares(&self) -> Arc<LiveLocationShares> {
let inner = self.inner.live_location_shares().await;
Arc::new(LiveLocationShares::new(inner))
}
/// Forget this room.
@@ -1214,12 +1180,11 @@ impl Room {
// 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());
}
}
if server_names.is_empty()
&& let Ok(invite_details) = self.inner.invite_details().await
&& 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?;
@@ -1303,12 +1268,6 @@ impl Room {
}
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
}
/// A listener for receiving call decline events in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
@@ -1675,23 +1634,30 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
type Error = ClientError;
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
fn draft_thumbnail(
thumbnail_info: Option<ThumbnailInfo>,
thumbnail_source: Option<UploadSource>,
) -> Result<Option<DraftThumbnail>, ClientError> {
if let Some(info) = thumbnail_info
&& let Some(source) = thumbnail_source
{
let (data, filename) = read_upload_source(source)?;
Ok(Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
}))
} else {
Ok(None)
}
}
match value {
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Image {
@@ -1701,26 +1667,12 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
width: image_info.width,
height: image_info.height,
blurhash: image_info.blurhash,
thumbnail,
thumbnail: draft_thumbnail(image_info.thumbnail_info, thumbnail_source)?,
},
})
}
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Video {
@@ -1731,7 +1683,7 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
height: video_info.height,
duration: video_info.duration,
blurhash: video_info.blurhash,
thumbnail,
thumbnail: draft_thumbnail(video_info.thumbnail_info, thumbnail_source)?,
},
})
}
@@ -16,8 +16,8 @@ use std::collections::HashMap;
use anyhow::Result;
use ruma::{
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevels as RumaPowerLevels},
};
use crate::{
@@ -22,7 +22,7 @@ use crate::{
error::ClientError,
notification_settings::RoomNotificationMode,
room::{
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom, power_levels::RoomPowerLevels,
},
room_member::RoomMember,
ruma::RtcCallIntent,
+11 -11
View File
@@ -17,30 +17,30 @@
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
},
Room as SdkRoom,
ruma::{
RoomId,
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
};
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_deduplicate_versions,
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_identifiers,
new_filter_invite, new_filter_joined, new_filter_low_priority, new_filter_non_left,
new_filter_none, new_filter_normalized_match_room_name, new_filter_not, new_filter_space,
new_filter_unread, BoxedFilterFn, RoomCategory,
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_identifiers, new_filter_invite, new_filter_joined, new_filter_low_priority,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_not, new_filter_space, new_filter_unread,
},
unable_to_decrypt_hook::UtdHookManager,
};
use crate::{
TaskHandle,
room::{Membership, Room},
runtime::get_runtime_handle,
TaskHandle,
};
#[derive(Debug, thiserror::Error, uniffi::Error)]
+1 -1
View File
@@ -1,5 +1,5 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
use ruma::{UserId, events::room::power_levels::UserPowerLevel};
use crate::error::{ClientError, NotYetImplemented};
+1 -1
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use matrix_sdk::{Client, room_preview::RoomPreview as SdkRoomPreview};
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
use crate::{
+10 -14
View File
@@ -21,8 +21,13 @@ use std::{
use extension_trait::extension_trait;
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
use ruma::{
assign,
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
OwnedRoomId, OwnedUserId, UInt, UserId, assign,
events::{
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
RoomAccountDataEventType as RumaRoomAccountDataEventType,
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
@@ -36,6 +41,8 @@ use ruma::{
poll::start::PollKind as RumaPollKind,
push_rules::PushRulesEventContent,
room::{
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
message::{
AudioInfo as RumaAudioInfo,
AudioMessageEventContent as RumaAudioMessageEventContent,
@@ -53,8 +60,6 @@ use ruma::{
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
},
rtc::notification::{
CallIntent as RumaCallIntent, NotificationType as RumaNotificationType,
@@ -72,10 +77,6 @@ use ruma::{
TagEventContent, TagInfo as RumaTagInfo, TagName as RumaTagName,
UserTagName as RumaUserTagName,
},
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
RoomAccountDataEventType as RumaRoomAccountDataEventType,
},
matrix_uri::MatrixId as RumaMatrixId,
push::{
@@ -83,8 +84,6 @@ use ruma::{
Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule,
},
serde::JsonObject,
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
OwnedRoomId, OwnedUserId, UInt, UserId,
};
use tracing::info;
@@ -391,11 +390,7 @@ pub enum MessageType {
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
if let Some(caption) = caption { (caption, Some(filename)) } else { (filename, None) }
}
impl TryFrom<MessageType> for RumaMessageType {
@@ -1714,6 +1709,7 @@ pub enum RoomAccountDataEvent {
/// The name of a tag.
#[derive(Clone, PartialEq, Eq, Hash, uniffi::Enum)]
#[uniffi::export(Eq, Hash)]
pub enum TagName {
/// `m.favourite`: The user's favorite rooms.
Favorite,
+1 -1
View File
@@ -39,7 +39,7 @@ mod sys {
mod sys {
use std::future::Future;
use matrix_sdk_common::executor::{spawn, JoinHandle};
use matrix_sdk_common::executor::{JoinHandle, spawn};
/// A dummy guard that does nothing when dropped.
/// This is used for the Wasm implementation to match
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use matrix_sdk::{
deserialized_responses::TimelineEvent,
message_search::{
GlobalSearchIterator as SdkGlobalSearchIterator,
RoomSearchIterator as SdkRoomSearchIterator, SearchError as SdkSearchError,
},
};
use matrix_sdk_ui::timeline::TimelineDetails;
use tokio::sync::Mutex;
use crate::{
client::Client,
error::ClientError,
room::Room,
timeline::{ProfileDetails, TimelineItemContent},
utils::Timestamp,
};
#[derive(uniffi::Error, thiserror::Error, Debug)]
pub enum SearchError {
#[error("Failed to search through the index: {0}")]
IndexError(String),
#[error("Failed to load event content for search result: {0}")]
EventLoadError(String),
}
impl From<SdkSearchError> for SearchError {
fn from(err: SdkSearchError) -> Self {
match err {
SdkSearchError::IndexError(err) => SearchError::IndexError(err.to_string()),
SdkSearchError::EventLoadError(err) => SearchError::EventLoadError(err.to_string()),
}
}
}
#[matrix_sdk_ffi_macros::export]
impl Room {
/// Search for messages in this room matching the given query, returning an
/// iterator over the results that yields `num_results_per_batch` results at
/// a time.
pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator {
RoomSearchIterator {
sdk_room: self.inner.clone(),
inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomSearchIterator {
sdk_room: matrix_sdk::room::Room,
inner: Mutex<SdkRoomSearchIterator>,
}
#[matrix_sdk_ffi_macros::export]
impl RoomSearchIterator {
/// Return a list of events for the next batch of search results, or `None`
/// if there are no more results.
pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, SearchError> {
let Some(events) = self.inner.lock().await.next_events().await? else {
return Ok(None);
};
let mut results = Vec::with_capacity(events.len());
for event in events {
if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await {
results.push(result);
}
}
results.shrink_to_fit();
Ok(Some(results))
}
}
#[derive(Clone, uniffi::Record)]
pub struct RoomSearchResult {
event_id: String,
sender: String,
sender_profile: ProfileDetails,
content: TimelineItemContent,
timestamp: Timestamp,
}
impl RoomSearchResult {
async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> {
let sender = event.sender()?;
let event_id = event.event_id().unwrap().to_string();
let timestamp =
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();
let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?;
let profile = TimelineDetails::from_initial_value(
matrix_sdk_ui::timeline::Profile::load(room, &sender).await,
);
Some(Self {
event_id,
sender: sender.to_string(),
sender_profile: ProfileDetails::from(profile),
content: TimelineItemContent::from(content),
timestamp,
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum SearchRoomFilter {
/// All the joined rooms (= DMs + non-DMs).
Rooms,
/// Only joined DM rooms.
Dms,
/// Only joined non-DM (group) rooms.
NonDms,
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Search across all all rooms for the given query, returning an iterator
/// over the results.
pub async fn search_messages(
&self,
query: String,
filter: SearchRoomFilter,
num_results_per_batch: u32,
) -> Result<GlobalSearchIterator, ClientError> {
let sdk_client = (*self.inner).clone();
let mut search = sdk_client.search_messages(query, num_results_per_batch as usize);
match filter {
SearchRoomFilter::Rooms => {}
SearchRoomFilter::Dms => search = search.only_dm_rooms().await?,
SearchRoomFilter::NonDms => search = search.no_dms().await?,
}
Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) })
}
}
#[derive(uniffi::Record)]
pub struct GlobalSearchResult {
room_id: String,
result: RoomSearchResult,
}
#[derive(uniffi::Object)]
pub struct GlobalSearchIterator {
sdk_client: matrix_sdk::Client,
inner: Mutex<SdkGlobalSearchIterator>,
}
#[matrix_sdk_ffi_macros::export]
impl GlobalSearchIterator {
/// Return a list of events for the next batch of search results, or `None`
/// if there are no more results.
pub async fn next_events(&self) -> Result<Option<Vec<GlobalSearchResult>>, SearchError> {
let Some(events) = self.inner.lock().await.next_events().await? else {
return Ok(None);
};
let mut results = Vec::with_capacity(events.len());
for (room_id, event) in events {
let Some(room) = self.sdk_client.get_room(&room_id) else {
continue;
};
if let Some(result) = RoomSearchResult::from(&room, event).await {
results.push(GlobalSearchResult { room_id: room_id.to_string(), result });
}
}
results.shrink_to_fit();
Ok(Some(results))
}
}
@@ -16,13 +16,13 @@ use std::sync::{Arc, RwLock};
use futures_util::StreamExt;
use matrix_sdk::{
Account,
encryption::{
Encryption,
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::key::verification::VerificationMethod,
Account,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::UserId;
@@ -246,16 +246,15 @@ impl SessionVerificationController {
sender: &UserId,
flow_id: impl AsRef<str>,
) {
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:?}"
);
return;
}
}
if sender != self.user_identity.user_id()
&& let Some(status) = self.encryption.cross_signing_status().await
&& !status.is_complete()
{
warn!(
"Cannot verify other users until our own device's cross-signing status \
is complete: {status:?}"
);
return;
}
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
@@ -290,15 +289,10 @@ impl SessionVerificationController {
) -> Result<(), ClientError> {
if let Some(ongoing_verification_request) =
self.verification_request.read().unwrap().clone()
&& !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::from_str(
"There is another verification flow ongoing.",
None,
));
}
return Err(ClientError::from_str("There is another verification flow ongoing.", None));
}
*self.verification_request.write().unwrap() = Some(verification_request.clone());
+4 -4
View File
@@ -15,23 +15,23 @@
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
SpaceService as UISpaceService,
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
};
use ruma::RoomId;
use crate::{
TaskHandle,
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_preview::RoomType,
runtime::get_runtime_handle,
TaskHandle,
};
/// The main entry point into the Spaces facilities.
+24 -2
View File
@@ -26,8 +26,8 @@ use matrix_sdk_ui::{
};
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle, TaskHandle,
TaskHandle, error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle,
};
#[derive(uniffi::Enum)]
@@ -128,6 +128,28 @@ impl SyncServiceBuilder {
Arc::new(Self { builder, ..this })
}
/// Set a custom Sliding Sync connection ID for the room list service.
///
/// By default [`matrix_sdk_ui::room_list_service::DEFAULT_CONNECTION_ID`]
/// is used. Set a different value for secondary processes such as iOS
/// Share Extensions that are not meant to reuse the main app's
/// connection.
pub fn with_room_list_connection_id(self: Arc<Self>, connection_id: String) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_room_list_conn_id(connection_id);
Arc::new(Self { builder, ..this })
}
/// Set a custom timeline limit for the room list service.
///
/// When set, overrides the default timeline limit of
/// [`matrix_sdk_ui::room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
pub fn with_room_list_timeline_limit(self: Arc<Self>, limit: u32) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_room_list_timeline_limit(limit);
Arc::new(Self { builder, ..this })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService {
@@ -15,12 +15,12 @@
use std::sync::Arc;
use matrix_sdk_ui::timeline::{
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
};
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
EventId,
events::{AnySyncTimelineEvent, TimelineEventType},
};
use super::FocusEventError;
@@ -17,7 +17,7 @@ use std::collections::HashMap;
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
use ruma::events::{
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, StateEventContentChange,
StateEventContentChange, room::history_visibility::HistoryVisibility as RumaHistoryVisibility,
};
use crate::{
@@ -40,7 +40,9 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
Content::CallInvite => TimelineItemContent::CallInvite,
Content::RtcNotification => TimelineItemContent::RtcNotification,
Content::RtcNotification { call_intent } => TimelineItemContent::RtcNotification {
call_intent: call_intent.map(|s| s.to_string()),
},
Content::MembershipChange(membership) => {
let reason = match membership.content() {
@@ -159,7 +161,9 @@ pub enum TimelineItemContent {
content: MsgLikeContent,
},
CallInvite,
RtcNotification,
RtcNotification {
call_intent: Option<String>,
},
RoomMembership {
user_id: String,
user_display_name: Option<String>,
@@ -265,7 +269,6 @@ pub enum OtherState {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar {
url: Option<String>,
},
@@ -363,7 +366,6 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherStateEventContentChange> for OtherSt
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
Content::RoomAliases(_) => Self::RoomAliases,
Content::RoomAvatar(c) => {
let url = match c {
FullContent::Original { content, .. } => {
+26 -7
View File
@@ -38,8 +38,9 @@ use matrix_sdk_ui::timeline::{
use mime::Mime;
use reply::{EmbeddedEventDetails, InReplyToDetails};
use ruma::{
assign,
EventId, UInt, assign,
events::{
AnyMessageLikeEventContent,
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::{
unstable_end::UnstablePollEndEventContent,
@@ -53,9 +54,7 @@ use ruma::{
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
TextMessageEventContent,
},
AnyMessageLikeEventContent,
},
EventId, UInt,
};
use tokio::sync::Mutex;
use tracing::{error, warn};
@@ -284,8 +283,17 @@ impl Timeline {
// be that the listener be called before the initial items have been
// handled by the caller. See #3535 for details.
// First, pass all the items as a reset update.
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
// Note we pass initial items as a reset update, as a way to give the callers a
// unified way to handle the initial batch of items as well as other
// batches, instead of having a separate callback for the initial items.
//
// Start with passing all the items of a *non-empty* timeline as a reset update
// (if the initial items are empty, then the timeline would transition
// from empty to empty, which is a no-op).
if !timeline_items.is_empty() {
listener
.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
}
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(timeline_stream);
@@ -1012,6 +1020,9 @@ pub struct EventTimelineItem {
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
/// The raw Matrix event type string (e.g. `"m.room.message"`), or `None`
/// when the original type is not available (e.g. redacted events).
event_type_raw: Option<String>,
timestamp: Timestamp,
local_send_state: Option<EventSendState>,
local_created_at: Option<u64>,
@@ -1037,6 +1048,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
event_type_raw: item.content().event_type_str(),
timestamp: item.timestamp().into(),
local_send_state: item.send_state().map(|s| s.into()),
local_created_at: item.local_created_at().map(|t| t.0.into()),
@@ -1314,6 +1326,13 @@ impl LazyTimelineItemProvider {
fn contains_only_emojis(&self) -> bool {
self.0.contains_only_emojis()
}
/// Returns the full raw JSON string of the latest version of the event
/// (including edits). Returns `None` for local echoes that haven't been
/// echoed back by the server yet.
fn latest_json(&self) -> Option<String> {
Some(self.0.latest_json()?.json().get().to_owned())
}
}
/// Mimic the [`UiLatestEventValue`] type.
@@ -1387,7 +1406,7 @@ mod galleries {
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
use ruma::{EventId, assign, events::room::message::TextMessageEventContent};
use tokio::sync::Mutex;
use tracing::error;
@@ -1395,7 +1414,7 @@ mod galleries {
error::RoomError,
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
runtime::get_runtime_handle,
timeline::{build_thumbnail_info, Timeline, UploadSource},
timeline::{Timeline, UploadSource, build_thumbnail_info},
};
#[derive(uniffi::Record)]
@@ -15,7 +15,7 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
use ruma::events::{MessageLikeEventContent, room::MediaSource as RumaMediaSource};
use super::{
content::{BeaconInfo, LiveLocationContent, Reaction},
@@ -14,7 +14,7 @@
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
use super::{ProfileDetails, content::TimelineItemContent};
use crate::{event::EventOrTransactionId, utils::Timestamp};
#[derive(Clone, uniffi::Object)]
@@ -21,22 +21,22 @@ use matrix_sdk::room::{
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::timeline::{
RoomExt,
thread_list_service::{
ThreadListItem as UIThreadListItem, ThreadListItemEvent as UIThreadListItemEvent,
ThreadListPaginationState as UIThreadListPaginationState,
ThreadListService as UIThreadListService,
ThreadListServiceError as UIThreadListServiceError,
},
RoomExt,
};
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
use crate::{
TaskHandle,
error::ClientError,
runtime::get_runtime_handle,
timeline::{ProfileDetails, TimelineItemContent},
utils::Timestamp,
TaskHandle,
};
/// A thread subscription (MSC4306).
+4 -4
View File
@@ -41,10 +41,10 @@ impl UnableToDecryptHook for UtdHook {
// 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;
}
if let Some(duration) = &info.time_to_decrypt
&& *duration < IGNORE_UTD_PERIOD
{
return;
}
// Report the UTD to the client.
+8
View File
@@ -266,6 +266,7 @@ pub fn get_element_call_required_permissions(
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
download_files: true,
}
}
@@ -331,6 +332,8 @@ pub struct WidgetCapabilities {
pub update_delayed_event: bool,
/// This allows the widget to send events with a delay.
pub send_delayed_event: bool,
/// This allows the widget to download files (avatars)
pub download_files: bool,
}
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
@@ -341,6 +344,7 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
download_file: value.download_files,
}
}
}
@@ -353,6 +357,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
download_files: value.download_file,
}
}
}
@@ -553,5 +558,8 @@ mod tests {
// RTC decline
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
// Download avatars
cap_assert("org.matrix.msc4039.download_file");
}
}
+1
View File
@@ -52,6 +52,7 @@ All notable changes to this project will be documented in this file.
### Refactor
- [**breaking**] `TtlStoreValue` was moved and renamed to `matrix_sdk_common::ttl_cache::TtlValue`.
- [**breaking**] `Gap::prev_token` has been renamed to `Gap::token` since it's now used for both
the previous batch token and the next batch token.
([#6236](https://github.com/matrix-org/matrix-rust-sdk/pull/6236))
+7
View File
@@ -40,6 +40,12 @@ experimental-encrypted-state-events = [
"matrix-sdk-crypto?/experimental-encrypted-state-events"
]
# Enable experimental support for pushing secrets
experimental-push-secrets = [
"e2e-encryption",
"matrix-sdk-crypto?/experimental-push-secrets"
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
@@ -113,6 +119,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
wasm-bindgen-test.workspace = true
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
gloo-timers = { workspace = true, features = ["futures"] }
[lints]
@@ -202,7 +202,6 @@ fn update_push_room_context(
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
// TODO: Use if let chain once stable
if let Some(member) = context.state_changes.member(room_id, user_id) {
push_rules.user_display_name =
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
@@ -230,7 +229,6 @@ pub async fn get_push_room_context(
let member_count = room_info.active_members_count();
// TODO: Use if let chain once stable
let user_display_name = if let Some(member) = context.state_changes.member(room_id, user_id) {
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
@@ -8,6 +8,7 @@ use std::{
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_common::ttl_cache::TtlValue;
use matrix_sdk_test::{TestResult, event_factory::EventFactory};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, TransactionId, UserId,
@@ -46,7 +47,7 @@ use serde_json::json;
use super::{
DependentQueuedRequestKind, DisplayName, DynStateStore, RoomLoadSettings,
SupportedVersionsResponse, TtlStoreValue, WellKnownResponse, send_queue::SentRequestKey,
SupportedVersionsResponse, WellKnownResponse, send_queue::SentRequestKey,
};
use crate::{
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
@@ -531,7 +532,7 @@ impl StateStoreIntegrationTests for DynStateStore {
self.set_kv_data(
StateStoreDataKey::SupportedVersions,
StateStoreDataValue::SupportedVersions(TtlStoreValue::new(supported_versions.clone())),
StateStoreDataValue::SupportedVersions(TtlValue::new(supported_versions.clone())),
)
.await?;
@@ -539,7 +540,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::SupportedVersions(stored_supported_versions))) =
self.get_kv_data(StateStoreDataKey::SupportedVersions).await
);
assert_let!(Some(stored_supported_versions) = stored_supported_versions.into_data());
let stored_supported_versions = stored_supported_versions.into_data();
assert_eq!(supported_versions, stored_supported_versions);
let stored_supported = stored_supported_versions.supported_versions();
@@ -563,7 +564,7 @@ impl StateStoreIntegrationTests for DynStateStore {
self.set_kv_data(
StateStoreDataKey::WellKnown,
StateStoreDataValue::WellKnown(TtlStoreValue::new(Some(well_known.clone()))),
StateStoreDataValue::WellKnown(TtlValue::new(Some(well_known.clone()))),
)
.await?;
@@ -571,7 +572,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
self.get_kv_data(StateStoreDataKey::WellKnown).await
);
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
let stored_well_known = stored_well_known.into_data();
assert_eq!(stored_well_known, Some(well_known));
self.remove_kv_data(StateStoreDataKey::WellKnown).await?;
@@ -579,7 +580,7 @@ impl StateStoreIntegrationTests for DynStateStore {
self.set_kv_data(
StateStoreDataKey::WellKnown,
StateStoreDataValue::WellKnown(TtlStoreValue::new(None)),
StateStoreDataValue::WellKnown(TtlValue::new(None)),
)
.await?;
@@ -587,7 +588,7 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
self.get_kv_data(StateStoreDataKey::WellKnown).await
);
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
let stored_well_known = stored_well_known.into_data();
assert_eq!(stored_well_known, None);
Ok(())
@@ -20,10 +20,11 @@ use std::{
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, ttl_cache::TtlValue};
use ruma::{
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
api::client::discovery::get_capabilities::v3::Capabilities,
canonical_json::{RedactedBecause, redact},
events::{
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
@@ -40,7 +41,7 @@ use tracing::{debug, instrument, warn};
use super::{
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
RoomLoadSettings, StateChanges, StateStore, StoreError, SupportedVersionsResponse,
TtlStoreValue, WellKnownResponse,
WellKnownResponse,
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
traits::ComposerDraft,
};
@@ -60,8 +61,8 @@ struct MemoryStoreInner {
composer_drafts: HashMap<(OwnedRoomId, Option<OwnedEventId>), ComposerDraft>,
user_avatar_url: HashMap<OwnedUserId, OwnedMxcUri>,
sync_token: Option<String>,
supported_versions: Option<TtlStoreValue<SupportedVersionsResponse>>,
well_known: Option<TtlStoreValue<Option<WellKnownResponse>>>,
supported_versions: Option<TtlValue<SupportedVersionsResponse>>,
well_known: Option<TtlValue<Option<WellKnownResponse>>>,
filters: HashMap<String, String>,
utd_hook_manager_data: Option<GrowableBloom>,
one_time_key_uploaded_error: bool,
@@ -92,6 +93,7 @@ struct MemoryStoreInner {
seen_knock_requests: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, OwnedUserId>>,
thread_subscriptions: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, StoredThreadSubscription>>,
thread_subscriptions_catchup_tokens: Option<Vec<ThreadSubscriptionCatchupToken>>,
homeserver_capabilities: Option<TtlValue<Capabilities>>,
}
/// In-memory, non-persistent implementation of the `StateStore`.
@@ -195,6 +197,10 @@ impl StateStore for MemoryStore {
.thread_subscriptions_catchup_tokens
.clone()
.map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens),
StateStoreDataKey::HomeserverCapabilities => inner
.homeserver_capabilities
.clone()
.map(StateStoreDataValue::HomeserverCapabilities),
})
}
@@ -270,6 +276,13 @@ impl StateStore for MemoryStore {
"Session data is not a list of thread subscription catchup tokens",
));
}
StateStoreDataKey::HomeserverCapabilities => {
inner.homeserver_capabilities = Some(
value
.into_homeserver_capabilities()
.expect("Session data is not a homeserver capabilities"),
);
}
}
Ok(())
@@ -304,6 +317,7 @@ impl StateStore for MemoryStore {
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
inner.thread_subscriptions_catchup_tokens = None;
}
StateStoreDataKey::HomeserverCapabilities => inner.homeserver_capabilities = None,
}
Ok(())
}
+1 -1
View File
@@ -95,7 +95,7 @@ pub use self::{
traits::{
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken, TtlStoreValue,
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken,
WellKnownResponse,
},
};
+24 -1
View File
@@ -117,6 +117,14 @@ pub enum QueuedRequestKind {
#[serde(default)]
accumulated: Vec<AccumulatedSentMediaInfo>,
},
/// A redaction of another event to send.
Redaction {
/// The ID of the event to redact.
redacts: OwnedEventId,
/// The reason for the event being redacted.
reason: Option<String>,
},
}
impl From<SerializableEventContent> for QueuedRequestKind {
@@ -421,12 +429,27 @@ pub enum SentRequestKey {
/// The parent transaction returned an uploaded resource URL.
Media(SentMediaInfo),
/// The parent transaction returned a redaction event when it succeeded.
Redaction {
/// The event ID returned by the server.
event_id: OwnedEventId,
/// The ID of the redacted event.
redacts: OwnedEventId,
/// The reason for the event being redacted.
reason: Option<String>,
},
}
impl SentRequestKey {
/// Converts the current parent key into an event id, if possible.
pub fn into_event_id(self) -> Option<OwnedEventId> {
as_variant!(self, Self::Event { event_id, .. } => event_id)
match self {
Self::Event { event_id, .. } | Self::Redaction { event_id, .. } => Some(event_id),
_ => None,
}
}
/// Converts the current parent key into information about a sent media, if
+39 -97
View File
@@ -22,14 +22,17 @@ use std::{
use as_variant::as_variant;
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloom;
use matrix_sdk_common::AsyncTraitDeps;
use matrix_sdk_common::{AsyncTraitDeps, ttl_cache::TtlValue};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId,
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
api::{
SupportedVersions,
client::discovery::discover_homeserver::{
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
MatrixVersion, SupportedVersions,
client::discovery::{
discover_homeserver::{
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
},
get_capabilities::v3::Capabilities,
},
},
events::{
@@ -41,7 +44,6 @@ use ruma::{
receipt::{Receipt, ReceiptThread, ReceiptType},
},
serde::Raw,
time::SystemTime,
};
use serde::{Deserialize, Serialize};
@@ -258,7 +260,7 @@ pub trait StateStore: AsyncTraitDeps {
event_type: RoomAccountDataEventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Self::Error>;
/// Get an event out of the user room receipt store.
/// Get a user's read receipt for a given room and receipt type and thread.
///
/// # Arguments
///
@@ -269,7 +271,7 @@ pub trait StateStore: AsyncTraitDeps {
///
/// * `thread` - The thread containing this receipt.
///
/// * `user_id` - The id of the user for who the receipt should be fetched.
/// * `user_id` - The id of the user for whom the receipt should be fetched.
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
@@ -278,7 +280,7 @@ pub trait StateStore: AsyncTraitDeps {
user_id: &UserId,
) -> Result<Option<(OwnedEventId, Receipt)>, Self::Error>;
/// Get events out of the event room receipt store.
/// Get an event's read receipts for a given room, receipt type, and thread.
///
/// # Arguments
///
@@ -1041,37 +1043,6 @@ where
}
}
/// A TTL value in the store whose data can only be accessed before it expires.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlStoreValue<T> {
/// The data of the item.
#[serde(flatten)]
data: T,
/// Last time we fetched this data from the server, in milliseconds since
/// epoch.
last_fetch_ts: f64,
}
impl<T> TtlStoreValue<T> {
/// The number of milliseconds after which the data is considered stale.
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
/// Construct a new `TtlStoreValue` with the given data.
pub fn new(data: T) -> Self {
Self { data, last_fetch_ts: now_timestamp_ms() }
}
/// Get the data of this value, if it hasn't expired.
pub fn into_data(self) -> Option<T> {
if now_timestamp_ms() - self.last_fetch_ts >= Self::STALE_THRESHOLD {
None
} else {
Some(self.data)
}
}
}
/// Serialisable representation of get_supported_versions::Response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SupportedVersionsResponse {
@@ -1089,7 +1060,16 @@ impl SupportedVersionsResponse {
/// Note: Matrix versions and features that Ruma cannot parse, or does not
/// know about, are discarded.
pub fn supported_versions(&self) -> SupportedVersions {
SupportedVersions::from_parts(&self.versions, &self.unstable_features)
let mut supported_versions =
SupportedVersions::from_parts(&self.versions, &self.unstable_features);
// We need at least one supported version to be able to make requests, so we
// default to Matrix 1.0.
if supported_versions.versions.is_empty() {
supported_versions.versions.insert(MatrixVersion::V1_0);
}
supported_versions
}
}
@@ -1120,15 +1100,6 @@ impl From<discover_homeserver::Response> for WellKnownResponse {
}
}
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
fn now_timestamp_ms() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("System clock was before 1970.")
.as_secs_f64()
* 1000.0
}
/// A value for key-value data that should be persisted into the store.
#[derive(Debug, Clone)]
pub enum StateStoreDataValue {
@@ -1136,10 +1107,10 @@ pub enum StateStoreDataValue {
SyncToken(String),
/// The supported versions of the server.
SupportedVersions(TtlStoreValue<SupportedVersionsResponse>),
SupportedVersions(TtlValue<SupportedVersionsResponse>),
/// The well-known information of the server.
WellKnown(TtlStoreValue<Option<WellKnownResponse>>),
WellKnown(TtlValue<Option<WellKnownResponse>>),
/// A filter with the given ID.
Filter(String),
@@ -1172,6 +1143,9 @@ pub enum StateStoreDataValue {
/// See documentation of [`ThreadSubscriptionCatchupToken`] for more
/// details.
ThreadSubscriptionsCatchupTokens(Vec<ThreadSubscriptionCatchupToken>),
/// The capabilities the homeserver supports or disables.
HomeserverCapabilities(TtlValue<Capabilities>),
}
/// Tokens to use when catching up on thread subscriptions.
@@ -1352,12 +1326,12 @@ impl StateStoreDataValue {
}
/// Get this value if it is the supported versions metadata.
pub fn into_supported_versions(self) -> Option<TtlStoreValue<SupportedVersionsResponse>> {
pub fn into_supported_versions(self) -> Option<TtlValue<SupportedVersionsResponse>> {
as_variant!(self, Self::SupportedVersions)
}
/// Get this value if it is the well-known metadata.
pub fn into_well_known(self) -> Option<TtlStoreValue<Option<WellKnownResponse>>> {
pub fn into_well_known(self) -> Option<TtlValue<Option<WellKnownResponse>>> {
as_variant!(self, Self::WellKnown)
}
@@ -1373,6 +1347,12 @@ impl StateStoreDataValue {
) -> Option<Vec<ThreadSubscriptionCatchupToken>> {
as_variant!(self, Self::ThreadSubscriptionsCatchupTokens)
}
/// Get this value if it is the data for the capabilities the homeserver
/// supports or disables.
pub fn into_homeserver_capabilities(self) -> Option<TtlValue<Capabilities>> {
as_variant!(self, Self::HomeserverCapabilities)
}
}
/// A key for key-value data.
@@ -1415,6 +1395,9 @@ pub enum StateStoreDataKey<'a> {
/// A list of thread subscriptions catchup tokens.
ThreadSubscriptionsCatchupTokens,
/// A list of capabilities that the homeserver supports.
HomeserverCapabilities,
}
impl StateStoreDataKey<'_> {
@@ -1460,6 +1443,9 @@ impl StateStoreDataKey<'_> {
/// [`ThreadSubscriptionsCatchupTokens`][Self::ThreadSubscriptionsCatchupTokens] variant.
pub const THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS: &'static str =
"thread_subscriptions_catchup_tokens";
/// Key prefix to use for the homeserver's [`Capabilities`].
pub const HOMESERVER_CAPABILITIES: &'static str = "homeserver_capabilities";
}
/// Compare two thread subscription changes bump stamps, given a fixed room and
@@ -1492,47 +1478,3 @@ pub fn compare_thread_subscription_bump_stamps(
true
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{SupportedVersionsResponse, TtlStoreValue, now_timestamp_ms};
#[test]
fn test_stale_ttl_store_value() {
// Definitely stale.
let ttl_value = TtlStoreValue {
data: (),
last_fetch_ts: now_timestamp_ms() - TtlStoreValue::<()>::STALE_THRESHOLD - 1.0,
};
assert!(ttl_value.into_data().is_none());
// Definitely not stale.
let ttl_value = TtlStoreValue::new(());
assert!(ttl_value.into_data().is_some());
}
#[test]
fn test_stale_ttl_store_value_serialize_roundtrip() {
let server_info = SupportedVersionsResponse {
versions: vec!["1.2".to_owned(), "1.3".to_owned(), "1.4".to_owned()],
unstable_features: [("org.matrix.msc3916.stable".to_owned(), true)].into(),
};
let ttl_value = TtlStoreValue { data: server_info.clone(), last_fetch_ts: 1000.0 };
let json = json!({
"versions": ["1.2", "1.3", "1.4"],
"unstable_features": {
"org.matrix.msc3916.stable": true,
},
"last_fetch_ts": 1000.0,
});
assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
let deserialized =
serde_json::from_value::<TtlStoreValue<SupportedVersionsResponse>>(json).unwrap();
assert_eq!(deserialized.data, server_info);
assert!(deserialized.last_fetch_ts - ttl_value.last_fetch_ts < 0.0001);
}
}
+13 -2
View File
@@ -7,12 +7,23 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Features
- [**breaking**] Change to the stable identifiers for `m.history_not_shared`.
We still support reading the unstable identifier.
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
- Add a method to check the validity of edits.
([#6454](https://github.com/matrix-org/matrix-rust-sdk/pull/6454))
- A background task monitor has been added, that can spawn background tasks and monitor their
execution on a separate channel. Such tasks can run forever, or they can run for one-shot jobs.
([#6075](https://github.com/matrix-org/matrix-rust-sdk/pull/6075) &&
[#6421](https://github.com/matrix-org/matrix-rust-sdk/pull/6421))
- Add `AcquireCrossProcessLockResult` and `AcquireCrossProcessLockFn`
for convenience in generalizing cross-process lock acquisition.
([#6326](https://github.com/matrix-org/matrix-rust-sdk/pull/6326))
- Add support in the `MemoryStore`'s implementation of `EventCacheStore` for
having duplicate events in a room, where each duplicate is in a different
`LinkedChunk`. This is useful, e.g., when an event is in a room and a
thread in that room.
- [**breaking**] In order to support having duplicate events in the same room (in different `LinkedChunk`'s) a few
functions were changed in `RelationalLinkedChunk`. The items in the `Iterator` returned by `RelationalLinkedChunk::items`
now also include the `LinkedChunkId` in which the `Item` was found. Additionally, `RelationalLinkedChunk::save_item`
+1
View File
@@ -66,6 +66,7 @@ wasm-bindgen-test.workspace = true
[target.'cfg(target_family = "wasm")'.dev-dependencies]
# Enable the JS feature for getrandom.
getrandom = { workspace = true, default-features = false, features = ["wasm_js"] }
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
js-sys.workspace = true
[lints]
@@ -62,6 +62,31 @@ use crate::{
/// This is used to know if a lock has been dirtied.
pub type CrossProcessLockGeneration = u64;
/// A trait that represents any function which can be used to
/// acquire the underlying lock of a [`CrossProcessLock`].
///
/// For example, this can be useful when writing a function which
/// is parameterized to acquire the underlying lock through either
/// [`CrossProcessLock::spin_lock`] or [`CrossProcessLock::try_lock_once`].
pub trait AcquireCrossProcessLockFn<L>
where
Self: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
L: TryLock + Clone + SendOutsideWasm + 'static,
{
}
impl<L, T> AcquireCrossProcessLockFn<L> for T
where
T: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
L: TryLock + Clone + SendOutsideWasm + 'static,
{
}
/// A convenience type for the [`Result`] returned from calling
/// or [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
pub type AcquireCrossProcessLockResult<E> =
Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, E>;
/// Trait used to try to take a lock. Foundation of [`CrossProcessLock`].
pub trait TryLock {
#[cfg(not(target_family = "wasm"))]
@@ -276,10 +301,8 @@ where
///
/// The lock can be obtained but it can be dirty. In all cases, the renew
/// task will run in the background.
#[instrument(skip(self), fields(?self.lock_key, ?self.config))]
pub async fn try_lock_once(
&self,
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
#[instrument(skip(self), fields(?self.lock_key, ?self.config, ?self.generation))]
pub async fn try_lock_once(&self) -> AcquireCrossProcessLockResult<L::LockError> {
// If it's not `MultiProcess`, this behaves as a no-op
let CrossProcessLockConfig::MultiProcess { holder_name } = &self.config else {
let guard = CrossProcessLockGuard::new(self.num_holders.clone(), self.is_dirty.clone());
@@ -447,7 +470,7 @@ where
pub async fn spin_lock(
&self,
max_backoff: Option<u32>,
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
) -> AcquireCrossProcessLockResult<L::LockError> {
// If there is no holder, this behaves as a no-op
let max_backoff = max_backoff.unwrap_or(MAX_BACKOFF_MS);
@@ -1249,7 +1249,7 @@ pub enum WithheldCode {
/// that the session was not marked as "shared_history".
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
#[ruma_enum(rename = "io.element.msc4268.history_not_shared", alias = "m.history_not_shared")]
#[ruma_enum(rename = "m.history_not_shared", alias = "io.element.msc4268.history_not_shared")]
HistoryNotShared,
#[doc(hidden)]
@@ -0,0 +1,194 @@
// Copyright 2026 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::AnySyncTimelineEvent, serde::Raw};
use serde::Deserialize;
use crate::deserialized_responses::EncryptionInfo;
/// Represents all possible validation errors that can occur when processing
/// an event edit.
///
/// These errors ensure that a replacement event complies with the rules
/// required to safely and correctly modify an existing event.
///
/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
#[derive(Debug, thiserror::Error)]
pub enum EditValidityError {
/// Occurs when the sender of the replacement event does not match
/// the sender of the original event.
///
/// Only the original sender is allowed to edit their own event.
#[error(
"the sender of the original event isn't the same as the sender of the replacement event"
)]
InvalidSender,
/// Occurs when either the original event or the replacement event contains
/// a state key.
///
/// State events are not allowed to be edited.
#[error("the original event or the replacement event contains a state key")]
StateKeyPresent,
/// Occurs when the content type of the original event differs from
/// that of the replacement event.
///
/// Edits must not change the events content type, as this would
/// introduce semantic inconsistencies.
#[error(
"the content type of the original event is `{content_type}` while the replacement is a `{replacement_type}`"
)]
MismatchContentType {
/// The content type of the original event.
content_type: String,
/// The content type of the replacement event.
replacement_type: String,
},
/// Occurs when the original event is itself already a replacement (edit).
#[error("the original event is an edit as well")]
OriginalEventIsReplacement,
/// Occurs when the replacement event is not a replacement for the original
/// event.
#[error("the replacement event is not a replacement for the original event")]
NotReplacement,
/// Occurs when a required field is missing from either the original
/// or the replacement event.
///
/// The event is considered malformed and cannot be validated.
#[error("the event was encrypted, as such it should have an `m.new_content` field")]
MissingNewContent,
#[error(transparent)]
InvalidJson(#[from] serde_json::Error),
/// Occurs when the original event is encrypted but the replacement
/// event is not.
#[error("the original event was encrypted while the replacement is not")]
ReplacementNotEncrypted,
}
/// This implements the Matrix spec rule set for validity of replacement events
/// (edits). Invalid replacements must be ignored.
///
/// This function implements the steps documented in the [spec] with one
/// exception, the step to check if the room IDs match isn't done. The JSON of
/// the event might not contain the room ID if it wasn received over a `/sync`
/// request.
///
/// *Warning*: Callers must ensure that the original event and replacement event
/// belong to the same room, that is, they have the same room ID.
///
/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
pub fn check_validity_of_replacement_events(
original_json: &Raw<AnySyncTimelineEvent>,
original_encryption_info: Option<&EncryptionInfo>,
replacement_json: &Raw<AnySyncTimelineEvent>,
replacement_encryption_info: Option<&EncryptionInfo>,
) -> Result<(), EditValidityError> {
const REPLACEMENT_REL_TYPE: &str = "m.replace";
#[derive(Debug, Deserialize)]
struct MinimalEvent<'a> {
sender: &'a str,
event_id: &'a str,
#[serde(rename = "type")]
event_type: &'a str,
state_key: Option<&'a str>,
content: MinimalContent<'a>,
}
#[derive(Debug, Deserialize)]
struct MinimalContent<'a> {
#[serde(borrow, rename = "m.relates_to")]
relates_to: Option<MinimalRelatesTo<'a>>,
#[serde(rename = "m.new_content")]
new_content: Option<MinimalNewContent>,
}
#[derive(Debug, Deserialize)]
struct MinimalNewContent {}
#[derive(Debug, Deserialize)]
struct MinimalRelatesTo<'a> {
rel_type: Option<&'a str>,
event_id: Option<&'a str>,
}
let original_event = original_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
let replacement_event = replacement_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
// We don't check the room ID here since this event might have been received
// over /sync, in this case the JSON likely won't contain the room ID field.
// The original event and replacement event must have the same sender (i.e. you
// cannot edit someone elses messages).
if original_event.sender != replacement_event.sender {
return Err(EditValidityError::InvalidSender);
}
// This check isn't part of the list in the spec, but it makes sense to check if
// the replacement event is has the correct rel_type and if it's an edit for the
// original event.
if let Some(relates_to) = replacement_event.content.relates_to {
if relates_to.rel_type != Some(REPLACEMENT_REL_TYPE)
|| relates_to.event_id != Some(original_event.event_id)
{
return Err(EditValidityError::NotReplacement);
}
} else {
return Err(EditValidityError::NotReplacement);
}
// The replacement and original events must have the same type (i.e. you cannot
// change the original events type).
if original_event.event_type != replacement_event.event_type {
return Err(EditValidityError::MismatchContentType {
content_type: original_event.event_type.to_owned(),
replacement_type: replacement_event.event_type.to_owned(),
});
}
// The replacement and original events must not have a state_key property (i.e.
// you cannot edit state events at all).
if original_event.state_key.is_some() || replacement_event.state_key.is_some() {
return Err(EditValidityError::StateKeyPresent);
}
// The original event must not, itself, have a rel_type of m.replace (i.e. you
// cannot edit an edit — though you can send multiple edits for a single
// original event).
if let Some(relates_to) = original_event.content.relates_to
&& relates_to.rel_type == Some(REPLACEMENT_REL_TYPE)
{
return Err(EditValidityError::OriginalEventIsReplacement);
}
// The replacement event (once decrypted, if appropriate) must have an
// m.new_content property.
if replacement_encryption_info.is_some() && replacement_event.content.new_content.is_none() {
return Err(EditValidityError::MissingNewContent);
}
// If the original event was encrypted, the replacement should be too.
if original_encryption_info.is_some() && replacement_encryption_info.is_none() {
return Err(EditValidityError::ReplacementNotEncrypted);
}
Ok(())
}
+4 -1
View File
@@ -17,6 +17,8 @@
use std::pin::Pin;
use ruma::{RoomVersionId, room_version_rules::RoomVersionRules};
#[cfg(test)]
matrix_sdk_test_utils::init_tracing_for_tests!();
@@ -27,6 +29,7 @@ pub use ruma;
pub mod cross_process_lock;
pub mod debug;
pub mod deserialized_responses;
mod edit_validation;
pub mod executor;
pub mod failures_cache;
pub mod linked_chunk;
@@ -47,7 +50,7 @@ pub mod ttl_cache;
pub mod js_tracing;
pub use cross_process_lock::LEASE_DURATION_MS;
use ruma::{RoomVersionId, room_version_rules::RoomVersionRules};
pub use edit_validation::*;
/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on
/// wasm.
+89 -11
View File
@@ -25,7 +25,7 @@
//! let mut failures = monitor.subscribe();
//!
//! // Spawn a monitored background task
//! let handle = monitor.spawn_background_task("my_task", async {
//! let handle = monitor.spawn_infinite_task("my_task", async {
//! loop {
//! // Do background work...
//! matrix_sdk_common::sleep::sleep(std::time::Duration::from_secs(1))
@@ -183,7 +183,7 @@ const FAILURE_CHANNEL_CAPACITY: usize = 8;
/// let mut failures = monitor.subscribe();
///
/// // Spawn a task that runs indefinitely
/// let _handle = monitor.spawn_background_task("worker", async {
/// let _handle = monitor.spawn_infinite_task("worker", async {
/// loop {
/// // Do work...
/// matrix_sdk_common::sleep::sleep(std::time::Duration::from_secs(1))
@@ -223,7 +223,10 @@ impl TaskMonitor {
self.failure_sender.subscribe()
}
/// Spawn a background task that is expected to run indefinitely.
/// Spawn a background task that is expected to **run forever**.
///
/// For one-off background tasks that are expected to complete successfully,
/// use [`Self::spawn_finite_task`] instead.
///
/// If the task completes (whether successfully or by panicking), it will be
/// reported as a [`BackgroundTaskFailure`] report through the broadcast
@@ -242,10 +245,46 @@ impl TaskMonitor {
///
/// A [`BackgroundTaskHandle`] that can be used to abort the task or check
/// if it has finished. This is the equivalent of tokio's `JoinHandle`.
pub fn spawn_background_task<F>(
pub fn spawn_infinite_task<F>(&self, name: impl Into<String>, future: F) -> BackgroundTaskHandle
where
F: Future<Output = ()> + SendOutsideWasm + 'static,
{
self.spawn_task_internal(name, future, true)
}
/// Spawn a background job that is expected to run once and complete
/// successfully in the background.
///
/// For long-term background jobs that are expected to run forever, use
/// [`Self::spawn_infinite_task`] instead.
///
/// If the task completes (by panicking), it will be reported as a
/// [`BackgroundTaskFailure`] report through the broadcast channel.
///
/// Use this for one-shot background tasks that should complete under normal
/// operation.
///
/// # Arguments
///
/// * `name` - A human-readable name for the task (for debugging purposes).
/// * `future` - The async task to run.
///
/// # Returns
///
/// A [`BackgroundTaskHandle`] that can be used to abort the task or check
/// if it has finished. This is the equivalent of tokio's `JoinHandle`.
pub fn spawn_finite_task<F>(&self, name: impl Into<String>, future: F) -> BackgroundTaskHandle
where
F: Future<Output = ()> + SendOutsideWasm + 'static,
{
self.spawn_task_internal(name, future, false)
}
fn spawn_task_internal<F>(
&self,
name: impl Into<String>,
future: F,
runs_forever: bool,
) -> BackgroundTaskHandle
where
F: Future<Output = ()> + SendOutsideWasm + 'static,
@@ -274,8 +313,14 @@ impl TaskMonitor {
let failure_reason = match result {
Ok(()) => {
// The task ended, this is considered an early termination.
BackgroundTaskFailureReason::EarlyTermination
if runs_forever {
// The background forever task ended, this is considered an early
// termination.
BackgroundTaskFailureReason::EarlyTermination
} else {
// The task ended successfully, no failure to report.
return;
}
}
Err(panic_payload) => BackgroundTaskFailureReason::Panic {
@@ -474,7 +519,13 @@ fn extract_panic_message(payload: &Box<dyn Any + Send>) -> Option<String> {
#[cfg(test)]
mod tests {
use std::time::Duration;
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use assert_matches::assert_matches;
use matrix_sdk_test_macros::async_test;
@@ -488,7 +539,7 @@ mod tests {
let mut failures = monitor.subscribe();
// Spawn a task that completes immediately.
let _handle = monitor.spawn_background_task("test_task", async {
let _handle = monitor.spawn_infinite_task("test_task", async {
// Completes immediately: this is an "early termination".
});
@@ -509,7 +560,7 @@ mod tests {
let mut failures = monitor.subscribe();
// Spawn a task that panics.
let _handle = monitor.spawn_background_task("panicking_task", async {
let _handle = monitor.spawn_infinite_task("panicking_task", async {
panic!("test panic message");
});
@@ -573,7 +624,7 @@ mod tests {
let mut failures = monitor.subscribe();
// Spawn a long-running task.
let handle = monitor.spawn_background_task("aborted_task", async {
let handle = monitor.spawn_infinite_task("aborted_task", async {
loop {
sleep(Duration::from_secs(10)).await;
}
@@ -599,7 +650,7 @@ mod tests {
// Spawn a long-running task.
let handle = monitor
.spawn_background_task("aborted_task", async {
.spawn_infinite_task("aborted_task", async {
loop {
sleep(Duration::from_secs(10)).await;
}
@@ -616,4 +667,31 @@ mod tests {
let result = timeout(failures.recv(), Duration::from_millis(100)).await;
assert!(result.is_err(), "should timeout, no failure expected for abort");
}
#[async_test]
async fn test_spawn_finite_task() {
let monitor = TaskMonitor::new();
let mut failures = monitor.subscribe();
let successful_completion = Arc::new(AtomicBool::new(false));
// Spawn a one-off background job that completes successfully.
let successful_completion_clone = successful_completion.clone();
let _handle = monitor.spawn_finite_task("one-shot job", async move {
sleep(Duration::from_millis(10)).await;
successful_completion_clone.store(true, Ordering::SeqCst);
});
// Give the task time to finish.
sleep(Duration::from_millis(20)).await;
// Should NOT receive a failure for successful completion.
let result = timeout(failures.recv(), Duration::from_millis(100)).await;
assert!(result.is_err(), "should timeout, no failure expected for abort");
assert!(
successful_completion.load(Ordering::SeqCst),
"background job should have completed successfully"
);
}
}
+142 -2
View File
@@ -17,7 +17,8 @@
use std::{borrow::Borrow, collections::HashMap, hash::Hash, time::Duration};
use ruma::time::Instant;
use ruma::time::{Instant, SystemTime};
use serde::{Deserialize, Serialize};
// One day is the default lifetime.
const DEFAULT_LIFETIME: Duration = Duration::from_secs(24 * 60 * 60);
@@ -123,10 +124,101 @@ impl<K: Eq + Hash, V: Clone> Default for TtlCache<K, V> {
}
}
/// A value that expires after some time.
///
/// This value is (de)serializable so it can be persisted in a store.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlValue<T> {
/// The data of the item.
#[serde(flatten)]
data: T,
/// Last time we fetched this data from the server, in milliseconds since
/// UNIX epoch.
///
/// When this field is missing during deserialization, it defaults to `0.0`,
/// which means that the data is always expired. This allows to be
/// compatible with data that was persisted before deciding to add an
/// expiration time.
#[serde(default = "default_timestamp")]
last_fetch_ts: Option<f64>,
}
impl<T> TtlValue<T> {
/// The number of milliseconds after which the data is considered stale.
///
/// This matches 1 day.
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24) as _;
/// Construct a new `TtlValue` with the given data.
pub fn new(data: T) -> Self {
Self { data, last_fetch_ts: Some(now_timestamp_ms()) }
}
/// Construct a new `TtlValue` with the given data that never expires.
pub fn without_expiry(data: T) -> Self {
Self { data, last_fetch_ts: None }
}
/// Converts from `&TtlValue<T>` to `TtlValue<&T>`.
pub fn as_ref(&self) -> TtlValue<&T> {
TtlValue { data: &self.data, last_fetch_ts: self.last_fetch_ts }
}
/// Transform the data of this `TtlValue` with the given function.
pub fn map<U, F>(self, f: F) -> TtlValue<U>
where
F: FnOnce(T) -> U,
{
TtlValue { data: f(self.data), last_fetch_ts: self.last_fetch_ts }
}
/// Whether this value has expired.
pub fn has_expired(&self) -> bool {
self.last_fetch_ts.is_some_and(|ts| now_timestamp_ms() - ts >= Self::STALE_THRESHOLD)
}
/// Mark this value has expired.
pub fn expire(&mut self) {
// We assume that the system time is always correct and we are far from the UNIX
// epoch so a timestamp of 0 should always be expired.
self.last_fetch_ts = Some(0.0)
}
/// Get a reference to the data of this value.
pub fn data(&self) -> &T {
&self.data
}
/// Get the data of this value.
pub fn into_data(self) -> T {
self.data
}
}
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
fn now_timestamp_ms() -> f64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("System clock was before 1970.")
.as_secs_f64()
* 1000.0
}
/// The default timestamp if it is missing during deserialization.
///
/// We expect that a value that was serialized always has an expiry time, so the
/// default is `Some(0.0)`.
fn default_timestamp() -> Option<f64> {
Some(0.0)
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::TtlCache;
use super::{TtlCache, TtlValue, now_timestamp_ms};
#[test]
fn test_ttl_cache_insertion() {
@@ -144,4 +236,52 @@ mod tests {
assert!(!cache.contains("A"));
assert!(cache.get("A").is_none(), "The item should have been removed from the cache");
}
#[test]
fn test_ttl_value_expiry() {
// Definitely stale.
let ttl_value = TtlValue {
data: (),
last_fetch_ts: Some(now_timestamp_ms() - TtlValue::<()>::STALE_THRESHOLD - 1.0),
};
assert!(ttl_value.has_expired());
// Definitely not stale.
let ttl_value = TtlValue::new(());
assert!(!ttl_value.has_expired());
// Cannot be stale.
let ttl_value = TtlValue::without_expiry(());
assert!(!ttl_value.has_expired());
}
#[test]
fn test_ttl_value_serialize_roundtrip() {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Data {
foo: String,
}
let data = Data { foo: "bar".to_owned() };
// With timestamp.
let ttl_value = TtlValue { data: data.clone(), last_fetch_ts: Some(1000.0) };
let json = json!({
"foo": "bar",
"last_fetch_ts": 1000.0,
});
assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
assert_eq!(deserialized.data, data);
assert!(deserialized.last_fetch_ts.unwrap() - ttl_value.last_fetch_ts.unwrap() < 0.0001);
// Without timestamp the value is always expired in theory.
let json = json!({
"foo": "bar",
});
let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
assert_eq!(deserialized.data, data);
assert!(deserialized.last_fetch_ts.unwrap() - 0.0 < 0.0001);
}
}
+23 -3
View File
@@ -8,6 +8,15 @@ All notable changes to this project will be documented in this file.
### Features
- [**breaking**] Change to the stable identifiers for `m.room_key_bundle`,
`m.history_not_shared` and `m.shared_history`. We still support reading the
unstable identifiers.
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
- Add support for MSC4385.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- Add new method `OlmMachine::push_secret_to_verified_devices`.
- Pushed secrets that we receive from verified devices are added to the
secrets inbox.
- Add `Store::{store,clear}_room_pending_key_bundle`,
`CryptoStore::get_pending_key_bundle_details_for_room` and
`CryptoStore::get_all_rooms_pending_key_bundle`, which can be used by
@@ -15,10 +24,8 @@ All notable changes to this project will be documented in this file.
[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) key
bundle.
([#6199](https://github.com/matrix-org/matrix-rust-sdk/pull/6199)), ([#6233](https://github.com/matrix-org/matrix-rust-sdk/pull/6233)),
- Add MSC4388 support to the QrcodeData struct.
([#6089](https://github.com/matrix-org/matrix-rust-sdk/pull/6089))
- Improved logging when we are sending secrets in `GossipMachine`.
([#6074](https://github.com/matrix-org/matrix-rust-sdk/pull/6074))
([#6083](https://github.com/matrix-org/matrix-rust-sdk/pull/6083))
@@ -36,6 +43,20 @@ All notable changes to this project will be documented in this file.
### Refactor
- [**breaking**] The `MegolmV1BackupKey::encrypt` now returns a `Result`
([#6477](https://github.com/matrix-org/matrix-rust-sdk/pull/6477))
- [**breaking**] `CryptoStore::get_secrets_from_inbox` now returns a `Vec` of
the secrets as strings, rather than a `Vec` of `GossippedSecret` structs.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- [**breaking**] `store::types::Changes::sessions` now stores a `Vec` of
`SecretsInboxItem`.
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
- **breaking** The `BackupDecryptionKey::new` and `DehydratedDeviceKey::new`
methods became infallible, they don't return a `Result` anymore.
([#5502](https://github.com/matrix-org/matrix-rust-sdk/pull/5502))
- [**breaking**] Remove cross-process lock generation logic from `OlmMachine`, which is now
implemented more generally in `matrix_sdk_common::cross_process_lock::CrossProcessLock`.
([#6326](https://github.com/matrix-org/matrix-rust-sdk/pull/6326))
- [**breaking**] The `MediaEncryptionInfo` fields changed to match the new fields of `EncryptedFile`
from Ruma. The serialized JSON format did not change and still matches the format of
`EncryptedFile` defined in the spec, without the `url` field. The `DecryptorError::KeyNonceLength`
@@ -50,7 +71,6 @@ All notable changes to this project will be documented in this file.
returns an MSC-specific struct now. The `rendezvous_url()` method has been
removed.
([#6081](https://github.com/matrix-org/matrix-rust-sdk/pull/6081))
- [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now
been eliminated entirely.
([#5963](https://github.com/matrix-org/matrix-rust-sdk/pull/5963))
+4
View File
@@ -40,6 +40,10 @@ test-send-sync = []
# Testing helpers for implementations based upon this
testing = ["matrix-sdk-test"]
# Enable experimental support for pushing secrets; see
# https://github.com/matrix-org/matrix-spec-proposals/pull/4385
experimental-push-secrets = []
[dependencies]
aes = { version = "0.8.4", default-features = false }
aquamarine.workspace = true
@@ -101,7 +101,10 @@ impl MegolmV1BackupKey {
/// Export the given inbound group session, and encrypt the data, ready for
/// writing to the backup.
pub async fn encrypt(&self, session: InboundGroupSession) -> KeyBackupData {
pub async fn encrypt(
&self,
session: InboundGroupSession,
) -> Result<KeyBackupData, vodozemac::pk_encryption::Error> {
let pk = PkEncryption::from_key(self.inner.key);
// The forwarding chains don't mean much, we only care whether we received the
@@ -117,7 +120,7 @@ impl MegolmV1BackupKey {
let key =
Zeroizing::new(serde_json::to_vec(&key).expect("Can't serialize exported room key"));
let message = pk.encrypt(&key);
let message = pk.encrypt(&key)?;
let session_data = EncryptedSessionDataInit {
ephemeral: Base64::new(message.ephemeral_key.to_vec()),
@@ -126,7 +129,7 @@ impl MegolmV1BackupKey {
}
.into();
KeyBackupDataInit {
Ok(KeyBackupDataInit {
first_message_index,
forwarded_count,
// TODO: is this actually used anywhere? seems to be completely
@@ -136,6 +139,6 @@ impl MegolmV1BackupKey {
is_verified: false,
session_data,
}
.into()
.into())
}
}
@@ -303,7 +303,7 @@ mod tests {
#[test]
fn base64_decoding() -> Result<(), DecodeError> {
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
let key = BackupDecryptionKey::new();
let base64 = key.to_base64();
let decoded_key = BackupDecryptionKey::from_base64(&base64)?;
@@ -316,7 +316,7 @@ mod tests {
#[test]
fn base58_decoding() -> Result<(), DecodeError> {
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
let key = BackupDecryptionKey::new();
let base64 = key.to_base58();
let decoded_key = BackupDecryptionKey::from_base58(&base64)?;
@@ -394,10 +394,10 @@ mod tests {
async fn test_encryption_cycle() {
let session = InboundGroupSession::from_export(&room_key()).unwrap();
let decryption_key = BackupDecryptionKey::new().unwrap();
let decryption_key = BackupDecryptionKey::new();
let encryption_key = decryption_key.megolm_v1_public_key();
let encrypted = encryption_key.encrypt(session).await;
let encrypted = encryption_key.encrypt(session).await.unwrap();
let _ = decryption_key
.decrypt_session_data(encrypted.session_data)
@@ -406,7 +406,7 @@ mod tests {
#[test]
fn key_matches() {
let decryption_key = BackupDecryptionKey::new().unwrap();
let decryption_key = BackupDecryptionKey::new();
let key_info = decryption_key.to_backup_info();
+14 -11
View File
@@ -534,7 +534,7 @@ impl BackupMachine {
}
let key_count = sessions.len();
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await?;
info!(
key_count = key_count,
@@ -556,10 +556,13 @@ impl BackupMachine {
async fn backup_keys(
sessions: Vec<InboundGroupSession>,
backup_key: &MegolmV1BackupKey,
) -> (
BTreeMap<OwnedRoomId, RoomKeyBackup>,
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
) {
) -> Result<
(
BTreeMap<OwnedRoomId, RoomKeyBackup>,
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
),
vodozemac::pk_encryption::Error,
> {
let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
BTreeMap::new();
@@ -568,7 +571,7 @@ impl BackupMachine {
let room_id = session.room_id().to_owned();
let session_id = session.session_id().to_owned();
let sender_key = session.sender_key().to_owned();
let session = backup_key.encrypt(session).await;
let session = backup_key.encrypt(session).await?;
session_record
.entry(room_id.to_owned())
@@ -586,7 +589,7 @@ impl BackupMachine {
.insert(session_id, session);
}
(backup, session_record)
Ok((backup, session_record))
}
/// Import the given room keys into our store.
@@ -701,7 +704,7 @@ mod tests {
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
let decryption_key = BackupDecryptionKey::new().expect("Can't create new recovery key");
let decryption_key = BackupDecryptionKey::new();
let backup_key = decryption_key.megolm_v1_public_key();
backup_key.set_version("1".to_owned());
@@ -838,7 +841,7 @@ mod tests {
let backup_machine = machine.backup_machine();
// We set up a backup key, so that we can test `backup_machine.backup()` later.
let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
let decryption_key = BackupDecryptionKey::new();
let backup_key = decryption_key.megolm_v1_public_key();
backup_key.set_version("1".to_owned());
backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
@@ -886,7 +889,7 @@ mod tests {
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
let backup_machine = machine.backup_machine();
let decryption_key = BackupDecryptionKey::new().unwrap();
let decryption_key = BackupDecryptionKey::new();
let mut backup_info = decryption_key.to_backup_info();
let result = backup_machine.verify_backup(backup_info.to_owned(), false).await.unwrap();
@@ -904,7 +907,7 @@ mod tests {
async fn test_fix_backup_key_mismatch() {
let store = MemoryStore::new();
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
let backup_decryption_key = BackupDecryptionKey::new();
store
.save_changes(Changes {
+2 -2
View File
@@ -23,7 +23,7 @@ use hmac::{
digest::{FixedOutput, MacError},
};
use pbkdf2::pbkdf2;
use rand::{RngCore, thread_rng};
use rand::{Rng, rng};
use sha2::{Sha256, Sha512};
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -247,7 +247,7 @@ impl AesHmacSha2Key {
/// The initialization vector will be clamped and will be used to encrypt
/// the ciphertext.
fn generate_iv() -> [u8; IV_SIZE] {
let mut rng = thread_rng();
let mut rng = rng();
let mut iv = [0u8; IV_SIZE];
rng.fill_bytes(&mut iv);
@@ -343,7 +343,7 @@ impl DehydratedDevice {
/// async fn example() -> anyhow::Result<()> {
/// # let machine: OlmMachine = unimplemented!();
/// // Create a new random key
/// let pickle_key = DehydratedDeviceKey::new()?;
/// let pickle_key = DehydratedDeviceKey::new();
///
/// // Create the dehydrated device.
/// let device = machine.dehydrated_devices().create().await?;
@@ -612,7 +612,7 @@ mod tests {
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
assert!(stored_key.is_none());
let pickle_key = DehydratedDeviceKey::new().unwrap();
let pickle_key = DehydratedDeviceKey::new();
dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
+17
View File
@@ -73,6 +73,11 @@ pub enum OlmError {
)]
MissingSession,
/// Encrypting of an Olm message failed because of a low-level cryptographic
/// issue occurred.
#[error(transparent)]
Encryption(#[from] vodozemac::olm::EncryptionError),
/// Encryption failed due to an error collecting the recipient devices.
#[error("encryption failed due to an error collecting the recipient devices: {0}")]
SessionRecipientCollectionError(SessionRecipientCollectionError),
@@ -444,3 +449,15 @@ pub enum SessionRecipientCollectionError {
#[error("Encryption failed because your device is not verified")]
SendingFromUnverifiedDevice,
}
/// Error representing a problem when pushing a secret
#[derive(Error, Debug)]
#[cfg(feature = "experimental-push-secrets")]
pub enum SecretPushError {
#[error("The requested secret is not available")]
MissingSecret,
/// The storage layer returned an error.
#[error(transparent)]
StoreError(#[from] CryptoStoreError),
}
@@ -18,7 +18,7 @@ use aes::{
Aes256,
cipher::{KeyIvInit, StreamCipher},
};
use rand::{RngCore, thread_rng};
use rand::{Rng, rng};
use ruma::{
events::room::{
EncryptedFile, EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileHashes,
@@ -216,7 +216,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> {
let mut key = [0u8; KEY_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut rng = thread_rng();
let mut rng = rng();
rng.fill_bytes(&mut key);
// Only populate the first 8 bytes with randomness, the rest is 0
@@ -15,7 +15,7 @@
use std::io::{Cursor, Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
use rand::{RngCore, thread_rng};
use rand::{Rng, rng};
use serde_json::Error as SerdeError;
use thiserror::Error;
use vodozemac::{base64_decode, base64_encode};
@@ -149,7 +149,7 @@ pub fn encrypt_room_key_export(
fn encrypt_helper(plaintext: &[u8], passphrase: &str, rounds: u32) -> String {
let mut salt = [0u8; SALT_SIZE];
let mut rng = thread_rng();
let mut rng = rng();
rng.fill_bytes(&mut salt);
@@ -1,4 +1,4 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
// Copyright 2020, 2026 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.
@@ -20,6 +20,8 @@
// If we don't trust the device store an object that remembers the request and
// let the users introspect that object.
#[cfg(feature = "experimental-push-secrets")]
use std::collections::HashMap;
use std::{
collections::{BTreeMap, BTreeSet, btree_map::Entry},
mem,
@@ -61,6 +63,11 @@ use crate::{
requests::{OutgoingRequest, ToDeviceRequest},
},
};
#[cfg(feature = "experimental-push-secrets")]
use crate::{
error::SecretPushError,
types::events::{olm_v1::DecryptedSecretPushEvent, secret_push::SecretPushContent},
};
#[derive(Clone, Debug)]
pub(crate) struct GossipMachine {
@@ -288,6 +295,98 @@ impl GossipMachine {
}
}
/// Push a secret to all of our other verified devices.
///
/// This function assumes that we already have Olm sessions with the other
/// devices. This can be done by calling
/// [`OlmMachine::get_missing_sessions()`].
///
/// * `secret_name` - The name of the secret to push
#[cfg(feature = "experimental-push-secrets")]
pub async fn push_secret_to_verified_devices(
&self,
secret_name: SecretName,
) -> Result<HashMap<OwnedDeviceId, OlmError>, SecretPushError> {
let content = if let Some(secret) = self.inner.store.export_secret(&secret_name).await? {
SecretPushContent::new(secret_name.clone(), secret)
} else {
info!(?secret_name, "Can't push a secret, secret isn't found");
return Err(SecretPushError::MissingSecret);
};
let devices = self.inner.store.get_user_devices(self.user_id()).await?;
let mut errors = HashMap::new();
for device in devices.devices() {
if !device.is_our_own_device() && device.is_verified() {
let event_type = content.event_type().to_owned();
match device.encrypt(&event_type, content.clone()).await {
Ok((_used_session, content, message_id)) => {
let encrypted_event_type = content.event_type().to_owned();
let request = ToDeviceRequest::new(
device.user_id(),
device.device_id().to_owned(),
&encrypted_event_type,
content.cast(),
);
let request = OutgoingRequest {
request_id: request.txn_id.clone(),
request: Arc::new(request.into()),
};
debug!(
device = ?device.device_id(),
event_type,
request_id = ?request.request_id,
?secret_name,
?message_id,
"Creating outgoing secret push to-device request",
);
self.inner
.outgoing_requests
.write()
.insert(request.request_id.clone(), request);
}
Err(err) => {
info!(?secret_name, device_id = ?device.device_id(), ?err, "Can't push secret to device");
errors.insert(device.device_id().to_owned(), err);
}
}
}
}
Ok(errors)
}
/// Handle a received secret push event.
///
/// Checks that the sender device is verified, then adds it to the changes.
#[cfg(feature = "experimental-push-secrets")]
pub async fn receive_secret_push_event(
&self,
sender_key: &Curve25519PublicKey,
event: &DecryptedSecretPushEvent,
changes: &mut Changes,
) -> Result<(), CryptoStoreError> {
// Only accept events from verified own-devices
let sender = &event.sender;
if sender != self.user_id() {
// Ignore if sent from a different user
warn!(?sender, "Received secret push from a different user");
return Ok(());
}
let Some(device) = self.inner.store.get_device_from_curve_key(sender, *sender_key).await?
else {
warn!(?sender, ?sender_key, "Received secret push from unknown device");
return Ok(());
};
if !device.is_verified() {
warn!(?sender, device_id = ?device.device_id(), "Received secret push from unverified device");
return Ok(());
}
changes.secrets.push(event.content.clone().into());
Ok(())
}
async fn handle_secret_request(
&self,
cache: &StoreCache,
@@ -917,7 +1016,7 @@ impl GossipMachine {
// So we put the secret into our inbox. Later users can inspect the contents of
// the inbox and decide if they want to activate the backup.
info!("Received a backup decryption key, storing it into the secret inbox.");
changes.secrets.push(secret);
changes.secrets.push(secret.into());
}
Ok(())
@@ -1112,6 +1211,8 @@ impl GossipMachine {
#[cfg(test)]
mod tests {
#[cfg(feature = "experimental-push-secrets")]
use std::ops::Deref;
use std::sync::Arc;
#[cfg(feature = "automatic-room-key-forwarding")]
@@ -2060,7 +2161,7 @@ mod tests {
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
let decryption_key = crate::store::types::BackupDecryptionKey::new().unwrap();
let decryption_key = crate::store::types::BackupDecryptionKey::new();
alice_machine
.backup_machine()
.save_decryption_key(Some(decryption_key), None)
@@ -2240,4 +2341,237 @@ mod tests {
assert_eq!(session.session_id(), group_session.session_id())
}
/// Set up OlmMachines for the secret-pushing tests
#[cfg(feature = "experimental-push-secrets")]
async fn set_up_secret_push() -> (
crate::machine::OlmMachine,
crate::identities::device::Device,
crate::machine::OlmMachine,
crate::identities::device::Device,
crate::store::types::BackupDecryptionKey,
) {
use crate::machine::test_helpers::get_machine_pair_with_setup_sessions_test_helper;
let alice_id = user_id!("@alice:localhost");
let (alice_machine, bob_machine) =
get_machine_pair_with_setup_sessions_test_helper(alice_id, alice_id, false).await;
let bob_device = alice_machine
.get_device(alice_id, bob_machine.device_id(), None)
.await
.unwrap()
.unwrap();
let alice_device = bob_machine
.get_device(alice_id, alice_machine.device_id(), None)
.await
.unwrap()
.unwrap();
let decryption_key = crate::store::types::BackupDecryptionKey::new();
alice_machine
.backup_machine()
.save_decryption_key(Some(decryption_key.clone()), None)
.await
.unwrap();
(alice_machine, alice_device, bob_machine, bob_device, decryption_key)
}
#[async_test]
#[cfg(feature = "experimental-push-secrets")]
async fn test_secret_pushing() {
let (alice_machine, _alice_device, _bob_machine, bob_device, _decryption_key) =
set_up_secret_push().await;
// try to push a secret, but the other device isn't verified, so nothing
// should happen
alice_machine
.inner
.key_request_machine
.push_secret_to_verified_devices(SecretName::RecoveryKey)
.await
.unwrap();
{
let alice_cache = alice_machine.store().cache().await.unwrap();
alice_machine
.inner
.key_request_machine
.collect_incoming_key_requests(&alice_cache)
.await
.unwrap();
}
let requests =
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
assert_eq!(requests.len(), 0);
// Now the device is trusted, so the secret should be pushed
bob_device.set_trust_state(LocalTrust::Verified);
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
alice_machine
.inner
.key_request_machine
.push_secret_to_verified_devices(SecretName::RecoveryKey)
.await
.unwrap();
{
let alice_cache = alice_machine.store().cache().await.unwrap();
alice_machine
.inner
.key_request_machine
.collect_incoming_key_requests(&alice_cache)
.await
.unwrap();
}
let requests =
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
assert_eq!(requests.len(), 1);
}
#[async_test]
#[cfg(feature = "experimental-push-secrets")]
async fn test_secret_push_receive() {
use futures_util::{FutureExt, pin_mut};
use serde_json::value::to_raw_value;
use tokio_stream::StreamExt;
use crate::EncryptionSyncChanges;
let (alice_machine, alice_device, bob_machine, bob_device, decryption_key) =
set_up_secret_push().await;
// Push the secret to Bob
bob_device.set_trust_state(LocalTrust::Verified);
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
alice_machine
.inner
.key_request_machine
.push_secret_to_verified_devices(SecretName::RecoveryKey)
.await
.unwrap();
{
let alice_cache = alice_machine.store().cache().await.unwrap();
alice_machine
.inner
.key_request_machine
.collect_incoming_key_requests(&alice_cache)
.await
.unwrap();
}
let requests =
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
assert_eq!(requests.len(), 1);
let request = requests.first().expect("We should have an outgoing to-device request");
// Since Alice is trusted, we should get the secret
alice_device.set_trust_state(LocalTrust::Verified);
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
let event: EncryptedToDeviceEvent =
request_to_event(bob_machine.user_id(), alice_machine.user_id(), request);
let event = Raw::from_json(to_raw_value(&event).unwrap());
let stream = bob_machine.store().secrets_stream();
pin_mut!(stream);
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
bob_machine
.receive_sync_changes(
EncryptionSyncChanges {
to_device_events: vec![event],
changed_devices: &Default::default(),
one_time_keys_counts: &Default::default(),
unused_fallback_keys: None,
next_batch_token: None,
},
&decryption_settings,
)
.await
.unwrap();
let secret = stream
.next()
.now_or_never()
.flatten()
.expect("The broadcaster should have sent out the secret");
assert_eq!(secret.secret.deref(), &decryption_key.to_base64())
}
#[async_test]
#[cfg(feature = "experimental-push-secrets")]
async fn test_secret_push_receive_untrusted() {
use futures_util::{FutureExt, pin_mut};
use serde_json::value::to_raw_value;
use tokio_stream::StreamExt;
use crate::EncryptionSyncChanges;
let (alice_machine, _alice_device, bob_machine, bob_device, _decryption_key) =
set_up_secret_push().await;
// Push the secret to Bob
bob_device.set_trust_state(LocalTrust::Verified);
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
alice_machine
.inner
.key_request_machine
.push_secret_to_verified_devices(SecretName::RecoveryKey)
.await
.unwrap();
{
let alice_cache = alice_machine.store().cache().await.unwrap();
alice_machine
.inner
.key_request_machine
.collect_incoming_key_requests(&alice_cache)
.await
.unwrap();
}
let requests =
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
assert_eq!(requests.len(), 1);
let request = requests.first().expect("We should have an outgoing to-device request");
// Test receiving the event. Alice isn't trusted, so the secret will be
// dropped
let event: EncryptedToDeviceEvent =
request_to_event(bob_machine.user_id(), alice_machine.user_id(), request);
let event = Raw::from_json(to_raw_value(&event).unwrap());
let stream = bob_machine.store().secrets_stream();
pin_mut!(stream);
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
bob_machine
.receive_sync_changes(
EncryptionSyncChanges {
to_device_events: vec![event.clone()],
changed_devices: &Default::default(),
one_time_keys_counts: &Default::default(),
unused_fallback_keys: None,
next_batch_token: None,
},
&decryption_settings,
)
.await
.unwrap();
assert!(stream.next().now_or_never().flatten().is_none());
}
}
+26 -125
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
// Copyright 2020, 2026 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.
@@ -66,6 +66,8 @@ use tracing::{
};
use vodozemac::{Curve25519PublicKey, Ed25519Signature, megolm::DecryptionError};
#[cfg(feature = "experimental-push-secrets")]
use crate::error::SecretPushError;
#[cfg(feature = "experimental-send-custom-to-device")]
use crate::session_manager::split_devices_for_share_strategy;
use crate::{
@@ -174,7 +176,6 @@ impl std::fmt::Debug for OlmMachine {
}
impl OlmMachine {
const CURRENT_GENERATION_STORE_KEY: &'static str = "generation-counter";
const HAS_MIGRATED_VERIFICATION_LATCH: &'static str = "HAS_MIGRATED_VERIFICATION_LATCH";
/// Create a new memory based OlmMachine.
@@ -959,8 +960,7 @@ impl OlmMachine {
}
}
/// Handle a received, decrypted, `io.element.msc4268.room_key_bundle`
/// to-device event.
/// Handle a received, decrypted, `m.room_key_bundle` to-device event.
#[instrument()]
async fn receive_room_key_bundle_data(
&self,
@@ -1409,6 +1409,13 @@ impl OlmMachine {
debug!("Received a room key bundle event {:?}", e);
self.receive_room_key_bundle_data(decrypted.result.sender_key, e, changes).await?;
}
#[cfg(feature = "experimental-push-secrets")]
AnyDecryptedOlmEvent::SecretPush(e) => {
self.inner
.key_request_machine
.receive_secret_push_event(&decrypted.result.sender_key, e, changes)
.await?;
}
AnyDecryptedOlmEvent::Custom(_) => {
warn!("Received an unexpected encrypted to-device event");
}
@@ -2013,6 +2020,21 @@ impl OlmMachine {
}
}
/// Push a secret to all of our other verified devices.
///
/// This function assumes that we already have Olm sessions with the other
/// devices. This can be done by calling
/// [`OlmMachine::get_missing_sessions()`].
///
/// * `secret_name` - The name of the secret to push
#[cfg(feature = "experimental-push-secrets")]
pub async fn push_secret_to_verified_devices(
&self,
secret_name: SecretName,
) -> Result<HashMap<OwnedDeviceId, OlmError>, SecretPushError> {
self.inner.key_request_machine.push_secret_to_verified_devices(secret_name).await
}
/// Get some metadata pertaining to a given group session.
///
/// This includes the session owner's Matrix user ID, their device ID, info
@@ -2823,127 +2845,6 @@ impl OlmMachine {
&self.inner.backup_machine
}
/// Syncs the database and in-memory generation counter.
///
/// This requires that the crypto store lock has been acquired already.
pub async fn initialize_crypto_store_generation(
&self,
generation: &Mutex<Option<u64>>,
) -> StoreResult<()> {
// Avoid reentrant initialization by taking the lock for the entire's function
// scope.
let mut gen_guard = generation.lock().await;
let prev_generation =
self.inner.store.get_custom_value(Self::CURRENT_GENERATION_STORE_KEY).await?;
let generation = match prev_generation {
Some(val) => {
// There was a value in the store. We need to signal that we're a different
// process, so we don't just reuse the value but increment it.
u64::from_le_bytes(val.try_into().map_err(|_| {
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
})?)
.wrapping_add(1)
}
None => 0,
};
tracing::debug!("Initialising crypto store generation at {generation}");
self.inner
.store
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, generation.to_le_bytes().to_vec())
.await?;
*gen_guard = Some(generation);
Ok(())
}
/// If needs be, update the local and on-disk crypto store generation.
///
/// ## Requirements
///
/// - This assumes that `initialize_crypto_store_generation` has been called
/// beforehand.
/// - This requires that the crypto store lock has been acquired.
///
/// # Arguments
///
/// * `generation` - The in-memory generation counter (or rather, the
/// `Mutex` wrapping it). This defines the "expected" generation on entry,
/// and, if we determine an update is needed, is updated to hold the "new"
/// generation.
///
/// # Returns
///
/// A tuple containing:
///
/// * A `bool`, set to `true` if another process has updated the generation
/// number in the `Store` since our expected value, and as such we've
/// incremented and updated it in the database. Otherwise, `false`.
///
/// * The (possibly updated) generation counter.
pub async fn maintain_crypto_store_generation(
&'_ self,
generation: &Mutex<Option<u64>>,
) -> StoreResult<(bool, u64)> {
let mut gen_guard = generation.lock().await;
// The database value must be there:
// - either we could initialize beforehand, thus write into the database,
// - or we couldn't, and then another process was holding onto the database's
// lock, thus
// has written a generation counter in there.
let actual_gen = self
.inner
.store
.get_custom_value(Self::CURRENT_GENERATION_STORE_KEY)
.await?
.ok_or_else(|| {
CryptoStoreError::InvalidLockGeneration("counter missing in store".to_owned())
})?;
let actual_gen =
u64::from_le_bytes(actual_gen.try_into().map_err(|_| {
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
})?);
let new_gen = match gen_guard.as_ref() {
Some(expected_gen) => {
if actual_gen == *expected_gen {
return Ok((false, actual_gen));
}
// Increment the biggest, and store it everywhere.
actual_gen.max(*expected_gen).wrapping_add(1)
}
None => {
// Some other process hold onto the lock when initializing, so we must reload.
// Increment database value, and store it everywhere.
actual_gen.wrapping_add(1)
}
};
tracing::debug!(
"Crypto store generation mismatch: previously known was {:?}, actual is {:?}, next is {}",
*gen_guard,
actual_gen,
new_gen
);
// Update known value.
*gen_guard = Some(new_gen);
// Update value in database.
self.inner
.store
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, new_gen.to_le_bytes().to_vec())
.await?;
Ok((true, new_gen))
}
/// Manage dehydrated devices.
pub fn dehydrated_devices(&self) -> DehydratedDevices {
DehydratedDevices { inner: self.to_owned() }
@@ -272,7 +272,7 @@ pub async fn build_encrypted_to_device_content_without_sender_data(
}))
.unwrap();
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
let content =
olm_session.build_encrypted_event(ciphertext, None).await.expect("could not encrypt");
@@ -290,7 +290,7 @@ async fn create_and_share_session_without_sender_data(
}))
.unwrap();
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
ToDeviceEvent::new(
alice.user_id().to_owned(),
olm_session.build_encrypted_event(ciphertext, None).await.unwrap(),

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