Compare commits

...

80 Commits

Author SHA1 Message Date
Damir Jelić 0b16d488ad chore: Release matrix-sdk version 0.8.0 (#4291)
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-11-19 14:11:19 +01:00
Damir Jelić d40aac89cb fix: Use the DisplayName struct to protect against homoglyph attacks 2024-11-19 11:54:01 +01:00
Damir Jelić e4ebeb8a42 feat(base): Introduce a DisplayName struct
This patch introduces a struct that normalizes and sanitizes display
names. Display names can be a source of abuse and can contain characters
which might make it hard to distinguish one display name from the other.

This struct attempts to make it easier to protect against such abuse.

Changelog: Introduce a DisplayName struct which normalizes and sanitizes
display names.

Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
2024-11-19 11:54:01 +01:00
Erik Johnston 22bbe0c32e Add 'conn_id' field to sync_once span
This is to make it easier to see which sync requests are for which
connection when debugging.
2024-11-19 11:45:32 +01:00
dependabot[bot] 05505a5a48 chore(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-19 08:32:34 +01:00
Timo 21bb85ac21 feat(Room): Check if the user is allowed to do a room mention before trying to send a call notify event. (#4271) 2024-11-18 16:15:28 +02:00
Benjamin Bouvier f1a442bad0 refactor(send queue): use a specialized mutex for locking access to the state store and being_sent
There was an implicit relationship that the `being_sent` lock needed to
be taken in order to do non-atomic state store operations. With the
change from this commit, the relationship is now more explicit: to get a
handle to the state store, or being_sent, you have to obtain a
`StoreLockGuard` by locking against the store itself. The `WeakClient`
isn't stored in the QueueStorage data structure itself, so it's the only
way to get a `dyn StateStore` from the `QueueStorage`.
2024-11-18 14:55:31 +01:00
Jorge Martín a8a83c3b45 feat(room_preview): Use room directory search as another data source 2024-11-18 13:22:41 +01:00
Tobias Fella 47246483fa doc(crypto): Fix typo
Signed-off-by: Tobias Fella <fella@posteo.de>
2024-11-16 17:24:17 +01:00
Hubert Chathi 31006ab3bf feat(crypto): pin identity when we withdraw verification 2024-11-16 10:26:58 +01:00
Doug 3ed5d34f49 feat(ffi): Add support for including captions with file uploads. 2024-11-15 20:10:50 +01:00
Benjamin Bouvier 232391c6b2 task(send queue): move some assertions back to logged errors
Better safe than panicky.
2024-11-15 10:35:31 +01:00
Jorge Martín cefd5a27f5 feat(ffi): make RoomPreviewInfo::room_type an enum, not an optional String 2024-11-14 16:41:55 +01:00
Jorge Martín 97952902a3 feat(ffi): add RoomPreviewInfo::num_active_members 2024-11-14 16:41:55 +01:00
Jorge Martín bf4a2ed297 feat(ffi): add is_direct and fn inviter to RoomPreview 2024-11-14 16:41:55 +01:00
Benjamin Bouvier a499988621 task(CI): rename the upload code coverage task to make its name clearer 2024-11-14 16:39:28 +01:00
Benjamin Bouvier 0d01cabb8d refactor(widget): get rid of unused limits parameter when constructing a WidgetMachine 2024-11-14 16:23:52 +01:00
Benjamin Bouvier f3c0309fbc refactor(widget): get rid of ProcessingContext and inline it in its callers 2024-11-14 16:23:52 +01:00
Benjamin Bouvier 8070e3c165 refactor(widget): tidy up and start commenting the widget code 2024-11-14 16:23:52 +01:00
Benjamin Bouvier 02c7c2cdfc test(send queue): caching a thumbnail of unknown dimensions removes it from cache after upload 2024-11-14 16:22:07 +01:00
Benjamin Bouvier 9b6de4e436 test(send queue): add more tests for cancellation 2024-11-14 15:33:59 +01:00
Benjamin Bouvier b7d4be9b65 test(send queue): add a test for cancelling an upload while the thumbnail upload is active 2024-11-14 15:33:59 +01:00
Benjamin Bouvier bc86027853 test(send queue): add a test for cancelling a media upload before it's active 2024-11-14 15:33:59 +01:00
Benjamin Bouvier 50db563363 feat(send queue): allow aborting media uploads 2024-11-14 15:33:59 +01:00
Benjamin Bouvier 8fa07ec22d task(send queue): being_sent is an Option, not a set anymore
There can be at most one thing being sent by the send queue, so make
this super explicit.
2024-11-14 15:33:59 +01:00
Timo 7aa930b81c feat(WidgetDriver): Send state from state sync and not from timeline to widget (#4254) 2024-11-14 15:55:25 +02:00
Benjamin Bouvier c02d8cee77 feat!(send queue): add a priority field to maintain ordering of sending
Prior to this patch, the send queue would not maintain the ordering of
sending a media *then* a text, because it would push back a dependent
request graduating into a queued request.

The solution implemented here consists in adding a new priority column
to the send queue, defaulting to 0 for existing events, and use higher
priorities for the media uploads, so they're considered before other
requests.

A high priority is also used for aggregation events that are sent late,
so they're sent as soon as possible, before other subsequent events.
2024-11-14 12:00:08 +01:00
Benjamin Bouvier 2872af234b test(send queue): add a test for the ordering of media vs other events 2024-11-14 12:00:08 +01:00
Jorge Martín d614878436 refactor(sdk): move formatted_caption_from to the SDK, rename it
Add the `markdown` feature to the SDK crate, otherwise we can't use `FormattedBody::markdown`.

Refactor the pattern matching into an if, add tests to check its behaviour.
2024-11-14 10:38:44 +01:00
Jorge Martín afaecdc457 feat(ffi): generate formatted captions for send_* media fns
Changelog: For `Timeline::send_*` fns, treat the passed `caption` parameter as markdown and use the HTML generated from it as the `formatted_caption` if there is none.
2024-11-14 10:38:44 +01:00
Ivan Enderlin aca83fb4ed refactor: Move Event and Gap into matrix_sdk_base::event_cache. 2024-11-13 15:25:58 +01:00
Ivan Enderlin c3e28f7e33 refactor: Move linked_chunk from matrix-sdk to matrix-sdk-common. 2024-11-13 15:25:58 +01:00
Ivan Enderlin 949cd78d94 refactor: Move event_cache_store/ to event_cache/store/ in matrix-sdk-base. 2024-11-13 15:25:58 +01:00
Benjamin Bouvier 99b9c50548 feat(send queue): implement unwedging for media uploads 2024-11-13 14:32:53 +01:00
Benjamin Bouvier 371e7bc052 task(tests): move error_too_large to the generic endpoint
So it can be reused in more contexts than just the sending of an event,
but also for uploads.
2024-11-13 14:32:53 +01:00
Benjamin Bouvier 0541ec7e3f refactor(send queue): use SendHandle for media uploads too 2024-11-13 14:32:53 +01:00
Ivan Enderlin 0509236cf8 doc(sdk): Improve documentation of Client::observe_events. 2024-11-13 11:24:08 +01:00
Ivan Enderlin 6cef7f20c5 feat(sdk): Implement Client::observe_events and Client::observe_room_events.
Changelog: This patch introduces a mechanism similar to
 `Client::add_event_handler` and `Client::add_room_event_handler`
 but with a reactive programming pattern. This patch adds
 `Client::observe_events` and `Client::observe_room_events`.

 ```rust
 // Get an observer.
 let observer =
     client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();

 // Subscribe to the observer.
 let mut subscriber = observer.subscribe();

 // Use the subscriber as a `Stream`.
 let (message_event, (room, push_actions)) = subscriber.next().await.unwrap();
 ```

 When calling `observe_events`, one has to specify the type of event
 (in the example, `SyncRoomMessageEvent`) and a context (in the example,
 `(Room, Vec<Action>)`, respectively for the room and the push actions).
2024-11-13 11:24:08 +01:00
Ivan Enderlin e798a51709 feat(sdk): Implement EventHandlerContext for tuples.
This patch implements `EventHandlerContext` for tuples where each part
implements `EventHandlerContext` itself.
2024-11-13 11:24:08 +01:00
Ivan Enderlin 8f8aad6f4d chore(cargo): Update eyeball-im-util to 0.7.0. 2024-11-13 11:16:30 +01:00
Ivan Enderlin af84c79e69 feat(base): Make ObservableMap::stream works on wasm32-unknown-unknown.
This patch updates `eyeball-im` and `eyeball-im-util` to integrate
https://github.com/jplatte/eyeball/pull/63/. With this new feature, we
can have a single implementation of `ObservableMap` (instead of 2: one
for all targets, one for `wasm32-u-u`). It makes it possible to get
`Client::rooms_stream` available on all targets now.
2024-11-13 11:16:30 +01:00
Ivan Enderlin a920c3fdec fix(ui): Disable share_pos() inside RoomListService.
This patch disables the call to `share_pos()` inside the
`RoomListService` because it creates slowness we need to investigate.
2024-11-13 09:33:04 +01:00
Benjamin Bouvier 9dd2d5ee3c task(architecture): address typo in architecture.md file about EncryptionSyncService 2024-11-12 16:24:07 +01:00
Benjamin Bouvier f341dc4131 refactor(ffi): remove duplicated fields in media event contents
The caption and filenames were weirdly duplicated in each media content,
when the expected behavior is well defined:

- if there's both a caption and a filename, body := caption, filename is
its own field.
- if there's only a filename, body := filename.

We can remove all duplicated fields, knowing this, and reconstruct the
body based on that information. This should make it clearer to FFI users
which is what, and provide a clearer API when creating the caption and
so on.
2024-11-12 16:22:09 +01:00
Damir Jelić d446eb933e test: Add a bunch examples to the MatrixMockServer docs 2024-11-12 14:51:31 +01:00
Damir Jelić 8f0f0fa4d4 chore(test): Don't require two room IDs in the mock_sync_room method 2024-11-12 14:51:31 +01:00
Benjamin Bouvier 36b96ccef2 task(tests): have the test clients use Matrix v1.12 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 5957232e54 task(tests): add a MockClientBuilder to help with creating Clients connected to a MatrixMockServer 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 6f60eea9ce task(tests): refactor mock system to use generic endpoints and avoid code duplication 2024-11-12 12:01:18 +01:00
Benjamin Bouvier 982c6eab54 feat(send queue): retry uploads if they've failed with transient errors 2024-11-12 11:19:05 +01:00
Benjamin Bouvier cfd0c5ce0c feat(media): allow passing a custom RequestConfig to an upload request 2024-11-12 11:19:05 +01:00
Benjamin Bouvier 8e2939bd91 refactor!(send queue): move RoomSendQueue::unwedge to the SendHandle type 2024-11-12 11:06:19 +01:00
Ivan Enderlin 66a79729ed test(ci): Re-enable Complement Crypto. 2024-11-12 10:46:59 +01:00
dependabot[bot] bd5f5f3fe0 chore(deps): bump crate-ci/typos from 1.27.0 to 1.27.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.0 to 1.27.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.27.0...v1.27.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 15:43:34 +01:00
Ivan Enderlin 403be3dea0 test(ci): Disable Complement Crypto for a short period of time. 2024-11-11 13:28:39 +01:00
Ivan Enderlin 4d39d176d9 fix(ffi): Simplify Client::new constructor.
This patch continues to simplification of the `matrix_sdk_ffi::Client`.
The constructor can receive a `enable_oidc_refresh_lock: bool` instead
of `cross_process_refresh_lock_id: Option<String>`, which was a copy of
`matrix_sdk::Client::cross_process_store_locks_holder_name`.

Now there is a single boolean to indicate whether
`Oidc::enable_cross_process_refresh_lock` should be called
or not. If it has to be called, it is possible to re-use
`matrix_sdk::Client::cross_process_store_locks_holder_name`. Once
again, there is a single place to read this data, it's not copied over
different semantics.
2024-11-11 13:28:39 +01:00
Ivan Enderlin d3a232607a fix(ffi): Replace enable_cross_process_refresh_lock by enable_oidc_refresh_crypto_lock.
This patch simplifies a little the `ClientBuilder` API:

* `enable_cross_process_refresh_lock` is removed
* `enable_oidc_refresh_crypto_lock` + `set_session_delegate` must be
  used instead.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 3070154a57 feat(ffi): Add ClientBuilder::cross_process_store_locks_holder_name. 2024-11-11 13:28:39 +01:00
Ivan Enderlin 563c3aae31 feat(ui): EncryptionSyncService and Notification are using Client::cross_process_store_locks_holder_name.
This patch removes the `process_id` argument from
`EncryptionSyncService::new()` and replaces it by
`Client::cross_process_store_locks_holder_name`. The “process ID” is
set when the `Client` is converted into another `Client` tailore for
notification in `NotificationClient` with `Client::notification_client`
which now has a new `cross_process_store_locks_holder_name` argument.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 90b8ba3c2e feat: Client::cross_process_store_locks_holder_name is used everywhere.
See the Changelog Section to get the details.

Changelog: `Client::cross_process_store_locks_holder_name` is used everywhere:
 - `StoreConfig::new()` now takes a
   `cross_process_store_locks_holder_name` argument.
 - `StoreConfig` no longer implements `Default`.
 - `BaseClient::new()` has been removed.
 - `BaseClient::clone_with_in_memory_state_store()` now takes a
   `cross_process_store_locks_holder_name` argument.
 - `BaseClient` no longer implements `Default`.
 - `EventCacheStoreLock::new()` no longer takes a `key` argument.
 - `BuilderStoreConfig` no longer has
   `cross_process_store_locks_holder_name` field for `Sqlite` and
   `IndexedDb`.
2024-11-11 13:28:39 +01:00
Ivan Enderlin 031a96200b feat(sdk): Add Client::cross_proces_store_locks_holder_name().
This patch adds `ClientInner::cross_process_store_locks_holder_name` and
its public method `Client::cross_process_store_locks_holder_name`. This
patch also adds `ClientBuilder::cross_process_store_locks_holider_name`
to configure this value.
2024-11-11 13:28:39 +01:00
Jorge Martín 57e78dd22b feat(ffi): Add Client::create_room_alias function 2024-11-11 13:26:15 +01:00
Jorge Martín 53900294d0 feat(room_alias): Add create_room_alias function
This associates a room alias with an existing room through its room id.
2024-11-11 13:26:15 +01:00
Damir Jelić f483f35573 chore: Allow backoff to be used in the cargo-deny config
Backoff seems to be unmaintained, there's no drop-in replacement so
let's silence the warning for now.
2024-11-11 13:16:02 +01:00
Jorge Martín 204e6e4ca0 feat(sliding_sync): Add m.room.join_rules to the required state
We need the join rules state event to prevent the SDK from assuming a room with an unknown (as in, not loaded) join rule is public.
2024-11-08 17:05:37 +01:00
Jorge Martín ca8c635f62 feat(ffi): add reason field to TimelineItemContent::RoomMembership 2024-11-08 16:40:50 +01:00
Timo b8a61cfc17 feat(WidgetDriver): Support widget redacts (#3987)
Changelog: Implement proper redact handling in the widget driver.
 This allows the Rust SDK widget driver to support widgets that
 rely on redacting.
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-11-08 14:21:35 +01:00
Jorge Martín ab61077a8b fix(ffi): match the right status code in Client::is_room_alias_available 2024-11-08 12:36:51 +01:00
Benjamin Bouvier 26bee1cc38 doc: start an architecture document with a high-level description of the crates 2024-11-08 13:11:19 +02:00
Jorge Martín 46232ee2c1 fix(sdk): add more invalid characters for room aliases 2024-11-08 11:55:26 +01:00
Jorge Martín 7c600fddf0 refactor(ffi): Improve is_room_alias_format_valid so it's more strict.
Previously this only used the Ruma checks, which only handled the initial `#` char and the domain part. With these changes, the name part is also validated, checking it's lowercase, with no whitespaces and containing only allowed chars, similar to what `DisplayName::to_room_alias_name` does.

Moved the code to the SDK crate so it can be properly tested.
2024-11-08 11:55:26 +01:00
Benjamin Bouvier 965a59d5b8 task(tests): create the client with MatrixMockServer::make_client() instead of embedding one into the struct 2024-11-07 17:37:58 +01:00
Benjamin Bouvier f032d16d20 task(tests): mock upload too 2024-11-07 17:37:58 +01:00
Benjamin Bouvier 57137cdd5b task(tests): introduce prebuilt mocks and mocking helpers 2024-11-07 17:37:58 +01:00
Damir Jelić 5d83808143 feat(base): Consider knocked members to be part of the room for display name disambiguation 2024-11-07 16:41:16 +01:00
Damir Jelić df465a0420 chore(base): Improve the docs for the AmbiguityCache 2024-11-07 16:41:16 +01:00
Damir Jelić 4ca69da93c chore(base): Improve the docs for the DisplayNameUsers struct 2024-11-07 16:41:16 +01:00
Damir Jelić 4039359512 chore(base): Clean up the display name ambiguity calculation logic 2024-11-07 16:41:16 +01:00
Damir Jelić 1304902cb4 refactor(base): Rename AmbiguityMap to DisplayNameUsers
The ambiguity map tracks the users which are using a single display
name, so let's reflect that in the name.
2024-11-07 16:41:16 +01:00
Damir Jelić 219be9b731 refactor(base)!: Rename DisplayName to RoomDisplayName 2024-11-07 16:41:16 +01:00
122 changed files with 6732 additions and 2294 deletions
+1
View File
@@ -10,6 +10,7 @@ exclude = [
version = 2
ignore = [
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
]
[licenses]
+1 -1
View File
@@ -304,7 +304,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.27.0
uses: crate-ci/typos@v1.27.3
clippy:
name: Run clippy
+2 -2
View File
@@ -1,6 +1,6 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Codecov
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
@@ -64,7 +64,7 @@ jobs:
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
+119
View File
@@ -0,0 +1,119 @@
# Architecture
The SDK is split into multiple layers:
```
WASM (external crate matrix-rust-sdk-crypto-wasm)
/
/ uniffi
/ /
/ bindings (matrix-sdk-ffi)
crypto |
bindings |
| |
| UI (matrix-sdk-ui)
| \
| \
| main (matrix-sdk)
| / /
crypto /
\ /
store (matrix-sdk-base, + all the store impls)
|
common (matrix-sdk-common)
```
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
`MemoryStore` which is defined in `matrix-sdk-base`.
## `crates/matrix-sdk`
This is the main crate, and one that is expected to be used by most consumers. Notable data types
include:
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
sync, etc.
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
allows running queries that are room-specific, notably sending events.
## `crates/matrix-sdk-base`
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
storage backends, as well as dummy in-memory implementations of these traits.
## `crates/matrix-sdk-common`
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
crates (the only crate it's using is test helpers).
## `crates/matrix-sdk-crypto`
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
## `crates/matrix-sdk-indexeddb`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
indexeddb backend (for use in Web browsers, via WebAssembly).
## `crates/matrix-sdk-qrcode`
Implementation of QR codes for interactive verifications, used in the crypto crate.
## `crates/matrix-sdk-sqlite`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
SQLite backend.
## `crates/matrix-sdk-store-encryption`
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
implement encryption at rest can use those primitives.
## `crates/matrix-sdk-ui`
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
everything related to crypto and E2EE for the current `Client`.
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
retrieving the list of current rooms, and exposing its entries.
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
down.
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
(aggregations) into single timeline items.
## `bindings/matrix-sdk-crypto-ffi/`
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
Element apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi/`
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi-macros/`
Macros used in `bindings/matrix-sdk-ffi`.
## `testing/matrix-sdk-test/`
Common test helpers, used by all the other crates.
## `testing/matrix-sdk-test-macros/`
Implementation of the `#[async_test]` test macro.
## `testing/matrix-sdk-integration-testing/`
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
setup is provided to ease running the tests, and it is compatible for running with Podman too.
# Inspiration
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
Generated
+46 -16
View File
@@ -1246,6 +1246,17 @@ dependencies = [
"uuid",
]
[[package]]
name = "decancer"
version = "3.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a41401dd84c9335e2f5aec7f64057e243585d62622260d41c245919a601ccc9"
dependencies = [
"lazy_static",
"paste",
"regex",
]
[[package]]
name = "delegate-display"
version = "2.1.1"
@@ -1642,22 +1653,21 @@ dependencies = [
[[package]]
name = "eyeball-im"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae8c5165c9770f3ec7cccce12f4c5d70f01fa8bf84cf30cfbfd5a1c6f8901d5"
checksum = "a1c02432230060cae0621e15803e073976d22974e0f013c9cb28a4ea1b484629"
dependencies = [
"futures-core",
"imbl",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "eyeball-im-util"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b6b037e2cdce928a432ecc2880c944e5436d8a38c827974b882ad373f60037"
checksum = "f63a70e454238b5f66a0a0544c3e6a38be765cb01f34da9b94a2f3ecd8777cf8"
dependencies = [
"arrayvec",
"eyeball-im",
@@ -2872,7 +2882,7 @@ dependencies = [
[[package]]
name = "matrix-sdk"
version = "0.7.1"
version = "0.8.0"
dependencies = [
"anyhow",
"anymap2",
@@ -2915,6 +2925,7 @@ dependencies = [
"mime2ext",
"once_cell",
"openidconnect",
"pin-project-lite",
"proptest",
"rand",
"reqwest",
@@ -2930,6 +2941,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"tokio-test",
"tokio-util",
"tower",
"tracing",
@@ -2946,7 +2958,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-base"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"as_variant",
"assert_matches",
@@ -2954,6 +2966,7 @@ dependencies = [
"assign",
"async-trait",
"bitflags 2.6.0",
"decancer",
"eyeball",
"eyeball-im",
"futures-executor",
@@ -2969,23 +2982,27 @@ dependencies = [
"ruma",
"serde",
"serde_json",
"similar-asserts",
"stream_assert",
"thiserror",
"tokio",
"tracing",
"unicode-normalization",
"uniffi",
"wasm-bindgen-test",
]
[[package]]
name = "matrix-sdk-common"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"assert_matches",
"async-trait",
"eyeball-im",
"futures-core",
"futures-util",
"gloo-timers",
"imbl",
"js-sys",
"matrix-sdk-test",
"proptest",
@@ -3005,7 +3022,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-crypto"
version = "0.7.2"
version = "0.8.0"
dependencies = [
"aes",
"anyhow",
@@ -3128,7 +3145,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-indexeddb"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"anyhow",
"assert_matches",
@@ -3196,7 +3213,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-qrcode"
version = "0.7.1"
version = "0.8.0"
dependencies = [
"byteorder",
"image",
@@ -3208,7 +3225,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-sqlite"
version = "0.7.1"
version = "0.8.0"
dependencies = [
"assert_matches",
"async-trait",
@@ -3235,7 +3252,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-store-encryption"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -3281,7 +3298,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-ui"
version = "0.7.0"
version = "0.8.0"
dependencies = [
"anyhow",
"as_variant",
@@ -5621,6 +5638,19 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "tokio-test"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
dependencies = [
"async-stream",
"bytes",
"futures-core",
"tokio",
"tokio-stream",
]
[[package]]
name = "tokio-util"
version = "0.7.11"
@@ -5891,9 +5921,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
dependencies = [
"tinyvec",
]
+11 -11
View File
@@ -32,8 +32,8 @@ as_variant = "1.2.0"
base64 = "0.22.0"
byteorder = "1.4.3"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.5.0", features = ["tracing"] }
eyeball-im-util = "0.6.0"
eyeball-im = { version = "0.5.1", features = ["tracing"] }
eyeball-im-util = "0.7.0"
futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = "0.3.26"
@@ -79,17 +79,17 @@ vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
wiremock = "0.6.0"
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
+4 -1
View File
@@ -74,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
.block_on(sqlite_store.save_changes(&changes))
.expect("initial filling of sqlite failed");
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
let base_client = BaseClient::with_store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
+8 -2
View File
@@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
@@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
+31 -17
View File
@@ -194,7 +194,7 @@ pub struct Client {
impl Client {
pub async fn new(
sdk_client: MatrixClient,
cross_process_refresh_lock_id: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
@@ -210,19 +210,27 @@ impl Client {
}
});
let cross_process_store_locks_holder_name =
sdk_client.cross_process_store_locks_holder_name().to_owned();
let client = Client {
inner: AsyncRuntimeDropped::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
};
if let Some(process_id) = cross_process_refresh_lock_id {
if enable_oidc_refresh_lock {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates when enabling the cross-process lock"
))?;
}
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
client
.inner
.oidc()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
}
if let Some(session_delegate) = session_delegate {
@@ -695,7 +703,7 @@ impl Client {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<String, ClientError> {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = self.inner.media().upload(&mime_type, data);
let request = self.inner.media().upload(&mime_type, data, None);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
@@ -1130,21 +1138,27 @@ impl Client {
Ok(())
}
/// Checks if a room alias is available in the current homeserver.
/// Checks if a room alias is not in use yet.
///
/// Returns:
/// - `Ok(true)` if the room alias is available.
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
/// status code).
/// - An `Err` otherwise.
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
let alias = RoomAliasId::parse(alias)?;
match self.inner.resolve_room_alias(&alias).await {
// The room alias was resolved, so it's already in use.
Ok(_) => Ok(false),
Err(HttpError::Reqwest(error)) => {
match error.status() {
// The room alias wasn't found, so it's available.
Some(StatusCode::NOT_FOUND) => Ok(true),
_ => Err(HttpError::Reqwest(error).into()),
}
}
Err(error) => Err(error.into()),
}
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
}
/// Creates a new room alias associated with the provided room id.
pub async fn create_room_alias(
&self,
room_alias: String,
room_id: String,
) -> Result<(), ClientError> {
let room_alias = RoomAliasId::parse(room_alias)?;
let room_id = RoomId::parse(room_id)?;
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
}
}
+20 -13
View File
@@ -260,7 +260,8 @@ pub struct ClientBuilder {
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_refresh_lock_id: Option<String>,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
disable_built_in_root_certificates: bool,
@@ -284,7 +285,8 @@ impl ClientBuilder {
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_refresh_lock_id: None,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
session_delegate: None,
additional_root_certificates: Default::default(),
disable_built_in_root_certificates: false,
@@ -300,14 +302,18 @@ impl ClientBuilder {
})
}
pub fn enable_cross_process_refresh_lock(
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
process_id: String,
session_delegate: Box<dyn ClientSessionDelegate>,
holder_name: String,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_refresh_lock_id = Some(process_id);
builder.session_delegate = Some(session_delegate.into());
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
Arc::new(builder)
}
@@ -472,6 +478,11 @@ impl ClientBuilder {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
inner_builder =
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
if let Some(session_paths) = &builder.session_paths {
let data_path = PathBuf::from(&session_paths.data_path);
let cache_path = PathBuf::from(&session_paths.cache_path);
@@ -614,12 +625,8 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
Ok(Arc::new(
Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)
.await?,
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
}
+6 -29
View File
@@ -25,8 +25,7 @@ use ruma::{
},
TimelineEventType,
},
EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId,
UserId,
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
@@ -40,7 +39,7 @@ use crate::{
room_info::RoomInfo,
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{FocusEventError, ReceiptType, Timeline},
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
utils::u64_to_uint,
TaskHandle,
};
@@ -790,10 +789,8 @@ impl Room {
pub async fn withdraw_verification_and_resend(
&self,
user_ids: Vec<String>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let user_ids: Vec<OwnedUserId> =
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
@@ -805,7 +802,7 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
@@ -823,10 +820,8 @@ impl Room {
pub async fn ignore_device_trust_and_resend(
&self,
devices: HashMap<String, Vec<String>>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let encryption = self.inner.client().encryption();
for (user_id, device_ids) in devices.iter() {
@@ -841,28 +836,10 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
/// Attempt to manually resend messages that failed to send due to issues
/// that should now have been fixed.
///
/// This is useful for example, when there's a
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
/// the user may have re-verified on a different device and would now
/// like to send the failed message that's waiting on this device.
///
/// # Arguments
///
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo that should be unwedged.
pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> {
let transaction_id: &TransactionId = transaction_id.as_str().into();
self.inner.send_queue().unwedge(transaction_id).await?;
Ok(())
}
}
/// Generates a `matrix.to` permalink to the given room alias.
+8 -5
View File
@@ -1,14 +1,17 @@
use matrix_sdk::DisplayName;
use ruma::RoomAliasId;
use matrix_sdk::RoomDisplayName;
/// Verifies the passed `String` matches the expected room alias format.
/// Verifies the passed `String` matches the expected room alias format:
///
/// This means it's lowercase, with no whitespace chars, has a single leading
/// `#` char and a single `:` separator between the local and domain parts, and
/// the local part only contains characters that can't be percent encoded.
#[matrix_sdk_ffi_macros::export]
fn is_room_alias_format_valid(alias: String) -> bool {
RoomAliasId::parse(alias).is_ok()
matrix_sdk::utils::is_room_alias_format_valid(alias)
}
/// Transforms a Room's display name into a valid room alias name.
#[matrix_sdk_ffi_macros::export]
fn room_alias_name_from_room_display_name(room_name: String) -> String {
DisplayName::Named(room_name).to_room_alias_name()
RoomDisplayName::Named(room_name).to_room_alias_name()
}
+44 -4
View File
@@ -1,9 +1,12 @@
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::space::SpaceRoomJoinRule;
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{client::JoinRule, error::ClientError, room::Membership, utils::AsyncRuntimeDropped};
use crate::{
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
utils::AsyncRuntimeDropped,
};
/// A room preview for a room. It's intended to be used to represent rooms that
/// aren't joined yet.
@@ -25,7 +28,8 @@ impl RoomPreview {
topic: info.topic.clone(),
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
num_joined_members: info.num_joined_members,
room_type: info.room_type.as_ref().map(|room_type| room_type.to_string()),
num_active_members: info.num_active_members,
room_type: info.room_type.as_ref().into(),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
@@ -33,6 +37,7 @@ impl RoomPreview {
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
})
}
@@ -45,6 +50,13 @@ impl RoomPreview {
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.leave().await.map_err(Into::into)
}
/// Get the user who created the invite, if any.
pub async fn inviter(&self) -> Option<RoomMember> {
let room = self.client.get_room(&self.inner.room_id)?;
let invite_details = room.invite_details().await.ok()?;
invite_details.inviter.and_then(|m| m.try_into().ok())
}
}
impl RoomPreview {
@@ -68,14 +80,18 @@ pub struct RoomPreviewInfo {
pub avatar_url: Option<String>,
/// The number of joined members.
pub num_joined_members: u64,
/// The number of active members, if known (joined + invited).
pub num_active_members: Option<u64>,
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: Option<String>,
pub room_type: RoomType,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
/// The membership state for the current user, if known.
pub membership: Option<Membership>,
/// The join rule for this room (private, public, knock, etc.).
pub join_rule: JoinRule,
/// Whether the room is direct or not, if known.
pub is_direct: Option<bool>,
}
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
@@ -97,3 +113,27 @@ impl TryFrom<SpaceRoomJoinRule> for JoinRule {
})
}
}
/// The type of room for a [`RoomPreviewInfo`].
#[derive(Debug, Clone, uniffi::Enum)]
pub enum RoomType {
/// It's a plain chat room.
Room,
/// It's a space that can group several rooms.
Space,
/// It's a custom implementation.
Custom { value: String },
}
impl From<Option<&RumaRoomType>> for RoomType {
fn from(value: Option<&RumaRoomType>) -> Self {
match value {
Some(RumaRoomType::Space) => RoomType::Space,
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
// SAFETY: this was checked in the match branch above
value: value.unwrap().to_string(),
},
_ => RoomType::Room,
}
}
}
+33 -60
View File
@@ -262,6 +262,23 @@ pub enum MessageType {
Other { msgtype: String, body: String },
}
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
/// If the filename field is present in a media message, clients should treat
/// body as a caption instead of a file name. Otherwise, the body is the
/// file name.
///
/// So:
/// - if a media has a filename and a caption, the body is the caption, filename
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = serde_json::Error;
@@ -273,35 +290,39 @@ impl TryFrom<MessageType> for RumaMessageType {
}))
}
MessageType::Image { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
RumaImageMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.raw_filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Image(event_content)
}
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
RumaAudioMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.raw_filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Audio(event_content)
}
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
RumaVideoMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.raw_filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Video(event_content)
}
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
RumaFileMessageEventContent::new(body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.raw_filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::File(event_content)
}
MessageType::Notice { content } => {
@@ -335,9 +356,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -347,9 +365,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -361,9 +376,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -373,9 +385,6 @@ impl From<RumaMessageType> for MessageType {
},
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
raw_filename: c.filename.clone(),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
@@ -452,15 +461,6 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -471,15 +471,6 @@ pub struct ImageMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -492,15 +483,6 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
@@ -511,15 +493,6 @@ pub struct VideoMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
/// The original body field, deserialized from the event. Prefer the use of
/// `filename` and `caption` over this.
pub body: String,
/// The original formatted body field, deserialized from the event. Prefer
/// the use of `filename` and `formatted_caption` over this.
pub formatted: Option<FormattedBody>,
/// The original filename field, deserialized from the event. Prefer the use
/// of `filename` over this.
pub raw_filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
+2 -2
View File
@@ -112,9 +112,9 @@ impl SyncServiceBuilder {
#[matrix_sdk_ffi_macros::export]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock(app_identifier);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
@@ -16,7 +16,7 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::room::MediaSource;
use ruma::events::{room::MediaSource, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
@@ -49,11 +49,18 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
}
Content::MembershipChange(membership) => TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
},
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
reason,
}
}
Content::ProfileChange(profile) => {
let (display_name, prev_display_name) = profile
@@ -161,6 +168,7 @@ pub enum TimelineItemContent {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
+55 -4
View File
@@ -81,6 +81,7 @@ use crate::{
mod content;
pub use content::MessageContent;
use matrix_sdk::utils::formatted_body_from;
use crate::error::QueueWedgeError;
@@ -270,7 +271,7 @@ impl Timeline {
msg: Arc<RoomMessageEventContentWithoutRelation>,
) -> Result<Arc<SendHandle>, ClientError> {
match self.inner.send((*msg).to_owned().with_relation(None).into()).await {
Ok(handle) => Ok(Arc::new(SendHandle { inner: Mutex::new(Some(handle)) })),
Ok(handle) => Ok(Arc::new(SendHandle::new(handle))),
Err(err) => {
error!("error when sending a message: {err}");
Err(anyhow::anyhow!(err).into())
@@ -289,6 +290,8 @@ impl Timeline {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let base_image_info = BaseImageInfo::try_from(&image_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
@@ -297,7 +300,7 @@ impl Timeline {
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
.formatted_caption(formatted_caption);
self.send_attachment(
url,
@@ -321,6 +324,8 @@ impl Timeline {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
@@ -351,6 +356,8 @@ impl Timeline {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
@@ -383,6 +390,8 @@ impl Timeline {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
@@ -409,15 +418,22 @@ impl Timeline {
self: Arc<Self>,
url: String,
file_info: FileInfo,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
use_send_queue: bool,
) -> Arc<SendAttachmentJoinHandle> {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let base_file_info: BaseFileInfo =
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::File(base_file_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(
url,
@@ -710,11 +726,18 @@ impl Timeline {
}
}
/// A handle to perform actions onto a local echo.
#[derive(uniffi::Object)]
pub struct SendHandle {
inner: Mutex<Option<matrix_sdk::send_queue::SendHandle>>,
}
impl SendHandle {
fn new(handle: matrix_sdk::send_queue::SendHandle) -> Self {
Self { inner: Mutex::new(Some(handle)) }
}
}
#[matrix_sdk_ffi_macros::export]
impl SendHandle {
/// Try to abort the sending of the current event.
@@ -732,10 +755,32 @@ impl SendHandle {
.await
.map_err(|err| anyhow::anyhow!("error when saving in store: {err}"))?)
} else {
warn!("trying to abort an send handle that's already been actioned");
warn!("trying to abort a send handle that's already been actioned");
Ok(false)
}
}
/// Attempt to manually resend messages that failed to send due to issues
/// that should now have been fixed.
///
/// This is useful for example, when there's a
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
/// the user may have re-verified on a different device and would now
/// like to send the failed message that's waiting on this device.
///
/// # Arguments
///
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo that should be unwedged.
pub async fn try_resend(self: Arc<Self>) -> Result<(), ClientError> {
let locked = self.inner.lock().await;
if let Some(handle) = locked.as_ref() {
handle.unwedge().await?;
} else {
warn!("trying to unwedge a send handle that's been aborted");
}
Ok(())
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -1273,4 +1318,10 @@ impl LazyTimelineItemProvider {
latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()),
}
}
/// For local echoes, return the associated send handle; returns `None` for
/// remote echoes.
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
}
}
+67 -11
View File
@@ -2,24 +2,80 @@
All notable changes to this project will be documented in this file.
# unreleased
## [0.8.0] - 2024-11-19
### Bug Fixes
- Add more invalid characters for room aliases.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
- `AmbiguityCache` contains the room member's user ID
- `AmbiguityCache` contains the room member's user ID.
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
request an animated thumbnail They both take a `MediaThumbnailSettings`
instead of `MediaThumbnailSize`.
- Consider knocked members to be part of the room for display name
disambiguation.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
- Allow aborting media uploads.
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
### Refactor
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
- Rename `AmbiguityMap` to `DisplayNameUsers`.
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
don't acquire the lock for every room they read.
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- `Client::get_stripped_rooms` has finally been removed.
- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail
- They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`.
- The `StateStore` methods to access data in the media cache where moved to a separate
`EventCacheStore` trait.
- The `instant` module was removed, use the `ruma::time` module instead.
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
- [**breaking**] The `StateStore` methods to access data in the media cache
where moved to a separate `EventCacheStore` trait.
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
# 0.7.0
+5 -2
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.8.0"
[package.metadata.docs.rs]
all-features = true
@@ -50,6 +50,7 @@ assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.4.0", features = ["serde"] }
decancer = "3.2.4"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
@@ -60,14 +61,15 @@ matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
regex = "1.11.0"
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
unicode-normalization = "0.1.24"
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uniffi = { workspace = true, optional = true }
regex = "1.11.1"
[dev-dependencies]
assert_matches = { workspace = true }
@@ -77,6 +79,7 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
similar-asserts = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+34 -29
View File
@@ -16,15 +16,13 @@
#[cfg(feature = "e2e-encryption")]
use std::sync::Arc;
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt, iter,
ops::Deref,
};
use eyeball::{SharedObservable, Subscriber};
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::{Vector, VectorDiff};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::Stream;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
@@ -70,9 +68,9 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
error::{Error, Result},
event_cache_store::EventCacheStoreLock,
event_cache::store::EventCacheStoreLock,
response_processors::AccountDataProcessor,
rooms::{
normal::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons},
@@ -139,11 +137,6 @@ impl fmt::Debug for BaseClient {
}
impl BaseClient {
/// Create a new default client.
pub fn new() -> Self {
BaseClient::with_store_config(StoreConfig::default())
}
/// Create a new client.
///
/// # Arguments
@@ -173,8 +166,12 @@ impl BaseClient {
/// Clones the current base client to use the same crypto store but a
/// different, in-memory store config, and resets transient state.
#[cfg(feature = "e2e-encryption")]
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
let config = StoreConfig::new().state_store(MemoryStore::new());
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder_name: &str,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
.state_store(MemoryStore::new());
let config = config.crypto_store(self.crypto_store.clone());
let copy = Self {
@@ -207,8 +204,12 @@ impl BaseClient {
/// different, in-memory store config, and resets transient state.
#[cfg(not(feature = "e2e-encryption"))]
#[allow(clippy::unused_async)]
pub async fn clone_with_in_memory_state_store(&self) -> Result<Self> {
let config = StoreConfig::new().state_store(MemoryStore::new());
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder: &str,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
.state_store(MemoryStore::new());
Ok(Self::with_store_config(config))
}
@@ -233,7 +234,6 @@ impl BaseClient {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
#[cfg(not(target_arch = "wasm32"))]
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
self.store.rooms_stream()
}
@@ -1332,7 +1332,7 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
let mut user_ids = BTreeSet::new();
let mut ambiguity_map: BTreeMap<String, BTreeSet<OwnedUserId>> = BTreeMap::new();
let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
for raw_event in &response.chunk {
let member = match raw_event.deserialize() {
@@ -1363,7 +1363,11 @@ impl BaseClient {
if let StateEvent::Original(e) = &member {
if let Some(d) = &e.content.displayname {
ambiguity_map.entry(d.clone()).or_default().insert(member.state_key().clone());
let display_name = DisplayName::new(d);
ambiguity_map
.entry(display_name)
.or_default()
.insert(member.state_key().clone());
}
}
@@ -1689,12 +1693,6 @@ impl BaseClient {
}
}
impl Default for BaseClient {
fn default() -> Self {
Self::new()
}
}
fn handle_room_member_event_for_profiles(
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
@@ -1737,8 +1735,9 @@ mod tests {
use super::BaseClient;
use crate::{
store::StateStoreExt, test_utils::logged_in_base_client, DisplayName, RoomState,
SessionMeta,
store::{StateStoreExt, StoreConfig},
test_utils::logged_in_base_client,
RoomDisplayName, RoomState, SessionMeta,
};
#[async_test]
@@ -1869,7 +1868,7 @@ mod tests {
assert_eq!(room.state(), RoomState::Invited);
assert_eq!(
room.compute_display_name().await.expect("fetching display name failed"),
DisplayName::Calculated("Kyra".to_owned())
RoomDisplayName::Calculated("Kyra".to_owned())
);
}
@@ -1945,7 +1944,9 @@ mod tests {
let user_id = user_id!("@alice:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -2003,7 +2004,9 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -2063,7 +2066,9 @@ mod tests {
let inviter_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
@@ -14,9 +14,11 @@
//! SDK-specific variations of response types from Ruma.
use std::{collections::BTreeMap, fmt, iter};
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
pub use matrix_sdk_common::deserialized_responses::*;
use once_cell::sync::Lazy;
use regex::Regex;
use ruma::{
events::{
room::{
@@ -31,6 +33,7 @@ use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
@@ -67,6 +70,178 @@ pub struct AmbiguityChanges {
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::MXID_PATTERN)
.expect("We should be able to create a regex from our static MXID pattern")
});
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
.expect("We should be able to create a regex from our static left-to-right pattern")
});
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
.expect("We should be able to create a regex from our static hidden characters pattern")
});
/// Regex to match `i` characters.
///
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
});
/// Regex to match `0` characters.
///
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
});
/// Regex to match a couple of dot-like characters, also matches an actual dot.
///
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
/// ambiguous.
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
});
/// A high-level wrapper for strings representing display names.
///
/// This wrapper provides attempts to determine whether a display name
/// contains characters that could make it ambiguous or easily confused
/// with similar names.
///
///
/// # Examples
///
/// ```
/// use matrix_sdk_base::deserialized_responses::DisplayName;
///
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
///
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
/// ```
///
/// ```
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
/// let display_name = DisplayName::new("@alice:localhost");
///
/// // The display name looks like an MXID, which makes it ambiguous.
/// assert!(display_name.is_inherently_ambiguous());
/// ```
#[derive(Debug, Clone, Eq)]
pub struct DisplayName {
raw: String,
decancered: Option<String>,
}
impl Hash for DisplayName {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(decancered) = &self.decancered {
decancered.hash(state);
} else {
self.raw.hash(state);
}
}
}
impl PartialEq for DisplayName {
fn eq(&self, other: &Self) -> bool {
match (self.decancered.as_deref(), other.decancered.as_deref()) {
(None, None) => self.raw == other.raw,
(None, Some(_)) | (Some(_), None) => false,
(Some(this), Some(other)) => this == other,
}
}
}
impl DisplayName {
/// Regex pattern matching an MXID.
const MXID_PATTERN: &str = "@.+[:.].+";
/// Regex pattern matching some left-to-right formatting marks:
/// * LTR and RTL marks U+200E and U+200F
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
/// Regex pattern matching bunch of unicode control characters and otherwise
/// misleading/invisible characters.
///
/// This includes:
/// * various width spaces U+2000 - U+200D
/// * Combining characters U+0300 - U+036F
/// * Blank/invisible characters (U2800, U2062-U2063)
/// * Arabic Letter RTL mark U+061C
/// * Zero width no-break space (BOM) U+FEFF
const HIDDEN_CHARACTERS_PATTERN: &str =
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
/// Creates a new [`DisplayName`] from the given raw string.
///
/// The raw display name is transformed into a Unicode-normalized form, with
/// common confusable characters removed to reduce ambiguity.
///
/// **Note**: If removing confusable characters fails,
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
/// [`DisplayName::as_normalized_str()`] will return `None.
pub fn new(raw: &str) -> Self {
let normalized = raw.nfd().collect::<String>();
let replaced = DOT_REGEX.replace_all(&normalized, ":");
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
// We re-run the dot replacement because decancer normalized a lot of weird
// characets into a `.`, it just doesn't do that for /u{1d16d}.
let replaced = DOT_REGEX.replace_all(&replaced, ":");
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
replaced.to_string()
});
Self { raw: raw.to_owned(), decancered }
}
/// Is this display name considered to be ambiguous?
///
/// If the display name has cancer (i.e. fails normalisation or has a
/// different normalised form) or looks like an MXID, then it's ambiguous.
pub fn is_inherently_ambiguous(&self) -> bool {
// If we look like an MXID or have hidden characters then we're ambiguous.
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
}
/// Returns the underlying raw and and unsanitized string of this
/// [`DisplayName`].
pub fn as_raw_str(&self) -> &str {
&self.raw
}
/// Returns the underlying normalized and and sanitized string of this
/// [`DisplayName`].
///
/// Returns `None` if normalization failed during construction of this
/// [`DisplayName`].
pub fn as_normalized_str(&self) -> Option<&str> {
self.decancered.as_deref()
}
fn has_hidden_characters(&self) -> bool {
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
}
fn looks_like_an_mxid(&self) -> bool {
self.decancered
.as_deref()
.map(|d| MXID_REGEX.is_match(d))
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
}
}
/// A deserialized response for the rooms members API call.
///
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
@@ -294,10 +469,12 @@ impl MemberEvent {
///
/// It there is no `displayname` in the event's content, the localpart or
/// the user ID is returned.
pub fn display_name(&self) -> &str {
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart())
pub fn display_name(&self) -> DisplayName {
DisplayName::new(
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart()),
)
}
}
@@ -310,3 +487,240 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
}
}
}
#[cfg(test)]
mod test {
macro_rules! assert_display_name_eq {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
similar_asserts::assert_eq!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_display_name_ne {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
assert_ne!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
name.is_inherently_ambiguous(),
"The display {:?} should be considered amgibuous",
name
);
};
}
macro_rules! assert_not_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
!name.is_inherently_ambiguous(),
"The display {:?} should not be considered amgibuous",
name
);
};
}
#[test]
fn test_display_name_inherently_ambiguous() {
// These should not be inherently ambiguous, only if another similarly looking
// display name appears should they be considered to be ambiguous.
assert_not_ambiguous!("Alice");
assert_not_ambiguous!("Carol");
assert_not_ambiguous!("Car0l");
assert_not_ambiguous!("Ivan");
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
assert_not_ambiguous!("Sahasrahla");
// Left to right is fine, if it's the only one in the room.
assert_not_ambiguous!("\u{202e}alharsahas");
// These on the other hand contain invisible chars.
assert_ambiguous!("Sa̴hasrahla");
assert_ambiguous!("Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_capitalization() {
// Display name with different capitalization
assert_display_name_eq!("Alice", "alice");
}
#[test]
fn test_display_name_equality_different_names() {
// Different display names
assert_display_name_ne!("Alice", "Carol");
}
#[test]
fn test_display_name_equality_capital_l() {
// Different display names
assert_display_name_eq!("Hello", "HeIlo");
}
#[test]
fn test_display_name_equality_confusable_zero() {
// Different display names
assert_display_name_eq!("Carol", "Car0l");
}
#[test]
fn test_display_name_equality_cyrilic() {
// Display name with scritpure symbols
assert_display_name_eq!("alice", "аlice");
}
#[test]
fn test_display_name_equality_scriptures() {
// Display name with scritpure symbols
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
}
#[test]
fn test_display_name_equality_frakturs() {
// Display name with fraktur symbols
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
}
#[test]
fn test_display_name_equality_circled() {
// Display name with circled symbols
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
}
#[test]
fn test_display_name_equality_squared() {
// Display name with squared symbols
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
}
#[test]
fn test_display_name_equality_big_unicode() {
// Display name with big unicode letters
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
}
#[test]
fn test_display_name_equality_left_to_right() {
// Display name with a left-to-right character
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
}
#[test]
fn test_display_name_equality_diacritical() {
// Display name with a diacritical mark.
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
}
#[test]
fn test_display_name_equality_zero_width_joiner() {
// Display name with a zero-width joiner
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
}
#[test]
fn test_display_name_equality_zero_width_space() {
// Display name with zero-width space.
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_ligatures() {
// Display name with a ligature.
assert_display_name_eq!("ff", "\u{FB00}");
}
#[test]
fn test_display_name_confusable_mxid_colon() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid\u{0589}domain.tld");
assert_ambiguous!("@mxid\u{05c3}domain.tld");
assert_ambiguous!("@mxid\u{0703}domain.tld");
assert_ambiguous!("@mxid\u{0a83}domain.tld");
assert_ambiguous!("@mxid\u{16ec}domain.tld");
assert_ambiguous!("@mxid\u{205a}domain.tld");
assert_ambiguous!("@mxid\u{2236}domain.tld");
assert_ambiguous!("@mxid\u{fe13}domain.tld");
assert_ambiguous!("@mxid\u{fe52}domain.tld");
assert_ambiguous!("@mxid\u{fe30}domain.tld");
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
}
#[test]
fn test_display_name_confusable_mxid_dot() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain\u{0701}tld");
assert_ambiguous!("@mxid:domain\u{0702}tld");
assert_ambiguous!("@mxid:domain\u{2024}tld");
assert_ambiguous!("@mxid:domain\u{fe52}tld");
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_a() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_l() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain.tId");
assert_ambiguous!("@mxid:domain.t\u{217c}d");
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
assert_ambiguous!("@mxid:domain.t\u{2223}d");
}
}
@@ -0,0 +1,30 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
pub mod store;
/// The kind of event the event storage holds.
pub type Event = SyncTimelineEvent;
/// The kind of gap the event storage holds.
#[derive(Clone, Debug)]
pub struct Gap {
/// The token to use in the query, extracted from a previous "from" /
/// "end" field of a `/messages` response.
pub prev_token: String,
}
@@ -193,7 +193,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache_store::{
/// # use matrix_sdk_base::event_cache::store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
@@ -217,7 +217,9 @@ macro_rules! event_cache_store_integration_tests {
() => {
mod event_cache_store_integration_tests {
use matrix_sdk_test::async_test;
use $crate::event_cache_store::{EventCacheStoreIntegrationTests, IntoEventCacheStore};
use $crate::event_cache::store::{
EventCacheStoreIntegrationTests, IntoEventCacheStore,
};
use super::get_event_cache_store;
@@ -249,7 +251,7 @@ macro_rules! event_cache_store_integration_tests_time {
use std::time::Duration;
use matrix_sdk_test::async_test;
use $crate::event_cache_store::IntoEventCacheStore;
use $crate::event_cache::store::IntoEventCacheStore;
use super::get_event_cache_store;
@@ -60,7 +60,10 @@ impl fmt::Debug for EventCacheStoreLock {
impl EventCacheStoreLock {
/// Create a new lock around the [`EventCacheStore`].
pub fn new<S>(store: S, key: String, holder: String) -> Self
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessStoreLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
where
S: IntoEventCacheStore,
{
@@ -69,7 +72,7 @@ impl EventCacheStoreLock {
Self {
cross_process_lock: CrossProcessStoreLock::new(
LockableEventCacheStore(store.clone()),
key,
"default".to_owned(),
holder,
),
store,
@@ -98,6 +98,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// This should not raise an error when the `uri` parameter points to an
/// unknown media, and it should return an Ok result in this case.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
+2 -2
View File
@@ -28,7 +28,7 @@ mod client;
pub mod debug;
pub mod deserialized_responses;
mod error;
pub mod event_cache_store;
pub mod event_cache;
pub mod latest_event;
pub mod media;
pub mod notification_settings;
@@ -56,7 +56,7 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo,
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
RoomStateFilter,
};
+8 -5
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeSet, HashMap},
sync::Arc,
};
@@ -30,7 +30,8 @@ use ruma::{
};
use crate::{
deserialized_responses::{MemberEvent, SyncOrStrippedState},
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
store::ambiguity_map::is_display_name_ambiguous,
MinimalRoomMemberEvent,
};
@@ -67,8 +68,10 @@ impl RoomMember {
} = room_info;
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
let display_name_ambiguous =
users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1);
let display_name = event.display_name();
let display_name_ambiguous = users_display_names
.get(&display_name)
.is_some_and(|s| is_display_name_ambiguous(&display_name, s));
let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
Self {
@@ -245,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> {
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
pub(crate) max_power_level: i64,
pub(crate) room_creator: Option<OwnedUserId>,
pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet<OwnedUserId>>,
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
}
+26 -12
View File
@@ -50,7 +50,7 @@ use crate::MinimalStateEvent;
/// The name of the room, either from the metadata or calculated
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DisplayName {
pub enum RoomDisplayName {
/// The room has been named explicitly as
Named(String),
/// The room has a canonical alias that should be used
@@ -66,9 +66,9 @@ pub enum DisplayName {
}
const WHITESPACE_REGEX: &str = r"\s+";
const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+";
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
impl DisplayName {
impl RoomDisplayName {
/// Transforms the current display name into the name part of a
/// `RoomAliasId`.
pub fn to_room_alias_name(&self) -> String {
@@ -97,14 +97,16 @@ impl DisplayName {
}
}
impl fmt::Display for DisplayName {
impl fmt::Display for RoomDisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
RoomDisplayName::Named(s)
| RoomDisplayName::Calculated(s)
| RoomDisplayName::Aliased(s) => {
write!(f, "{s}")
}
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
DisplayName::Empty => write!(f, "Empty Room"),
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
RoomDisplayName::Empty => write!(f, "Empty Room"),
}
}
}
@@ -574,7 +576,7 @@ mod tests {
use ruma::events::tag::{TagInfo, TagName, Tags};
use super::{BaseRoomInfo, RoomNotableTags};
use crate::DisplayName;
use crate::RoomDisplayName;
#[test]
fn test_handle_notable_tags_favourite() {
@@ -608,21 +610,33 @@ mod tests {
#[test]
fn test_room_alias_from_room_display_name_lowercases() {
assert_eq!("roomalias", DisplayName::Named("RoomAlias".to_owned()).to_room_alias_name());
assert_eq!(
"roomalias",
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_whitespace() {
assert_eq!("room-alias", DisplayName::Named("Room Alias".to_owned()).to_room_alias_name());
assert_eq!(
"room-alias",
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
assert_eq!("roomalias", DisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name());
assert_eq!(
"roomalias",
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
);
}
#[test]
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
assert_eq!("roomalias", DisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name());
assert_eq!(
"roomalias",
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
);
}
}
+56 -48
View File
@@ -61,13 +61,13 @@ use tokio::sync::broadcast;
use tracing::{debug, field::debug, info, instrument, warn};
use super::{
members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent,
members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
RoomMember, RoomNotableTags,
};
#[cfg(feature = "experimental-sliding-sync")]
use crate::latest_event::LatestEvent;
use crate::{
deserialized_responses::{MemberEvent, RawSyncOrStrippedState},
deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState},
notification_settings::RoomNotificationMode,
read_receipts::RoomReadReceipts,
store::{DynStateStore, Result as StoreResult, StateStoreExt},
@@ -572,8 +572,8 @@ impl Room {
/// [`Self::cached_display_name`].
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
pub async fn compute_display_name(&self) -> StoreResult<DisplayName> {
let update_cache = |new_val: DisplayName| {
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
let update_cache = |new_val: RoomDisplayName| {
self.inner.update_if(|info| {
if info.cached_display_name.as_ref() != Some(&new_val) {
info.cached_display_name = Some(new_val.clone());
@@ -591,13 +591,13 @@ impl Room {
if let Some(name) = inner.name() {
let name = name.trim().to_owned();
drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`.
return Ok(update_cache(DisplayName::Named(name)));
return Ok(update_cache(RoomDisplayName::Named(name)));
}
if let Some(alias) = inner.canonical_alias() {
let alias = alias.alias().trim().to_owned();
drop(inner); // See above comment.
return Ok(update_cache(DisplayName::Aliased(alias)));
return Ok(update_cache(RoomDisplayName::Aliased(alias)));
}
inner.summary.clone()
@@ -703,7 +703,7 @@ impl Room {
///
/// This cache is refilled every time we call
/// [`Self::compute_display_name`].
pub fn cached_display_name(&self) -> Option<DisplayName> {
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
self.inner.read().cached_display_name.clone()
}
@@ -819,8 +819,7 @@ impl Room {
})
.collect::<BTreeMap<_, _>>();
let display_names =
member_events.iter().map(|e| e.display_name().to_owned()).collect::<Vec<_>>();
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
let room_info = self.member_room_info(&display_names).await?;
let mut members = Vec::new();
@@ -900,7 +899,7 @@ impl Room {
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let display_names = [event.display_name().to_owned()];
let display_names = [event.display_name()];
let room_info = self.member_room_info(&display_names).await?;
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
@@ -911,7 +910,7 @@ impl Room {
/// Async because it can read from storage.
async fn member_room_info<'a>(
&self,
display_names: &'a [String],
display_names: &'a [DisplayName],
) -> StoreResult<MemberRoomInfo<'a>> {
let max_power_level = self.max_power_level();
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
@@ -1103,7 +1102,7 @@ pub struct RoomInfo {
/// Filled by calling [`Room::compute_display_name`]. It's automatically
/// filled at start when creating a room, or on every successful sync.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cached_display_name: Option<DisplayName>,
pub(crate) cached_display_name: Option<RoomDisplayName>,
/// Cached user defined notification mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1767,7 +1766,10 @@ impl RoomStateFilter {
/// Calculate room name according to step 3 of the [naming algorithm].
///
/// [naming algorithm]: https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room
fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&str>) -> DisplayName {
fn compute_display_name_from_heroes(
num_joined_invited: u64,
mut heroes: Vec<&str>,
) -> RoomDisplayName {
let num_heroes = heroes.len() as u64;
let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
@@ -1789,12 +1791,12 @@ fn compute_display_name_from_heroes(num_joined_invited: u64, mut heroes: Vec<&st
// User is alone.
if num_joined_invited <= 1 {
if names.is_empty() {
DisplayName::Empty
RoomDisplayName::Empty
} else {
DisplayName::EmptyWas(names)
RoomDisplayName::EmptyWas(names)
}
} else {
DisplayName::Calculated(names)
RoomDisplayName::Calculated(names)
}
}
@@ -1850,8 +1852,8 @@ mod tests {
use crate::latest_event::LatestEvent;
use crate::{
rooms::RoomNotableTags,
store::{IntoStateStore, MemoryStore, StateChanges, StateStore},
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent,
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
RoomInfoNotableUpdateReasons, SessionMeta,
};
@@ -2126,7 +2128,7 @@ mod tests {
assert_eq!(
info.cached_display_name.as_ref(),
Some(&DisplayName::Calculated("lol".to_owned())),
Some(&RoomDisplayName::Calculated("lol".to_owned())),
);
assert_eq!(
info.cached_user_defined_notification_mode.as_ref(),
@@ -2138,7 +2140,9 @@ mod tests {
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -2216,7 +2220,9 @@ mod tests {
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -2331,7 +2337,7 @@ mod tests {
#[async_test]
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
let (_, room) = make_room_test_helper(RoomState::Joined);
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2341,7 +2347,7 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
}
@@ -2352,20 +2358,20 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Named("Test Room".to_owned())
RoomDisplayName::Named("Test Room".to_owned())
);
}
#[async_test]
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
let (_, room) = make_room_test_helper(RoomState::Invited);
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2378,7 +2384,7 @@ mod tests {
});
room.inner.update(|info| info.base_info.name = Some(room_name));
assert_eq!(room.compute_display_name().await.unwrap(), DisplayName::Empty);
assert_eq!(room.compute_display_name().await.unwrap(), RoomDisplayName::Empty);
}
#[async_test]
@@ -2388,7 +2394,7 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
}
@@ -2399,13 +2405,13 @@ mod tests {
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Aliased("test".to_owned())
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Named("Test Room".to_owned())
RoomDisplayName::Named("Test Room".to_owned())
);
}
@@ -2447,7 +2453,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2469,7 +2475,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2499,7 +2505,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2524,7 +2530,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Matthew".to_owned())
RoomDisplayName::Calculated("Matthew".to_owned())
);
}
@@ -2579,7 +2585,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
);
}
@@ -2628,7 +2634,7 @@ mod tests {
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
);
}
@@ -2658,7 +2664,7 @@ mod tests {
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap(),
DisplayName::EmptyWas("Matthew".to_owned())
RoomDisplayName::EmptyWas("Matthew".to_owned())
);
}
@@ -2672,7 +2678,9 @@ mod tests {
use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
// Given a room,
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
client
.set_session_meta(
@@ -3054,34 +3062,34 @@ mod tests {
#[test]
fn test_calculate_room_name() {
let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
actual = compute_display_name_from_heroes(5, vec![]);
assert_eq!(DisplayName::Calculated("5 people".to_owned()), actual);
assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
actual = compute_display_name_from_heroes(0, vec![]);
assert_eq!(DisplayName::Empty, actual);
assert_eq!(RoomDisplayName::Empty, actual);
actual = compute_display_name_from_heroes(1, vec![]);
assert_eq!(DisplayName::Empty, actual);
assert_eq!(RoomDisplayName::Empty, actual);
actual = compute_display_name_from_heroes(1, vec!["a"]);
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
}
#[test]
@@ -695,6 +695,10 @@ async fn cache_latest_events(
changes: Option<&StateChanges>,
store: Option<&Store>,
) {
use crate::{
deserialized_responses::DisplayName, store::ambiguity_map::is_display_name_ambiguous,
};
let mut encrypted_events =
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
@@ -752,11 +756,13 @@ async fn cache_latest_events(
.as_original()
.and_then(|profile| profile.content.displayname.as_ref())
.and_then(|display_name| {
let display_name = DisplayName::new(display_name);
changes.ambiguity_maps.get(room.room_id()).and_then(
|map_for_room| {
map_for_room
.get(display_name)
.map(|user_ids| user_ids.len() > 1)
map_for_room.get(&display_name).map(|users| {
is_display_name_ambiguous(&display_name, users)
})
},
)
});
+346 -89
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
sync::Arc,
};
@@ -24,28 +24,24 @@ use ruma::{
},
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tracing::trace;
use tracing::{instrument, trace};
use super::{DynStateStore, Result, StateChanges};
use crate::{
deserialized_responses::{AmbiguityChange, RawMemberEvent},
deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent},
store::StateStoreExt,
};
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[derive(Debug)]
struct AmbiguityMap {
display_name: String,
/// A map of users that use a certain display name.
#[derive(Debug, Clone)]
struct DisplayNameUsers {
display_name: DisplayName,
users: BTreeSet<OwnedUserId>,
}
impl AmbiguityMap {
impl DisplayNameUsers {
/// Remove the given [`UserId`] from the map, marking that the [`UserId`]
/// doesn't use the display name anymore.
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
self.users.remove(user_id);
@@ -56,6 +52,8 @@ impl AmbiguityMap {
}
}
/// Add the given [`UserId`] from the map, marking that the [`UserId`]
/// is using the display name.
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
let ambiguous_user =
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
@@ -65,46 +63,73 @@ impl AmbiguityMap {
ambiguous_user
}
/// How many users are using this display name.
fn user_count(&self) -> usize {
self.users.len()
}
/// Is the display name considered to be ambiguous.
fn is_ambiguous(&self) -> bool {
self.user_count() > 1
is_display_name_ambiguous(&self.display_name, &self.users)
}
}
fn is_member_active(membership: &MembershipState) -> bool {
use MembershipState::*;
matches!(membership, Join | Invite | Knock)
}
#[derive(Debug)]
pub(crate) struct AmbiguityCache {
pub store: Arc<DynStateStore>,
pub cache: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[instrument(ret)]
pub(crate) fn is_display_name_ambiguous(
display_name: &DisplayName,
users_with_display_name: &BTreeSet<OwnedUserId>,
) -> bool {
trace!("Checking if a display name is ambiguous");
display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1
}
impl AmbiguityCache {
/// Create a new [`AmbiguityCache`] backed by the given state store.
pub fn new(store: Arc<DynStateStore>) -> Self {
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
}
/// Handle a newly received [`SyncRoomMemberEvent`] for the given room.
pub async fn handle_event(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<()> {
// Synapse seems to have a bug where it puts the same event into the
// state and the timeline sometimes.
// Synapse seems to have a bug where it puts the same event into the state and
// the timeline sometimes.
//
// Since our state, e.g. the old display name, already ended up inside
// the state changes and we're pulling stuff out of the cache if it's
// there calculating this twice for the same event will result in an
// incorrect AmbiguityChange overwriting the correct one. In other
// words, this method is not idempotent so we make it by ignoring
// duplicate events.
// Since our state, e.g. the old display name, already ended up inside the state
// changes and we're pulling stuff out of the cache if it's there calculating
// this twice for the same event will result in an incorrect AmbiguityChange
// overwriting the correct one. In other words, this method is not idempotent so
// we make it by ignoring duplicate events.
if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) {
return Ok(());
}
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
let (mut old_map, mut new_map) =
self.calculate_changes(changes, room_id, member_event).await?;
let display_names_same = match (&old_map, &new_map) {
(Some(a), Some(b)) => a.display_name == b.display_name,
_ => false,
};
// If the user's display name didn't change, then there's nothing more to
// calculate here.
if display_names_same {
return Ok(());
}
@@ -126,16 +151,21 @@ impl AmbiguityCache {
trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}");
self.add_change(room_id, member_event.event_id().to_owned(), change);
self.changes
.entry(room_id.to_owned())
.or_default()
.insert(member_event.event_id().to_owned(), change);
Ok(())
}
/// Update the [`AmbiguityCache`] state for the given room with a pair of
/// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`].
fn update(
&mut self,
room_id: &RoomId,
old_map: Option<AmbiguityMap>,
new_map: Option<AmbiguityMap>,
old_map: Option<DisplayNameUsers>,
new_map: Option<DisplayNameUsers>,
) {
let entry = self.cache.entry(room_id.to_owned()).or_default();
@@ -148,74 +178,102 @@ impl AmbiguityCache {
}
}
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
/// Get the previously used display name, if any, of the member described in
/// the given new [`SyncRoomMemberEvent`].
async fn get_old_display_name(
&self,
changes: &StateChanges,
room_id: &RoomId,
new_event: &SyncRoomMemberEvent,
) -> Result<Option<String>> {
let user_id = new_event.state_key();
let old_event = if let Some(m) = changes
.state
.get(room_id)
.and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str()))
{
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, user_id).await?
};
let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) };
if is_member_active(old_event.membership()) {
let display_name = if let Some(d) = changes
.profiles
.get(room_id)
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
{
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, user_id)
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
old_event.original_content().and_then(|c| c.displayname.clone())
};
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
} else {
Ok(None)
}
}
async fn get(
/// Get the [`DisplayNameUsers`] for the given display name in the given
/// room.
///
/// This method will get the [`DisplayNameUsers`] from the cache, if the
/// cache doesn't contain such an entry, it falls back to the state
/// store.
async fn get_users_with_display_name(
&mut self,
room_id: &RoomId,
display_name: &DisplayName,
) -> Result<DisplayNameUsers> {
Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) {
DisplayNameUsers { display_name: display_name.clone(), users: u.clone() }
} else {
let users_with_display_name =
self.store.get_users_with_display_name(room_id, display_name).await?;
DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name }
})
}
/// Calculate the change in the users that use a display name a
/// [`SyncRoomMemberEvent`] will cause for a given room.
///
/// Returns the [`DisplayNameUsers`] before the member event is applied and
/// the [`DisplayNameUsers`] after the member event is applied to the
/// room state.
async fn calculate_changes(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &SyncRoomMemberEvent,
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
use MembershipState::*;
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
}) {
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, member_event.state_key()).await?
};
// FIXME: Use let chains once stable
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
if matches!(event.membership(), Join | Invite) {
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
}) {
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, member_event.state_key())
.await?
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
event.original_content().and_then(|c| c.displayname.clone())
};
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
} else {
None
}
} else {
None
};
) -> Result<(Option<DisplayNameUsers>, Option<DisplayNameUsers>)> {
let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?;
let old_map = if let Some(old_name) = old_display_name.as_deref() {
let old_display_name_map =
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
u.clone()
} else {
self.store.get_users_with_display_name(room_id, old_name).await?
};
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
let old_display_name = DisplayName::new(old_name);
Some(self.get_users_with_display_name(room_id, &old_display_name).await?)
} else {
None
};
let new_map = if matches!(member_event.membership(), Join | Invite) {
let new_map = if is_member_active(member_event.membership()) {
let new = member_event
.as_original()
.and_then(|ev| ev.content.displayname.as_deref())
.unwrap_or_else(|| member_event.state_key().localpart());
// We don't allow other users to set the display name, so if we
// have a more trusted version of the display
// name use that.
// We don't allow other users to set the display name, so if we have a more
// trusted version of the display name use that.
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
new
} else if let Some(old) = old_display_name.as_deref() {
@@ -224,22 +282,221 @@ impl AmbiguityCache {
new
};
let new_display_name_map = if let Some(u) =
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
{
u.clone()
} else {
self.store.get_users_with_display_name(room_id, new_display_name).await?
};
let new_display_name = DisplayName::new(new_display_name);
Some(AmbiguityMap {
display_name: new_display_name.to_owned(),
users: new_display_name_map,
})
Some(self.get_users_with_display_name(room_id, &new_display_name).await?)
} else {
None
};
Ok((old_map, new_map))
}
#[cfg(test)]
fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool {
self.cache
.get(room_id)
.and_then(|display_names| {
display_names
.get(display_name)
.map(|user_ids| is_display_name_ambiguous(display_name, user_ids))
})
.unwrap_or_else(|| {
panic!(
"The display name {:?} should be part of the cache {:?}",
display_name, self.cache
)
})
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::async_test;
use ruma::{room_id, server_name, user_id, EventId};
use serde_json::json;
use super::*;
use crate::store::{IntoStateStore, MemoryStore};
fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent {
let server_name = server_name!("localhost");
serde_json::from_value(json!({
"content": {
"displayname": display_name,
"membership": "join"
},
"event_id": EventId::new(server_name),
"origin_server_ts": 152037280,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
}))
.expect("We should be able to deserialize the static member event")
}
macro_rules! assert_ambiguity {
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)?
) => {
assert_ambiguity!(
[ $( ($user, $display_name) ),* ],
[ $( ($check_display_name, $ambiguous) ),* ],
"The test failed the ambiguity assertions"
)
};
(
[ $( ($user:literal, $display_name:literal) ),* ],
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ],
$description:literal $(,)?
) => {
let store = MemoryStore::new();
let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store());
let changes = Default::default();
let room_id = room_id!("!foo:bar");
macro_rules! add_display_name {
($u:literal, $n:literal) => {
let event = generate_event(user_id!($u), $n);
ambiguity_cache
.handle_event(&changes, room_id, &event)
.await
.expect("We should be able to handle a member event to calculate the ambiguity.");
};
}
macro_rules! assert_display_name_ambiguity {
($n:literal, $a:expr) => {
let display_name = DisplayName::new($n);
if ambiguity_cache.check(room_id, &display_name) != $a {
let foo = if $a { "be" } else { "not be" };
panic!("{}: the display name {} should {} ambiguous", $description, $n, foo);
}
};
}
$(
add_display_name!($user, $display_name);
)*
$(
assert_display_name_ambiguity!($check_display_name, $ambiguous);
)*
};
}
#[async_test]
async fn test_disambiguation() {
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("alice", false)],
"Alice is alone in the room"
);
assert_ambiguity!(
[("@alice:localhost", "alice")],
[("Alice", false)],
"Alice is alone in the room and has a capitalized display name"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "alice")],
[("alice", true)],
"Alice and bob share a display name"
);
assert_ambiguity!(
[
("@alice:localhost", "alice"),
("@bob:localhost", "alice"),
("@carol:localhost", "carol")
],
[("alice", true), ("carol", false)],
"Alice and Bob share a display name, while Carol is unique"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")],
[("alice", true)],
"Alice and Bob share a display name that is differently capitalized"
);
assert_ambiguity!(
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
[("alice", true)],
"Bob tries to impersonate Alice using a cyrilic а"
);
assert_ambiguity!(
[("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")],
[("@bob:localhost", true)],
"Alice tries to impersonate bob using an mxid"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using scripture symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using fraktur symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using circled symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using squared symbols"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using big unicode letters"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using left to right shenanigans"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a diacritical mark"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")],
[("Sahasrahla", true)],
"Bob tries to impersonate Alice using a zero-width space"
);
assert_ambiguity!(
[("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")],
[("ff", true)],
"Bob tries to impersonate Alice using a ligature"
);
}
}
@@ -1,6 +1,6 @@
//! Trait and macro of integration tests for StateStore implementations.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
@@ -34,7 +34,8 @@ use ruma::{
use serde_json::{json, value::Value as JsonValue};
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DynStateStore, ServerCapabilities,
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
ServerCapabilities,
};
use crate::{
deserialized_responses::MemberEvent,
@@ -85,6 +86,8 @@ pub trait StateStoreIntegrationTests {
async fn test_display_names_saving(&self);
/// Test operations with the send queue.
async fn test_send_queue(&self);
/// Test priority of operations with the send queue.
async fn test_send_queue_priority(&self);
/// Test operations related to send queue dependents.
async fn test_send_queue_dependents(&self);
/// Test saving/restoring server capabilities.
@@ -139,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore {
room.handle_state_event(&topic_event);
changes.add_state_event(room_id, topic_event, topic_raw);
let mut room_ambiguity_map = BTreeMap::new();
let mut room_ambiguity_map = HashMap::new();
let mut room_profiles = BTreeMap::new();
let member_json: &JsonValue = &test_json::MEMBER;
let member_event: SyncRoomMemberEvent =
serde_json::from_value(member_json.clone()).unwrap();
let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap();
let displayname = DisplayName::new(
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
);
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
room_profiles.insert(user_id.to_owned(), (&member_event).into());
@@ -254,6 +259,8 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_populate_store(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
self.populate().await?;
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
@@ -288,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"Expected to find 1 joined user ids"
);
assert_eq!(
self.get_users_with_display_name(room_id, "example").await?.len(),
self.get_users_with_display_name(room_id, &display_name).await?.len(),
2,
"Expected to find 2 display names for room"
);
@@ -960,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_room_removal(&self) -> Result<()> {
let room_id = room_id();
let user_id = user_id();
let display_name = DisplayName::new("example");
let stripped_room_id = stripped_room_id();
self.populate().await?;
@@ -988,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore {
"still joined users found"
);
assert!(
self.get_users_with_display_name(room_id, "example").await?.is_empty(),
self.get_users_with_display_name(room_id, &display_name).await?.is_empty(),
"still display names found"
);
assert!(self
@@ -1143,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore {
async fn test_display_names_saving(&self) {
let room_id = room_id!("!test_display_names_saving:localhost");
let user_id = user_id();
let user_display_name = "User";
let user_display_name = DisplayName::new("User");
let second_user_id = user_id!("@second:localhost");
let third_user_id = user_id!("@third:localhost");
let other_display_name = "Raoul";
let unknown_display_name = "Unknown";
let other_display_name = DisplayName::new("Raoul");
let unknown_display_name = DisplayName::new("Unknown");
// No event in store.
let mut display_names = vec![user_display_name.to_owned()];
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert!(users.is_empty());
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert!(names.is_empty());
@@ -1165,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore {
.insert(user_display_name.to_owned(), [user_id.to_owned()].into());
self.save_changes(&changes).await.unwrap();
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 1);
@@ -1180,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore {
self.save_changes(&changes).await.unwrap();
display_names.push(other_display_name.to_owned());
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
assert_eq!(users.len(), 1);
let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap();
let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap();
assert_eq!(users.len(), 2);
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
assert_eq!(names.len(), 2);
@@ -1212,7 +1220,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// Reading it will work.
let pending = self.load_send_queue_requests(room_id).await.unwrap();
@@ -1236,7 +1244,7 @@ impl StateStoreIntegrationTests for DynStateStore {
)
.unwrap();
self.save_send_queue_request(room_id, txn, event.into()).await.unwrap();
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
}
// Reading all the events should work.
@@ -1334,7 +1342,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
.unwrap();
self.save_send_queue_request(room_id2, txn.clone(), event.into()).await.unwrap();
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
}
// Add and remove one event for room3.
@@ -1344,7 +1352,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
.unwrap();
self.save_send_queue_request(room_id3, txn.clone(), event.into()).await.unwrap();
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
}
@@ -1357,6 +1365,64 @@ impl StateStoreIntegrationTests for DynStateStore {
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
}
async fn test_send_queue_priority(&self) {
let room_id = room_id!("!test_send_queue:localhost");
// No queued event in store at first.
let events = self.load_send_queue_requests(room_id).await.unwrap();
assert!(events.is_empty());
// Saving one request should work.
let low0_txn = TransactionId::new();
let ev0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
.unwrap();
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
// Saving one request with higher priority should work.
let high_txn = TransactionId::new();
let ev1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
.unwrap();
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
// Saving another request with the low priority should work.
let low1_txn = TransactionId::new();
let ev2 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
.unwrap();
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
// The requests should be ordered from higher priority to lower, and when equal,
// should use the insertion order instead.
let pending = self.load_send_queue_requests(room_id).await.unwrap();
assert_eq!(pending.len(), 3);
{
assert_eq!(pending[0].transaction_id, high_txn);
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "high");
}
{
assert_eq!(pending[1].transaction_id, low0_txn);
let deserialized = pending[1].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low0");
}
{
assert_eq!(pending[2].transaction_id, low1_txn);
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
assert_eq!(content.body(), "low1");
}
}
async fn test_send_queue_dependents(&self) {
let room_id = room_id!("!test_send_queue_dependents:localhost");
@@ -1365,7 +1431,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event0 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into()).await.unwrap();
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
// No dependents, to start with.
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
@@ -1427,7 +1493,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let event1 =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
.unwrap();
self.save_send_queue_request(room_id, txn1.clone(), event1.into()).await.unwrap();
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
self.save_dependent_queued_request(
room_id,
@@ -1609,6 +1675,12 @@ macro_rules! statestore_integration_tests {
store.test_send_queue().await;
}
#[async_test]
async fn test_send_queue_priority() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_priority().await;
}
#[async_test]
async fn test_send_queue_dependents() {
let store = get_store().await.expect("creating store failed").into_state_store();
@@ -42,7 +42,8 @@ use super::{
StateChanges, StateStore, StoreError,
};
use crate::{
deserialized_responses::RawAnySyncOrStrippedState, store::QueueWedgeError,
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
store::QueueWedgeError,
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
};
@@ -61,7 +62,7 @@ pub struct MemoryStore {
utd_hook_manager_data: StdRwLock<Option<GrowableBloom>>,
account_data: StdRwLock<HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
profiles: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<String, BTreeSet<OwnedUserId>>>>,
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>>,
members: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MembershipState>>>,
room_info: StdRwLock<HashMap<OwnedRoomId, RoomInfo>>,
room_state: StdRwLock<
@@ -701,7 +702,7 @@ impl StateStore for MemoryStore {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>> {
Ok(self
.display_names
@@ -715,21 +716,18 @@ impl StateStore for MemoryStore {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
if display_names.is_empty() {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
}
let read_guard = &self.display_names.read().unwrap();
let Some(room_names) = read_guard.get(room_id) else {
return Ok(BTreeMap::new());
return Ok(HashMap::new());
};
Ok(display_names
.iter()
.filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone())))
.collect())
Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect())
}
async fn get_account_data_event(
@@ -807,13 +805,14 @@ impl StateStore for MemoryStore {
room_id: &RoomId,
transaction_id: OwnedTransactionId,
kind: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
self.send_queue_events
.write()
.unwrap()
.entry(room_id.to_owned())
.or_default()
.push(QueuedRequest { kind, transaction_id, error: None });
.push(QueuedRequest { kind, transaction_id, error: None, priority });
Ok(())
}
@@ -867,7 +866,11 @@ impl StateStore for MemoryStore {
&self,
room_id: &RoomId,
) -> Result<Vec<QueuedRequest>, Self::Error> {
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
let mut ret =
self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone();
// Inverted order of priority, use stable sort to keep insertion order.
ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority));
Ok(ret)
}
async fn update_send_queue_request_status(
+18 -22
View File
@@ -21,7 +21,7 @@
//! store.
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt,
ops::Deref,
result::Result as StdResult,
@@ -29,9 +29,7 @@ use std::{
sync::{Arc, RwLock as StdRwLock},
};
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::{Vector, VectorDiff};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::Stream;
use once_cell::sync::OnceCell;
@@ -60,7 +58,8 @@ use tokio::sync::{broadcast, Mutex, RwLock};
use tracing::warn;
use crate::{
event_cache_store,
deserialized_responses::DisplayName,
event_cache::store as event_cache_store,
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
};
@@ -267,7 +266,6 @@ impl Store {
/// Get a stream of all the rooms changes, in addition to the existing
/// rooms.
#[cfg(not(target_arch = "wasm32"))]
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
self.rooms.read().unwrap().stream()
}
@@ -387,7 +385,7 @@ pub struct StateChanges {
/// A map from room id to a map of a display name and a set of user ids that
/// share that display name in the given room.
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
}
impl StateChanges {
@@ -483,7 +481,8 @@ impl StateChanges {
/// ```
/// # use matrix_sdk_base::store::StoreConfig;
///
/// let store_config = StoreConfig::new();
/// let store_config =
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
/// ```
#[derive(Clone)]
pub struct StoreConfig {
@@ -491,6 +490,7 @@ pub struct StoreConfig {
pub(crate) crypto_store: Arc<DynCryptoStore>,
pub(crate) state_store: Arc<DynStateStore>,
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
cross_process_store_locks_holder_name: String,
}
#[cfg(not(tarpaulin_include))]
@@ -502,17 +502,20 @@ impl fmt::Debug for StoreConfig {
impl StoreConfig {
/// Create a new default `StoreConfig`.
///
/// To learn more about `cross_process_store_locks_holder_name`, please read
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
#[must_use]
pub fn new() -> Self {
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
Self {
#[cfg(feature = "e2e-encryption")]
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
state_store: Arc::new(MemoryStore::new()),
event_cache_store: event_cache_store::EventCacheStoreLock::new(
event_cache_store::MemoryStore::new(),
"default-key".to_owned(),
"matrix-sdk-base".to_owned(),
cross_process_store_locks_holder_name.clone(),
),
cross_process_store_locks_holder_name,
}
}
@@ -532,21 +535,14 @@ impl StoreConfig {
}
/// Set a custom implementation of an `EventCacheStore`.
///
/// The `key` and `holder` arguments represent the key and holder inside the
/// [`CrossProcessStoreLock::new`][matrix_sdk_common::store_locks::CrossProcessStoreLock::new].
pub fn event_cache_store<S>(mut self, event_cache_store: S, key: String, holder: String) -> Self
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
where
S: event_cache_store::IntoEventCacheStore,
{
self.event_cache_store =
event_cache_store::EventCacheStoreLock::new(event_cache_store, key, holder);
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
event_cache_store,
self.cross_process_store_locks_holder_name.clone(),
);
self
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::new()
}
}
+106 -182
View File
@@ -14,212 +14,137 @@
//! An [`ObservableMap`] implementation.
#[cfg(not(target_arch = "wasm32"))]
mod impl_non_wasm32 {
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
use eyeball_im::{ObservableVector, Vector, VectorDiff};
use futures_util::Stream;
use eyeball_im::{ObservableVector, Vector, VectorDiff};
use futures_util::Stream;
/// An observable map.
///
/// This is an “observable map” naive implementation. Just like regular
/// hashmap, we have a redirection from a key to a position, and from a
/// position to a value. The (key, position) tuples are stored in an
/// [`HashMap`]. The (position, value) tuples are stored in an
/// [`ObservableVector`]. The (key, position) tuple is only provided for
/// fast _reading_ implementations, like `Self::get` and
/// `Self::get_or_create`. The (position, value) tuples are observable,
/// this is what interests us the most here.
///
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
/// of this custom implementation? Because we want to continue providing
/// `VectorDiff` when observing the changes, so that the rest of the API in
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
/// produce `MapDiff`, which would be quite different.
/// Plus, we would like to re-use all our existing code, test, stream
/// adapters and so on.
///
/// This is a trade-off. This implementation is simple enough for the
/// moment, and basically does the job.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>
where
V: Clone + Send + Sync + 'static,
{
/// The (key, position) tuples.
mapping: HashMap<K, usize>,
/// An observable map.
///
/// This is an “observable map” naive implementation. Just like regular
/// hashmap, we have a redirection from a key to a position, and from a
/// position to a value. The (key, position) tuples are stored in an
/// [`HashMap`]. The (position, value) tuples are stored in an
/// [`ObservableVector`]. The (key, position) tuple is only provided for
/// fast _reading_ implementations, like `Self::get` and
/// `Self::get_or_create`. The (position, value) tuples are observable,
/// this is what interests us the most here.
///
/// Why not implementing a new `ObservableMap` type in `eyeball-im` instead
/// of this custom implementation? Because we want to continue providing
/// `VectorDiff` when observing the changes, so that the rest of the API in
/// the Matrix Rust SDK aren't broken. Indeed, an `ObservableMap` must
/// produce `MapDiff`, which would be quite different.
/// Plus, we would like to re-use all our existing code, test, stream
/// adapters and so on.
///
/// This is a trade-off. This implementation is simple enough for the
/// moment, and basically does the job.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>
where
V: Clone + 'static,
{
/// The (key, position) tuples.
mapping: HashMap<K, usize>,
/// The values where the indices are the `position` part of
/// `Self::mapping`.
values: ObservableVector<V>,
/// The values where the indices are the `position` part of
/// `Self::mapping`.
values: ObservableVector<V>,
}
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq,
V: Clone + 'static,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self { mapping: HashMap::new(), values: ObservableVector::new() }
}
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq,
V: Clone + Send + Sync + 'static,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self { mapping: HashMap::new(), values: ObservableVector::new() }
}
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
match self.mapping.get(&key) {
Some(position) => {
self.values.set(*position, value);
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) -> usize {
match self.mapping.get(&key) {
Some(position) => {
self.values.set(*position, value);
*position
}
None => {
let position = self.values.len();
*position
}
None => {
let position = self.values.len();
self.values.push_back(value);
self.mapping.insert(key, position);
self.values.push_back(value);
self.mapping.insert(key, position);
position
}
position
}
}
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
self.mapping.get(key).and_then(|position| self.values.get(*position))
}
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
let position = match self.mapping.get(key) {
Some(position) => *position,
None => {
let value = default();
let position = self.values.len();
self.values.push_back(value);
self.mapping.insert(key.to_owned(), position);
position
}
};
self.values
.get(position)
.expect("Value should be present or has just been inserted, but it's missing")
}
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.values.iter()
}
/// Get a [`Stream`] of the values.
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
self.values.subscribe().into_values_and_batched_stream()
}
/// Remove a `V` value based on their ID, if it exists.
///
/// Returns the removed value.
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
where
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
let position = self.mapping.remove(key)?;
Some(self.values.remove(position))
}
}
}
#[cfg(target_arch = "wasm32")]
mod impl_wasm32 {
use std::{borrow::Borrow, collections::BTreeMap, hash::Hash};
/// An observable map for Wasm. It's a simple wrapper around `BTreeMap`.
#[derive(Debug)]
pub(crate) struct ObservableMap<K, V>(BTreeMap<K, V>)
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
V: Clone + 'static;
impl<K, V> ObservableMap<K, V>
where
K: Hash + Eq + Ord,
V: Clone + 'static,
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
/// Create a new `Self`.
pub(crate) fn new() -> Self {
Self(BTreeMap::new())
}
self.mapping.get(key).and_then(|position| self.values.get(*position))
}
/// Insert a new `V` in the collection.
///
/// If the `V` value already exists, it will be updated to the new one.
pub(crate) fn insert(&mut self, key: K, value: V) {
self.0.insert(key, value);
}
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
let position = match self.mapping.get(key) {
Some(position) => *position,
None => {
let value = default();
let position = self.values.len();
/// Reading one `V` value based on their ID, if it exists.
pub(crate) fn get<L>(&self, key: &L) -> Option<&V>
where
K: Borrow<L>,
L: Hash + Eq + Ord + ?Sized,
{
self.0.get(key)
}
self.values.push_back(value);
self.mapping.insert(key.to_owned(), position);
/// Reading one `V` value based on their ID, or create a new one (by
/// using `default`).
pub(crate) fn get_or_create<L, F>(&mut self, key: &L, default: F) -> &V
where
K: Borrow<L>,
L: Hash + Eq + ?Sized + ToOwned<Owned = K>,
F: FnOnce() -> V,
{
self.0.entry(key.to_owned()).or_insert_with(default)
}
position
}
};
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.0.values()
}
self.values
.get(position)
.expect("Value should be present or has just been inserted, but it's missing")
}
/// Remove a `V` value based on their ID, if it exists.
///
/// Returns the removed value.
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
where
K: Borrow<L>,
L: Hash + Eq + Ord + ?Sized,
{
self.0.remove(key)
}
/// Return an iterator over the existing values.
pub(crate) fn iter(&self) -> impl Iterator<Item = &V> {
self.values.iter()
}
/// Get a [`Stream`] of the values.
pub(crate) fn stream(&self) -> (Vector<V>, impl Stream<Item = Vec<VectorDiff<V>>>) {
self.values.subscribe().into_values_and_batched_stream()
}
/// Remove a `V` value based on their ID, if it exists.
///
/// Returns the removed value.
pub(crate) fn remove<L>(&mut self, key: &L) -> Option<V>
where
K: Borrow<L>,
L: Hash + Eq + ?Sized,
{
let position = self.mapping.remove(key)?;
Some(self.values.remove(position))
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use impl_non_wasm32::ObservableMap;
#[cfg(target_arch = "wasm32")]
pub(crate) use impl_wasm32::ObservableMap;
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
use eyeball_im::VectorDiff;
#[cfg(not(target_arch = "wasm32"))]
use stream_assert::{assert_closed, assert_next_eq, assert_pending};
use super::ObservableMap;
@@ -314,7 +239,6 @@ mod tests {
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_stream() {
let mut map = ObservableMap::<char, char>::new();
@@ -125,6 +125,12 @@ pub struct QueuedRequest {
///
/// `None` if the request is in the queue, waiting to be sent.
pub error: Option<QueueWedgeError>,
/// At which priority should this be handled?
///
/// The bigger the value, the higher the priority at which this request
/// should be handled.
pub priority: usize,
}
impl QueuedRequest {
+20 -9
View File
@@ -14,7 +14,7 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt,
sync::Arc,
};
@@ -46,7 +46,9 @@ use super::{
StoreError,
};
use crate::{
deserialized_responses::{RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState},
deserialized_responses::{
DisplayName, RawAnySyncOrStrippedState, RawMemberEvent, RawSyncOrStrippedState,
},
MinimalRoomMemberEvent, RoomInfo, RoomMemberships,
};
@@ -206,7 +208,7 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>, Self::Error>;
/// Get all the users that use the given display names in the given room.
@@ -219,8 +221,8 @@ pub trait StateStore: AsyncTraitDeps {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error>;
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error>;
/// Get an event out of the account data store.
///
@@ -358,6 +360,7 @@ pub trait StateStore: AsyncTraitDeps {
room_id: &RoomId,
transaction_id: OwnedTransactionId,
request: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error>;
/// Updates a send queue request with the given content, and resets its
@@ -390,6 +393,10 @@ pub trait StateStore: AsyncTraitDeps {
) -> Result<bool, Self::Error>;
/// Loads all the send queue requests for the given room.
///
/// The resulting vector of queued requests should be ordered from higher
/// priority to lower priority, and respect the insertion order when
/// priorities are equal.
async fn load_send_queue_requests(
&self,
room_id: &RoomId,
@@ -562,7 +569,7 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>, Self::Error> {
self.0.get_users_with_display_name(room_id, display_name).await.map_err(Into::into)
}
@@ -570,8 +577,8 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>, Self::Error> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>, Self::Error> {
self.0.get_users_with_display_names(room_id, display_names).await.map_err(Into::into)
}
@@ -641,8 +648,12 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
room_id: &RoomId,
transaction_id: OwnedTransactionId,
content: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
self.0.save_send_queue_request(room_id, transaction_id, content).await.map_err(Into::into)
self.0
.save_send_queue_request(room_id, transaction_id, content, priority)
.await
.map_err(Into::into)
}
async fn update_send_queue_request(
+4 -2
View File
@@ -18,12 +18,14 @@
use ruma::{owned_user_id, UserId};
use crate::{BaseClient, SessionMeta};
use crate::{store::StoreConfig, BaseClient, SessionMeta};
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
/// one otherwise.
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
let client = BaseClient::new();
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let user_id =
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
client
+7
View File
@@ -2,3 +2,10 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Refactor
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
+4 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.8.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -21,7 +21,10 @@ uniffi = ["dep:uniffi"]
[dependencies]
async-trait = { workspace = true }
eyeball-im = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
imbl = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -343,7 +343,7 @@ impl SyncTimelineEvent {
/// Get the event id of this `SyncTimelineEvent` if the event has any valid
/// id.
pub fn event_id(&self) -> Option<OwnedEventId> {
self.kind.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
self.kind.event_id()
}
/// Returns a reference to the (potentially decrypted) Matrix event inside
@@ -529,6 +529,12 @@ impl TimelineEventKind {
}
}
/// Get the event id of this `TimelineEventKind` if the event has any valid
/// id.
pub fn event_id(&self) -> Option<OwnedEventId> {
self.raw().get_field::<OwnedEventId>("event_id").ok().flatten()
}
/// If the event was a decrypted event that was successfully decrypted, get
/// its encryption info. Otherwise, `None`.
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
+1
View File
@@ -25,6 +25,7 @@ pub mod debug;
pub mod deserialized_responses;
pub mod executor;
pub mod failures_cache;
pub mod linked_chunk;
pub mod ring_buffer;
pub mod store_locks;
pub mod timeout;
@@ -13,6 +13,7 @@
// limitations under the License.
#![allow(dead_code)]
#![allow(rustdoc::private_intra_doc_links)]
//! A linked chunk is the underlying data structure that holds all events.
@@ -56,7 +57,7 @@ macro_rules! assert_items_eq {
let chunk = $iterator .next().expect("next chunk (expect items)");
assert!(chunk.is_items(), "chunk should contain items");
let $crate::event_cache::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
let $crate::linked_chunk::ChunkContent::Items(items) = chunk.content() else {
unreachable!()
};
@@ -934,7 +935,6 @@ impl ChunkIdentifierGenerator {
#[repr(transparent)]
pub struct ChunkIdentifier(u64);
#[cfg(test)]
impl PartialEq<u64> for ChunkIdentifier {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
@@ -963,7 +963,7 @@ impl Position {
/// # Panic
///
/// This method will panic if it will underflow, i.e. if the index is 0.
pub(super) fn decrement_index(&mut self) {
pub fn decrement_index(&mut self) {
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
}
}
@@ -1346,7 +1346,8 @@ where
}
/// A type representing what to do when the system has to handle an empty chunk.
pub(crate) enum EmptyChunk {
#[derive(Debug)]
pub enum EmptyChunk {
/// Keep the empty chunk.
Keep,
+105 -101
View File
@@ -1,6 +1,70 @@
# UNRELEASED
# Changelog
Changes:
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Features
- Pin identity when we withdraw verification.
- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow
applications to receive notifications about received `m.room_key.withheld`
events.
([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)),
([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674))
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings.
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
- Expose new method `OlmMachine::upload_device_keys()`.
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
- Expose new method `CryptoStore::import_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
- Expose new method `BackupMachine::backup_version`.
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
- Add data types to parse the QR code data for the QR code login defined in.
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
- Expose new method `CryptoStore::clear_caches`.
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
- Expose new method `OlmMachine::device_creation_time`.
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
- Log more details about the Olm session after encryption and decryption.
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
- When Olm message decryption fails, report the error code(s) from the failure.
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
- Expose new methods `OlmMachine::set_room_settings` and
`OlmMachine::get_room_settings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Add new properties `session_rotation_period` and
`session_rotation_period_msgs` to `store::RoomSettings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
keys whose metadata lacked check fields.
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
- Add new API `Device::encrypt_event_raw` that allows
to encrypt an event to a specific device.
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
- Add new API `store::Store::export_room_keys_stream` that provides room
keys on demand.
- Include event timestamps on logs from event decryption.
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
### Refactor
- Add new method `OlmMachine::try_decrypt_room_event`.
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
@@ -8,14 +72,14 @@ Changes:
- Add reason code to `matrix_sdk_common::deserialized_responses::UnableToDecryptInfo`.
([#4116](https://github.com/matrix-org/matrix-rust-sdk/pull/4116))
- The `UserIdentity` struct has been renamed to `OtherUserIdentity`
- [**breaking**] The `UserIdentity` struct has been renamed to `OtherUserIdentity`.
([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036]))
- The `UserIdentities` enum has been renamed to `UserIdentity`
- [**breaking**] The `UserIdentities` enum has been renamed to `UserIdentity`.
([#4036](https://github.com/matrix-org/matrix-rust-sdk/pull/4036]))
- Change the withheld code for keys not shared due to the `IdentityBasedStrategy`, from `m.unauthorised`
to `m.unverified`.
- Change the withheld code for keys not shared due to the
`IdentityBasedStrategy`, from `m.unauthorised` to `m.unverified`.
([#3985](https://github.com/matrix-org/matrix-rust-sdk/pull/3985))
- Improve logging for undecryptable Megolm events.
@@ -59,47 +123,47 @@ Changes:
Breaking changes:
- `VerificationRequestState::Transitioned` now includes a new field
- [**breaking**] `VerificationRequestState::Transitioned` now includes a new field
`other_device_data` of type `DeviceData`.
([#4153](https://github.com/matrix-org/matrix-rust-sdk/pull/4153))
- `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type,
- [**breaking**] `OlmMachine::decrypt_room_event` now returns a `DecryptedRoomEvent` type,
instead of the more generic `TimelineEvent` type.
- **NOTE**: this version causes changes to the format of the serialised data in
- [**breaking**] **NOTE**: this version causes changes to the format of the serialised data in
the CryptoStore, meaning that, once upgraded, it will not be possible to roll
back applications to earlier versions without breaking user sessions.
- Renamed `VerificationLevel::PreviouslyVerified` to
- [**breaking**] Renamed `VerificationLevel::PreviouslyVerified` to
`VerificationLevel::VerificationViolation`.
- `OlmMachine::decrypt_room_event` now takes a `DecryptionSettings` argument,
which includes a `TrustRequirement` indicating the required trust level for
the sending device. When it is called with `TrustRequirement` other than
`TrustRequirement::Unverified`, it may return the new
`MegolmError::SenderIdentityNotTrusted` variant if the sending device does not
satisfy the required trust level.
- [**breaking**] `OlmMachine::decrypt_room_event` now takes a
`DecryptionSettings` argument, which includes a `TrustRequirement` indicating
the required trust level for the sending device. When it is called with
`TrustRequirement` other than `TrustRequirement::Unverified`, it may return
the new `MegolmError::SenderIdentityNotTrusted` variant if the sending device
does not satisfy the required trust level.
([#3899](https://github.com/matrix-org/matrix-rust-sdk/pull/3899))
- Change the structure of the `SenderData` enum to separate variants for
previously-verified, unverified and verified.
- [**breaking**] Change the structure of the `SenderData` enum to separate
variants for previously-verified, unverified and verified.
([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877))
- Where `EncryptionInfo` is returned it may include the new `PreviouslyVerified`
variant of `VerificationLevel` to indicate that the user was previously
verified and is no longer verified.
- [**breaking**] Where `EncryptionInfo` is returned it may include the new
`PreviouslyVerified` variant of `VerificationLevel` to indicate that the user
was previously verified and is no longer verified.
([#3877](https://github.com/matrix-org/matrix-rust-sdk/pull/3877))
- Expose new methods `OwnUserIdentity::was_previously_verified`,
- [**breaking**] Expose new methods `OwnUserIdentity::was_previously_verified`,
`OwnUserIdentity::withdraw_verification`, and
`OwnUserIdentity::has_verification_violation`, which track whether our own
identity was previously verified.
([#3846](https://github.com/matrix-org/matrix-rust-sdk/pull/3846))
- Add a new `error_on_verified_user_problem` property to
- [**breaking**] Add a new `error_on_verified_user_problem` property to
`CollectStrategy::DeviceBasedStrategy`, which, when set, causes
`OlmMachine::share_room_key` to fail with an error if any verified users on
the recipient list have unsigned devices, or are no lonver verified.
the recipient list have unsigned devices, or are no longer verified.
When `CallectStrategy::IdentityBasedStrategy` is used,
`OlmMachine::share_room_key` will fail with an error if any verified users on
@@ -109,103 +173,43 @@ Breaking changes:
Also remove `CollectStrategy::new_device_based`: callers should construct a
`CollectStrategy::DeviceBasedStrategy` directly.
`EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of
a list of booleans.
`EncryptionSettings::new` now takes a `CollectStrategy` argument, instead of a
list of booleans.
([#3810](https://github.com/matrix-org/matrix-rust-sdk/pull/3810))
([#3816](https://github.com/matrix-org/matrix-rust-sdk/pull/3816))
([#3896](https://github.com/matrix-org/matrix-rust-sdk/pull/3896))
- Remove the method `OlmMachine::clear_crypto_cache()`, crypto stores are not
supposed to have any caches anymore.
- [**breaking**] Remove the method `OlmMachine::clear_crypto_cache()`, crypto
stores are not supposed to have any caches anymore.
- Add a `custom_account` argument to the `OlmMachine::with_store()` method, this
allows users to learn their identity keys before they get access to the user
and device ID.
- [**breaking**] Add a `custom_account` argument to the
`OlmMachine::with_store()` method, this allows users to learn their identity
keys before they get access to the user and device ID.
([#3451](https://github.com/matrix-org/matrix-rust-sdk/pull/3451))
- Add a `backup_version` argument to `CryptoStore`'s
- [**breaking**] Add a `backup_version` argument to `CryptoStore`'s
`inbound_group_sessions_for_backup`,
`mark_inbound_group_sessions_as_backed_up` and
`inbound_group_session_counts` methods.
([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
`mark_inbound_group_sessions_as_backed_up` and `inbound_group_session_counts`
methods. ([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
- Rename the `OlmMachine::invalidate_group_session` method to
`OlmMachine::discard_room_key`
- [**breaking**] Rename the `OlmMachine::invalidate_group_session` method to
`OlmMachine::discard_room_key`.
- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
- [**breaking**] Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
(Call it with `olm_machine.store().export_room_keys(...)`.)
- Add new `dehydrated` property to `olm::account::PickledAccount`.
- [**breaking**] Add new `dehydrated` property to `olm::account::PickledAccount`.
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
- Remove deprecated `OlmMachine::import_room_keys`.
- [**breaking**] Remove deprecated `OlmMachine::import_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
- Add the `SasState::Created` variant to differentiate the state between the
- [**breaking**] Add the `SasState::Created` variant to differentiate the state between the
party that sent the verification start and the party that received it.
Deprecations:
- Deprecate `BackupMachine::import_backed_up_room_keys`.
- [**breaking**] Deprecate `BackupMachine::import_backed_up_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
Additions:
- Expose new method `OlmMachine::room_keys_withheld_received_stream`, to allow
applications to receive notifications about received `m.room_key.withheld`
events.
([#3660](https://github.com/matrix-org/matrix-rust-sdk/pull/3660)),
([#3674](https://github.com/matrix-org/matrix-rust-sdk/pull/3674))
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
- Expose new method `OlmMachine::upload_device_keys()`.
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
- Expose new method `CryptoStore::import_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
- Expose new method `BackupMachine::backup_version`.
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
- Add data types to parse the QR code data for the QR code login defined in
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
- Expose new method `CryptoStore::clear_caches`.
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
- Expose new method `OlmMachine::device_creation_time`.
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
- Log more details about the Olm session after encryption and decryption.
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
- When Olm message decryption fails, report the error code(s) from the failure.
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
- Expose new methods `OlmMachine::set_room_settings` and
`OlmMachine::get_room_settings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Add new properties `session_rotation_period` and
`session_rotation_period_msgs` to `store::RoomSettings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
keys whose metadata lacked check fields.
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
- Add new API `Device::encrypt_event_raw` that allows
to encrypt an event to a specific device.
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
- Add new API `store::Store::export_room_keys_stream` that provides room
keys on demand.
- Include event timestamps on logs from event decryption.
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
# 0.7.2
+1 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.2"
version = "0.8.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -803,6 +803,9 @@ impl OtherUserIdentityData {
/// reported to the user. In order to remove this notice users have to
/// verify again or to withdraw the verification requirement.
pub fn withdraw_verification(&self) {
// We also pin when we withdraw, since withdrawing implicitly acknowledges
// the identity change
self.pin();
self.previously_verified.store(false, Ordering::SeqCst)
}
@@ -1770,6 +1773,45 @@ pub(crate) mod tests {
assert!(other_identity.inner.has_pin_violation());
}
#[async_test]
async fn test_resolve_identity_pin_violation_with_withdraw_verification() {
use test_json::keys_query_sets::IdentityChangeDataSet as DataSet;
let my_user_id = user_id!("@me:localhost");
let machine = OlmMachine::new(my_user_id, device_id!("ABCDEFGH")).await;
machine.bootstrap_cross_signing(false).await.unwrap();
let keys_query = DataSet::key_query_with_identity_a();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
// Simulate an identity change
let keys_query = DataSet::key_query_with_identity_b();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let other_user_id = DataSet::user_id();
let other_identity =
machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
// For testing purpose mark it as previously verified
other_identity.mark_as_previously_verified().await.unwrap();
// The identity should need user approval now
assert!(other_identity.identity_needs_user_approval());
// We withdraw verification
other_identity.withdraw_verification().await.unwrap();
// The identity should not need any user approval now
let other_identity =
machine.get_identity(other_user_id, None).await.unwrap().unwrap().other().unwrap();
assert!(!other_identity.identity_needs_user_approval());
// And should not have a pin violation
assert!(!other_identity.inner.has_pin_violation());
}
#[async_test]
async fn test_resolve_identity_verification_violation_with_withdraw() {
use test_json::keys_query_sets::VerificationViolationTestData as DataSet;
+1 -1
View File
@@ -737,7 +737,7 @@ impl OlmMachine {
.await
}
/// Get the a key claiming request for the user/device pairs that we are
/// Get a key claiming request for the user/device pairs that we are
/// missing Olm sessions for.
///
/// Returns None if no key claiming request needs to be sent out.
+6 -1
View File
@@ -2,7 +2,9 @@
All notable changes to this project will be documented in this file.
# UNRELEASED
## [0.8.0] - 2024-11-19
### Features
- Improve the efficiency of objects stored in the crypto store.
([#3645](https://github.com/matrix-org/matrix-rust-sdk/pull/3645), [#3651](https://github.com/matrix-org/matrix-rust-sdk/pull/3651))
@@ -11,3 +13,6 @@ All notable changes to this project will be documented in this file.
- `save_change` performance improvement, all encryption and serialization
is done now outside of the db transaction.
### Bug Fixes
- Use the `DisplayName` struct to protect against homoglyph attacks.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-indexeddb"
version = "0.7.0"
version = "0.8.0"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Web's IndexedDB Storage backend for matrix-sdk"
license = "Apache-2.0"
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet, HashSet},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
sync::Arc,
};
@@ -23,7 +23,7 @@ use gloo_utils::format::JsValueSerdeExt;
use growable_bloom_filter::GrowableBloom;
use indexed_db_futures::prelude::*;
use matrix_sdk_base::{
deserialized_responses::RawAnySyncOrStrippedState,
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
store::{
ChildTransactionId, ComposerDraft, DependentQueuedRequest, DependentQueuedRequestKind,
QueuedRequest, QueuedRequestKind, SentRequestKey, SerializableEventContent,
@@ -437,6 +437,8 @@ struct PersistedQueuedRequest {
pub error: Option<QueueWedgeError>,
priority: Option<usize>,
// Migrated fields: keep these private, they're not used anymore elsewhere in the code base.
/// Deprecated (from old format), now replaced with error field.
is_wedged: Option<bool>,
@@ -459,7 +461,10 @@ impl PersistedQueuedRequest {
_ => self.error,
};
Some(QueuedRequest { kind, transaction_id: self.transaction_id, error })
// By default, events without a priority have a priority of 0.
let priority = self.priority.unwrap_or(0);
Some(QueuedRequest { kind, transaction_id: self.transaction_id, error, priority })
}
}
@@ -660,7 +665,15 @@ impl_state_store!({
let store = tx.object_store(keys::DISPLAY_NAMES)?;
for (room_id, ambiguity_maps) in &changes.ambiguity_maps {
for (display_name, map) in ambiguity_maps {
let key = self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name));
let key = self.encode_key(
keys::DISPLAY_NAMES,
(
room_id,
display_name
.as_normalized_str()
.unwrap_or_else(|| display_name.as_raw_str()),
),
);
store.put_key_val(&key, &self.serialize_value(&map)?)?;
}
@@ -1117,12 +1130,18 @@ impl_state_store!({
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>> {
self.inner
.transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)?
.object_store(keys::DISPLAY_NAMES)?
.get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))?
.get(&self.encode_key(
keys::DISPLAY_NAMES,
(
room_id,
display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()),
),
))?
.await?
.map(|f| self.deserialize_value::<BTreeSet<OwnedUserId>>(&f))
.unwrap_or_else(|| Ok(Default::default()))
@@ -1131,10 +1150,12 @@ impl_state_store!({
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
let mut map = HashMap::new();
if display_names.is_empty() {
return Ok(BTreeMap::new());
return Ok(map);
}
let txn = self
@@ -1142,15 +1163,24 @@ impl_state_store!({
.transaction_on_one_with_mode(keys::DISPLAY_NAMES, IdbTransactionMode::Readonly)?;
let store = txn.object_store(keys::DISPLAY_NAMES)?;
let mut map = BTreeMap::new();
for display_name in display_names {
if let Some(user_ids) = store
.get(&self.encode_key(keys::DISPLAY_NAMES, (room_id, display_name)))?
.get(
&self.encode_key(
keys::DISPLAY_NAMES,
(
room_id,
display_name
.as_normalized_str()
.unwrap_or_else(|| display_name.as_raw_str()),
),
),
)?
.await?
.map(|f| self.deserialize_value::<BTreeSet<OwnedUserId>>(&f))
.transpose()?
{
map.insert(display_name.as_ref(), user_ids);
map.insert(display_name, user_ids);
}
}
@@ -1329,6 +1359,7 @@ impl_state_store!({
room_id: &RoomId,
transaction_id: OwnedTransactionId,
kind: QueuedRequestKind,
priority: usize,
) -> Result<()> {
let encoded_key = self.encode_key(keys::ROOM_SEND_QUEUE, room_id);
@@ -1357,6 +1388,7 @@ impl_state_store!({
error: None,
is_wedged: None,
event: None,
priority: Some(priority),
});
// Save the new vector into db.
@@ -1460,11 +1492,14 @@ impl_state_store!({
.get(&encoded_key)?
.await?;
let prev = prev.map_or_else(
let mut prev = prev.map_or_else(
|| Ok(Vec::new()),
|val| self.deserialize_value::<Vec<PersistedQueuedRequest>>(&val),
)?;
// Inverted stable ordering on priority.
prev.sort_by(|lhs, rhs| rhs.priority.unwrap_or(0).cmp(&lhs.priority.unwrap_or(0)));
Ok(prev.into_iter().filter_map(PersistedQueuedRequest::into_queued_request).collect())
}
+3
View File
@@ -2,3 +2,6 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
No notable changes in this release.
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-qrcode"
description = "Library to encode and decode QR codes for interactive verifications in Matrix land"
version = "0.7.1"
version = "0.8.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
+12
View File
@@ -2,3 +2,15 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
### Bug Fixes
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Refactor
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-sqlite"
version = "0.7.1"
version = "0.8.0"
edition = "2021"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Sqlite storage backend for matrix-sdk"
@@ -0,0 +1,3 @@
-- Add a priority column, defaulting to 0 for all events in the send queue.
ALTER TABLE "send_queue_events"
ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
+1 -1
View File
@@ -14,7 +14,7 @@
use deadpool_sqlite::{CreatePoolError, PoolError};
#[cfg(feature = "event-cache")]
use matrix_sdk_base::event_cache_store::EventCacheStoreError;
use matrix_sdk_base::event_cache::store::EventCacheStoreError;
#[cfg(feature = "state-store")]
use matrix_sdk_base::store::StoreError as StateStoreError;
#[cfg(feature = "crypto-store")]
@@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc};
use async_trait::async_trait;
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
use matrix_sdk_base::{
event_cache_store::EventCacheStore,
event_cache::store::EventCacheStore,
media::{MediaRequestParameters, UniqueKey},
};
use matrix_sdk_store_encryption::StoreCipher;
@@ -279,7 +279,7 @@ mod tests {
};
use matrix_sdk_base::{
event_cache_store::{EventCacheStore, EventCacheStoreError},
event_cache::store::{EventCacheStore, EventCacheStoreError},
event_cache_store_integration_tests, event_cache_store_integration_tests_time,
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
};
@@ -387,7 +387,7 @@ mod encrypted_tests {
use std::sync::atomic::{AtomicU32, Ordering::SeqCst};
use matrix_sdk_base::{
event_cache_store::EventCacheStoreError, event_cache_store_integration_tests,
event_cache::store::EventCacheStoreError, event_cache_store_integration_tests,
event_cache_store_integration_tests_time,
};
use once_cell::sync::Lazy;
+83 -30
View File
@@ -1,6 +1,6 @@
use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
fmt, iter,
path::Path,
sync::Arc,
@@ -9,7 +9,7 @@ use std::{
use async_trait::async_trait;
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
use matrix_sdk_base::{
deserialized_responses::{RawAnySyncOrStrippedState, SyncOrStrippedState},
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState, SyncOrStrippedState},
store::{
migration_helpers::RoomInfoV1, ChildTransactionId, DependentQueuedRequest,
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind,
@@ -69,7 +69,7 @@ mod keys {
/// This is used to figure whether the sqlite database requires a migration.
/// Every new SQL migration should imply a bump of this number, and changes in
/// the [`SqliteStateStore::run_migrations`] function..
const DATABASE_VERSION: u8 = 9;
const DATABASE_VERSION: u8 = 10;
/// A sqlite based cryptostore.
#[derive(Clone)]
@@ -307,6 +307,17 @@ impl SqliteStateStore {
.await?;
}
if from < 10 && to >= 10 {
conn.with_transaction(move |txn| {
// Run the migration.
txn.execute_batch(include_str!(
"../migrations/state_store/009_send_queue_priority.sql"
))?;
txn.set_db_version(10)
})
.await?;
}
Ok(())
}
@@ -1294,13 +1305,34 @@ impl StateStore for SqliteStateStore {
let room_id = this.encode_key(keys::DISPLAY_NAME, room_id);
for (name, user_ids) in display_names {
let name = this.encode_key(keys::DISPLAY_NAME, name);
let encoded_name = this.encode_key(
keys::DISPLAY_NAME,
name.as_normalized_str().unwrap_or_else(|| name.as_raw_str()),
);
let data = this.serialize_json(&user_ids)?;
if user_ids.is_empty() {
txn.remove_display_name(&room_id, &name)?;
txn.remove_display_name(&room_id, &encoded_name)?;
// We can't do a migration to merge the previously distinct buckets of
// user IDs since the display names themselves are hashed before they
// are persisted in the store. So the store will always retain two
// buckets: one for raw display names and one for normalised ones.
//
// We therefore do the next best thing, which is a sort of a soft
// migration: we fetch both the raw and normalised buckets, then merge
// the user IDs contained in them into a separate, temporary merged
// bucket. The SDK then operates on the merged buckets exclusively. See
// the comment in `get_users_with_display_names` for details.
//
// If the merged bucket is empty, that must mean that both the raw and
// normalised buckets were also empty, so we can remove both from the
// store.
let raw_name = this.encode_key(keys::DISPLAY_NAME, name.as_raw_str());
txn.remove_display_name(&room_id, &raw_name)?;
} else {
txn.set_display_name(&room_id, &name, &data)?;
// We only create new buckets with the normalized display name.
txn.set_display_name(&room_id, &encoded_name, &data)?;
}
}
}
@@ -1489,10 +1521,13 @@ impl StateStore for SqliteStateStore {
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
display_name: &DisplayName,
) -> Result<BTreeSet<OwnedUserId>> {
let room_id = self.encode_key(keys::DISPLAY_NAME, room_id);
let names = vec![self.encode_key(keys::DISPLAY_NAME, display_name)];
let names = vec![self.encode_key(
keys::DISPLAY_NAME,
display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()),
)];
Ok(self
.acquire()
@@ -1509,33 +1544,49 @@ impl StateStore for SqliteStateStore {
async fn get_users_with_display_names<'a>(
&self,
room_id: &RoomId,
display_names: &'a [String],
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
display_names: &'a [DisplayName],
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
let mut result = HashMap::new();
if display_names.is_empty() {
return Ok(BTreeMap::new());
return Ok(result);
}
let room_id = self.encode_key(keys::DISPLAY_NAME, room_id);
let mut names_map = display_names
.iter()
.map(|n| (self.encode_key(keys::DISPLAY_NAME, n), n.as_ref()))
.flat_map(|display_name| {
// We encode the display name as the `raw_str()` and the normalized string.
//
// This is for compatibility reasons since:
// 1. Previously "Alice" and "alice" were considered to be distinct display
// names, while we now consider them to be the same so we need to merge the
// previously distinct buckets of user IDs.
// 2. We can't do a migration to merge the previously distinct buckets of user
// IDs since the display names itself are hashed before they are persisted
// in the store.
let raw =
(self.encode_key(keys::DISPLAY_NAME, display_name.as_raw_str()), display_name);
let normalized = display_name.as_normalized_str().map(|normalized| {
(self.encode_key(keys::DISPLAY_NAME, normalized), display_name)
});
iter::once(raw).chain(normalized.into_iter())
})
.collect::<BTreeMap<_, _>>();
let names = names_map.keys().cloned().collect();
self.acquire()
.await?
.get_display_names(room_id, names)
.await?
.into_iter()
.map(|(name, data)| {
Ok((
names_map
.remove(name.as_slice())
.expect("returned display names were requested"),
self.deserialize_json(&data)?,
))
})
.collect::<Result<BTreeMap<_, _>>>()
for (name, data) in
self.acquire().await?.get_display_names(room_id, names).await?.into_iter()
{
let display_name =
names_map.remove(name.as_slice()).expect("returned display names were requested");
let user_ids: BTreeSet<_> = self.deserialize_json(&data)?;
result.entry(display_name).or_insert_with(BTreeSet::new).extend(user_ids);
}
Ok(result)
}
async fn get_account_data_event(
@@ -1685,6 +1736,7 @@ impl StateStore for SqliteStateStore {
room_id: &RoomId,
transaction_id: OwnedTransactionId,
content: QueuedRequestKind,
priority: usize,
) -> Result<(), Self::Error> {
let room_id_key = self.encode_key(keys::SEND_QUEUE, room_id);
let room_id_value = self.serialize_value(&room_id.to_owned())?;
@@ -1699,7 +1751,7 @@ impl StateStore for SqliteStateStore {
self.acquire()
.await?
.with_transaction(move |txn| {
txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content) VALUES (?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content))?;
txn.prepare_cached("INSERT INTO send_queue_events (room_id, room_id_val, transaction_id, content, priority) VALUES (?, ?, ?, ?, ?)")?.execute((room_id_key, room_id_value, transaction_id.to_string(), content, priority))?;
Ok(())
})
.await
@@ -1761,14 +1813,14 @@ impl StateStore for SqliteStateStore {
// Note: ROWID is always present and is an auto-incremented integer counter. We
// want to maintain the insertion order, so we can sort using it.
// Note 2: transaction_id is not encoded, see why in `save_send_queue_event`.
let res: Vec<(String, Vec<u8>, Option<Vec<u8>>)> = self
let res: Vec<(String, Vec<u8>, Option<Vec<u8>>, usize)> = self
.acquire()
.await?
.prepare(
"SELECT transaction_id, content, wedge_reason FROM send_queue_events WHERE room_id = ? ORDER BY ROWID",
"SELECT transaction_id, content, wedge_reason, priority FROM send_queue_events WHERE room_id = ? ORDER BY priority DESC, ROWID",
|mut stmt| {
stmt.query((room_id,))?
.mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))
.collect()
},
)
@@ -1780,6 +1832,7 @@ impl StateStore for SqliteStateStore {
transaction_id: entry.0.into(),
kind: self.deserialize_json(&entry.1)?,
error: entry.2.map(|v| self.deserialize_value(&v)).transpose()?,
priority: entry.3,
});
}
@@ -2,3 +2,6 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2024-11-19
No notable changes in this release.
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-store-encryption"
version = "0.7.0"
version = "0.8.0"
edition = "2021"
description = "Helpers for encrypted storage keys for the Matrix SDK"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
+22 -11
View File
@@ -2,29 +2,40 @@
All notable changes to this project will be documented in this file.
# unreleased
## [0.8.0] - 2024-11-19
Breaking changes:
### Bug Fixes
- `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`.
- `Timeline::send_attachment` now takes an `impl Into<PathBuf>` for the path of
the file to send.
- `Timeline::item_by_transaction_id` has been renamed to `Timeline::local_item_by_transaction_id`
(always returns local echoes).
Bug fixes:
- Disable `share_pos()` inside `RoomListService`.
- `UtdHookManager` no longer re-reports UTD events as late decryptions.
([#3480](https://github.com/matrix-org/matrix-rust-sdk/pull/3480))
- Messages that we were unable to decrypt no longer display a red padlock.
([#3956](https://github.com/matrix-org/matrix-rust-sdk/issues/3956))
Other changes:
- `UtdHookManager` no longer reports UTD events that were already reported in a
previous session.
([#3519](https://github.com/matrix-org/matrix-rust-sdk/pull/3519))
### Features
- Add `m.room.join_rules` to the required state.
- `EncryptionSyncService` and `Notification` are using
`Client::cross_process_store_locks_holder_name`.
### Refactor
- [**breaking**] `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`.
- [**breaking**] `Timeline::send_attachment` now takes an `impl Into<PathBuf>`
for the path of the file to send.
- [**breaking**] `Timeline::item_by_transaction_id` has been renamed to
`Timeline::local_item_by_transaction_id` (always returns local echoes).
# 0.7.0
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-ui"
description = "GUI-centric utilities on top of matrix-rust-sdk (experimental)."
version = "0.7.0"
version = "0.8.0"
edition = "2021"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"
@@ -88,11 +88,7 @@ impl EncryptionSyncService {
/// Creates a new instance of a `EncryptionSyncService`.
///
/// This will create and manage an instance of [`matrix_sdk::SlidingSync`].
/// The `process_id` is used as the identifier of that instance, as such
/// make sure to not reuse a name used by another process, at the risk
/// of causing problems.
pub async fn new(
process_id: String,
client: Client,
poll_and_network_timeouts: Option<(Duration, Duration)>,
with_locking: WithLocking,
@@ -119,7 +115,13 @@ impl EncryptionSyncService {
if with_locking {
// Gently try to enable the cross-process lock on behalf of the user.
match client.encryption().enable_cross_process_store_lock(process_id).await {
match client
.encryption()
.enable_cross_process_store_lock(
client.cross_process_store_locks_holder_name().to_owned(),
)
.await
{
Ok(()) | Err(matrix_sdk::Error::BadCryptoStoreState) => {
// Ignore; we've already set the crypto store lock to
// something, and that's sufficient as
@@ -111,7 +111,8 @@ impl NotificationClient {
parent_client: Client,
process_setup: NotificationProcessSetup,
) -> Result<Self, Error> {
let client = parent_client.notification_client().await?;
let client = parent_client.notification_client(Self::LOCK_ID.to_owned()).await?;
Ok(NotificationClient {
client,
parent_client,
@@ -242,7 +243,6 @@ impl NotificationClient {
};
let encryption_sync = EncryptionSyncService::new(
Self::LOCK_ID.to_owned(),
self.client.clone(),
Some((Duration::from_secs(3), Duration::from_secs(4))),
with_locking,
@@ -88,6 +88,7 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[
(StateEventType::RoomCanonicalAlias, ""),
(StateEventType::RoomPowerLevels, ""),
(StateEventType::CallMember, "*"),
(StateEventType::RoomJoinRules, ""),
];
/// The default `required_state` constant value for sliding sync room
@@ -135,9 +136,10 @@ impl RoomListService {
}))
.with_typing_extension(assign!(http::request::Typing::default(), {
enabled: Some(true),
}))
// We don't deal with encryption device messages here so this is safe
.share_pos();
}));
// TODO: Re-enable once we know it creates slowness.
// // We don't deal with encryption device messages here so this is safe
// .share_pos();
let sliding_sync = builder
.add_cached_list(
+4 -13
View File
@@ -435,15 +435,11 @@ pub struct SyncServiceBuilder {
/// Is the cross-process lock for the crypto store enabled?
with_cross_process_lock: bool,
/// Application identifier, used as the cross-process lock value, if
/// applicable.
identifier: String,
}
impl SyncServiceBuilder {
fn new(client: Client) -> Self {
Self { client, with_cross_process_lock: false, identifier: "app".to_owned() }
Self { client, with_cross_process_lock: false }
}
/// Enables the cross-process lock, if the sync service is being built in a
@@ -454,14 +450,10 @@ impl SyncServiceBuilder {
/// external process attempting to decrypt notifications. In general,
/// `with_cross_process_lock` should not be called.
///
/// An app identifier can be provided too, to identify the current process;
/// if it's not provided, a default value of "app" is used as the
/// application identifier.
pub fn with_cross_process_lock(mut self, app_identifier: Option<String>) -> Self {
/// Be sure to have configured
/// [`Client::cross_process_store_locks_holder_name`] accordingly.
pub fn with_cross_process_lock(mut self) -> Self {
self.with_cross_process_lock = true;
if let Some(app_identifier) = app_identifier {
self.identifier = app_identifier;
}
self
}
@@ -477,7 +469,6 @@ impl SyncServiceBuilder {
let encryption_sync = Arc::new(
EncryptionSyncService::new(
self.identifier,
self.client,
None,
WithLocking::from(self.with_cross_process_lock),
@@ -570,6 +570,11 @@ impl EventTimelineItem {
EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
}
}
/// For local echoes, return the associated send handle.
pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
}
}
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
@@ -40,8 +40,7 @@ async fn test_smoke_encryption_sync_works() -> anyhow::Result<()> {
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
let sync_permit_guard = sync_permit.clone().lock_owned().await;
let encryption_sync =
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
let stream = encryption_sync.sync(sync_permit_guard);
pin_mut!(stream);
@@ -186,8 +185,7 @@ async fn test_encryption_sync_one_fixed_iteration() -> anyhow::Result<()> {
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
let sync_permit_guard = sync_permit.lock_owned().await;
let encryption_sync =
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
// Run all the iterations.
encryption_sync.run_fixed_iterations(1, sync_permit_guard).await?;
@@ -218,8 +216,7 @@ async fn test_encryption_sync_two_fixed_iterations() -> anyhow::Result<()> {
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
let sync_permit_guard = sync_permit.lock_owned().await;
let encryption_sync =
EncryptionSyncService::new("tests".to_owned(), client, None, WithLocking::Yes).await?;
let encryption_sync = EncryptionSyncService::new(client, None, WithLocking::Yes).await?;
encryption_sync.run_fixed_iterations(2, sync_permit_guard).await?;
@@ -254,8 +251,7 @@ async fn test_encryption_sync_always_reloads_todevice_token() -> anyhow::Result<
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
let sync_permit_guard = sync_permit.lock_owned().await;
let encryption_sync =
EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes)
.await?;
EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?;
let stream = encryption_sync.sync(sync_permit_guard);
pin_mut!(stream);
@@ -363,15 +359,14 @@ async fn test_notification_client_does_not_upload_duplicate_one_time_keys() -> a
info!("Creating the notification client");
let notification_client = client
.notification_client()
.notification_client("tests".to_owned())
.await
.expect("We should be able to build a notification client");
let sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new_for_testing()));
let sync_permit_guard = sync_permit.lock_owned().await;
let encryption_sync =
EncryptionSyncService::new("tests".to_owned(), client.clone(), None, WithLocking::Yes)
.await?;
EncryptionSyncService::new(client.clone(), None, WithLocking::Yes).await?;
let stream = encryption_sync.sync(sync_permit_guard);
pin_mut!(stream);
@@ -357,6 +357,7 @@ async fn test_sync_all_states() -> Result<(), Error> {
["m.room.canonical_alias", ""],
["m.room.power_levels", ""],
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
],
"include_heroes": true,
"filters": {
@@ -590,6 +591,7 @@ async fn test_sync_resumes_from_previous_state() -> Result<(), Error> {
}
#[async_test]
#[ignore] // `share_pos()` has been disabled in the room list, see there to learn more.
async fn test_sync_resumes_from_previous_state_after_restart() -> Result<(), Error> {
let tmp_dir = TempDir::new().unwrap();
let store_path = tmp_dir.path();
@@ -2220,6 +2222,7 @@ async fn test_room_subscription() -> Result<(), Error> {
["m.room.canonical_alias", ""],
["m.room.power_levels", ""],
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
["m.room.create", ""],
["m.room.pinned_events", ""],
],
@@ -2258,6 +2261,7 @@ async fn test_room_subscription() -> Result<(), Error> {
["m.room.canonical_alias", ""],
["m.room.power_levels", ""],
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
["m.room.create", ""],
["m.room.pinned_events", ""],
],
+165 -43
View File
@@ -1,51 +1,14 @@
# unreleased
# Changelog
Breaking changes:
All notable changes to this project will be documented in this file.
- Renamed `VerificationLevel::PreviouslyVerified` to `VerificationLevel::VerificationViolation`.
- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the identity is unverified and previously it was verified.
- Replace the `Notification` type from Ruma in `SyncResponse` and `Client::register_notification_handler`
by a custom one
- `Room::can_user_redact` and `Member::can_redact` are split between `*_redact_own` and `*_redact_other`
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
- `AmbiguityCache` contains the room member's user ID
- Replace `impl MediaEventContent` with `&impl MediaEventContent` in
`Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail`
- A custom sliding sync proxy set with `ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered proxy.
- `Client::get_profile` was moved to `Account` and renamed to `Account::fetch_user_profile_of`. `Account::get_profile` was renamed to `Account::fetch_user_profile`.
- The `HttpError::UnableToCloneRequest` error variant has been removed because it was never used or
generated by the SDK.
- The `Error::InconsistentState` error variant has been removed because it was never used or
generated by the SDK.
- The widget capabilities in the FFI now need two additional flags: `update_delayed_event`, `send_delayed_event`.
- `Room::event` now takes an optional `RequestConfig` to allow for tweaking the network behavior.
- The `instant` module was removed, use the `ruma::time` module instead.
- Add `ClientBuilder::sqlite_store_with_cache_path` to build a client that stores caches in a different directory to state/crypto.
- The `body` parameter in `get_media_file` has been replaced with a `filename` parameter now that Ruma has a `filename()` method.
## [0.8.0] - 2024-11-19
Additions:
### Bug Fixes
- new `UserIdentity::pin` method.
- new `ClientBuilder::with_decryption_trust_requirement` method.
- new `ClientBuilder::with_room_key_recipient_strategy` method
- new `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData setters, analogous to the GlobalAccountData
- new `RequestConfig.max_concurrent_requests` which allows to limit the maximum number of concurrent requests the internal HTTP client issues (all others have to wait until the number drops below that threshold again)
- Expose new method `Client::Oidc::login_with_qr_code()`.
([#3466](https://github.com/matrix-org/matrix-rust-sdk/pull/3466))
- Add the `ClientBuilder::add_root_certificates()` method which re-exposes the
`reqwest::ClientBuilder::add_root_certificate()` functionality.
- Add `Room::get_user_power_level(user_id)` and `Room::get_suggested_user_role(user_id)` to be able to fetch power level info about an user without loading the room member list.
- Add new method `discard_room_key` on `Room` that allows to discard the current
outbound session for that room. Can be used by clients as a dev tool like the `/discardsession` command.
- Add a new `LinkedChunk` data structure to represents all events per room ([#3166](https://github.com/matrix-org/matrix-rust-sdk/pull/3166)).
- Add new methods for tracking (on device only) the user's recently visited rooms called `Account::track_recently_visited_room(roomId)` and `Account::get_recently_visited_rooms()`
- Add `send_call_notification` and `send_call_notification_if_needed` methods. This allows to implement sending ring events on call start.
- The `get_media_content`, `get_media_file` and `get_file` methods of the
`Media` api now support the new authenticated media endpoints.
- WidgetDriver: Support the `"delay"` field in the `send_event` widget actions.
This allows to send delayed events, as defined in [MSC4157](https://github.com/matrix-org/matrix-spec-proposals/pull/4157)
- Add more invalid characters for room aliases.
Bug fixes:
- Match the right status code in `Client::is_room_alias_available`.
- Fix a bug where room keys were considered to be downloaded before backups were
enabled. This bug only affects the
@@ -53,6 +16,165 @@ Bug fixes:
made to download a room key, if a decryption failure with a given room key
would have been encountered before the backups were enabled.
### Documentation
- Improve documentation of `Client::observe_events`.
### Features
- Add `create_room_alias` function.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- `EncryptionSyncService` and `Notification` are using `Client::cross_process_store_locks_holder_name`.
- Allow passing a custom `RequestConfig` to an upload request.
- Retry uploads if they've failed with transient errors.
- Implement `EventHandlerContext` for tuples.
- Introduce a mechanism similar to `Client::add_event_handler` and
`Client::add_room_event_handler` but with a reactive programming pattern. Add
`Client::observe_events` and `Client::observe_room_events`.
```rust
// Get an observer.
let observer =
client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
// Subscribe to the observer.
let mut subscriber = observer.subscribe();
// Use the subscriber as a `Stream`.
let (message_event, (room, push_actions)) = subscriber.next().await.unwrap();
```
When calling `observe_events`, one has to specify the type of event (in the
example, `SyncRoomMessageEvent`) and a context (in the example, `(Room,
Vec<Action>)`, respectively for the room and the push actions).
- Implement unwedging for media uploads.
- Send state from state sync and not from timeline to widget ([#4254](https://github.com/matrix-org/matrix-rust-sdk/pull/4254))
- Allow aborting media uploads.
- Add `RoomPreviewInfo::num_active_members`.
- Use room directory search as another data source.
- Check if the user is allowed to do a room mention before trying to send a call
notify event.
([#4271](https://github.com/matrix-org/matrix-rust-sdk/pull/4271))
- Add `Client::cross_process_store_locks_holder_name()`.
- Add a `PreviouslyVerified` variant to `VerificationLevel` indicating that the
identity is unverified and previously it was verified.
- New `UserIdentity::pin` method.
- New `ClientBuilder::with_decryption_trust_requirement` method.
- New `ClientBuilder::with_room_key_recipient_strategy` method
- New `Room.set_account_data` and `Room.set_account_data_raw` RoomAccountData
setters, analogous to the GlobalAccountData
- New `RequestConfig.max_concurrent_requests` which allows to limit the maximum
number of concurrent requests the internal HTTP client issues (all others have
to wait until the number drops below that threshold again)
- Implement proper redact handling in the widget driver. This allows the Rust
SDK widget driver to support widgets that rely on redacting.
### Refactor
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
- Improve `is_room_alias_format_valid` so it's more strict.
- Remove duplicated fields in media event contents.
- Use `SendHandle` for media uploads too.
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
- Move `formatted_caption_from` to the SDK, rename it.
- Tidy up and start commenting the widget code.
- Get rid of `ProcessingContext` and inline it in its callers.
- Get rid of unused `limits` parameter when constructing a `WidgetMachine`.
- Use a specialized mutex for locking access to the state store and
`being_sent`.
- Renamed `VerificationLevel::PreviouslyVerified` to
`VerificationLevel::VerificationViolation`.
- [**breaking**] Replace the `Notification` type from Ruma in `SyncResponse` and
`Client::register_notification_handler` by a custom one.
- [**breaking**] The ambiguity maps in `SyncResponse` are moved to `JoinedRoom`
and `LeftRoom`.
- [**breaking**] `Room::can_user_redact` and `Member::can_redact` are split
between `*_redact_own` and `*_redact_other`.
- [**breaking**] `AmbiguityCache` contains the room member's user ID.
- [**breaking**] Replace `impl MediaEventContent` with `&impl MediaEventContent` in
`Media::get_file`/`Media::remove_file`/`Media::get_thumbnail`/`Media::remove_thumbnail`
- [**breaking**] A custom sliding sync proxy set with
`ClientBuilder::sliding_sync_proxy` now takes precedence over a discovered
proxy.
- [**breaking**] `Client::get_profile` was moved to `Account` and renamed to
`Account::fetch_user_profile_of`. `Account::get_profile` was renamed to
`Account::fetch_user_profile`.
- [**breaking**] The `HttpError::UnableToCloneRequest` error variant has been
removed because it was never used or generated by the SDK.
- [**breaking**] The `Error::InconsistentState` error variant has been removed
because it was never used or generated by the SDK.
- [**breaking**] The widget capabilities in the FFI now need two additional
flags: `update_delayed_event`, `send_delayed_event`.
- [**breaking**] `Room::event` now takes an optional `RequestConfig` to allow
for tweaking the network behavior.
- [**breaking**] The `instant` module was removed, use the `ruma::time` module
instead.
- [**breaking**] Add `ClientBuilder::sqlite_store_with_cache_path` to build a
client that stores caches in a different directory to state/crypto.
- [**breaking**] The `body` parameter in `get_media_file` has been replaced with
a `filename` parameter now that Ruma has a `filename()` method.
# 0.7.0
Breaking changes:
+3 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.1"
version = "0.8.0"
[package.metadata.docs.rs]
features = ["docsrs"]
@@ -97,6 +97,7 @@ matrix-sdk-sqlite = { workspace = true, optional = true }
matrix-sdk-test = { workspace = true, optional = true }
mime = "0.3.16"
mime2ext = "0.1.52"
pin-project-lite = { workspace = true }
rand = { workspace = true , optional = true }
ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965", "unstable-msc3930", "unstable-msc3245-v1-compat", "unstable-msc2867"] }
serde = { workspace = true }
@@ -144,6 +145,7 @@ serde_urlencoded = "0.7.1"
similar-asserts = { workspace = true }
stream_assert = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tokio-test = "0.4.4"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.33"
+1 -1
View File
@@ -252,7 +252,7 @@ impl Account {
///
/// [`Media::upload()`]: crate::Media::upload
pub async fn upload_avatar(&self, content_type: &Mime, data: Vec<u8>) -> Result<OwnedMxcUri> {
let upload_response = self.client.media().upload(content_type, data).await?;
let upload_response = self.client.media().upload(content_type, data, None).await?;
self.set_avatar_url(Some(&upload_response.content_uri)).await?;
Ok(upload_response.content_uri)
}
+1 -1
View File
@@ -148,7 +148,7 @@ impl From<AttachmentInfo> for FileInfo {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
/// Base metadata about a thumbnail.
pub struct BaseThumbnailInfo {
/// The height of the thumbnail in pixels.
+71 -30
View File
@@ -101,16 +101,21 @@ pub struct ClientBuilder {
room_key_recipient_strategy: CollectStrategy,
#[cfg(feature = "e2e-encryption")]
decryption_trust_requirement: TrustRequirement,
cross_process_store_locks_holder_name: String,
}
impl ClientBuilder {
const DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME: &str = "main";
pub(crate) fn new() -> Self {
Self {
homeserver_cfg: None,
#[cfg(feature = "experimental-sliding-sync")]
sliding_sync_version_builder: SlidingSyncVersionBuilder::Native,
http_cfg: None,
store_config: BuilderStoreConfig::Custom(StoreConfig::default()),
store_config: BuilderStoreConfig::Custom(StoreConfig::new(
Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
)),
request_config: Default::default(),
respect_login_well_known: true,
server_versions: None,
@@ -122,6 +127,8 @@ impl ClientBuilder {
room_key_recipient_strategy: Default::default(),
#[cfg(feature = "e2e-encryption")]
decryption_trust_requirement: TrustRequirement::Untrusted,
cross_process_store_locks_holder_name:
Self::DEFAULT_CROSS_PROCESS_STORE_LOCKS_HOLDER_NAME.to_owned(),
}
}
@@ -208,7 +215,6 @@ impl ClientBuilder {
path: path.as_ref().to_owned(),
cache_path: None,
passphrase: passphrase.map(ToOwned::to_owned),
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
};
self
}
@@ -226,7 +232,6 @@ impl ClientBuilder {
path: path.as_ref().to_owned(),
cache_path: Some(cache_path.as_ref().to_owned()),
passphrase: passphrase.map(ToOwned::to_owned),
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
};
self
}
@@ -237,7 +242,6 @@ impl ClientBuilder {
self.store_config = BuilderStoreConfig::IndexedDb {
name: name.to_owned(),
passphrase: passphrase.map(ToOwned::to_owned),
event_cache_store_lock_holder: "matrix-sdk".to_owned(),
};
self
}
@@ -258,7 +262,9 @@ impl ClientBuilder {
/// # let custom_state_store = MemoryStore::new();
/// use matrix_sdk::{config::StoreConfig, Client};
///
/// let store_config = StoreConfig::new().state_store(custom_state_store);
/// let store_config =
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
/// .state_store(custom_state_store);
/// let client_builder = Client::builder().store_config(store_config);
/// ```
pub fn store_config(mut self, store_config: StoreConfig) -> Self {
@@ -424,6 +430,20 @@ impl ClientBuilder {
self
}
/// Set the cross-process store locks holder name.
///
/// The SDK provides cross-process store locks (see
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
/// `holder_name` will be the value used for all cross-process store locks
/// used by the `Client` being built.
///
/// If 2 concurrent `Client`s are running in 2 different process, this
/// method must be called with different `hold_name` values.
pub fn cross_process_store_locks_holder_name(mut self, holder_name: String) -> Self {
self.cross_process_store_locks_holder_name = holder_name;
self
}
/// Create a [`Client`] with the options set on this builder.
///
/// # Errors
@@ -457,13 +477,17 @@ impl ClientBuilder {
base_client
} else {
#[allow(unused_mut)]
let mut client =
BaseClient::with_store_config(build_store_config(self.store_config).await?);
let mut client = BaseClient::with_store_config(
build_store_config(self.store_config, &self.cross_process_store_locks_holder_name)
.await?,
);
#[cfg(feature = "e2e-encryption")]
{
client.room_key_recipient_strategy = self.room_key_recipient_strategy;
client.decryption_trust_requirement = self.decryption_trust_requirement;
}
client
};
@@ -529,6 +553,7 @@ impl ClientBuilder {
send_queue,
#[cfg(feature = "e2e-encryption")]
self.encryption_settings,
self.cross_process_store_locks_holder_name,
)
.await;
@@ -547,20 +572,16 @@ pub fn sanitize_server_name(s: &str) -> crate::Result<OwnedServerName, IdParseEr
)
}
#[allow(clippy::unused_async)] // False positive when building with !sqlite & !indexeddb
#[allow(clippy::unused_async, unused)] // False positive when building with !sqlite & !indexeddb
async fn build_store_config(
builder_config: BuilderStoreConfig,
cross_process_store_locks_holder_name: &str,
) -> Result<StoreConfig, ClientBuildError> {
#[allow(clippy::infallible_destructuring_match)]
let store_config = match builder_config {
#[cfg(feature = "sqlite")]
BuilderStoreConfig::Sqlite {
path,
cache_path,
passphrase,
event_cache_store_lock_holder,
} => {
let store_config = StoreConfig::new()
BuilderStoreConfig::Sqlite { path, cache_path, passphrase } => {
let store_config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
.state_store(
matrix_sdk_sqlite::SqliteStateStore::open(&path, passphrase.as_deref()).await?,
)
@@ -570,8 +591,6 @@ async fn build_store_config(
passphrase.as_deref(),
)
.await?,
"default-key".to_owned(),
event_cache_store_lock_holder,
);
#[cfg(feature = "e2e-encryption")]
@@ -583,11 +602,11 @@ async fn build_store_config(
}
#[cfg(feature = "indexeddb")]
BuilderStoreConfig::IndexedDb { name, passphrase, event_cache_store_lock_holder } => {
BuilderStoreConfig::IndexedDb { name, passphrase } => {
build_indexeddb_store_config(
&name,
passphrase.as_deref(),
event_cache_store_lock_holder,
cross_process_store_locks_holder_name,
)
.await?
}
@@ -603,28 +622,28 @@ async fn build_store_config(
async fn build_indexeddb_store_config(
name: &str,
passphrase: Option<&str>,
event_cache_store_lock_holder: String,
cross_process_store_locks_holder_name: &str,
) -> Result<StoreConfig, ClientBuildError> {
let cross_process_store_locks_holder_name = cross_process_store_locks_holder_name.to_owned();
#[cfg(feature = "e2e-encryption")]
let store_config = {
let (state_store, crypto_store) =
matrix_sdk_indexeddb::open_stores_with_name(name, passphrase).await?;
StoreConfig::new().state_store(state_store).crypto_store(crypto_store)
StoreConfig::new(cross_process_store_locks_holder_name)
.state_store(state_store)
.crypto_store(crypto_store)
};
#[cfg(not(feature = "e2e-encryption"))]
let store_config = {
let state_store = matrix_sdk_indexeddb::open_state_store(name, passphrase).await?;
StoreConfig::new().state_store(state_store)
StoreConfig::new(cross_process_store_locks_holder_name).state_store(state_store)
};
let store_config = {
tracing::warn!("The IndexedDB backend does not implement an event cache store, falling back to the in-memory event cache store…");
store_config.event_cache_store(
matrix_sdk_base::event_cache_store::MemoryStore::new(),
"default-key".to_owned(),
event_cache_store_lock_holder,
)
store_config.event_cache_store(matrix_sdk_base::event_cache::store::MemoryStore::new())
};
Ok(store_config)
@@ -634,7 +653,7 @@ async fn build_indexeddb_store_config(
async fn build_indexeddb_store_config(
_name: &str,
_passphrase: Option<&str>,
_event_cache_store_lock_holder: String,
_event_cache_store_lock_holder_name: &str,
) -> Result<StoreConfig, ClientBuildError> {
panic!("the IndexedDB is only available on the 'wasm32' arch")
}
@@ -679,13 +698,11 @@ enum BuilderStoreConfig {
path: std::path::PathBuf,
cache_path: Option<std::path::PathBuf>,
passphrase: Option<String>,
event_cache_store_lock_holder: String,
},
#[cfg(feature = "indexeddb")]
IndexedDb {
name: String,
passphrase: Option<String>,
event_cache_store_lock_holder: String,
},
Custom(StoreConfig),
}
@@ -753,6 +770,7 @@ pub(crate) mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::{async_test, test_json};
use serde_json::{json_internal, Value as JsonValue};
#[cfg(feature = "experimental-sliding-sync")]
use url::Url;
use wiremock::{
matchers::{method, path},
@@ -1129,4 +1147,27 @@ pub(crate) mod tests {
object
})
}
#[async_test]
async fn test_cross_process_store_locks_holder_name() {
{
let homeserver = make_mock_homeserver().await;
let client =
ClientBuilder::new().homeserver_url(homeserver.uri()).build().await.unwrap();
assert_eq!(client.cross_process_store_locks_holder_name(), "main");
}
{
let homeserver = make_mock_homeserver().await;
let client = ClientBuilder::new()
.homeserver_url(homeserver.uri())
.cross_process_store_locks_holder_name("foo".to_owned())
.build()
.await
.unwrap();
assert_eq!(client.cross_process_store_locks_holder_name(), "foo");
}
}
}
+261 -24
View File
@@ -17,7 +17,7 @@
use std::{
collections::{btree_map, BTreeMap},
fmt::{self, Debug},
future::Future,
future::{ready, Future},
pin::Pin,
sync::{Arc, Mutex as StdMutex, RwLock as StdRwLock, Weak},
};
@@ -33,7 +33,7 @@ use imbl::Vector;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::crypto::store::LockableCryptoStore;
use matrix_sdk_base::{
event_cache_store::EventCacheStoreLock,
event_cache::store::EventCacheStoreLock,
store::{DynStateStore, ServerCapabilities},
sync::{Notification, RoomUpdates},
BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta,
@@ -45,13 +45,14 @@ use ruma::{
api::{
client::{
account::whoami,
alias::get_alias,
alias::{create_alias, get_alias},
device::{delete_devices, get_devices, update_device},
directory::{get_public_rooms, get_public_rooms_filtered},
discovery::{
get_capabilities::{self, Capabilities},
get_supported_versions,
},
error::ErrorKind,
filter::{create_filter::v3::Request as FilterUploadRequest, FilterDefinition},
knock::knock_room,
membership::{join_room_by_id, join_room_by_id_or_alias},
@@ -87,7 +88,8 @@ use crate::{
error::{HttpError, HttpResult},
event_cache::EventCache,
event_handler::{
EventHandler, EventHandlerDropGuard, EventHandlerHandle, EventHandlerStore, SyncEvent,
EventHandler, EventHandlerContext, EventHandlerDropGuard, EventHandlerHandle,
EventHandlerStore, ObservableEventHandler, SyncEvent,
},
http_client::HttpClient,
matrix_auth::MatrixAuth,
@@ -274,6 +276,17 @@ pub(crate) struct ClientInner {
/// deduplicate multiple calls to a method.
pub(crate) locks: ClientLocks,
/// The cross-process store locks holder name.
///
/// The SDK provides cross-process store locks (see
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
/// `holder_name` is the value used for all cross-process store locks
/// used by this `Client`.
///
/// If multiple `Client`s are running in different processes, this
/// value MUST be different for each `Client`.
cross_process_store_locks_holder_name: String,
/// A mapping of the times at which the current user sent typing notices,
/// keyed by room.
pub(crate) typing_notice_times: StdRwLock<BTreeMap<OwnedRoomId, Instant>>,
@@ -340,6 +353,7 @@ impl ClientInner {
event_cache: OnceCell<EventCache>,
send_queue: Arc<SendQueueData>,
#[cfg(feature = "e2e-encryption")] encryption_settings: EncryptionSettings,
cross_process_store_locks_holder_name: String,
) -> Arc<Self> {
let client = Self {
server,
@@ -350,6 +364,7 @@ impl ClientInner {
http_client,
base_client,
locks: Default::default(),
cross_process_store_locks_holder_name,
server_capabilities: RwLock::new(server_capabilities),
typing_notice_times: Default::default(),
event_handlers: Default::default(),
@@ -424,6 +439,16 @@ impl Client {
&self.inner.locks
}
/// The cross-process store locks holder name.
///
/// The SDK provides cross-process store locks (see
/// [`matrix_sdk_common::store_locks::CrossProcessStoreLock`]). The
/// `holder_name` is the value used for all cross-process store locks
/// used by this `Client`.
pub fn cross_process_store_locks_holder_name(&self) -> &str {
&self.inner.cross_process_store_locks_holder_name
}
/// Change the homeserver URL used by this client.
///
/// # Arguments
@@ -657,8 +682,6 @@ impl Client {
/// # Examples
///
/// ```no_run
/// # use url::Url;
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// use matrix_sdk::{
/// deserialized_responses::EncryptionInfo,
/// event_handler::Ctx,
@@ -675,14 +698,7 @@ impl Client {
/// };
/// use serde::{Deserialize, Serialize};
///
/// # futures_executor::block_on(async {
/// # let client = matrix_sdk::Client::builder()
/// # .homeserver_url(homeserver)
/// # .server_versions([ruma::api::MatrixVersion::V1_0])
/// # .build()
/// # .await
/// # .unwrap();
/// #
/// # async fn example(client: Client) {
/// client.add_event_handler(
/// |ev: SyncRoomMessageEvent, room: Room, client: Client| async move {
/// // Common usage: Room event plus room and client.
@@ -748,11 +764,11 @@ impl Client {
/// client.add_event_handler(move |ev: SyncRoomMessageEvent | async move {
/// println!("Calling the handler with identifier {data}");
/// });
/// # });
/// # }
/// ```
pub fn add_event_handler<Ev, Ctx, H>(&self, handler: H) -> EventHandlerHandle
where
Ev: SyncEvent + DeserializeOwned + Send + 'static,
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
H: EventHandler<Ev, Ctx>,
{
self.add_event_handler_impl(handler, None)
@@ -774,12 +790,133 @@ impl Client {
handler: H,
) -> EventHandlerHandle
where
Ev: SyncEvent + DeserializeOwned + Send + 'static,
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
H: EventHandler<Ev, Ctx>,
{
self.add_event_handler_impl(handler, Some(room_id.to_owned()))
}
/// Observe a specific event type.
///
/// `Ev` represents the kind of event that will be observed. `Ctx`
/// represents the context that will come with the event. It relies on the
/// same mechanism as [`Client::add_event_handler`]. The main difference is
/// that it returns an [`ObservableEventHandler`] and doesn't require a
/// user-defined closure. It is possible to subscribe to the
/// [`ObservableEventHandler`] to get an [`EventHandlerSubscriber`], which
/// implements a [`Stream`]. The `Stream::Item` will be of type `(Ev,
/// Ctx)`.
///
/// # Example
///
/// Let's see a classical usage:
///
/// ```
/// use futures_util::StreamExt as _;
/// use matrix_sdk::{
/// ruma::{events::room::message::SyncRoomMessageEvent, push::Action},
/// Client, Room,
/// };
///
/// # async fn example(client: Client) -> Option<()> {
/// let observer =
/// client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
///
/// let mut subscriber = observer.subscribe();
///
/// let (event, (room, push_actions)) = subscriber.next().await?;
/// # Some(())
/// # }
/// ```
///
/// Now let's see how to get several contexts that can be useful for you:
///
/// ```
/// use matrix_sdk::{
/// deserialized_responses::EncryptionInfo,
/// ruma::{
/// events::room::{
/// message::SyncRoomMessageEvent, topic::SyncRoomTopicEvent,
/// },
/// push::Action,
/// },
/// Client, Room,
/// };
///
/// # async fn example(client: Client) {
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + `Client`.
/// let _ = client.observe_events::<SyncRoomMessageEvent, (Room, Client)>();
///
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + `EncryptionInfo`
/// // to distinguish between unencrypted events and events that were decrypted
/// // by the SDK.
/// let _ = client
/// .observe_events::<SyncRoomMessageEvent, (Room, Option<EncryptionInfo>)>(
/// );
///
/// // Observe `SyncRoomMessageEvent` and fetch `Room` + push actions.
/// // For example, an event with `Action::SetTweak(Tweak::Highlight(true))`
/// // should be highlighted in the timeline.
/// let _ =
/// client.observe_events::<SyncRoomMessageEvent, (Room, Vec<Action>)>();
///
/// // Observe `SyncRoomTopicEvent` and fetch nothing else.
/// let _ = client.observe_events::<SyncRoomTopicEvent, ()>();
/// # }
/// ```
///
/// [`EventHandlerSubscriber`]: crate::event_handler::EventHandlerSubscriber
pub fn observe_events<Ev, Ctx>(&self) -> ObservableEventHandler<(Ev, Ctx)>
where
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
{
self.observe_room_events_impl(None)
}
/// Observe a specific room, and event type.
///
/// This method works the same way as [`Client::observe_events`], except
/// that the observability will only be applied for events in the room with
/// the specified ID. See that method for more details.
pub fn observe_room_events<Ev, Ctx>(
&self,
room_id: &RoomId,
) -> ObservableEventHandler<(Ev, Ctx)>
where
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
{
self.observe_room_events_impl(Some(room_id.to_owned()))
}
/// Shared implementation for `Client::observe_events` and
/// `Client::observe_room_events`.
fn observe_room_events_impl<Ev, Ctx>(
&self,
room_id: Option<OwnedRoomId>,
) -> ObservableEventHandler<(Ev, Ctx)>
where
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + SyncOutsideWasm + 'static,
Ctx: EventHandlerContext + SendOutsideWasm + SyncOutsideWasm + 'static,
{
// The default value is `None`. It becomes `Some((Ev, Ctx))` once it has a
// new value.
let shared_observable = SharedObservable::new(None);
ObservableEventHandler::new(
shared_observable.clone(),
self.event_handler_drop_guard(self.add_event_handler_impl(
move |event: Ev, context: Ctx| {
shared_observable.set(Some((event, context)));
ready(())
},
room_id,
)),
)
}
/// Remove the event handler associated with the handle.
///
/// Note that you **must not** call `remove_event_handler` from the
@@ -1038,6 +1175,34 @@ impl Client {
self.send(request, None).await
}
/// Checks if a room alias is not in use yet.
///
/// Returns:
/// - `Ok(true)` if the room alias is available.
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
/// status code).
/// - An `Err` otherwise.
pub async fn is_room_alias_available(&self, alias: &RoomAliasId) -> HttpResult<bool> {
match self.resolve_room_alias(alias).await {
// The room alias was resolved, so it's already in use.
Ok(_) => Ok(false),
Err(error) => {
match error.client_api_error_kind() {
// The room alias wasn't found, so it's available.
Some(ErrorKind::NotFound) => Ok(true),
_ => Err(error),
}
}
}
}
/// Creates a new room alias associated with a room.
pub async fn create_room_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> HttpResult<()> {
let request = create_alias::v3::Request::new(alias.to_owned(), room_id.to_owned());
self.send(request, None).await?;
Ok(())
}
/// Update the homeserver from the login response well-known if needed.
///
/// # Arguments
@@ -2201,7 +2366,15 @@ impl Client {
}
/// Create a new specialized `Client` that can process notifications.
pub async fn notification_client(&self) -> Result<Client> {
///
/// See [`CrossProcessStoreLock::new`] to learn more about
/// `cross_process_store_locks_holder_name`.
///
/// [`CrossProcessStoreLock::new`]: matrix_sdk_common::store_locks::CrossProcessStoreLock::new
pub async fn notification_client(
&self,
cross_process_store_locks_holder_name: String,
) -> Result<Client> {
let client = Client {
inner: ClientInner::new(
self.inner.auth_ctx.clone(),
@@ -2210,13 +2383,17 @@ impl Client {
#[cfg(feature = "experimental-sliding-sync")]
self.sliding_sync_version(),
self.inner.http_client.clone(),
self.inner.base_client.clone_with_in_memory_state_store().await?,
self.inner
.base_client
.clone_with_in_memory_state_store(&cross_process_store_locks_holder_name)
.await?,
self.inner.server_capabilities.read().await.clone(),
self.inner.respect_login_well_known,
self.inner.event_cache.clone(),
self.inner.send_queue_data.clone(),
#[cfg(feature = "e2e-encryption")]
self.inner.e2ee.encryption_settings,
cross_process_store_locks_holder_name,
)
.await,
};
@@ -2328,7 +2505,7 @@ pub(crate) mod tests {
api::{client::room::create_room::v3::Request as CreateRoomRequest, MatrixVersion},
assign,
events::ignored_user_list::IgnoredUserListEventContent,
owned_room_id, room_id, RoomId, ServerName, UserId,
owned_room_id, room_alias_id, room_id, RoomId, ServerName, UserId,
};
use serde_json::json;
use tokio::{
@@ -2346,8 +2523,8 @@ pub(crate) mod tests {
client::WeakClient,
config::{RequestConfig, SyncSettings},
test_utils::{
logged_in_client, no_retry_test_client, set_client_session, test_client_builder,
test_client_builder_with_server,
logged_in_client, mocks::MatrixMockServer, no_retry_test_client, set_client_session,
test_client_builder, test_client_builder_with_server,
},
Error,
};
@@ -2739,7 +2916,10 @@ pub(crate) mod tests {
let memory_store = Arc::new(MemoryStore::new());
let client = Client::builder()
.insecure_server_name_no_tls(server_name)
.store_config(StoreConfig::new().state_store(memory_store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(memory_store.clone()),
)
.build()
.await
.unwrap();
@@ -2758,7 +2938,10 @@ pub(crate) mod tests {
let client = Client::builder()
.insecure_server_name_no_tls(server_name)
.store_config(StoreConfig::new().state_store(memory_store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(memory_store.clone()),
)
.build()
.await
.unwrap();
@@ -2915,4 +3098,58 @@ pub(crate) mod tests {
.await
.unwrap_err();
}
#[async_test]
async fn test_is_room_alias_available_if_alias_is_not_resolved() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_room_directory_resolve_alias().not_found().expect(1).mount().await;
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
assert_matches!(ret, Ok(true));
}
#[async_test]
async fn test_is_room_alias_available_if_alias_is_resolved() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server
.mock_room_directory_resolve_alias()
.ok("!some_room_id:matrix.org", Vec::new())
.expect(1)
.mount()
.await;
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
assert_matches!(ret, Ok(false));
}
#[async_test]
async fn test_is_room_alias_available_if_error_found() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_room_directory_resolve_alias().error500().expect(1).mount().await;
let ret = client.is_room_alias_available(room_alias_id!("#some_alias:matrix.org")).await;
assert_matches!(ret, Err(_));
}
#[async_test]
async fn test_create_room_alias() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_create_room_alias().ok().expect(1).mount().await;
let ret = client
.create_room_alias(
room_alias_id!("#some_alias:matrix.org"),
room_id!("!some_room:matrix.org"),
)
.await;
assert_matches!(ret, Ok(()));
}
}
+25 -4
View File
@@ -25,7 +25,7 @@ use eyeball::Subscriber;
use matrix_sdk_common::boxed_into_future;
use ruma::events::room::{EncryptedFile, EncryptedFileInit};
use crate::{Client, Result, TransmissionProgress};
use crate::{config::RequestConfig, Client, Media, Result, TransmissionProgress};
/// Future returned by [`Client::upload_encrypted_file`].
#[allow(missing_debug_implementations)]
@@ -34,11 +34,18 @@ pub struct UploadEncryptedFile<'a, R: ?Sized> {
content_type: &'a mime::Mime,
reader: &'a mut R,
send_progress: SharedObservable<TransmissionProgress>,
request_config: Option<RequestConfig>,
}
impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> {
pub(crate) fn new(client: &'a Client, content_type: &'a mime::Mime, reader: &'a mut R) -> Self {
Self { client, content_type, reader, send_progress: Default::default() }
Self {
client,
content_type,
reader,
send_progress: Default::default(),
request_config: None,
}
}
/// Replace the default `SharedObservable` used for tracking upload
@@ -55,6 +62,15 @@ impl<'a, R: ?Sized> UploadEncryptedFile<'a, R> {
self
}
/// Replace the default request config used for the upload request.
///
/// The timeout value will be overridden with a reasonable default, based on
/// the size of the encrypted payload.
pub fn with_request_config(mut self, request_config: RequestConfig) -> Self {
self.request_config = Some(request_config);
self
}
/// Get a subscriber to observe the progress of sending the request
/// body.
#[cfg(not(target_arch = "wasm32"))]
@@ -71,16 +87,21 @@ where
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
let Self { client, content_type, reader, send_progress } = self;
let Self { client, content_type, reader, send_progress, request_config } = self;
Box::pin(async move {
let mut encryptor = matrix_sdk_base::crypto::AttachmentEncryptor::new(reader);
let mut buf = Vec::new();
encryptor.read_to_end(&mut buf)?;
// Override the reasonable upload timeout value, based on the size of the
// encrypted payload.
let request_config =
request_config.map(|config| config.timeout(Media::reasonable_upload_timeout(&buf)));
let response = client
.media()
.upload(content_type, buf)
.upload(content_type, buf, request_config)
.with_send_progress_observable(send_progress)
.await?;
+2
View File
@@ -1458,6 +1458,8 @@ impl Encryption {
/// caches.
///
/// The provided `lock_value` must be a unique identifier for this process.
/// Check [`Client::cross_process_store_locks_holder_name`] to
/// get the global value.
pub async fn enable_cross_process_store_lock(&self, lock_value: String) -> Result<(), Error> {
// If the lock has already been created, don't recreate it from scratch.
if let Some(prev_lock) = self.client.locks().cross_process_crypto_store_lock.get() {
+1 -1
View File
@@ -25,7 +25,7 @@ use matrix_sdk_base::crypto::{
CryptoStoreError, DecryptorError, KeyExportError, MegolmError, OlmError,
};
use matrix_sdk_base::{
event_cache_store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState,
event_cache::store::EventCacheStoreError, Error as SdkBaseError, QueueWedgeError, RoomState,
StoreError,
};
use reqwest::Error as ReqwestError;
-1
View File
@@ -54,7 +54,6 @@ use self::paginator::PaginatorError;
use crate::{client::WeakClient, Client};
mod deduplicator;
mod linked_chunk;
mod pagination;
mod room;
@@ -18,11 +18,11 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration};
use eyeball::Subscriber;
use matrix_sdk_base::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_common::linked_chunk::ChunkContent;
use tokio::time::timeout;
use tracing::{debug, instrument, trace};
use super::{
linked_chunk::ChunkContent,
paginator::{PaginationResult, PaginatorState},
room::{
events::{Gap, RoomEvents},
@@ -14,24 +14,14 @@
use std::cmp::Ordering;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
pub use matrix_sdk_base::event_cache::{Event, Gap};
use matrix_sdk_common::linked_chunk::{
Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position,
};
use ruma::OwnedEventId;
use tracing::{debug, error, warn};
use super::super::{
deduplicator::{Decoration, Deduplicator},
linked_chunk::{Chunk, ChunkIdentifier, EmptyChunk, Error, Iter, LinkedChunk, Position},
};
/// An alias for the real event type.
pub(crate) type Event = SyncTimelineEvent;
#[derive(Clone, Debug)]
pub struct Gap {
/// The token to use in the query, extracted from a previous "from" /
/// "end" field of a `/messages` response.
pub prev_token: String,
}
use super::super::deduplicator::{Decoration, Deduplicator};
const DEFAULT_CHUNK_CAPACITY: usize = 128;
@@ -107,3 +107,38 @@ impl<T> Deref for Ctx<T> {
&self.0
}
}
// `EventHandlerContext` for tuples.
impl EventHandlerContext for () {
fn from_data(_data: &EventHandlerData<'_>) -> Option<Self> {
Some(())
}
}
macro_rules! impl_context_for_tuple {
( $( $ty:ident ),* $(,)? ) => {
#[allow(non_snake_case)]
impl< $( $ty ),* > EventHandlerContext for ( $( $ty ),* , )
where
$( $ty : EventHandlerContext, )*
{
fn from_data(data: &EventHandlerData<'_>) -> Option<Self> {
$(
let $ty = $ty ::from_data(data)?;
)*
Some(( $( $ty ),* , ))
}
}
};
}
impl_context_for_tuple!(A);
impl_context_for_tuple!(A, B);
impl_context_for_tuple!(A, B, C);
impl_context_for_tuple!(A, B, C, D);
impl_context_for_tuple!(A, B, C, D, E);
impl_context_for_tuple!(A, B, C, D, E, F);
impl_context_for_tuple!(A, B, C, D, E, F, G);
impl_context_for_tuple!(A, B, C, D, E, F, G, H);
+296 -2
View File
@@ -40,16 +40,20 @@ use std::{
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering::SeqCst},
RwLock,
Arc, RwLock, Weak,
},
task::{Context, Poll},
};
use anymap2::any::CloneAnySendSync;
use eyeball::{SharedObservable, Subscriber};
use futures_core::Stream;
use futures_util::stream::{FuturesUnordered, StreamExt};
use matrix_sdk_base::{
deserialized_responses::{EncryptionInfo, SyncTimelineEvent},
SendOutsideWasm, SyncOutsideWasm,
};
use pin_project_lite::pin_project;
use ruma::{events::AnySyncStateEvent, push::Action, serde::Raw, OwnedRoomId};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::value::RawValue as RawJsonValue;
@@ -287,7 +291,7 @@ impl Client {
room_id: Option<OwnedRoomId>,
) -> EventHandlerHandle
where
Ev: SyncEvent + DeserializeOwned + Send + 'static,
Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static,
H: EventHandler<Ev, Ctx>,
{
let handler_fn: Box<EventHandlerFn> = Box::new(move |data| {
@@ -535,11 +539,139 @@ impl_event_handler!(A, B, C, D, E, F);
impl_event_handler!(A, B, C, D, E, F, G);
impl_event_handler!(A, B, C, D, E, F, G, H);
/// An observer of events (may be tailored to a room).
///
/// To create such observer, use [`Client::observe_events`] or
/// [`Client::observe_room_events`].
#[derive(Debug)]
pub struct ObservableEventHandler<T> {
/// This type is actually nothing more than a thin glue layer between the
/// [`EventHandler`] mechanism and the reactive programming types from
/// [`eyeball`]. Here, we use a [`SharedObservable`] that is updated by the
/// [`EventHandler`].
shared_observable: SharedObservable<Option<T>>,
/// This type owns the [`EventHandlerDropGuard`]. As soon as this type goes
/// out of scope, the event handler is unregistered/removed.
///
/// [`EventHandlerSubscriber`] holds a weak, non-owning reference, to this
/// guard. It is useful to detect when to close the [`Stream`]: as soon as
/// this type goes out of scope, the subscriber will close itself on poll.
event_handler_guard: Arc<EventHandlerDropGuard>,
}
impl<T> ObservableEventHandler<T> {
pub(crate) fn new(
shared_observable: SharedObservable<Option<T>>,
event_handler_guard: EventHandlerDropGuard,
) -> Self {
Self { shared_observable, event_handler_guard: Arc::new(event_handler_guard) }
}
/// Subscribe to this observer.
///
/// It returns an [`EventHandlerSubscriber`], which implements [`Stream`].
/// See its documentation to learn more.
pub fn subscribe(&self) -> EventHandlerSubscriber<T> {
EventHandlerSubscriber::new(
self.shared_observable.subscribe(),
// The subscriber holds a weak non-owning reference to the event handler guard, so that
// it can detect when this observer is dropped, and can close the subscriber's stream.
Arc::downgrade(&self.event_handler_guard),
)
}
}
pin_project! {
/// The subscriber of an [`ObservableEventHandler`].
///
/// To create such subscriber, use [`ObservableEventHandler::subscribe`].
///
/// This type implements [`Stream`], which means it is possible to poll the
/// next value asynchronously. In other terms, polling this type will return
/// the new event as soon as they are synced. See [`Client::observe_events`]
/// to learn more.
#[derive(Debug)]
pub struct EventHandlerSubscriber<T> {
// The `Subscriber` associated to the `SharedObservable` inside
// `ObservableEventHandle`.
//
// Keep in mind all this API is just a thin glue layer between
// `EventHandle` and `SharedObservable`, that's… maagiic!
#[pin]
subscriber: Subscriber<Option<T>>,
// A weak non-owning reference to the event handler guard from
// `ObservableEventHandler`. When this type is polled (via its `Stream`
// implementation), it is possible to detect whether the observable has
// been dropped by upgrading this weak reference, and close the `Stream`
// if it needs to.
event_handler_guard: Weak<EventHandlerDropGuard>,
}
}
impl<T> EventHandlerSubscriber<T> {
fn new(
subscriber: Subscriber<Option<T>>,
event_handler_handle: Weak<EventHandlerDropGuard>,
) -> Self {
Self { subscriber, event_handler_guard: event_handler_handle }
}
}
impl<T> Stream for EventHandlerSubscriber<T>
where
T: Clone,
{
type Item = T;
fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.project();
let Some(_) = this.event_handler_guard.upgrade() else {
// The `EventHandlerHandle` has been dropped via `EventHandlerDropGuard`. It
// means the `ObservableEventHandler` has been dropped. It's time to
// close this stream.
return Poll::Ready(None);
};
// First off, the subscriber is of type `Subscriber<Option<T>>` because the
// `SharedObservable` starts with a `None` value to indicate it has no yet
// received any update. We want the `Stream` to return `T`, not `Option<T>`. We
// then filter out all `None` value.
//
// Second, when a `None` value is met, we want to poll again (hence the `loop`).
// At best, there is a new value to return. At worst, the subscriber will return
// `Poll::Pending` and will register the wakers accordingly.
loop {
match this.subscriber.as_mut().poll_next(context) {
// Stream has been closed somehow.
Poll::Ready(None) => return Poll::Ready(None),
// The initial value (of the `SharedObservable` behind `self.subscriber`) has been
// polled. We want to filter it out.
Poll::Ready(Some(None)) => {
// Loop over.
continue;
}
// We have a new value!
Poll::Ready(Some(Some(value))) => return Poll::Ready(Some(value)),
// Classical pending.
Poll::Pending => return Poll::Pending,
}
}
}
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, InvitedRoomBuilder, JoinedRoomBuilder, DEFAULT_TEST_ROOM_ID,
};
use stream_assert::{assert_closed, assert_pending, assert_ready};
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
use std::{
@@ -753,6 +885,20 @@ mod tests {
Ok(())
}
#[async_test]
#[allow(dependency_on_unit_never_type_fallback)]
async fn test_add_event_handler_with_tuples() -> crate::Result<()> {
let client = logged_in_client(None).await;
client.add_event_handler(
|_ev: OriginalSyncRoomMemberEvent, (_room, _client): (Room, Client)| future::ready(()),
);
// If it compiles, it works. No need to assert anything.
Ok(())
}
#[async_test]
#[allow(dependency_on_unit_never_type_fallback)]
async fn test_remove_event_handler() -> crate::Result<()> {
@@ -870,4 +1016,152 @@ mod tests {
assert_eq!(counter.load(SeqCst), 1);
Ok(())
}
#[async_test]
#[allow(dependency_on_unit_never_type_fallback)]
async fn test_observe_events() -> crate::Result<()> {
let client = logged_in_client(None).await;
let room_id_0 = room_id!("!r0.matrix.org");
let room_id_1 = room_id!("!r1.matrix.org");
let observable = client.observe_events::<OriginalSyncRoomNameEvent, Room>();
let mut subscriber = observable.subscribe();
assert_pending!(subscriber);
let mut response_builder = SyncResponseBuilder::new();
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_0).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"name": "Name 0"
},
"event_id": "$ev0",
"origin_server_ts": 1,
"sender": "@mnt_io:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 1,
}
})),
))
.build_sync_response();
client.process_sync(response).await?;
let (room_name, room) = assert_ready!(subscriber);
assert_eq!(room_name.event_id.as_str(), "$ev0");
assert_eq!(room.room_id(), room_id_0);
assert_eq!(room.name().unwrap(), "Name 0");
assert_pending!(subscriber);
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id_1).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"name": "Name 1"
},
"event_id": "$ev1",
"origin_server_ts": 2,
"sender": "@mnt_io:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 2,
}
})),
))
.build_sync_response();
client.process_sync(response).await?;
let (room_name, room) = assert_ready!(subscriber);
assert_eq!(room_name.event_id.as_str(), "$ev1");
assert_eq!(room.room_id(), room_id_1);
assert_eq!(room.name().unwrap(), "Name 1");
assert_pending!(subscriber);
drop(observable);
assert_closed!(subscriber);
Ok(())
}
#[async_test]
#[allow(dependency_on_unit_never_type_fallback)]
async fn test_observe_room_events() -> crate::Result<()> {
let client = logged_in_client(None).await;
let room_id = room_id!("!r0.matrix.org");
let observable_for_room =
client.observe_room_events::<OriginalSyncRoomNameEvent, (Room, Client)>(room_id);
let mut subscriber_for_room = observable_for_room.subscribe();
assert_pending!(subscriber_for_room);
let mut response_builder = SyncResponseBuilder::new();
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"name": "Name 0"
},
"event_id": "$ev0",
"origin_server_ts": 1,
"sender": "@mnt_io:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 1,
}
})),
))
.build_sync_response();
client.process_sync(response).await?;
let (room_name, (room, _client)) = assert_ready!(subscriber_for_room);
assert_eq!(room_name.event_id.as_str(), "$ev0");
assert_eq!(room.name().unwrap(), "Name 0");
assert_pending!(subscriber_for_room);
let response = response_builder
.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"name": "Name 1"
},
"event_id": "$ev1",
"origin_server_ts": 2,
"sender": "@mnt_io:matrix.org",
"state_key": "",
"type": "m.room.name",
"unsigned": {
"age": 2,
}
})),
))
.build_sync_response();
client.process_sync(response).await?;
let (room_name, (room, _client)) = assert_ready!(subscriber_for_room);
assert_eq!(room_name.event_id.as_str(), "$ev1");
assert_eq!(room.name().unwrap(), "Name 1");
assert_pending!(subscriber_for_room);
drop(observable_for_room);
assert_closed!(subscriber_for_room);
Ok(())
}
}
+4 -3
View File
@@ -25,9 +25,10 @@ pub use matrix_sdk_base::crypto;
pub use matrix_sdk_base::{
deserialized_responses,
store::{DynStateStore, MemoryStore, StateStoreExt},
ComposerDraft, ComposerDraftType, DisplayName, QueueWedgeError, Room as BaseRoom,
RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomMember as BaseRoomMember,
RoomMemberships, RoomState, SessionMeta, StateChanges, StateStore, StoreError,
ComposerDraft, ComposerDraftType, QueueWedgeError, Room as BaseRoom,
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomMember as BaseRoomMember, RoomMemberships, RoomState, SessionMeta, StateChanges,
StateStore, StoreError,
};
pub use matrix_sdk_common::*;
pub use reqwest;
+29 -12
View File
@@ -40,7 +40,8 @@ use tempfile::{Builder as TempFileBuilder, NamedTempFile, TempDir};
use tokio::{fs::File as TokioFile, io::AsyncWriteExt};
use crate::{
attachment::Thumbnail, futures::SendRequest, Client, Error, Result, TransmissionProgress,
attachment::Thumbnail, config::RequestConfig, futures::SendRequest, Client, Error, Result,
TransmissionProgress,
};
/// A conservative upload speed of 1Mbps
@@ -144,8 +145,11 @@ impl Media {
/// * `content_type` - The type of the media, this will be used as the
/// content-type header.
///
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
/// media.
/// * `data` - Vector of bytes to be uploaded to the server.
///
/// * `request_config` - Optional request configuration for the HTTP client,
/// overriding the default. If not provided, a reasonable timeout value is
/// inferred.
///
/// # Examples
///
@@ -159,25 +163,38 @@ impl Media {
/// # let mut client = Client::new(homeserver).await?;
/// let image = fs::read("/home/example/my-cat.jpg")?;
///
/// let response = client.media().upload(&mime::IMAGE_JPEG, image).await?;
/// let response =
/// client.media().upload(&mime::IMAGE_JPEG, image, None).await?;
///
/// println!("Cat URI: {}", response.content_uri);
/// # anyhow::Ok(()) };
/// ```
pub fn upload(&self, content_type: &Mime, data: Vec<u8>) -> SendUploadRequest {
let timeout = std::cmp::max(
Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
MIN_UPLOAD_REQUEST_TIMEOUT,
);
pub fn upload(
&self,
content_type: &Mime,
data: Vec<u8>,
request_config: Option<RequestConfig>,
) -> SendUploadRequest {
let request_config = request_config.unwrap_or_else(|| {
self.client.request_config().timeout(Self::reasonable_upload_timeout(&data))
});
let request = assign!(media::create_content::v3::Request::new(data), {
content_type: Some(content_type.essence_str().to_owned()),
});
let request_config = self.client.request_config().timeout(timeout);
self.client.send(request, Some(request_config))
}
/// Returns a reasonable upload timeout for an upload, based on the size of
/// the data to be uploaded.
pub(crate) fn reasonable_upload_timeout(data: &[u8]) -> Duration {
std::cmp::max(
Duration::from_secs(data.len() as u64 / DEFAULT_UPLOAD_SPEED),
MIN_UPLOAD_REQUEST_TIMEOUT,
)
}
/// Preallocates an MXC URI for a media that will be uploaded soon.
///
/// This preallocates an URI *before* any content is uploaded to the server.
@@ -630,7 +647,7 @@ impl Media {
let upload_thumbnail = self.upload_thumbnail(thumbnail, send_progress.clone());
let upload_attachment = async move {
self.upload(content_type, data)
self.upload(content_type, data, None)
.with_send_progress_observable(send_progress)
.await
.map_err(Error::from)
@@ -653,7 +670,7 @@ impl Media {
};
let response = self
.upload(&thumbnail.content_type, thumbnail.data)
.upload(&thumbnail.content_type, thumbnail.data, None)
.with_send_progress_observable(send_progress)
.await?;
let url = response.content_uri;
+6 -2
View File
@@ -2052,7 +2052,7 @@ impl Room {
) -> MessageType {
// If caption is set, use it as body, and filename as the file name; otherwise,
// body is the filename, and the filename is not set.
// https://github.com/tulir/matrix-spec-proposals/blob/body-as-caption/proposals/2530-body-as-caption.md
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
let (body, filename) = match caption {
Some(caption) => (caption, Some(filename.to_owned())),
None => (filename.to_owned(), None),
@@ -2263,7 +2263,7 @@ impl Room {
) -> Result<send_state_event::v3::Response> {
self.ensure_room_joined()?;
let upload_response = self.client.media().upload(mime, data).await?;
let upload_response = self.client.media().upload(mime, data, None).await?;
let mut info = info.unwrap_or_default();
info.blurhash = upload_response.blurhash;
info.mimetype = Some(mime.to_string());
@@ -2975,6 +2975,10 @@ impl Room {
return Ok(());
}
if !self.can_user_trigger_room_notification(self.own_user_id()).await? {
return Ok(());
}
self.send_call_notification(
self.room_id().to_string().to_owned(),
ApplicationType::Call,
+123 -6
View File
@@ -18,9 +18,11 @@
//! This offers a few capabilities for previewing the content of the room as
//! well.
use futures_util::future::join_all;
use matrix_sdk_base::{RoomInfo, RoomState};
use ruma::{
api::client::{membership::joined_members, state::get_state_events},
directory::PublicRoomJoinRule,
events::room::{history_visibility::HistoryVisibility, join_rules::JoinRule},
room::RoomType,
space::SpaceRoomJoinRule,
@@ -29,7 +31,7 @@ use ruma::{
use tokio::try_join;
use tracing::{instrument, warn};
use crate::{Client, Room};
use crate::{room_directory_search::RoomDirectorySearch, Client, Room};
/// The preview of a room, be it invited/joined/left, or not.
#[derive(Debug, Clone)]
@@ -55,6 +57,9 @@ pub struct RoomPreview {
/// The number of joined members.
pub num_joined_members: u64,
/// The number of active members, if known (joined + invited).
pub num_active_members: Option<u64>,
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: Option<RoomType>,
@@ -83,6 +88,7 @@ impl RoomPreview {
room_info: RoomInfo,
is_direct: Option<bool>,
num_joined_members: u64,
num_active_members: Option<u64>,
state: Option<RoomState>,
) -> Self {
RoomPreview {
@@ -107,6 +113,7 @@ impl RoomPreview {
},
is_world_readable: *room_info.history_visibility() == HistoryVisibility::WorldReadable,
num_joined_members,
num_active_members,
state,
is_direct,
}
@@ -121,6 +128,7 @@ impl RoomPreview {
room.clone_info(),
is_direct,
room.joined_members_count(),
Some(room.active_members_count()),
Some(room.state()),
)
}
@@ -134,21 +142,76 @@ impl RoomPreview {
) -> crate::Result<Self> {
// Use the room summary endpoint, if available, as described in
// https://github.com/deepbluev7/matrix-doc/blob/room-summaries/proposals/3266-room-summary.md
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via).await {
match Self::from_room_summary(client, room_id.clone(), room_or_alias_id, via.clone()).await
{
Ok(res) => return Ok(res),
Err(err) => {
warn!("error when previewing room from the room summary endpoint: {err}");
}
}
// TODO: (optimization) Use the room search directory, if available:
// - if the room directory visibility is public,
// - then use a public room filter set to this room id
// Try room directory search next.
match Self::from_room_directory_search(client, &room_id, room_or_alias_id, via).await {
Ok(Some(res)) => return Ok(res),
Ok(None) => warn!("Room '{room_or_alias_id}' not found in room directory search."),
Err(err) => {
warn!("Searching for '{room_or_alias_id}' in room directory search failed: {err}");
}
}
// Resort to using the room state endpoint, as well as the joined members one.
Self::from_state_events(client, &room_id).await
}
/// Get a [`RoomPreview`] by searching in the room directory for the
/// provided room alias or room id and transforming the [`RoomDescription`]
/// into a preview.
pub(crate) async fn from_room_directory_search(
client: &Client,
room_id: &RoomId,
room_or_alias_id: &RoomOrAliasId,
via: Vec<OwnedServerName>,
) -> crate::Result<Option<Self>> {
// Get either the room alias or the room id without the leading identifier char
let search_term = if room_or_alias_id.is_room_alias_id() {
Some(room_or_alias_id.as_str()[1..].to_owned())
} else {
None
};
// If we have no alias, filtering using a room id is impossible, so just take
// the first 100 results and try to find the current room #YOLO
let batch_size = if search_term.is_some() { 20 } else { 100 };
if via.is_empty() {
// Just search in the current homeserver
search_for_room_preview_in_room_directory(
client.clone(),
search_term,
batch_size,
None,
room_id,
)
.await
} else {
let mut futures = Vec::new();
// Search for all servers and retrieve the results
for server in via {
futures.push(search_for_room_preview_in_room_directory(
client.clone(),
search_term.clone(),
batch_size,
Some(server),
room_id,
));
}
let joined_results = join_all(futures).await;
Ok(joined_results.into_iter().flatten().next().flatten())
}
}
/// Get a [`RoomPreview`] using MSC3266, if available on the remote server.
///
/// Will fail with a 404 if the API is not available.
@@ -178,6 +241,8 @@ impl RoomPreview {
response.membership.map(|membership| RoomState::from(&membership))
};
let num_active_members = cached_room.as_ref().map(|r| r.active_members_count());
let is_direct = if let Some(cached_room) = cached_room {
cached_room.is_direct().await.ok()
} else {
@@ -191,6 +256,7 @@ impl RoomPreview {
topic: response.topic,
avatar_url: response.avatar_url,
num_joined_members: response.num_joined_members.into(),
num_active_members,
room_type: response.room_type,
join_rule: response.join_rule,
is_world_readable: response.world_readable,
@@ -238,8 +304,59 @@ impl RoomPreview {
let room = client.get_room(room_id);
let state = room.as_ref().map(|room| room.state());
let num_active_members = room.as_ref().map(|r| r.active_members_count());
let is_direct = if let Some(room) = room { room.is_direct().await.ok() } else { None };
Ok(Self::from_room_info(room_info, is_direct, num_joined_members, state))
Ok(Self::from_room_info(
room_info,
is_direct,
num_joined_members,
num_active_members,
state,
))
}
}
async fn search_for_room_preview_in_room_directory(
client: Client,
filter: Option<String>,
batch_size: u32,
server: Option<OwnedServerName>,
expected_room_id: &RoomId,
) -> crate::Result<Option<RoomPreview>> {
let mut directory_search = RoomDirectorySearch::new(client);
directory_search.search(filter, batch_size, server).await?;
let (results, _) = directory_search.results();
for room_description in results {
// Iterate until we find a room description with a matching room id
if room_description.room_id != expected_room_id {
continue;
}
return Ok(Some(RoomPreview {
room_id: room_description.room_id,
canonical_alias: room_description.alias,
name: room_description.name,
topic: room_description.topic,
avatar_url: room_description.avatar_url,
num_joined_members: room_description.joined_members,
num_active_members: None,
// Assume it's a room
room_type: None,
join_rule: match room_description.join_rule {
PublicRoomJoinRule::Public => SpaceRoomJoinRule::Public,
PublicRoomJoinRule::Knock => SpaceRoomJoinRule::Knock,
PublicRoomJoinRule::_Custom(rule) => SpaceRoomJoinRule::_Custom(rule),
_ => {
panic!("Unexpected PublicRoomJoinRule {:?}", room_description.join_rule)
}
},
is_world_readable: room_description.is_world_readable,
state: None,
is_direct: None,
}));
}
Ok(None)
}
+348 -156
View File
@@ -77,7 +77,7 @@
//! no thumbnails):
//!
//! - The file's content is immediately cached in the
//! [`matrix_sdk_base::event_cache_store::EventCacheStore`], using an MXC ID
//! [`matrix_sdk_base::event_cache::store::EventCacheStore`], using an MXC ID
//! that is temporary and designates a local URI without any possible doubt.
//! - An initial media event is created and uses this temporary MXC ID, and
//! propagated as a local echo for an event.
@@ -129,17 +129,17 @@
//! remembered and fixed up into the media event, just before sending it.
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, HashMap},
str::FromStr as _,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock as SyncRwLock,
Arc, RwLock,
},
};
use as_variant::as_variant;
use matrix_sdk_base::{
event_cache_store::EventCacheStoreError,
event_cache::store::EventCacheStoreError,
media::MediaRequestParameters,
store::{
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
@@ -161,7 +161,7 @@ use ruma::{
serde::Raw,
OwnedEventId, OwnedRoomId, OwnedTransactionId, TransactionId,
};
use tokio::sync::{broadcast, Notify, RwLock};
use tokio::sync::{broadcast, oneshot, Mutex, Notify, OwnedMutexGuard};
use tracing::{debug, error, info, instrument, trace, warn};
#[cfg(feature = "e2e-encryption")]
@@ -171,7 +171,7 @@ use crate::{
config::RequestConfig,
error::RetryKind,
room::{edit::EditedContent, WeakRoom},
Client, Room,
Client, Media, Room,
};
mod upload;
@@ -310,7 +310,7 @@ impl Client {
pub(super) struct SendQueueData {
/// Mapping of room to their unique send queue.
rooms: SyncRwLock<BTreeMap<OwnedRoomId, RoomSendQueue>>,
rooms: RwLock<BTreeMap<OwnedRoomId, RoomSendQueue>>,
/// Is the whole mechanism enabled or disabled?
///
@@ -449,7 +449,7 @@ impl RoomSendQueue {
let send_handle = SendHandle {
room: self.clone(),
transaction_id: transaction_id.clone(),
is_upload: false,
media_handles: None,
};
let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
@@ -542,7 +542,7 @@ impl RoomSendQueue {
continue;
}
let queued_request = match queue.peek_next_to_send().await {
let (queued_request, cancel_upload_rx) = match queue.peek_next_to_send().await {
Ok(Some(request)) => request,
Ok(None) => {
@@ -571,8 +571,9 @@ impl RoomSendQueue {
continue;
};
match Self::handle_request(&room, queued_request).await {
Ok(parent_key) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await {
match Self::handle_request(&room, queued_request, cancel_upload_rx).await {
Ok(Some(parent_key)) => match queue.mark_as_sent(&txn_id, parent_key.clone()).await
{
Ok(()) => match parent_key {
SentRequestKey::Event(event_id) => {
let _ = updates.send(RoomSendQueueUpdate::SentEvent {
@@ -594,6 +595,10 @@ impl RoomSendQueue {
}
},
Ok(None) => {
debug!("Request has been aborted while running, continuing.");
}
Err(err) => {
let is_recoverable = match err {
crate::Error::Http(ref http_err) => {
@@ -661,11 +666,14 @@ impl RoomSendQueue {
info!("exited sending task");
}
/// Handles a single request and returns the [`SentRequestKey`] on success.
/// Handles a single request and returns the [`SentRequestKey`] on success
/// (unless the request was cancelled, in which case it'll return
/// `None`).
async fn handle_request(
room: &Room,
request: QueuedRequest,
) -> Result<SentRequestKey, crate::Error> {
cancel_upload_rx: Option<oneshot::Receiver<()>>,
) -> Result<Option<SentRequestKey>, crate::Error> {
match request.kind {
QueuedRequestKind::Event { content } => {
let (event, event_type) = content.raw();
@@ -677,7 +685,7 @@ impl RoomSendQueue {
.await?;
trace!(txn_id = %request.transaction_id, event_id = %res.event_id, "event successfully sent");
Ok(SentRequestKey::Event(res.event_id))
Ok(Some(SentRequestKey::Event(res.event_id)))
}
QueuedRequestKind::MediaUpload {
@@ -688,52 +696,83 @@ impl RoomSendQueue {
} => {
trace!(%relates_to, "uploading media related to event");
let mime = Mime::from_str(&content_type).map_err(|_| {
crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType {
mime_type: content_type.clone(),
})
})?;
let fut = async move {
let mime = Mime::from_str(&content_type).map_err(|_| {
crate::Error::SendQueueWedgeError(QueueWedgeError::InvalidMimeType {
mime_type: content_type.clone(),
})
})?;
let data = room
.client()
.event_cache_store()
.lock()
.await?
.get_media_content(&cache_key)
.await?
.ok_or(crate::Error::SendQueueWedgeError(
QueueWedgeError::MissingMediaContent,
))?;
let data = room
.client()
.event_cache_store()
.lock()
.await?
.get_media_content(&cache_key)
.await?
.ok_or(crate::Error::SendQueueWedgeError(
QueueWedgeError::MissingMediaContent,
))?;
#[cfg(feature = "e2e-encryption")]
let media_source = if room.is_encrypted().await? {
trace!("upload will be encrypted (encrypted room)");
let mut cursor = std::io::Cursor::new(data);
let encrypted_file =
room.client().upload_encrypted_file(&mime, &mut cursor).await?;
MediaSource::Encrypted(Box::new(encrypted_file))
} else {
trace!("upload will be in clear text (room without encryption)");
let res = room.client().media().upload(&mime, data).await?;
MediaSource::Plain(res.content_uri)
#[cfg(feature = "e2e-encryption")]
let media_source = if room.is_encrypted().await? {
trace!("upload will be encrypted (encrypted room)");
let mut cursor = std::io::Cursor::new(data);
let encrypted_file = room
.client()
.upload_encrypted_file(&mime, &mut cursor)
.with_request_config(RequestConfig::short_retry())
.await?;
MediaSource::Encrypted(Box::new(encrypted_file))
} else {
trace!("upload will be in clear text (room without encryption)");
let request_config = RequestConfig::short_retry()
.timeout(Media::reasonable_upload_timeout(&data));
let res =
room.client().media().upload(&mime, data, Some(request_config)).await?;
MediaSource::Plain(res.content_uri)
};
#[cfg(not(feature = "e2e-encryption"))]
let media_source = {
let request_config = RequestConfig::short_retry()
.timeout(Media::reasonable_upload_timeout(&data));
let res =
room.client().media().upload(&mime, data, Some(request_config)).await?;
MediaSource::Plain(res.content_uri)
};
let uri = match &media_source {
MediaSource::Plain(uri) => uri,
MediaSource::Encrypted(encrypted_file) => &encrypted_file.url,
};
trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded");
Ok(SentRequestKey::Media(SentMediaInfo {
file: media_source,
thumbnail: thumbnail_source,
}))
};
#[cfg(not(feature = "e2e-encryption"))]
let media_source = {
let res = room.client().media().upload(&mime, data).await?;
MediaSource::Plain(res.content_uri)
let wait_for_cancel = async move {
if let Some(rx) = cancel_upload_rx {
rx.await
} else {
std::future::pending().await
}
};
let uri = match &media_source {
MediaSource::Plain(uri) => uri,
MediaSource::Encrypted(encrypted_file) => &encrypted_file.url,
};
trace!(%relates_to, mxc_uri = %uri, "media successfully uploaded");
tokio::select! {
biased;
Ok(SentRequestKey::Media(SentMediaInfo {
file: media_source,
thumbnail: thumbnail_source,
}))
_ = wait_for_cancel => {
Ok(None)
}
res = fut => {
res.map(Some)
}
}
}
}
}
@@ -753,26 +792,6 @@ impl RoomSendQueue {
self.inner.notifier.notify_one();
}
}
/// Unwedge a local echo identified by its transaction identifier and try to
/// resend it.
pub async fn unwedge(&self, transaction_id: &TransactionId) -> Result<(), RoomSendQueueError> {
self.inner
.queue
.mark_as_unwedged(transaction_id)
.await
.map_err(RoomSendQueueError::StorageError)?;
// Wake up the queue, in case the room was asleep before unwedging the request.
self.inner.notifier.notify_one();
let _ = self
.inner
.updates
.send(RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id.to_owned() });
Ok(())
}
}
impl From<&crate::Error> for QueueWedgeError {
@@ -833,32 +852,96 @@ struct RoomSendQueueInner {
_task: JoinHandle<()>,
}
/// Information about a request being sent right this moment.
struct BeingSentInfo {
/// Transaction id of the thing being sent.
transaction_id: OwnedTransactionId,
/// For an upload request, a trigger to cancel the upload before it
/// completes.
cancel_upload: Option<oneshot::Sender<()>>,
}
impl BeingSentInfo {
/// Aborts the upload, if a trigger is available.
///
/// Consumes the object because the sender is a oneshot and will be consumed
/// upon sending.
fn cancel_upload(self) -> bool {
if let Some(cancel_upload) = self.cancel_upload {
let _ = cancel_upload.send(());
true
} else {
false
}
}
}
/// A specialized lock that guards both against the state store and the
/// [`Self::being_sent`] data.
#[derive(Clone)]
struct QueueStorage {
struct StoreLock {
/// Reference to the client, to get access to the underlying store.
client: WeakClient,
/// To which room is this storage related.
room_id: OwnedRoomId,
/// All the queued requests that are being sent at the moment.
/// The one queued request that is being sent at the moment, along with
/// associated data that can be useful to act upon it.
///
/// It also serves as an internal lock on the storage backend.
being_sent: Arc<RwLock<BTreeSet<OwnedTransactionId>>>,
/// Also used as the lock to access the state store.
being_sent: Arc<Mutex<Option<BeingSentInfo>>>,
}
impl QueueStorage {
/// Create a new queue for queuing requests to be sent later.
fn new(client: WeakClient, room: OwnedRoomId) -> Self {
Self { room_id: room, being_sent: Default::default(), client }
impl StoreLock {
/// Gets a hold of the locked store and [`Self::being_sent`] pair.
async fn lock(&self) -> StoreLockGuard {
StoreLockGuard {
client: self.client.clone(),
being_sent: self.being_sent.clone().lock_owned().await,
}
}
}
/// Small helper to get a strong Client from the weak one.
/// A lock guard obtained through locking with [`StoreLock`].
/// `being_sent` data.
struct StoreLockGuard {
/// Reference to the client, to get access to the underlying store.
client: WeakClient,
/// The one queued request that is being sent at the moment, along with
/// associated data that can be useful to act upon it.
being_sent: OwnedMutexGuard<Option<BeingSentInfo>>,
}
impl StoreLockGuard {
/// Get a client from the locked state, useful to get a handle on a store.
fn client(&self) -> Result<Client, RoomSendQueueStorageError> {
self.client.get().ok_or(RoomSendQueueStorageError::ClientShuttingDown)
}
}
/// Push a new event to be sent in the queue.
#[derive(Clone)]
struct QueueStorage {
/// A lock to make sure the state store is only accessed once at a time, to
/// make some store operations atomic.
store: StoreLock,
/// To which room is this storage related.
room_id: OwnedRoomId,
}
impl QueueStorage {
/// Default priority for a queued request.
const LOW_PRIORITY: usize = 0;
/// High priority for a queued request that must be handled before others.
const HIGH_PRIORITY: usize = 10;
/// Create a new queue for queuing requests to be sent later.
fn new(client: WeakClient, room: OwnedRoomId) -> Self {
Self { room_id: room, store: StoreLock { client, being_sent: Default::default() } }
}
/// Push a new event to be sent in the queue, with a default priority of 0.
///
/// Returns the transaction id chosen to identify the request.
async fn push(
@@ -867,9 +950,17 @@ impl QueueStorage {
) -> Result<OwnedTransactionId, RoomSendQueueStorageError> {
let transaction_id = TransactionId::new();
self.client()?
self.store
.lock()
.await
.client()?
.store()
.save_send_queue_request(&self.room_id, transaction_id.clone(), request)
.save_send_queue_request(
&self.room_id,
transaction_id.clone(),
request,
Self::LOW_PRIORITY,
)
.await?;
Ok(transaction_id)
@@ -879,17 +970,36 @@ impl QueueStorage {
///
/// It is required to call [`Self::mark_as_sent`] after it's been
/// effectively sent.
async fn peek_next_to_send(&self) -> Result<Option<QueuedRequest>, RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let mut being_sent = self.being_sent.write().await;
async fn peek_next_to_send(
&self,
) -> Result<Option<(QueuedRequest, Option<oneshot::Receiver<()>>)>, RoomSendQueueStorageError>
{
let mut guard = self.store.lock().await;
let queued_requests =
self.client()?.store().load_send_queue_requests(&self.room_id).await?;
guard.client()?.store().load_send_queue_requests(&self.room_id).await?;
if let Some(request) = queued_requests.iter().find(|queued| !queued.is_wedged()) {
being_sent.insert(request.transaction_id.clone());
let (cancel_upload_tx, cancel_upload_rx) =
if matches!(request.kind, QueuedRequestKind::MediaUpload { .. }) {
let (tx, rx) = oneshot::channel();
(Some(tx), Some(rx))
} else {
Default::default()
};
Ok(Some(request.clone()))
let prev = guard.being_sent.replace(BeingSentInfo {
transaction_id: request.transaction_id.clone(),
cancel_upload: cancel_upload_tx,
});
if let Some(prev) = prev {
error!(
prev_txn = ?prev.transaction_id,
"a previous request was still active while picking a new one"
);
}
Ok(Some((request.clone(), cancel_upload_rx)))
} else {
Ok(None)
}
@@ -899,7 +1009,12 @@ impl QueueStorage {
/// with the given transaction id as not being sent anymore, so it can
/// be removed from the queue later.
async fn mark_as_not_being_sent(&self, transaction_id: &TransactionId) {
self.being_sent.write().await.remove(transaction_id);
let was_being_sent = self.store.lock().await.being_sent.take();
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
if prev_txn != Some(transaction_id) {
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after transient error)");
}
}
/// Marks a request popped with [`Self::peek_next_to_send`] and identified
@@ -911,10 +1026,15 @@ impl QueueStorage {
reason: QueueWedgeError,
) -> Result<(), RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let mut being_sent = self.being_sent.write().await;
being_sent.remove(transaction_id);
let mut guard = self.store.lock().await;
let was_being_sent = guard.being_sent.take();
Ok(self
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
if prev_txn != Some(transaction_id) {
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after permanent error)");
}
Ok(guard
.client()?
.store()
.update_send_queue_request_status(&self.room_id, transaction_id, Some(reason))
@@ -928,6 +1048,9 @@ impl QueueStorage {
transaction_id: &TransactionId,
) -> Result<(), RoomSendQueueStorageError> {
Ok(self
.store
.lock()
.await
.client()?
.store()
.update_send_queue_request_status(&self.room_id, transaction_id, None)
@@ -942,10 +1065,15 @@ impl QueueStorage {
parent_key: SentRequestKey,
) -> Result<(), RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let mut being_sent = self.being_sent.write().await;
being_sent.remove(transaction_id);
let mut guard = self.store.lock().await;
let was_being_sent = guard.being_sent.take();
let client = self.client()?;
let prev_txn = was_being_sent.as_ref().map(|info| info.transaction_id.as_ref());
if prev_txn != Some(transaction_id) {
error!(prev_txn = ?prev_txn, "previous active request didn't match that we expect (after successful send");
}
let client = guard.client()?;
let store = client.store();
// Update all dependent requests.
@@ -970,12 +1098,14 @@ impl QueueStorage {
&self,
transaction_id: &TransactionId,
) -> Result<bool, RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let being_sent = self.being_sent.read().await;
let guard = self.store.lock().await;
if being_sent.contains(transaction_id) {
if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref())
== Some(transaction_id)
{
// Save the intent to redact the event.
self.client()?
guard
.client()?
.store()
.save_dependent_queued_request(
&self.room_id,
@@ -988,8 +1118,11 @@ impl QueueStorage {
return Ok(true);
}
let removed =
self.client()?.store().remove_send_queue_request(&self.room_id, transaction_id).await?;
let removed = guard
.client()?
.store()
.remove_send_queue_request(&self.room_id, transaction_id)
.await?;
Ok(removed)
}
@@ -1005,12 +1138,14 @@ impl QueueStorage {
transaction_id: &TransactionId,
serializable: SerializableEventContent,
) -> Result<bool, RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let being_sent = self.being_sent.read().await;
let guard = self.store.lock().await;
if being_sent.contains(transaction_id) {
if guard.being_sent.as_ref().map(|info| info.transaction_id.as_ref())
== Some(transaction_id)
{
// Save the intent to edit the associated event.
self.client()?
guard
.client()?
.store()
.save_dependent_queued_request(
&self.room_id,
@@ -1023,7 +1158,7 @@ impl QueueStorage {
return Ok(true);
}
let edited = self
let edited = guard
.client()?
.store()
.update_send_queue_request(&self.room_id, transaction_id, serializable.into())
@@ -1044,12 +1179,8 @@ impl QueueStorage {
file_media_request: MediaRequestParameters,
thumbnail: Option<(FinishUploadThumbnailInfo, MediaRequestParameters, Mime)>,
) -> Result<(), RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
// TODO refactor to make the relationship between being_sent and the store more
// obvious.
let _guard = self.being_sent.read().await;
let client = self.client()?;
let guard = self.store.lock().await;
let client = guard.client()?;
let store = client.store();
let thumbnail_info =
@@ -1069,6 +1200,7 @@ impl QueueStorage {
thumbnail_source: None, // the thumbnail has no thumbnails :)
related_to: send_event_txn.clone(),
},
Self::LOW_PRIORITY,
)
.await?;
@@ -1099,6 +1231,7 @@ impl QueueStorage {
thumbnail_source: None,
related_to: send_event_txn.clone(),
},
Self::LOW_PRIORITY,
)
.await?;
@@ -1129,7 +1262,8 @@ impl QueueStorage {
transaction_id: &TransactionId,
key: String,
) -> Result<Option<ChildTransactionId>, RoomSendQueueStorageError> {
let client = self.client()?;
let guard = self.store.lock().await;
let client = guard.client()?;
let store = client.store();
let requests = store.load_send_queue_requests(&self.room_id).await?;
@@ -1159,7 +1293,8 @@ impl QueueStorage {
&self,
room: &RoomSendQueue,
) -> Result<Vec<LocalEcho>, RoomSendQueueStorageError> {
let client = self.client()?;
let guard = self.store.lock().await;
let client = guard.client()?;
let store = client.store();
let local_requests =
@@ -1172,7 +1307,7 @@ impl QueueStorage {
send_handle: SendHandle {
room: room.clone(),
transaction_id: queued.transaction_id,
is_upload: false,
media_handles: None,
},
send_error: queued.error,
},
@@ -1216,8 +1351,8 @@ impl QueueStorage {
DependentQueuedRequestKind::FinishUpload {
local_echo,
file_upload: _,
thumbnail_info: _,
file_upload,
thumbnail_info,
} => {
// Materialize as an event local echo.
Some(LocalEcho {
@@ -1225,11 +1360,13 @@ impl QueueStorage {
content: LocalEchoContent::Event {
serialized_event: SerializableEventContent::new(&local_echo.into())
.ok()?,
// TODO this should be a `SendAttachmentHandle`!
send_handle: SendHandle {
room: room.clone(),
transaction_id: dep.own_transaction_id.into(),
is_upload: true,
media_handles: Some(MediaHandles {
upload_thumbnail_txn: thumbnail_info.map(|info| info.txn),
upload_file_txn: file_upload,
}),
},
send_error: None,
},
@@ -1320,6 +1457,7 @@ impl QueueStorage {
&self.room_id,
dependent_request.own_transaction_id.into(),
serializable.into(),
Self::HIGH_PRIORITY,
)
.await
.map_err(RoomSendQueueStorageError::StateStoreError)?;
@@ -1406,6 +1544,7 @@ impl QueueStorage {
&self.room_id,
dependent_request.own_transaction_id.into(),
serializable.into(),
Self::HIGH_PRIORITY,
)
.await
.map_err(RoomSendQueueStorageError::StateStoreError)?;
@@ -1465,10 +1604,9 @@ impl QueueStorage {
&self,
new_updates: &mut Vec<RoomSendQueueUpdate>,
) -> Result<(), RoomSendQueueError> {
// Keep the lock until we're done touching the storage.
let _being_sent = self.being_sent.read().await;
let guard = self.store.lock().await;
let client = self.client()?;
let client = guard.client()?;
let store = client.store();
let dependent_requests = store
@@ -1539,10 +1677,10 @@ impl QueueStorage {
&self,
dependent_event_id: &ChildTransactionId,
) -> Result<bool, RoomSendQueueStorageError> {
// Keep the lock until we're done touching the storage.
let _being_sent = self.being_sent.read().await;
Ok(self
.store
.lock()
.await
.client()?
.store()
.remove_dependent_queued_request(&self.room_id, dependent_event_id)
@@ -1706,18 +1844,37 @@ pub enum RoomSendQueueStorageError {
OperationNotImplementedYet,
}
/// Extra transaction IDs useful during an upload.
#[derive(Clone, Debug)]
struct MediaHandles {
/// Transaction id used when uploading the thumbnail.
///
/// Optional because a media can be uploaded without a thumbnail.
upload_thumbnail_txn: Option<OwnedTransactionId>,
/// Transaction id used when uploading the media itself.
upload_file_txn: OwnedTransactionId,
}
/// A handle to manipulate an event that was scheduled to be sent to a room.
// TODO (bnjbvr): consider renaming `SendEventHandle`, unless we can reuse it for medias too.
#[derive(Clone, Debug)]
pub struct SendHandle {
/// Link to the send queue used to send this request.
room: RoomSendQueue,
/// Transaction id used for the sent request.
///
/// If this is a media upload, this is the "main" transaction id, i.e. the
/// one used to send the event, and that will be seen by observers.
transaction_id: OwnedTransactionId,
is_upload: bool,
/// Additional handles for a media upload.
media_handles: Option<MediaHandles>,
}
impl SendHandle {
fn nyi_for_uploads(&self) -> Result<(), RoomSendQueueStorageError> {
if self.is_upload {
if self.media_handles.is_some() {
Err(RoomSendQueueStorageError::OperationNotImplementedYet)
} else {
Ok(())
@@ -1731,9 +1888,25 @@ impl SendHandle {
#[instrument(skip(self), fields(room_id = %self.room.inner.room.room_id(), txn_id = %self.transaction_id))]
pub async fn abort(&self) -> Result<bool, RoomSendQueueStorageError> {
trace!("received an abort request");
self.nyi_for_uploads()?;
if self.room.inner.queue.cancel_event(&self.transaction_id).await? {
let queue = &self.room.inner.queue;
if let Some(handles) = &self.media_handles {
if queue.abort_upload(&self.transaction_id, handles).await? {
// Propagate a cancelled update.
let _ = self.room.inner.updates.send(RoomSendQueueUpdate::CancelledLocalEvent {
transaction_id: self.transaction_id.clone(),
});
return Ok(true);
}
// If it failed, it means the sending of the event is not a
// dependent request anymore. Fall back to the regular
// code path below, that handles aborting sending of an event.
}
if queue.cancel_event(&self.transaction_id).await? {
trace!("successful abort");
// Propagate a cancelled update too.
@@ -1797,6 +1970,43 @@ impl SendHandle {
.await
}
/// Unwedge a local echo identified by its transaction identifier and try to
/// resend it.
pub async fn unwedge(&self) -> Result<(), RoomSendQueueError> {
let room = &self.room.inner;
room.queue
.mark_as_unwedged(&self.transaction_id)
.await
.map_err(RoomSendQueueError::StorageError)?;
// If we have media handles, also try to unwedge them.
//
// It's fine to always do it to *all* the transaction IDs at once, because only
// one of the three requests will be active at the same time, i.e. only
// one entry will be updated in the store. The other two are either
// done, or dependent requests.
if let Some(handles) = &self.media_handles {
room.queue
.mark_as_unwedged(&handles.upload_file_txn)
.await
.map_err(RoomSendQueueError::StorageError)?;
if let Some(txn) = &handles.upload_thumbnail_txn {
room.queue.mark_as_unwedged(txn).await.map_err(RoomSendQueueError::StorageError)?;
}
}
// Wake up the queue, in case the room was asleep before unwedging the request.
room.notifier.notify_one();
let _ = room
.updates
.send(RoomSendQueueUpdate::RetryEvent { transaction_id: self.transaction_id.clone() });
Ok(())
}
/// Send a reaction to the event as soon as it's sent.
///
/// If returning `Ok(None)`; this means the reaction couldn't be sent
@@ -1872,7 +2082,7 @@ impl SendReactionHandle {
let handle = SendHandle {
room: self.room.clone(),
transaction_id: self.transaction_id.clone().into(),
is_upload: false,
media_handles: None,
};
handle.abort().await
@@ -1884,24 +2094,6 @@ impl SendReactionHandle {
}
}
/// A handle to execute actions while sending an attachment.
///
/// In the future, this may support cancellation, subscribing to progress, etc.
#[derive(Clone, Debug)]
pub struct SendAttachmentHandle {
/// Reference to the send queue for the room where this attachment was sent.
_room: RoomSendQueue,
/// Transaction id for the sending of the event itself.
_transaction_id: OwnedTransactionId,
/// Transaction id for the file upload.
_file_upload: OwnedTransactionId,
/// Transaction id for the thumbnail upload.
_thumbnail_transaction_id: Option<OwnedTransactionId>,
}
/// From a given source of [`DependentQueuedRequest`], return only the most
/// meaningful, i.e. the ones that wouldn't be overridden after applying the
/// others.
+177 -36
View File
@@ -33,20 +33,21 @@ use ruma::{
};
use tracing::{debug, error, instrument, trace, warn, Span};
use super::{QueueStorage, RoomSendQueue, RoomSendQueueError, SendAttachmentHandle};
use super::{QueueStorage, RoomSendQueue, RoomSendQueueError};
use crate::{
attachment::AttachmentConfig,
send_queue::{
LocalEcho, LocalEchoContent, RoomSendQueueStorageError, RoomSendQueueUpdate, SendHandle,
LocalEcho, LocalEchoContent, MediaHandles, RoomSendQueueStorageError, RoomSendQueueUpdate,
SendHandle,
},
Client, Room,
};
/// Create a [`MediaRequest`] for a file we want to store locally before
/// sending it.
/// Create an [`OwnedMxcUri`] for a file or thumbnail we want to store locally
/// before sending it.
///
/// This uses a MXC ID that is only locally valid.
fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
fn make_local_uri(txn_id: &TransactionId) -> OwnedMxcUri {
// This mustn't represent a potentially valid media server, otherwise it'd be
// possible for an attacker to return malicious content under some
// preconditions (e.g. the cache store has been cleared before the upload
@@ -54,10 +55,16 @@ fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParamete
// which is guaranteed to be on the local machine. As a result, the only attack
// possible would be coming from the user themselves, which we consider a
// non-threat.
OwnedMxcUri::from(format!("mxc://send-queue.localhost/{txn_id}"))
}
/// Create a [`MediaRequest`] for a file we want to store locally before
/// sending it.
///
/// This uses a MXC ID that is only locally valid.
fn make_local_file_media_request(txn_id: &TransactionId) -> MediaRequestParameters {
MediaRequestParameters {
source: MediaSource::Plain(OwnedMxcUri::from(format!(
"mxc://send-queue.localhost/{txn_id}"
))),
source: MediaSource::Plain(make_local_uri(txn_id)),
format: MediaFormat::File,
}
}
@@ -73,9 +80,7 @@ fn make_local_thumbnail_media_request(
) -> MediaRequestParameters {
// See comment in [`make_local_file_media_request`].
MediaRequestParameters {
source: MediaSource::Plain(OwnedMxcUri::from(format!(
"mxc://send-queue.localhost/{txn_id}"
))),
source: MediaSource::Plain(make_local_uri(txn_id)),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(width, height)),
}
}
@@ -141,7 +146,7 @@ impl RoomSendQueue {
content_type: Mime,
data: Vec<u8>,
mut config: AttachmentConfig,
) -> Result<SendAttachmentHandle, RoomSendQueueError> {
) -> Result<SendHandle, RoomSendQueueError> {
let Some(room) = self.inner.room.get() else {
return Err(RoomSendQueueError::RoomDisappeared);
};
@@ -249,27 +254,23 @@ impl RoomSendQueue {
self.inner.notifier.notify_one();
let send_handle = SendHandle {
room: self.clone(),
transaction_id: send_event_txn.clone().into(),
media_handles: Some(MediaHandles { upload_thumbnail_txn, upload_file_txn }),
};
let _ = self.inner.updates.send(RoomSendQueueUpdate::NewLocalEvent(LocalEcho {
transaction_id: send_event_txn.clone().into(),
content: LocalEchoContent::Event {
serialized_event: SerializableEventContent::new(&event_content.into())
.map_err(RoomSendQueueStorageError::JsonSerialization)?,
// TODO: this should be a `SendAttachmentHandle`!
send_handle: SendHandle {
room: self.clone(),
transaction_id: send_event_txn.clone().into(),
is_upload: true,
},
send_handle: send_handle.clone(),
send_error: None,
},
}));
Ok(SendAttachmentHandle {
_room: self.clone(),
_transaction_id: send_event_txn.into(),
_file_upload: upload_file_txn,
_thumbnail_transaction_id: upload_thumbnail_txn,
})
Ok(send_handle)
}
}
@@ -322,18 +323,27 @@ impl QueueStorage {
let from_req =
make_local_thumbnail_media_request(&info.txn, info.height, info.width);
trace!( from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store");
if info.height == uint!(0) || info.width == uint!(0) {
trace!(from = ?from_req.source, "removing thumbnail with unknown dimension from cache store");
// Reuse the same format for the cached thumbnail with the final MXC ID.
let new_format = from_req.format.clone();
cache_store
.remove_media_content(&from_req)
.await
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
} else {
trace!(from = ?from_req.source, to = ?new_source, "renaming thumbnail file key in cache store");
cache_store
.replace_media_key(
&from_req,
&MediaRequestParameters { source: new_source, format: new_format },
)
.await
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
// Reuse the same format for the cached thumbnail with the final MXC ID.
let new_format = from_req.format.clone();
cache_store
.replace_media_key(
&from_req,
&MediaRequestParameters { source: new_source, format: new_format },
)
.await
.map_err(RoomSendQueueStorageError::EventCacheStoreError)?;
}
}
}
@@ -353,7 +363,12 @@ impl QueueStorage {
client
.store()
.save_send_queue_request(&self.room_id, event_txn, new_content.into())
.save_send_queue_request(
&self.room_id,
event_txn,
new_content.into(),
Self::HIGH_PRIORITY,
)
.await
.map_err(RoomSendQueueStorageError::StateStoreError)?;
@@ -395,10 +410,136 @@ impl QueueStorage {
client
.store()
.save_send_queue_request(&self.room_id, next_upload_txn, request)
.save_send_queue_request(&self.room_id, next_upload_txn, request, Self::HIGH_PRIORITY)
.await
.map_err(RoomSendQueueStorageError::StateStoreError)?;
Ok(())
}
/// Try to abort an upload that would be ongoing.
///
/// Return true if any media (media itself or its thumbnail) was being
/// uploaded. In this case, the media event has also been removed from
/// the send queue. If it returns false, then the uploads already
/// happened, and the event sending *may* have started.
#[instrument(skip(self, handles))]
pub(super) async fn abort_upload(
&self,
event_txn: &TransactionId,
handles: &MediaHandles,
) -> Result<bool, RoomSendQueueStorageError> {
let mut guard = self.store.lock().await;
let client = guard.client()?;
// Keep the lock until we're done touching the storage.
debug!("trying to abort an upload");
let store = client.store();
let upload_file_as_dependent = ChildTransactionId::from(handles.upload_file_txn.clone());
let event_as_dependent = ChildTransactionId::from(event_txn.to_owned());
let mut removed_dependent_upload = false;
let mut removed_dependent_event = false;
if let Some(thumbnail_txn) = &handles.upload_thumbnail_txn {
if store.remove_send_queue_request(&self.room_id, thumbnail_txn).await? {
// The thumbnail upload existed as a request: either it was pending (something
// else was being sent), or it was actively being sent.
trace!("could remove thumbnail request, removing 2 dependent requests now");
// 1. Try to abort sending using the being_sent info, in case it was active.
if let Some(info) = guard.being_sent.as_ref() {
if info.transaction_id == *thumbnail_txn {
// SAFETY: we knew it was Some(), two lines above.
let info = guard.being_sent.take().unwrap();
if info.cancel_upload() {
trace!("aborted ongoing thumbnail upload");
}
}
}
// 2. Remove the dependent requests.
removed_dependent_upload = store
.remove_dependent_queued_request(&self.room_id, &upload_file_as_dependent)
.await?;
if !removed_dependent_upload {
warn!("unable to find the dependent file upload request");
}
removed_dependent_event = store
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
.await?;
if !removed_dependent_event {
warn!("unable to find the dependent media event upload request");
}
}
}
// If we're here:
// - either there was no thumbnail to upload,
// - or the thumbnail request has terminated already.
//
// So the next target is the upload request itself, in both cases.
if !removed_dependent_upload {
if store.remove_send_queue_request(&self.room_id, &handles.upload_file_txn).await? {
// The upload existed as a request: either it was pending (something else was
// being sent), or it was actively being sent.
trace!("could remove file upload request, removing 1 dependent request");
// 1. Try to abort sending using the being_sent info, in case it was active.
if let Some(info) = guard.being_sent.as_ref() {
if info.transaction_id == handles.upload_file_txn {
// SAFETY: we knew it was Some(), two lines above.
let info = guard.being_sent.take().unwrap();
if info.cancel_upload() {
trace!("aborted ongoing file upload");
}
}
}
// 2. Remove the dependent request.
if !store
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
.await?
{
warn!("unable to find the dependent media event upload request");
}
} else {
// The upload was not in the send queue, so it's completed.
//
// It means the event sending is either still queued as a dependent request, or
// it's graduated into a request.
if !removed_dependent_event
&& !store
.remove_dependent_queued_request(&self.room_id, &event_as_dependent)
.await?
{
// The media event has been promoted into a request, or the promoted request
// has been sent already: we couldn't abort, let the caller decide what to do.
debug!("uploads already happened => deferring to aborting an event sending");
return Ok(false);
}
}
}
// At this point, all the requests and dependent requests have been cleaned up.
// Perform the final step: empty the cache from the local items.
{
let event_cache = client.event_cache_store().lock().await?;
event_cache
.remove_media_content_for_uri(&make_local_uri(&handles.upload_file_txn))
.await?;
if let Some(txn) = &handles.upload_thumbnail_txn {
event_cache.remove_media_content_for_uri(&make_local_uri(txn)).await?;
}
}
debug!("successfully aborted!");
Ok(true)
}
}
+1 -1
View File
@@ -703,7 +703,7 @@ impl SlidingSync {
|| !self.inner.lists.read().await.is_empty()
}
#[instrument(skip_all, fields(pos))]
#[instrument(skip_all, fields(pos, conn_id = self.inner.id))]
async fn sync_once(&self) -> Result<UpdateSummary> {
let (request, request_config, position_guard) =
self.generate_sync_request(&mut LazyTransactionId::new()).await?;
+3
View File
@@ -14,6 +14,9 @@ use url::Url;
pub mod events;
#[cfg(not(target_arch = "wasm32"))]
pub mod mocks;
use crate::{
config::RequestConfig,
matrix_auth::{MatrixSession, MatrixSessionTokens},
File diff suppressed because it is too large Load Diff
+152
View File
@@ -21,9 +21,12 @@ use std::sync::{Arc, RwLock};
use futures_core::Stream;
#[cfg(feature = "e2e-encryption")]
use futures_util::StreamExt;
#[cfg(feature = "markdown")]
use ruma::events::room::message::FormattedBody;
use ruma::{
events::{AnyMessageLikeEventContent, AnyStateEventContent},
serde::Raw,
RoomAliasId,
};
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
#[cfg(feature = "e2e-encryption")]
@@ -190,8 +193,61 @@ impl IntoRawStateEventContent for &Box<RawJsonValue> {
}
}
const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:{}\\";
/// Verifies the passed `String` matches the expected room alias format:
///
/// This means it's lowercase, with no whitespace chars, has a single leading
/// `#` char and a single `:` separator between the local and domain parts, and
/// the local part only contains characters that can't be percent encoded.
pub fn is_room_alias_format_valid(alias: String) -> bool {
let alias_parts: Vec<&str> = alias.split(':').collect();
if alias_parts.len() != 2 {
return false;
}
let local_part = alias_parts[0];
let has_valid_format = local_part.chars().skip(1).all(|c| {
c.is_ascii()
&& !c.is_whitespace()
&& !c.is_control()
&& !INVALID_ROOM_ALIAS_NAME_CHARS.contains(c)
});
let is_lowercase = alias.to_lowercase() == alias;
// Checks both the local part and the domain part
has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok()
}
/// Given a pair of optional `body` and `formatted_body` parameters,
/// returns a formatted body.
///
/// Return the formatted body if available, or interpret the `body` parameter as
/// markdown, if provided.
#[cfg(feature = "markdown")]
pub fn formatted_body_from(
body: Option<&str>,
formatted_body: Option<FormattedBody>,
) -> Option<FormattedBody> {
if formatted_body.is_some() {
formatted_body
} else {
body.and_then(FormattedBody::markdown)
}
}
#[cfg(test)]
mod test {
#[cfg(feature = "markdown")]
use assert_matches2::{assert_let, assert_matches};
#[cfg(feature = "markdown")]
use ruma::events::room::message::FormattedBody;
#[cfg(feature = "markdown")]
use crate::utils::formatted_body_from;
use crate::utils::is_room_alias_format_valid;
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_channel_observable_get_set() {
@@ -202,4 +258,100 @@ mod test {
assert_eq!(observable.set(10), 1);
assert_eq!(observable.get(), 10);
}
#[test]
fn test_is_room_alias_format_valid_when_it_has_no_leading_hash_char_is_not_valid() {
assert!(!is_room_alias_format_valid("alias:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_it_has_several_colon_chars_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias:something:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_it_has_no_colon_chars_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias.domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_server_part_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias:".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_name_part_has_whitespace_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias with whitespace:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_name_part_has_control_char_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias\u{0009}:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() {
assert!(!is_room_alias_format_valid("#a#lias,{t\\est}:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_name_part_is_not_lowercase_is_not_valid() {
assert!(!is_room_alias_format_valid("#Alias:domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_server_part_is_not_lowercase_is_not_valid() {
assert!(!is_room_alias_format_valid("#alias:Domain.org".to_owned()))
}
#[test]
fn test_is_room_alias_format_valid_when_has_valid_format() {
assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned()))
}
#[test]
#[cfg(feature = "markdown")]
fn test_formatted_body_from_nothing_returns_none() {
assert_matches!(formatted_body_from(None, None), None);
}
#[test]
#[cfg(feature = "markdown")]
fn test_formatted_body_from_only_formatted_body_returns_the_formatted_body() {
let formatted_body = FormattedBody::html(r"<h1>Hello!</h1>");
assert_let!(
Some(result_formatted_body) = formatted_body_from(None, Some(formatted_body.clone()))
);
assert_eq!(formatted_body.body, result_formatted_body.body);
assert_eq!(result_formatted_body.format, result_formatted_body.format);
}
#[test]
#[cfg(feature = "markdown")]
fn test_formatted_body_from_markdown_body_returns_a_processed_formatted_body() {
let markdown_body = Some(r"# Parsed");
assert_let!(Some(result_formatted_body) = formatted_body_from(markdown_body, None));
let expected_formatted_body = FormattedBody::html("<h1>Parsed</h1>\n".to_owned());
assert_eq!(expected_formatted_body.body, result_formatted_body.body);
assert_eq!(expected_formatted_body.format, result_formatted_body.format);
}
#[test]
#[cfg(feature = "markdown")]
fn test_formatted_body_from_body_and_formatted_body_returns_the_formatted_body() {
let markdown_body = Some(r"# Markdown");
let formatted_body = FormattedBody::html(r"<h1>HTML</h1>");
assert_let!(
Some(result_formatted_body) =
formatted_body_from(markdown_body, Some(formatted_body.clone()))
);
assert_eq!(formatted_body.body, result_formatted_body.body);
assert_eq!(formatted_body.format, result_formatted_body.format);
}
}
-2
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)] // temporary
use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
use serde::Deserialize;
@@ -124,7 +124,6 @@ pub(super) enum ReadEventRequest {
event_type: StateEventType,
state_key: StateKeySelector,
},
#[allow(dead_code)]
ReadMessageLikeEvent {
#[serde(rename = "type")]
event_type: MessageLikeEventType,
@@ -44,7 +44,7 @@ pub(crate) enum IncomingMessage {
/// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event.
///
/// This means that the machine previously subscribed to some events
/// (`Action::Subscribe` request).
/// ([`crate::widget::Action::Subscribe`] request).
MatrixEventReceived(Raw<AnyTimelineEvent>),
}
+98 -59
View File
@@ -14,8 +14,6 @@
//! No I/O logic of the [`WidgetDriver`].
#![warn(unreachable_pub)]
use std::{fmt, iter, time::Duration};
use driver_req::UpdateDelayedEventRequest;
@@ -54,7 +52,6 @@ use super::{
filter::{MatrixEventContent, MatrixEventFilterInput},
Capabilities, StateKeySelector,
};
use crate::widget::EventFilter;
mod driver_req;
mod from_widget;
@@ -71,8 +68,11 @@ pub(crate) use self::{
incoming::{IncomingMessage, MatrixDriverResponse},
};
/// Action (a command) that client (driver) must perform.
#[derive(Clone, Debug)]
/// A command to perform in reaction to an [`IncomingMessage`].
///
/// There are also initial actions that may be performed at the creation of a
/// [`WidgetMachine`].
#[derive(Debug)]
pub(crate) enum Action {
/// Send a raw message to the widget.
SendToWidget(String),
@@ -94,23 +94,33 @@ pub(crate) enum Action {
/// Subscribe to the events in the *current* room, i.e. a room which this
/// widget is instantiated with. The client is aware of the room.
#[allow(dead_code)]
Subscribe,
/// Unsuscribe from the events in the *current* room. Symmetrical to
/// `Subscribe`.
#[allow(dead_code)]
Unsubscribe,
}
/// No I/O state machine.
///
/// Handles interactions with the widget as well as the `MatrixDriver`.
/// Handles interactions with the widget as well as the
/// [`crate::widget::MatrixDriver`].
pub(crate) struct WidgetMachine {
/// Unique identifier for the widget.
///
/// Allows distinguishing different widgets.
widget_id: String,
/// The room to which this widget machine is attached.
room_id: OwnedRoomId,
/// Outstanding requests sent to the widget (mapped by uuid).
pending_to_widget_requests: PendingRequests<ToWidgetRequestMeta>,
/// Outstanding requests sent to the matrix driver (mapped by uuid).
pending_matrix_driver_requests: PendingRequests<MatrixDriverRequestMeta>,
/// Current negotiation state for capabilities.
capabilities: CapabilitiesState,
}
@@ -122,12 +132,9 @@ impl WidgetMachine {
widget_id: String,
room_id: OwnedRoomId,
init_on_content_load: bool,
limits: Option<RequestLimits>,
) -> (Self, Vec<Action>) {
let limits = limits.unwrap_or_else(|| RequestLimits {
max_pending_requests: 15,
response_timeout: Duration::from_secs(10),
});
let limits =
RequestLimits { max_pending_requests: 15, response_timeout: Duration::from_secs(10) };
let mut machine = Self {
widget_id,
@@ -137,8 +144,10 @@ impl WidgetMachine {
capabilities: CapabilitiesState::Unset,
};
let actions = (!init_on_content_load).then(|| machine.negotiate_capabilities());
(machine, actions.unwrap_or_default())
let initial_actions =
if init_on_content_load { Vec::new() } else { machine.negotiate_capabilities() };
(machine, initial_actions)
}
/// Main entry point to drive the state machine.
@@ -149,9 +158,11 @@ impl WidgetMachine {
match event {
IncomingMessage::WidgetMessage(raw) => self.process_widget_message(&raw),
IncomingMessage::MatrixDriverResponse { request_id, response } => {
self.process_matrix_driver_response(request_id, response)
}
IncomingMessage::MatrixEventReceived(event) => {
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
error!("Received matrix event before capabilities negotiation");
@@ -172,10 +183,8 @@ impl WidgetMachine {
fn process_widget_message(&mut self, raw: &str) -> Vec<Action> {
let message = match serde_json::from_str::<IncomingWidgetMessage>(raw) {
Ok(msg) => msg,
Err(e) => {
// TODO: There is a special error handling required for the invalid
// messages. Refer to the `widget-api-poc` for implementation notes.
error!("Failed to parse incoming message: {e}");
Err(error) => {
error!("couldn't deserialize incoming widget message: {error}");
return Vec::new();
}
};
@@ -203,21 +212,22 @@ impl WidgetMachine {
) -> Vec<Action> {
let request = match raw_request.deserialize() {
Ok(r) => r,
Err(e) => return vec![self.send_from_widget_error_response(raw_request, e)],
Err(e) => return vec![Self::send_from_widget_error_response(raw_request, e)],
};
match request {
FromWidgetRequest::SupportedApiVersions {} => {
let response = SupportedApiVersionsResponse::new();
vec![self.send_from_widget_response(raw_request, response)]
vec![Self::send_from_widget_response(raw_request, response)]
}
FromWidgetRequest::ContentLoaded {} => {
let response = vec![self.send_from_widget_response(raw_request, JsonObject::new())];
self.capabilities
.is_unset()
.then(|| [&response, self.negotiate_capabilities().as_slice()].concat())
.unwrap_or(response)
let mut response =
vec![Self::send_from_widget_response(raw_request, JsonObject::new())];
if self.capabilities.is_unset() {
response.append(&mut self.negotiate_capabilities());
}
response
}
FromWidgetRequest::ReadEvent(req) => self
@@ -245,17 +255,20 @@ impl WidgetMachine {
action.map(|a| vec![a]).unwrap_or_default()
});
let response = self.send_from_widget_response(raw_request, OpenIdResponse::Pending);
let response =
Self::send_from_widget_response(raw_request, OpenIdResponse::Pending);
iter::once(response).chain(request_action).collect()
}
FromWidgetRequest::DelayedEventUpdate(req) => {
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
let text =
"Received send update delayed event request before capabilities were negotiated";
return vec![self.send_from_widget_error_response(raw_request, text)];
return vec![Self::send_from_widget_error_response(raw_request, text)];
};
if !capabilities.update_delayed_event {
return vec![self.send_from_widget_error_response(
return vec![Self::send_from_widget_error_response(
raw_request,
format!(
"Not allowed: missing the {} capability.",
@@ -263,19 +276,22 @@ impl WidgetMachine {
),
)];
}
let (request, request_action) =
self.send_matrix_driver_request(UpdateDelayedEventRequest {
action: req.action,
delay_id: req.delay_id,
});
request.then(|res, machine| {
vec![machine.send_from_widget_result_response(
request.then(|res, _machine| {
vec![Self::send_from_widget_result_response(
raw_request,
// This is mapped to another type because the update_delay_event::Response
// does not impl Serialize
res.map(Into::<UpdateDelayedEventResponse>::into),
)]
});
request_action.map(|a| vec![a]).unwrap_or_default()
}
}
@@ -288,20 +304,25 @@ impl WidgetMachine {
) -> Option<Action> {
let CapabilitiesState::Negotiated(capabilities) = &self.capabilities else {
let text = "Received read event request before capabilities were negotiated";
return Some(self.send_from_widget_error_response(raw_request, text));
return Some(Self::send_from_widget_error_response(raw_request, text));
};
match request {
ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => {
let filter_fn = |f: &EventFilter| f.matches_message_like_event_type(&event_type);
if !capabilities.read.iter().any(filter_fn) {
return Some(self.send_from_widget_error_response(raw_request, "Not allowed"));
if !capabilities.read.iter().any(|f| f.matches_message_like_event_type(&event_type))
{
return Some(Self::send_from_widget_error_response(
raw_request,
"Not allowed to read message like event",
));
}
const DEFAULT_EVENT_LIMIT: u32 = 50;
let limit = limit.unwrap_or(DEFAULT_EVENT_LIMIT);
let request = ReadMessageLikeEventRequest { event_type, limit };
let (request, action) = self.send_matrix_driver_request(request);
request.then(|result, machine| {
let response = result.and_then(|mut events| {
let CapabilitiesState::Negotiated(capabilities) = &machine.capabilities
@@ -313,10 +334,13 @@ impl WidgetMachine {
events.retain(|e| capabilities.raw_event_matches_read_filter(e));
Ok(ReadEventResponse { events })
});
vec![machine.send_from_widget_result_response(raw_request, response)]
vec![Self::send_from_widget_result_response(raw_request, response)]
});
action
}
ReadEventRequest::ReadStateEvent { event_type, state_key } => {
let allowed = match &state_key {
StateKeySelector::Any => capabilities
@@ -339,13 +363,16 @@ impl WidgetMachine {
if allowed {
let request = ReadStateEventRequest { event_type, state_key };
let (request, action) = self.send_matrix_driver_request(request);
request.then(|result, machine| {
request.then(|result, _machine| {
let response = result.map(|events| ReadEventResponse { events });
vec![machine.send_from_widget_result_response(raw_request, response)]
vec![Self::send_from_widget_result_response(raw_request, response)]
});
action
} else {
Some(self.send_from_widget_error_response(raw_request, "Not allowed"))
Some(Self::send_from_widget_error_response(
raw_request,
"Not allowed to read state event",
))
}
}
}
@@ -371,8 +398,9 @@ impl WidgetMachine {
Default::default()
}),
};
if !capabilities.send_delayed_event && request.delay.is_some() {
return Some(self.send_from_widget_error_response(
return Some(Self::send_from_widget_error_response(
raw_request,
format!(
"Not allowed: missing the {} capability.",
@@ -380,17 +408,23 @@ impl WidgetMachine {
),
));
}
if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) {
return Some(self.send_from_widget_error_response(raw_request, "Not allowed"));
return Some(Self::send_from_widget_error_response(
raw_request,
"Not allowed to send event",
));
}
let (request, action) = self.send_matrix_driver_request(request);
request.then(|mut result, machine| {
if let Ok(r) = result.as_mut() {
r.set_room_id(machine.room_id.clone());
}
vec![machine.send_from_widget_result_response(raw_request, result)]
vec![Self::send_from_widget_result_response(raw_request, result)]
});
action
}
@@ -445,38 +479,39 @@ impl WidgetMachine {
}
}
#[instrument(skip_all, fields(request_id))]
#[instrument(skip_all)]
fn send_from_widget_response(
&self,
raw_request: Raw<FromWidgetRequest>,
response_data: impl Serialize,
) -> Action {
let mut object = raw_request
.deserialize_as::<IndexMap<String, Box<RawJsonValue>>>()
.expect("Failed to converted FromWidgetRequest to object representation");
let response_data = serde_json::value::to_raw_value(&response_data)
.expect("Failed to serialize response data");
object.insert("response".to_owned(), response_data);
let serialized = serde_json::to_string(&object).expect("Failed to serialize response");
let f = || {
let mut object = raw_request.deserialize_as::<IndexMap<String, Box<RawJsonValue>>>()?;
let response_data = serde_json::value::to_raw_value(&response_data)?;
object.insert("response".to_owned(), response_data);
serde_json::to_string(&object)
};
// SAFETY: we expect the raw request to be a valid JSON map, to which we add a
// new field.
let serialized = f().expect("error when attaching response to incoming request");
Action::SendToWidget(serialized)
}
fn send_from_widget_error_response(
&self,
raw_request: Raw<FromWidgetRequest>,
error: impl fmt::Display,
) -> Action {
self.send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error))
Self::send_from_widget_response(raw_request, FromWidgetErrorResponse::new(error))
}
fn send_from_widget_result_response(
&self,
raw_request: Raw<FromWidgetRequest>,
result: Result<impl Serialize, impl fmt::Display>,
) -> Action {
match result {
Ok(res) => self.send_from_widget_response(raw_request, res),
Err(msg) => self.send_from_widget_error_response(raw_request, msg),
Ok(res) => Self::send_from_widget_response(raw_request, res),
Err(msg) => Self::send_from_widget_error_response(raw_request, msg),
}
}
@@ -487,7 +522,7 @@ impl WidgetMachine {
) -> (ToWidgetRequestHandle<'_, T::ResponseData>, Option<Action>) {
#[derive(Serialize)]
#[serde(tag = "api", rename = "toWidget", rename_all = "camelCase")]
struct ToWidgetRequestSerHelper<'a, T> {
struct ToWidgetRequestSerdeHelper<'a, T> {
widget_id: &'a str,
request_id: Uuid,
action: &'static str,
@@ -495,7 +530,7 @@ impl WidgetMachine {
}
let request_id = Uuid::new_v4();
let full_request = ToWidgetRequestSerHelper {
let full_request = ToWidgetRequestSerdeHelper {
widget_id: &self.widget_id,
request_id,
action: T::ACTION,
@@ -553,7 +588,7 @@ impl WidgetMachine {
let update = NotifyCapabilitiesChanged { approved, requested };
let (_request, action) = machine.send_to_widget_request(update);
(subscribe_required).then(|| Action::Subscribe).into_iter().chain(action).collect()
subscribe_required.then(|| Action::Subscribe).into_iter().chain(action).collect()
});
action.map(|a| vec![a]).unwrap_or_default()
@@ -590,9 +625,13 @@ impl MatrixDriverRequestMeta {
}
}
/// Current negotiation state for capabilities.
enum CapabilitiesState {
/// Capabilities have never been defined.
Unset,
/// We're currently negotiating capabilities.
Negotiating,
/// The capabilities have already been negotiated.
Negotiated(Capabilities),
}

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