Compare commits

...

744 Commits

Author SHA1 Message Date
Ivan Enderlin 1e938df90d Merge pull request #4921 from Hywan/release-0.11.0
chore: Release matrix-sdk version 0.11.0
2025-04-11 11:06:43 +02:00
Ivan Enderlin 5d55bb4955 chore: Release matrix-sdk version 0.11.0 2025-04-11 10:51:30 +02:00
Ivan Enderlin 8abd9fb303 doc(releasing): Fixing markdown.
Remove unbreakable spaces, and add indentations.
2025-04-11 10:42:37 +02:00
Ivan Enderlin d5ee644443 refactor(base): Reorganize the state_events processors.
This patch tries to clarify the processors in `state_events` by
putting them in two modules: `sync` and `stripped`, along with a bit
of renaming:

- `collect_sync` becomes `sync::collect`,
- `collect_sync_from_timeline` becomes `sync::collect_from_timeline`,
- `collect_stripped` becomes `stripped::collect`.

I believe this is an improvement.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 06022aa23a chore(base): Remove a _ in front of a variable.
This ptach removes a `_` in front of a variable: it is used, if and only
if the `e2e-encryption` feature is enabled. Make it stand out.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 8ea0d9542d chore(base): Rename process_if_candidate to process_if_relevant. 2025-04-11 10:32:08 +02:00
Ivan Enderlin 63938bf2c7 chore(base): Remove an unnecessary allow(clippy…). 2025-04-11 10:32:08 +02:00
Ivan Enderlin acff6c2e1d doc(base): Fix an intra-link in the documentation. 2025-04-11 10:32:08 +02:00
Ivan Enderlin 1013843071 refactor(base): Create the timeline::build response processor.
This patch does the following things:

1. moves the `BaseClient::handle_timeline` method as the
   `timeline::build` response processor.
2. moves the `BaseClient::update_push_room_context` method as a private
   function of the `timeline` response processor module,
3. moves the `BaseClient::get_push_room_context` method as a public
   function of the `timeline` response processor module.

The scope of the `timeline::build` processor is a bit too broad, but at
least the number of methods on `BaseClient` are greatly reduced.
2025-04-11 10:32:08 +02:00
Ivan Enderlin a0bfd3e21e chore(clippy): Make Clippy happy. 2025-04-11 10:32:08 +02:00
Ivan Enderlin 68352e8339 chore(base): Rename variables for the sake of clarity.
Don't know what `event`, `e`, `e`, `e` or `r` represent? Me neither.
This patch tries to fix that.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 6a5e24a64a refactor(base): Create the verification::process_if_candidate response processor.
This patch creates a new `verification::process_if_candidate` response
process. The idea is the check whether a `AnySyncTimelineEvent` suits to
be a verification event that must be processed.

This piece of code was repeated in three different places in the code.
Now it's unique.
2025-04-11 10:32:08 +02:00
Ivan Enderlin db6d1b4cd6 refactor(base): BaseClient::handle_timeline no longer handle state events.
This patch removes the `ignore_state_events` argument from
`BaseClient::handle_timeline`. This method no longer handles state
events. Consequently, the `user_ids` and `ambiguity_cache` arguments are
also removed.

This patch updates the flow to use the new
`state_events::collect_sync_from_timeline` processor, and to re-use the
`state_events::dispatch_and_get_new_users` processor.

The positive impact of this patch is that it re-uses existing code
(hurray). However, the downside is that state events are deserialized
twice for the moment.
2025-04-11 10:32:08 +02:00
Ivan Enderlin b0c1eda682 refactor(base): Create the state_events::dispatch_and_get_new_users response processor.
This patch extracts the `BaseClient::handle_state` method as a new
response processor named `state_events::dispatch_and_get_new_users`.
It appears that we can do something more elegant with the returned new
users. See the next patch.
2025-04-11 10:32:08 +02:00
Ivan Enderlin afecd6d508 chore(base): Simplify code by calling iter::zip instead of Iterator::zip.
This patch uses `iter::zip` to replace `Iterator::zip`. It does exactly
the same, it's just shorter.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 52b490f5b6 chore(base): Remove allow(unused_mut) for the context.
This is used everytime now :-).
2025-04-11 10:32:08 +02:00
Ivan Enderlin 097558ca1b chore(base): Rewrite code to avoid an allocation and calling twice the same function.
This patch rewrites `e2e::decrypt::sync_timeline_event` to avoid calling
a `verification` twice with the same arguments, and to avoid a string
allocation (the `to_string`).

Also, I wasn't comfortable with the `starts_with` which wasn't included
a trailing `.`. At least now we rely on strongly typed event type.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 2194a81d74 refactor(base): Create the e2ee::decrypt::sync_timeline_event response processor.
This patch extracts the `BaseClient::decrypt_sync_room_event` method
into the new `e2ee::decrypt::sync_timeline_event` response processor.
2025-04-11 10:32:08 +02:00
Ivan Enderlin 37146da27a chore(base): Rename from_sync_v3 to …_v2.
The version of something is 3 but the sync version is 2. Confusing.
2025-04-11 10:32:08 +02:00
Mauro Romito 9300f47b40 feat(bindings): join_rule in NotificationItem 2025-04-11 10:29:59 +02:00
Mauro Romito 52f0aafb1e feat(bindings): expose is_public in notifications 2025-04-11 10:29:59 +02:00
dependabot[bot] bfbbe89989 chore(deps): Bump crossbeam-channel from 0.5.13 to 0.5.15
Bumps [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) from 0.5.13 to 0.5.15.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.13...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 16:52:10 +02:00
Richard van der Hoff d25e236a75 Merge pull request #4917 from matrix-org/rav/tracing_improvements
Cleanups and extensions to tracing span logic
2025-04-10 11:47:53 +01:00
Damir Jelić ab90c1b945 refactor(sdk): Migrate away from the backoff crate to the backon crate
Since backon also has WASM support, this should mean that we can get rid
of the WASM specific HTTP client implementation.
2025-04-10 11:58:06 +02:00
Jorge Martín 005e506c9b refactor(ffi): expose Room::member_with_sender_info.
Reuse this for `RoomPreview` too, removing `RoomMembershipDetails`, which contained the same info.
2025-04-10 09:48:15 +02:00
Jorge Martín 3fe457db83 refactor(room): change Room::own_membership_details to member_with_sender_info.
The function will now return the previously added `RoomMemberWithSenderInfo` struct.
2025-04-10 09:48:15 +02:00
Jorge Martín 69ab855efa refactor(room): add RoomMemberWithSenderInfo struct.
This will hold the info for both the room member whose sender id was provided and the sender of the `m.room.member` event from which the `RoomMember` is built.
2025-04-10 09:48:15 +02:00
Richard van der Hoff 66c7ba60a9 sdk: reduce clobber attached to tracing spans for http requests
We don't need to list the `server_versions` every time we do an http
request. Further, `config` is listed under both `skip` and `fields`, which ends
up being a no-op. I don't think it's very useful (and is quite noisy), so
let's remove it.
2025-04-09 19:55:38 +01:00
Richard van der Hoff 3e320b8289 sdk: propagate tracing span for outgoing requests
In parallel to a sliding sync request, we also check for and send pending outgoing
requests from the crypto stack. Currently, these drop the tracing span, which
loses valuable data. We should propagate the span.

We also add an extra layer of instrumentation so that we can differentiate
between the results of the sliding sync itself, and the outgoing requests.
2025-04-09 19:55:38 +01:00
Richard van der Hoff 2922389037 UI: Allow attaching a parent tracing span for sync service
Currently, if you have two clients both syncing away inside the same process,
it's approximately impossible to see which logs belong to which client. It
would be much better if we could use a tracing span to distinguish the two
clients.

I expect this to be mostly useful in integration tests, but it might be useful
elsewhere too.
2025-04-09 19:54:01 +01:00
Damir Jelić 68e3cdebdd chore: Bump the blake3 version 2025-04-09 16:37:40 +02:00
Damir Jelić 511cf78d51 chore: Bump the rusqlite version 2025-04-09 16:37:40 +02:00
Damir Jelić f4eea708fa chore: Update the ByteSize crate version we're using 2025-04-09 16:37:40 +02:00
Richard van der Hoff 1d3107ebcb multiverse: reduce redraw rate
This reduces the framerate from ~62fps to 10fps.

This is a workaround for a problem in my terminal, so apologies for inflicting
it on everyone else, but here we are, and 10fps seems like it should be enough
for anyone.

The problem in question is specifically when I try to select some text by
dragging the mouse (eg, to copy a generated recovery key). If I start a drag,
but a redraw happens before the mouse has moved [a certain distance?], then the
drag doesn't work, and nothing gets selected. By reducing the framerate, I have
a much better chance of successfully starting a drag.
2025-04-09 16:10:31 +02:00
Richard van der Hoff 073b4bae03 multiverse: allow recovery keys with h or l in them
Currently, `h` and `l` are intercepted by the parent view to change tab,
meaning it's impossible to enter a recovery key which contains those
characters.

The fix here is very blunt: it just disables `h` and `l` for tab-changing. I
considered making it dependent on which tab is open, or what's going on in the
'Encryption' tab, but given you need to know about the alternatives (tab/cursor
keys) to switch away from the Encryption tab, I don't think that makes sense.
2025-04-09 14:53:36 +02:00
Richard van der Hoff 35e13b8730 Merge pull request #4903 from matrix-org/rav/history_sharing/upload_bundle_prep
crypto: prep work for sharing room key history bundles
2025-04-09 10:40:42 +01:00
Damir Jelić f266fb9c38 chore(ffi): Update the changelog so it conforms to our cargo-release setup 2025-04-09 11:26:59 +02:00
Damir Jelić 2dfd334ade chore: Include the matrix-sdk-ffi crate in the release process 2025-04-09 11:26:59 +02:00
Damir Jelić 89d0cc5a76 chore: Add a missing PR link in one of the changelogs 2025-04-09 09:16:53 +02:00
Stefan Ceriu 628440632d chore(ffi): move reactions from EventTimelineItem to MsgLikeContent to keep in line with the UI crate types 2025-04-08 17:47:17 +03:00
Stefan Ceriu 16a3d9d78b chore(ffi): introduce MsgLike Content and Kind and move mappings 2025-04-08 17:47:17 +03:00
Stefan Ceriu d89a7d6c18 chore(ffi): move MsgLike related types to their own file 2025-04-08 17:47:17 +03:00
Stefan Ceriu 87c70789fe chore(ffi): move replies to their own file 2025-04-08 17:47:17 +03:00
Damir Jelić 9cde7f46bc docs: Use the github background color for the logo on dark themes 2025-04-08 16:13:59 +02:00
Damir Jelić 3e2b95cdb9 doc: Update the main readme
Now with a logo.
2025-04-08 14:26:20 +02:00
Richard van der Hoff dc3bd69af1 crypto: rename MaybeEncryptedRoomKey::Withheld to MissingSession
This is only used to indicate missing sessions, and the `code` is no longer
used, so let's rename the variant and remove the redundant field.
2025-04-08 12:47:08 +01:00
Richard van der Hoff da14cef6c1 crypto: Factor out a EncryptForDevicesResultBuilder
We want to re-use all this logic, so putting it in a separate type will
help. Plus I think it's cleaner.
2025-04-08 12:47:08 +01:00
Ivan Enderlin a1ac363383 refactor(base): Create account_data::for_room response processor.
This patch moves the `handle_room_account_data` method as the new
`account_data::for_room` response processor.

Instead of taking the `BaseClient` to fetch the room in `on_room_info`,
it now takes a `BaseStateStore`, which is safer and more straight to
the point.
2025-04-08 13:20:31 +02:00
Ivan Enderlin 9e5ef57a5f refactor(base): AccountDataProcessor becomes a response processor.
This patch changes `AccountDataProcessor` to
`processors::account_data::global`. The next patch will introduce a
per-room account data processor, hence the renaming to `Global` to make
the difference between the twos.
2025-04-08 13:20:31 +02:00
Ivan Enderlin da86552648 refactor(base): Remove a pub(crate).
This patch removes a `pub(crate)`. This is a private method only.
2025-04-08 13:20:31 +02:00
Ivan Enderlin c0f4e90965 refactor(base): Rename a couple of variables.
This patch renames a couple of variables because it's important
to understand that those stripped state events are coming from the
`invite_state` value of a sliding sync response. This is a pretty
important detail.
2025-04-08 13:20:31 +02:00
Ivan Enderlin cf393bf8e1 refactor(base): verification must be behind e2e-encryption. 2025-04-08 13:20:31 +02:00
Ivan Enderlin 2ee0e175fa refactor(base): Create the state_events::collect_* response processors.
This patch creates the `state_events::collect_sync`
and `collect_stripped` response processors. It
removes the `BaseClient::deserialize_state_events` and
`deserialize_stripped_state_events` methods. It also removes a couple of
`Vec` allocations and a couple of clones.
2025-04-08 13:20:31 +02:00
Ivan Enderlin 5c011a8400 refactor(base): Create the changes::save_and_apply response processor.
This patch creates the `changes::save_and_apply` response
processor. It consists of moving `BaseClient::apply_changes` plus
the code that is always around. This patch helps to remove the
`BaseClient::load_previous_ignored_user_list` method.
2025-04-08 13:20:31 +02:00
Ivan Enderlin b6b0c556b9 doc(crypto): Fix typos. 2025-04-08 13:20:31 +02:00
Ivan Enderlin 0c5f0b8d26 chore(base): Remove the with_e2ee internal module.
Just because it's clearer.
2025-04-08 13:20:31 +02:00
Ivan Enderlin e7e15ca280 refactor(base): Create the tracked_users response processor.
This patch creates the new `tracked_users::update_if_necessary` response
processor, and uses it in two places where the code was duplicated.
2025-04-08 13:20:31 +02:00
Ivan Enderlin 222cffd502 chore(base): Move e2ee.rs to e2ee/to_device.rs. 2025-04-08 13:20:31 +02:00
Ivan Enderlin cbef772eaa refactor(base): Move handle_room_member_event_for_profiles as a processor.
This patch moves the `handle_room_member_event_for_profiles`
function inside the collection of response processors under the name
`profiles::upsert_or_delete`.
2025-04-08 13:20:31 +02:00
Ivan Enderlin e7e9c7bcf2 refactor(base): Use response_processors::Context everywhere.
This patch updates codes in `BaseClient` to use `Context` as much as
possible.
2025-04-08 13:20:31 +02:00
Richard van der Hoff f1ea3e64d0 crypto: new result type for encrypt_session_for
Reduce the size of the tuple that this thing returns by defining a new result
type.

I haven't put the `share_infos` in the struct, because I'm going to reuse the
same struct for another method where `share_infos` aren't needed.
2025-04-08 12:18:21 +01:00
Richard van der Hoff 257deb4b94 crypto: factor out collect_recipients_for_share_strategy
For cases where we don't have a whole `EncryptionSettings`, we need
a finer-grained method.
2025-04-08 12:18:21 +01:00
Jorge Martín 3ece8e62b5 refactor(ffi): export NotificationItem::thread_id in the FFI layer 2025-04-08 11:53:39 +02:00
Jorge Martin Espinosa f7f07e7389 fix(ui): extract NotificationEvent::thread_id to its own function (#4899) 2025-04-08 08:30:14 +00:00
dependabot[bot] 8d0928ff7c chore(deps): Bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 to 6f67ee9ac810f0192ea7b3d2086406f97847bcf9.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99...6f67ee9ac810f0192ea7b3d2086406f97847bcf9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 09:13:50 +01:00
maan2003 39212db9ce fix(wasm): don't kill task on drop
Signed-off-by: Manmeet Singh <manmeetmann2003@gmail.com>
2025-04-08 09:10:24 +02:00
Johannes Marbach 056d4a79d0 refactor(send_queue): vectorize media handles on SendHandle (#4898)
This was broken out of
https://github.com/matrix-org/matrix-rust-sdk/pull/4838 and is a
preliminary step towards implementing
[MSC4274](https://github.com/matrix-org/matrix-spec-proposals/pull/4274).
The `media_handles` field on `SendHandle` is turned into a vector so
that it can hold handles for several media when upload a gallery later.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-04-08 08:53:08 +02:00
Jorge Martin Espinosa 5753ca3a64 misc(ffi): add thread_id to NotificationItem (#4895)
This is needed to identify the event as being in a thread, and to reply
to it inside the thread instead of in the room's timeline.

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

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

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:
2025-04-07 17:53:44 +02:00
Damir Jelić f3291d15c5 feat(multiverse): Move the details view to be beside the timeline
This allows us to view the debug screen at the same time as the
timeline. The details view can be to the right of the timeline or bellow
the timeline and we can switch using ALT-t.
2025-04-07 14:17:10 +02:00
Damir Jelić 413070aecb feat(multiverse): Shrink the room list to 25% of the screen 2025-04-07 14:17:10 +02:00
Damir Jelić 288251b1b5 feat(multiverse): Keep the timeline scrolled to the bottom 2025-04-07 14:17:10 +02:00
Damir Jelić 6c3e3bb519 feat(multiverse): Tweak the keybindings for the details view a bit 2025-04-07 14:17:10 +02:00
Damir Jelić 2e8b396c09 feat(multiverse): Add global keybindings to go directly into a details view 2025-04-07 14:17:10 +02:00
Damir Jelić ed048af903 chore(multiverse): Fix some clippy warnings 2025-04-07 14:17:10 +02:00
Damir Jelić 28e475b1fc chore(multiverse): Get rid of an unused dependency 2025-04-07 14:17:10 +02:00
Damir Jelić 5a13bd5e76 feat(multiverse): Allow to backpaginate while looking at the room details 2025-04-07 14:17:10 +02:00
Damir Jelić 6966302467 fix(multiverse): Properly document the shortcuts on the help screen 2025-04-07 14:17:10 +02:00
Damir Jelić fd6ce02d70 feat(multiverse): Select the first item in the developer settings 2025-04-07 14:17:10 +02:00
Damir Jelić 2debfd4c4d fix(multiverse): The encryption settings don't need a separate block anymore 2025-04-07 14:17:10 +02:00
Damir Jelić 19c40fd2da fix(multiverse): Better rendering for the OAuth approval URL when resetting recovery 2025-04-07 14:17:10 +02:00
Damir Jelić d5d0368ba8 feat(multiverse): Add support to reset your identity 2025-04-07 14:17:10 +02:00
Damir Jelić 899eb04f05 feat(multiverse): Use F8 to open the details view instead of CTRL-D
CTRL-D might become important once we add scrolling to the timeline
2025-04-07 14:17:10 +02:00
Damir Jelić 187280d573 feat(multiverse): Show an exit screen instead of printing things to stdout 2025-04-07 14:17:10 +02:00
Damir Jelić 036d14e9e3 feat(multiverse): Settings view 2025-04-07 14:17:10 +02:00
Damir Jelić 226229d63b refactor(multiverse): Move the widgets of the details mode into the details module 2025-04-07 14:17:10 +02:00
Damir Jelić 0cf018cc1b feat(multiverse): Allow the help screen to be closed with ESC as well 2025-04-07 14:17:10 +02:00
Damir Jelić c3d7a760f7 feat(multiverse): Add an input line so we can send messages 2025-04-07 14:17:10 +02:00
Damir Jelić d51cf1e76e refactor(multiverse): Change the keybinding for the reaction sending feature 2025-04-07 14:17:10 +02:00
Damir Jelić 71b6b213c4 refactor(multiverse): Move the details mode into a tab-based popout 2025-04-07 14:17:10 +02:00
Damir Jelić ec23638567 refactor(multiverse): Move some global shortcuts behind modifiers 2025-04-07 14:17:10 +02:00
Damir Jelić 688a56a077 feat(multiverse): Always render the timeline, despite showing some details 2025-04-07 14:17:10 +02:00
Damir Jelić 8413e856fe refactor(multiverse): Move the timeline rendering into a separate widget 2025-04-07 14:17:10 +02:00
Damir Jelić 3f16e77686 refactor(multiverse): Don't unwrap when subscribing to the event cache 2025-04-07 14:17:10 +02:00
Damir Jelić a4779299f6 refactor(multiverse): Move the details views under the room view module 2025-04-07 14:17:10 +02:00
Damir Jelić 50934f5bc9 refactor(multiverse): Move the room view into a separate widget 2025-04-07 14:17:10 +02:00
Damir Jelić fad63b9a64 feat(multiverse): Recovery support 2025-04-07 14:17:10 +02:00
Damir Jelić d999cf9180 refactor(multiverse): Convert the status widget into a stateful widget
The status widget depends on the main app state, instead of cloning the
app state let's just use a StatefulWidget instead.
2025-04-07 14:17:10 +02:00
Damir Jelić 3cd60c5b01 doc(multiverse): Document the status widget a bit better 2025-04-07 14:17:10 +02:00
Damir Jelić 2e4587f824 refactor(multiverse): Move all the widgets into a separate module 2025-04-07 14:17:10 +02:00
Damir Jelić 3996f7c0d6 feat(multiverse): Add a help screen 2025-04-07 14:17:10 +02:00
Damir Jelić 774bff00a0 refactor(multiverse): Events view 2025-04-07 14:17:10 +02:00
Damir Jelić d6196e6c5c refactor(multiverse): Move the linked chunk view into a popout widget 2025-04-07 14:17:10 +02:00
Damir Jelić b49fd2b473 feat(multiverse): Only open the read receipt screen if a room is selected 2025-04-07 14:17:10 +02:00
Damir Jelić f31119a013 refactor(multiverse): Turn the read receipt rendering logic into a widget 2025-04-07 14:17:10 +02:00
Damir Jelić fb4caf40aa refactor(multiverse): Make the Status struct a true widget 2025-04-07 14:17:10 +02:00
Damir Jelić 238fbdbe82 refactor(multiverse): Use the Mutex from the common crate to avoid calling unwrap 2025-04-07 14:17:10 +02:00
Damir Jelić a1d42cdf06 refactor(multiverse): Shorten some overly long lines 2025-04-07 14:17:10 +02:00
Damir Jelić 1c134a78de refactor(multiverse): Use a mpsc channel to propagate status messages to the status widget 2025-04-07 14:17:10 +02:00
Damir Jelić b5d1c14e29 refactor(multiverse): Rename set_status_message to set_message 2025-04-07 14:17:10 +02:00
Damir Jelić a28ec70816 refactor(multiverse): Move the set_status_message under the Status widget 2025-04-07 14:17:10 +02:00
Damir Jelić e1b393c39f refactor(multiverse): Move the status message into a separate module 2025-04-07 14:17:10 +02:00
Damir Jelić a345c47a31 refactor(multiverse): Merge the two App impl blocks 2025-04-07 14:17:10 +02:00
Damir Jelić 64feee41ef refactor(multiverse): Move the room subscription logic into the RoomList 2025-04-07 14:17:10 +02:00
Damir Jelić 9f947e019f refactor(multiverse): Split the get_selected_room_id method into two methods 2025-04-07 14:17:10 +02:00
Damir Jelić be74cb4a16 refactor(multiverse): Move the get_selected_room_id under the RoomList 2025-04-07 14:17:10 +02:00
Damir Jelić 409f08dc2b refactor(multiverse): Simplify the constructor 2025-04-07 14:17:10 +02:00
Damir Jelić a94a03766b refactor(multiverse): Move the closure listening for new data into a separate method 2025-04-07 14:17:10 +02:00
Damir Jelić 988fd18b78 refactor(multiverse): Move the RoomList widget into a separate module 2025-04-07 14:17:10 +02:00
Damir Jelić 68b848602a refactor(multiverse): Turn the RoomList struct into a widget 2025-04-07 14:17:10 +02:00
Damir Jelić f7d6fe2dbf refactor(multiverse): Rename StatefulList to RoomList
While we're at it, move the impl block closer to the struct.
2025-04-07 14:17:10 +02:00
Damir Jelić c2a9523cbb refactor(multiverse): Remove the generics from the StatefulList struct 2025-04-07 14:17:10 +02:00
Ivan Enderlin ee879354b7 doc(sqlite,ffi): Add #4894 in the CHANGELOG.mds. 2025-04-07 14:05:40 +02:00
Ivan Enderlin c3fd571623 feat(ffi): Add ClientBuilder::system_is_memory_constrained().
This patch adds `ClientBuilder::system_is_memory_constrained`
so that the client can be built with that in mind.
Behind the scene, for the moment, it only calls
`SqliteStoreConfig::with_low_memory_config` instead of
`SqliteStoreConfig::new`, but this flag can be used for other use cases.
2025-04-07 14:05:40 +02:00
Ivan Enderlin 52ec6a4539 feat(sqlite): Add SqliteStoreConfig::with_low_memory_config.
This patch adds a new constructor for `SqliteStoreConfig`, which sets
some defaults tailored for low memory usage.

This patch adds tests asserting the defaults for `new` and
`with_low_memory_config`.
2025-04-07 14:05:40 +02:00
Ivan Enderlin a57322466c refactor(base): Simplify the e2ee response processor.
This patch explores a way to simplify the call sites of the `e2ee`
response processor by creating one response processor for `/v3/sync` and
one for MSC4186. The idea is to:

- simplify the call site by having less code,
- isolating the “dispatch” of a the response values into the `e2ee`
  response processor,
- make it easier to test this response processor based on a `Response`
  structs directly.
2025-04-07 13:52:29 +02:00
Ivan Enderlin 90ce6e85ad refactor(base): Centralise processors that require e2e-encryption. 2025-04-07 13:52:29 +02:00
Ivan Enderlin e94fd64276 refactor(base): BaseClient uses response processors and remove duplicated code.
This patch updates
`BaseClient::receive_sync_repsonse_with_requested_required_states` to
use the `response_processors`. This patch also removes duplicated code
with the processors.
2025-04-07 13:52:29 +02:00
Ivan Enderlin 0c7cf58d4d refactor(base): BaseClient::process_sliding_sync_e2ee uses response processors. 2025-04-07 13:52:29 +02:00
Ivan Enderlin e3b2e0fa3e feat(base): Add the Verification request processor. 2025-04-07 13:52:29 +02:00
Ivan Enderlin 4619221429 feat(base): Add the DecryptLatestEvents response processor. 2025-04-07 13:52:29 +02:00
Ivan Enderlin 9b316ed405 feat(base): Add the E2EE response processor. 2025-04-07 13:52:29 +02:00
Ivan Enderlin 0a633ca75c feat(base): Add the Context struct.
This patch introduces a new `Context` type that holds the state changes
and the room info notable updates. Processors will exchange this
context. That's the only data that is mutable and exchangeable between
processors.
2025-04-07 13:52:29 +02:00
Ivan Enderlin 2e57733f05 refactor(base): Move response_processors.rs into response_processors/account_data.rs.
This patch creates the `response_processors` module and
moves the existing `response_processors.rs` file into
`response_processors/account_data.rs`.
2025-04-07 13:52:29 +02:00
Kévin Commaille b94be8d509 Upgrade tokio
To get rid of advisory

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-07 12:04:06 +02:00
Kévin Commaille 24e6d780fc Upgrade Ruma to version 0.12.2
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-07 12:04:06 +02:00
Ivan Enderlin d9157e5b83 refactor(ffi): Call methods on Room instead of redoing the same work.
This patch updates `Room::report_content` and `Room::report_room` to
call the same methods on `matrix_sdk` instead of re-implementing them.
2025-04-07 11:54:25 +02:00
dependabot[bot] b4a8089b40 chore(deps): Bump openssl from 0.10.70 to 0.10.72 2025-04-04 23:28:09 +02:00
Kévin Commaille a736dc9f96 doc(sdk): Cleanup changelog entries for Oidc/OAuth API changes
There were a lot of changes, and it was hard to follow, especially with
methods and types that changed and were then removed.

This groups everything under a single entry, with a short summary of
the changes, followed by a list of per-PR changes.

The detailed list is reorganized to put the biggest changes first, like
the renaming, and a few entries were cleaned up or removed, because
they mention a method or type that is removed in another entry.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 15:00:54 +02:00
Kévin Commaille dd094ea38e doc(oauth): Update docs
Make sure they are up-to-date.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 15:00:54 +02:00
Kévin Commaille b2cd81a992 test(sdk): Fix compilation error
Introduced by merging #4886 and #4887 around the same time.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 14:45:06 +02:00
Ivan Enderlin d5ceb5f99a refactor(sdk): Reduce the size of Error::CrossProcessLockStore.
This patch boxes the error in `Error::CrossProcessLockStore` to reduce
the size of this variant (from 24 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 956386a3ed refactor(sdk): Reduce the size of Error::SendQueueWedgeError.
This patch boxes the error in `Error::SendQueueWedgeError` to reduce the
size of this variant (from 32 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin b2a4032432 refactor(sdk): Reduce the size of Error::EventCache.
This patch boxes the error in `Error::EventCache` to reduce the size of
this variant (from 32 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 38378f7bae refactor(sdk): Reduce the size of Error::OAuth.
This patch boxes the error in `Error::OAuth` to reduce the size of this
variant (from 160 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin e12264bcb6 refactor(sdk): Reduce the size of Error::WrongRoomState.
This patch boxes the error in `Error::WrongRoomState` to reduce the
size of this variant (from 24 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 385df955c3 refactor(sdk): Reduce the size of Error::SlidingSync.
This patch boxes the error in `Error::SlidingSync` to reduce the size of
this variant (from 72 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 47a1db9e16 refactor(sdk): Reduce the size of Error::QrCodeScanError.
This patch boxes the error in `Error::QrCodeScanError` to reduce the
size of this variant (from 72 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin ed82f07d7d refactor(sdk): Reduce the size of Error::EventCacheStore.
This patch boxes the error in `Error::EventCacheStore` to reduce the size of
this variant (from 32 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 4ddd1468c2 refactor(sdk): Reduce the size of Error::StateStore.
This patch boxes the error in `Error::StateStore` to reduce the size of
this variant (from 40 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin b274d36e11 refactor(sdk): Reduce the size of Error::MegolmError.
This patch boxes the error in `Error::MegolmError` to reduce the size of this
variant (from 72 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 7ae82f3afb refactor(sdk): Reduce the size of Error::OlmError.
This patch boxes the error in `Error::OlmError` to reduce the size of this
variant (from 80 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 111306b411 refactor(sdk): Reduce the size of Error::CryptoStoreError.
This patch boxes the error in `Error::CryptoStoreError` to reduce the
size of this variant (from 72 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin c8fb8ad9ca refactor(sdk): Reduce the size of Error::Http.
This patch boxes the error in `Error::Http` to reduce the size of this
variant (from 160 bytes to 16 bytes).
2025-04-04 13:25:55 +02:00
Ivan Enderlin 2f7525c3c8 chore(base): Reduce the size of DependentQueuedRequestKind.
This patch boxes `local_echo` in
`DependentQueuedRequestKind::FinishUpload`, this variant' size is 512
bytes, compared to the second largest variant which is 104 bytes. To
reduce the global size of this enum, `local_echo` is now a `Box<_>`.
2025-04-04 13:25:55 +02:00
Ivan Enderlin 511fc96835 chore(base): Replace iter().any() by contains().
This is usually faster to use `contains()` than `iter().any()` on
a slice.

See https://rust-lang.github.io/rust-clippy/master/index.html#manual_contains.
2025-04-04 13:25:55 +02:00
Ivan Enderlin cb539bc72b chore(base): Reduce the size of AnySyncOrStrippedState.
This patch reduces the size of `anySyncOrStrippedState`. The `Sync`
variant is 528 bytes, the `Stripped` variant is 232. First off, there is
a non-negligible difference in size between the two, but still, we can
reduce the size of the enum by boxing all values.
2025-04-04 13:25:55 +02:00
Kévin Commaille 2b450a0a6a chore: Add changelog for Client::logout()
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 13:14:55 +02:00
Kévin Commaille fc93690d1f test(sdk): Add test for Client::logout()
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 13:14:55 +02:00
Kévin Commaille 43431b88da feat(sdk): Add Client::logout() to log out regardless of the auth API
It simplifies code for users, and avoids to have to match on
`AuthApi`, which is a non-exhaustive enum.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-04 13:14:55 +02:00
Ivan Enderlin 13ca27d66a chore(cargo): Update eyeball and imbl.
This patch updates `eyeball`, `eyeball-im` and `eyeball-im-util` from
`main` branch to latest releases.

This patch also updates `imbl` to 5.0.
2025-04-04 10:09:12 +02:00
Stefan Ceriu 9f7179263a fix(ffi): correctly populate all audio content fields when converting from FFI types to Ruma
- fixes forwarding audio and voice messages that would previously show up as files because of missing fields
2025-04-03 15:50:17 +03:00
Kévin Commaille c8da9cb462 refactor(oauth): Remove the issuer from OAuthAuthData
It is actually unused, and now that we only need homeserver URLs for
static registrations, users don't need to access it easily.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-03 12:52:17 +03:00
Kévin Commaille 678938951e chore: Update changelog for OAuthRegistrationStore removal
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-03 12:52:17 +03:00
Kévin Commaille 8883e081af refactor(oauth): Remove OAuthRegistrationStore
MSC2966 was updated, clients should re-register for every log in, so we
don't need to store the client IDs between logins.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-04-03 12:52:17 +03:00
Johannes Marbach c4d9ec98c3 feat!(ffi): merge send_reply and send_thread_reply (#4880)
This pushes down the `Reply` struct to be provided when sending a reply, and merges the `send_reply` and `send_thread_reply` FFI functions.

This is a small follow-up on https://github.com/matrix-org/matrix-rust-sdk/pull/4852/files#r2016594024.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-04-03 11:19:21 +02:00
Johannes Marbach dccd836dc6 feat!(timeline): allow sending media as (thread) replies (#4852)
This makes it possible to reply with a media, as part of a thread or not.

Fixes #4835.

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-04-02 12:25:06 +00:00
Benjamin Bouvier c719cd11f3 fix(event cache): properly clear all rooms, including those sleeping in the store backend 2025-04-02 13:54:43 +02:00
Benjamin Bouvier 42133a60c8 fix(ffi): use EventCache::clear_all_rooms() to clear the events caches
Clearing only the store backend, while not emptying the in-memory linked
chunks, will lead to out-of-sync state across the in-memory caches and
the database. As a result, it's safer to call the existing
`EventCacheInner::clear_all_rooms`, which will clear all the rooms
manually.
2025-04-02 13:54:43 +02:00
Benjamin Bouvier d30dc7177f refactor(sqlite): rename gaps to gap_chunks / events_chunks to event_chunks
And some comments have been tweaked too.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier e42be87798 test(event cache): add tests for save_event() and find_event_relations()
And fix the sql backend \o/
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 524040b33c refactor(event cache): have EventCacheStore::clear_all_rooms_chunks delete all the events' contents 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 0227b3f554 refactor(event cache): regroup code to compute the filter strings 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 6a076b0989 refactor(event cache): don't return the event itself, in find_event_with_relations
And rename it `find_event_relations`.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 9b1a5b7102 refactor(event cache): don't have find_event_with_relations return redaction events
A redaction event would be either applied a priori (by the server, when
returning the sync response), or the event cache would handle it, and
redact it in the database; in any case, we'd never see the original
event in its non-redacted form, so there's no point in returning the
redaction event itself.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 0bb72064b5 refactor(event cache): don't have find_event_with_relations return replies
It's unclear whether it's useful, especially in the case where it would
return an entire reply chain. It's not possible to filter in replies
only, using the function either, which is a sign that replies shouldn't
be indexed, IMO. In any case, that's something we can add back in the
future, if we want to.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 8af68d7389 feat(event cache): get the transitive closure of related events when getting an event's relations 2025-04-02 13:26:15 +02:00
Benjamin Bouvier cde0a9e24b refactor(event cache): get rid of the AllEventsCache 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 9423e41a06 refactor(event cache): use the store methods to retrieve an event by id, with or without its relations 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 3489cbd5d7 feat(event cache): allow retrieving an event and all its relations 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 65be779bb0 refactor(event cache): move relation extraction into common store helpers 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 45f1dca6a3 refactor(event cache): don't return a position in find_event
Getting the position when reading an event is no longer required:

- the only use case for reading the position out of the event cache was
when we wanted to replace a redacted item into the linked chunk; now
with save_event(), we can replace it without having to know its
position.

As an extra measure of caution, I've also included the room_id in the
`events` table, next to the event_id, so that looking for an event is
still restricted to a single room.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 913b2a5f78 feat(event cache): allow to persist an out-of-band event into storage 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 627e2ca5a6 refactor(linked chunk): rejigger the relational linked chunk so it can handle indexed items
This is necessary to save out-of-band items into the relational linked
chunk. I'm not quite sure of the value to keep it generic, at this
point, but at least it makes testing easy.
2025-04-02 13:26:15 +02:00
Benjamin Bouvier 3d8af1b972 feat(event cache): extract an event's relationship before inserting it into the database 2025-04-02 13:26:15 +02:00
Benjamin Bouvier 03f5d0222e refactor(event cache): store the events' content independently of their position in a chunk 2025-04-02 13:26:15 +02:00
dependabot[bot] af85447328 chore(deps): bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 0fee5fb278312d962ff465bb38dc4cae9f446de2 to 27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/0fee5fb278312d962ff465bb38dc4cae9f446de2...27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 09:07:21 +02:00
Andy Balaam d34e11b9f6 Fixes #4871 (hopefully). In test code, sync after other user cross-signs 2025-04-01 14:56:13 +02:00
Benjamin Bouvier 231073c9c3 chore(sqlite): log underlying errors in many OpenStoreError variants
This would help understanding what's the underlying error each time.
2025-04-01 14:51:32 +02:00
Andy Balaam 34a3fb4efb Fix typo decodeable -> decodable 2025-04-01 12:07:29 +01:00
dependabot[bot] a38c3b5dc5 chore(deps): bump crate-ci/typos from 1.30.2 to 1.31.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.30.2 to 1.31.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.30.2...v1.31.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 12:07:29 +01:00
Ivan Enderlin 1a1310e205 doc(ffi,sqlite,sdk): Update CHANGELOG.mds. 2025-04-01 11:50:20 +02:00
Ivan Enderlin d7b6fae2a3 feat(ffi): Add session_pool_max_size, …_cache_size and …_journal_size_limit.
This patch adds 3 methods on `ClientBuilder`:

1. `session_pool_max_size`,
2. `session_cache_size`,
3. `session_journal_size_limit`.

Respective fields are also added.

These values control the `SqliteStoreConfig`, used to control the
stores, especially their memory consumption.
2025-04-01 11:50:20 +02:00
Ivan Enderlin bb87a728ac doc(sqlite): Fix a broken link. 2025-04-01 11:50:20 +02:00
Ivan Enderlin d60810c2af refactor(ffi): Rename ClientBuilder::passphrase to session_passphrase.
This patch renames the `passphrase` method of `ClientBuilder` to
`session_passphrase` for more consistency with the `session_paths`
method.
2025-04-01 11:50:20 +02:00
Ivan Enderlin a6a4579ef9 feat(sdk): Add ClientBuilder::sqlite_store_with_config_and_cache_path.
This patch a new `sqlite_store_with_config_and_cache_path` method on
`ClientBuilder`.
2025-04-01 11:50:20 +02:00
Ivan Enderlin a084a5b08b feat(sdk): BuilderStoreConfig::Sqlite embeds SqliteStoreConfig.
This patch removes the `path` and `passphrase` fields from
`BuilderStoreConfig::Sqlite`, and replaces them by `SqliteStoreConfig`.

This patch then opens the stores with `open_with_config` instead of
`open`.
2025-04-01 11:50:20 +02:00
Ivan Enderlin 4112162092 feat(sqlite): Add SqliteStoreConfig::path() to override the path.
This patch adds `SqliteStoreConfig::path()` to override the path passed
to the constructor `new`.
2025-04-01 11:50:20 +02:00
Ivan Enderlin f341572616 feat(sqlite): SqliteStoreConfig implements Clone and Debug.
This patch implements `Clone` and `Debug` for `SqliteStoreConfig`.
2025-04-01 11:50:20 +02:00
Ivan Enderlin a047784278 feat(ffi): Add Client::restore_session_with and RoomLoadSettings.
This patch adds `Client::restore_session_with` along with the
`RoomLoadSettings` enum.
2025-04-01 10:50:18 +02:00
Ivan Enderlin 7ac3fa1f4a refactor(ffi): Inline restore_session_inner.
This patch inlines the `restore_session_inner` into its unique call
site.
2025-04-01 10:50:18 +02:00
Kevin Boos c30ec0ed8a chore(sdk): Reduce log verbosity from info -> trace in a few functions (#4747)
At the default `INFO` log level, the log gets inundated with thousands
of emitted statements, primarily related to
`is_display_name_ambiguous()`. The execution of this tiny function
certainly doesn't need to be traced every time, at least not at the info
log level.

Note: some of these could be debatably reduced to debug level rather
than trace level, but I went with "trace" because they all seem to be
trace statements rather than actual debug dump outputs (there is no
actual program state dumped out).

Signed-off-by: Kevin Boos
[kevinaboos@gmail.com](mailto:kevinaboos@gmail.com)

---------

Signed-off-by: Kevin Boos <1139460+kevinaboos@users.noreply.github.com>
2025-03-31 15:11:06 +00:00
Ivan Enderlin d6f2fd4304 test(base): Test BaseStateStore::load_rooms.
This patch adds tests for `BaseStateStore::load_rooms`. This patch also
updates the `test_derive_from_other` test.
2025-03-31 16:47:58 +02:00
Ivan Enderlin cf5b8d3b33 test(base): Test StateStore::get_room_infos with the RoomLoadSettings arguments.
This patch tests the new behaviour of `StateStore::get_room_infos`.
2025-03-31 16:47:58 +02:00
Ivan Enderlin 2a67d7472a feat(base,sqlite,indexeddb): Use RoomLoadSettings to load all or one room.
This patch updates `BaseStateStore` and the `StateStore` trait along
with its implementors, to return all rooms or a single room from
`StateStore::get_room_infos`.

See the previous patch for more context.
2025-03-31 16:47:58 +02:00
Ivan Enderlin 9f74be26c3 feat(base): Introduce RoomLoadSettings.
This patch introduces the `RoomLoadSettings` enum. It is helpful to load
either all rooms or one room when activating a `BaseClient`, i.e. when a
session is initialized or restored.

It addresses a broader problem where, for large accounts with large
caches, creating a `BaseClient` takes many resources. In a resource
constrainted context, like a push notification process, it can eat all
resources up to the point the process is killed (then notifications can
be missed).

The idea is then to force the `BaseClient` to load a single room.

This patch installs the `RoomLoadSettings` argument everywhere it needs
to be. The next patch will use `RoomLoadSettings` to load either all
rooms or a single one.
2025-03-31 16:47:58 +02:00
Damir Jelić fb04539418 refactor(encryption): Simplify the parsing of OAuthCrossSigningResetInfo
Now that OAuth isn't behind a feature flag a bunch of things can be
simplified.

Seems that we don't need a Client object either.
2025-03-31 13:06:28 +02:00
Richard van der Hoff 192cf0154a integration-test: Remove postgres container (#4858)
Followup on https://github.com/matrix-org/matrix-rust-sdk/pull/3983: now
that we don't have a sliding sync proxy, we don't need a postgres
container.
2025-03-28 16:51:12 +00:00
Damir Jelić 9acd649742 chore: Remove the RSA security advisory from our deny config
Since we don't depend on mas anymore, we don't depend on RSA either.

Let's remove the exception, lest we reintroduce the dependency and
security issue.
2025-03-28 17:08:42 +01:00
Ivan Enderlin e8fcdf4360 doc(base): Update the description in the CHANGELOG.md. 2025-03-28 16:11:44 +01:00
Ivan Enderlin 43dbb6a021 refactor(base): Split BaseStateStore::set_or_reload_session into 3 distinct methods.
This patch splits `BaseStateStore::set_or_reload_session` into 3
distinct methods:

1. `set_session_meta` (hello again!),
2. `load_rooms`,
3. `load_sync_token`.

This patch also renames
`BaseStateStore::set_or_reload_session_from_other` into
`derive_from_other` to clarify its semantics. It calls these 3 methods
above as a combo.

Finally, this patch also updates `BaseClient::activate` to call these 3
methods above individually.
2025-03-28 16:11:44 +01:00
Ivan Enderlin de615f2ffe refactor(base): Rename BaseClient::set_or_reload_session and ::logged_in.
This patch renames `BaseClient::set_or_reload_session`
to `BaseClient::activate`, and `BaseClient::logged_in` to
`BaseClient::is_activated`.

The idea behind these renamings is to introduce a “state” for the
`BaseClient`: it is activated when is has a `SessionMeta`, has loaded
its data from the storages, and has an `OlmMachine`. Consequently, the
`logged_in` method is renamed `is_activated` for the symmetry. If one
wants to know if the client is logged in, it can use `is_activated`, or
also `MatrixAuth::logged_in`.
2025-03-28 16:11:44 +01:00
Ivan Enderlin f11eec4caf doc(base): Update CHANGELOG.md. 2025-03-28 16:11:44 +01:00
Ivan Enderlin f445a5ca57 test(base): Add tests for Store::set_or_reload_session*.
This patch adds tests for the `set_or_reload_session` and
`set_or_reload_session_from_other` methods.
2025-03-28 16:11:44 +01:00
Ivan Enderlin 68605de596 feat(base): Add Store::set_or_reload_session_from_other.
This patch adds the `Store::set_or_reload_session_from_other` method to
isolate the behaviour of deriving a store from another one.
2025-03-28 16:11:44 +01:00
Ivan Enderlin db97d616f6 refactor(base): Rename set_session_meta to set_or_reload_session.
This patch renames the various `set_session_meta` methods to
`set_or_reload_session`. The idea is to highlight that the method is not
a simple setter: it sets but it _also_ updates the store' state.

The private shortcut method in `matrix_sdk::Client::set_session_meta`
is removed, and caller uses `client.base_client().set_or_reload_session`
instead. Why removing this `Client::set_session_meta` shortcut
method? Because it was creating confusion with another method:
`Client::restoring_session`. This private shortcut method wasn't used in
a lot of places, then I believe it's a nice improvement.
2025-03-28 16:11:44 +01:00
Damir Jelić e2e5b39afa test(sdk): Test that resetting cross-signing with an invalid password errors out 2025-03-28 15:23:52 +01:00
Damir Jelić 6fec953ff0 test(sdk): Use the MatrixMockServer for the cross-signing reset test 2025-03-28 15:23:52 +01:00
Damir Jelić 95befc9a25 fix(encryption): Return the uiaa error if we have one in the identity reset loop 2025-03-28 15:23:52 +01:00
Stefan Ceriu cb92971657 chore(ui): move TimelineItemContent::UnableToDecrypt to MsgLikeKind::UnableToDecrypt 2025-03-28 15:30:47 +02:00
Stefan Ceriu 5a35fec894 chore(ui): rename RedactedMessage to just Redacted 2025-03-28 15:30:47 +02:00
Stefan Ceriu 43b8f83e4b chore(ui): move TimelineItemContent::RedactedMessage to MsgLikeKind::RedactedMessage 2025-03-28 15:30:47 +02:00
Stefan Ceriu 0952255a50 chore(ui): reoder MsgLike helpers so that as_foo and is_foo are grouped 2025-03-28 15:30:47 +02:00
Ivan Enderlin 8738c4dbfd test(sqlite): Add test for cache_size and journal_size_limit.
This patch adds tests for checking the `PRAGMA cache_size` and `PRAGMA
journal_size_limit`.
2025-03-28 13:36:32 +01:00
Ivan Enderlin 99436f8e79 test(sqlite): Test the new RuntimeConfig type. 2025-03-28 13:36:32 +01:00
Ivan Enderlin 339b220488 feat(sqlite): Introduce RuntimeConfig which includes cache_size
This patch updates `StoreOpenConfig` to hold a new type: `RuntimeConfig`.

This `RuntimeConfig` type is passed to a new `SqliteAsyncConnExt`
method, named `apply_runtime_config`. Depending on the values passed
here, the `optimize`, `cache_size` (new!) and `journal_size_limit`
methods will be called automatically.

The goal of this type is to automate a flow we keep repeating in
all the stores. This is error-prone. This type brings uniformity and
consistency.

This patch also makes all `open_with_pool` methods on the stores private
(they were public before):

1. they were never used as far as I know because getting a `SqlitePool`
   isn't possible since the `pool` attribute is private…
2. it's better to keep control of this flow.
2025-03-28 13:36:32 +01:00
Ivan Enderlin 661f381e34 chore: Run rustfmt with an older nightly version. 2025-03-28 10:54:48 +01:00
Ivan Enderlin 8d4ccf6442 doc(sdk,base): Update CHANGELOG.mds. 2025-03-28 10:54:48 +01:00
Ivan Enderlin bd6b7c2ce1 refactor(base): Rename BaseClient::store to state_store.
This patch renames `BaseClient::store` to `state_store`, and
inevitably `Client::store` to `state_store` too.
2025-03-28 10:54:48 +01:00
Ivan Enderlin 9152d84b06 refactor(base): Rename BaseClient::store to state_store.
This patch pursues the same goal as the previous one: `Store` has
been renamed `BaseStateStore`, so the `store` field holding this
`BaseStateStore` is renamed `state_store`.
2025-03-28 10:54:48 +01:00
Ivan Enderlin c044f81d7b refactor(base): Rename Store to BaseStateStore.
This patch renames `Store` to `BaseStateStore`. Ideally, I would
like to rename to `StateStore` but that's already a trait name
(`traits::StateStore`).

Why this renaming? To clarify what store it is.
2025-03-28 10:54:48 +01:00
Stefan Ceriu 5730f0e00e chore(ui): introduce an as_message MsgLikeContent helper and use in tests 2025-03-27 15:53:31 +02:00
Stefan Ceriu 76f92ba9af chore(ui): rename AggregatedTimelineItemContent to MsgLikeContent and AggregatedTimelineItemContentKind to MsgLikeKind.
- we decided on this naming convention to keep consistent with Ruma which uses `MsgLike` as well
2025-03-27 15:53:31 +02:00
Stefan Ceriu d599c72278 chore(ui): use a newer version of as_variant to match nested types. 2025-03-27 15:53:31 +02:00
Stefan Ceriu e5243e32be chore(ui): simplify the fetch_replied_to_event method 2025-03-27 15:53:31 +02:00
Stefan Ceriu db18e7fd74 chore(ui): simplify the test by using more of the TimelineItemContent helpers 2025-03-27 15:53:31 +02:00
Johannes Marbach f3baf7efd2 refactor(timeline): push the reply logic down into matrix_sdk (#4842)
This achieves step 2 of #4835.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-03-27 13:47:51 +01:00
Benjamin Bouvier f6e223edf6 refactor(timeline): bump as_variant and make more use of its pattern matching form 2025-03-27 10:42:22 +01:00
Stefan Ceriu 4f3b40d6fb feat!(timeline): Introduce an extra TimelineItemContent layer that holds aggregations (#4839)
In preparation for threads we have realised that the `reactions`,
`thread_root` and `in_reply_to` were only available on `Message` types,
which doesn't play well with Stickers and Polls.

This PR introduces a new `Aggregated` `TimelineItemContent` variant
which holds the message `kind` (Message, Sticker, Poll) as well as well
as any related aggregated data. it will help treat them all in a similar
fashion as well as account for future changes.

There are no functional changes, it's mostly about moving code around
and the FFI interfaces haven't changed.

Part of #4833.
2025-03-26 16:27:03 +00:00
Richard van der Hoff 6e480271d3 crypto: get_most_recent_session: return None for unknown device (#4846)
If we have a device whose Curve25519 key we don't know, then
self-evidently we can't have any active Olm sessions with that device.
Currently, we return an `EventError::MissingSenderKey` in this case, but
(a) the definition of that error doesn't match this situation, and (b)
it complicates handling in methods that call `DeviceData::encrypt`
(currently only `DeviceData::maybe_encrypt_room_key`, but I want to add
a second).

Other than `DeviceData::encrypt`, the only place where
`get_most_recent_session` is called is `mark_device_as_wedged`. In that
case, we have just looked up the device by its Curve25519 key, so we
know it must have one.

We can therefore be reasonably certain that this change is a no-op.
2025-03-26 14:42:24 +00:00
Ivan Enderlin e60cf18337 refactor(base): Rename BaseClient::with_store_config to new.
This renames the `BaseClient::with_store_config` constructor to `new`:

1. there is no alternative constructor, it's the only one,
2. the `with_` prefix is usually reserved to (i) builders or (ii)
   alternative constructors,
3. I believe it clarifies the code.
2025-03-26 15:34:08 +01:00
Ivan Enderlin 6409adb879 doc(base): Remove an empty line in an example. 2025-03-26 15:34:08 +01:00
Ivan Enderlin 915e0e83bc doc(base): Fix documentation of BaseClient::with_store_config.
The documentation is outdated, `config` is now a `StoreConfig`.
2025-03-26 15:34:08 +01:00
Ivan Enderlin 8323ecdc8b doc(base): Improve documentation of BaseClient.
This patch:

* fixes the mention of “no IO”, it lacks the “network” part,
* adds an example of how to build a `BaseClient`,
* mentions that it is better be used via `matrix_sdk`.
2025-03-26 15:34:08 +01:00
Kévin Commaille e0e9c06ca4 Don't use serde to avoid returning an error
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-26 15:26:55 +01:00
Kévin Commaille eb313efdeb Don't qualify error! macro
It's already imported.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-26 15:26:55 +01:00
Kévin Commaille bc22ff1221 refactor(oauth): Introduce AccountManagementUrlBuilder
It allows to reuse the URL for different actions more easily than having
to call `OAuth::account_management_url` every time for a different
action.

It also adds a method with fallback if we want to ignore action
serialization errors, to always present a URL.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-26 15:26:55 +01:00
Benjamin Bouvier 8c988beaf2 doc(linked chunk): tweak comments related to clearing/dropping a linked chunk 2025-03-26 11:57:48 +01:00
Benjamin Bouvier 752c9baf7c refactor(timeline): simplify removal of duplicated local echo item 2025-03-26 11:33:20 +01:00
Benjamin Bouvier 766772f654 feat(timeline): insert the start of the timeline in places where it's required 2025-03-26 11:33:20 +01:00
Benjamin Bouvier f5b6767253 feat(timeline): add a timeline start virtual item 2025-03-26 11:33:20 +01:00
Benjamin Bouvier 5ce045ee02 refactor(timeline): avoid unnecessary shadowing variables 2025-03-26 11:33:20 +01:00
Benjamin Bouvier b83889dcba fix(ffi): propagate initial values before the future is picked by a runtime 2025-03-26 11:18:23 +01:00
Ivan Enderlin cfc839f71b doc(sqlite) Add entry to the changelog. 2025-03-26 11:01:52 +01:00
Ivan Enderlin 660d4e7ccb feat(sqlite): Add StoreOpenConfig and open_with_config for all stores.
This patch adds a new `StoreOpenConfing` type to configure the store
when opening it and when creating the pool of connections to SQLite via
`deadpool_sqlite`.

This patch also adds a new `open_with_config` constructor on all
stores, namely `SqliteCryptoStore`, `SqliteEventCacheStore` and
`SqliteStateStore`.
2025-03-26 11:01:52 +01:00
Benjamin Bouvier 404dd3949f test: remove unused helpers 2025-03-26 11:01:14 +01:00
Benjamin Bouvier 693c8df8d0 test(timeline): more of the same, with a new EventFactory method to add a receipt with a given timestamp 2025-03-26 11:01:14 +01:00
Benjamin Bouvier d587d5f145 test(timeline): make more use of MatrixMockServer / EventFactory 2025-03-26 11:01:14 +01:00
Benjamin Bouvier be6daa5930 test: add EventFactory::typing for typing notifications and get rid of more old cruft 2025-03-26 11:01:14 +01:00
Benjamin Bouvier acee5415c5 test: get rid of a few global statics for read receipts event contents 2025-03-26 11:01:14 +01:00
Benjamin Bouvier 6399a99452 test(timeline): use the EventFactory a bit more 2025-03-26 11:01:14 +01:00
Benjamin Bouvier b5aa2113db test: add a way to create ephemeral read receipts events in the EventFactory 2025-03-26 11:01:14 +01:00
Benjamin Bouvier 1585b0c32e test(timeline): use more MatrixMockServer 2025-03-26 11:01:14 +01:00
Benjamin Bouvier 3c01f88ab8 benchmark(linked chunk): add a benchmark for reading events out of the store 2025-03-26 10:29:01 +01:00
Benjamin Bouvier 785312856e fix(linked chunk): don't leak upon drop
The previous code in `LinkedChunk::drop()` would call `clear()`, which
would reset the chunk to an empty events chunk; preinitializing a vector
of 128 items. But this item chunk would never be dropped, so this would
cause a leak.

The solution is to split the semantics of *resetting* a linked chunk
(what was called `clear` before), from *clearing* it: clearing will put
it in an dangling state, and it's the caller's responsibility to do
something about it; as such, it's marked unsafe. `LinkedChunk::drop()`
may now call `clear()` (which is fine; last use of the linked chunk);
and `LinkedChunk::reset()` will call `clear()` and reset the first
chunk, which is fine too.
2025-03-26 10:29:01 +01:00
Benjamin Bouvier fc8a6dc9b1 refactor(ffi): always try to load InReplyToDetails from the event cache or network 2025-03-25 11:06:31 +01:00
Benjamin Bouvier 7b8694e465 refactor(timeline): better encapsulate RepliedToEvent's fields 2025-03-25 11:06:31 +01:00
Benjamin Bouvier 655f62c331 refactor(timeline): don't ever look into the timeline's items to fetch the replied-to event content
This is a nice simplification, because this means that:

1. we use a single way to get the event (event-cache-or-network)
2. we don't have to reconstruct a `RoomMessageEventContent` from a
timeline's message, which seems a bit error prone
3. there's a single way to get the replied-to info for an event,
4. and that's actually independent of the timeline, so we can improve
the code for #4835
2025-03-25 11:06:31 +01:00
Benjamin Bouvier 53732e0ff2 refactor(timeline): use load_or_fetch_event when fetching a reply's content 2025-03-25 11:06:31 +01:00
Benjamin Bouvier 4cae122854 refactor(room): use load_or_fetch_event when creating an edit event 2025-03-25 11:06:31 +01:00
Benjamin Bouvier 2a11494c33 feat(room): introduce a new method to load from cache or network 2025-03-25 11:06:31 +01:00
Benjamin Bouvier 1dddd97d96 refactor(timeline): simplify ReplyContent::Message 2025-03-25 11:06:31 +01:00
Johannes Marbach f8236a8b96 feat(timeline): add functions for sending messages in threads
`send_reply` is geared towards clients that don't render threads themselves. It sends a reply and optionally forwards it onto any existing thread.

This PR adds `send_thread_reply` for clients that do render threads themselves. This sends a message onto a thread, regardless of whether a thread existed before, and includes the fallback for non-threaded clients only if the message is not itself a reply on the thread.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-03-24 14:31:20 +01:00
Kévin Commaille aa07108c98 fix(oauth): Put cross-process module behind e2e-encryption feature
Since it requires the crypto store.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille 9aed0cc933 chore: Add changelog about experimental-oidc feature removal
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille f6c5addf55 refactor(sdk): Remove experimental-oidc feature
Now that is compiles under WASM and that the API was cleaned up, it
should be okay.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille 9434a112d9 fix(oauth): Don't run OAuth tests under WASM
They almost all require a mock server.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille c4ec32cb78 fix(sdk): Gate QR login imports and methods for WASM
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille c5b8c812b3 fix(oauth): Use WASM-compatible types from matrix-sdk-common
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille fdc2ca0c9e fix(oauth): Do not expose OAuthRegistrationStore under wasm32
It usually won't be possible to write data to a file.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 14:04:04 +02:00
Kévin Commaille dcd0e078f6 docs(qr-login): Update docs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 09:32:05 +00:00
Kévin Commaille 78b79a758f feat(oauth-cli): Use OAuthRegistrationStore
It's probably the recommended way to do registration when the client can create files.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 09:32:05 +00:00
Kévin Commaille 29f6606d99 refactor(examples): Rename oidc_cli to oauth_cli
And update the docs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-24 09:32:05 +00:00
Kévin Commaille 94f0beec51 chore: Add changelog for login with registration methods
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 590d1d7890 test(oauth): Add test for OAuth::use_registration_method
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 400c92fc89 refactor(oauth): Reuse the AuthorizationServerMetadata when possible
Avoids repeated calls to the same endpoint in the same flow.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille b3e82a05db refactor(oauth): Merge OAuth::login_with_oidc_callback() and OAuth::finish_login()
Accept a URL or a query string for simplicity.

That way we don't need to expose AuthorizationResponse.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille a8aa364757 refactor(oauth): Allow to use any registration method with OAuth::login
Gets rid of OAuth::url_for_oidc since it can be replaced by a call to
OAuth::login now.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 7457ecb1a8 feat(oauth): Allow to use any registration method with login_with_qr_code
Introduces the ClientRegistrationMethod type

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 01caf56edc refactor(oauth): Rename OAuth::configure to restore_or_register_client
The name is more explicit about what the function does.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 6f07d008c9 refactor(oauth): Inline store_client_registration and load_client_registration
They are one- or two-liners and are only used once.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille b408087320 refactor(oauth): OAuth::login doesn't return a Result
There is actually no way to get an error.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 19:17:13 +01:00
Kévin Commaille 22cbce82ce refactor(oauth): Use tokio::fs APIs instead of spawn_blocking
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille ecdc68aa1c chore: Add changelog for OidcRegistrations changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 4a0bf80ab0 test(oauth): Add checks that client ID is written to OAuthRegistrationStore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 095425f664 refactor(oauth): Do not take static registrations in default OAuthRegistrationStore constructor
It complicates the constructor and most clients will probably not need to use it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille ca4e212e98 refactor(oauth): Make OAuthRegistrationStore methods async
Since they perform blocking I/O we probably don't want to block a thread on that.

We use spawn_blocking, the alternative would be to use tokio::fs functions, which do the same thing and would require to load the whole file content in memory before (de)serialization.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 0b0f84b784 refactor(oauth): Don't ignore errors when reading the file of the OAuthRegistrationStore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille fbd4a7dc38 refactor(oauth): Get rid of OAuthError::UnknownError
Instead add a variant for OAuthRegistrationStoreError to
OAuthClientRegistrationError.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille cb90d7fee6 refactor(oauth): Avoid impossible error
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 1a79ea94ed refactor(oauth): Improve OAuthRegistrationError variants
Make them more precise instead of wrapping several error types into a single variant.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille c3328a03f6 refactor(oauth): Avoid unnecessary allocations when accessing OAuthRegistrationStore data
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 6803538c2e refactor(oauth): Rename OidcRegistrations to OAuthRegistrationStore
Use the same prefix as the other types in the OAuth 2.0 API, and use the
same suffix as other data-persisting APIs for consistency.

It also avoids to have two modules with very similar names, the only
difference being a trailing `s`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Kévin Commaille 9d27e9b379 refactor(oauth): Make registrations module private
Since it only contains 2 types, it doesn't seem worth it to expose it,
we can just expose the types elsewhere.
2025-03-21 10:49:46 +01:00
Kévin Commaille 8683ca4d13 refactor(oauth): Re-export ClientID from the oauth module
Since it is now used everywhere, there is no reason to reexport it from
the registrations module.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-21 10:49:46 +01:00
Benjamin Bouvier d4f5ac152a feat(ffi): log the log targets and levels 2025-03-21 09:37:25 +01:00
Benjamin Bouvier 31a1724390 feat(ffi): add support for log bundles 2025-03-21 09:37:25 +01:00
Kévin Commaille c034818c92 chore: Add changelog for OAuth::login changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-20 16:02:18 +00:00
Kévin Commaille e1fe479008 refactor(oauth): Get rid of OAuthError::MissingDeviceId
Since we are the ones generating the device ID, we have a way to avoid this error. Even if in practice, it's probably always included in the server's response.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-20 16:02:18 +00:00
Kévin Commaille 530659b59d feat(oauth): Allow user to log into the same session again
Can be useful with soft logouts, without requiring the user to recreate a new Client to log in again.

Returns an error if the new session is different from the current one.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-20 16:02:18 +00:00
Kévin Commaille 45dd96e30a refactor(oauth): Merge finish_authorization and finish_login
That way users only need to call finish_login, since there is no other
reason to call finish_authorization currently.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-20 16:02:18 +00:00
Hugh Nimmo-Smith 3f4c1fd1bb feat(widgets, element-call): Update the widget url generation
Fixes: #4793

There was a previous PR https://github.com/matrix-org/matrix-rust-sdk/pull/4802 which attempted to implement this, but missed some backwards compatibility needs.

This updated PR has the original commit and then additional commits to add the compatibility (along with tests for the new intent param generally).
2025-03-20 14:28:45 +00:00
Benjamin Bouvier 5acaaf5865 fix(ffi): call the back-pagination status callback immediately 2025-03-20 10:26:54 +01:00
Benjamin Bouvier 156501dbbd chore(event cache): add logs here and there 2025-03-20 10:26:54 +01:00
Hugh Nimmo-Smith a0eb9340d5 Revert "feat(widgets, element-call) Update the widget url generation (#4802)"
This reverts commit 3b9ae3e65e.
2025-03-19 17:49:21 +01:00
Jonas Richard Richter dbdbfd0b38 feat(notification): Add support for custom conditional push rules (#4587)
---
Signed-off-by: Jonas Richard Richter <jonas-richard.richter@telekom.de>
2025-03-19 12:50:02 +01:00
Ivan Enderlin 1d9d4d3b3a chore(sdk): Annotate RoomEventCacheState::remove_events with #[instrument].
This patch annotates `RoomEventCacheState::remove_events` with the
`#[instrument]` proc-macro so that it is logged when called.
2025-03-19 12:15:59 +01:00
Ivan Enderlin 8d16b3265c refactor(sdk): RoomEventCacheState checks if events to remove aren't empty.
This patch updates `RoomEventCacheState::remove_events` to check whether
the set of events are not empty before removing them. When removing
`in_memory_events`, it avoids taking a write lock on the `RoomEvents`
for nothing for example.
2025-03-19 12:15:59 +01:00
Ivan Enderlin 9c37a0393c fix(base): Check the lazy_previous of the first chunk matches the new first chunk.
This patch adds a new check when inserting a new first chunk. It makes
some tests to fail but because they were not realistic. This patch then
updates these tests.
2025-03-19 11:18:05 +01:00
Ivan Enderlin 82ef6232e7 doc(sdk): Precise in which context a variable can or cannot be used. 2025-03-19 11:02:09 +01:00
Timo 3b9ae3e65e feat(widgets, element-call) Update the widget url generation (#4802)
Fixes: https://github.com/matrix-org/matrix-rust-sdk/issues/4793

Co-authored-by: Valere <bill.carson@valrsoft.com>
2025-03-19 11:29:07 +02:00
Kévin Commaille a539518cd4 chore: Add changelog for Oidc renaming
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille f61cd60147 refactor(oauth): Change the Oauth prefix in test utils with OAuth
For consistency.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille b9c970dc43 refactor(oauth): Rename OauthGrantType to OAuthGrantType
For consistency.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille ba5e395a59 refactor(oauth): Change Oauth prefix for error types to OAuth
For consistency.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille c46e6623fe refactor(oauth): Rename OauthClient and OauthHttpClient to OAuthClient and OAuthHttpClient
For consistency

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 7ad1b113dc doc(oauth): Change mentions of OpenID Connect to OAuth 2.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille c0d3ed1a90 refactor(oauth): Rename provider_metadata to server_metadata
"Provider" is an OpenID Connect term. OAuth 2.0 uses the "authorization
server" term.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 00d7a77ebe refactor(encryption): Rename OidcCrossSigningResetInfo to OAuthCrossSigningResetInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille f29d3fd666 refactor(oauth): Rename OidcAuthCodeUrlBuilder to OAuthAuthCodeUrlBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 47204830a9 refactor(oauth): Rename OidcError to OAuthError
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille f4bb14a30e refactor(oauth): Rename OidcSession to OAuthSession
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 0a345a3124 refactor(oauth): Rename OidcAuthData to OAuthAuthData
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 450a66ad11 refactor(oauth): Rename OidcCtx to OAuthCtx
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 6f3694cfa9 refactor(oauth): Rename Oidc API to OAuth
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille 1658610f93 refactor(sdk): Rename oidc module to oauth
Since we mostly use OAuth 2.0 now.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 17:18:34 +01:00
Kévin Commaille f9b1bdb22d chore: Add changelog for LocalServerBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 09:55:01 +01:00
Kévin Commaille f8abb85e9e refactor(oidc_cli): Use LocalServerBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 09:55:01 +01:00
Kévin Commaille 1b5e6462ee refactor(sdk): Use LocalServerBuilder with SsoLoginBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 09:55:01 +01:00
Kévin Commaille fbdd8839e6 feat(sdk): Expose a local server builder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-18 09:55:01 +01:00
dependabot[bot] d86117ac70 chore(deps): bump crate-ci/typos from 1.30.1 to 1.30.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.30.1 to 1.30.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.30.1...v1.30.2)

---
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>
2025-03-17 20:24:01 +01:00
dependabot[bot] 914b7125cf chore(deps): bump tj-actions/changed-files
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from dcc7a0cba800f454d79fff4b993e8c3555bcc0a8 to 0fee5fb278312d962ff465bb38dc4cae9f446de2.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/dcc7a0cba800f454d79fff4b993e8c3555bcc0a8...0fee5fb278312d962ff465bb38dc4cae9f446de2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 18:32:00 +02:00
Richard van der Hoff ad0223cafb Merge pull request #4775 from matrix-org/rav/history_sharing/room_key_bundle
crypto: support for building key bundles
2025-03-17 11:45:38 +00:00
Richard van der Hoff a870c02eab test: snapshot test for HistoricRoomKey::debug 2025-03-17 11:24:52 +00:00
Richard van der Hoff 002e77616d crypto: support for building key bundles
Add a method to CryptoStore which will construct a key bundle, ready for
encrypting and sharing with invited users.

Part of https://github.com/matrix-org/matrix-rust-sdk/issues/4504
2025-03-17 11:24:52 +00:00
Michael Telatynski d777e68c4a Pin tj-actions/changed-files
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-17 10:26:15 +00:00
Ivan Enderlin cabb345a1c fix(xtask): Add --limit 100 to gh pr list.
Because yes, some weeks, we are very productive!

This patch adds `--limit 100` to `gh pr list` so that we are sure to not
miss pull requests if there are many.
2025-03-14 18:04:33 +01:00
Kévin Commaille 2f08f27b59 chore: Add changelog about removing mas-oidc-client
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 3a7b0e9404 refactor(oidc): Remove dependency on mas-oidc-client
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 7713ce768a refactor(oidc): Create ClientMetadata type
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille aea573d001 refactor(oidc): Import code to register a client
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 7ca6494efa refactor(oidc): Remove support for software statement
It is not mentionned in MSC2966
2025-03-14 18:03:34 +01:00
Kévin Commaille 6f44853bf7 refactor(oidc): Use Url for the issuer
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 2c6c818005 refactor(oidc): Use ruma's server metadata type
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille abc4fbc2f7 refactor(oidc): Import code to discover OIDC provider configuration
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 9adff21f78 refactor(oidc): Import code for building the account management URL
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-14 18:03:34 +01:00
Kévin Commaille 91f9ef85ae refactor(oidc): Add type alias for oauth2 errors
To have less verbose and more predictable error types.
2025-03-14 18:03:34 +01:00
Ivan Enderlin 17c6ad6b70 test(ui): Ensure Timeline runs a redecryption for all UTD.
This patch adds two tests, ensuring UTD stored in the event cache are
decrypted, whether they come from the initial items or paginated items.
2025-03-14 14:18:21 +01:00
Ivan Enderlin 8f61bdb046 fix(ui): Events received from the event cache trigger a decryption.
This patch fixes a bug where events coming from the event cache might be
encrypted, see https://github.com/matrix-org/matrix-rust-sdk/issues/4762
to learn more.

This patch updates the `room_event_cache_updates_task` to call
`TimelineController::retry_event_decryption` if the origin is `Cache`.
2025-03-14 14:18:21 +01:00
Ivan Enderlin 53c36226cb refactor(ui): TimelineController::retry_event_decryption no longer needs a &Room.
This patch removes the room `&Room` argument of
`TimelineController::retry_event_decryption`. The `TimelineController`
already has the room with its `room(&self) -> &Room` method. This method
was always used to fetch the room, let's expect `retry_event_decryption`
to do that by itself.

It also prevents passing the “wrong” room. This is more robust this way.
2025-03-14 14:18:21 +01:00
Ivan Enderlin 5a22944f52 chore(ui): Move an inline comment.
This patch moves an inline comment in its correct place. Code
was inserted between the comment and the part of the code it was
documenting.
2025-03-14 10:51:49 +01:00
Ivan Enderlin 494f93d2a4 chore(ui): Move the RoomKeyInfo task inside its own function.
This patch moves the task responsibles to handle the `RoomKeyInfo`
updates into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin d7849a1aa5 chore(ui): Move the BackupState task inside its own function.
This patch moves the task responsibles to handle the `BackupState`
updates into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin bc9adaab06 chore(ui): Move the rooms keys from backups task inside its own function.
This patch moves the task responsibles to handle the room keys from
backups into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin 1ec47ca24f chore(ui): Move the RoomSendQueueUpdate task inside its own function.
This patch moves the task responsibles to handle the
`RoomSendQueueUpdate`s into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin faa2fa2ef0 chore(ui): Move the pinned event IDs stream task inside its own function.
This patch moves the task responsibles to handle the pinned event IDs
stream into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin 33e8b453ee chore(ui): Move the RoomEventCacheUpdate task inside its own function.
This patch moves the task responsibles to handle the
`RoomEventCacheUpdate`s into its own function.

The goal is to get simpler code.
2025-03-14 10:51:49 +01:00
Ivan Enderlin b9fadd0a10 fix(base): Do not define the RoomInfoNotableUpdate's channel based on number of rooms.
When `BaseClient` is created, the rooms aren't loaded yet. Then,
calculating the size of the channel for `RoomInfoNotableUpdate` is
useless, as it will always be zero, and we will fallback to 500 every
time. By the way, 500 is a nice default. This patch uses this value as
the only channel's size.
2025-03-14 10:43:12 +01:00
Richard van der Hoff 294fd79947 multiverse: support passing a server URL
Allow use of a server url (eg `http://localhost:8008`), enabling connection to
a local server rather than one which supports well-known, TLS, and the rest.
2025-03-13 20:16:54 +01:00
Ivan Enderlin 8b6096729c fix(sdk): event_cache::Room::replace_all_events_by takes an EventsOrigin.
This patch updates `replace_all_events_by` to take an `EventsOrigin`.
It is called in two places: by `add_initial_events`, in which case it
is `EventsOrigin::Cache`, and by `handle_timeline`, in which case it is
`EventsOrigin::Sync`.
2025-03-13 09:23:24 +01:00
Ivan Enderlin 6f370daaed fix(sdk): Replace assert! by debug_assert!.
This patch replaces a call to `assert_eq` and `assert` by
`debug_assert_eq` and `debug_assert`. We don't want to panic in
production :-].
2025-03-13 09:23:24 +01:00
Ivan Enderlin 3c694e7909 fix(sdk): event_cache::Room::clear sends EventsOrigin::Cache.
This patch updates the `EventsOrigin` value sent by `Room::clear` to be
`Cache` instead of `Sync`. No events are added, but the `VectorDiff`s
are generated from the event cache itself, not from a sync.
2025-03-13 09:23:24 +01:00
Ivan Enderlin c3245a4f22 fix(sdk): Events loaded from the cache have EventsOrigin::Cache.
This patch fixes the `EventsOrigin` value for events loaded from the
cache, it was `Pagination`, now it is `Cache`.
2025-03-13 09:23:24 +01:00
Ivan Enderlin da89a53605 feat(ui): Add EventItemOrigin::Cache.
With more and more events coming from the event cache, it's nice to know
that an event actually from the cache instead of having `None`.
2025-03-13 09:23:24 +01:00
Ivan Enderlin 968582af01 chore(ui): Update a log message.
This patch changes the log message. A `TimelineItemPosition::UpdateAt`
can only happen for remote event, not local event. The log message
was talking about _decryption_ but an `UpdateAt` can also happen for
redaction. This was misleading.
2025-03-13 09:23:24 +01:00
Ivan Enderlin 07c7b6ab2a fix(ui): Pass the correct RemoteEventOrigin to replace_with_initial_remote_events.
This patch updates the `RemoteEventOrigin` value passed to
`TimelineController::replace_with_initial_remote_events` when there is
a lag with the event cache. The previous value was `Sync`, but it should
be `Cache` since these events come from the event _cache_ (it was in the
name, easy).
2025-03-13 09:23:24 +01:00
Ivan Enderlin 5aae0cbcd9 test: Fix a test in NotificationClient.
With the previous patch, `NotificationClient` with sliding sync now
knows immediately when the room is encrypted or not.
2025-03-12 16:53:59 +01:00
Ivan Enderlin 3ea842dae4 test(base): Test that room encryption is correctly computed. 2025-03-12 16:53:59 +01:00
Ivan Enderlin 31e0bfa400 feat: Install RequestedRequiredStates and add handle_encryption_state.
This patch updates the sync code to include the
`RequestedRequiredStates` type.

This patch also adds `RoomInfo::handle_encryption_state` which
is able to mark an encryption state as synced depending of
`RequestedRequiredStates` (read the comment in the code).

This patch also updates the documentation of
`RoomInfo::handle_state_event` to clarify the impact of a
`m.room.encryption` state event.
2025-03-12 16:53:59 +01:00
Ivan Enderlin d32b10de80 feat: Introduce RequestedRequiredStates.
This patch introduces a new type: `RequestedRequiredStates`, which keeps
track of all `required_states` passed to a sync request. So far, there
is only a `From` implementation for MSC4186.
2025-03-12 16:53:59 +01:00
Kévin Commaille 215853cf67 chore: Upgrade ruma
To pull in`GrantType::DeviceCode` and a fix for the generated `DeviceId`
length.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-12 13:41:40 +01:00
Jorge Martín a941cc824d fix(ffi): Restore some needed OIDC prompts in the FFI layer
These prompts were used in the Element X app, probably in some other clients too.

Since Ruma removed these cases, we're just passing them as `_Custom(value)` ones, which do work.
2025-03-12 13:12:09 +01:00
Benjamin Bouvier d3daa18bf8 feat!(ffi): rename setup_tracing into init_platform (#4790)
And make it take a boolean indicating whether we want to set up a
lightweight tokio runtime or not, instead of having
`setup_lightweight_tokio_runtime` as a public function + another
function, both of which would have to be called anyways.

cc @stefanceriu @jmartinesp
2025-03-12 09:41:01 +00:00
Ivan Enderlin 2ac3b6e9a2 chore(ffi): Remove useless block_on.
This patch removes the last `block_on` calls in `matrix-sdk-ffi`. Those
are artifacts of the past.
2025-03-11 16:37:44 +01:00
Benjamin Bouvier e81817c1b2 chore(ci): add new exceptions for cargo-deny 2025-03-11 16:05:52 +01:00
Benjamin Bouvier 01bb8093d0 feat(ffi): add a function to setup a lightweight tokio runtime
Creating many threads may use a bit of memory: on a machine with N
devices, exactly N*2 MB of memory may be consumed.

That might be a lot for a NSE process on iOS, which can only have up to
16 MB of RAM allocated for it. For this case, we introduce a new FFI
method `setup_lightweight_tokio_runtime` which will spawn at most 4
worker threads and 1 blocking thread. This should be sufficient for most
use cases.
2025-03-11 16:05:52 +01:00
Ivan Enderlin 1565067cee doc(ffi): Update the CHANGELOG.md. 2025-03-11 15:39:50 +01:00
Ivan Enderlin ecc603171b feat(ffi): Add RoomInfo::encryption_state.
This patch adds the `EncryptionState` onto the new
`RoomInfo::encryption_state` field.
2025-03-11 15:39:50 +01:00
Benjamin Bouvier 7f3308bd2b feat(sdk): don't trigger the ignored user list change if it hasn't changed since the previous time 2025-03-11 15:03:42 +01:00
Benjamin Bouvier 06d5fdb5ff fix(event cache): enable foreign keys on a connection basis
As opposed to WAL mode, foreign keys must be enabled for each database
connection, according to
https://www.sqlite.org/foreignkeys.html#fk_enable

Unfortunately, we can't track which connection objects have already
executed the pragma, so the safer we can do is enable it everytime we
try to acquire a connection from the pool.

Fixes #4785.
2025-03-11 15:03:42 +01:00
Benjamin Bouvier 6047d369a6 refactor(event cache): call clear() instead of doing it manually in clear_all_rooms 2025-03-11 15:03:42 +01:00
Benjamin Bouvier 961a893b8c test(event cache): double-check cascading happened in the clear linked chunk test 2025-03-11 15:03:42 +01:00
Benjamin Bouvier 2927974396 fix(event cache): don't try to remove a previous gap if it's the only chunk in memory
A linked chunk never wants to be empty. However, after a limited gap
that doesn't contain events, it may be shrunk to the latest chunk that's
a gap.

If later we decide to remove the gap (because it's been resolved with no
events), then we would try to remove the last chunk, which is not
correct.

Ideally, we'd keep an events chunk around; but if we have an events
chunk *before* a gap, that may look like missing events to the user, at
least until the gap has been resolved.

The fix to this problem is to *not* optimize / remove the gap, if it's
the only chunk kept in memory. This was only a memory optimization, but
it's not absolutely required per se.
2025-03-11 15:03:42 +01:00
Benjamin Bouvier 8c780fc5d5 chore(event cache): don't make use of .not() when it's not useful
`.not()` is useful in assertions, at best, but using it where `!` would
suffice is a bad code smell.
2025-03-11 15:03:42 +01:00
Benjamin Bouvier 8867d203e7 chore(event cache): add spans for RoomEventCache methods
So we know which room some logs messages correspond to.
2025-03-11 15:03:42 +01:00
Ivan Enderlin cf5f14ef5d feat(base): Reduce memory usage of BaseClient::room_info_notable_update_sender.
This patch reduces the memory usage of the broadcast channel used by
`BaseClient::room_info_notable_update_sender`. So far, its size was
`u16::MAX`. Considering `RoomInfoNotableUpdate` is 24 bytes, the channel
was allocating 1.5Mb of memory, which is way too much. It is creating
problems on systems where the process has limited resources, like the
Notification Service Extension on iOS.

For a regular users with 200 rooms, the memory usage becomes 24Kb, which
is 65'536 times less.
2025-03-11 14:47:53 +01:00
Ivan Enderlin 132f063769 feat(base): Add ObservableMap::len.
This patch implements `ObservableMap::len`, which is useful to count of
values it contains.
2025-03-11 14:47:53 +01:00
Ivan Enderlin 915cb13d45 fix(ffi): Remove Room::is_encrypted.
This API is now deprecated.
2025-03-11 14:03:42 +01:00
Kévin Commaille 0089da10cc refactor(ffi): Use methods on OidcConfiguration to construct parts
Changing the `TryInto` implementation into a method makes the code easier to follow.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-11 13:55:12 +01:00
Kévin Commaille 28293d0f2b chore: Add changelog for url_for_oidc changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-11 13:55:12 +01:00
Kévin Commaille d3e64295cf refactor(oidc): Add redirect URI as an argument of url_for_oidc
Being able to always use the first redirect URI in the client metadata
seems to be very specific to the FFI bindings.

For example clients that need to bind a port on localhost need to
provide a custom redirect URI each time.

 So we ask for the redirect URI, and keep the current behavior only for
the bindings.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-11 13:55:12 +01:00
Kévin Commaille 6cd3217c2e refactor(oidc): Don't take the client metadata as an argument of url_for_oidc
The OidcRegistrations already hold the metadata. We can just clone it lazily when we need it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-11 13:55:12 +01:00
Ivan Enderlin eba2a7a6e3 doc(ffi): Update the CHANGELOG.md. 2025-03-11 12:28:16 +01:00
Ivan Enderlin a98b822eeb feat(ffi): Replace Room::is_encrypted by encryption_state and latest_encryption_state. 2025-03-11 12:28:16 +01:00
Ivan Enderlin 0a80021742 doc: Update the CHANGELOG.mds. 2025-03-11 12:28:16 +01:00
Ivan Enderlin 63e8fc84a3 test(sdk): Test encryption_state() vs latest_encryption_state(). 2025-03-11 12:28:16 +01:00
Ivan Enderlin fe0fb641f3 test(base): Test EncryptionState helpers. 2025-03-11 12:28:16 +01:00
Ivan Enderlin 1c43bc7e29 test(base): Test EncryptionState::NotEncrypted. 2025-03-11 12:28:16 +01:00
Ivan Enderlin d03ed3063c feat: Introduce EncryptionState.
This patch introduces the new `EncryptionState` to represent the 3
possible states: `Encrypted`, `NotEncrypted` or `Unknown`. All the
`is_encrypted` methods have been replaced by `encryption_state`.
The most noticable change is in `matrix_sdk::Room` where `async fn
is_encrypted(&self) -> Result<bool>` has been replaced by `fn fn
encryption_state(&self) -> EncryptionState`. However, a new `async
fn latest_encryption_state(&self) -> Result<EncryptionState>` method
“restores” the previous behaviour by calling `request_encryption_state`
if necessary.

The idea is that the caller is now responsible to call
`request_encryption_state` if desired, or use `latest_encryption_state`
to automate the call if necessary. `encryption_state` is now non-async
and infallible everywhere.

`matrix-sdk-ffi` has been updated but no methods have been added for
the moment.
2025-03-11 12:28:16 +01:00
Stefan Ceriu ea8664c487 Merge pull request #4780 from matrix-org/stefan/invitesRoomSummaryFallback
Invites room summary fallback
2025-03-11 11:02:23 +02:00
Stefan Ceriu ca025f8cca feat(ffi): forget the room when rejecting invites
- we're doing this as an extra layer of protection against spam attacks.
2025-03-11 10:16:27 +02:00
Stefan Ceriu 78e19fce32 chore(sdk): rewrite the room summary fallback test on top of the MatrixMockServer 2025-03-11 09:22:29 +02:00
Andy Balaam c8536e9e46 fix(crypto): Redecrypt non-UTD messages to remove no-longer-relevant warning shields (#4644)
Fixes https://github.com/element-hq/element-meta/issues/2697
Fixes https://github.com/element-hq/crypto-internal/issues/398

I'm sorry it's a big change. I've tried to break it into decent commits,
and I did a couple of preparatory PRs to make it less painful, but it's
still a bit to get your head around.

The basic idea is that when a session is updated and we call
`retry_event_decryption`, we don't only look at UTDs any more - now we
also look at decrypted events, and re-request their `EncryptionInfo`, in
case it has improved.

---------

Signed-off-by: Andy Balaam <mail@artificialworlds.net>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-03-11 07:01:54 +00:00
Benjamin Bouvier 1caa6069db refactor(timeline): move is_utd() to TimelineItemContent
It's unusual to have the method on the parent type when the field type
could also hold the method. In fact, this was the only bool getter
inspecting the timeline's content, so let's move the method next to as
its siblings, for consistency, and let's spell it out fully for clarity.
2025-03-11 07:43:53 +01:00
Stefan Ceriu abe8338e5c chore(ffi): expose a method for retrieving rooms based on their identifier 2025-03-10 19:11:59 +02:00
Stefan Ceriu 5373e39ce5 chore(ffi): remove now unnecessary invited_room and inviter methods as those should be retrieved through the room preview 2025-03-10 19:11:58 +02:00
Stefan Ceriu 5875973c13 feature(ffi): have previews for invited rooms fallback to cached client data if fetching the preview fails
- relates to element-hq/element-x-ios/issues/3713
- this will allow us to interact with them even if the given homeserver doesn't have MSC3266 enabled
2025-03-10 19:11:58 +02:00
dependabot[bot] 3fbf159d0e chore(deps): bump crate-ci/typos from 1.30.0 to 1.30.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.30.0 to 1.30.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.30.0...v1.30.1)

---
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>
2025-03-10 16:38:20 +01:00
Kévin Commaille b5c4fe3f7d test(sdk): Allow any MockEndpoint to override the expected access token
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 10:52:53 +01:00
Kévin Commaille 516d066d4c test(sdk): Add a constructor for MockEndpoint on MatrixMockServer
Allows to reduce duplication and will allow to add common logic.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 10:52:53 +01:00
Kévin Commaille fbcd5a71aa test(sdk): Always call MockEndpoint::respond_with
Instead of MockBuilder::respond_with. This reduces duplcation and will
allow to add some common logic when building the endpoints.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 10:52:53 +01:00
Ivan Enderlin b5a23086fd test(sdk): Add test for maybe_apply_new_redaction.
This patch adds a test for `maybe_apply_new_redaction` when the redacted
event is not loaded in-memory, i.e. when it lives in the store only.
2025-03-10 09:45:41 +01:00
Kévin Commaille a9ce3f6963 chore: Add changelog for merging SessionTokens
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 09:12:38 +01:00
Kévin Commaille a27f8f79a4 refactor(sdk): Move the session tokens into the AuthCtx
To avoid duplicating the code between both authentication APIs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 09:12:38 +01:00
Kévin Commaille dd01479c6b refactor(sdk): Use a single SessionTokens type
Since MatrixSessionTokens and OidcSessionTokens are identical.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-10 09:12:38 +01:00
dependabot[bot] e7f85ba545 chore(deps): bump ring from 0.17.8 to 0.17.13
Bumps [ring](https://github.com/briansmith/ring) from 0.17.8 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:41:43 +02:00
Andy Balaam 48767da6cc refactor(test): Make use of is_utd method in integration test 2025-03-07 14:20:45 +00:00
Andy Balaam 73754399be feat(timeline): Provide is_utd on EventTimelineItem 2025-03-07 14:20:45 +00:00
Kévin Commaille 18f5668e3e Add assertion messages
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-07 13:10:52 +01:00
Kévin Commaille bc92e55b53 Improve tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-07 13:10:52 +01:00
Kévin Commaille 230feff430 test(sdk): Add tests for handle_refresh_tokens and Oidc
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-07 13:10:52 +01:00
Kévin Commaille 8bb4387dc4 fix(oidc): Match the proper error type for invalid refresh token
Since we do not use mas-oidc-client anymore, the error to match has changed.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-07 13:10:52 +01:00
Kévin Commaille 2506ba8364 refactor(oidc): Use oauth2 for token revocation
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-07 12:50:58 +01:00
Damir Jelić daad6d662f fix(multiverse): Don't wait for sync service state changes when shutting down
The SyncService::stop method guarantees that the sync service will be
stopped after it has completed so there's no need to wait for state
changes.

The state change might not even come, if you pressed `S` to stop the
sync service manually.
2025-03-06 16:16:11 +01:00
Damir Jelić 53853c2d9a refactor(multiverse): Put the login logic into a separate function 2025-03-06 15:46:07 +01:00
Damir Jelić 40de714e81 refactor(multiverse): Use clap to simplify the CLI argument parsing 2025-03-06 15:46:07 +01:00
Damir Jelić 27bde16843 refactor(multiverse): Simplify the terminal and panic hook setups 2025-03-06 15:46:07 +01:00
Damir Jelić 5e8f8d5513 refactor(multiverse): Simplify the tracing setup 2025-03-06 15:46:07 +01:00
Damir Jelić 120970c4ea chore(multiverse): Bump the deps 2025-03-06 15:46:07 +01:00
Kévin Commaille 740e729606 docs(oidc): Document the arguments of url_for_oidc
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille 60b140b684 chore: Add changelog for using oauth2
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille 9a165468eb test(oidc): Add more checks for the authorization URL
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille e15897b3f1 refactor(oidc): Use oauth2 for authorization code grant
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille 52f98582f1 refactor(oidc): Use oauth2 client for refreshing access tokens
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille 2e72c23868 refactor(oidc): Move error types to the error module
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Kévin Commaille 0967027feb refactor(oidc): Use ClientId type from oauth2
Avoids to use 2 similar types with the same name.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-06 12:27:30 +01:00
Andy Balaam 6c9b1ef3c1 fix(common): Rename all snapshots in deserialized_responses to have shorter names 2025-03-05 15:29:44 +00:00
Damir Jelić 8cceded0ae refactor(oidc): Move the fallback issuer discovery logic into a separate method 2025-03-05 15:37:04 +01:00
Jorge Martín ff181475a0 fix(client): Add handle_verification_events field to BaseClient.
This is done to fix an issue with these events being received and processed twice when `NotificationProcessSetup` is `SingleProcess`, causing issues with user verification.

This can be used to ignore verification requests in this sliding sync instance, preventing issues found where several sliding sync instances with the same client process events simultaneously and re-process the same verification request events during their initial syncs.
2025-03-05 15:09:31 +01:00
Andy Balaam 074c0e59e0 fix(common): Shorten the name of the snapshot_test_encryption_info 2025-03-05 14:03:51 +00:00
Kévin Commaille 1d7c60c46a chore: Add changelog about ID tokens support removal
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-05 14:19:27 +01:00
Kévin Commaille 377f34fae2 refactor(oidc): Get rid of OidcBackend
Now that we don't use it for tests, we don't need it anymore.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-05 14:19:27 +01:00
Kévin Commaille 26cb805e0f test(oidc): Use MatrixMockServer in the remaining tests
Gets rid of the MockImpl for OidcBackend.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-05 14:19:27 +01:00
Kévin Commaille 81dbe2060c refactor(oidc): Remove support for ID tokens
ID tokens are a feature of OpenID Connect, we don't need them to support OAuth 2.0.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-05 14:19:27 +01:00
Ivan Enderlin fd0fca436b chore(sdk): Remove the request_body instrument's field.
Many fields here are not argument of the `send` method, but are set
later with `Span::record`. Grepping all these fields reveal they are all
set except `request_body` apparently.
2025-03-05 14:15:29 +01:00
Ivan Enderlin 3d653d3fdc fix(sqlite): Design a new schema to get faster insertions.
This patch is twofold. First off, it provides a new schema allowing to
improve the performance of `SqliteEventCacheStore` for 100_000 events
from 6.7k events/sec to 284k events/sec on my machine.

Second, it now assumes that `EventCacheStore` does NOT store invalid
events. It was already the case, but the SQLite schema was not rejecting
invalid event in case some were handled. It's now explicitely forbidden.
2025-03-05 13:57:08 +01:00
Ivan Enderlin b22bb3ee9f fix(sqlite): Use a prepared statement to insert events.
This patch uses a prepared statement to insert events in the linked
chunks. It offers more predictable performance, and SQLite prefers that.
2025-03-05 13:57:08 +01:00
Ivan Enderlin 7f17b4be7b bench: Add a benchmark for the LinkedChunk with the EventCacheStore. 2025-03-05 13:57:08 +01:00
Benjamin Bouvier fa3a9d81e3 refactor(event cache): use Ruma's is_redacted() method instead of original_content()
This is cheaper, as it doesn't require cloning the content and
immediately throw it away. This method was introduced recently, thanks
to Kevin for it.
2025-03-05 12:14:04 +01:00
Ivan Enderlin 892c99f0f3 test(sqlite): Improve a test to check uniqueness constraint. 2025-03-05 12:02:30 +01:00
Ivan Enderlin 8d8846a259 chore(sdk): Remove EventsPostProcessing.
This patch removes the `EventsPostProcessing` type, it assumes
`with_events_muts` will always return events that will be post-process.
The case where `EventsPostProcessing::None` becomes a `vec![]`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 9d63af6271 chore(sdk): maybe_apply_new_redaction no longer takes a &RoomVersionId.
This patch updates `maybe_apply_new_redaction` to remove the first
`&RoomVersionId` argument. Indeed, due to the refactoring, it's now
possible for `maybe_apply_new_redaction` to read this value directly
from `Self::room_version`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 37ad82adfc doc(sdk): Add missing documentation. 2025-03-05 11:30:55 +01:00
Ivan Enderlin 57953b9ae9 chore(sdk): Make Clippy happy. 2025-03-05 11:30:55 +01:00
Ivan Enderlin 777fb920f6 fix(sdk): maybe_apply_new_redaction updates in-store events.
This patch updates `maybe_apply_new_redaction` so that it is able to
update/redact an event found in the store.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 05750e871b task(sdk): maybe_apply_new_redaction uses find_event.
This patch updates `maybe_apply_new_redaction` to use `find_event`, so
that the target event is looked up in memory or in the store.

The case where it is in the store is a simple `todo!()` for the moment.
I wanted to separate the update of the `maybe_apply_new_redaction`
signature from the `InStore` implementation. The method is now async and
returns a `Result`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 5a11b8b836 task(sdk): RoomEventCacheState::find_event returns the event location.
This patch introduces `EventLocation` to know if an event has been found
in the memory (in `RoomEvents`) or in the store (in `EventCacheStore`).

This is used by the `RoomEventCacheState::find_event`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 8a785ea855 task(sdk): Move maybe_apply_new_redaction from RoomEvents inside RoomEventCacheState.
This patch moves the `maybe_apply_new_redaction` method from
`RoomEvents` inside `RoomEventCacheState` so that it has an access
to the store (necessary for the next patch). This patch creates a new
`RoomEvents::replace_event_at` method, which is a thin wrapper around
`LinkedChunk::replace_item_at`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 1874a76f67 task(sdk): Rename a variable, and AllEventsCache only stores valid events.
This patch renames `sync_timeline_events` into `timeline_events`.
Moreover, this change has spotted a possible improvement
in `AllEventsCache` where it now receives events from
`collect_valid_and_duplicated_events`, which allows to only store valid
events in it.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 0b2b528962 task(sdk): Callback in with_events_mut returns an EventsPostProcessing.
This patch updates the callback passed to `with_events_mut`. It now
returns an `EventsPostProcessing` which can automatically run the, now
inlined, `on_new_events`.

This patch updates where the `RoomVersionId` is also stored. It's not
held by `RoomEventCacheState` instead of `RoomEventCacheInner`.
2025-03-05 11:30:55 +01:00
Ivan Enderlin 2036c3da9d doc(sdk): Fix documentation of with_events_mut. 2025-03-05 11:30:55 +01:00
Benjamin Bouvier 7694b016da chore: disable LTO on release builds
I have to disable LTO every time I'm building any final binary using the
SDK, because otherwise, the builds can take easily more than 10 minutes
to complete, killing iteration times, and making it almost impractical
to use the programs (benchmarks, or multiverse).

I think it should be the decision of the final embedder to enable or
disable LTO, and that for the purpose of our own binaries hosted in the
SDK repository, we don't need the absolute best performance (or, for the
sake of benchmarking, we can tweak the profiling profile).
2025-03-05 10:55:15 +01:00
Benjamin Bouvier 6fdd59157a fix(timeline): steal hiddden receipts from the previous item, when inserting in the middle 2025-03-05 09:42:14 +01:00
Benjamin Bouvier 0d7096fa94 test(timeline): add regression test for duplicate read receipts 2025-03-05 09:42:14 +01:00
Benjamin Bouvier a94dc4e89b chore(timeline): add more logs for read receipts 2025-03-05 09:42:14 +01:00
Benjamin Bouvier bf965b2a17 feat(timeline): add an invariant check that there's no duplicate read receipts 2025-03-05 09:42:14 +01:00
Benjamin Bouvier 9c87625910 chore(event cache): include the room id in the sending linked chunk updates to the store log 2025-03-05 08:56:37 +01:00
Benjamin Bouvier 3f1543504a test(timeline): add an equivalent test when storage's enabled 2025-03-05 08:56:37 +01:00
Benjamin Bouvier 3773968d19 fix(timeline): remove events when back-paginating too
The previous strategy was incorrect, see the new doc comment explaining
why with the example taken from the regression test.
2025-03-05 08:56:37 +01:00
Kévin Commaille d28d4ce799 test(timeline): add a regression test for the incorrect timeline ordering 2025-03-05 08:56:37 +01:00
Benjamin Bouvier bffb19b23a refactor!(sdk): bump the MSRV, yay for async closures 🥳 2025-03-04 18:10:59 +01:00
Benjamin Bouvier 6aea4c827a feat(ffi): allow setting the media retention policy from the FFI layer 2025-03-04 18:10:59 +01:00
Benjamin Bouvier ac3250c58b refactor(event cache): use u64 instead of usize in MediaCachePolicy
This is more predictible and we're still far from 128-bits wide cpu,
right? RIGHT?
2025-03-04 18:10:59 +01:00
Benjamin Bouvier 6fe0880e11 feat(ffi): add a method to clear all the non-critical caches of a client 2025-03-04 18:10:59 +01:00
Stefan Ceriu 78282bf1e1 chore(sdk-base): fix typos following typos crate bump to 1.30.0 2025-03-04 11:28:36 +02:00
dependabot[bot] 43d25127c3 chore(deps): bump crate-ci/typos from 1.29.7 to 1.30.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.29.7 to 1.30.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.29.7...v1.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 11:28:36 +02:00
bitfriend c33c61a256 feat(ci): Implement CI to detect long path in pushed commit 2025-03-03 16:57:36 +00:00
Benjamin Bouvier def4be5a9f refactor(event cache): make use of Chunk::num_items when deciding whether to drop a previous empty event chunk 2025-03-03 16:04:15 +01:00
Benjamin Bouvier 9bc0d8b0d9 refactor(event cache): rename Chunk::len to Chunk::num_items 2025-03-03 16:04:15 +01:00
Benjamin Bouvier 0924b2e343 refactor(event cache): get rid of EmptyChunkRule::Keep which is only used in testing
This isn't useful to keep, since it's only used in testing. Worst case,
we can revert this commit in the future.
2025-03-03 16:04:15 +01:00
Benjamin Bouvier 8b6e75980b refactor(event cache): don't keep an empty events chunk before a gap
The linked chunk always starts with an empty events chunk. If we receive
a gap from sync, then we will immediately push a gap chunk; in this
case, it might be better to replace the events chunk with a gap chunk.
This is equivalent to removing the empty events chunk, after pushing
back the first one (we can't do it before, otherwise we might get rid of
the only chunk in the linked chunk, which breaks the invariant that a
linked chunk is never empty).
2025-03-03 16:04:15 +01:00
dependabot[bot] 5fd0cb0ddb chore(deps): bump bnjbvr/cargo-machete from 0.7.1 to 0.8.0
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 0.7.1 to 0.8.0.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/v0.7.1...v0.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 15:38:57 +01:00
Benjamin Bouvier b5edc86a52 refactor(event cache): rename clear to clear_pending 2025-03-03 14:56:33 +01:00
Benjamin Bouvier d09655989d feat(event cache): don't include useless updates when clearing/resetting a linked chunk 2025-03-03 14:56:33 +01:00
Benjamin Bouvier 83415ac6ca refactor(event cache): clear all pending updates when resetting/shrinking a linked chunk 2025-03-03 14:56:33 +01:00
Kévin Commaille cc7fb63c6d refactor(sdk): Remove clone_request method
http::Request implements Clone since http 1.0.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-03-03 14:32:34 +01:00
Benjamin Bouvier f5195222a7 refactor(ffi): move the TimelineEventTypeFilter to timeline/configuration
Pure code motion, nothing else.
2025-03-03 12:40:54 +01:00
Benjamin Bouvier cecf15a34a refactor(ffi): unify a bit more Room::timeline_with_configuration and RoomListItem::init_timeline
The two last missing pieces will be the UTD hook and loading events from
the persistent storage.
2025-03-03 12:40:54 +01:00
Damir Jelić 95b53d7e01 chore: Tweak the weekly-report command to include PR numbers 2025-02-28 15:51:38 +01:00
Andy Balaam 8cd70854ba refactor(crypto): Keep a long-lived DecryptionRetryTask in TimelineController 2025-02-28 12:35:04 +00:00
Andy Balaam dbaa36ec3e refactor(timeline): Share should_retry logic between the two places we use it 2025-02-28 12:35:04 +00:00
Andy Balaam 8976233905 refactor(timeline): Move the code to find which events to redecrypt into the async task 2025-02-28 12:35:04 +00:00
Andy Balaam 82d47d800c refactor(timeline): Pass requests to retry decryption through a channel, instead of spawning a task directly 2025-02-28 12:35:04 +00:00
Andy Balaam e84ad97edf refactor(timeline): Adjust tests that retry decryption to wait with timeouts 2025-02-28 12:35:04 +00:00
Andy Balaam d447342cbd refactor(timeline): Split finding retry indices into its own function 2025-02-28 12:35:04 +00:00
Andy Balaam c74ecff3f0 refactor(timeline): Move finding retry indices into DecryptionRetryTask 2025-02-28 12:35:04 +00:00
Andy Balaam a0282ec71b refactor(timeline): Move the decryption retrying into a separate struct 2025-02-28 12:35:04 +00:00
Ivan Enderlin a67f9d5bbf chore(sdk): Log all updates. 2025-02-28 13:07:45 +01:00
Benjamin Bouvier f7297edd61 refactor(event cache): get rid of get_or_wait_for_token
Also the tests, they were not quite useful to port to the new mechanism
because they made little sense.
2025-02-28 13:00:35 +01:00
Benjamin Bouvier 87a6037924 refactor(event cache): consolidate logic around returning the previous gap token 2025-02-28 13:00:35 +01:00
Benjamin Bouvier ee710e34dd test(event cache): turn test into failing regression test 2025-02-28 13:00:35 +01:00
Benjamin Bouvier 55143e1790 refactor(event cache): call /messages directly in the room pagination
And don'y rely on the `Paginator`. This simplifies the code a bit,
avoids a few methods on the `Paginator`, and makes it more
straightforward the pagination happens.
2025-02-28 12:24:59 +01:00
Damir Jelić 7a0bf9b9b9 chore(sdk): Don't repeat a log line about the list of users for a /keys/query
The crypto crate already logs this, so no need to repeat the whole list
of users in the main crate.
2025-02-27 16:33:13 +01:00
Damir Jelić b422b93c78 chore(crypto): Lower a very noisy log line
This partially reverts: 66fcaeb2bad125dcdf884aaec6528633d2f1ec32
2025-02-27 16:33:13 +01:00
Benjamin Bouvier 4742aa298a fix(event cache): wait for the initial previous-batch token, if there wasn't any 2025-02-27 10:28:02 +01:00
Benjamin Bouvier f9f389d9ec chore(event cache): remove unused errors 2025-02-26 17:20:45 +01:00
Hanadi 7dba05f4c5 feat(sdk): Add Room::report_room
solves this https://github.com/matrix-org/matrix-rust-sdk/issues/4681

- add room report_room api from
https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidreport
- expose report_room on room ffi

---------

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2025-02-26 16:55:57 +01:00
Kévin Commaille f02a7d15ab test(sdk): Run integration tests for experimental-oidc feature too
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 16:08:00 +01:00
Kévin Commaille 54ab46dcb4 test(oidc): Use MatrixMockServer for cross-signing test
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 14:31:46 +01:00
Kévin Commaille 9b406cff87 test(oidc): Use mock server and client as much as possible
We keep the mock backend for endpoints that require an ID token for now,
as it would involve generating them on the fly.
And since support for ID tokens is going to be removed, it is not worth
it to implement that.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 14:31:46 +01:00
Kévin Commaille 5791ac9b76 test(oidc): Add an OauthMockServer and use it for qrcode login tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 14:31:46 +01:00
Kévin Commaille 6026b0c4b7 test(oidc): Use MatrixMockServer for qrcode login tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 14:31:46 +01:00
Kévin Commaille 52909b0eeb test(oidc): Simplify qrcode login tests
Since it only uses OAuth 2.0 now, we can remove the ID token and JWKS.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-26 14:31:46 +01:00
Benjamin Bouvier 1feb77bbef doc(event cache): tweak paginate_backwards_with_network doc comment 2025-02-26 14:09:08 +01:00
Benjamin Bouvier 2e1b051a4d refactor(event cache): split handling a successful network pagination into its own function 2025-02-26 14:09:08 +01:00
Benjamin Bouvier 15fd892b63 chore(event cache): remove the sync_ prefix from timeline_events_diff
And simplify pluralization, use more natural terms (new instead of
next), etc.
2025-02-26 14:09:08 +01:00
Benjamin Bouvier 4833403d65 refactor(event cache): simplify signature of RoomEventCacheState::with_events_mut 2025-02-26 14:09:08 +01:00
Benjamin Bouvier 061a2f739a refactor(event cache): handle pagination status in a single location 2025-02-26 14:09:08 +01:00
Benjamin Bouvier 86b5cb4dba refactor(event cache): split disk and network paginations into smaller functions 2025-02-26 14:09:08 +01:00
Benjamin Bouvier 74bc3dfb6e refactor(event cache): don't hold onto a live instance of the paginator in RoomEventCache
Instead of keeping state for the `Paginator` instance, we create one
when needs be, in the `run_backwards_impl` method, and initialize it
with a previous-batch token. This is simpler than keeping one alive, and
making sure that we reset it in the right places.
2025-02-26 14:09:08 +01:00
Benjamin Bouvier 7841ed8637 refactor(event cache): remove RoomPagination::hit_timeline_end()
The event cache doesn't use the paginator for forwards pagination, so it
doesn't make sense to expose this method. Moreover, the event cache
always listens to sync in real-time, so technically it's always hit the
timeline end.

As a proof of this, this method wasn't even tested.
2025-02-26 14:09:08 +01:00
Doug 19df945155 fix(ffi): Correctly indicate OIDC support when fetching metadata fails. 2025-02-25 19:24:07 +02:00
Damir Jelić 3e3bff76de fixup! feat(crypto): Add support for the shared_history flag defined in MSC3061 2025-02-25 16:52:23 +01:00
Damir Jelić ea073f55f0 doc(crypto): Document all the arguments of the InboundGroupSession::new method 2025-02-25 16:52:23 +01:00
Damir Jelić c1e28aa156 test(crypto): Add a snapshot test for the inbound group session pickle 2025-02-25 16:52:23 +01:00
Damir Jelić af62f09e37 test(crypto): Test that the shared history flag gets set when we ourselves crate a session 2025-02-25 16:52:23 +01:00
Damir Jelić 9a33385697 test(crypto): Test that the shared history flag gets set when creating sessions 2025-02-25 16:52:23 +01:00
Damir Jelić bfa89bc73f feat(crypto): Add support for the shared_history flag defined in MSC3061
This patch adds support for the `shared_history` flag from MSC3061 to
the `m.room_key` content, exported room keys, and backed-up room keys.

The flag is now persisted in our `InboundGroupSession`. Additionally,
when creating a new `InboundGroupSession`, we ensure the
`shared_history`  flag is set appropriately.

MSC3061: https://github.com/matrix-org/matrix-spec-proposals/pull/3061
2025-02-25 16:52:23 +01:00
Damir Jelić e1d05fa53c refactor(crypto): Use struct destructuring instead of separate field access in some places
In several places, we access almost all the fields of a struct to
create an `InboundGroupSession` from a pure data struct.

When new fields are added to these data structs, it's easy to
overlook updating the `InboundGroupSession` accordingly.

By using struct destructuring, we ensure that newly added fields  are
explicitly considered, making it harder to forget one of the newly added
fields.
2025-02-25 16:52:23 +01:00
Damir Jelić b0ccc94b26 refactor(crypto): Simplify some tests by using a session created from a helper function 2025-02-25 16:52:23 +01:00
Damir Jelić b2356a0232 doc(crypto): Improve the documentation for the encrypted Content serialization
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-02-25 15:58:42 +01:00
Damir Jelić 3bb883387e refactor(crypto): Use the DecryptedOlmV1Event type when encrypting to-device events
This ensures that new fields that are added to the
`m.olm.v1.curve25519-aes-sha` content need to be added in a single
place.
2025-02-25 15:58:42 +01:00
Damir Jelić 506a36b210 fix(crypto): Fix the serialization of DecryptedOlmV1Event 2025-02-25 15:58:42 +01:00
Damir Jelić 8c1966a237 refactor(crypto): Don't require event_type to return a static string 2025-02-25 15:58:42 +01:00
Kévin Commaille 09513eaa5e refactor(oidc): Only support authorization URL parameters defined in MSCs
`prompt=create` is defined in MSC2964, and
`login_hint=mxid:@user:server.name` is defined in MSC4198.
The other parameters came from OpenID Connect.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-25 15:34:36 +01:00
Kévin Commaille fda9177a70 refactor(oidc): Remove support for Pushed Authorization Requests
It is a small optimization which makes the URL smaller, but it is not
part of the next-gen auth MSCs and is not supported by the oauth2 crate,
so let's drop it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-25 15:34:36 +01:00
Benjamin Bouvier 21960a5ba2 chore(event cache): add more logs to the auto-shrinking mechanism 2025-02-25 14:13:33 +01:00
Benjamin Bouvier 0819ab1dad refactor(event cache): apply review comments of #4708 2025-02-25 12:28:28 +01:00
Ivan Enderlin 475ad79360 fix(sdk): RoomEventCache::event looks inside the store.
This patch fixes `RoomEventCache::event` to look inside `RoomEvents` but
also inside the `EventCacheStore` to look for an event. It ultimately
fallbacks to `AllEventsCache` because we can't get rid of it for the
moment.
2025-02-25 12:07:17 +01:00
Ivan Enderlin 7b52306ff2 feat(base): Add EventCacheStore::find_event.
This patch adds the method `find_event` on the `EventCacheStore` trait.
It helps to find a single event from the store.
2025-02-25 12:07:17 +01:00
Benjamin Bouvier e5f6d026ff ci: use an hardcoded version of cargo-machete in CI (#4710)
Should resolve the CI issues around cargo-machete.

See also:
https://github.com/bnjbvr/cargo-machete/issues/156#issuecomment-2681308436
2025-02-25 11:35:18 +01:00
Benjamin Bouvier 5dd5710758 feat(event cache): auto-shrink a room event cache's chunk after all listeners are left 2025-02-24 17:40:50 +01:00
Ivan Enderlin 37b62dfed1 test(sdk): Add a big test for a deduplication + event removals.
This patch adds a test for deduplication that covers unloaded and loaded
chunk with event removals in both, with a finaly backwards pagination.
Yummy.
2025-02-24 17:37:47 +01:00
Ivan Enderlin d21a4152de chore(sdk): Code cleanup.
This patch puts the `Ok` outside the `match` for a better ergonomics.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 8c2dcd7b5d task(sdk): Make the code more robust around event removals.
This patch makes the code more robust around event removals. Sorting
events by their position is no longer done in the `Deduplicator` but in
a new `RoomEventCacheState::remove_events` method, which removes events
in the store and in the `RoomEvents`. This method is responsible to sort
events, this stuff is less fragile like so.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 019b4a20f6 chore(common): Rename EmptyChunk to EmptyChunkRule.
This patch renames `EmptyChunk` into `EmptyChunkRule`. Name suggested by
@stefanceriu, it makes a lot more sense, thanks!
2025-02-24 17:37:47 +01:00
Ivan Enderlin 30a9a972ce tes(sdk): Deduplicator dispatches duplicated events in memory vs in store.
This patch tests that `Deduplicator` dispatches duplicated events in the
correct field of `DeduplicationOutcome`.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 22ba1684b2 chore(sdk): Rename a test helper and some variables.
Nothing fancy here. Just regular chore tasks.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 0b12ec2b38 test(sdk): Deduplicator excludes invalid events.
This patch adds a test ensuring that `Deduplicator` excludes invalid
events, i.e. event with no ID.
2025-02-24 17:37:47 +01:00
Ivan Enderlin a71f5bf21f test(sdk): Test Deduplicator filters events in the input.
This patch adds a test ensuring that `Deduplicator` is able to find
duplicates in its own inputs.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 9bd7cfda5f test(sdk): Rename a test. 2025-02-24 17:37:47 +01:00
Ivan Enderlin c1a13f7f98 test(sdk): Test sort_events_by_position_descending.
This patch adds a test for `sort_events_by_position_descending`. It
also updates this function so that events are sorted by their chunk
identifier from newest to oldest, it makes no difference but it matches
the order of the position indices too. Everything “dimension” is
descending.
2025-02-24 17:37:47 +01:00
Ivan Enderlin a362584bb3 task(sdk): Use DeduplicationOutcome to remove events in their correct place.
This patch uses `DeduplicationOutcome` to remove events either in
memory, or in the store, when required. The `remove_events_by_id` method
has been renamed `remove_events_by_position`.
2025-02-24 17:37:47 +01:00
Ivan Enderlin f9ce7628ff task(sdk): Redesign Deduplicator::filter_duplicate_events.
This patch redesigns `Deduplicator::filter_duplicate_events`.

First off, `filter_duplicate_events` does remove events with no valid
ID. At the same time, it removes duplicate events within the new events
(`events`). This check was done in the `BloomFilterDeduplicator` but
not in the `StoreDeduplicator`. Now it's done at the front of these
implementations, directly inside `Deduplicator`.

Second, this patch introduces `DeduplicationOutcome` to replace the
return type `(Vec<Event>, Vec<OwnedEventId>)`, especially because
now it would have become `(Vec<Event>, Vec<(OwnedEventId, Position)>,
Vec<(OwnedEventId, Position)>)`. Why?

1. Because the positions of the duplicated events are returned,
2. We differentiate between in-memory vs. in-store duplicated events.

Third, now there are positions associated to duplicated events, events
must be sorted. It's the role of `sort_events_by_position_descending`.

This way, `DeduplicatorOutcome` brings guarantees and less checks are
required.
2025-02-24 17:37:47 +01:00
Ivan Enderlin 43c066e837 task(base): EventCacheStore::filter_duplicated_events returns Position.
This patch changes `EventCacheStore::filter_duplicated_events` to return
the `Position` of the duplicated event.
2025-02-24 17:37:47 +01:00
Benjamin Bouvier f3f37a33fd fix(event cache): override reached_start when there's a mismatch between network and disk
It could be that we have a mismatch between network and disk, after
running a back-pagination:

- network indicates start of the timeline, aka there's no previous-batch
token
- but in the persisted storage, we do have an initial empty events chunk

Because of this, we could have weird transitions from "I've reached the
start of the room" to "I haven't actually reached it", if calling the
`run_backwards()` method manually.

This patch rewrites the logic when returning `reached_start`, so that
it's more precise:

- when reloading an events chunk from disk, rely on the previous chunk
property to indicate whether we've reached the start of the timeline,
thus avoiding unnecessary calls to back-paginations.
- after resolving a gap via the network, override the result of
`reached_start` with a boolean that indicates 1. there are no more gaps
and 2. there's no previous chunk (actual previous or lazily-loaded).

In the future, we should consider NOT having empty events chunks, if we
can.
2025-02-24 14:47:21 +01:00
Benjamin Bouvier 39c6481f96 feat(event cache): include the lazy previous chunk in the debug string, if available 2025-02-24 14:47:21 +01:00
Benjamin Bouvier 66b9d334ef feat(event cache): shrink the linked chunk upon gappy syncs 2025-02-24 14:47:21 +01:00
Benjamin Bouvier e64cb2c4f1 feat(event cache): implement RoomEventCacheState::shrink_to_last_chunk 2025-02-24 14:47:21 +01:00
Benjamin Bouvier 4f47868930 feat(linked chunk): allow replacing a linked chunk's content with a raw chunk 2025-02-24 14:47:21 +01:00
Benjamin Bouvier 4c115b6ad5 feat(event cache): don't store a gap if we've deduplicated all events during sync 2025-02-24 14:47:21 +01:00
Benjamin Bouvier 242a1047bd doc(event cache): clarify that RoomEvents::updates() is only for storage updates
And rename it accordingly to `RoomEvents::store_updates`.

Note: no changelog, because this is an internal API only.
2025-02-24 14:47:21 +01:00
Kévin Commaille 2f3cab431f chore: Add changelog for Oidc::logout
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 14:19:48 +01:00
Kévin Commaille 55f514897b refactor(oidc): Only revoke one token for logout
The server is supposed to revoke any token associated with the token that we revoke.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 14:19:48 +01:00
Kévin Commaille d4b92de8e4 refactor(oidc): Remove support for OIDC RP-Initiated logout
Token revocation was split out from MSC2964 to MSC4254, and RP-Initiated
logout is now mentioned only as an alternative.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 14:19:48 +01:00
Kévin Commaille 25d39997a4 chore: Add changelog for moving qrcode module
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 13:39:23 +01:00
Kévin Commaille 254ce8923b refactor(oidc): Use OidcBackend with LoginWithQrCode
Will allow to share code when the backend is switched to the oauth2 crate too.

It will also allow to expose the device authorization grant directly in Oidc, if necessary.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 13:39:23 +01:00
Kévin Commaille 0a4db305b9 refactor(oidc): Move qrcode module inside oidc
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-24 13:39:23 +01:00
Benjamin Bouvier 90ac2181e9 test: rename some MatrixMockServer helpers functions to make it clear they're matchers
The `.from()`, `.with_delay()` and `.limit()` functions are not very
explicit about what they did, and the `with_delay` one in particular led
me to think that it would introduce a delay *in the response*, while it
indicated a delay was expected as part of the matched URL.

Instead, this patch proposes to prefix all matchers with `match_`, to
make it clearer that… they will match the incoming query: match_from,
match_delay, match_limit.

Thanks to this change, the `RoomMessagesResponseTemplate` can be renamed
from `delayed` to `with_delay`, which was my original intent, before I
noticed another `with_delay` with totally different semantics.
2025-02-24 11:09:25 +01:00
Ivan Enderlin bdf5fad992 chore(common): Remove LinkedChunkBuilderTest.
This patch removes `LinkedChunkBuilderTest` and updates tests
accordingly.
2025-02-21 12:06:08 +01:00
Ivan Enderlin 05be62183a task: Remove all usages of LinkedChunkBuilderTest.
This patch replaces all usages of `LinkedChunkBuilderTest` by
`from_all_chunks`.
2025-02-21 12:06:08 +01:00
Ivan Enderlin d545419684 test(common): Add lazy_loader::from_all_chunks.
This patch adds the new `from_all_chunks` function in the
`linked_chunk::lazy_loader` module. It is only used for testing
purposes. It aims at replacing `LinkedChunkBuilderTest` (see next
patches). Why? Because `from_all_chunks` uses `from_last_chunk` and
`insert_new_first_chunk`: if `from_all_chunks` is able to find all
errors that `LinkedChunkBuilderTest` finds, it's a bingo. Transitively,
it proves that `from_last_chunk` and `insert_new_first_chunk` are
correct!
2025-02-21 12:06:08 +01:00
Jonas Platte f900db49dd feat(sdk): Re-export the base crate's store module
Fixes a broken intra-doc link.
2025-02-21 09:36:50 +01:00
Jonas Platte 1373f99288 refactor: Use NonZeroUsize::new + unwrap in const contexts
Make clippy happy.
2025-02-21 09:36:50 +01:00
Jonas Platte f56bc4c0d6 chore: Bump nightly 2025-02-21 09:36:50 +01:00
Benjamin Bouvier 60efcbc55d refactor(event cache): use Option<&mut> in Chunk::insert_before 2025-02-20 16:28:22 +01:00
Benjamin Bouvier 30589ca899 refactor(event cache): use Option<&mut> in Chunk::unlink 2025-02-20 16:28:22 +01:00
Damir Jelić 61fa339163 refactor(crypto): Add a constructor to create an InboundGroupSession from a m.room_key event 2025-02-20 12:28:45 +01:00
Damir Jelić 3f5efc1ff6 docs(crypto): Update some docs for the InboundGroupSession 2025-02-20 12:28:45 +01:00
Ivan Enderlin 23f72ba15f test(common): Test ability for AsVector to generate several VectorDiff::Remove.
This patch adds a test ensuring that `AsVector` generates the correct
`VectorDiff::Remove` when a non-empty chunk is removed.
2025-02-20 10:52:59 +01:00
Ivan Enderlin a25acf7e62 feat(common): Update::RemoveChunk emits VectorDiff::Remove.
This patch updates `Update::RemoveChunk` to emit `VectorDiff::Remove`.
Until now, `RemoveChunk` was expecting the chunk to be
empty, because it is how it is used so far. However, with
https://github.com/matrix-org/matrix-rust-sdk/pull/4694, it can change
rapidly.
2025-02-20 10:52:59 +01:00
Benjamin Bouvier c3fc310f29 refactor(event cache): simplify back-pagination 2025-02-20 10:51:06 +01:00
Benjamin Bouvier b9c7ffe7c3 doc(timeline): tweak comment in pagination to explain why it's correct 2025-02-20 10:51:06 +01:00
Benjamin Bouvier 017a947fc1 doc(timeline): fix a broken link to all_remote_events 2025-02-20 10:01:57 +01:00
Benjamin Bouvier 5c57631a6c refactor(event cache): simplify flow when deciding to resolve a gap
The code before this patch was doing this:

- look if there's any prev-batch token available right now, aka look if
there's a gap in the in-memory linked chunk
- look at the first chunk; if it's a gap, return to the caller so it
resolves it

The check is done twice at two different levels, which is confusing.
Instead, this patch rewrites it so that the chunk is done only in
`load_more_event_backwards()`.

Note this is also correct for the case storage is disabled; in this
case, we early return and always try to resolve the gap anyways.
2025-02-19 15:39:58 +01:00
Ivan Enderlin 3495cab7ad refactor(common): builder::LinkedChunkBuilder::* becomes lazy_loader::*.
This patch renames the `builder` module to `lazy_loader`.
The `LinkedChunkBuilder`'s methods are now functions.
The `LinkedChunkBuilder` struct is removed. Finally,
`LinkedChunkBuilderError` is renamed `LazyLoaderError`.

The `LinkedChunkBuilderTest` struct is kept for the moment. It's going
to be replaced soon.
2025-02-19 14:38:56 +01:00
bitfriend 7a06bdb695 Rename snapshots to reduce filename length (#4625) 2025-02-19 13:29:51 +00:00
Ivan Enderlin 6c57003d17 feat(sqlite) Add an index on events.event_id and .room_id.
This patch adds an index on `events.event_id` and on `events.room_id`
so that queries on this column are faster. It mostly happens for the
`Deduplicator`, which runs for every backwards pagination or sync.

This patch also updates the query in `filter_duplicated_events` to
sort event by their `chunk_id` and `position` so that the results are
constant, it helps when testing.
2025-02-19 11:50:23 +01:00
Kévin Commaille 2eb2ae7959 refactor(oidc): Use the GET /auth_metadata Matrix endpoint (#4673)
This is the method to get the server metadata in the latest draft of
[MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965).

We still keep the old behavior with `GET /auth_issuer` as fallback for
now because it has wider server support.

There are some pre-main commit cleanups to simplify the main commit.
This can be reviewed commit by commit.

The changes were tested with the oidc_cli example on beta.matrix.org.

Closes #4550.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-18 17:41:48 +01:00
Ivan Enderlin a055aa3e57 chore(sdk): Fix comments and rename variables. 2025-02-18 17:13:12 +01:00
Ivan Enderlin 5c7a733f49 task(common): LinkedChunkBuilder detects cycles. 2025-02-18 17:13:12 +01:00
Ivan Enderlin 00ae386b74 test(sdk): Test RoomEvents::debug_string.
This patch moves `chunk_debug_string` from `rooms/mod.rs` to
`rooms/event.rs`. In addition, it restores (and rewrites) a test,
initially for `chunk_debug_string`, now for `RoomEvents::debug_string`
whichh is the public API.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 9ad7ca8f11 test(sdk): Write a full integration test for the event cache lazy-loading. 2025-02-18 17:13:12 +01:00
Ivan Enderlin a43ce05200 task(sdk): LinkedChunkBuider::load_previous_chunk supports lazy_previous.
This patch installs `lazy_previous` in the `LinkedChunkBuilder`.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 7d4dfb5c2d fix(sdk): Change semantics about duplicated events for a backwards pagination.
This patch changes the semantics regarding what to do in case of
duplicated events received during a backwards pagination.

Previously, the strategy for both sync and backwards pagination was
the same: With the new received events, when duplicated events are
detected, the old events are removed and the new ones are kept.

The strategy is reversed for backwards pagination: the old events are
always kept, and the duplicated events are removed from the new events.

The rest of the patch is about removing dead code because of this
change.
2025-02-18 17:13:12 +01:00
Ivan Enderlin d9e9006e61 feat(sdk): event_cache::Pagination::run_backwards paginates in the event cache.
This patch updates `Pagination::run_backwards_impl` to paginate in the
event cache. The flow is now as follow:

- backwards pagination tries to load and to insert a new previous chunk
   from the store
  - if the new chunk contains events, they are returned, pagination done
  - if the new chunk is a gap, the flow continues
- (as previously) check for a prev batch token (it exists in the newly
  inserted gap)
- (as previously) run a network request, replace the gap by the new
  events
- etc.

The new part is to load and to insert a new previous chunk. The rest
is stays the same. The code has been moved in code to keep the lock
releases happy and to clarify the code.
2025-02-18 17:13:12 +01:00
Ivan Enderlin bfec34db20 task(sdk) Add RoomEventCacheState::load_more_events_backwards.
This patch adds the `RoomEventCacheState::load_more_events_backwards`
method to load a new chunk and to insert it at the beginning of the
`LinkedChunk`.

It uses the new `EventCacheStore::load_previous_chunk` method, along
with the new `LinkedChunkBuilder::insert_new_first_chunk` method.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 0aae72c161 feat(sdk): The EventCache loads only the last chunk when initialised.
This patch updates `RoomEventCacheState::new` to load a single chunks
of events instead of all events. It solves bugs where all events were
loaded, while removing the gaps in between, thus the `Timeline` wasn't
able to load the missing events to fill the gaps.
2025-02-18 17:13:12 +01:00
Ivan Enderlin fbd8b9c816 chore(sdk): Remove useless indentations. 2025-02-18 17:13:12 +01:00
Ivan Enderlin d6120a5985 doc(sdk): Events from BackPaginationOutcome are deduplicated…
This patch removes a `TODO` in `BackPaginationOutcome`.
Events it contains are deduplicated by the `EventCache` (see
`event_cache::Deduplicator`) when inserted inside `RoomEventCache`.
2025-02-18 17:13:12 +01:00
Ivan Enderlin a8f7939126 doc(ui): Fix a typo. 2025-02-18 17:13:12 +01:00
Ivan Enderlin 217429c3fe test(common): Add tests for LinkedChunkBuilder::insert_new_first_chunk.
This patch adds tests for the
`LinkedChunkBuilder::insert_new_first_chunk` method.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 716958bb86 test(common): Add tests for LinkedChunkBuilder::from_last_chunk.
This patch adds tests for the `LinkedChunkBuilder::from_last_chunk`
method.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 1319558eb6 test(base): Add test_linked_chunk_incremental_loading.
This patch adds a test for all event cache store
implementations that tests a linked chunk incremental
loading, i.e. the `EventCacheStore::load_last_chunk` and
`EventCacheStore::load_previous_chunk` methods.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 096d478593 chore(base): Split LinkedChunkBuilder and create LinkedChunkBuilderTest.
Most of the methods on `LinkedChunkBuilder` are now only used
for testing. This patch splits `LinkedChunkBuilder` and creates
`LinkedChunkBuilderTest`. This new type is part of the public
API because it's used in other crates, but it's hidden from the
documentation.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 8fcd5a91c4 test(sdk): Update tests.
This patch updates 3 tests that were expected to read all events: a
pagination is now necessary.
2025-02-18 17:13:12 +01:00
Ivan Enderlin 155042e46c task(common): LinkedChunkBuilder gains from_last_chunk and insert_new_first_chunk.
This patch adds two methods on `LinkedChunkBuilder`: `from_last_chunk`
to build a new `LinkedChunk` with a single provided chunk, and
`insert_new_first_chunk` to insert a new chunk at the beginning of the
provided `LinkedChunk`.
2025-02-18 17:13:12 +01:00
Ivan Enderlin e03d40e946 chore(sdk): Rename RoomEvents::with_initial_chunks.
This patch renames `RoomEvents::with_initial_chunks` to
`with_initial_linked_chunk`. It avoids a confusion between several
chunks, like `RawChunk`s, and `LinkedChunk` which represents several
`Chunk`s.
2025-02-18 17:13:12 +01:00
Benjamin Bouvier 2671769d9f fix(compilation): fix benchmark compilation on non-linux platforms 2025-02-18 13:58:23 +01:00
Jorge Martín 8d9d83f15f feat(ffi): add history_visibility_override param to the create room fn 2025-02-18 13:08:02 +01:00
Ivan Enderlin 6bc9dc5c6a test(sdk): Improve a test.
This patch adds an event to be sure it is removed later during the error
recovery.
2025-02-18 11:38:24 +01:00
Ivan Enderlin d6566484a1 doc(sqlite): Fix typos in comments. 2025-02-18 11:38:24 +01:00
Ivan Enderlin 0e4d8ec62f feat(sqlite): Detect cycles when loading last chunk of LinkedChunk.
This patch updates `SqliteEventCacheStore::load_last_chunk` to detect
cycle for the last chunk only.
2025-02-18 11:38:24 +01:00
Ivan Enderlin f9c6f897c8 feat(common): Detect cycles when loading last chunk of LinkedChunk.
This patch updates `RelationalLinkedChunk::load_last_chunk` to detect
cycle for the last chunk only.
2025-02-18 11:38:24 +01:00
Ivan Enderlin 7252a685a6 test(common): Test RelationalLinkedChunk::load_last_chunk and load_previous_chunk.
This patch adds tests for the `load_last_chunk` and
`load_previous_chunk` methods on `RelationalLinkedChunk`.
2025-02-18 11:38:24 +01:00
Ivan Enderlin bed4d5034e test(sqlite): Test SqliteEventCacheStore::load_last_chunk and load_previous_chunk.
This patch adds tests for the `SqliteEventCacheStore::load_last_chunk`
and `load_previous_chunk` methods.
2025-02-18 11:38:24 +01:00
Ivan Enderlin e2a2f32e82 task(sqlite): Implement load_last_chunk and last_previous_chunk.
This patch replaces `todo!()` by real implementations for the
`load_last_chunk` and `last_previous_chunk` methods.
2025-02-18 11:38:24 +01:00
Ivan Enderlin 334c66b0a0 task(base): Update EventCacheStore to add load_last_chunk and load_previous_chunk.
This patch update the `EventCacheStore` trait to:

1. rename `reload_linked_chunk` into `load_all_chunks` and put this
   method behind `#[cfg(test)]` so that it is removed from the public API,
2. add `load_last_chunk`,
3. add `load_previous_chunk`.

These 2 new methods are implemented inside the `MemoryStore` (with its
real implementation in the `RelationalLinkedChunk`), but `todo!()` are
added for the SQLite implementation.
2025-02-18 11:38:24 +01:00
Kévin Commaille ca392b08c9 chore: Add changelog for UserSession deserialization
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-18 11:32:44 +01:00
Kévin Commaille e9a34f6359 refactor(oidc): Remove support for deserializing previous UserSession format
The format changed 10 months ago and since it contains the tokens, it should have be reserialized already in that time.

Afaict EX clients do not serialize that type, the bindings have their own `Session` type for that.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-18 11:32:44 +01:00
dependabot[bot] 6411d27096 chore(deps): bump crate-ci/typos from 1.29.5 to 1.29.7
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.29.5 to 1.29.7.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.29.5...v1.29.7)

---
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>
2025-02-17 16:47:15 +01:00
Ivan Enderlin 07f0017d30 feat(common): Introduce Chunk::lazy_previous in the LinkedChunk.
This patch introduces `Chunk::lazy_previous` which is a key feature to
support lazy-loading of a `LinkedChunk`. When a chunk is loaded, if it
is the first, it keeps in memory whether it has a previous chunk or not.
Thus, it is possible to insert new chunk in front of the `LinkedChunk`,
and `Update`s will correctly continue to link chunks between them (with
`NewItemsChunk` and `NewGapChunk`).

Example, imagine the following chunks: [0] <-> [1] <-> [2]. If [2] is
the only one being loaded. Then its previous chunk, [1], is loaded from
the store (because [2]'s previous is [1] in the store). Then [1] is
replaced by [3] and [4]. We get this: [4] <-> [3] <-> [1] <-> [2]. If
the `Update::New*Chunk` for [4] doesn't contain a `previous`, the store
is out of sync: in the store, [4] has no previous, but [0] still has [1]
for its `next`.

With this `lazy_previous`, the links are correctly computed.
2025-02-17 14:09:17 +01:00
Benjamin Bouvier 59f9d12da5 perf(timeline): make replacing replies much faster by indexing replies 2025-02-17 12:24:19 +01:00
Kévin Commaille 1c114978e4 refactor(oidc): Remove method to authorize arbitrary scope
Only the scopes necessary during login are specified in MSC2967 now.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-17 12:02:43 +01:00
Stefan Ceriu 629421214f change(crypto): have the RoomIdentityProvider return processing changes when identities transition to IdentityState::Verified too (#4670)
While implementing user verification on Element X I realized that the
`IdentityStatusChanges` stream does not notify us when an user becomes
`Verified`, which is a shame as it's perfect for powering app updates
after verifying users (i.e. all the decorations we show throughout the
app: room header, room details, room member list, room member profile
etc.)

Had a look at the existing code and, while that seems completely
intentional, there is no reason why we can't expand its purview.
2025-02-17 11:48:35 +02:00
Jorge Martín 97d772dd05 test: reverse the logic behind test_focused_timeline_reacts so it checks no reactions are received 2025-02-17 09:47:23 +01:00
Jorge Martín f28c64ba21 test: modify pinned event tests to make sure we don't add events from paginations or syncs to the timeline 2025-02-17 09:47:23 +01:00
Jorge Martín 20dd15e256 fix(timeline): Don't add events to the pinned events timeline when we receive them on paginations/syncs 2025-02-17 09:47:23 +01:00
Ivan Enderlin f33d10468d refactor(base): Remove the sliding_sync::http re-export.
This patch removes the `pub use ruma::api::client::sync::sync_events::v5
as http` re-export in `matrix_sdk_base::sliding_sync`.
2025-02-14 14:00:39 +01:00
Ivan Enderlin d3b3b4db10 chore(base): Remove the sliding_sync module.
This patch inlines `sliding_sync::http` inside `sliding_sync`. Then, the
`sliding_sync/mod.rs` file is renamed to `sliding_sync.rs`.
2025-02-14 14:00:39 +01:00
Ivan Enderlin 38e28643f1 fix: Remove support for MSC3575. 2025-02-14 14:00:39 +01:00
Valere 9de6d28270 logging(crypto): Add more logs when identity or devices change 2025-02-14 10:43:42 +01:00
Benjamin Bouvier 9f47201bab bench: add a profiling profile that doesn't enable LTO for quick rebuilds 2025-02-14 09:02:38 +01:00
Benjamin Bouvier 0b7140c123 bench: add a benchmark to measure how long it takes to fill a timeline with lots of initial items 2025-02-14 09:02:38 +01:00
Damir Jelić 28a4918ff6 chore(test): Increase the timeout for the sync service offline mode test 2025-02-13 17:00:42 +01:00
Benjamin Bouvier 534cd599f4 doc: remove internal links to macro
Those worked fine until now, but it seems they started to fail after
including the `testing` feature in the benchmark repository. Oh well.
2025-02-13 16:02:10 +01:00
Benjamin Bouvier 910a5ce90a ci: add a task to compile benchmarks
This adds a task to compile the benchmarks in CI, without running them,
and with the lowest level of optimization that's available (the `dev`
profile).
2025-02-13 16:02:10 +01:00
Benjamin Bouvier dadd01a4ea chore: fix benchmarks and use the MatrixMockServer in there too 2025-02-13 16:02:10 +01:00
Damir Jelić c4a9059814 Merge branch 'oidc_e2e' 2025-02-13 15:59:20 +01:00
Damir Jelić 51a1cd3c67 Merge pull request #4604 from zecakeh/qr-login-oauth2
refactor(auth-qrcode): Use oauth2 crate instead of openidconnect
2025-02-13 15:42:51 +01:00
Kévin Commaille c6d2ab4637 chore: Fix changelog location
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-13 15:26:11 +01:00
Kévin Commaille c6c7307d6e Merge branch 'main' into qr-login-oauth2 2025-02-13 15:20:10 +01:00
Kévin Commaille 9c9944aa0c chore: Update changelog for TLS 1.2
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-13 14:21:25 +01:00
Kévin Commaille b311197d41 feat(sdk): Only allow TLS 1.2 or newer
As recommended by BCP 195.

It shouldn't be a problem with rustls that only supports TLS 1.2 and 1.3, but with native-tls it depends on the implementation.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-13 14:21:25 +01:00
Benjamin Bouvier 1068d88c3e fix(event cache store): shortcut when there's no duplicate events to check at all
Otherwise this causes a panic when repeating the events variable, when
generating the SQL query below.
2025-02-13 13:43:49 +01:00
Damir Jelić 861078a95e feat: Add a memoized variant of Oidc::fetch_account_management_url 2025-02-13 12:32:42 +01:00
Damir Jelić aa9aef44f7 refactor: Rename Oidc::account_management_url to fetch_account_management_url 2025-02-13 12:32:42 +01:00
Damir Jelić f2ad11a56a refactor(client): Create a common struct for client caches 2025-02-13 12:32:42 +01:00
Damir Jelić 12c327292f feat(common): Add a simple TTL cache implementation 2025-02-13 12:32:42 +01:00
Kévin Commaille 31e78c2a1b refactor(oidc): Only support public clients (#4634)
This should be the most common case, and is already the only case
supported by the higher level APIs like `url_for_oidc` and
`login_with_qr_code`. It simplifies the API because we can call
`restore_registered_client` directly from `register_client`, which was a
TODO.

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

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-13 11:40:17 +01:00
Benjamin Bouvier 8a64922130 test(event cache): address review comments and add a test for storage deduplication 2025-02-13 10:53:20 +01:00
Benjamin Bouvier 2ae142f257 refactor(event cache): get rid of a few generics
The YAGNI crew strikes again.
2025-02-13 10:53:20 +01:00
Benjamin Bouvier faa0e6e554 feat(event cache): allow using the bloom filter OR the store to deduplicate events 2025-02-13 10:53:20 +01:00
Benjamin Bouvier b95cf79a6d refactor(event cache): move the gist of deduplication into BloomFilterDeduplicator 2025-02-13 10:53:20 +01:00
Benjamin Bouvier 28cd8beb77 refactor(event cache): rename Deduplicator to BloomFilterDeduplicator 2025-02-13 10:53:20 +01:00
Ivan Enderlin 1918bd5f6b chore(base): Rename variables. 2025-02-12 16:50:14 +01:00
Ivan Enderlin d45addee10 feat(base): Add EventCacheStore::filter_duplicated_events.
This patch adds and implements the
`EventCacheStore::filter_duplicated_events` method. It is implemented on
the `MemoryStore` and the `SqliteEventCacheStore`.

This method remove the unique events and reutrn the duplicated events.
2025-02-12 16:50:14 +01:00
Ivan Enderlin ed16e91aed fix: RoomEventCache::subscribe is now infallible.
This patch updates `RoomEventCache::subscribe` to be infallible. This
method wasn't able to return something else than an `Ok`. The return
type has been updated from `Result<T>` to `T`.
2025-02-12 16:35:03 +01:00
Ivan Enderlin 714caae545 chore(sqlite): Remove a useless indentation.
This patch removes a useless indentation.
2025-02-12 16:13:40 +01:00
Andy Balaam 25bb607b27 feat(crypto): Allow fetching encryption info using session ID 2025-02-12 14:00:14 +00:00
Andy Balaam c9a6ae9549 fix(tests): Prevent test flake by using different names for the test stores 2025-02-12 13:45:20 +00:00
Benjamin Bouvier 58099fd6b5 test(timeline): add new tests for fetching replies that are UTD or sticker or polls 2025-02-12 14:28:17 +01:00
Benjamin Bouvier c5856a33f0 test(timeline): rewrite and comment fetch_details() test 2025-02-12 14:28:17 +01:00
Benjamin Bouvier a5f115f21f test(timeline): use the MatrixMockServer in integration/timeline/replies 2025-02-12 14:28:17 +01:00
Benjamin Bouvier ddd84e231b feat(timeline): support more timeline item content kinds in replied-to details 2025-02-12 14:28:17 +01:00
Benjamin Bouvier 51e9df87f5 chore(timeline): add some logs when fetching a reply details 2025-02-12 14:28:17 +01:00
Benjamin Bouvier aec4d37a2e refactor(event cache): fold all_deduplicated computation into collect_valid_and_duplicated_events 2025-02-12 14:10:14 +01:00
Benjamin Bouvier ceafc2155f refactor(event cache): move the Deduplicator instance to RoomEventCacheState
The `RoomEvents` doesn't hold the `Deduplicator` instance now, it's the
role of the `RoomEventCacheState`. This slightly simplifies the code, in
a few cases.
2025-02-12 14:10:14 +01:00
Benjamin Bouvier 4a37d6ebe2 refactor(event cache): move remove_events_and_update_insert_position to the public impl block
Only code motion.
2025-02-12 14:10:14 +01:00
Benjamin Bouvier 10095f8627 refactor(event cache): rename RoomEvents::remove_events to remove_events_by_id
And move it to the public implementation.

Only code motion and renaming, no changes in functionality.
2025-02-12 14:10:14 +01:00
Benjamin Bouvier 84bb1ab595 refactor(event cache): extract deduplication outside the RoomEvents events methods 2025-02-12 14:10:14 +01:00
Ivan Enderlin fce7999890 chore(common) Split the test_insert_items_at tests.
This patch splits the `test_insert_items_at` test into 5 tests.
2025-02-11 17:32:20 +01:00
Ivan Enderlin 10b72ef4b4 test(common): Update test_replace_item.
This patch updates the `test_replace_item` test to ensure
`Update::ReplaceItem` is correct.
2025-02-11 17:32:20 +01:00
Ivan Enderlin bfbb354c39 chore(common): Split a test into 3 tests.
This patch splits the `test_replace_at` test into 3 smaller tests.
2025-02-11 17:32:20 +01:00
Ivan Enderlin 9db137af44 refactor(common): LinkedChunk can start by a gap.
This patch removes the invariant stating that a `LinkedChunk` must start
by a chunk of type items. This has never been really useful but it's now
annoying to have this (with iterative loading of a `LinkedChunk` via the
`EventCache`, it's now possible to get a gap as the first chunk). Let's
remove this invariant.
2025-02-11 17:32:20 +01:00
Stefan Ceriu 2999d10fb9 fix(ffi): check that our own device is cross-signed before responding to incoming user verification requests 2025-02-11 16:39:26 +02:00
Kévin Commaille 654885a925 fix(ui): Demote aggregation target not found log to TRACE level
We encountered this warning a lot in the logs after upgrading the SDK today.

My understanding is that this path is expected if the event is not yet in the timeline, so it's nothing to warn about.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-11 15:37:35 +01:00
Damir Jelić 8042abe5f5 fix(recovery): Delete the known secrets from 4s when disabling recovery 2025-02-11 15:28:33 +01:00
Kévin Commaille 65ee18a52d feat(sqlite): Run VACUUM operation after removing a room
A room can be associated to a lot of data, depending on the number of members in the room.
So freeing space on the filesystem should be worth it in some cases.

An (extreme) example: I have a test account that is in ~60 rooms, a few of those big public rooms, including Matrix HQ. The size of the matrix-sdk-state.sqlite3 file is 542 MB. Using this PR and leaving, then forgetting Matrix HQ brings the DB down to 255 MB.
2025-02-11 14:13:25 +00:00
Benjamin Bouvier 69588d5266 test(timeline): use a builder pattern to create a TestTimeline 2025-02-11 11:26:47 +01:00
Benjamin Bouvier 7b77b19bc0 refactor(timeline): get rid of the optional in is_room_encrypted field
The `EventTimelineItem` would get from a bool to an `Option<bool>`. If
the metadata's `is_room_encrypted` was set to `None`, then it would use
`false` as the value, before wrapping it again into an `Option`.

Since the only reader of `EventTimelineItem::is_room_encrypted()`
doesn't really care about the difference between `Some(false)` and
`None`, in `EventTimelineItem::get_shield()`, we can use a plain `bool`
instead of an `Option`, and not distinguish `Some(false)` from `None`.
At worst, it means that sometimes we don't know the room encryption
status yet, and consider the room unencrypted; as soon as we'll get an
update about encryption state, all the items will be marked as encrypted
anyways, if needs be.
2025-02-11 11:26:47 +01:00
Benjamin Bouvier 357b36b287 refactor(timeline): simplify transition from unencrypted -> encrypted 2025-02-11 11:26:47 +01:00
Benjamin Bouvier a5f0473e1b test(timeline): use the MatrixMockServer and EventFactory in integration/timeline/mod.rs 2025-02-11 11:26:47 +01:00
Stefan Ceriu 8d74d46d80 chore(ffi): expose UserIdentity was_previously_verified and has_verification_violation methods 2025-02-10 18:28:55 +02:00
Benjamin Bouvier 9f2c572709 fix(timeline): maintain aggregations when an event is deduplicated (#4576)
## Some context

An aggregation is an event that relates to another event: for instance,
a
reaction, a poll response, and so on and so forth.
                               
## Some requirements
                                              
Because of the sync mechanisms and federation, it can happen that a
related
event is received *before* receiving the event it relates to. Those
events
must be accounted for, stashed somewhere, and reapplied later, if/when
the
related-to event shows up.
In addition to that, a room's event cache can also decide to move events
around, in its own internal representation (likely because it ran into
some
duplicate events, or it managed to decrypt a previously UTD event).
When that happens, a timeline opened on the given room
will see a removal then re-insertion of the given event. If that event
was
the target of aggregations, then those aggregations must be re-applied
when
the given event is reinserted.
                                                                       
## Some solution
      
To satisfy both requirements, the [`Aggregations`] "manager" object
provided
by this PR will take care of memoizing aggregations, **for the entire
lifetime of the timeline** (or until it's clear'd by some
caller). Aggregations are saved in memory, and have the same lifetime as
that of a timeline. This makes it possible to apply pending aggregations
to cater for the first use case, and to never lose any aggregations in
the
second use case.

## Some points for the reviewer

- I think the most controversial point is that all aggregations are
memoized for the entire lifetime of the timeline. Would that become an
issue, we can get back to some incremental scheme, in the future:
instead of memoizing aggregations for the entire lifetime of the
timeline, we'd attach them to a single timeline item. When that item is
removed, we'd put the aggregations back into a "pending" stash of
aggregations. If the item is reinserted later, we could peek at the
pending stash of aggregations, remove any that's in there, and reapply
them to the reinserted event. This is what the [first version of this
patch](https://github.com/matrix-org/matrix-rust-sdk/pull/4576/commits/ec64b9e0bcc9a2d05dd8c910a8710b72466ef51c)
did, in a much more adhoc way, for reactions only; based on the current
PR, we could do the same in a simpler manner
- while the PR has small commits, they don't quite make sense to review
individually, I'm afraid, as I was trying to find a way to make a
general system that would work not only for reactions, poll responses
and ends. As a matter of fact, the first commits may have introduced
code that is changed in subsequent commits, making the review a bit
hazardous. Happy to have a live reviewing party over Element Call, if
that helps, considering the size of the patch.
- future work may include using the aggregations manager for edits too,
leading to more code removal.
2025-02-10 15:38:25 +00:00
Jorge Martín 4b6dd5c857 fix(ffi): Client::resolve_room_alias was mapping the wrong error type
This is used to check if the alias is resolved or not.
2025-02-10 09:39:00 +01:00
Stefan Ceriu 83dd11ea7d chore(ffi): expose the whole sender profile when receiving a verification request 2025-02-07 11:47:59 +02:00
Jorge Martín 6c2a88cdc0 test: Fix flaky test_publishing_room_alias 2025-02-07 09:00:07 +01:00
Benjamin Bouvier e00d57fee2 test: wait for the redact endpoint to be hit in test_abort_before_being_sent
The test ends up with checking that the redact endpoint has been hit
once. It's actually the send queue doing the redaction as a dependent
send request, and it doesn't provide any notification mechanism in this
case, so we can't really know when it's done doing it.

One solution would be to not check the number of calls to the redact/
endpoint, but that means checking for fewer things. Instead, I made it
so that when hit, the endpoint will signal it to the main task using a
oneshot channel; then the main task waits with a long timeout for the
receiving end to get the notification it's been sent, which should be
sufficient.
2025-02-06 17:20:04 +01:00
Stefan Ceriu ce44c6e4e7 chore(base): don't show timeline verification requests as last messages 2025-02-06 14:01:05 +02:00
Stefan Ceriu f9ff4fff50 feat(ffi): add support for starting and responding to user verification requests 2025-02-06 14:01:05 +02:00
Benjamin Bouvier 2291a61379 ci: add a new feature set to test experimental-oidc too
This would help find test failures specific to experimental-oidc, as
well as doctests failing (which would have prevented the failures fixed
in https://github.com/matrix-org/matrix-rust-sdk/pull/4614 to happen in
the first place).
2025-02-06 11:21:31 +01:00
Stefan Ceriu d8f37509af chore(ffi): reduce the verbosity of the store locks and ambiguity map 2025-02-05 17:45:57 +02:00
Jorge Martín dddbcfbabb fix(ffi): Align RoomList::preview_room with Client::get_room_preview_* functions.
This removes the restriction applied in the FFI layer so only invited and knocked rooms can return room previews.
2025-02-05 14:06:50 +01:00
Ivan Enderlin ed8c1d543a doc(sdk): Add #4627 in the CHANGELOG.md. 2025-02-05 13:29:08 +01:00
Ivan Enderlin 3e02d90a27 chore(sdk): Remove RoomEventCacheUpdate::Clear.
This patch removes the `Clear` variant of the `RoomEventCacheUpdate`
enum. This one is not needed anymore since we have
`UpdateTimelineEvents` which contains updates as `Vec<VectorDiff<_>>`.
`VectorDiff` _has_ a `Clear` variant. It resulted in a double clear
every time.

This patch updates `RoomEventCacheInner::reset` and
`RoomEventCacheInner::with_events_mut` to annotate them with a
`#[must_use]`. Since they return the updates as `VectorDiff`s,
they **must** be broadcasted/propagated somewhere, likely with
`RoomEventCacheUpdate`. This mechanism ensures to not miss updates.
2025-02-05 13:29:08 +01:00
Ivan Enderlin 954b16ad39 Merge pull request #4603 from zecakeh/media-cache-auto-cleanup
feat(base): Add automatic media cache cleanups to MediaService
2025-02-05 13:24:20 +01:00
Kévin Commaille ed18c5113f fix: Fix changelogs for new media service feature after new release
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-05 00:28:56 +01:00
Kévin Commaille 0f4b3aa187 Merge branch 'main' into media-cache-auto-cleanup 2025-02-05 00:12:18 +01:00
Ivan Enderlin 8a7658745d chore: Fallback to jplatte/eyeball instead of Hywan/eyeball's fork. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 2ea39877cc fix: Add github.com/Hywan/eyeball in the allow-git list. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 4212691cf0 fix(ui): Don't use 0 as the initial value for Skip.
This patch fixes an issue where 0 was used as the initial value for
the `Skip` higher-order stream in the `TimelineSubscriber`. This is
wrong, as the `SkipCount` value may have been modified before the
`TimelineSubscriber` is created.

This patch provides a test to reproduce the problem.
2025-02-04 19:43:38 +01:00
Ivan Enderlin d66fe79579 task(ui): Create TimelineSubscriber.
This patch gathers the logic of the `Timeline::subscribe` into a single
type: `TimelineSubscriber`.

The `TimelineController::subscribe_batched_and_limited` method is
renamed `subscribe` to match `Timeline::subscribe`. Things are simpler
to apprehend.

The `TimelineSubscriber` type configures the subscriber/stream in a
single place. It takes an `&ObservableItems` and a `&SkipCount`, and
configures everything. It also provides a single place to document the
behaviour of the subscriber, with the `Skip` higher-order stream.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 88caf11842 chore(ui): Rename Timeline::subscribe to subscribe_raw.
This patch renames the (test only) `Timeline::subscribe` method to
`Timeline::subscribe_raw`.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 5320d952e5 test(ui): Test the Timeline lazy backwards pagination. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 7eae832b8c test: set_timeline_prev_batch takes anything that implements Into<String>. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 1d18ab03d7 test(ui): Improve the assert_timeline_stream macro. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 337bc2c097 doc(ui): Fix a typo. 2025-02-04 19:43:38 +01:00
Ivan Enderlin 7e59ae99d0 featui): Timeline::subscribe() supports lazy backwards pagination.
This patch updates the pagination mechanism of the `Timeline` to support
lazy backwards pagination.

`Timeline::paginate_backwards` already does different things whether the
timeline focus is live or focused. When it's live, the method will first
try to paginate backwards lazily, by adjusting the `count` value of the
`Skip` stream used by the `Timeline::subscribe` method. If there is not
enough items to provide, the greedy pagination will run.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 51feda1042 task(ui): TimelineStateTransaction::commit adjusts the Skip's count.
This patch updates `TimelineStateTransaction::commit` to adjust the
count value of the `Skip` higher-order stream.

This patch also creates `TimelineStateTransaction::new` to simplify the
creation of a state transaction.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 0a520e4f9f task(ui): Apply the Skip higher-order stream for the Timeline.
This patch applies the `Skip` higher-order stream on the `Timeline`
subscriber. The method `TimelineController::subscribe_batched` is
renamed `subscribe_batched_and_limited` at the same time.

The `Skip` stream uses the `TimelineMetadata::subscriber_skip_count`
observable value as the `count_stream`. The initial count value is 0.

No test needs to be changed so far.
2025-02-04 19:43:38 +01:00
Ivan Enderlin e07212d356 task(ui): Add TimelineMetadata::subscriber_skip_count.
This patch adds the `subscriber_skip_count` field to `TimelineMetadata`.
It's going to be used to define the `count` value of the `Skip`
higher-order stream that is going to be applied to the `Timeline`
subscriber.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 1d52073b45 task(ui): Add the SkipCount type to help computing the count for Skip.
This patch adds functions like `compute_next`,
`compute_next_when_paginating_backwards` and
`compute_next_when_paginating_forwards`, which are necessary to
correctly compute the `count` value for the `Skip` higher-order stream.

This patch adds the associated test suite.
2025-02-04 19:43:38 +01:00
Ivan Enderlin d85d6cfbca refactor(ui): Rename TimelineStream to TimelineWithDropHandle.
This patch renames `TimelineStream` to `TimelineWithDropHandle`, as the
former name was too vague and was not clarify what the type was doing.
The new name makes it clear that it attaches a `TimelineDropHandle` to a
subscriber (since it is part of the `subscriber` module!).
2025-02-04 19:43:38 +01:00
Ivan Enderlin 892cb9116c refactor(ui): Move TimelineStream into the new subscriber module.
This patch moves the `TimelineStream` type into the new `subscriber`
module. The idea is to add more code related to subscribers in the next
patches.
2025-02-04 19:43:38 +01:00
Ivan Enderlin 5e9d291ca3 chore(ui): Clone items only once when subscribing.
This patch updates `TimelineController::subscribe` to use
`VectorSubscriber::into_values_and_batched_stream`. It returns
the cloned items along with the stream. It saves the need to call
`ObservableItems::clone_items`, thus saving the clone of all items.

tl;dr: `clone_items()` clones… items… and `subscribe()` also clones the
items. There is 2 clones. With `into_values_and_batched_stream()`, there
is 1 clone.
2025-02-04 19:43:38 +01:00
Benjamin Bouvier b74d64a456 test(timeline): use the MatrixMockServer in integration/timeline/edit 2025-02-04 17:20:02 +01:00
Benjamin Bouvier cd8f5cf5d4 test(timeline): use the MatrixMockServer in integration/timeline/echo 2025-02-04 17:20:02 +01:00
Benjamin Bouvier 1dc20aa9aa test(timeline): use the MatrixMockServer in integration/timeline/reactions 2025-02-04 17:20:02 +01:00
Ivan Enderlin bdab9951af doc(changelog): Add entries for the performance improvement patches.
This patch adds #4601, #4608, #4612 and #4616 in their respective
`CHANGELOG.md`s.
2025-02-04 16:59:13 +01:00
Kévin Commaille f641a639cd Merge branch 'main' into media-cache-auto-cleanup 2025-02-04 11:56:32 +01:00
Kévin Commaille 049021fe27 refactor(auth-qrcode): Inline declaration of oauth2 client type
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:30:57 +01:00
Kévin Commaille 4db32b15ba chore(auth-qrcode): Update copyright date
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:27:58 +01:00
Kévin Commaille 619346acad chore: Add changelog entry for oauth2
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 19:48:50 +01:00
Kévin Commaille 525f9866a4 refactor(auth-qrcode): Rename everything OIDC to OAuth 2.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 19:41:04 +01:00
Kévin Commaille d7dc1c9b5b refactor(auth-qrcode): Use oauth2 crate instead of openidconnect
The MSCs are now only based on OAuth 2.0, which is simpler than OpenID Connect.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 18:42:54 +01:00
Kévin Commaille 7da3aaaa8a chore: Add new PR links to MediaRetentionPolicy changelog entries
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 12:28:43 +01:00
Kévin Commaille 5aaa6bf187 feat(base): Add automatic media cache cleanups to MediaService
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 12:20:13 +01:00
Kévin Commaille af9a5edd59 feat(executor): Expose JoinHandle::is_finished method on wasm32
For parity with tokio's JoinHandle.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 12:10:31 +01:00
Kévin Commaille 6e764644b3 feat(base): Require that EventCacheStoreMedia implementations implement Clone
We want to be able to send the store to a new task, so the easiest way is to be able to clone it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 12:09:18 +01:00
Kévin Commaille 8dc2ec9dc4 feat(base): Allow to clone MediaService
We want to be able to send it to a new task, so the easiest way is to be able to clone it.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 12:03:12 +01:00
Kévin Commaille 4e1ae3d5e9 feat(base): Store last media cleanup time with EventCacheStoreMedia
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 11:52:42 +01:00
Kévin Commaille 582b3a91d6 refactor(sqlite): Add methods to get and set values in the kv table by (de)serializing them
Since it's a common occurrence, it will reduce duplication.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-02 11:47:43 +01:00
Kévin Commaille f7467ff57a feat(base): Add setting for frequency of automatic media cache cleanups
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-01 17:51:47 +01:00
Daniel Kilimnik 2e16021f14 fix(oidc): remove unnecessary e2e-encryption imports 2025-01-19 16:08:51 +01:00
394 changed files with 38119 additions and 21405 deletions
+2 -3
View File
@@ -9,8 +9,7 @@ exclude = [
[advisories]
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." },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
]
[licenses]
@@ -60,7 +59,7 @@ allow-git = [
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
"https://github.com/jplatte/async-compat",
"https://github.com/element-hq/async-compat",
# We can release vodozemac whenever we need but let's not block development
# on releases.
"https://github.com/matrix-org/vodozemac",
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
toolchain: nightly-2025-02-20
components: rustfmt
- name: Run Benchmarks
+25 -22
View File
@@ -295,7 +295,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.29.5
uses: crate-ci/typos@v1.31.1
lint:
name: Lint
@@ -314,7 +314,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
toolchain: nightly-2025-02-20
components: clippy, rustfmt
- name: Load cache
@@ -342,27 +342,9 @@ jobs:
runs-on: ubuntu-latest
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
# run several docker containers with the same networking stack so the hostname 'synapse'
# maps to the synapse container, etc.
services:
# synapse needs a postgres container
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: syncv3
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
@@ -401,3 +383,24 @@ jobs:
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
run: |
cargo nextest run -p matrix-sdk-integration-testing
compile-bench:
name: 🚄 Compile benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Compile benchmarks (no run)
run: |
cargo bench --profile dev --no-run
+2 -19
View File
@@ -26,26 +26,9 @@ jobs:
name: Code Coverage
runs-on: "ubuntu-latest"
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
# run several docker containers with the same networking stack so the hostname 'synapse'
# maps to the synapse container, etc.
services:
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: syncv3
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
+40
View File
@@ -0,0 +1,40 @@
# Check if the path of changed file is longer than 260 characters
# that windows filesystem allows
name: Detect long path among changed files
on:
workflow_dispatch:
pull_request: # focus on the changed files in current PR
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
long-path:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@6f67ee9ac810f0192ea7b3d2086406f97847bcf9 # v45
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
MAX_LENGTH: 120 # set max length to 120, considering the base path of app project that uses matrix-sdk
run: |
for file in ${ALL_CHANGED_FILES}; do
if [ ${#file} -gt $MAX_LENGTH ]; then
echo "File path is too long. Length: ${#file}, Path: $file"
exit 1
fi
done
exit 0
@@ -9,4 +9,4 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Machete
uses: bnjbvr/cargo-machete@main
uses: bnjbvr/cargo-machete@v0.8.0
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-11-26
toolchain: nightly-2025-02-20
- name: Install Node.js
uses: actions/setup-node@v4
Generated
+263 -893
View File
File diff suppressed because it is too large Load Diff
+26 -23
View File
@@ -18,7 +18,7 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.83"
rust-version = "1.85"
[workspace.dependencies]
anyhow = "1.0.95"
@@ -29,13 +29,13 @@ assert_matches2 = "0.1.2"
async-rx = "0.1.3"
async-stream = "0.3.5"
async-trait = "0.1.85"
as_variant = "1.2.0"
as_variant = "1.3.0"
base64 = "0.22.1"
byteorder = "1.5.0"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.6.0", features = ["tracing"] }
eyeball-im-util = "0.8.0"
eyeball-im = { version = "0.7.0", features = ["tracing"] }
eyeball-im-util = "0.9.0"
futures-core = "0.3.31"
futures-executor = "0.3.31"
futures-util = "0.3.31"
@@ -45,9 +45,9 @@ growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.2.0"
imbl = "4.0.1"
imbl = "5.0.0"
indexmap = "2.7.1"
insta = { version = "1.42.1", features = ["json"] }
insta = { version = "1.42.1", features = ["json", "redactions"] }
itertools = "0.14.0"
js-sys = "0.3.69"
mime = "0.3.17"
@@ -60,7 +60,7 @@ reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.1", features = [
ruma = { version = "0.12.2", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -75,7 +75,7 @@ ruma = { version = "0.12.1", features = [
"unstable-msc4140",
"unstable-msc4171",
] }
ruma-common = { version = "0.15.1" }
ruma-common = "0.15.2"
serde = "1.0.217"
serde_html_form = "0.2.7"
serde_json = "1.0.138"
@@ -101,21 +101,17 @@ web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.10.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
lto = true
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.11.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.11.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.11.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.11.0", default-features = false }
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
@@ -143,8 +139,15 @@ debug = 2
inherits = "dbg"
opt-level = 3
[profile.profiling]
inherits = "release"
# LTO is too slow to compile.
lto = false
# Get symbol names for profiling purposes.
debug = true
[patch.crates-io]
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
+49 -21
View File
@@ -1,40 +1,68 @@
![Build Status](https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat-square)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
[![Docs - Main](https://img.shields.io/badge/docs-main-blue.svg?style=flat-square)](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
[![Docs - Stable](https://img.shields.io/crates/v/matrix-sdk?color=blue&label=docs&style=flat-square)](https://docs.rs/matrix-sdk)
<h1 align="center">Matrix Rust SDK</h1>
<div align="center">
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
<br/><br/>
<img src="contrib/logo.svg">
<br>
<hr>
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
<a href="https://crates.io/crates/matrix-sdk/">
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
<br>
<a href="https://docs.rs/matrix-sdk/">
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
<br>
<br>
</div>
# matrix-rust-sdk
**matrix-rust-sdk** is an implementation of a [Matrix][] client-server library in [Rust][].
The Matrix Rust SDK is a collection of libraries that make it easier to build
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
syncing, and room state, so you can focus on your app's logic and UI. Whether
you're writing a small bot, a desktop client, or something in between, the SDK
is designed to be flexible, async-friendly, and ready to use out of the box.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
## Project structure
The rust-sdk consists of multiple crates that can be picked at your convenience:
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
- **matrix-sdk** - High level client library, with batteries included, you're most likely
interested in this.
- **matrix-sdk-base** - No (network) IO client state machine that can be used to embed a
Matrix client in your project or build a full fledged network enabled client
lib on top of it.
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
used to add Matrix E2EE support to your client or client library.
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) A high-level client library that makes it easy to build
full-featured UI clients with minimal setup. Check out our reference client,
[multiverse](https://github.com/matrix-org/matrix-rust-sdk/tree/main/labs/multiverse), for an example.
- [matrix-sdk](https://docs.rs/matrix-sdk/latest/matrix_sdk/) A mid-level client library, ideal for building bots, custom
clients, or higher-level abstractions. You can find example usage in the
[examples directory](https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples).
- [matrix-sdk-crypto](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/) A standalone encryption state machine with no network I/O,
providing end-to-end encryption support for Matrix clients and libraries.
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
for a step-by-step introduction.
## Status
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
The library is considered production ready and backs multiple client
implementations such as Element X
[[1]](https://github.com/element-hq/element-x-ios)
[[2]](https://github.com/element-hq/element-x-android),
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
confident to build upon it.
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
Development of the SDK has been primarily sponsored by Element though accepts
contributions from all.
## Bindings
Some crates of the **matrix-rust-sdk** can be embedded inside other
environments, like Swift, Kotlin, JavaScript, Node.js etc. Please,
explore the [`bindings/`](./bindings/) directory to learn more.
The higher-level crates of the Matrix Rust SDK can be embedded in other
environments such as Swift, Kotlin, JavaScript, and Node.js. Check out the
[bindings/](./bindings/) directory to learn more about how to integrate the SDK
into your language of choice.
## License
+19 -18
View File
@@ -13,35 +13,36 @@ The procedure is as follows:
1. Switch to a release branch:
```bash
git switch -c release-x.y.z
  ```
```bash
git switch -c release-x.y.z
```
2. Prepare the release. This will update the `README.md`, set the versions in
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
```bash
cargo xtask release prepare --execute minor|patch|rc
```
```bash
cargo xtask release prepare --execute minor|patch|rc
```
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
satisfied, push the branch and open a PR.
```bash
git push --set-upstream origin/release-x.y.z
```
```bash
git push --set-upstream origin/release-x.y.z
```
4. Pass the review and merge the branch as you would with any other branch.
5. Create tags for your new release, publish the release on crates.io and push
the tags:
```bash
# Switch to main first.
git switch main
# Pull in the now-merged release commit(s).
git pull
# Create tags, publish the release on crates.io, and push the tags.
cargo xtask release publish --execute
```
For more information on cargo-release: https://github.com/crate-ci/cargo-release
```bash
# Switch to main first.
git switch main
# Pull in the now-merged release commit(s).
git pull
# Create tags, publish the release on crates.io, and push the tags.
cargo xtask release publish --execute
```
For more information on cargo-release: https://github.com/crate-ci/cargo-release
+9 -1
View File
@@ -14,7 +14,7 @@ matrix-sdk-crypto = { workspace = true }
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test = { workspace = true }
matrix-sdk-ui = { workspace = true }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite"] }
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
@@ -29,6 +29,10 @@ pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
name = "crypto_bench"
harness = false
[[bench]]
name = "linked_chunk"
harness = false
[[bench]]
name = "store_bench"
harness = false
@@ -37,5 +41,9 @@ harness = false
name = "room_bench"
harness = false
[[bench]]
name = "timeline"
harness = false
[package.metadata.release]
release = false
+10 -3
View File
@@ -8,7 +8,7 @@ can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.ht
## Running the benchmarks
The benchmark can be simply run by using the `bench` command of `cargo`:
The benchmark can be run by using the `bench` command of `cargo`:
```bash
$ cargo bench
@@ -16,6 +16,13 @@ $ cargo bench
This will work from the workspace directory of the rust-sdk.
To lower compile times, you might be interested in using the `profiling` profile, that's optimized
for a fair tradeoff between compile times and runtime performance:
```bash
$ cargo bench --profile profiling
```
If you want to pass options to the benchmark [you'll need to specify the name of
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
@@ -23,7 +30,7 @@ the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench
$ cargo bench --bench crypto_bench -- # Your options go here
```
If you want to run only a specific benchmark, simply pass the name of the
If you want to run only a specific benchmark, pass the name of the
benchmark as an argument:
```bash
@@ -65,7 +72,7 @@ permisive value is `-1`:
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
```
To generate flame graphs feature simply enable the profiling mode using the
To generate flame graphs feature, enable the profiling mode using the
`--profile-time` command line flag:
```bash
+256
View File
@@ -0,0 +1,256 @@
use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
linked_chunk::{lazy_loader, LinkedChunk, Update},
SqliteEventCacheStore,
};
use matrix_sdk_base::event_cache::{
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
Event, Gap,
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
use ruma::{room_id, EventId};
use tempfile::tempdir;
use tokio::runtime::Builder;
#[derive(Clone, Debug)]
enum Operation {
PushItemsBack(Vec<Event>),
PushGapBack(Gap),
}
fn writing(c: &mut Criterion) {
// Create a new asynchronous runtime.
let runtime = Builder::new_multi_thread()
.enable_time()
.enable_io()
.build()
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
let mut group = c.benchmark_group("writing");
group.sample_size(10).measurement_time(Duration::from_secs(30));
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
let sqlite_temp_dir = tempdir().unwrap();
// Declare new stores for this set of events.
let stores: [(&str, Option<Arc<DynEventCacheStore>>); 3] = [
("none", None),
("memory store", Some(MemoryStore::default().into_event_cache_store())),
(
"sqlite store",
runtime.block_on(async {
Some(
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
.await
.unwrap()
.into_event_cache_store(),
)
}),
),
];
for (store_name, store) in stores {
// Create the operations we want to bench.
let mut operations = Vec::new();
{
let mut events = (0..number_of_events)
.map(|nth| {
event_factory
.text_msg("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.into_event()
})
.peekable();
let mut gap_nth = 0;
while events.peek().is_some() {
{
let events_to_push_back = events.by_ref().take(80).collect::<Vec<_>>();
if events_to_push_back.is_empty() {
break;
}
operations.push(Operation::PushItemsBack(events_to_push_back));
}
{
operations.push(Operation::PushGapBack(Gap {
prev_token: format!("gap{gap_nth}"),
}));
gap_nth += 1;
}
}
}
// Define the throughput.
group.throughput(Throughput::Elements(number_of_events));
// Get a bencher.
group.bench_with_input(
BenchmarkId::new(store_name, number_of_events),
&operations,
|bencher, operations| {
// Bench the routine.
bencher.to_async(&runtime).iter_batched(
|| operations.clone(),
|operations| async {
// The routine to bench!
let mut linked_chunk = LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
for operation in operations {
match operation {
Operation::PushItemsBack(events) => linked_chunk.push_items_back(events),
Operation::PushGapBack(gap) => linked_chunk.push_gap_back(gap),
}
}
if let Some(store) = &store {
let updates = linked_chunk.updates().unwrap().take();
store.handle_linked_chunk_updates(room_id, updates).await.unwrap();
// Empty the store.
store.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await.unwrap();
}
},
BatchSize::SmallInput
)
},
);
{
let _guard = runtime.enter();
drop(store);
}
}
}
group.finish()
}
fn reading(c: &mut Criterion) {
// Create a new asynchronous runtime.
let runtime = Builder::new_multi_thread()
.enable_time()
.enable_io()
.build()
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
let mut group = c.benchmark_group("reading");
group.sample_size(10);
for num_events in [10, 100, 1000, 10_000, 100_000] {
let sqlite_temp_dir = tempdir().unwrap();
// Declare new stores for this set of events.
let stores: [(&str, Arc<DynEventCacheStore>); 2] = [
("memory store", MemoryStore::default().into_event_cache_store()),
(
"sqlite store",
runtime.block_on(async {
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
.await
.unwrap()
.into_event_cache_store()
}),
),
];
for (store_name, store) in stores {
// Store some events and gap chunks in the store.
{
let mut events = (0..num_events)
.map(|nth| {
event_factory
.text_msg("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.into_event()
})
.peekable();
let mut lc =
LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
let mut num_gaps = 0;
while events.peek().is_some() {
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
if events_chunk.is_empty() {
break;
}
lc.push_items_back(events_chunk);
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
num_gaps += 1;
}
// Now persist the updates to recreate this full linked chunk.
let updates = lc.updates().unwrap().take();
runtime.block_on(store.handle_linked_chunk_updates(room_id, updates)).unwrap();
}
// Define the throughput.
group.throughput(Throughput::Elements(num_events));
// Get a bencher.
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
// Bench the routine.
bencher.to_async(&runtime).iter(|| async {
// Load the last chunk first,
let (last_chunk, chunk_id_gen) = store.load_last_chunk(room_id).await.unwrap();
let mut lc =
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
.expect("no error when reconstructing the linked chunk")
.expect("there is a linked chunk in the store");
// Then load until the start of the linked chunk.
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
while let Some(prev) =
store.load_previous_chunk(room_id, cur_chunk_id).await.unwrap()
{
cur_chunk_id = prev.identifier;
lazy_loader::insert_new_first_chunk(&mut lc, prev)
.expect("no error when linking the previous lazy-loaded chunk");
}
})
});
{
let _guard = runtime.enter();
drop(store);
}
}
}
group.finish()
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
criterion
}
criterion_group! {
name = event_cache;
config = criterion();
targets = writing, reading,
}
criterion_main!(event_cache);
+39 -61
View File
@@ -1,14 +1,12 @@
use std::{sync::Arc, time::Duration};
use std::time::Duration;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
};
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
api::client::membership::get_member_events,
@@ -18,13 +16,9 @@ use ruma::{
serde::Raw,
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
};
use serde::Serialize;
use serde_json::json;
use tokio::runtime::Builder;
use wiremock::{
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
Mock, MockServer, Request, ResponseTemplate,
};
use wiremock::{Request, ResponseTemplate};
pub fn receive_all_members_benchmark(c: &mut Criterion) {
const MEMBERS_IN_ROOM: usize = 100000;
@@ -61,17 +55,18 @@ 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(
let base_client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
.block_on(base_client.activate(
SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
},
RoomLoadSettings::default(),
None,
))
.expect("Could not set session meta");
@@ -109,9 +104,7 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
let sender_id = owned_user_id!("@sender:example.com");
let f = EventFactory::new().room(&room_id).sender(&sender_id);
let (client, server) = runtime.block_on(logged_in_client_with_server());
let mut sync_response_builder = SyncResponseBuilder::new();
let mut joined_room_builder =
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
@@ -133,17 +126,15 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
}
}
)));
let response_json =
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
runtime.block_on(mock_sync(&server, response_json, None));
let sync_settings = SyncSettings::default();
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
runtime.block_on(server.reset());
let (server, client, room) = runtime.block_on(async move {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
runtime.block_on(
Mock::given(method("GET"))
.and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*"))
let room = server.sync_room(&client, joined_room_builder).await;
server
.mock_room_event()
.respond_with(move |r: &Request| {
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
let event_id_str = segments[6];
@@ -157,10 +148,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
.set_delay(Duration::from_millis(50))
.set_body_json(event.json())
})
.mount(&server),
);
.mount()
.await;
client.event_cache().subscribe().unwrap();
(server, client, room)
});
let room = client.get_room(&room_id).expect("Room not found");
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
@@ -171,15 +166,6 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
group.throughput(Throughput::Elements(count as u64));
group.sample_size(10);
let client = Arc::new(client);
{
let client = client.clone();
runtime.spawn_blocking(move || {
client.event_cache().subscribe().unwrap();
});
}
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
b.to_async(&runtime).iter(|| async {
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
@@ -187,7 +173,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
// Reset cache so it always loads the events from the mocked endpoint
client.event_cache().empty_immutable_cache().await;
client
.event_cache_store()
.lock()
.await
.unwrap()
.clear_all_rooms_chunks()
.await
.unwrap();
let timeline = Timeline::builder(&room)
.with_focus(TimelineFocus::PinnedEvents {
@@ -206,40 +199,25 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
{
let _guard = runtime.enter();
runtime.block_on(server.reset());
drop(server);
}
group.finish();
}
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
let mut mock_builder = Mock::given(method("GET"))
.and(path("/_matrix/client/r0/sync"))
.and(header("authorization", "Bearer 1234"));
if let Some(since) = since {
mock_builder = mock_builder.and(query_param("since", since));
} else {
mock_builder = mock_builder.and(query_param_is_missing("since"));
}
mock_builder
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(server)
.await;
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
{
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
))
}
criterion
#[cfg(not(target_os = "linux"))]
{
Criterion::default()
}
}
criterion_group! {
+3 -4
View File
@@ -2,9 +2,8 @@ use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
authentication::matrix::{MatrixSession, MatrixSessionTokens},
config::StoreConfig,
Client, RoomInfo, RoomState, StateChanges,
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
SessionTokens, StateChanges,
};
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
use matrix_sdk_sqlite::SqliteStateStore;
@@ -51,7 +50,7 @@ pub fn restore_session(c: &mut Criterion) {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
},
tokens: MatrixSessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
};
// Start the benchmark.
+140
View File
@@ -0,0 +1,140 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
use matrix_sdk_ui::Timeline;
use ruma::{
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
EventId,
};
use tokio::runtime::Builder;
/// Benchmark the time it takes to create a timeline (with read receipt
/// support), when there are many initial events at rest in the event cache.
///
/// `NUM_EVENTS` is the number of events that will be stored initially in the
/// event cache. It will be a mix of messages, reactions, edits and redactions,
/// so there are some aggregations to take into account by the timeline as well.
pub fn create_timeline_with_initial_events(c: &mut Criterion) {
const NUM_EVENTS: usize = 10000;
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let sender_id = owned_user_id!("@sender:example.com");
let other_sender_id = owned_user_id!("@other_sender:example.com");
let another_sender_id = owned_user_id!("@another_sender:example.com");
let f = EventFactory::new().room(&room_id);
let mut events = Vec::new();
for i in 0..NUM_EVENTS {
let sender = match i % 3 {
0 => &sender_id,
1 => &other_sender_id,
2 => &another_sender_id,
_ => unreachable!("math genius over here"),
};
let event_id = EventId::parse(format!("$event{i}")).unwrap();
let j = i % 10;
if j < 6 {
// Messages.
events.push(
f.text_msg(format!("Message {i}"))
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
} else if j < 8 {
// Reactions.
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
events.push(
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
);
} else if j == 8 {
// Edit.
// Note: (i-3)%3 is the same as i%3 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
events.push(
f.text_msg(format!("* Message {}v2", i - 3))
.edit(
&prev_event,
RoomMessageEventContentWithoutRelation::text_plain(format!(
"Message {}v2",
i - 3
)),
)
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
} else if j == 9 {
// Redaction.
// Note: (i-6)%3 is the same as i%6 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
events
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
}
}
let builder = JoinedRoomBuilder::new(&room_id)
.add_state_event(StateTestEvent::Encryption)
.add_timeline_bulk(events);
let room = runtime.block_on(async move {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
client.event_cache().subscribe().unwrap();
let room = server.sync_room(&client, builder).await;
drop(server);
room
});
let mut group = c.benchmark_group("Test");
group.throughput(Throughput::Elements(NUM_EVENTS as _));
group.sample_size(10);
group.bench_function(
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|b| {
b.to_async(&runtime).iter(|| async {
let timeline = Timeline::builder(&room)
.track_read_marker_and_receipts()
.build()
.await
.expect("Could not create timeline");
let (items, _) = timeline.subscribe().await;
assert_eq!(items.len(), 20);
});
},
);
group.finish();
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
{
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
100,
pprof::criterion::Output::Flamegraph(None),
))
}
#[cfg(not(target_os = "linux"))]
{
Criterion::default()
}
}
criterion_group! {
name = room;
config = criterion();
targets = create_timeline_with_initial_events
}
criterion_main!(room);
@@ -507,6 +507,7 @@ fn collect_sessions(
imported: session.imported,
backed_up: session.backed_up,
history_visibility: None,
shared_history: false,
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
};
+65 -1
View File
@@ -1,7 +1,24 @@
# unreleased
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.11.0] - 2025-04-11
Breaking changes:
- `TracingConfiguration` now includes a new field `trace_log_packs`, which gives a convenient way
to set the TRACE log level for multiple targets related to a given feature.
([#4824](https://github.com/matrix-org/matrix-rust-sdk/pull/4824))
- `setup_tracing` has been renamed `init_platform`; in addition to the `TracingConfiguration`
parameter it also now takes a boolean indicating whether to spawn a minimal tokio runtime for the
application; in general for main app processes this can be set to `false`, and memory-constrained
programs can set it to `true`.
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
original message and the associated error code and kind.
@@ -29,12 +46,59 @@ Breaking changes:
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
or fails to present).
- The rest of `AuthenticationError` is now found in the OidcError type.
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
- The `get_element_call_required_permissions` function now requires the device_id.
- Some `OidcPrompt` cases have been removed (`None`, `SelectAccount`).
- `Room::is_encrypted` is replaced by `Room::latest_encryption_state`
which returns a value of the new `EncryptionState` enum; another
`Room::encryption_state` non-async and infallible method is added to get the
`EncryptionState` without running a network request.
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can
safely replace:
```rust
room.is_encrypted().await?
```
by
```rust
room.latest_encryption_state().await?.is_encrypted()
```
- `ClientBuilder::passphrase` is renamed `session_passphrase`
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
- Merge `Timeline::send_thread_reply` into `Timeline::send_reply`. This
changes the parameters of `send_reply` which now requires passing the
event ID (and thread reply behaviour) inside a `ReplyParameters` struct.
([#4880](https://github.com/matrix-org/matrix-rust-sdk/pull/4880/))
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
Clients are supposed to re-register with the homeserver for every login.
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
Additions:
- Add `Encryption::get_user_identity` which returns `UserIdentity`
- Add `ClientBuilder::room_key_recipient_strategy`
- Add `Room::send_raw`
- Add `NotificationSettings::set_custom_push_rule`
- Expose `withdraw_verification` to `UserIdentity`
- Expose `report_room` to `Room`
- Add `RoomInfo::encryption_state`
([#4788](https://github.com/matrix-org/matrix-rust-sdk/pull/4788))
- Add `Timeline::send_thread_reply` for clients that need to start threads
themselves.
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
has less memory available than the current standard
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
+4 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-ffi"
version = "0.2.0"
version = "0.11.0"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
@@ -8,6 +8,7 @@ license = "Apache-2.0"
readme = "README.md"
rust-version = { workspace = true }
repository = "https://github.com/matrix-org/matrix-rust-sdk"
publish = false
[lib]
crate-type = ["cdylib", "staticlib"]
@@ -23,7 +24,7 @@ vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
[dependencies]
anyhow = { workspace = true }
as_variant = { workspace = true }
async-compat = "0.2.1"
async-compat = "0.2.4"
eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
@@ -55,7 +56,6 @@ workspace = true
features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-widgets",
"markdown",
"rustls-tls", # note: differ from block below
@@ -69,7 +69,6 @@ workspace = true
features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-widgets",
"markdown",
"native-tls", # note: differ from block above
@@ -82,4 +81,4 @@ features = [
workspace = true
[package.metadata.release]
release = false
release = true
+77 -63
View File
@@ -5,18 +5,14 @@ use std::{
};
use matrix_sdk::{
authentication::oidc::{
registrations::OidcRegistrationsError,
types::{
iana::oauth::OAuthClientAuthenticationMethod,
oidc::ApplicationType,
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
requests::GrantType,
},
OidcError as SdkOidcError,
authentication::oauth::{
error::OAuthAuthorizationCodeError,
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
},
Error,
};
use ruma::serde::Raw;
use url::Url;
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
@@ -116,7 +112,7 @@ pub struct OidcConfiguration {
/// successful.
pub redirect_uri: String,
/// A URI that contains information about the client.
pub client_uri: Option<String>,
pub client_uri: String,
/// A URI that contains the client's logo.
pub logo_uri: Option<String>,
/// A URI that contains the client's terms of service.
@@ -126,50 +122,68 @@ pub struct OidcConfiguration {
/// An array of e-mail addresses of people responsible for this client.
pub contacts: Option<Vec<String>>,
/// Pre-configured registrations for use with issuers that don't support
/// Pre-configured registrations for use with homeservers that don't support
/// dynamic client registration.
pub static_registrations: HashMap<String, String>,
/// A file path where any dynamic registrations should be stored.
///
/// Suggested value: `{base_path}/oidc/registrations.json`
pub dynamic_registrations_file: String,
/// The keys of the map should be the URLs of the homeservers, but keys
/// using `issuer` URLs are also supported.
pub static_registrations: HashMap<String, String>,
}
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
type Error = OidcError;
impl OidcConfiguration {
pub(crate) fn redirect_uri(&self) -> Result<Url, OidcError> {
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)
}
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
let redirect_uri =
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
pub(crate) fn client_metadata(&self) -> Result<Raw<ClientMetadata>, OidcError> {
let redirect_uri = self.redirect_uri()?;
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
let client_uri = self.client_uri.localized_url()?;
let logo_uri = self.logo_uri.localized_url()?;
let policy_uri = self.policy_uri.localized_url()?;
let tos_uri = self.tos_uri.localized_url()?;
let contacts = self.contacts.clone();
ClientMetadata {
application_type: Some(ApplicationType::Native),
redirect_uris: Some(vec![redirect_uri]),
grant_types: Some(vec![
GrantType::RefreshToken,
GrantType::AuthorizationCode,
GrantType::DeviceCode,
]),
// A native client shouldn't use authentication as the credentials could be intercepted.
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
let metadata = ClientMetadata {
// The server should display the following fields when getting the user's consent.
client_name,
contacts,
client_uri,
logo_uri,
policy_uri,
tos_uri,
..Default::default()
..ClientMetadata::new(
ApplicationType::Native,
vec![
OAuthGrantType::AuthorizationCode { redirect_uris: vec![redirect_uri] },
OAuthGrantType::DeviceCode,
],
client_uri,
)
};
Raw::new(&metadata).map_err(|_| OidcError::MetadataInvalid)
}
pub(crate) fn registration_data(&self) -> Result<ClientRegistrationData, OidcError> {
let client_metadata = self.client_metadata()?;
let mut registration_data = ClientRegistrationData::new(client_metadata);
if !self.static_registrations.is_empty() {
let static_registrations = self
.static_registrations
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
return None;
};
Some((issuer, ClientId::new(client_id.clone())))
})
.collect();
registration_data.static_registrations = Some(static_registrations);
}
.validate()
.map_err(|_| OidcError::MetadataInvalid)
Ok(registration_data)
}
}
@@ -182,8 +196,6 @@ pub enum OidcError {
NotSupported,
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
MetadataInvalid,
#[error("Failed to use the supplied registrations file path.")]
RegistrationsPathInvalid,
#[error("The supplied callback URL used to complete OIDC is invalid.")]
CallbackUrlInvalid,
#[error("The OIDC login was cancelled by the user.")]
@@ -193,23 +205,17 @@ pub enum OidcError {
Generic { message: String },
}
impl From<SdkOidcError> for OidcError {
fn from(e: SdkOidcError) -> OidcError {
impl From<SdkOAuthError> for OidcError {
fn from(e: SdkOAuthError) -> OidcError {
match e {
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
_ => OidcError::Generic { message: e.to_string() },
}
}
}
impl From<OidcRegistrationsError> for OidcError {
fn from(e: OidcRegistrationsError) -> OidcError {
match e {
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
SdkOAuthError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RedirectUri(_))
| SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::InvalidState) => {
OidcError::CallbackUrlInvalid
}
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::Cancelled) => {
OidcError::Cancelled
}
_ => OidcError::Generic { message: e.to_string() },
}
}
@@ -218,7 +224,7 @@ impl From<OidcRegistrationsError> for OidcError {
impl From<Error> for OidcError {
fn from(e: Error) -> OidcError {
match e {
Error::Oidc(e) => e.into(),
Error::OAuth(e) => (*e).into(),
_ => OidcError::Generic { message: e.to_string() },
}
}
@@ -227,17 +233,25 @@ impl From<Error> for OidcError {
/* Helpers */
trait OptionExt {
/// Convenience method to convert a string to a URL and returns it as a
/// Localized URL. No localization is actually performed.
/// Convenience method to convert an `Option<String>` to a URL and returns
/// it as a Localized URL. No localization is actually performed.
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
}
impl OptionExt for Option<String> {
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
self.as_deref()
.map(|uri| -> Result<Localized<Url>, OidcError> {
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
})
.transpose()
self.as_deref().map(StrExt::localized_url).transpose()
}
}
trait StrExt {
/// Convenience method to convert a string to a URL and returns it as a
/// Localized URL. No localization is actually performed.
fn localized_url(&self) -> Result<Localized<Url>, OidcError>;
}
impl StrExt for str {
fn localized_url(&self) -> Result<Localized<Url>, OidcError> {
Ok(Localized::new(Url::parse(self).map_err(|_| OidcError::MetadataInvalid)?, []))
}
}
+267 -290
View File
@@ -1,45 +1,41 @@
use std::{
collections::HashMap,
fmt::Debug,
path::Path,
sync::{Arc, RwLock},
};
use anyhow::{anyhow, Context as _};
use async_compat::get_runtime_handle;
use matrix_sdk::{
authentication::oidc::{
registrations::{ClientId, OidcRegistrations},
requests::account_management::AccountManagementActionFull,
types::{
client_credentials::ClientCredentials,
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
requests::Prompt as SdkOidcPrompt,
},
OidcAuthorizationData, OidcSession,
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
},
event_cache::EventCacheError,
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
MediaThumbnailSettings,
MediaRetentionPolicy, MediaThumbnailSettings,
},
reqwest::StatusCode,
ruma::{
api::client::{
discovery::get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
user_directory::search_users,
},
events::{
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
message::MessageType,
},
AnyInitialStateEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
sliding_sync::Version as SdkSlidingSyncVersion,
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
store::RoomLoadSettings as SdkRoomLoadSettings,
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -47,16 +43,16 @@ use matrix_sdk_ui::notification_client::{
};
use mime::Mime;
use ruma::{
api::client::{
alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo,
uiaa::UserIdentifier,
},
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
events::{
ignored_user_list::IgnoredUserListEventContent,
key::verification::request::ToDeviceKeyVerificationRequestEvent,
room::{
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
message::OriginalSyncRoomMessageEvent,
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
@@ -70,13 +66,14 @@ use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
use super::{room::Room, session_verification::SessionVerificationController};
use crate::{
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
encryption::Encryption,
notification::NotificationClient,
notification_settings::NotificationSettings,
room::RoomHistoryVisibility,
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
ruma::{AuthData, MediaSource},
@@ -204,12 +201,27 @@ impl Client {
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(
move |event: ToDeviceKeyVerificationRequestEvent| async move {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(
&event.sender,
event.content.transaction_id,
)
.await;
}
},
);
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
}
});
@@ -231,7 +243,7 @@ impl Client {
client
.inner
.oidc()
.oauth()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
}
@@ -264,27 +276,16 @@ impl Client {
impl Client {
/// Information about login options for the client's homeserver.
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
let oidc = self.inner.oidc();
let (supports_oidc_login, supported_oidc_prompts) = match oidc
.fetch_authentication_issuer()
.await
{
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
Ok(metadata) => {
let prompts = metadata
.prompt_values_supported
.as_ref()
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
let oauth = self.inner.oauth();
let (supports_oidc_login, supported_oidc_prompts) = match oauth.server_metadata().await {
Ok(metadata) => {
let prompts =
metadata.prompt_values_supported.into_iter().map(Into::into).collect();
(true, prompts)
}
Err(error) => {
error!("Failed to fetch OIDC provider metadata: {error}");
(true, Default::default())
}
},
(true, prompts)
}
Err(error) => {
error!("Failed to fetch authentication issuer: {error}");
error!("Failed to fetch OIDC provider metadata: {error}");
(false, Default::default())
}
};
@@ -389,51 +390,46 @@ impl Client {
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns. If a failure occurs and a callback isn't available, make sure
/// to call `abort_oidc_auth` to inform the client of this.
///
/// # Arguments
///
/// * `oidc_configuration` - The configuration used to load the credentials
/// of the client if it is already registered with the authorization
/// server, or register the client and store its credentials if it isn't.
///
/// * `prompt` - The desired user experience in the web UI. No value means
/// that the user wishes to login into an existing account, and a value of
/// `Create` means that the user wishes to register a new account.
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: OidcPrompt,
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
let static_registrations = oidc_configuration
.static_registrations
.iter()
.filter_map(|(issuer, client_id)| {
let Ok(issuer) = Url::parse(issuer) else {
tracing::error!("Failed to parse {:?}", issuer);
return None;
};
Some((issuer, ClientId(client_id.clone())))
})
.collect::<HashMap<_, _>>();
let registrations = OidcRegistrations::new(
registrations_file,
oidc_metadata.clone(),
static_registrations,
)?;
prompt: Option<OidcPrompt>,
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
let registration_data = oidc_configuration.registration_data()?;
let redirect_uri = oidc_configuration.redirect_uri()?;
let data =
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
let mut url_builder = self.inner.oauth().login(redirect_uri, None, Some(registration_data));
if let Some(prompt) = prompt {
url_builder = url_builder.prompt(vec![prompt.into()]);
}
let data = url_builder.build().await?;
Ok(Arc::new(data))
}
/// Aborts an existing OIDC login operation that might have been cancelled,
/// failed etc.
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
self.inner.oidc().abort_authorization(&authorization_data.state).await;
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OAuthAuthorizationData>) {
self.inner.oauth().abort_login(&authorization_data.state).await;
}
/// Completes the OIDC login process.
pub async fn login_with_oidc_callback(
&self,
authorization_data: Arc<OidcAuthorizationData>,
callback_url: String,
) -> Result<(), OidcError> {
pub async fn login_with_oidc_callback(&self, callback_url: String) -> Result<(), OidcError> {
let url = Url::parse(&callback_url).or(Err(OidcError::CallbackUrlInvalid))?;
self.inner.oidc().login_with_oidc_callback(&authorization_data, url).await?;
self.inner.oauth().finish_login(url.into()).await?;
Ok(())
}
@@ -465,11 +461,34 @@ impl Client {
}
/// Restores the client from a `Session`.
///
/// It reloads the entire set of rooms from the previous session.
///
/// If you want to control the amount of rooms to reloads, check
/// [`Client::restore_session_with`].
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
self.restore_session_with(session, RoomLoadSettings::All).await
}
/// Restores the client from a `Session`.
///
/// It reloads a set of rooms controlled by [`RoomLoadSettings`].
pub async fn restore_session_with(
&self,
session: Session,
room_load_settings: RoomLoadSettings,
) -> Result<(), ClientError> {
let sliding_sync_version = session.sliding_sync_version.clone();
let auth_session: AuthSession = session.try_into()?;
self.restore_session_inner(auth_session).await?;
self.inner
.restore_session_with(
auth_session,
room_load_settings
.try_into()
.map_err(|error| ClientError::Generic { msg: error })?,
)
.await?;
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
Ok(())
@@ -498,7 +517,7 @@ impl Client {
let q = self.inner.send_queue();
let mut subscriber = q.subscribe_errors();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
// Respawn tasks for rooms that had unsent events. At this point we've just
// created the subscriber, so it'll be notified about errors.
q.respawn_tasks_for_rooms_with_unsent_requests().await;
@@ -533,15 +552,6 @@ impl Client {
}
impl Client {
/// Restores the client from an `AuthSession`.
pub(crate) async fn restore_session_inner(
&self,
session: impl Into<AuthSession>,
) -> anyhow::Result<()> {
self.inner.restore_session(session).await?;
Ok(())
}
/// Whether or not the client's homeserver supports the password login flow.
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
let login_types = self.inner.matrix_auth().get_login_types().await?;
@@ -578,7 +588,7 @@ impl Client {
delegate.map(|delegate| {
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
let client_clone = self.clone();
let session_change_task = RUNTIME.spawn(async move {
let session_change_task = get_runtime_handle().spawn(async move {
loop {
match session_change_receiver.recv().await {
Ok(session_change) => client_clone.process_session_change(session_change),
@@ -604,17 +614,24 @@ impl Client {
&self,
action: Option<AccountManagementAction>,
) -> Result<Option<String>, ClientError> {
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
if !matches!(self.inner.auth_api(), Some(AuthApi::OAuth(..))) {
return Ok(None);
}
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
Ok(url) => Ok(url.map(|u| u.to_string())),
let mut url_builder = match self.inner.oauth().account_management_url().await {
Ok(Some(url_builder)) => url_builder,
Ok(None) => return Ok(None),
Err(e) => {
tracing::error!("Failed retrieving account management URL: {e}");
Err(e.into())
error!("Failed retrieving account management URL: {e}");
return Err(e.into());
}
};
if let Some(action) = action {
url_builder = url_builder.action(action.into());
}
Ok(Some(url_builder.build().to_string()))
}
pub fn user_id(&self) -> Result<String, ClientError> {
@@ -663,8 +680,8 @@ impl Client {
}
/// Retrieves an avatar cached from a previous call to [`Self::avatar_url`].
pub fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
Ok(RUNTIME.block_on(self.inner.account().get_cached_avatar_url())?.map(Into::into))
pub async fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
Ok(self.inner.account().get_cached_avatar_url().await?.map(Into::into))
}
pub fn device_id(&self) -> Result<String, ClientError> {
@@ -710,7 +727,7 @@ impl Client {
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
get_runtime_handle().spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
@@ -777,8 +794,11 @@ impl Client {
.await?
.context("Failed retrieving user identity")?;
let session_verification_controller =
SessionVerificationController::new(self.inner.encryption(), user_identity);
let session_verification_controller = SessionVerificationController::new(
self.inner.encryption(),
user_identity,
self.inner.account(),
);
*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
@@ -786,34 +806,9 @@ impl Client {
Ok(Arc::new(session_verification_controller))
}
/// Log out the current user. This method returns an optional URL that
/// should be presented to the user to complete logout (in the case of
/// Session having been authenticated using OIDC).
pub async fn logout(&self) -> Result<Option<String>, ClientError> {
let Some(auth_api) = self.inner.auth_api() else {
return Err(anyhow!("Missing authentication API").into());
};
match auth_api {
AuthApi::Matrix(a) => {
tracing::info!("Logging out via the homeserver.");
a.logout().await?;
Ok(None)
}
AuthApi::Oidc(api) => {
tracing::info!("Logging out via OIDC.");
let end_session_builder = api.logout().await?;
if let Some(builder) = end_session_builder {
let url = builder.build()?.url;
return Ok(Some(url.to_string()));
}
Ok(None)
}
_ => Err(anyhow!("Unknown authentication API").into()),
}
/// Log the current user out.
pub async fn logout(&self) -> Result<(), ClientError> {
Ok(self.inner.logout().await?)
}
/// Registers a pusher with given parameters
@@ -870,6 +865,24 @@ impl Client {
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
}
/// Get a room by its ID.
///
/// # Arguments
///
/// * `room_id` - The ID of the room to get.
///
/// # Returns
///
/// A `Result` containing an optional room, or a `ClientError`.
/// This method will not initialize the room's timeline or populate it with
/// events.
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let sdk_room = self.inner.get_room(&room_id);
let room = sdk_room.map(|room| Arc::new(Room::new(room)));
Ok(room)
}
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_room = self.inner.get_dm_room(&user_id);
@@ -913,8 +926,8 @@ impl Client {
SyncServiceBuilder::new((*self.inner).clone())
}
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
let inner = RUNTIME.block_on(self.inner.notification_settings());
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
let inner = self.inner.notification_settings().await;
Arc::new(NotificationSettings::new((*self.inner).clone(), inner))
}
@@ -959,7 +972,7 @@ impl Client {
listener: Box<dyn IgnoredUsersListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.subscribe_to_ignore_user_list_changes();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(user_ids) = subscriber.next().await {
listener.call(user_ids);
}
@@ -1044,11 +1057,12 @@ impl Client {
let room_alias = RoomAliasId::parse(&room_alias)?;
match self.inner.resolve_room_alias(&room_alias).await {
Ok(response) => Ok(Some(response.into())),
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
Some(StatusCode::NOT_FOUND) => Ok(None),
_ => Err(http_error.into()),
Err(error) => match error.client_api_error_kind() {
// The room alias wasn't found, so we return None.
Some(ErrorKind::NotFound) => Ok(None),
// In any other case we just return the error, mapped.
_ => Err(error.into()),
},
Err(error) => Err(error.into()),
}
}
@@ -1152,6 +1166,42 @@ impl Client {
let alias = RoomAliasId::parse(alias)?;
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
}
/// Set the media retention policy.
pub async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), ClientError> {
let closure = async || -> Result<_, EventCacheError> {
let store = self.inner.event_cache_store().lock().await?;
Ok(store.set_media_retention_policy(policy).await?)
};
Ok(closure().await?)
}
/// Clear all the non-critical caches for this Client instance.
///
/// - This will empty all the room's persisted event caches, so all rooms
/// will start as if they were empty.
/// - This will empty the media cache according to the current media
/// retention policy.
pub async fn clear_caches(&self) -> Result<(), ClientError> {
let closure = async || -> Result<_, EventCacheError> {
// Clean up the media cache according to the current media retention policy.
self.inner.event_cache_store().lock().await?.clean_up_media_cache().await?;
// Clear all the room chunks. It's important to *not* call
// `EventCacheStore::clear_all_rooms_chunks` here, because there might be live
// observers of the linked chunks, and that would cause some very bad state
// mismatch.
self.inner.event_cache().clear_all_rooms().await?;
Ok(())
};
Ok(closure().await?)
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
@@ -1232,7 +1282,7 @@ impl Client {
fn process_session_change(&self, session_change: SessionChange) {
if let Some(delegate) = self.delegate.read().unwrap().clone() {
debug!("Applying session change: {session_change:?}");
RUNTIME.spawn_blocking(move || match session_change {
get_runtime_handle().spawn_blocking(move || match session_change {
SessionChange::UnknownToken { soft_logout } => {
delegate.did_receive_auth_error(soft_logout);
}
@@ -1251,13 +1301,7 @@ impl Client {
session_delegate: Arc<dyn ClientSessionDelegate>,
user_id: &UserId,
) -> anyhow::Result<SessionTokens> {
let session = session_delegate.retrieve_session_from_keychain(user_id.to_string())?;
let auth_session = TryInto::<AuthSession>::try_into(session)?;
match auth_session {
AuthSession::Oidc(session) => Ok(SessionTokens::Oidc(session.user.tokens)),
AuthSession::Matrix(session) => Ok(SessionTokens::Matrix(session.tokens)),
_ => anyhow::bail!("Unexpected session kind."),
}
Ok(session_delegate.retrieve_session_from_keychain(user_id.to_string())?.into_tokens())
}
fn session_inner(client: matrix_sdk::Client) -> Result<Session, ClientError> {
@@ -1279,6 +1323,38 @@ impl Client {
}
}
/// Configure how many rooms will be restored when restoring the session with
/// [`Client::restore_session_with`].
///
/// Please, see the documentation of [`matrix_sdk::store::RoomLoadSettings`] to
/// learn more.
#[derive(uniffi::Enum)]
pub enum RoomLoadSettings {
/// Load all rooms from the `StateStore` into the in-memory state store
/// `BaseStateStore`.
All,
/// Load a single room from the `StateStore` into the in-memory state
/// store `BaseStateStore`.
///
/// Please, be careful with this option. Read the documentation of
/// [`RoomLoadSettings`].
One { room_id: String },
}
impl TryInto<SdkRoomLoadSettings> for RoomLoadSettings {
type Error = String;
fn try_into(self) -> Result<SdkRoomLoadSettings, Self::Error> {
Ok(match self {
Self::All => SdkRoomLoadSettings::All,
Self::One { room_id } => {
SdkRoomLoadSettings::One(RoomId::parse(room_id).map_err(|error| error.to_string())?)
}
})
}
}
#[derive(uniffi::Record)]
pub struct NotificationPowerLevels {
pub room: i32,
@@ -1378,6 +1454,8 @@ pub struct CreateRoomParameters {
#[uniffi(default = None)]
pub join_rule_override: Option<JoinRule>,
#[uniffi(default = None)]
pub history_visibility_override: Option<RoomHistoryVisibility>,
#[uniffi(default = None)]
pub canonical_alias: Option<String>,
}
@@ -1425,6 +1503,12 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
if let Some(history_visibility_override) = value.history_visibility_override {
let content =
RoomHistoryVisibilityEventContent::new(history_visibility_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
if let Some(power_levels) = value.power_level_content_override {
@@ -1537,11 +1621,7 @@ impl Session {
AuthApi::Matrix(a) => {
let matrix_sdk::authentication::matrix::MatrixSession {
meta: matrix_sdk::SessionMeta { user_id, device_id },
tokens:
matrix_sdk::authentication::matrix::MatrixSessionTokens {
access_token,
refresh_token,
},
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
} = a.session().context("Missing session")?;
Ok(Session {
@@ -1555,30 +1635,13 @@ impl Session {
})
}
// Build the session from the OIDC UserSession.
AuthApi::Oidc(api) => {
let matrix_sdk::authentication::oidc::UserSession {
AuthApi::OAuth(api) => {
let matrix_sdk::authentication::oauth::UserSession {
meta: matrix_sdk::SessionMeta { user_id, device_id },
tokens:
matrix_sdk::authentication::oidc::OidcSessionTokens {
access_token,
refresh_token,
latest_id_token,
},
issuer,
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
} = api.user_session().context("Missing session")?;
let client_id = api
.client_credentials()
.context("OIDC client credentials are missing.")?
.client_id()
.to_owned();
let client_metadata =
api.client_metadata().context("OIDC client metadata is missing.")?.clone();
let oidc_data = OidcSessionData {
client_id,
client_metadata,
latest_id_token: latest_id_token.map(|t| t.to_string()),
issuer,
};
let client_id = api.client_id().context("OIDC client ID is missing.")?.clone();
let oidc_data = OidcSessionData { client_id };
let oidc_data = serde_json::to_string(&oidc_data).ok();
Ok(Session {
@@ -1594,6 +1657,10 @@ impl Session {
_ => Err(anyhow!("Unknown authentication API").into()),
}
}
fn into_tokens(self) -> matrix_sdk::SessionTokens {
SessionTokens { access_token: self.access_token, refresh_token: self.refresh_token }
}
}
impl TryFrom<Session> for AuthSession {
@@ -1611,35 +1678,19 @@ impl TryFrom<Session> for AuthSession {
if let Some(oidc_data) = oidc_data {
// Create an OidcSession.
let oidc_data = serde_json::from_str::<OidcUnvalidatedSessionData>(&oidc_data)?
.validate()
.context("OIDC metadata validation failed.")?;
let latest_id_token = oidc_data
.latest_id_token
.map(TryInto::try_into)
.transpose()
.context("OIDC latest_id_token is invalid.")?;
let oidc_data = serde_json::from_str::<OidcSessionData>(&oidc_data)?;
let user_session = matrix_sdk::authentication::oidc::UserSession {
let user_session = matrix_sdk::authentication::oauth::UserSession {
meta: matrix_sdk::SessionMeta {
user_id: user_id.try_into()?,
device_id: device_id.into(),
},
tokens: matrix_sdk::authentication::oidc::OidcSessionTokens {
access_token,
refresh_token,
latest_id_token,
},
issuer: oidc_data.issuer,
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
};
let session = OidcSession {
credentials: ClientCredentials::None { client_id: oidc_data.client_id },
metadata: oidc_data.client_metadata,
user: user_session,
};
let session = OAuthSession { client_id: oidc_data.client_id, user: user_session };
Ok(AuthSession::Oidc(session.into()))
Ok(AuthSession::OAuth(session.into()))
} else {
// Create a regular Matrix Session.
let session = matrix_sdk::authentication::matrix::MatrixSession {
@@ -1647,10 +1698,7 @@ impl TryFrom<Session> for AuthSession {
user_id: user_id.try_into()?,
device_id: device_id.into(),
},
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
access_token,
refresh_token,
},
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
};
Ok(AuthSession::Matrix(session))
@@ -1660,64 +1708,9 @@ impl TryFrom<Session> for AuthSession {
/// Represents a client registration against an OpenID Connect authentication
/// issuer.
#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub(crate) struct OidcSessionData {
client_id: String,
client_metadata: VerifiedClientMetadata,
latest_id_token: Option<String>,
issuer: String,
}
/// Represents an unverified client registration against an OpenID Connect
/// authentication issuer. Call `validate` on this to use it for restoration.
#[derive(Deserialize)]
#[serde(try_from = "OidcUnvalidatedSessionDataDeHelper")]
pub(crate) struct OidcUnvalidatedSessionData {
client_id: String,
client_metadata: ClientMetadata,
latest_id_token: Option<String>,
issuer: String,
}
impl OidcUnvalidatedSessionData {
/// Validates the data so that it can be used.
fn validate(self) -> Result<OidcSessionData, ClientMetadataVerificationError> {
Ok(OidcSessionData {
client_id: self.client_id,
client_metadata: self.client_metadata.validate()?,
latest_id_token: self.latest_id_token,
issuer: self.issuer,
})
}
}
#[derive(Deserialize)]
struct OidcUnvalidatedSessionDataDeHelper {
client_id: String,
client_metadata: ClientMetadata,
latest_id_token: Option<String>,
issuer_info: Option<AuthenticationServerInfo>,
issuer: Option<String>,
}
impl TryFrom<OidcUnvalidatedSessionDataDeHelper> for OidcUnvalidatedSessionData {
type Error = String;
fn try_from(value: OidcUnvalidatedSessionDataDeHelper) -> Result<Self, Self::Error> {
let OidcUnvalidatedSessionDataDeHelper {
client_id,
client_metadata,
latest_id_token,
issuer_info,
issuer,
} = value;
let issuer = issuer
.or(issuer_info.map(|info| info.issuer))
.ok_or_else(|| "missing field `issuer`".to_owned())?;
Ok(Self { client_id, client_metadata, latest_id_token, issuer })
}
client_id: ClientId,
}
#[derive(uniffi::Enum)]
@@ -1735,8 +1728,12 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
match value {
AccountManagementAction::Profile => Self::Profile,
AccountManagementAction::SessionsList => Self::SessionsList,
AccountManagementAction::SessionView { device_id } => Self::SessionView { device_id },
AccountManagementAction::SessionEnd { device_id } => Self::SessionEnd { device_id },
AccountManagementAction::SessionView { device_id } => {
Self::SessionView { device_id: device_id.into() }
}
AccountManagementAction::SessionEnd { device_id } => {
Self::SessionEnd { device_id: device_id.into() }
}
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
}
@@ -1798,7 +1795,6 @@ impl MediaFileHandle {
#[derive(Clone, uniffi::Enum)]
pub enum SlidingSyncVersion {
None,
Proxy { url: String },
Native,
}
@@ -1806,7 +1802,6 @@ impl From<SdkSlidingSyncVersion> for SlidingSyncVersion {
fn from(value: SdkSlidingSyncVersion) -> Self {
match value {
SdkSlidingSyncVersion::None => Self::None,
SdkSlidingSyncVersion::Proxy { url } => Self::Proxy { url: url.to_string() },
SdkSlidingSyncVersion::Native => Self::Native,
}
}
@@ -1818,9 +1813,6 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
fn try_from(value: SlidingSyncVersion) -> Result<Self, Self::Error> {
Ok(match value {
SlidingSyncVersion::None => Self::None,
SlidingSyncVersion::Proxy { url } => Self::Proxy {
url: Url::parse(&url).map_err(|e| ClientError::Generic { msg: e.to_string() })?,
},
SlidingSyncVersion::Native => Self::Native,
})
}
@@ -1828,9 +1820,11 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
#[derive(Clone, uniffi::Enum)]
pub enum OidcPrompt {
/// The Authorization Server must not display any authentication or consent
/// user interface pages.
None,
/// The Authorization Server should prompt the End-User to create a user
/// account.
///
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
Create,
/// The Authorization Server should prompt the End-User for
/// reauthentication.
@@ -1840,47 +1834,30 @@ pub enum OidcPrompt {
/// returning information to the Client.
Consent,
/// The Authorization Server should prompt the End-User to select a user
/// account.
///
/// This enables an End-User who has multiple accounts at the Authorization
/// Server to select amongst the multiple accounts that they might have
/// current sessions for.
SelectAccount,
/// The Authorization Server should prompt the End-User to create a user
/// account.
///
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
Create,
/// An unknown value.
Unknown { value: String },
}
impl From<&SdkOidcPrompt> for OidcPrompt {
fn from(value: &SdkOidcPrompt) -> Self {
impl From<RumaOidcPrompt> for OidcPrompt {
fn from(value: RumaOidcPrompt) -> Self {
match value {
SdkOidcPrompt::None => Self::None,
SdkOidcPrompt::Login => Self::Login,
SdkOidcPrompt::Consent => Self::Consent,
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
SdkOidcPrompt::Create => Self::Create,
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
_ => Self::Unknown { value: value.to_string() },
RumaOidcPrompt::Create => Self::Create,
value => match value.as_str() {
"consent" => Self::Consent,
"login" => Self::Login,
_ => Self::Unknown { value: value.to_string() },
},
}
}
}
impl From<OidcPrompt> for SdkOidcPrompt {
impl From<OidcPrompt> for RumaOidcPrompt {
fn from(value: OidcPrompt) -> Self {
match value {
OidcPrompt::None => Self::None,
OidcPrompt::Login => Self::Login,
OidcPrompt::Consent => Self::Consent,
OidcPrompt::SelectAccount => Self::SelectAccount,
OidcPrompt::Create => Self::Create,
OidcPrompt::Unknown { value } => Self::Unknown(value),
OidcPrompt::Consent => Self::from("consent"),
OidcPrompt::Login => Self::from("login"),
OidcPrompt::Unknown { value } => value.into(),
}
}
}
+123 -46
View File
@@ -1,8 +1,9 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::{
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
CollectStrategy, TrustRequirement,
@@ -16,14 +17,13 @@ use matrix_sdk::{
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError,
RumaApiError, SqliteStoreConfig,
};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use url::Url;
use zeroize::Zeroizing;
use super::{client::Client, RUNTIME};
use super::client::Client;
use crate::{
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
@@ -104,7 +104,7 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
_ => HumanQrLoginError::Unknown,
},
QRCodeLoginError::Oidc(e) => {
QRCodeLoginError::OAuth(e) => {
if let Some(e) = e.as_request_token_error() {
match e {
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
@@ -153,8 +153,8 @@ pub enum QrLoginProgress {
/// first digit is a zero.
check_code_string: String,
},
/// We are waiting for the login and for the OIDC provider to give us an
/// access token.
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// The login has successfully finished.
Done,
@@ -255,9 +255,13 @@ impl From<ClientError> for ClientBuildError {
#[derive(Clone, uniffi::Object)]
pub struct ClientBuilder {
session_paths: Option<SessionPaths>,
session_passphrase: Zeroizing<Option<String>>,
session_pool_max_size: Option<usize>,
session_cache_size: Option<u32>,
session_journal_size_limit: Option<u32>,
system_is_memory_constrained: bool,
username: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
passphrase: Zeroizing<Option<String>>,
user_agent: Option<String>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
proxy: Option<String>,
@@ -284,9 +288,13 @@ impl ClientBuilder {
pub fn new() -> Arc<Self> {
Arc::new(Self {
session_paths: None,
session_passphrase: Zeroizing::new(None),
session_pool_max_size: None,
session_cache_size: None,
session_journal_size_limit: None,
system_is_memory_constrained: false,
username: None,
homeserver_cfg: None,
passphrase: Zeroizing::new(None),
user_agent: None,
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
proxy: None,
@@ -363,6 +371,74 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set the passphrase for the stores given to
/// [`ClientBuilder::session_paths`].
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_passphrase = Zeroizing::new(passphrase);
Arc::new(builder)
}
/// Set the pool max size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes an async pool of connections. This method controls
/// the size of the pool. The larger the pool is, the more memory is
/// consumed, but also the more the app is reactive because it doesn't need
/// to wait on a pool to be available to run queries.
///
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_pool_max_size = pool_max_size
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
Arc::new(builder)
}
/// Set the cache size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes a SQLite connection. This method controls the cache
/// size, in **bytes (!)**.
///
/// The cache represents data SQLite holds in memory at once per open
/// database file. The default cache implementation does not allocate the
/// full amount of cache memory all at once. Cache memory is allocated
/// in smaller chunks on an as-needed basis.
///
/// See [`SqliteStoreConfig::cache_size`] to learn more.
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_cache_size = cache_size;
Arc::new(builder)
}
/// Set the size limit for the SQLite WAL files of stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store uses the WAL journal mode. This method controls the size
/// limit of the WAL files, in **bytes (!)**.
///
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_journal_size_limit = limit;
Arc::new(builder)
}
/// Tell the client that the system is memory constrained, like in a push
/// notification process for example.
///
/// So far, at the time of writing (2025-04-07), it changes the defaults of
/// [`SqliteStoreConfig`], so one might not need to call
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
/// check [`SqliteStoreConfig::with_low_memory_config`].
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.system_is_memory_constrained = true;
Arc::new(builder)
}
pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.username = Some(username);
@@ -387,12 +463,6 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.passphrase = Zeroizing::new(passphrase);
Arc::new(builder)
}
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.user_agent = Some(user_agent);
@@ -521,11 +591,29 @@ impl ClientBuilder {
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
inner_builder = inner_builder.sqlite_store_with_cache_path(
data_path,
cache_path,
builder.passphrase.as_deref(),
);
let mut sqlite_store_config = if builder.system_is_memory_constrained {
SqliteStoreConfig::with_low_memory_config(data_path)
} else {
SqliteStoreConfig::new(data_path)
};
sqlite_store_config =
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
if let Some(size) = builder.session_pool_max_size {
sqlite_store_config = sqlite_store_config.pool_max_size(size);
}
if let Some(size) = builder.session_cache_size {
sqlite_store_config = sqlite_store_config.cache_size(size);
}
if let Some(limit) = builder.session_journal_size_limit {
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
}
inner_builder = inner_builder
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
} else {
debug!("Not using a store path.");
}
@@ -604,22 +692,10 @@ impl ClientBuilder {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
}
SlidingSyncVersionBuilder::Proxy { url } => {
inner_builder = inner_builder.sliding_sync_version_builder(
MatrixSlidingSyncVersionBuilder::Proxy {
url: Url::parse(&url)
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
},
)
}
SlidingSyncVersionBuilder::Native => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
}
SlidingSyncVersionBuilder::DiscoverProxy => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
}
SlidingSyncVersionBuilder::DiscoverNative => {
inner_builder = inner_builder
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
@@ -629,7 +705,8 @@ impl ClientBuilder {
if let Some(config) = builder.request_config {
let mut updated_config = matrix_sdk::config::RequestConfig::default();
if let Some(retry_limit) = config.retry_limit {
updated_config = updated_config.retry_limit(retry_limit);
updated_config =
updated_config.retry_limit(retry_limit.try_into().unwrap_or(usize::MAX));
}
if let Some(timeout) = config.timeout {
updated_config = updated_config.timeout(Duration::from_millis(timeout));
@@ -641,8 +718,9 @@ impl ClientBuilder {
));
}
}
if let Some(retry_timeout) = config.retry_timeout {
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
if let Some(max_retry_time) = config.max_retry_time {
updated_config =
updated_config.max_retry_time(Duration::from_millis(max_retry_time));
}
inner_builder = inner_builder.request_config(updated_config);
}
@@ -673,8 +751,8 @@ impl ClientBuilder {
///
/// This method will build the client and immediately attempt to log the
/// client in using the provided [`QrCodeData`] using the login
/// mechanism described in [MSC4108]. As such this methods requires OIDC
/// support as well as sliding sync support.
/// mechanism described in [MSC4108]. As such this methods requires OAuth
/// 2.0 support as well as sliding sync support.
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
@@ -700,17 +778,18 @@ impl ClientBuilder {
}
})?;
let client_metadata =
oidc_configuration.try_into().map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let registration_data = oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let oidc = client.inner.oidc();
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
let oauth = client.inner.oauth();
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(&registration_data));
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
@@ -722,8 +801,8 @@ impl ClientBuilder {
}
}
#[derive(Clone)]
/// The store paths the client will use when built.
#[derive(Clone)]
struct SessionPaths {
/// The path that the client will use to store its data.
data_path: String,
@@ -742,14 +821,12 @@ pub struct RequestConfig {
/// Max number of concurrent requests. No value means no limits.
max_concurrent_requests: Option<u64>,
/// Base delay between retries.
retry_timeout: Option<u64>,
max_retry_time: Option<u64>,
}
#[derive(Clone, uniffi::Enum)]
pub enum SlidingSyncVersionBuilder {
None,
Proxy { url: String },
Native,
DiscoverProxy,
DiscoverNative,
}
+31 -18
View File
@@ -1,5 +1,6 @@
use std::sync::Arc;
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
@@ -9,7 +10,6 @@ use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
#[derive(uniffi::Object)]
@@ -230,7 +230,7 @@ impl Encryption {
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
let mut stream = self.inner.backups().state_stream();
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = stream.next().await {
let Ok(state) = state else { continue };
listener.on_update(state.into());
@@ -267,7 +267,7 @@ impl Encryption {
) -> Arc<TaskHandle> {
let mut stream = self.inner.recovery().state_stream();
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = stream.next().await {
listener.on_update(state.into());
}
@@ -294,7 +294,7 @@ impl Encryption {
let task = if let Some(listener) = progress_listener {
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
Some(RUNTIME.spawn(async move {
Some(get_runtime_handle().spawn(async move {
while let Some(progress) = progress_stream.next().await {
let Ok(progress) = progress else { continue };
listener.on_update(progress.into());
@@ -335,7 +335,7 @@ impl Encryption {
let mut progress_stream = enable.subscribe_to_progress();
let task = RUNTIME.spawn(async move {
let task = get_runtime_handle().spawn(async move {
while let Some(progress) = progress_stream.next().await {
let Ok(progress) = progress else { continue };
progress_listener.on_update(progress.into());
@@ -400,7 +400,7 @@ impl Encryption {
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.verification_state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(verification_state) = subscriber.next().await {
listener.on_update(verification_state.into());
}
@@ -478,15 +478,6 @@ impl UserIdentity {
Ok(self.inner.pin().await?)
}
/// Remove the requirement for this identity to be verified.
///
/// If an identity was previously verified and is not anymore it will be
/// reported to the user. In order to remove this notice users have to
/// verify again or to withdraw the verification requirement.
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
Ok(self.inner.withdraw_verification().await?)
}
/// Get the public part of the Master key of this user identity.
///
/// The public part of the Master key is usually used to uniquely identify
@@ -504,6 +495,28 @@ impl UserIdentity {
pub fn is_verified(&self) -> bool {
self.inner.is_verified()
}
/// True if we verified this identity at some point in the past.
///
/// To reset this latch back to `false`, one must call
/// [`UserIdentity::withdraw_verification()`].
pub fn was_previously_verified(&self) -> bool {
self.inner.was_previously_verified()
}
/// Remove the requirement for this identity to be verified.
///
/// If an identity was previously verified and is not anymore it will be
/// reported to the user. In order to remove this notice users have to
/// verify again or to withdraw the verification requirement.
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
Ok(self.inner.withdraw_verification().await?)
}
/// Was this identity previously verified, and is no longer?
pub fn has_verification_violation(&self) -> bool {
self.inner.has_verification_violation()
}
}
#[derive(uniffi::Object)]
@@ -557,7 +570,7 @@ impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningRe
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
match value {
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
encryption::CrossSigningResetAuthType::OAuth(info) => Self::Oidc { info: info.into() },
}
}
}
@@ -568,8 +581,8 @@ pub struct OidcCrossSigningResetInfo {
pub approval_url: String,
}
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
impl From<&matrix_sdk::encryption::OAuthCrossSigningResetInfo> for OidcCrossSigningResetInfo {
fn from(value: &matrix_sdk::encryption::OAuthCrossSigningResetInfo) -> Self {
Self { approval_url: value.approval_url.to_string() }
}
}
+5 -3
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
use matrix_sdk::{
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
@@ -137,8 +137,8 @@ impl From<sync_service::Error> for ClientError {
}
}
impl From<OidcError> for ClientError {
fn from(e: OidcError) -> Self {
impl From<OAuthError> for ClientError {
fn from(e: OAuthError) -> Self {
Self::new(e)
}
}
@@ -288,6 +288,8 @@ pub enum RoomError {
TimelineUnavailable,
#[error("Invalid thumbnail data")]
InvalidThumbnailData,
#[error("Invalid replied to event ID")]
InvalidRepliedToEventId,
#[error("Failed sending attachment")]
FailedSendingAttachment,
}
-2
View File
@@ -30,12 +30,10 @@ mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod timeline_event_filter;
mod tracing;
mod utils;
mod widget;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
use self::{
+11 -2
View File
@@ -5,7 +5,11 @@ use matrix_sdk_ui::notification_client::{
};
use ruma::{EventId, RoomId};
use crate::{client::Client, error::ClientError, event::TimelineEvent};
use crate::{
client::{Client, JoinRule},
error::ClientError,
event::TimelineEvent,
};
#[derive(uniffi::Enum)]
pub enum NotificationEvent {
@@ -25,9 +29,11 @@ pub struct NotificationRoomInfo {
pub display_name: String,
pub avatar_url: Option<String>,
pub canonical_alias: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_public: bool,
}
#[derive(uniffi::Record)]
@@ -42,6 +48,7 @@ pub struct NotificationItem {
/// information to create a push context.
pub is_noisy: Option<bool>,
pub has_mention: Option<bool>,
pub thread_id: Option<String>,
}
impl NotificationItem {
@@ -54,7 +61,6 @@ impl NotificationItem {
NotificationEvent::Invite { sender: event.sender.to_string() }
}
};
Self {
event,
sender_info: NotificationSenderInfo {
@@ -66,12 +72,15 @@ impl NotificationItem {
display_name: item.room_computed_display_name,
avatar_url: item.room_avatar_url,
canonical_alias: item.room_canonical_alias,
join_rule: item.room_join_rule.try_into().ok(),
joined_members_count: item.joined_members_count,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_public: item.is_room_public,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
thread_id: item.thread_id.map(|t| t.to_string()),
}
}
}
@@ -10,13 +10,350 @@ use matrix_sdk::{
Client as MatrixClient,
};
use ruma::{
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
RoomId,
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
},
Int, RoomId, UInt,
};
use tokio::sync::RwLock as AsyncRwLock;
use crate::error::NotificationSettingsError;
#[derive(Clone, Default, uniffi::Enum)]
pub enum ComparisonOperator {
/// Equals
#[default]
Eq,
/// Less than
Lt,
/// Greater than
Gt,
/// Greater or equal
Ge,
/// Less or equal
Le,
}
impl From<SdkComparisonOperator> for ComparisonOperator {
fn from(value: SdkComparisonOperator) -> Self {
match value {
SdkComparisonOperator::Eq => Self::Eq,
SdkComparisonOperator::Lt => Self::Lt,
SdkComparisonOperator::Gt => Self::Gt,
SdkComparisonOperator::Ge => Self::Ge,
SdkComparisonOperator::Le => Self::Le,
}
}
}
impl From<ComparisonOperator> for SdkComparisonOperator {
fn from(value: ComparisonOperator) -> Self {
match value {
ComparisonOperator::Eq => Self::Eq,
ComparisonOperator::Lt => Self::Lt,
ComparisonOperator::Gt => Self::Gt,
ComparisonOperator::Ge => Self::Ge,
ComparisonOperator::Le => Self::Le,
}
}
}
#[derive(Debug, Clone, Default, uniffi::Enum)]
pub enum JsonValue {
/// Represents a `null` value.
#[default]
Null,
/// Represents a boolean.
Bool { value: bool },
/// Represents an integer.
Integer { value: i64 },
/// Represents a string.
String { value: String },
}
impl From<SdkJsonValue> for JsonValue {
fn from(value: SdkJsonValue) -> Self {
match value {
SdkJsonValue::Null => Self::Null,
SdkJsonValue::Bool(b) => Self::Bool { value: b },
SdkJsonValue::Integer(i) => Self::Integer { value: i.into() },
SdkJsonValue::String(s) => Self::String { value: s },
}
}
}
impl From<JsonValue> for SdkJsonValue {
fn from(value: JsonValue) -> Self {
match value {
JsonValue::Null => Self::Null,
JsonValue::Bool { value } => Self::Bool(value),
JsonValue::Integer { value } => Self::Integer(Int::new(value).unwrap_or_default()),
JsonValue::String { value } => Self::String(value),
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum PushCondition {
/// A glob pattern match on a field of the event.
EventMatch {
/// The [dot-separated path] of the property of the event to match.
///
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
key: String,
/// The glob-style pattern to match against.
///
/// Patterns with no special glob characters should be treated as having
/// asterisks prepended and appended when testing the condition.
pattern: String,
},
/// Matches unencrypted messages where `content.body` contains the owner's
/// display name in that room.
ContainsDisplayName,
/// Matches the current number of members in the room.
RoomMemberCount { prefix: ComparisonOperator, count: u64 },
/// Takes into account the current power levels in the room, ensuring the
/// sender of the event has high enough power to trigger the
/// notification.
SenderNotificationPermission {
/// The field in the power level event the user needs a minimum power
/// level for.
///
/// Fields must be specified under the `notifications` property in the
/// power level event's `content`.
key: String,
},
/// Exact value match on a property of the event.
EventPropertyIs {
/// The [dot-separated path] of the property of the event to match.
///
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
key: String,
/// The value to match against.
value: JsonValue,
},
/// Exact value match on a value in an array property of the event.
EventPropertyContains {
/// The [dot-separated path] of the property of the event to match.
///
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
key: String,
/// The value to match against.
value: JsonValue,
},
}
impl TryFrom<SdkPushCondition> for PushCondition {
type Error = ();
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
Ok(match value {
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
SdkPushCondition::RoomMemberCount { is } => {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
}
SdkPushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key }
}
SdkPushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
SdkPushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
}
_ => return Err(()),
})
}
}
impl From<PushCondition> for SdkPushCondition {
fn from(value: PushCondition) -> Self {
match value {
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
is: RoomMemberCountIs {
prefix: prefix.into(),
count: UInt::new(count).unwrap_or_default(),
},
},
PushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key }
}
PushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
PushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
}
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum RuleKind {
/// User-configured rules that override all other kinds.
Override,
/// Lowest priority user-defined rules.
Underride,
/// Sender-specific rules.
Sender,
/// Room-specific rules.
Room,
/// Content-specific rules.
Content,
Custom {
value: String,
},
}
impl From<SdkRuleKind> for RuleKind {
fn from(value: SdkRuleKind) -> Self {
match value {
SdkRuleKind::Override => Self::Override,
SdkRuleKind::Underride => Self::Underride,
SdkRuleKind::Sender => Self::Sender,
SdkRuleKind::Room => Self::Room,
SdkRuleKind::Content => Self::Content,
SdkRuleKind::_Custom(_) => Self::Custom { value: value.as_str().to_owned() },
_ => Self::Custom { value: value.to_string() },
}
}
}
impl From<RuleKind> for SdkRuleKind {
fn from(value: RuleKind) -> Self {
match value {
RuleKind::Override => Self::Override,
RuleKind::Underride => Self::Underride,
RuleKind::Sender => Self::Sender,
RuleKind::Room => Self::Room,
RuleKind::Content => Self::Content,
RuleKind::Custom { value } => SdkRuleKind::from(value),
}
}
}
#[derive(Clone, uniffi::Enum)]
/// Enum representing the push notification tweaks for a rule.
pub enum Tweak {
/// A string representing the sound to be played when this notification
/// arrives.
///
/// A value of "default" means to play a default sound. A device may choose
/// to alert the user by some other means if appropriate, eg. vibration.
Sound { value: String },
/// A boolean representing whether or not this message should be highlighted
/// in the UI.
Highlight { value: bool },
/// A custom tweak
Custom {
/// The name of the custom tweak (`set_tweak` field)
name: String,
/// The value of the custom tweak as an encoded JSON string
value: String,
},
}
impl TryFrom<SdkTweak> for Tweak {
type Error = String;
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
Ok(match value {
SdkTweak::Sound(sound) => Self::Sound { value: sound },
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
SdkTweak::Custom { name, value } => {
let json_string = serde_json::to_string(&value)
.map_err(|e| format!("Failed to serialize custom tweak value: {}", e))?;
Self::Custom { name, value: json_string }
}
_ => return Err("Unsupported tweak type".to_owned()),
})
}
}
impl TryFrom<Tweak> for SdkTweak {
type Error = String;
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
Ok(match value {
Tweak::Sound { value } => Self::Sound(value),
Tweak::Highlight { value } => Self::Highlight(value),
Tweak::Custom { name, value } => {
let json_value: serde_json::Value = serde_json::from_str(&value)
.map_err(|e| format!("Failed to deserialize custom tweak value: {}", e))?;
let value = serde_json::from_value(json_value)
.map_err(|e| format!("Failed to convert JSON value: {}", e))?;
Self::Custom { name, value }
}
})
}
}
#[derive(Clone, uniffi::Enum)]
/// Enum representing the push notification actions for a rule.
pub enum Action {
/// Causes matching events to generate a notification.
Notify,
/// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
SetTweak { value: Tweak },
}
impl TryFrom<SdkAction> for Action {
type Error = String;
fn try_from(value: SdkAction) -> Result<Self, Self::Error> {
Ok(match value {
SdkAction::Notify => Self::Notify,
SdkAction::SetTweak(tweak) => Self::SetTweak {
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
},
_ => return Err("Unsupported action type".to_owned()),
})
}
}
impl TryFrom<Action> for SdkAction {
type Error = String;
fn try_from(value: Action) -> Result<Self, Self::Error> {
Ok(match value {
Action::Notify => Self::Notify,
Action::SetTweak { value } => Self::SetTweak(
value.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
),
})
}
}
/// Enum representing the push notification modes for a room.
#[derive(Clone, uniffi::Enum)]
pub enum RoomNotificationMode {
@@ -267,7 +604,7 @@ impl NotificationSettings {
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
.await?;
Ok(enabled)
}
@@ -280,7 +617,7 @@ impl NotificationSettings {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(
RuleKind::Override,
SdkRuleKind::Override,
PredefinedOverrideRuleId::IsRoomMention,
enabled,
)
@@ -292,7 +629,7 @@ impl NotificationSettings {
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
.await?;
Ok(enabled)
}
@@ -304,14 +641,14 @@ impl NotificationSettings {
let notification_settings = self.sdk_notification_settings.read().await;
// Check stable identifier
if let Ok(enabled) = notification_settings
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
.is_push_rule_enabled(SdkRuleKind::Override, ".m.rule.encrypted_event")
.await
{
enabled
} else {
// Check unstable identifier
notification_settings
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
.is_push_rule_enabled(SdkRuleKind::Override, ".org.matrix.msc4028.encrypted_event")
.await
.unwrap_or(false)
}
@@ -332,7 +669,7 @@ impl NotificationSettings {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(
RuleKind::Override,
SdkRuleKind::Override,
PredefinedOverrideRuleId::IsUserMention,
enabled,
)
@@ -344,7 +681,7 @@ impl NotificationSettings {
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
.is_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call)
.await?;
Ok(enabled)
}
@@ -353,7 +690,7 @@ impl NotificationSettings {
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
.set_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
.await?;
Ok(())
}
@@ -363,7 +700,7 @@ impl NotificationSettings {
let notification_settings = self.sdk_notification_settings.read().await;
let enabled = notification_settings
.is_push_rule_enabled(
RuleKind::Override,
SdkRuleKind::Override,
PredefinedOverrideRuleId::InviteForMe.as_str(),
)
.await?;
@@ -378,7 +715,7 @@ impl NotificationSettings {
let notification_settings = self.sdk_notification_settings.read().await;
notification_settings
.set_push_rule_enabled(
RuleKind::Override,
SdkRuleKind::Override,
PredefinedOverrideRuleId::InviteForMe.as_str(),
enabled,
)
@@ -386,6 +723,30 @@ impl NotificationSettings {
Ok(())
}
/// Sets a custom push rule with the given actions and conditions.
pub async fn set_custom_push_rule(
&self,
rule_id: String,
rule_kind: RuleKind,
actions: Vec<Action>,
conditions: Vec<PushCondition>,
) -> Result<(), NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let actions: Result<Vec<_>, _> =
actions.into_iter().map(|action| action.try_into()).collect();
let actions = actions.map_err(|e| NotificationSettingsError::Generic { msg: e })?;
notification_settings
.create_custom_conditional_push_rule(
rule_id,
rule_kind.into(),
actions,
conditions.into_iter().map(|condition| condition.into()).collect(),
)
.await?;
Ok(())
}
/// Unmute a room.
///
/// # Arguments
+200 -33
View File
@@ -233,20 +233,34 @@ pub struct TracingFileConfiguration {
#[derive(PartialEq, PartialOrd)]
enum LogTarget {
// External crates.
Hyper,
// FFI modules.
MatrixSdkFfi,
// SDK base modules.
MatrixSdkBaseEventCache,
MatrixSdkBaseSlidingSync,
MatrixSdkBaseStoreAmbiguityMap,
// SDK common modules.
MatrixSdkCommonStoreLocks,
// SDK modules.
MatrixSdk,
MatrixSdkClient,
MatrixSdkCrypto,
MatrixSdkCryptoAccount,
MatrixSdkOidc,
MatrixSdkHttpClient,
MatrixSdkSlidingSync,
MatrixSdkBaseSlidingSync,
MatrixSdkUiTimeline,
MatrixSdkEventCache,
MatrixSdkBaseEventCache,
MatrixSdkEventCacheStore,
MatrixSdkHttpClient,
MatrixSdkOidc,
MatrixSdkSendQueue,
MatrixSdkSlidingSync,
// SDK UI modules.
MatrixSdkUiTimeline,
}
impl LogTarget {
@@ -254,6 +268,10 @@ impl LogTarget {
match self {
LogTarget::Hyper => "hyper",
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
LogTarget::MatrixSdk => "matrix_sdk",
LogTarget::MatrixSdkClient => "matrix_sdk::client",
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
@@ -261,11 +279,10 @@ impl LogTarget {
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
}
}
}
@@ -282,25 +299,64 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
];
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
LogTarget::MatrixSdkCommonStoreLocks, // Too verbose
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
];
/// A log pack can be used to set the trace log level for a group of multiple
/// log targets at once, for debugging purposes.
#[derive(uniffi::Enum)]
pub enum TraceLogPacks {
/// Enables all the logs relevant to the event cache.
EventCache,
/// Enables all the logs relevant to the send queue.
SendQueue,
/// Enables all the logs relevant to the timeline.
Timeline,
}
impl TraceLogPacks {
// Note: all the log targets returned here must be part of
// `DEFAULT_TARGET_LOG_LEVELS`.
fn targets(&self) -> &[LogTarget] {
match self {
TraceLogPacks::EventCache => &[
LogTarget::MatrixSdkEventCache,
LogTarget::MatrixSdkBaseEventCache,
LogTarget::MatrixSdkEventCacheStore,
],
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
}
}
}
#[derive(uniffi::Record)]
pub struct TracingConfiguration {
/// The desired log level
/// The desired log level.
log_level: LogLevel,
/// Additional targets that the FFI client would like to use e.g.
/// the target names for created [`crate::tracing::Span`]
extra_targets: Option<Vec<String>>,
/// All the log packs, that will be set to `TRACE` when they're enabled.
trace_log_packs: Vec<TraceLogPacks>,
/// Additional targets that the FFI client would like to use.
///
/// This can include, for instance, the target names for created
/// [`crate::tracing::Span`]. These targets will use the global log level by
/// default.
extra_targets: Vec<String>,
/// Whether to log to stdout, or in the logcat on Android.
write_to_stdout_or_system: bool,
@@ -316,47 +372,111 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
let mut filters = vec!["panic=error".to_owned()];
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
// Use the default if the log level shouldn't be changed for this target or
// if it's already logging more than requested
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
level.as_str()
let global_level = config.log_level;
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, default_level)| {
let level = if IMMUTABLE_LOG_TARGETS.contains(target) {
// If the target is immutable, keep the log level.
*default_level
} else if config.trace_log_packs.iter().any(|pack| pack.targets().contains(target)) {
// If a log pack includes that target, set the associated log level to TRACE.
LogLevel::Trace
} else if *default_level > global_level {
// If the default level is more verbose than the global level, keep the default.
*default_level
} else {
config.log_level.as_str()
// Otherwise, use the global level.
global_level
};
filters.push(format!("{}={}", target.as_str(), level));
filters.push(format!("{}={}", target.as_str(), level.as_str()));
});
// Finally append the extra targets requested by the client
if let Some(extra_targets) = &config.extra_targets {
for target in extra_targets {
filters.push(format!("{}={}", target, config.log_level.as_str()));
}
// Finally append the extra targets requested by the client.
for target in &config.extra_targets {
filters.push(format!("{}={}", target, config.log_level.as_str()));
}
filters.join(",")
}
/// Sets up logs and the tokio runtime for the current application.
///
/// If `use_lightweight_tokio_runtime` is set to true, this will set up a
/// lightweight tokio runtime, for processes that have memory limitations (like
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
/// multithreaded tokio runtime will be set up.
#[matrix_sdk_ffi_macros::export]
pub fn setup_tracing(config: TracingConfiguration) {
pub fn init_platform(config: TracingConfiguration, use_lightweight_tokio_runtime: bool) {
log_panics();
let env_filter = build_tracing_filter(&config);
tracing_subscriber::registry()
.with(EnvFilter::new(build_tracing_filter(&config)))
.with(EnvFilter::new(&env_filter))
.with(text_layers(config))
.init();
// Log the log levels 🧠.
tracing::info!(env_filter, "Logging has been set up");
if use_lightweight_tokio_runtime {
setup_lightweight_tokio_runtime();
} else {
setup_multithreaded_tokio_runtime();
}
}
fn setup_multithreaded_tokio_runtime() {
async_compat::set_runtime_builder(Box::new(|| {
eprintln!("spawning a multithreaded tokio runtime");
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
builder
}));
}
fn setup_lightweight_tokio_runtime() {
async_compat::set_runtime_builder(Box::new(|| {
eprintln!("spawning a lightweight tokio runtime");
// Get the number of available cores through the system, if possible.
let num_available_cores =
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
// The number of worker threads will be either that or 4, whichever is smaller.
let num_worker_threads = num_available_cores.min(4);
// Chosen by a fair dice roll.
let num_blocking_threads = 2;
// 1 MiB of memory per worker thread. Should be enough for everyone™.
let max_memory_bytes = 1024 * 1024;
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder
.enable_all()
.worker_threads(num_worker_threads)
.thread_stack_size(max_memory_bytes)
.max_blocking_threads(num_blocking_threads);
builder
}));
}
#[cfg(test)]
mod tests {
use super::build_tracing_filter;
use crate::platform::TraceLogPacks;
#[test]
fn test_default_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Error,
extra_targets: Some(vec!["super_duper_app".to_owned()]),
trace_log_packs: Vec::new(),
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
};
@@ -377,9 +497,12 @@ mod tests {
matrix_sdk::sliding_sync=info,\
matrix_sdk_base::sliding_sync=info,\
matrix_sdk_ui::timeline=info,\
matrix_sdk::send_queue=info,\
matrix_sdk::event_cache=info,\
matrix_sdk_base::event_cache=info,\
matrix_sdk_sqlite::event_cache_store=info,\
matrix_sdk_common::store_locks=warn,\
matrix_sdk_base::store::ambiguity_map=warn,\
super_duper_app=error"
);
}
@@ -388,7 +511,8 @@ mod tests {
fn test_trace_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Trace,
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
trace_log_packs: Vec::new(),
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
};
@@ -409,11 +533,54 @@ mod tests {
matrix_sdk::sliding_sync=trace,\
matrix_sdk_base::sliding_sync=trace,\
matrix_sdk_ui::timeline=trace,\
matrix_sdk::send_queue=trace,\
matrix_sdk::event_cache=trace,\
matrix_sdk_base::event_cache=trace,\
matrix_sdk_sqlite::event_cache_store=trace,\
matrix_sdk_common::store_locks=warn,\
matrix_sdk_base::store::ambiguity_map=warn,\
super_duper_app=trace,\
some_other_span=trace"
);
}
#[test]
fn test_trace_log_packs() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Info,
trace_log_packs: vec![TraceLogPacks::EventCache, TraceLogPacks::SendQueue],
extra_targets: vec!["super_duper_app".to_owned()],
write_to_stdout_or_system: true,
write_to_files: None,
};
let filter = build_tracing_filter(&config);
assert_eq!(
filter,
r#"panic=error,
hyper=warn,
matrix_sdk_ffi=info,
matrix_sdk=info,
matrix_sdk::client=trace,
matrix_sdk_crypto=debug,
matrix_sdk_crypto::olm::account=trace,
matrix_sdk::oidc=trace,
matrix_sdk::http_client=debug,
matrix_sdk::sliding_sync=info,
matrix_sdk_base::sliding_sync=info,
matrix_sdk_ui::timeline=info,
matrix_sdk::send_queue=trace,
matrix_sdk::event_cache=trace,
matrix_sdk_base::event_cache=trace,
matrix_sdk_sqlite::event_cache_store=trace,
matrix_sdk_common::store_locks=warn,
matrix_sdk_base::store::ambiguity_map=warn,
super_duper_app=info"#
.split('\n')
.map(|s| s.trim())
.collect::<Vec<_>>()
.join("")
);
}
}
+114 -73
View File
@@ -1,19 +1,20 @@
use std::{collections::HashMap, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use async_compat::get_runtime_handle;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
TryFromReportedContentScoreError,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
use mime::Mime;
use ruma::{
api::client::room::report_content,
assign,
events::{
call::notify,
@@ -30,7 +31,6 @@ use ruma::{
use tokio::sync::RwLock;
use tracing::{error, warn};
use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
@@ -39,10 +39,10 @@ use crate::{
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
room_info::RoomInfo,
room_member::RoomMember,
room_member::{RoomMember, RoomMemberWithSenderInfo},
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
timeline::{
configuration::{AllowedMessageTypes, TimelineConfiguration},
configuration::{TimelineConfiguration, TimelineFilter},
ReceiptType, SendHandle, Timeline,
},
utils::u64_to_uint,
@@ -110,8 +110,8 @@ impl Room {
self.inner.avatar_url().map(|m| m.to_string())
}
pub fn is_direct(&self) -> bool {
RUNTIME.block_on(self.inner.is_direct()).unwrap_or(false)
pub async fn is_direct(&self) -> bool {
self.inner.is_direct().await.unwrap_or(false)
}
pub fn is_public(&self) -> bool {
@@ -161,21 +161,6 @@ impl Room {
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
}
/// For rooms one is invited to, retrieves the room member information for
/// the user who invited the logged-in user to a room.
pub async fn inviter(&self) -> Option<RoomMember> {
if self.inner.state() == RoomState::Invited {
self.inner
.invite_details()
.await
.ok()
.and_then(|a| a.inviter)
.and_then(|m| m.try_into().ok())
} else {
None
}
}
/// Forces the currently active room key, which is used to encrypt messages,
/// to be rotated.
///
@@ -206,30 +191,50 @@ impl Room {
) -> Result<Arc<Timeline>, ClientError> {
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
builder = builder.with_focus(configuration.focus.try_into()?);
builder = builder
.with_focus(configuration.focus.try_into()?)
.with_date_divider_mode(configuration.date_divider_mode.into());
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
builder = builder.event_filter(move |event, room_version_id| {
default_event_filter(event, room_version_id)
&& match event {
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
types.contains(&content.msgtype.into())
if configuration.track_read_receipts {
builder = builder.track_read_marker_and_receipts();
}
match configuration.filter {
TimelineFilter::All => {
// #nofilter.
}
TimelineFilter::OnlyMessage { types } => {
builder = builder.event_filter(move |event, room_version_id| {
default_event_filter(event, room_version_id)
&& match event {
AnySyncTimelineEvent::MessageLike(msg) => {
match msg.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
types.contains(&content.msgtype.into())
}
_ => false,
}
}
_ => false,
},
_ => false,
}
});
}
});
}
TimelineFilter::EventTypeFilter { filter: event_type_filter } => {
builder = builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
});
}
}
if let Some(internal_id_prefix) = configuration.internal_id_prefix {
builder = builder.with_internal_id_prefix(internal_id_prefix);
}
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
let timeline = builder.build().await?;
Ok(Timeline::new(timeline))
}
@@ -237,8 +242,12 @@ impl Room {
self.inner.room_id().to_string()
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
pub fn encryption_state(&self) -> EncryptionState {
self.inner.encryption_state()
}
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
Ok(self.inner.latest_encryption_state().await?)
}
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
@@ -252,13 +261,13 @@ impl Room {
}
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
Ok(member.try_into().context("Unknown state membership")?)
}
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
Ok(avatar_url_string)
@@ -268,12 +277,28 @@ impl Room {
&self,
user_id: String,
) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let user_id = UserId::parse(&*user_id)?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.display_name().map(|m| m.to_owned());
Ok(avatar_url_string)
}
/// Get the membership details for the current user.
///
/// Returns:
/// - If the user was present in the room, a
/// [`matrix_sdk::room::RoomMemberWithSenderInfo`] containing both the
/// user info and the member info of the sender of the `m.room.member`
/// event.
/// - If the current user is not present, an error.
pub async fn member_with_sender_info(
&self,
user_id: String,
) -> Result<RoomMemberWithSenderInfo, ClientError> {
let user_id = UserId::parse(&*user_id)?;
self.inner.member_with_sender_info(&user_id).await?.try_into()
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
RoomInfo::new(&self.inner).await
}
@@ -283,7 +308,7 @@ impl Room {
listener: Box<dyn RoomInfoListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.subscribe_info();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while subscriber.next().await.is_some() {
match self.room_info().await {
Ok(room_info) => listener.call(room_info),
@@ -375,17 +400,34 @@ impl Room {
score: Option<i32>,
reason: Option<String>,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
let int_score = score.map(|value| value.into());
self.inner
.client()
.send(report_content::v3::Request::new(
self.inner.room_id().into(),
event_id,
int_score,
.report_content(
EventId::parse(event_id)?,
score.map(TryFrom::try_from).transpose().map_err(
|error: TryFromReportedContentScoreError| ClientError::Generic {
msg: error.to_string(),
},
)?,
reason,
))
)
.await?;
Ok(())
}
/// Reports a room as inappropriate to the server.
/// The caller is not required to be joined to the room to report it.
///
/// # Arguments
///
/// * `reason` - The reason the room is being reported.
///
/// # Errors
///
/// Returns an error if the room is not found or on rate limit
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.report_room(reason).await?;
Ok(())
}
@@ -569,7 +611,7 @@ impl Room {
self: Arc<Self>,
listener: Box<dyn TypingNotificationsListener>,
) -> Arc<TaskHandle> {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let (_event_handler_drop_guard, mut subscriber) =
self.inner.subscribe_to_typing_notifications();
while let Ok(typing_user_ids) = subscriber.recv().await {
@@ -580,29 +622,28 @@ impl Room {
})))
}
pub fn subscribe_to_identity_status_changes(
pub async fn subscribe_to_identity_status_changes(
&self,
listener: Box<dyn IdentityStatusChangeListener>,
) -> Arc<TaskHandle> {
) -> Result<Arc<TaskHandle>, ClientError> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let status_changes = room.subscribe_to_identity_status_changes().await;
if let Ok(status_changes) = status_changes {
// TODO: what to do with failures?
let mut status_changes = pin!(status_changes);
while let Some(identity_status_changes) = status_changes.next().await {
listener.call(
identity_status_changes
.into_iter()
.map(|change| {
let user_id = change.user_id.to_string();
IdentityStatusChange { user_id, changed_to: change.changed_to }
})
.collect(),
);
}
let status_changes = room.subscribe_to_identity_status_changes().await?;
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let mut status_changes = pin!(status_changes);
while let Some(identity_status_changes) = status_changes.next().await {
listener.call(
identity_status_changes
.into_iter()
.map(|change| {
let user_id = change.user_id.to_string();
IdentityStatusChange { user_id, changed_to: change.changed_to }
})
.collect(),
);
}
})))
}))))
}
/// Set (or unset) a flag on the room to indicate that the user has
@@ -853,7 +894,7 @@ impl Room {
) -> Result<Arc<TaskHandle>, ClientError> {
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let handle = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(stream);
while let Some(requests) = stream.next().await {
listener.call(requests.into_iter().map(Into::into).collect());
@@ -1003,9 +1044,9 @@ impl Room {
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let subscription = room.observe_live_location_shares();
let mut stream = subscription.subscribe();
let stream = subscription.subscribe();
let mut pinned_stream = pin!(stream);
while let Some(event) = pinned_stream.next().await {
@@ -15,13 +15,13 @@
use std::{fmt::Debug, sync::Arc};
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use ruma::ServerName;
use tokio::sync::RwLock;
use super::RUNTIME;
use crate::{error::ClientError, task_handle::TaskHandle};
#[derive(uniffi::Enum)]
@@ -137,11 +137,11 @@ impl RoomDirectorySearch {
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.read().await.results();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
}
+3 -1
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use matrix_sdk::RoomState;
use matrix_sdk::{EncryptionState, RoomState};
use tracing::warn;
use crate::{
@@ -14,6 +14,7 @@ use crate::{
#[derive(uniffi::Record)]
pub struct RoomInfo {
id: String,
encryption_state: EncryptionState,
creator: Option<String>,
/// The room's name from the room state event if received from sync, or one
/// that's been computed otherwise.
@@ -84,6 +85,7 @@ impl RoomInfo {
Ok(Self {
id: room.room_id().to_string(),
encryption_state: room.encryption_state(),
creator: room.creator().as_ref().map(ToString::to_string),
display_name: room.cached_display_name().map(|name| name.to_string()),
raw_name: room.name(),
+15 -42
View File
@@ -2,6 +2,7 @@
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt, TryFutureExt};
use matrix_sdk::ruma::{
@@ -26,10 +27,9 @@ use crate::{
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
utils::AsyncRuntimeDropped,
TaskHandle, RUNTIME,
TaskHandle,
};
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -92,7 +92,7 @@ impl RoomListService {
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
let state_stream = self.inner.state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(state_stream);
while let Some(state) = state_stream.next().await {
@@ -128,7 +128,7 @@ impl RoomListService {
Duration::from_millis(delay_before_hiding_in_ms.into()),
);
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(sync_indicator_stream);
while let Some(sync_indicator) = sync_indicator_stream.next().await {
@@ -167,7 +167,7 @@ impl RoomList {
Ok(RoomListLoadingStateResult {
state: loading_state.get().into(),
state_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(loading_state);
while let Some(loading_state) = loading_state.next().await {
@@ -237,7 +237,7 @@ impl RoomList {
let dynamic_entries_controller =
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(entries_stream);
while let Some(diffs) = entries_stream.next().await {
@@ -557,8 +557,8 @@ impl RoomListItem {
self.inner.avatar_url().map(|uri| uri.to_string())
}
fn is_direct(&self) -> bool {
RUNTIME.block_on(self.inner.inner_room().is_direct()).unwrap_or(false)
async fn is_direct(&self) -> bool {
self.inner.inner_room().is_direct().await.unwrap_or(false)
}
fn canonical_alias(&self) -> Option<String> {
@@ -574,29 +574,8 @@ impl RoomListItem {
self.inner.inner_room().state().into()
}
/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited.
///
/// ⚠️ Holding on to this room instance after it has been joined is not
/// safe. Use `full_room` instead.
#[deprecated(note = "Please use `preview_room` instead.")]
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Invited) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited],
actual: self.membership(),
});
}
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited or knocked rooms.
///
/// An error will be returned if the room is in a state other than invited
/// or knocked.
/// invited, knocked or banned rooms.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
@@ -604,16 +583,6 @@ impl RoomListItem {
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
// Validate internal room state.
let membership = self.membership();
if !matches!(membership, Membership::Invited | Membership::Knocked) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited, Membership::Knocked],
actual: membership,
}
.into());
}
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
@@ -711,7 +680,11 @@ impl RoomListItem {
/// **Note**: this info may not be reliable if you don't set up
/// `m.room.encryption` as required state.
async fn is_encrypted(&self) -> bool {
self.inner.is_encrypted().await.unwrap_or(false)
self.inner
.latest_encryption_state()
.await
.map(|state| state.is_encrypted())
.unwrap_or(false)
}
async fn latest_event(&self) -> Option<EventTimelineItem> {
@@ -108,3 +108,25 @@ impl TryFrom<SdkRoomMember> for RoomMember {
})
}
}
/// Contains the current user's room member info and the optional room member
/// info of the sender of the `m.room.member` event that this info represents.
#[derive(Clone, uniffi::Record)]
pub struct RoomMemberWithSenderInfo {
/// The room member.
room_member: RoomMember,
/// The info of the sender of the event `room_member` is based on, if
/// available.
sender_info: Option<RoomMember>,
}
impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSenderInfo {
type Error = ClientError;
fn try_from(value: matrix_sdk::room::RoomMemberWithSenderInfo) -> Result<Self, Self::Error> {
Ok(Self {
room_member: value.room_member.try_into()?,
sender_info: value.sender_info.map(|member| member.try_into()).transpose()?,
})
}
}
+16 -24
View File
@@ -7,7 +7,7 @@ use crate::{
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_member::RoomMember,
room_member::{RoomMember, RoomMemberWithSenderInfo},
utils::AsyncRuntimeDropped,
};
@@ -51,11 +51,23 @@ impl RoomPreview {
/// Leave the room if the room preview state is either joined, invited or
/// knocked.
///
/// If rejecting an invite then also forget it as an extra layer of
/// protection against spam attacks.
///
/// Will return an error otherwise.
pub async fn leave(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.leave().await.map_err(Into::into)
let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
room.leave().await.map_err(ClientError::from)?;
if should_forget {
_ = self.forget().await;
}
Ok(())
}
/// Get the user who created the invite, if any.
@@ -74,32 +86,12 @@ impl RoomPreview {
}
/// Get the membership details for the current user.
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
pub async fn own_membership_details(&self) -> Option<RoomMemberWithSenderInfo> {
let room = self.client.get_room(&self.inner.room_id)?;
let (own_member, sender_member) = match room.own_membership_details().await {
Ok(memberships) => memberships,
Err(error) => {
warn!("Couldn't get membership info: {error}");
return None;
}
};
Some(RoomMembershipDetails {
own_room_member: own_member.try_into().ok()?,
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
})
room.member_with_sender_info(self.client.user_id()?).await.ok()?.try_into().ok()
}
}
/// Contains the current user's room member info and the optional room member
/// info of the sender of the `m.room.member` event that this info represents.
#[derive(uniffi::Record)]
pub struct RoomMembershipDetails {
pub own_room_member: RoomMember,
pub sender_room_member: Option<RoomMember>,
}
impl RoomPreview {
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
Self { client, inner }
+18 -1
View File
@@ -34,7 +34,7 @@ use ruma::{
MessageType as RumaMessageType,
NoticeMessageEventContent as RumaNoticeMessageEventContent,
RoomMessageEventContentWithoutRelation,
TextMessageEventContent as RumaTextMessageEventContent,
TextMessageEventContent as RumaTextMessageEventContent, UnstableAmplitude,
UnstableAudioDetailsContentBlock as RumaUnstableAudioDetailsContentBlock,
UnstableVoiceContentBlock as RumaUnstableVoiceContentBlock,
VideoInfo as RumaVideoInfo,
@@ -362,6 +362,8 @@ impl TryFrom<MessageType> for RumaMessageType {
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
event_content.audio = content.audio.map(Into::into);
event_content.voice = content.voice.map(Into::into);
Self::Audio(event_content)
}
MessageType::Video { content } => {
@@ -658,6 +660,15 @@ impl From<RumaUnstableAudioDetailsContentBlock> for UnstableAudioDetailsContent
}
}
impl From<UnstableAudioDetailsContent> for RumaUnstableAudioDetailsContentBlock {
fn from(details: UnstableAudioDetailsContent) -> Self {
Self::new(
details.duration,
details.waveform.iter().map(|x| UnstableAmplitude::new(x.to_owned())).collect(),
)
}
}
#[derive(Clone, uniffi::Record)]
pub struct UnstableVoiceContent {}
@@ -667,6 +678,12 @@ impl From<RumaUnstableVoiceContentBlock> for UnstableVoiceContent {
}
}
impl From<UnstableVoiceContent> for RumaUnstableVoiceContentBlock {
fn from(_details: UnstableVoiceContent) -> Self {
Self::new()
}
}
#[derive(Clone, uniffi::Record)]
pub struct VideoInfo {
pub duration: Option<Duration>,
@@ -1,5 +1,6 @@
use std::sync::{Arc, RwLock};
use async_compat::get_runtime_handle;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
@@ -7,13 +8,13 @@ use matrix_sdk::{
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
ruma::events::key::verification::VerificationMethod,
Account,
};
use ruma::UserId;
use tracing::{error, info};
use tracing::{error, warn};
use super::RUNTIME;
use crate::{error::ClientError, utils::Timestamp};
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
#[derive(uniffi::Object)]
pub struct SessionVerificationEmoji {
@@ -39,12 +40,12 @@ pub enum SessionVerificationData {
}
/// Details about the incoming verification request
#[derive(Debug, uniffi::Record)]
#[derive(uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
sender_profile: UserProfile,
flow_id: String,
device_id: String,
display_name: Option<String>,
device_display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: Timestamp,
}
@@ -66,6 +67,7 @@ pub type Delegate = Arc<RwLock<Option<Box<dyn SessionVerificationControllerDeleg
pub struct SessionVerificationController {
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
delegate: Delegate,
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
@@ -94,15 +96,7 @@ impl SessionVerificationController {
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
self.set_ongoing_verification_request(verification_request)
}
/// Accept the previously acknowledged verification request
@@ -118,7 +112,7 @@ impl SessionVerificationController {
}
/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
.user_identity
@@ -126,15 +120,31 @@ impl SessionVerificationController {
.await
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
self.set_ongoing_verification_request(verification_request)
}
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
/// Request verification for the given user
pub async fn request_user_verification(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;
Ok(())
let user_identity = self
.encryption
.get_user_identity(&user_id)
.await?
.ok_or(ClientError::new("Unknown user identity"))?;
if user_identity.is_verified() {
return Err(ClientError::new("User is already verified"));
}
let methods = vec![VerificationMethod::SasV1];
let verification_request = user_identity
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
self.set_ongoing_verification_request(verification_request)
}
/// Transition the current verification request into a SAS verification
@@ -155,7 +165,8 @@ impl SessionVerificationController {
}
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
get_runtime_handle()
.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
@@ -202,50 +213,91 @@ impl SessionVerificationController {
}
impl SessionVerificationController {
pub(crate) fn new(encryption: Encryption, user_identity: UserIdentity) -> Self {
pub(crate) fn new(
encryption: Encryption,
user_identity: UserIdentity,
account: Account,
) -> Self {
SessionVerificationController {
encryption,
user_identity,
account,
delegate: Arc::new(RwLock::new(None)),
verification_request: Arc::new(RwLock::new(None)),
sas_verification: Arc::new(RwLock::new(None)),
}
}
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);
let Some(request) = self
.encryption
.get_verification_request(&event.sender, &event.content.transaction_id)
.await
else {
error!("Failed retrieving verification request");
return;
};
if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
/// Ask the controller to process an incoming request based on the sender
/// and flow identifier. It will fetch the request, verify that it's in the
/// correct state and then and notify the delegate.
pub(crate) async fn process_incoming_verification_request(
&self,
sender: &UserId,
flow_id: impl AsRef<str>,
) {
if sender != self.user_identity.user_id() {
if let Some(status) = self.encryption.cross_signing_status().await {
if !status.is_complete() {
warn!("Cannot verify other users until our own device's cross-signing status is complete: {:?}", status);
return;
}
}
}
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
error!("Failed retrieving verification request");
return;
};
let VerificationRequestState::Requested { other_device_data, .. } = request.state() else {
error!("Received verification request event but the request is in the wrong state.");
return;
};
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
error!("Failed fetching user profile for verification request");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_profile: UserProfile {
user_id: request.other_user_id().to_string(),
display_name: sender_profile.displayname,
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
},
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
device_display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
}
fn set_ongoing_verification_request(
&self,
verification_request: VerificationRequest,
) -> Result<(), ClientError> {
if let Some(ongoing_verification_request) =
self.verification_request.read().unwrap().clone()
{
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::new("There is another verification flow ongoing."));
}
}
*self.verification_request.write().unwrap() = Some(verification_request.clone());
get_runtime_handle().spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
async fn listen_to_verification_request_changes(
@@ -271,7 +323,7 @@ impl SessionVerificationController {
}
let delegate = delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
get_runtime_handle().spawn(Self::listen_to_sas_verification_changes(
verification,
delegate,
));
+2 -2
View File
@@ -14,6 +14,7 @@
use std::{fmt::Debug, sync::Arc, time::Duration};
use async_compat::get_runtime_handle;
use futures_util::pin_mut;
use matrix_sdk::{crypto::types::events::UtdCause, Client};
use matrix_sdk_ui::{
@@ -29,7 +30,6 @@ use tracing::error;
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
RUNTIME,
};
#[derive(uniffi::Enum)]
@@ -84,7 +84,7 @@ impl SyncService {
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
let state_stream = self.inner.state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(state_stream);
while let Some(state) = state_stream.next().await {
@@ -1,7 +1,65 @@
use ruma::EventId;
use std::sync::Arc;
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
EventId,
};
use super::FocusEventError;
use crate::{error::ClientError, event::RoomMessageEventMessageType};
use crate::{
error::ClientError,
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
};
#[derive(uniffi::Object)]
pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
}
#[uniffi::constructor]
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
}
}
impl TimelineEventTypeFilter {
/// Filters an [`event`] to decide whether it should be part of the timeline
/// based on [`AnySyncTimelineEvent::event_type()`].
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
self.inner.filter(event)
}
}
#[derive(uniffi::Enum, Clone)]
pub enum FilterTimelineEventType {
MessageLike { event_type: MessageLikeEventType },
State { event_type: StateEventType },
}
impl From<FilterTimelineEventType> for TimelineEventType {
fn from(value: FilterTimelineEventType) -> TimelineEventType {
match value {
FilterTimelineEventType::MessageLike { event_type } => {
ruma::events::MessageLikeEventType::from(event_type).into()
}
FilterTimelineEventType::State { event_type } => {
ruma::events::StateEventType::from(event_type).into()
}
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
@@ -52,28 +110,27 @@ impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
}
#[derive(uniffi::Enum)]
pub enum AllowedMessageTypes {
pub enum TimelineFilter {
/// Show all the events in the timeline, independent of their type.
All,
Only { types: Vec<RoomMessageEventMessageType> },
/// Show only `m.room.messages` of the given room message types.
OnlyMessage {
/// A list of [`RoomMessageEventMessageType`] that will be allowed to
/// appear in the timeline.
types: Vec<RoomMessageEventMessageType>,
},
/// Show only events which match this filter.
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
}
/// Various options used to configure the timeline's behavior.
///
/// # Arguments
///
/// * `internal_id_prefix` -
///
/// * `allowed_message_types` -
///
/// * `date_divider_mode` -
#[derive(uniffi::Record)]
pub struct TimelineConfiguration {
/// What should the timeline focus on?
pub focus: TimelineFocus,
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
/// in the timeline
pub allowed_message_types: AllowedMessageTypes,
/// How should we filter out events from the timeline?
pub filter: TimelineFilter,
/// An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
@@ -82,4 +139,11 @@ pub struct TimelineConfiguration {
/// How often to insert date dividers
pub date_divider_mode: DateDividerMode,
/// Should the read receipts and read markers be tracked for the timeline
/// items in this instance?
///
/// As this has a non negligible performance impact, make sure to enable it
/// only when you need it.
pub track_read_receipts: bool,
}
+14 -220
View File
@@ -12,73 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
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 as RumaMediaSource, EventContent, FullStateEventContent};
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
use ruma::events::FullStateEventContent;
use super::ProfileDetails;
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
utils::Timestamp,
};
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match value {
Content::Message(message) => {
let msgtype = message.msgtype().msgtype().to_owned();
match TryInto::<MessageContent>::try_into(message) {
Ok(message) => TimelineItemContent::Message { content: message },
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: msgtype,
error: error.to_string(),
},
}
}
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
let media_source = RumaMediaSource::from(content.source.clone());
if let Err(error) = media_source.verify() {
return TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
};
}
match TryInto::<ImageInfo>::try_into(&content.info) {
Ok(info) => TimelineItemContent::Sticker {
body: content.body.clone(),
info,
source: Arc::new(MediaSource { media_source }),
},
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
},
}
}
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
Content::MsgLike(msg_like) => match msg_like.try_into() {
Ok(content) => TimelineItemContent::MsgLike { content },
Err((error, event_type)) => TimelineItemContent::FailedToParseMessageLike {
event_type,
error: error.to_string(),
},
},
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::UnableToDecrypt(msg) => {
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
}
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
@@ -137,65 +95,13 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
}
}
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub in_reply_to: Option<Arc<InReplyToDetails>>,
pub thread_root: Option<String>,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
type Error = ClientError;
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
Ok(Self {
msg_type: value.msgtype().clone().try_into()?,
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
})
}
}
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum TimelineItemContent {
Message {
content: MessageContent,
},
RedactedMessage,
Sticker {
body: String,
info: ImageInfo,
source: Arc<MediaSource>,
},
Poll {
question: String,
kind: PollKind,
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<Timestamp>,
has_been_edited: bool,
MsgLike {
content: MsgLikeContent,
},
CallInvite,
CallNotify,
UnableToDecrypt {
msg: EncryptedMessage,
},
RoomMembership {
user_id: String,
user_display_name: Option<String>,
@@ -223,94 +129,6 @@ pub enum TimelineItemContent {
},
}
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
}
impl InReplyToDetails {
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
Self { event_id, event }
}
}
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: event.content().clone().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
TimelineDetails::Error(err) => {
RepliedToEventDetails::Error { message: err.to_string() }
}
};
Self { event_id, event }
}
}
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
/// The Curve25519 key of the sender.
sender_key: String,
},
// Other fields not included because UniFFI doesn't have the concept of
// deprecated fields right now.
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
/// What we know about what caused this UTD. E.g. was this event sent
/// when we were not a member of this room?
cause: UtdCause,
},
Unknown,
}
impl EncryptedMessage {
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id, cause: *cause }
}
Message::Unknown => Self::Unknown,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct Reaction {
pub key: String,
@@ -463,27 +281,3 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContent {
fn from(value: PollResult) -> Self {
TimelineItemContent::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
answers: value
.answers
.into_iter()
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: value.votes,
end_time: value.end_time.map(|t| t.into()),
has_been_edited: value.has_been_edited,
}
}
}
+92 -72
View File
@@ -16,7 +16,7 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use content::{InReplyToDetails, RepliedToEventDetails};
use async_compat::get_runtime_handle;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt as _};
use matrix_sdk::{
@@ -25,14 +25,18 @@ use matrix_sdk::{
BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
room::edit::EditedContent as SdkEditedContent,
Error,
event_cache::RoomPaginationStatus,
room::{
edit::EditedContent as SdkEditedContent,
reply::{EnforceThread, Reply},
},
};
use matrix_sdk_ui::timeline::{
self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
use reply::{InReplyToDetails, RepliedToEventDetails};
use ruma::{
events::{
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
@@ -46,7 +50,7 @@ use ruma::{
},
receipt::ReceiptThread,
room::message::{
ForwardThread, LocationMessageEventContent, MessageType,
LocationMessageEventContent, MessageType, ReplyWithinThread,
RoomMessageEventContentWithoutRelation,
},
AnyMessageLikeEventContent,
@@ -60,7 +64,8 @@ use tokio::{
use tracing::{error, warn};
use uuid::Uuid;
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
use self::content::TimelineItemContent;
pub use self::msg_like::MessageContent;
use crate::{
client::ProgressWatcher,
error::{ClientError, RoomError},
@@ -72,13 +77,13 @@ use crate::{
},
task_handle::TaskHandle,
utils::Timestamp,
RUNTIME,
};
pub mod configuration;
mod content;
mod msg_like;
mod reply;
pub use content::MessageContent;
use matrix_sdk::utils::formatted_body_from;
use crate::error::QueueWedgeError;
@@ -121,9 +126,10 @@ impl Timeline {
.info(attachment_info)
.caption(params.caption)
.formatted_caption(formatted_caption)
.mentions(params.mentions.map(Into::into));
.mentions(params.mentions.map(Into::into))
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
let handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
let mut request =
self.inner.send_attachment(params.filename, mime_type, attachment_config);
@@ -133,7 +139,7 @@ impl Timeline {
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
get_runtime_handle().spawn(async move {
while let Some(progress) = subscriber.next().await {
progress_watcher.transmission_progress(progress.into());
}
@@ -201,32 +207,65 @@ pub struct UploadParameters {
caption: Option<String>,
/// Optional HTML-formatted caption, for clients that support it.
formatted_caption: Option<FormattedBody>,
// Optional intentional mentions to be sent with the media.
/// Optional intentional mentions to be sent with the media.
mentions: Option<Mentions>,
/// Optional parameters for sending the media as (threaded) reply.
reply_params: Option<ReplyParameters>,
/// Should the media be sent with the send queue, or synchronously?
///
/// Watching progress only works with the synchronous method, at the moment.
use_send_queue: bool,
}
#[derive(uniffi::Record)]
pub struct ReplyParameters {
/// The ID of the event to reply to.
event_id: String,
/// Whether to enforce a thread relation.
enforce_thread: bool,
/// If enforcing a threaded relation, whether the message is a reply on a
/// thread.
reply_within_thread: bool,
}
impl TryInto<Reply> for ReplyParameters {
type Error = RoomError;
fn try_into(self) -> Result<Reply, Self::Error> {
let event_id =
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
let enforce_thread = if self.enforce_thread {
EnforceThread::Threaded(if self.reply_within_thread {
ReplyWithinThread::Yes
} else {
ReplyWithinThread::No
})
} else {
EnforceThread::MaybeThreaded
};
Ok(Reply { event_id, enforce_thread })
}
}
#[matrix_sdk_ffi_macros::export]
impl Timeline {
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
let (timeline_items, timeline_stream) = self.inner.subscribe().await;
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// It's important that the initial items are passed *before* we forward the
// stream updates, with a guaranteed ordering. Otherwise, it could
// be that the listener be called before the initial items have been
// handled by the caller. See #3535 for details.
// First, pass all the items as a reset update.
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
values: timeline_items,
}))]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(timeline_stream);
// It's important that the initial items are passed *before* we forward the
// stream updates, with a guaranteed ordering. Otherwise, it could
// be that the listener be called before the initial items have been
// handled by the caller. See #3535 for details.
// First, pass all the items as a reset update.
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
values: timeline_items,
}))]);
// Then forward new items.
while let Some(diffs) = timeline_stream.next().await {
listener
@@ -236,7 +275,7 @@ impl Timeline {
}
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
RUNTIME.spawn(async move {
get_runtime_handle().spawn(async move {
self.inner.retry_decryption(&session_ids).await;
});
}
@@ -255,10 +294,14 @@ impl Timeline {
.await
.context("can't subscribe to the back-pagination status on a focused timeline")?;
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Send the current state even if it hasn't changed right away.
listener.on_update(initial);
// Send the current state even if it hasn't changed right away.
//
// Note: don't do it in the spawned function, so that the caller is immediately
// aware of the current state, and this doesn't depend on the async runtime
// having an available worker
listener.on_update(initial);
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(status) = subscriber.next().await {
listener.on_update(status);
}
@@ -441,7 +484,7 @@ impl Timeline {
Ok(())
}
pub fn end_poll(
pub async fn end_poll(
self: Arc<Self>,
poll_start_event_id: String,
text: String,
@@ -451,29 +494,25 @@ impl Timeline {
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
RUNTIME.spawn(async move {
if let Err(err) = self.inner.send(event_content).await {
error!("unable to end poll: {err}");
}
});
if let Err(err) = self.inner.send(event_content).await {
error!("unable to end poll: {err}");
}
Ok(())
}
/// Send a reply.
///
/// If the replied to event has a thread relation, it is forwarded on the
/// reply so that clients that support threads can render the reply
/// inside the thread.
pub async fn send_reply(
&self,
msg: Arc<RoomMessageEventContentWithoutRelation>,
event_id: String,
reply_params: ReplyParameters,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
let replied_to_info = self
.inner
.replied_to_info_from_event_id(&event_id)
.await
.map_err(|err| anyhow::anyhow!(err))?;
self.inner
.send_reply((*msg).clone(), replied_to_info, ForwardThread::Yes)
.send_reply((*msg).clone(), reply_params.try_into()?)
.await
.map_err(|err| anyhow::anyhow!(err))?;
Ok(())
@@ -621,19 +660,12 @@ impl Timeline {
) -> Result<Arc<InReplyToDetails>, ClientError> {
let event_id = EventId::parse(&event_id_str)?;
let replied_to: Result<RepliedToEvent, Error> =
if let Some(event) = self.inner.item_by_event_id(&event_id).await {
Ok(RepliedToEvent::from_timeline_item(&event))
} else {
match self.inner.room().event(&event_id, None).await {
Ok(timeline_event) => Ok(RepliedToEvent::try_from_timeline_event_for_room(
timeline_event,
self.inner.room(),
)
.await?),
Err(e) => Err(e),
}
};
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
Ok(event) => RepliedToEvent::try_from_timeline_event_for_room(event, self.inner.room())
.await
.map_err(ClientError::from),
Err(e) => Err(ClientError::from(e)),
};
match replied_to {
Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new(
@@ -757,7 +789,7 @@ pub trait TimelineListener: Sync + Send {
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait PaginationStatusListener: Sync + Send {
fn on_update(&self, status: LiveBackPaginationStatus);
fn on_update(&self, status: RoomPaginationStatus);
}
#[derive(Clone, uniffi::Object)]
@@ -934,6 +966,7 @@ impl TimelineItem {
match self.0.as_virtual()? {
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
}
}
@@ -1028,7 +1061,6 @@ pub struct EventTimelineItem {
is_editable: bool,
content: TimelineItemContent,
timestamp: Timestamp,
reactions: Vec<Reaction>,
local_send_state: Option<EventSendState>,
local_created_at: Option<u64>,
read_receipts: HashMap<String, Receipt>,
@@ -1039,20 +1071,6 @@ pub struct EventTimelineItem {
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
let reactions = item
.reactions()
.iter()
.map(|(k, v)| Reaction {
key: k.to_owned(),
senders: v
.into_iter()
.map(|(sender_id, info)| ReactionSenderData {
sender_id: sender_id.to_string(),
timestamp: info.timestamp.into(),
})
.collect(),
})
.collect();
let item = Arc::new(item);
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
let read_receipts =
@@ -1066,7 +1084,6 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
is_editable: item.is_editable(),
content: item.content().clone().into(),
timestamp: item.timestamp().into(),
reactions,
local_send_state: item.send_state().map(|s| s.into()),
local_created_at: item.local_created_at().map(|t| t.0.into()),
read_receipts,
@@ -1213,6 +1230,9 @@ pub enum VirtualTimelineItem {
/// The user's own read marker.
ReadMarker,
/// The timeline start, that is, the *oldest* event in time for that room.
TimelineStart,
}
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
@@ -0,0 +1,224 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
use super::{content::Reaction, reply::InReplyToDetails};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
timeline::content::ReactionSenderData,
utils::Timestamp,
};
#[derive(Clone, uniffi::Enum)]
pub enum MsgLikeKind {
/// An `m.room.message` event or extensible event, including edits.
Message { content: MessageContent },
/// An `m.sticker` event.
Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
/// An `m.poll.start` event.
Poll {
question: String,
kind: PollKind,
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<Timestamp>,
has_been_edited: bool,
},
/// A redacted message.
Redacted,
/// An `m.room.encrypted` event that could not be decrypted.
UnableToDecrypt { msg: EncryptedMessage },
}
/// A special kind of [`super::TimelineItemContent`] that groups together
/// different room message types with their respective reactions and thread
/// information.
#[derive(Clone, uniffi::Record)]
pub struct MsgLikeContent {
pub kind: MsgLikeKind,
pub reactions: Vec<Reaction>,
/// Event ID of the thread root, if this is a threaded message.
pub thread_root: Option<String>,
/// The event this message is replying to, if any.
pub in_reply_to: Option<Arc<InReplyToDetails>>,
}
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
type Error = (ClientError, String);
fn try_from(value: matrix_sdk_ui::timeline::MsgLikeContent) -> Result<Self, Self::Error> {
use matrix_sdk_ui::timeline::MsgLikeKind as Kind;
let reactions = value
.reactions
.iter()
.map(|(k, v)| Reaction {
key: k.to_owned(),
senders: v
.into_iter()
.map(|(sender_id, info)| ReactionSenderData {
sender_id: sender_id.to_string(),
timestamp: info.timestamp.into(),
})
.collect(),
})
.collect();
let in_reply_to = value.in_reply_to.map(|r| Arc::new(r.into()));
let thread_root = value.thread_root.map(|id| id.to_string());
Ok(match value.kind {
Kind::Message(message) => {
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
.map_err(|e| (e, message.msgtype().msgtype().to_owned()))?;
Self {
kind: MsgLikeKind::Message {
content: MessageContent {
msg_type,
body: message.body().to_owned(),
is_edited: message.is_edited(),
mentions: message.mentions().cloned().map(|m| m.into()),
},
},
reactions,
in_reply_to,
thread_root,
}
}
Kind::Sticker(sticker) => {
let content = sticker.content();
let media_source = RumaMediaSource::from(content.source.clone());
media_source
.verify()
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
let image_info = TryInto::<ImageInfo>::try_into(&content.info)
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
Self {
kind: MsgLikeKind::Sticker {
body: content.body.clone(),
info: image_info,
source: Arc::new(MediaSource { media_source }),
},
reactions,
in_reply_to,
thread_root,
}
}
Kind::Poll(poll_state) => {
let results = poll_state.results();
Self {
kind: MsgLikeKind::Poll {
question: results.question,
kind: PollKind::from(results.kind),
max_selections: results.max_selections,
answers: results
.answers
.into_iter()
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: results.votes,
end_time: results.end_time.map(|t| t.into()),
has_been_edited: results.has_been_edited,
},
reactions,
in_reply_to,
thread_root,
}
}
Kind::Redacted => {
Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root }
}
Kind::UnableToDecrypt(msg) => Self {
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
reactions,
in_reply_to,
thread_root,
},
})
}
}
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {
/// The Curve25519 key of the sender.
sender_key: String,
},
// Other fields not included because UniFFI doesn't have the concept of
// deprecated fields right now.
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
/// What we know about what caused this UTD. E.g. was this event sent
/// when we were not a member of this room?
cause: UtdCause,
},
Unknown,
}
impl EncryptedMessage {
pub(crate) fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id, cause: *cause }
}
Message::Unknown => Self::Unknown,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
@@ -0,0 +1,68 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_ui::timeline::TimelineDetails;
use super::{content::TimelineItemContent, ProfileDetails};
#[derive(Clone, uniffi::Object)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
}
impl InReplyToDetails {
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
Self { event_id, event }
}
}
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: event.content().clone().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
TimelineDetails::Error(err) => {
RepliedToEventDetails::Error { message: err.to_string() }
}
};
Self { event_id, event }
}
}
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
@@ -1,55 +0,0 @@
use std::sync::Arc;
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
use ruma::events::{AnySyncTimelineEvent, TimelineEventType};
use crate::event::{MessageLikeEventType, StateEventType};
#[derive(uniffi::Object)]
pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
}
#[uniffi::constructor]
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
}
}
impl TimelineEventTypeFilter {
/// Filters an [`event`] to decide whether it should be part of the timeline
/// based on [`AnySyncTimelineEvent::event_type()`].
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
self.inner.filter(event)
}
}
#[derive(uniffi::Enum, Clone)]
pub enum FilterTimelineEventType {
MessageLike { event_type: MessageLikeEventType },
State { event_type: StateEventType },
}
impl From<FilterTimelineEventType> for TimelineEventType {
fn from(value: FilterTimelineEventType) -> TimelineEventType {
match value {
FilterTimelineEventType::MessageLike { event_type } => {
ruma::events::MessageLikeEventType::from(event_type).into()
}
FilterTimelineEventType::State { event_type } => {
ruma::events::StateEventType::from(event_type).into()
}
}
}
}
+2 -2
View File
@@ -166,7 +166,7 @@ impl Span {
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
pub enum LogLevel {
Error,
Warn,
@@ -176,7 +176,7 @@ pub enum LogLevel {
}
impl LogLevel {
fn to_tracing_level(&self) -> tracing::Level {
fn to_tracing_level(self) -> tracing::Level {
match self {
LogLevel::Error => tracing::Level::ERROR,
LogLevel::Warn => tracing::Level::WARN,
+2 -2
View File
@@ -14,7 +14,7 @@
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::TOKIO1 as RUNTIME;
use async_compat::get_runtime_handle;
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
use tracing::warn;
@@ -54,7 +54,7 @@ impl<T> AsyncRuntimeDropped<T> {
impl<T> Drop for AsyncRuntimeDropped<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
let _guard = get_runtime_handle().enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
+60 -14
View File
@@ -1,5 +1,6 @@
use std::sync::{Arc, Mutex};
use async_compat::get_runtime_handle;
use language_tags::LanguageTag;
use matrix_sdk::{
async_trait,
@@ -8,7 +9,7 @@ use matrix_sdk::{
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::{room::Room, RUNTIME};
use crate::room::Room;
#[derive(uniffi::Record)]
pub struct WidgetDriverAndHandle {
@@ -140,12 +141,31 @@ impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
}
}
/// Defines the intent of showing the call.
///
/// This controls whether to show or skip the lobby.
#[derive(uniffi::Enum, Clone)]
pub enum Intent {
/// The user wants to start a call.
StartCall,
/// The user wants to join an existing call.
JoinExisting,
}
impl From<Intent> for matrix_sdk::widget::Intent {
fn from(value: Intent) -> Self {
match value {
Intent::StartCall => Self::StartCall,
Intent::JoinExisting => Self::JoinExisting,
}
}
}
/// Properties to create a new virtual Element Call widget.
#[derive(uniffi::Record, Clone)]
pub struct VirtualElementCallWidgetOptions {
/// The url to the app.
/// The url to the Element Call app including any `/room` path if required.
///
/// E.g. <https://call.element.io>, <https://call.element.dev>
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
pub element_call_url: String,
/// The widget id.
@@ -188,11 +208,6 @@ pub struct VirtualElementCallWidgetOptions {
/// Default: `false`
pub app_prompt: Option<bool>,
/// Don't show the lobby and join the call immediately.
///
/// Default: `false`
pub skip_lobby: Option<bool>,
/// Make it not possible to get to the calls list in the webview.
///
/// Default: `true`
@@ -201,13 +216,38 @@ pub struct VirtualElementCallWidgetOptions {
/// The font to use, to adapt to the system font.
pub font: Option<String>,
/// Can be used to pass a PostHog id to element call.
pub analytics_id: Option<String>,
/// The encryption system to use.
///
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
pub encryption: EncryptionSystem,
/// The intent of showing the call.
/// If the user wants to start a call or join an existing one.
/// Controls if the lobby is skipped or not.
pub intent: Option<Intent>,
/// Do not show the screenshare button.
pub hide_screensharing: bool,
/// Can be used to pass a PostHog id to element call.
pub posthog_user_id: Option<String>,
/// The host of the posthog api.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub posthog_api_host: Option<String>,
/// The key for the posthog api.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub posthog_api_key: Option<String>,
/// The url to use for submitting rageshakes.
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub rageshake_submit_url: Option<String>,
/// Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub sentry_dsn: Option<String>,
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
/// Supported since Element Call v0.9.0. Only used by the embedded package.
pub sentry_environment: Option<String>,
}
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
@@ -220,11 +260,17 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
preload: value.preload,
font_scale: value.font_scale,
app_prompt: value.app_prompt,
skip_lobby: value.skip_lobby,
confine_to_room: value.confine_to_room,
font: value.font,
analytics_id: value.analytics_id,
posthog_user_id: value.posthog_user_id,
encryption: value.encryption.into(),
intent: value.intent.map(Into::into),
hide_screensharing: value.hide_screensharing,
posthog_api_host: value.posthog_api_host,
posthog_api_key: value.posthog_api_key,
rageshake_submit_url: value.rageshake_submit_url,
sentry_dsn: value.sentry_dsn,
sentry_environment: value.sentry_environment,
}
}
}
@@ -501,7 +547,7 @@ impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
// This could require a prompt to the user. Ideally the callback
// interface would just be async, but that's not supported yet so use
// one of tokio's blocking task threads instead.
RUNTIME
get_runtime_handle()
.spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
.await
// propagate panics from the blocking task
+32
View File
@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" width="250" viewBox="0 0 512 512">
<style>
@media (prefers-color-scheme: dark) {
rect.bg { fill: none; }
}
@media (prefers-color-scheme: light) {
rect.bg { fill: #17191C; }
}
</style>
<g clip-path="url(#clip0_7151_5134)">
<rect class="bg" width="512" height="512" />
<path d="M437.539 522.597L437.062 517.672L451.585 504.129C454.537 501.365 453.431 495.837 449.663 494.43L431.107 487.495L429.649 482.721L441.207 466.64C443.569 463.374 441.396 458.16 437.438 457.507L417.865 454.328L415.516 449.931L423.744 431.878C425.428 428.197 422.3 423.498 418.254 423.649L398.404 424.34L395.264 420.533L399.824 401.186C400.741 397.253 396.759 393.271 392.826 394.188L373.479 398.748L369.66 395.608L370.351 375.758C370.501 371.737 365.803 368.597 362.134 370.268L344.093 378.496L339.696 376.135L336.505 356.561C335.877 352.591 330.638 350.43 327.372 352.792L311.291 364.35L306.517 362.905L299.582 344.35C298.175 340.581 292.634 339.475 289.883 342.415L276.34 356.938L271.415 356.461L260.962 339.563C258.852 336.146 253.173 336.146 251.075 339.563L240.622 356.461L235.697 356.938L222.129 342.39C219.365 339.45 213.837 340.543 212.43 344.324L205.495 362.88L200.721 364.325L184.64 352.767C181.374 350.405 176.148 352.578 175.507 356.536L172.316 376.109L167.919 378.471L149.878 370.242C146.209 368.571 141.498 371.712 141.661 375.733L142.352 395.583L138.533 398.723L119.186 394.163C115.253 393.246 111.271 397.228 112.188 401.161L116.748 420.508L113.608 424.315L93.7577 423.624C89.7374 423.498 86.5966 428.172 88.2675 431.853L96.4965 449.906L94.1346 454.303L74.561 457.482C70.591 458.11 68.4301 463.349 70.792 466.615L82.3502 482.696L80.8929 487.47L62.3369 494.405C58.568 495.812 57.4624 501.353 60.4148 504.104L74.9379 517.647L74.4605 522.572L57.5629 533.025C54.1457 535.135 54.1457 540.814 57.5629 542.912L74.4605 553.365L74.9379 558.289L60.3771 571.883C57.4373 574.647 58.5303 580.175 62.2993 581.582L80.8552 588.517L82.3125 593.291L70.7543 609.372C68.405 612.638 70.5659 617.864 74.5233 618.505L94.0843 621.684L96.4462 626.081L88.2173 644.122C86.5464 647.79 89.6997 652.501 93.7074 652.351L113.557 651.66L116.698 655.479L112.138 674.826C111.221 678.746 115.203 682.741 119.135 681.811L138.483 677.251L142.302 680.392L141.611 700.242C141.46 704.262 146.159 707.403 149.828 705.732L167.868 697.503L172.266 699.865L175.457 719.426C176.085 723.408 181.324 725.557 184.59 723.22L200.671 711.637L205.445 713.094L212.38 731.65C213.787 735.419 219.328 736.524 222.079 733.572L235.622 719.049L240.547 719.551L251 736.449C253.11 739.841 258.764 739.866 260.887 736.449L271.339 719.551L276.264 719.049L289.807 733.572C292.571 736.524 298.099 735.419 299.506 731.65L306.441 713.094L311.215 711.637L327.296 723.22C330.563 725.569 335.789 723.408 336.43 719.426L339.621 699.865L344.018 697.503L362.059 705.732C365.727 707.403 370.426 704.275 370.275 700.242L369.584 680.392L373.391 677.251L392.738 681.811C396.671 682.729 400.653 678.746 399.736 674.826L395.176 655.479L398.316 651.66L418.166 652.351C422.187 652.514 425.327 647.79 423.657 644.122L415.428 626.081L417.777 621.684L437.351 618.505C441.333 617.877 443.506 612.651 441.119 609.372L429.561 593.291L431.019 588.517L449.575 581.582C453.344 580.162 454.449 574.634 451.497 571.883L436.974 558.34L437.451 553.415L454.349 542.962C457.766 540.852 457.778 535.198 454.349 533.075L437.539 522.597Z" fill="#F74C00"/>
<ellipse cx="197.264" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
<ellipse cx="314.125" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
<path d="M230.597 485.777C236.497 497.197 246.672 504.739 258.235 504.739C271.41 504.739 282.782 494.948 288.083 480.787L230.597 485.777Z" fill="#17191C"/>
<ellipse cx="210.602" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
<ellipse cx="326.827" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
<g clip-path="url(#clip1_7151_5134)">
<path d="M186.081 188.249V206.978H186.604C191.603 199.815 197.647 194.293 204.66 190.413C211.674 186.459 219.807 184.519 228.91 184.519C237.64 184.519 245.624 186.235 252.862 189.592C260.1 192.95 265.547 198.994 269.352 207.5C273.456 201.456 279.052 196.084 286.066 191.458C293.08 186.832 301.437 184.519 311.062 184.519C318.374 184.519 325.164 185.414 331.432 187.205C337.7 188.995 342.997 191.831 347.474 195.785C351.951 199.74 355.384 204.814 357.92 211.156C360.383 217.499 361.651 225.109 361.651 234.063V326.661H323.672V248.24C323.672 243.614 323.523 239.212 323.15 235.108C322.777 231.004 321.807 227.422 320.24 224.438C318.598 221.379 316.285 218.991 313.151 217.2C310.017 215.409 305.764 214.514 300.467 214.514C295.094 214.514 290.767 215.559 287.484 217.573C284.2 219.662 281.589 222.274 279.724 225.632C277.858 228.915 276.59 232.645 275.993 236.899C275.396 241.077 275.023 245.33 275.023 249.583V326.661H237.044V249.061C237.044 244.957 236.969 240.928 236.745 236.899C236.596 232.869 235.775 229.213 234.432 225.781C233.089 222.423 230.85 219.662 227.717 217.648C224.583 215.633 220.031 214.589 213.913 214.589C212.122 214.589 209.734 214.962 206.824 215.782C203.914 216.603 201.004 218.095 198.244 220.334C195.483 222.572 193.095 225.781 191.155 229.959C189.215 234.138 188.245 239.659 188.245 246.449V326.735H150.266V188.249H186.081Z" fill="white"/>
<path d="M72.2223 70.8792V441.121H98.86V450H62V62H98.86V70.8792H72.2223Z" fill="white"/>
<path d="M439.785 441.121V70.8792H413.147V62H450.007V450H413.147V441.121H439.785Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_7151_5134">
<rect width="512" height="512" fill="white"/>
</clipPath>
<clipPath id="clip1_7151_5134">
<rect width="388" height="388" fill="white" transform="translate(62 62)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+35
View File
@@ -6,6 +6,41 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.11.0] - 2025-04-11
### Features
- [**breaking**] The `Client::subscribe_to_ignore_user_list_changes()`
method will now only trigger whenever the ignored user list has
changed from what was previously known, instead of triggering
every time an ignore-user-list event has been received from sync.
([#4779](https://github.com/matrix-org/matrix-rust-sdk/pull/4779))
- [**breaking**] The `MediaRetentionPolicy` can now trigger regular cleanups
with its new `cleanup_frequency` setting.
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
- `Clone` is a supertrait of `EventCacheStoreMedia`.
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
- There are new `'static` bounds in `MediaService` for the media cache stores
- `event_cache::store::MemoryStore` implements `Clone`.
- `BaseClient` now has a `handle_verification_events` field which is `true` by
default and can be negated so the `NotificationClient` won't handle received
verification events too, causing errors in the `VerificationMachine`.
- [**breaking**] `Room::is_encryption_state_synced` has been removed
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
- [**breaking**] `Room::is_encrypted` is replaced by `Room::encryption_state`
which returns a value of the new `EncryptionState` enum
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
### Refactor
- [**breaking**] `BaseClient::store` is renamed `state_store`
([#4851](https://github.com/matrix-org/matrix-rust-sdk/pull/4851))
- [**breaking**] `BaseClient::with_store_config` is renamed `new`
([#4847](https://github.com/matrix-org/matrix-rust-sdk/pull/4847))
- [**breaking**] `BaseClient::set_session_metadata` is renamed
`activate`, and `BaseClient::logged_in` is renamed `is_activated`
([#4850](https://github.com/matrix-org/matrix-rust-sdk/pull/4850))
## [0.10.0] - 2025-02-04
### Features
+1 -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.10.0"
version = "0.11.0"
[package.metadata.docs.rs]
all-features = true
@@ -65,7 +65,6 @@ ruma = { workspace = true, features = [
"canonical-json",
"unstable-msc2867",
"unstable-msc3381",
"unstable-msc3575",
"unstable-msc4186",
"rand",
] }
File diff suppressed because it is too large Load Diff
@@ -277,8 +277,10 @@ impl RawAnySyncOrStrippedState {
/// Try to deserialize the inner JSON as the expected type.
pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
match self {
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(raw.deserialize()?)),
Self::Stripped(raw) => Ok(AnySyncOrStrippedState::Stripped(raw.deserialize()?)),
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
Self::Stripped(raw) => {
Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
}
}
}
@@ -300,9 +302,15 @@ impl RawAnySyncOrStrippedState {
#[derive(Clone, Debug)]
pub enum AnySyncOrStrippedState {
/// An event from a room in joined or left state.
Sync(AnySyncStateEvent),
///
/// The value is `Box`ed because it is quite large. Let's keep the size of
/// `Self` as small as possible.
Sync(Box<AnySyncStateEvent>),
/// An event from a room in invited state.
Stripped(AnyStrippedStateEvent),
///
/// The value is `Box`ed because it is quite large. Let's keep the size of
/// `Self` as small as possible.
Stripped(Box<AnyStrippedStateEvent>),
}
impl AnySyncOrStrippedState {
@@ -21,20 +21,24 @@ use matrix_sdk_common::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
VerificationState,
},
linked_chunk::{
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
Update,
},
linked_chunk::{lazy_loader, ChunkContent, ChunkIdentifier as CId, Position, Update},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
push::Action, room_id, uint, RoomId,
api::client::media::get_content_thumbnail::v3::Method,
event_id,
events::{
relation::RelationType,
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
},
mxc_uri,
push::Action,
room_id, uint, EventId, RoomId,
};
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
use crate::{
event_cache::{Event, Gap},
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
};
@@ -43,6 +47,15 @@ use crate::{
///
/// Keep in sync with [`check_test_event`].
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
make_test_event_with_event_id(room_id, content, None)
}
/// Same as [`make_test_event`], with an extra event id.
pub fn make_test_event_with_event_id(
room_id: &RoomId,
content: &str,
event_id: Option<&EventId>,
) -> TimelineEvent {
let encryption_info = EncryptionInfo {
sender: (*ALICE).into(),
sender_device: None,
@@ -51,14 +64,14 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
session_id: Some("mysessionid9".to_owned()),
};
let event = EventFactory::new()
.text_msg(content)
.room(room_id)
.sender(*ALICE)
.into_raw_timeline()
.cast();
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
if let Some(event_id) = event_id {
builder = builder.event_id(event_id);
}
let event = builder.into_raw_timeline().cast();
TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
@@ -100,7 +113,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
/// `EventCacheStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`event_cache_store_integration_tests!`] macro.
/// `event_cache_store_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreIntegrationTests {
@@ -114,6 +127,10 @@ pub trait EventCacheStoreIntegrationTests {
/// the store.
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
/// Test loading a linked chunk incrementally (chunk by chunk) from the
/// store.
async fn test_linked_chunk_incremental_loading(&self);
/// Test that rebuilding a linked chunk from an empty store doesn't return
/// anything.
async fn test_rebuild_empty_linked_chunk(&self);
@@ -123,10 +140,18 @@ pub trait EventCacheStoreIntegrationTests {
/// Test that removing a room from storage empties all associated data.
async fn test_remove_room(&self);
}
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
/// Test that filtering duplicated events works as expected.
async fn test_filter_duplicated_events(&self);
/// Test that an event can be found or not.
async fn test_find_event(&self);
/// Test that finding event relations works as expected.
async fn test_find_event_relations(&self);
/// Test that saving an event works as expected.
async fn test_save_event(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -341,7 +366,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
// new items on 2
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(room_id, "sup")],
@@ -352,8 +377,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();
// The linked chunk is correctly reloaded.
let raws = self.reload_linked_chunk(room_id).await.unwrap();
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
let lc =
lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(room_id).await.unwrap())
.unwrap()
.unwrap();
let mut chunks = lc.chunks();
@@ -392,10 +419,228 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert!(chunks.next().is_none());
}
async fn test_linked_chunk_incremental_loading(&self) {
let room_id = room_id!("!r0:matrix.org");
let event = |msg: &str| make_test_event(room_id, msg);
// Load the last chunk, but none exists yet.
{
let (last_chunk, chunk_identifier_generator) =
self.load_last_chunk(room_id).await.unwrap();
assert!(last_chunk.is_none());
assert_eq!(chunk_identifier_generator.current(), 0);
}
self.handle_linked_chunk_updates(
room_id,
vec![
// new chunk for items
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![event("a"), event("b")],
},
// new chunk for a gap
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "morbier".to_owned() },
},
// new chunk for items
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 2
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![event("c"), event("d"), event("e")],
},
],
)
.await
.unwrap();
// Load the last chunk.
let mut linked_chunk = {
let (last_chunk, chunk_identifier_generator) =
self.load_last_chunk(room_id).await.unwrap();
assert_eq!(chunk_identifier_generator.current(), 2);
let linked_chunk = lazy_loader::from_last_chunk::<DEFAULT_CHUNK_CAPACITY, _, _>(
last_chunk,
chunk_identifier_generator,
)
.unwrap() // unwrap the `Result`
.unwrap(); // unwrap the `Option`
let mut rchunks = linked_chunk.rchunks();
// A unique chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 2);
assert_eq!(chunk.lazy_previous(), Some(CId::new(1)));
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 3);
check_test_event(&events[0], "c");
check_test_event(&events[1], "d");
check_test_event(&events[2], "e");
});
});
assert!(rchunks.next().is_none());
linked_chunk
};
// Load the previous chunk: this is a gap.
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk =
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
let mut rchunks = linked_chunk.rchunks();
// The last chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 2);
assert!(chunk.lazy_previous().is_none());
// Already asserted, but let's be sure nothing breaks.
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 3);
check_test_event(&events[0], "c");
check_test_event(&events[1], "d");
check_test_event(&events[2], "e");
});
});
// The new chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 1);
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
});
});
assert!(rchunks.next().is_none());
}
// Load the previous chunk: these are items.
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk =
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
let mut rchunks = linked_chunk.rchunks();
// The last chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 2);
assert!(chunk.lazy_previous().is_none());
// Already asserted, but let's be sure nothing breaks.
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 3);
check_test_event(&events[0], "c");
check_test_event(&events[1], "d");
check_test_event(&events[2], "e");
});
});
// Its previous chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 1);
assert!(chunk.lazy_previous().is_none());
// Already asserted, but let's be sure nothing breaks.
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
});
});
// The new chunk.
assert_matches!(rchunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 0);
assert!(chunk.lazy_previous().is_none());
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 2);
check_test_event(&events[0], "a");
check_test_event(&events[1], "b");
});
});
assert!(rchunks.next().is_none());
}
// Load the previous chunk: there is none.
{
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
let previous_chunk = self.load_previous_chunk(room_id, first_chunk).await.unwrap();
assert!(previous_chunk.is_none());
}
// One last check: a round of assert by using the forwards chunk iterator
// instead of the backwards chunk iterator.
{
let mut chunks = linked_chunk.chunks();
// The first chunk.
assert_matches!(chunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 0);
assert!(chunk.lazy_previous().is_none());
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 2);
check_test_event(&events[0], "a");
check_test_event(&events[1], "b");
});
});
// The second chunk.
assert_matches!(chunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 1);
assert!(chunk.lazy_previous().is_none());
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
});
});
// The third and last chunk.
assert_matches!(chunks.next(), Some(chunk) => {
assert_eq!(chunk.identifier(), 2);
assert!(chunk.lazy_previous().is_none());
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 3);
check_test_event(&events[0], "c");
check_test_event(&events[1], "d");
check_test_event(&events[2], "e");
});
});
assert!(chunks.next().is_none());
}
}
async fn test_rebuild_empty_linked_chunk(&self) {
// When I rebuild a linked chunk from an empty store, it's empty.
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
assert!(rebuild_linked_chunk(raw_parts).is_none());
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
self.load_all_chunks(&DEFAULT_TEST_ROOM_ID).await.unwrap(),
)
.unwrap();
assert!(linked_chunk.is_none());
}
async fn test_clear_all_rooms_chunks(&self) {
@@ -444,15 +689,23 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
.unwrap();
// Sanity check: both linked chunks can be reloaded.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
.unwrap()
.is_some());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
.unwrap()
.is_some());
// Clear the chunks.
self.clear_all_rooms_chunks().await.unwrap();
// Both rooms now have no linked chunk.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
.unwrap()
.is_none());
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
.unwrap()
.is_none());
}
async fn test_remove_room(&self) {
@@ -495,13 +748,272 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
self.remove_room(r0).await.unwrap();
// Check that r0 doesn't have a linked chunk anymore.
let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
let r0_linked_chunk = self.load_all_chunks(r0).await.unwrap();
assert!(r0_linked_chunk.is_empty());
// Check that r1 is unaffected.
let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
let r1_linked_chunk = self.load_all_chunks(r1).await.unwrap();
assert!(!r1_linked_chunk.is_empty());
}
async fn test_filter_duplicated_events(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
let event_brigand = event("brigand du jorat");
let event_raclette = event("raclette");
let event_morbier = event("morbier");
let event_gruyere = event("gruyère");
let event_tome = event("tome");
let event_mont_dor = event("mont d'or");
self.handle_linked_chunk_updates(
room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![event_comte.clone(), event_brigand.clone()],
},
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "brillat-savarin".to_owned() },
},
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![event_morbier.clone(), event_mont_dor.clone()],
},
],
)
.await
.unwrap();
// Add other events in another room, to ensure filtering take the `room_id` into
// account.
self.handle_linked_chunk_updates(
another_room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![event_tome.clone()],
},
],
)
.await
.unwrap();
let duplicated_events = self
.filter_duplicated_events(
room_id,
vec![
event_comte.event_id().unwrap().to_owned(),
event_raclette.event_id().unwrap().to_owned(),
event_morbier.event_id().unwrap().to_owned(),
event_gruyere.event_id().unwrap().to_owned(),
event_tome.event_id().unwrap().to_owned(),
event_mont_dor.event_id().unwrap().to_owned(),
],
)
.await
.unwrap();
assert_eq!(duplicated_events.len(), 3);
assert_eq!(
duplicated_events[0],
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
);
assert_eq!(
duplicated_events[1],
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
);
assert_eq!(
duplicated_events[2],
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
);
}
async fn test_find_event(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
let event_gruyere = event("gruyère");
// Add one event in one room.
self.handle_linked_chunk_updates(
room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![event_comte.clone()],
},
],
)
.await
.unwrap();
// Add another event in another room.
self.handle_linked_chunk_updates(
another_room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![event_gruyere.clone()],
},
],
)
.await
.unwrap();
// Now let's find the event.
let event = self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");
assert_eq!(event.event_id(), event_comte.event_id());
// Now let's try to find an event that exists, but not in the expected room.
assert!(self
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
// Clearing the rooms also clears the event's storage.
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
assert!(self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
}
async fn test_find_event_relations(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
// Create event and related events for the first room.
let eid1 = event_id!("$event1:matrix.org");
let e1 = f.text_msg("comter").event_id(eid1).into_event();
let edit_eid1 = event_id!("$edit_event1:matrix.org");
let edit_e1 = f
.text_msg("* comté")
.event_id(edit_eid1)
.edit(eid1, RoomMessageEventContentWithoutRelation::text_plain("comté"))
.into_event();
let reaction_eid1 = event_id!("$reaction_event1:matrix.org");
let reaction_e1 = f.reaction(eid1, "👍").event_id(reaction_eid1).into_event();
let eid2 = event_id!("$event2:matrix.org");
let e2 = f.text_msg("galette saucisse").event_id(eid2).into_event();
// Create events for the second room.
let f = f.room(another_room_id);
let eid3 = event_id!("$event3:matrix.org");
let e3 = f.text_msg("gruyère").event_id(eid3).into_event();
let reaction_eid3 = event_id!("$reaction_event3:matrix.org");
let reaction_e3 = f.reaction(eid3, "👍").event_id(reaction_eid3).into_event();
// Save All The Things!
self.save_event(room_id, e1).await.unwrap();
self.save_event(room_id, edit_e1).await.unwrap();
self.save_event(room_id, reaction_e1).await.unwrap();
self.save_event(room_id, e2).await.unwrap();
self.save_event(another_room_id, e3).await.unwrap();
self.save_event(another_room_id, reaction_e3).await.unwrap();
// Finding relations without a filter returns all of them.
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
// Finding relations with a filter only returns a subset.
let relations = self
.find_event_relations(room_id, eid1, Some(&[RelationType::Replacement]))
.await
.unwrap();
assert_eq!(relations.len(), 1);
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
let relations = self
.find_event_relations(
room_id,
eid1,
Some(&[RelationType::Replacement, RelationType::Annotation]),
)
.await
.unwrap();
assert_eq!(relations.len(), 2);
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
// We can't find relations using the wrong room.
let relations = self
.find_event_relations(another_room_id, eid1, Some(&[RelationType::Replacement]))
.await
.unwrap();
assert!(relations.is_empty());
}
async fn test_save_event(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
let event = |msg: &str| make_test_event(room_id, msg);
let event_comte = event("comté");
let event_gruyere = event("gruyère");
// Add one event in one room.
self.save_event(room_id, event_comte.clone()).await.unwrap();
// Add another event in another room.
self.save_event(another_room_id, event_gruyere.clone()).await.unwrap();
// Events can be found, when searched in their own rooms.
let event = self
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");
assert_eq!(event.event_id(), event_comte.event_id());
let event = self
.find_event(another_room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.expect("failed to find an event");
assert_eq!(event.event_id(), event_gruyere.event_id());
// But they won't be returned when searching in the wrong room.
assert!(self
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
assert!(self
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
.await
.expect("failed to query for finding an event")
.is_none());
}
}
/// Macro building to allow your `EventCacheStore` implementation to run the
@@ -564,6 +1076,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
}
#[async_test]
async fn test_linked_chunk_incremental_loading() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_linked_chunk_incremental_loading().await;
}
#[async_test]
async fn test_rebuild_empty_linked_chunk() {
let event_cache_store =
@@ -584,6 +1103,34 @@ macro_rules! event_cache_store_integration_tests {
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_remove_room().await;
}
#[async_test]
async fn test_filter_duplicated_events() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_filter_duplicated_events().await;
}
#[async_test]
async fn test_find_event() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event().await;
}
#[async_test]
async fn test_find_event_relations() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event_relations().await;
}
#[async_test]
async fn test_save_event() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_save_event().await;
}
}
};
}
@@ -30,7 +30,7 @@ use crate::media::{MediaFormat, MediaRequestParameters};
/// [`EventCacheStoreMedia`] integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`event_cache_store_media_integration_tests!`] macro.
/// `event_cache_store_media_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreMediaIntegrationTests {
@@ -53,6 +53,9 @@ pub trait EventCacheStoreMediaIntegrationTests {
/// Test [`IgnoreMediaRetentionPolicy`] with the media content's retention
/// policy expiry.
async fn test_media_ignore_expiry(&self);
/// Test last media cleanup time storage.
async fn test_store_last_media_cleanup_time(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -941,6 +944,25 @@ where
let stored = self.get_media_content_inner(&request_5, time).await.unwrap();
assert!(stored.is_none());
}
async fn test_store_last_media_cleanup_time(&self) {
let initial = self.last_media_cleanup_time_inner().await.unwrap();
let new_time = initial.unwrap_or_else(SystemTime::now) + Duration::from_secs(60);
// With an empty policy.
let policy = MediaRetentionPolicy::empty();
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
let stored = self.last_media_cleanup_time_inner().await.unwrap();
assert_eq!(stored, initial);
// With the default policy.
let policy = MediaRetentionPolicy::default();
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
let stored = self.last_media_cleanup_time_inner().await.unwrap();
assert_eq!(stored, Some(new_time));
}
}
/// Macro building to allow your [`EventCacheStoreMedia`] implementation to run
@@ -1031,5 +1053,11 @@ macro_rules! event_cache_store_media_integration_tests {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_ignore_expiry().await;
}
#[async_test]
async fn test_store_last_media_cleanup_time() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_store_last_media_cleanup_time().await;
}
};
}
@@ -33,6 +33,7 @@ use serde::{Deserialize, Serialize};
///
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[non_exhaustive]
pub struct MediaRetentionPolicy {
/// The maximum authorized size of the overall media cache, in bytes.
@@ -50,7 +51,7 @@ pub struct MediaRetentionPolicy {
///
/// Defaults to 400 MiB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cache_size: Option<usize>,
pub max_cache_size: Option<u64>,
/// The maximum authorized size of a single media content, in bytes.
///
@@ -68,7 +69,7 @@ pub struct MediaRetentionPolicy {
///
/// Defaults to 20 MiB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_file_size: Option<usize>,
pub max_file_size: Option<u64>,
/// The duration after which unaccessed media content is considered
/// expired.
@@ -79,6 +80,17 @@ pub struct MediaRetentionPolicy {
/// Defaults to 60 days.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_access_expiry: Option<Duration>,
/// The duration between two automatic media cache cleanups.
///
/// If this is set, a cleanup will be triggered after the given duration
/// is elapsed, at the next call to the media cache API. If this is set to
/// zero, each call to the media cache API will trigger a cleanup. If this
/// is `None`, cleanups will only occur if they are triggered manually.
///
/// Defaults to running cleanups daily.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cleanup_frequency: Option<Duration>,
}
impl MediaRetentionPolicy {
@@ -91,17 +103,22 @@ impl MediaRetentionPolicy {
///
/// This means that all media will be cached and cleanups have no effect.
pub fn empty() -> Self {
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
Self {
max_cache_size: None,
max_file_size: None,
last_access_expiry: None,
cleanup_frequency: None,
}
}
/// Set the maximum authorized size of the overall media cache, in bytes.
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
pub fn with_max_cache_size(mut self, size: Option<u64>) -> Self {
self.max_cache_size = size;
self
}
/// Set the maximum authorized size of a single media content, in bytes.
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
pub fn with_max_file_size(mut self, size: Option<u64>) -> Self {
self.max_file_size = size;
self
}
@@ -113,6 +130,12 @@ impl MediaRetentionPolicy {
self
}
/// Set the duration between two automatic media cache cleanups.
pub fn with_cleanup_frequency(mut self, duration: Option<Duration>) -> Self {
self.cleanup_frequency = duration;
self
}
/// Whether this policy has limitations.
///
/// If this policy has no limitations, a cleanup job would have no effect.
@@ -130,7 +153,7 @@ impl MediaRetentionPolicy {
/// # Arguments
///
/// * `size` - The overall size of the media cache to check, in bytes.
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
self.max_cache_size.is_some_and(|max_size| size > max_size)
}
@@ -138,7 +161,7 @@ impl MediaRetentionPolicy {
/// bytes.
///
/// This is the lowest value between `max_cache_size` and `max_file_size`.
pub fn computed_max_file_size(&self) -> Option<usize> {
pub fn computed_max_file_size(&self) -> Option<u64> {
match (self.max_cache_size, self.max_file_size) {
(None, None) => None,
(None, Some(size)) => Some(size),
@@ -153,7 +176,7 @@ impl MediaRetentionPolicy {
/// # Arguments
///
/// * `size` - The size of the media content to check, in bytes.
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
pub fn exceeds_max_file_size(&self, size: u64) -> bool {
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
}
@@ -178,6 +201,24 @@ impl MediaRetentionPolicy {
.is_ok_and(|elapsed| elapsed >= max_duration)
})
}
/// Whether an automatic media cache cleanup should be triggered given the
/// time of the last cleanup.
///
/// # Arguments
///
/// * `current_time` - The current time.
///
/// * `last_cleanup_time` - The time of the last media cache cleanup.
pub fn should_clean_up(&self, current_time: SystemTime, last_cleanup_time: SystemTime) -> bool {
self.cleanup_frequency.is_some_and(|max_duration| {
current_time
.duration_since(last_cleanup_time)
// If this returns an error, the last cleanup time is newer than the current time.
// This shouldn't happen but in this case no cleanup job is needed.
.is_ok_and(|elapsed| elapsed >= max_duration)
})
}
}
impl Default for MediaRetentionPolicy {
@@ -189,6 +230,8 @@ impl Default for MediaRetentionPolicy {
max_file_size: Some(20 * 1024 * 1024),
// 60 days.
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
// 1 day.
cleanup_frequency: Some(Duration::from_secs(24 * 60 * 60)),
}
}
}
@@ -327,4 +370,36 @@ mod tests {
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
}
#[test]
fn test_media_retention_policy_cleanup_frequency() {
let epoch = SystemTime::UNIX_EPOCH;
let epoch_plus_60 = epoch + Duration::from_secs(60);
let epoch_plus_120 = epoch + Duration::from_secs(120);
let mut policy = MediaRetentionPolicy::empty();
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(0)));
assert!(policy.should_clean_up(epoch_plus_60, epoch));
assert!(policy.should_clean_up(epoch_plus_60, epoch_plus_60));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(30)));
assert!(policy.should_clean_up(epoch_plus_60, epoch));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(60)));
assert!(policy.should_clean_up(epoch_plus_60, epoch));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(90)));
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
}
}
@@ -12,12 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt;
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
use matrix_sdk_common::{
executor::{spawn, JoinHandle},
locks::Mutex,
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
};
use ruma::{time::SystemTime, MxcUri};
use tokio::sync::Mutex as AsyncMutex;
use tracing::error;
use super::MediaRetentionPolicy;
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
@@ -28,6 +33,11 @@ use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParamet
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
#[derive(Debug)]
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
inner: Arc<MediaServiceInner<Time>>,
}
#[derive(Debug)]
struct MediaServiceInner<Time: TimeProvider = DefaultTimeProvider> {
/// The time provider.
time_provider: Time,
@@ -36,6 +46,15 @@ pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
/// A mutex to ensure a single cleanup is running at a time.
cleanup_guard: AsyncMutex<()>,
/// The time of the last media cache cleanup.
last_media_cleanup_time: Mutex<Option<SystemTime>>,
/// The [`JoinHandle`] for an automatic media cleanup task.
///
/// Used to ensure that only one automatic cleanup is running at a time, and
/// to stop the cleanup when the [`MediaServiceInner`] is dropped.
automatic_media_cleanup_join_handle: Mutex<Option<JoinHandle<()>>>,
}
impl MediaService {
@@ -56,16 +75,20 @@ impl Default for MediaService {
impl<Time> MediaService<Time>
where
Time: TimeProvider,
Time: TimeProvider + 'static,
{
/// Construct a new `MediaService` with the given `TimeProvider` and an
/// empty `MediaRetentionPolicy`.
fn with_time_provider(time_provider: Time) -> Self {
Self {
let inner = MediaServiceInner {
time_provider,
policy: Mutex::new(MediaRetentionPolicy::empty()),
cleanup_guard: AsyncMutex::new(()),
}
last_media_cleanup_time: Mutex::new(None),
automatic_media_cleanup_join_handle: Mutex::new(None),
};
Self { inner: Arc::new(inner) }
}
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
@@ -76,10 +99,23 @@ where
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
pub fn restore(
&self,
policy: Option<MediaRetentionPolicy>,
last_media_cleanup_time: Option<SystemTime>,
) {
if let Some(policy) = policy {
*self.policy.lock() = policy;
*self.inner.policy.lock() = policy;
}
if let Some(time) = last_media_cleanup_time {
*self.inner.last_media_cleanup_time.lock() = Some(time);
}
}
/// Get the current time from the inner [`TimeProvider`].
fn now(&self) -> SystemTime {
self.inner.time_provider.now()
}
/// Set the `MediaRetentionPolicy` of this service.
@@ -89,21 +125,23 @@ where
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `policy` - The `MediaRetentionPolicy` to use.
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
&self,
store: &Store,
policy: MediaRetentionPolicy,
) -> Result<(), Store::Error> {
store.set_media_retention_policy_inner(policy).await?;
*self.policy.lock() = policy;
*self.inner.policy.lock() = policy;
self.maybe_spawn_automatic_media_cache_cleanup(store, self.now());
Ok(())
}
/// Get the `MediaRetentionPolicy` of this service.
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
*self.policy.lock()
*self.inner.policy.lock()
}
/// Add a media file's content in the media store.
@@ -118,7 +156,7 @@ where
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
pub async fn add_media_content<Store: EventCacheStoreMedia>(
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
&self,
store: &Store,
request: &MediaRequestParameters,
@@ -128,21 +166,20 @@ where
let policy = self.media_retention_policy();
if ignore_policy == IgnoreMediaRetentionPolicy::No
&& policy.exceeds_max_file_size(content.len())
&& policy.exceeds_max_file_size(content.len() as u64)
{
// We do not cache the content.
return Ok(());
}
let current_time = self.now();
store
.add_media_content_inner(
request,
content,
self.time_provider.now(),
policy,
ignore_policy,
)
.await
.add_media_content_inner(request, content, current_time, policy, ignore_policy)
.await?;
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
Ok(())
}
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
@@ -174,12 +211,17 @@ where
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `request` - The `MediaRequestParameters` of the file.
pub async fn get_media_content<Store: EventCacheStoreMedia>(
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
&self,
store: &Store,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Store::Error> {
store.get_media_content_inner(request, self.time_provider.now()).await
let current_time = self.now();
let content = store.get_media_content_inner(request, current_time).await?;
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
Ok(content)
}
/// Get a media file's content associated to an `MxcUri` from the
@@ -190,12 +232,17 @@ where
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `uri` - The `MxcUri` of the media file.
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
&self,
store: &Store,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Store::Error> {
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
let current_time = self.now();
let content = store.get_media_content_for_uri_inner(uri, current_time).await?;
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
Ok(content)
}
/// Clean up the media cache with the current `MediaRetentionPolicy`.
@@ -209,7 +256,15 @@ where
&self,
store: &Store,
) -> Result<(), Store::Error> {
let Ok(_guard) = self.cleanup_guard.try_lock() else {
self.clean_up_media_cache_inner(store, self.now()).await
}
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
&self,
store: &Store,
current_time: SystemTime,
) -> Result<(), Store::Error> {
let Ok(_guard) = self.inner.cleanup_guard.try_lock() else {
// There is another ongoing cleanup.
return Ok(());
};
@@ -221,7 +276,76 @@ where
return Ok(());
}
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
store.clean_up_media_cache_inner(policy, current_time).await?;
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
Ok(())
}
/// Spawn an automatic media cache cleanup, according to the media retention
/// policy.
///
/// A cleanup will be spawned if:
/// * The media retention policy's `cleanup_frequency` is set and enough
/// time has passed since the last cleanup.
/// * No other cleanup is running,
fn maybe_spawn_automatic_media_cache_cleanup<Store: EventCacheStoreMedia + 'static>(
&self,
store: &Store,
current_time: SystemTime,
) {
let mut join_handle = self.inner.automatic_media_cleanup_join_handle.lock();
if join_handle.as_ref().is_some_and(|join_handle| !join_handle.is_finished()) {
// There is an ongoing automatic media cache cleanup.
return;
}
let policy = self.media_retention_policy();
if policy.cleanup_frequency.is_none() || !policy.has_limitations() {
// Automatic cleanups are disabled or have no effect.
return;
}
let last_media_cleanup_time = *self.inner.last_media_cleanup_time.lock();
if last_media_cleanup_time.is_some_and(|last_cleanup_time| {
!policy.should_clean_up(current_time, last_cleanup_time)
}) {
// It is not time to clean up.
return;
}
let this = self.clone();
let store = store.clone();
let handle = spawn(async move {
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
error!("Failed to run automatic media cache cleanup: {error}");
}
});
*join_handle = Some(handle);
}
}
impl<Time> Clone for MediaService<Time>
where
Time: TimeProvider,
{
fn clone(&self) -> Self {
Self { inner: self.inner.clone() }
}
}
impl<Time> Drop for MediaServiceInner<Time>
where
Time: TimeProvider,
{
fn drop(&mut self) {
if let Some(join_handle) = self.automatic_media_cleanup_join_handle.lock().take() {
join_handle.abort();
}
}
}
@@ -234,9 +358,9 @@ where
/// over the `SystemTime`s provided to the store.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreMedia: AsyncTraitDeps {
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
/// The error type used by this media cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
/// The persisted media retention policy in the media cache.
async fn media_retention_policy_inner(
@@ -340,12 +464,15 @@ pub trait EventCacheStoreMedia: AsyncTraitDeps {
/// `cleanup_frequency` will be ignored.
///
/// * `current_time` - The current time, to be used to check for expired
/// content.
/// content and to be stored as the time of the last media cache cleanup.
async fn clean_up_media_cache_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error>;
/// The time of the last media cache cleanup.
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
}
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
@@ -385,7 +512,7 @@ impl IgnoreMediaRetentionPolicy {
/// An abstract trait to provide the current `SystemTime` for the
/// [`MediaService`].
pub trait TimeProvider {
pub trait TimeProvider: SendOutsideWasm + SyncOutsideWasm {
/// The current time.
fn now(&self) -> SystemTime;
}
@@ -402,7 +529,10 @@ impl TimeProvider for DefaultTimeProvider {
#[cfg(test)]
mod tests {
use std::{fmt, sync::MutexGuard};
use std::{
fmt,
sync::{Arc, MutexGuard},
};
use async_trait::async_trait;
use matrix_sdk_common::locks::Mutex;
@@ -420,9 +550,9 @@ mod tests {
media::{MediaFormat, MediaRequestParameters, UniqueKey},
};
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
struct MockEventCacheStoreMedia {
inner: Mutex<MockEventCacheStoreMediaInner>,
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
}
impl MockEventCacheStoreMedia {
@@ -529,7 +659,7 @@ mod tests {
) -> Result<(), Self::Error> {
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
if !ignore_policy && policy.exceeds_max_file_size(content.len() as u64) {
return Ok(());
}
@@ -617,6 +747,10 @@ mod tests {
Ok(())
}
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
Ok(self.inner().cleanup_time)
}
}
#[derive(Debug)]
@@ -658,7 +792,7 @@ mod tests {
// By default an empty policy is used.
assert!(!service.media_retention_policy().has_limitations());
service.restore(None);
service.restore(None, None);
assert!(!service.media_retention_policy().has_limitations());
assert!(!store.accessed());
@@ -676,7 +810,7 @@ mod tests {
assert_eq!(media_content.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Get media from request.
@@ -689,7 +823,7 @@ mod tests {
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
@@ -712,12 +846,12 @@ mod tests {
assert!(media_content.ignore_policy);
// Try a cleanup. With the empty policy the store should not be accessed.
assert_eq!(store.inner().cleanup_time, None);
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
assert!(!store.accessed());
assert_eq!(store.inner().cleanup_time, None);
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
}
#[async_test]
@@ -747,7 +881,7 @@ mod tests {
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// Check that restoring the policy works.
service.restore(Some(MediaRetentionPolicy::default()));
service.restore(Some(MediaRetentionPolicy::default()), None);
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
assert!(!store.accessed());
@@ -779,7 +913,7 @@ mod tests {
assert_eq!(media_content.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Get media from request.
@@ -792,7 +926,7 @@ mod tests {
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
@@ -805,7 +939,7 @@ mod tests {
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Add big media, it will not work because it is bigger than the max file size.
@@ -858,7 +992,7 @@ mod tests {
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
@@ -871,14 +1005,101 @@ mod tests {
assert_eq!(media.last_access, now);
// Try a cleanup, the store should be accessed.
assert_eq!(store.inner().cleanup_time, None);
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
service.inner.time_provider.set_now(now);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
assert!(store.accessed());
assert_eq!(store.inner().cleanup_time, Some(now));
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
}
#[async_test]
async fn test_media_service_automatic_cleanup() {
// 64 bytes content.
let content = vec![0; 64];
let uri_1 = mxc_uri!("mxc://localhost/media-1");
let request_1 = MediaRequestParameters {
source: MediaSource::Plain(uri_1.to_owned()),
format: MediaFormat::File,
};
let uri_2 = mxc_uri!("mxc://localhost/media-2");
let request_2 = MediaRequestParameters {
source: MediaSource::Plain(uri_2.to_owned()),
format: MediaFormat::File,
};
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// Set an empty policy.
let policy = MediaRetentionPolicy::empty();
service.set_media_retention_policy(&store, policy).await.unwrap();
// Add the contents.
service
.add_media_content(&store, &request_1, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.unwrap();
service
.add_media_content(&store, &request_2, content, IgnoreMediaRetentionPolicy::No)
.await
.unwrap();
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
// Try to launch an automatic cleanup.
let now = now + Duration::from_secs(60);
service.inner.time_provider.set_now(now);
service.maybe_spawn_automatic_media_cache_cleanup(&store, now);
// No cleanup was spawned since automatic cleanups are disabled.
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
// Set a policy with automatic cleanup every hour.
let policy = MediaRetentionPolicy::empty()
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)));
let now = now + Duration::from_secs(60);
service.inner.time_provider.set_now(now);
service.set_media_retention_policy(&store, policy).await.unwrap();
// No cleanup was spawned since the policy has no limitations.
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
// Set a policy with automatic cleanup every hour and a max file size.
let policy = MediaRetentionPolicy::empty()
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)))
.with_max_file_size(Some(512));
let now = now + Duration::from_secs(60);
service.inner.time_provider.set_now(now);
service.set_media_retention_policy(&store, policy).await.unwrap();
// A cleanup was spawned since there was no last_media_cleanup_time.
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
join_handle.await.unwrap();
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
// Try again one minute in the future, nothing is spawned because we need to
// wait for one hour.
let now = now + Duration::from_secs(60);
service.inner.time_provider.set_now(now);
service.get_media_content(&store, &request_1).await.unwrap();
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
// Try again 2 hours in the future, another cleanup is spawned.
let now = now + Duration::from_secs(2 * 60 * 60);
service.inner.time_provider.set_now(now);
service.get_media_content_for_uri(&store, uri_1).await.unwrap();
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
join_handle.await.unwrap();
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
}
}
@@ -12,20 +12,30 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock};
use std::{
collections::HashMap,
num::NonZeroUsize,
sync::{Arc, RwLock as StdRwLock},
};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
linked_chunk::{
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator, Position,
RawChunk, Update,
},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{
events::relation::RelationType,
time::{Instant, SystemTime},
MxcUri, OwnedMxcUri, RoomId,
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
};
use tracing::error;
use super::{
compute_filters_string, extract_event_relation,
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
EventCacheStore, EventCacheStoreError, Result,
};
@@ -37,10 +47,9 @@ use crate::{
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct MemoryStore {
inner: StdRwLock<MemoryStoreInner>,
inner: Arc<StdRwLock<MemoryStoreInner>>,
media_service: MediaService,
}
@@ -48,8 +57,9 @@ pub struct MemoryStore {
struct MemoryStoreInner {
media: RingBuffer<MediaContent>,
leases: HashMap<String, (String, Instant)>,
events: RelationalLinkedChunk<Event, Gap>,
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
media_retention_policy: Option<MediaRetentionPolicy>,
last_media_cleanup_time: SystemTime,
}
/// A media content in the `MemoryStore`.
@@ -71,20 +81,24 @@ struct MediaContent {
last_access: SystemTime,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
impl Default for MemoryStore {
fn default() -> Self {
// Given that the store is empty, we won't need to clean it up right away.
let last_media_cleanup_time = SystemTime::now();
let media_service = MediaService::new();
media_service.restore(None, Some(last_media_cleanup_time));
Self {
inner: StdRwLock::new(MemoryStoreInner {
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
events: RelationalLinkedChunk::new(),
media_retention_policy: None,
}),
// No need to call `restore()` since nothing is persisted.
media_service: MediaService::new(),
last_media_cleanup_time,
})),
media_service,
}
}
}
@@ -123,14 +137,37 @@ impl EventCacheStore for MemoryStore {
Ok(())
}
async fn reload_linked_chunk(
async fn load_all_chunks(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.reload_chunks(room_id)
.load_all_chunks(room_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_last_chunk(
&self,
room_id: &RoomId,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_last_chunk(room_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn load_previous_chunk(
&self,
room_id: &RoomId,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.load_previous_chunk(room_id, before_chunk_identifier)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
@@ -139,6 +176,97 @@ impl EventCacheStore for MemoryStore {
Ok(())
}
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
mut events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
// Collect all duplicated events.
let inner = self.inner.read().unwrap();
let mut duplicated_events = Vec::new();
for (event, position) in inner.events.unordered_room_items(room_id) {
// If `events` is empty, we can short-circuit.
if events.is_empty() {
break;
}
if let Some(known_event_id) = event.event_id() {
// This event is a duplicate!
if let Some(index) =
events.iter().position(|new_event_id| &known_event_id == new_event_id)
{
duplicated_events.push((events.remove(index), position));
}
}
}
Ok(duplicated_events)
}
async fn find_event(
&self,
room_id: &RoomId,
event_id: &EventId,
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event = inner.events.items().find_map(|(event, this_room_id)| {
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
});
Ok(event)
}
async fn find_event_relations(
&self,
room_id: &RoomId,
event_id: &EventId,
filters: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let filters = compute_filters_string(filters);
let related_events = inner
.events
.items()
.filter_map(|(event, this_room_id)| {
// Must be in the same room.
if room_id != this_room_id {
return None;
}
// Must have a relation.
let (related_to, rel_type) = extract_event_relation(event.raw())?;
// Must relate to the target item.
if related_to != event_id {
return None;
}
// Must not be filtered out.
if let Some(filters) = &filters {
filters.contains(&rel_type).then_some(event.clone())
} else {
Some(event.clone())
}
})
.collect();
Ok(related_events)
}
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
if event.event_id().is_none() {
error!(%room_id, "Trying to save an event with no ID");
return Ok(());
}
self.inner.write().unwrap().events.save_item(room_id.to_owned(), event);
Ok(())
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
@@ -268,7 +396,7 @@ impl EventCacheStoreMedia for MemoryStore {
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
// Do not store it.
return Ok(());
};
@@ -374,7 +502,7 @@ impl EventCacheStoreMedia for MemoryStore {
// First, check media content that exceed the max filesize.
if policy.computed_max_file_size().is_some() {
inner.media.retain(|content| {
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len())
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
});
}
@@ -392,7 +520,7 @@ impl EventCacheStoreMedia for MemoryStore {
// to count the number of old items to remove. Items are sorted by last access
// and old items are at the start.
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|(mut cache_size, mut items_to_remove), (index, content)| {
if content.ignore_policy {
// Do not count it.
@@ -401,7 +529,7 @@ impl EventCacheStoreMedia for MemoryStore {
let remove_item = if items_to_remove.is_empty() {
// We have not reached the max cache size yet.
if let Some(sum) = cache_size.checked_add(content.data.len()) {
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
cache_size = sum;
// Start removing items if we have exceeded the max cache size.
cache_size > max_cache_size
@@ -431,8 +559,14 @@ impl EventCacheStoreMedia for MemoryStore {
}
}
inner.last_media_cleanup_time = current_time;
Ok(())
}
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
}
}
#[cfg(test)]
@@ -32,6 +32,12 @@ use matrix_sdk_common::store_locks::{
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use ruma::{
events::{relation::RelationType, AnySyncTimelineEvent},
serde::Raw,
OwnedEventId,
};
use tracing::trace;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::EventCacheStoreIntegrationTests;
@@ -193,3 +199,51 @@ impl BackingStore for LockableEventCacheStore {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
/// Helper to extract the relation information from an event.
///
/// If the event isn't in relation to another event, then this will return
/// `None`. Otherwise, returns both the event id this event relates to, and the
/// kind of relation as a string (e.g. `m.replace`).
pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(OwnedEventId, String)> {
#[derive(serde::Deserialize)]
struct RelatesTo {
event_id: OwnedEventId,
rel_type: String,
}
#[derive(serde::Deserialize)]
struct EventContent {
#[serde(rename = "m.relates_to")]
rel: Option<RelatesTo>,
}
match event.get_field::<EventContent>("content") {
Ok(event_content) => {
event_content.and_then(|c| c.rel).map(|rel| (rel.event_id, rel.rel_type))
}
Err(err) => {
trace!("when extracting relation data from an event: {err}");
None
}
}
}
/// Compute the list of string filters to be applied when looking for an event's
/// relations.
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
// then.
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
filters.map(|filter| {
filter
.iter()
.map(|f| {
if *f == RelationType::Replacement {
"m.replace".to_owned()
} else {
f.to_string()
}
})
.collect()
})
}
@@ -16,10 +16,10 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{RawChunk, Update},
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
AsyncTraitDeps,
};
use ruma::{MxcUri, RoomId};
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
use super::{
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
@@ -69,17 +69,80 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// Return all the raw components of a linked chunk, so the caller may
/// reconstruct the linked chunk later.
async fn reload_linked_chunk(
#[doc(hidden)]
async fn load_all_chunks(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
/// Load the last chunk of the `LinkedChunk` holding all events of the room
/// identified by `room_id`.
///
/// This is used to iteratively load events for the `EventCache`.
async fn load_last_chunk(
&self,
room_id: &RoomId,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
/// Load the chunk before the chunk identified by `before_chunk_identifier`
/// of the `LinkedChunk` holding all events of the room identified by
/// `room_id`
///
/// This is used to iteratively load events for the `EventCache`.
async fn load_previous_chunk(
&self,
room_id: &RoomId,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
/// Clear persisted events for all the rooms.
///
/// This will empty and remove all the linked chunks stored previously,
/// using the above [`Self::handle_linked_chunk_updates`] methods.
/// using the above [`Self::handle_linked_chunk_updates`] methods. It
/// must *also* delete all the events' content, if they were stored in a
/// separate table.
///
/// ⚠ This is meant only for super specific use cases, where there shouldn't
/// be any live in-memory linked chunks. In general, prefer using
/// `EventCache::clear_all_rooms()` from the common SDK crate.
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
/// Given a set of event IDs, return the duplicated events along with their
/// position if there are any.
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
/// Find an event by its ID.
async fn find_event(
&self,
room_id: &RoomId,
event_id: &EventId,
) -> Result<Option<Event>, Self::Error>;
/// Find all the events that relate to a given event.
///
/// An additional filter can be provided to only retrieve related events for
/// a certain relationship.
async fn find_event_relations(
&self,
room_id: &RoomId,
event_id: &EventId,
filter: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error>;
/// Save an event, that might or might not be part of an existing linked
/// chunk.
///
/// If the event has no event id, it will not be saved, and the function
/// must return an Ok result early.
///
/// If the event was already stored with the same id, it must be replaced,
/// without causing an error.
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
@@ -236,17 +299,61 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
}
async fn reload_linked_chunk(
async fn load_all_chunks(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
self.0.load_all_chunks(room_id).await.map_err(Into::into)
}
async fn load_last_chunk(
&self,
room_id: &RoomId,
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
self.0.load_last_chunk(room_id).await.map_err(Into::into)
}
async fn load_previous_chunk(
&self,
room_id: &RoomId,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
self.0.load_previous_chunk(room_id, before_chunk_identifier).await.map_err(Into::into)
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
}
async fn filter_duplicated_events(
&self,
room_id: &RoomId,
events: Vec<OwnedEventId>,
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
self.0.filter_duplicated_events(room_id, events).await.map_err(Into::into)
}
async fn find_event(
&self,
room_id: &RoomId,
event_id: &EventId,
) -> Result<Option<Event>, Self::Error> {
self.0.find_event(room_id, event_id).await.map_err(Into::into)
}
async fn find_event_relations(
&self,
room_id: &RoomId,
event_id: &EventId,
filter: Option<&[RelationType]>,
) -> Result<Vec<Event>, Self::Error> {
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
}
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
self.0.save_event(room_id, event).await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
+35 -2
View File
@@ -10,7 +10,7 @@ use ruma::{
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::SyncRoomMessageEvent,
message::{MessageType, SyncRoomMessageEvent},
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
@@ -67,8 +67,13 @@ pub fn is_suitable_for_latest_event<'a>(
match event {
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
// Check if this is a replacement for another message. If it is, ignore it
if let Some(original_message) = message.as_original() {
// Don't show incoming verification requests
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
}
// Check if this is a replacement for another message. If it is, ignore it
let is_replacement =
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
@@ -589,6 +594,34 @@ mod tests {
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_verification_requests_are_unsuitable() {
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
KeyVerificationRequestEventContent::new(
"body".to_owned(),
vec![],
device_id!("device_id").to_owned(),
user_id!("@user_id:example.com").to_owned(),
),
)),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::NoUnsupportedMessageLikeType =
is_suitable_for_latest_event(&event, None)
);
}
#[test]
fn test_deserialize_latest_event() {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
+4 -3
View File
@@ -25,6 +25,7 @@ use serde::{Deserialize, Serialize};
pub use crate::error::{Error, Result};
mod client;
pub use client::RequestedRequiredStates;
pub mod debug;
pub mod deserialized_responses;
mod error;
@@ -55,9 +56,9 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate,
RoomMemberships, RoomState, RoomStateFilter,
apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember,
RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter,
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
+16 -17
View File
@@ -110,8 +110,8 @@
//! events ids in both sets. As a matter of fact, we have to manually handle
//! this edge case here. I hope that having an event database will help avoid
//! this kind of workaround here later.
//! - In addition to that, and as noted in the timeline code, it seems that the
//! sliding-sync proxy could return the same event multiple times in a sync
//! - In addition to that, and as noted in the timeline code, it seems that
//! sliding sync could return the same event multiple times in a sync
//! timeline, leading to incorrect results. We have to take that into account
//! by resetting the read counts *every* time we see an event that was the
//! target of the latest active read receipt.
@@ -245,10 +245,9 @@ impl RoomReadReceipts {
let mut counting_receipts = false;
for event in events {
// The sliding sync proxy sometimes sends the same event multiple times, so it
// can be at the beginning and end of a batch, for instance. In that
// case, just reset every time we see the event matching the
// receipt. NOTE: SS proxy workaround.
// Sliding sync sometimes sends the same event multiple times, so it can be at
// the beginning and end of a batch, for instance. In that case, just reset
// every time we see the event matching the receipt.
if let Some(event_id) = event.event_id() {
if event_id == receipt_event_id {
// Bingo! Switch over to the counting state, after resetting the
@@ -925,7 +924,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
.into_content();
let mut read_receipts = Default::default();
compute_unread_counts(
@@ -1007,7 +1006,7 @@ mod tests {
receipt_type_1.clone(),
receipt_thread_2.clone(),
)
.build();
.into_content();
// When I compute the notifications for this room (with no new events),
let mut read_receipts = RoomReadReceipts::default();
@@ -1070,7 +1069,7 @@ mod tests {
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
.into_content();
let mut read_receipts = RoomReadReceipts::default();
assert!(read_receipts.pending.is_empty());
@@ -1104,7 +1103,7 @@ mod tests {
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
.into_content();
// Sync with a read receipt *and* a single event that was already known: in that
// case, only consider the new events in isolation, and compute the
@@ -1372,7 +1371,7 @@ mod tests {
ReceiptType::Read,
ReceiptThread::Thread(owned_event_id!("$2")),
)
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
@@ -1391,7 +1390,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending[0], event_id!("$6"));
@@ -1409,7 +1408,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
@@ -1426,7 +1425,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
@@ -1443,7 +1442,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
@@ -1464,7 +1463,7 @@ mod tests {
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
.build();
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending.len(), 1);
@@ -1542,7 +1541,7 @@ mod tests {
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
.into_content();
let mut read_receipts = RoomReadReceipts::default();
@@ -26,36 +26,23 @@ use ruma::{
};
use tracing::{debug, instrument, trace, warn};
use crate::{store::Store, RoomInfo, StateChanges};
use super::super::Context;
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
/// Applies a function to an existing `RoomInfo` if present in changes, or one
/// loaded from the database.
fn map_info<F: FnOnce(&mut RoomInfo)>(
room_id: &RoomId,
changes: &mut StateChanges,
store: &Store,
f: F,
) {
if let Some(info) = changes.room_infos.get_mut(room_id) {
f(info);
} else if let Some(room) = store.room(room_id) {
let mut info = room.clone_info();
f(&mut info);
changes.add_room(info);
} else {
debug!(room = %room_id, "couldn't find room in state changes or store");
}
/// Create the [`Global`] account data processor.
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
Global::process(events)
}
#[must_use]
pub(crate) struct AccountDataProcessor {
pub struct Global {
parsed_events: Vec<AnyGlobalAccountDataEvent>,
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
}
impl AccountDataProcessor {
impl Global {
/// Creates a new processor for global account data.
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
let mut raw_by_type = BTreeMap::new();
let mut parsed_events = Vec::new();
@@ -87,23 +74,24 @@ impl AccountDataProcessor {
/// from the global account data and adds it to the room infos to
/// save.
#[instrument(skip_all)]
pub(crate) fn process_direct_rooms(
fn process_direct_rooms(
&self,
events: &[AnyGlobalAccountDataEvent],
store: &Store,
changes: &mut StateChanges,
state_store: &BaseStateStore,
state_changes: &mut StateChanges,
) {
for event in events {
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
for (user_identifier, rooms) in direct_event.content.iter() {
for room_id in rooms {
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
}
}
let rooms = store.rooms();
let rooms = state_store.rooms();
let mut old_dms = rooms
.iter()
.filter_map(|r| {
@@ -120,7 +108,7 @@ impl AccountDataProcessor {
}
}
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
map_info(room_id, changes, store, |info| {
map_info(room_id, state_changes, state_store, |info| {
info.base_info.dm_targets = new_direct_targets;
});
}
@@ -128,17 +116,17 @@ impl AccountDataProcessor {
// Remove the targets of old direct chats.
for room_id in old_dms.keys() {
trace!(?room_id, "Unmarking room as direct room");
map_info(room_id, changes, store, |info| {
map_info(room_id, state_changes, state_store, |info| {
info.base_info.dm_targets.clear();
});
}
}
}
/// Applies the processed data to the state changes.
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
/// Applies the processed data to the state changes and the state store.
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
// Fill in the content of `changes.account_data`.
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
// Process direct rooms.
let has_new_direct_room_data = self
@@ -147,16 +135,39 @@ impl AccountDataProcessor {
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
if has_new_direct_room_data {
self.process_direct_rooms(&self.parsed_events, store, changes);
self.process_direct_rooms(&self.parsed_events, state_store, &mut context.state_changes);
} else if let Ok(Some(direct_account_data)) =
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
state_store.get_account_data_event(GlobalAccountDataEventType::Direct).await
{
debug!("Found direct room data in the Store, applying it");
if let Ok(direct_account_data) = direct_account_data.deserialize() {
self.process_direct_rooms(&[direct_account_data], store, changes);
self.process_direct_rooms(
&[direct_account_data],
state_store,
&mut context.state_changes,
);
} else {
warn!("Failed to deserialize direct room account data");
}
}
}
}
/// Applies a function to an existing `RoomInfo` if present in changes, or one
/// loaded from the database.
fn map_info<F: FnOnce(&mut RoomInfo)>(
room_id: &RoomId,
changes: &mut StateChanges,
store: &BaseStateStore,
f: F,
) {
if let Some(info) = changes.room_infos.get_mut(room_id) {
f(info);
} else if let Some(room) = store.room(room_id) {
let mut info = room.clone_info();
f(&mut info);
changes.add_room(info);
} else {
debug!(room = %room_id, "couldn't find room in state changes or store");
}
}
@@ -0,0 +1,19 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod global;
mod room;
pub use global::{global, Global};
pub use room::for_room;
@@ -0,0 +1,143 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::{
events::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
serde::Raw,
RoomId,
};
use tracing::{instrument, warn};
use super::super::{Context, RoomInfoNotableUpdates};
use crate::{store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons, StateChanges};
#[instrument(skip_all, fields(?room_id))]
pub async fn for_room(
context: &mut Context,
room_id: &RoomId,
events: &[Raw<AnyRoomAccountDataEvent>],
state_store: &BaseStateStore,
) {
// Handle new events.
for raw_event in events {
match raw_event.deserialize() {
Ok(event) => {
context.state_changes.add_room_account_data(
room_id,
event.clone(),
raw_event.clone(),
);
match event {
AnyRoomAccountDataEvent::MarkedUnread(event) => {
on_room_info(
room_id,
&mut context.state_changes,
state_store,
|room_info| {
on_unread_marker(
room_id,
&event.content,
room_info,
&mut context.room_info_notable_updates,
);
},
);
}
AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => {
on_room_info(
room_id,
&mut context.state_changes,
state_store,
|room_info| {
on_unread_marker(
room_id,
&event.content.0,
room_info,
&mut context.room_info_notable_updates,
);
},
);
}
AnyRoomAccountDataEvent::Tag(event) => {
on_room_info(
room_id,
&mut context.state_changes,
state_store,
|room_info| {
room_info.base_info.handle_notable_tags(&event.content.tags);
},
);
}
// Nothing.
_ => {}
}
}
Err(err) => {
warn!("unable to deserialize account data event: {err}");
}
}
}
}
// Small helper to make the code easier to read.
//
// It finds the appropriate `RoomInfo`, allowing the caller to modify it, and
// save it in the correct place.
fn on_room_info<F>(
room_id: &RoomId,
state_changes: &mut StateChanges,
state_store: &BaseStateStore,
mut on_room_info: F,
) where
F: FnMut(&mut RoomInfo),
{
// `StateChanges` has the `RoomInfo`.
if let Some(room_info) = state_changes.room_infos.get_mut(room_id) {
// Show time.
on_room_info(room_info);
}
// The `BaseStateStore` has the `Room`, which has the `RoomInfo`.
else if let Some(room) = state_store.room(room_id) {
// Clone the `RoomInfo`.
let mut room_info = room.clone_info();
// Show time.
on_room_info(&mut room_info);
// Update the `RoomInfo` via `StateChanges`.
state_changes.add_room(room_info);
}
}
// Helper to update the unread marker for stable and unstable prefixes.
fn on_unread_marker(
room_id: &RoomId,
content: &MarkedUnreadEventContent,
room_info: &mut RoomInfo,
room_info_notable_updates: &mut RoomInfoNotableUpdates,
) {
if room_info.base_info.is_marked_unread != content.unread {
// Notify the room list about a manual read marker change if the
// value's changed.
room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
}
room_info.base_info.is_marked_unread = content.unread;
}
@@ -0,0 +1,105 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use eyeball::SharedObservable;
use ruma::{
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
serde::Raw,
};
use tracing::{error, instrument, trace};
use super::Context;
use crate::{
store::{BaseStateStore, StateStoreExt as _},
Result,
};
/// Save the [`StateChanges`] from the [`Context`] inside the
/// [`BaseStateStore`], and apply them on the in-memory rooms.
#[instrument(skip_all)]
pub async fn save_and_apply(
context: Context,
state_store: &BaseStateStore,
ignore_user_list_changes: &SharedObservable<Vec<String>>,
sync_token: Option<String>,
) -> Result<()> {
trace!("ready to submit changes to store");
let previous_ignored_user_list =
state_store.get_account_data_event_static().await.ok().flatten();
state_store.save_changes(&context.state_changes).await?;
if let Some(sync_token) = sync_token {
*state_store.sync_token.write().await = Some(sync_token);
}
apply_changes(context, state_store, ignore_user_list_changes, previous_ignored_user_list);
trace!("applied changes");
Ok(())
}
fn apply_changes(
context: Context,
state_store: &BaseStateStore,
ignore_user_list_changes: &SharedObservable<Vec<String>>,
previous_ignored_user_list: Option<Raw<IgnoredUserListEvent>>,
) {
let (state_changes, room_info_notable_updates) = context.into_parts();
if let Some(event) =
state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
{
match event.deserialize_as::<IgnoredUserListEvent>() {
Ok(event) => {
let user_ids: Vec<String> =
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
// Try to only trigger the observable if the ignored user list has changed,
// from the previous time we've seen it. If we couldn't load the previous event
// for any reason, always trigger.
if let Some(prev_user_ids) =
previous_ignored_user_list.and_then(|raw| raw.deserialize().ok()).map(|event| {
event
.content
.ignored_users
.keys()
.map(|id| id.to_string())
.collect::<Vec<_>>()
})
{
if user_ids != prev_user_ids {
ignore_user_list_changes.set(user_ids);
}
} else {
ignore_user_list_changes.set(user_ids);
}
}
Err(error) => {
error!("Failed to deserialize ignored user list event: {error}")
}
}
}
for (room_id, room_info) in &state_changes.room_infos {
if let Some(room) = state_store.room(room_id) {
let room_info_notable_update_reasons =
room_info_notable_updates.get(room_id).copied().unwrap_or_default();
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
}
}
}
@@ -0,0 +1,67 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
};
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::super::{verification, Context};
use crate::Result;
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
///
/// In the case of a decryption error, returns a [`TimelineEvent`]
/// representing the decryption error; in the case of problems with our
/// application, returns `Err`.
///
/// Returns `Ok(None)` if encryption is not configured.
pub async fn sync_timeline_event(
context: &mut Context,
olm_machine: Option<&OlmMachine>,
event: &Raw<AnySyncTimelineEvent>,
room_id: &RoomId,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Result<Option<TimelineEvent>> {
let Some(olm) = olm_machine else { return Ok(None) };
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: decryption_trust_requirement };
Ok(Some(
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
RoomEventDecryptionResult::Decrypted(decrypted) => {
let timeline_event = TimelineEvent::from(decrypted);
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
verification::process_if_relevant(
context,
&sync_timeline_event,
verification_is_allowed,
olm_machine,
room_id,
)
.await?;
}
timeline_event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
TimelineEvent::new_utd_event(event.clone(), utd_info)
}
},
))
}
@@ -0,0 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod decrypt;
pub mod to_device;
pub mod tracked_users;
@@ -0,0 +1,117 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeMap;
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
use ruma::{
api::client::sync::sync_events::{v3, v5, DeviceLists},
events::AnyToDeviceEvent,
serde::Raw,
OneTimeKeyAlgorithm, UInt,
};
use super::super::Context;
use crate::Result;
/// Process the to-device events and other related e2ee data based on a response
/// from a [MSC4186 request][`v5`].
///
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
pub async fn from_msc4186(
context: &mut Context,
to_device: Option<&v5::response::ToDevice>,
e2ee: &v5::response::E2EE,
olm_machine: Option<&OlmMachine>,
) -> Result<Output> {
process(
context,
olm_machine,
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
&e2ee.device_lists,
&e2ee.device_one_time_keys_count,
e2ee.device_unused_fallback_key_types.as_deref(),
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
)
.await
}
/// Process the to-device events and other related e2ee data based on a response
/// from a [`/v3/sync` request][`v3`].
///
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
pub async fn from_sync_v2(
context: &mut Context,
response: &v3::Response,
olm_machine: Option<&OlmMachine>,
) -> Result<Output> {
process(
context,
olm_machine,
response.to_device.events.clone(),
&response.device_lists,
&response.device_one_time_keys_count,
response.device_unused_fallback_key_types.as_deref(),
Some(response.next_batch.clone()),
)
.await
}
/// Process the to-device events and other related e2ee data.
///
/// This returns a list of all the to-device events that were passed in but
/// encrypted ones were replaced with their decrypted version.
async fn process(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
device_lists: &DeviceLists,
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
next_batch_token: Option<String>,
) -> Result<Output> {
let encryption_sync_changes = EncryptionSyncChanges {
to_device_events,
changed_devices: device_lists,
one_time_keys_counts,
unused_fallback_keys,
next_batch_token,
};
Ok(if let Some(olm_machine) = olm_machine {
// Let the crypto machine handle the sync response, this
// decrypts to-device events, but leaves room events alone.
// This makes sure that we have the decryption keys for the room
// events at hand.
let (events, room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
} else {
// If we have no `OlmMachine`, just return the events that were passed in.
// This should not happen unless we forget to set things up by calling
// `Self::activate()`.
Output {
decrypted_to_device_events: encryption_sync_changes.to_device_events,
room_key_updates: None,
}
})
}
pub struct Output {
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
}
@@ -0,0 +1,69 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::BTreeSet;
use matrix_sdk_crypto::OlmMachine;
use ruma::{OwnedUserId, RoomId};
use super::super::Context;
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
/// Update tracked users, if the room is encrypted.
pub async fn update(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
room_encryption_state: EncryptionState,
user_ids_to_track: &BTreeSet<OwnedUserId>,
) -> Result<()> {
if room_encryption_state.is_encrypted() {
if let Some(olm) = olm_machine {
if !user_ids_to_track.is_empty() {
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
}
}
}
Ok(())
}
/// Update tracked users, if the room is encrypted, or if the room has become
/// encrypted.
pub async fn update_or_set_if_room_is_newly_encrypted(
_context: &mut Context,
olm_machine: Option<&OlmMachine>,
user_ids_to_track: &BTreeSet<OwnedUserId>,
new_room_encryption_state: EncryptionState,
previous_room_encryption_state: EncryptionState,
room_id: &RoomId,
state_store: &BaseStateStore,
) -> Result<()> {
if new_room_encryption_state.is_encrypted() {
if let Some(olm) = olm_machine {
if !previous_room_encryption_state.is_encrypted() {
// The room turned on encryption in this sync, we need
// to also get all the existing users and mark them for
// tracking.
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
}
if !user_ids_to_track.is_empty() {
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
}
}
}
Ok(())
}
@@ -0,0 +1,229 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::{
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
};
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
use super::{verification, Context};
use crate::{
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
Result, Room,
};
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
/// [`Room`]s.
///
/// If we can decrypt them, change [`Room::latest_event`] to reflect what we
/// found, and remove any older encrypted events from
/// [`Room::latest_encrypted_events`].
pub async fn decrypt_from_rooms(
context: &mut Context,
rooms: Vec<Room>,
olm_machine: Option<&OlmMachine>,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Result<()> {
let Some(olm_machine) = olm_machine else {
return Ok(());
};
for room in rooms {
// Try to find a message we can decrypt and is suitable for using as the latest
// event. If we found one, set it as the latest and delete any older
// encrypted events
if let Some((found, found_index)) = find_suitable_and_decrypt(
context,
olm_machine,
&room,
&decryption_trust_requirement,
verification_is_allowed,
)
.await
{
room.on_latest_event_decrypted(
found,
found_index,
&mut context.state_changes,
&mut context.room_info_notable_updates,
);
}
}
Ok(())
}
async fn find_suitable_and_decrypt(
context: &mut Context,
olm_machine: &OlmMachine,
room: &Room,
decryption_trust_requirement: &TrustRequirement,
verification_is_allowed: bool,
) -> Option<(Box<LatestEvent>, usize)> {
let enc_events = room.latest_encrypted_events();
let power_levels = room.power_levels().await.ok();
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
// Walk backwards through the encrypted events, looking for one we can decrypt
for (i, event) in enc_events.iter().enumerate().rev() {
// Size of the `decrypt_sync_room_event` future should not impact this
// async fn since it is likely that there aren't even any encrypted
// events when calling it.
let decrypt_sync_room_event = Box::pin(decrypt_sync_room_event(
context,
olm_machine,
event,
room.room_id(),
decryption_trust_requirement,
verification_is_allowed,
));
if let Ok(decrypted) = decrypt_sync_room_event.await {
// We found an event we can decrypt
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
// We can deserialize it to find its type
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesCallNotify(_)
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
return Some((Box::new(LatestEvent::new(decrypted)), i));
}
_ => (),
}
}
}
}
None
}
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
///
/// In the case of a decryption error, returns a [`TimelineEvent`]
/// representing the decryption error; in the case of problems with our
/// application, returns `Err`.
///
/// Returns `Ok(None)` if encryption is not configured.
async fn decrypt_sync_room_event(
context: &mut Context,
olm_machine: &OlmMachine,
event: &Raw<AnySyncTimelineEvent>,
room_id: &RoomId,
decryption_trust_requirement: &TrustRequirement,
verification_is_allowed: bool,
) -> Result<TimelineEvent> {
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: *decryption_trust_requirement };
let event = match olm_machine
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
let event: TimelineEvent = decrypted.into();
if let Ok(sync_timeline_event) = event.raw().deserialize() {
verification::process_if_relevant(
context,
&sync_timeline_event,
verification_is_allowed,
Some(olm_machine),
room_id,
)
.await?;
}
event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
TimelineEvent::new_utd_event(event.clone(), utd_info)
}
};
Ok(event)
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
};
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
use super::{decrypt_from_rooms, Context};
use crate::{
rooms::normal::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client,
StateChanges,
};
#[async_test]
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
// Given a room
let user_id = user_id!("@u:u.to");
let room_id = room_id!("!r:u.to");
let client = logged_in_base_client(Some(user_id)).await;
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new()
.member(user_id)
.display_name("Alice")
.membership(MembershipState::Join)
.event_id(event_id!("$1")),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
let room = client.get_room(room_id).expect("Just-created room not found!");
// Sanity: it has no latest_encrypted_events or latest_event
assert!(room.latest_encrypted_events().is_empty());
assert!(room.latest_event().is_none());
// When I tell it to do some decryption
let mut context = Context::new(StateChanges::default(), Default::default());
decrypt_from_rooms(
&mut context,
vec![room.clone()],
client.olm_machine().await.as_ref(),
client.decryption_trust_requirement,
client.handle_verification_events,
)
.await
.unwrap();
// Then nothing changed
assert!(room.latest_encrypted_events().is_empty());
assert!(room.latest_event().is_none());
assert!(context.state_changes.room_infos.is_empty());
assert!(!context
.room_info_notable_updates
.get(room_id)
.copied()
.unwrap_or_default()
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
}
}
@@ -0,0 +1,54 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod account_data;
pub mod changes;
#[cfg(feature = "e2e-encryption")]
pub mod e2ee;
#[cfg(feature = "e2e-encryption")]
pub mod latest_event;
pub mod profiles;
pub mod state_events;
pub mod timeline;
#[cfg(feature = "e2e-encryption")]
pub mod verification;
use std::collections::BTreeMap;
use ruma::OwnedRoomId;
use crate::{RoomInfoNotableUpdateReasons, StateChanges};
type RoomInfoNotableUpdates = BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>;
#[cfg_attr(test, derive(Clone))]
pub(crate) struct Context {
pub(super) state_changes: StateChanges,
pub(super) room_info_notable_updates: RoomInfoNotableUpdates,
}
impl Context {
pub fn new(
state_changes: StateChanges,
room_info_notable_updates: RoomInfoNotableUpdates,
) -> Self {
Self { state_changes, room_info_notable_updates }
}
pub fn into_parts(self) -> (StateChanges, RoomInfoNotableUpdates) {
let Self { state_changes, room_info_notable_updates } = self;
(state_changes, room_info_notable_updates)
}
}
@@ -0,0 +1,58 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::{
events::{
room::member::{MembershipState, RoomMemberEventContent},
SyncStateEvent,
},
RoomId,
};
use super::Context;
/// Decide whether the profile must be created, updated or deleted based on the
/// [`RoomMemberEventContent`].
pub fn upsert_or_delete(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
) {
// Senders can fake the profile easily so we keep track of profiles that the
// member set themselves to avoid having confusing profile changes when a
// member gets kicked/banned.
if event.state_key() == event.sender() {
context
.state_changes
.profiles
.entry(room_id.to_owned())
.or_default()
.insert(event.sender().to_owned(), event.into());
}
if *event.membership() == MembershipState::Invite {
// Remove any profile previously stored for the invited user.
//
// A room member could have joined the room and left it later; in that case, the
// server may return a dummy, empty profile along the `leave` event. We
// don't want to reuse that empty profile when the member has been
// re-invited, so we remove it from the database.
context
.state_changes
.profiles_to_delete
.entry(room_id.to_owned())
.or_default()
.push(event.state_key().clone());
}
}
@@ -0,0 +1,145 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, BTreeSet},
iter,
};
use ruma::{
events::{room::member::MembershipState, AnySyncStateEvent},
serde::Raw,
OwnedUserId,
};
use serde::Deserialize;
use tracing::{instrument, warn};
use super::{profiles, Context};
use crate::{
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
RoomInfo,
};
/// Collect [`AnySyncStateEvent`].
pub mod sync {
use ruma::events::AnySyncTimelineEvent;
use super::{AnySyncStateEvent, Context, Raw};
/// Collect [`AnySyncStateEvent`] to [`AnySyncStateEvent`].
pub fn collect(
_context: &mut Context,
raw_events: &[Raw<AnySyncStateEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events)
}
/// Collect [`AnySyncTimelineEvent`] to [`AnySyncStateEvent`].
///
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
/// state events. The message-like events are filtered out.
pub fn collect_from_timeline(
_context: &mut Context,
raw_events: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
super::collect(raw_events.iter().filter_map(|raw_event| {
// Only state events have a `state_key` field.
match raw_event.get_field::<&str>("state_key") {
Ok(Some(_)) => Some(raw_event.cast_ref()),
_ => None,
}
}))
}
}
/// Collect [`AnyStrippedStateEvent`].
pub mod stripped {
use ruma::events::AnyStrippedStateEvent;
use super::{Context, Raw};
/// Collect [`AnyStrippedStateEvent`] to [`AnyStrippedStateEvent`].
pub fn collect(
_context: &mut Context,
raw_events: &[Raw<AnyStrippedStateEvent>],
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
super::collect(raw_events)
}
}
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
where
I: IntoIterator<Item = &'a Raw<T>>,
T: Deserialize<'a> + 'a,
{
raw_events
.into_iter()
.filter_map(|raw_event| match raw_event.deserialize() {
Ok(event) => Some((raw_event.clone(), event)),
Err(e) => {
warn!("Couldn't deserialize stripped state event: {e}");
None
}
})
.unzip()
}
/// Dispatch the state events and return the new users for this room.
///
/// `raw_events` and `events` must be generated from [`collect_sync`]. Events
/// must be exactly the same list of events that are in raw_events, but
/// deserialised. We demand them here to avoid deserialising multiple times.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn dispatch_and_get_new_users(
context: &mut Context,
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
) -> StoreResult<BTreeSet<OwnedUserId>> {
let mut user_ids = BTreeSet::new();
if raw_events.is_empty() {
return Ok(user_ids);
}
let mut state_events = BTreeMap::new();
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_state_event(event);
if let AnySyncStateEvent::RoomMember(member) = event {
ambiguity_cache
.handle_event(&context.state_changes, &room_info.room_id, member)
.await?;
match member.membership() {
MembershipState::Join | MembershipState::Invite => {
user_ids.insert(member.state_key().to_owned());
}
_ => (),
}
profiles::upsert_or_delete(context, &room_info.room_id, member);
}
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
}
context.state_changes.state.insert(room_info.room_id.clone(), state_events);
Ok(user_ids)
}
@@ -0,0 +1,360 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::SyncMessageLikeEvent;
use ruma::{
events::{
room::power_levels::{
RoomPowerLevelsEvent, RoomPowerLevelsEventContent, StrippedRoomPowerLevelsEvent,
},
AnyStrippedStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
StateEventType,
},
push::{Action, PushConditionRoomCtx},
RoomVersionId, UInt, UserId,
};
use tracing::{instrument, trace, warn};
use super::Context;
#[cfg(feature = "e2e-encryption")]
use super::{e2ee, verification};
use crate::{
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
store::{BaseStateStore, StateStoreExt as _},
sync::{Notification, Timeline},
Result, Room, RoomInfo,
};
/// Process a set of sync timeline event, and create a [`Timeline`].
///
/// For each event:
/// - will try to decrypt it,
/// - will process verification,
/// - will process redaction,
/// - will process notification.
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn build<'notification, 'e2ee>(
context: &mut Context,
room: &Room,
room_info: &mut RoomInfo,
timeline_inputs: builder::Timeline,
notification_inputs: builder::Notification<'notification>,
#[cfg(feature = "e2e-encryption")] e2ee: builder::E2EE<'e2ee>,
) -> Result<Timeline> {
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
let mut push_context =
get_push_room_context(context, room, room_info, notification_inputs.state_store).await?;
let room_id = room.room_id();
for raw_event in timeline_inputs.raw_events {
// Start by assuming we have a plaintext event. We'll replace it with a
// decrypted or UTD event below if necessary.
let mut timeline_event = TimelineEvent::new(raw_event);
// Do some special stuff on the `timeline_event` before collecting it.
match timeline_event.raw().deserialize() {
Ok(sync_timeline_event) => {
match &sync_timeline_event {
// State events are ignored. They must be processed separately.
AnySyncTimelineEvent::State(_) => {
// do nothing
}
// A room redaction.
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
redaction_event,
)) => {
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
if let Some(redacts) = redaction_event.redacts(room_version) {
room_info
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
context.state_changes.add_redaction(
room_id,
redacts,
timeline_event.raw().clone().cast(),
);
}
}
// Decrypt encrypted event, or process verification event.
#[cfg(feature = "e2e-encryption")]
AnySyncTimelineEvent::MessageLike(sync_message_like_event) => {
match sync_message_like_event {
AnySyncMessageLikeEvent::RoomEncrypted(
SyncMessageLikeEvent::Original(_),
) => {
if let Some(decrypted_timeline_event) =
Box::pin(e2ee::decrypt::sync_timeline_event(
context,
e2ee.olm_machine,
timeline_event.raw(),
room_id,
e2ee.decryption_trust_requirement,
e2ee.verification_is_allowed,
))
.await?
{
timeline_event = decrypted_timeline_event;
}
}
_ => {
Box::pin(verification::process_if_relevant(
context,
&sync_timeline_event,
e2ee.verification_is_allowed,
e2ee.olm_machine,
room_id,
))
.await?;
}
}
}
// Nothing particular to do.
#[cfg(not(feature = "e2e-encryption"))]
AnySyncTimelineEvent::MessageLike(_) => (),
}
if let Some(push_context) = &mut push_context {
update_push_room_context(context, push_context, room.own_user_id(), room_info)
} else {
push_context = get_push_room_context(
context,
room,
room_info,
notification_inputs.state_store,
)
.await?;
}
if let Some(context) = &push_context {
let actions =
notification_inputs.push_rules.get_actions(timeline_event.raw(), context);
if actions.iter().any(Action::should_notify) {
notification_inputs
.notifications
.entry(room_id.to_owned())
.or_default()
.push(Notification {
actions: actions.to_owned(),
event: RawAnySyncOrStrippedTimelineEvent::Sync(
timeline_event.raw().clone(),
),
});
}
timeline_event.push_actions = Some(actions.to_owned());
}
}
Err(error) => {
warn!("Error deserializing event: {error}");
}
}
// Finally, we have process the timeline event. We can collect it.
timeline.events.push(timeline_event);
}
Ok(timeline)
}
/// Set of types used by [`build`] to reduce the number of arguments by grouping
/// them by thematics.
pub mod builder {
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
use ruma::{
api::client::sync::sync_events::{v3, v5},
events::AnySyncTimelineEvent,
push::Ruleset,
serde::Raw,
OwnedRoomId,
};
use crate::{store::BaseStateStore, sync};
pub struct Timeline {
pub limited: bool,
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
pub prev_batch: Option<String>,
}
impl From<v3::Timeline> for Timeline {
fn from(value: v3::Timeline) -> Self {
Self { limited: value.limited, raw_events: value.events, prev_batch: value.prev_batch }
}
}
impl From<&v5::response::Room> for Timeline {
fn from(value: &v5::response::Room) -> Self {
Self {
limited: value.limited,
raw_events: value.timeline.clone(),
prev_batch: value.prev_batch.clone(),
}
}
}
pub struct Notification<'a> {
pub push_rules: &'a Ruleset,
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
pub state_store: &'a BaseStateStore,
}
impl<'a> Notification<'a> {
pub fn new(
push_rules: &'a Ruleset,
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
state_store: &'a BaseStateStore,
) -> Self {
Self { push_rules, notifications, state_store }
}
}
#[cfg(feature = "e2e-encryption")]
pub struct E2EE<'a> {
pub olm_machine: Option<&'a OlmMachine>,
pub decryption_trust_requirement: TrustRequirement,
pub verification_is_allowed: bool,
}
#[cfg(feature = "e2e-encryption")]
impl<'a> E2EE<'a> {
pub fn new(
olm_machine: Option<&'a OlmMachine>,
decryption_trust_requirement: TrustRequirement,
verification_is_allowed: bool,
) -> Self {
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
}
}
}
/// Update the push context for the given room.
///
/// Updates the context data from `context.state_changes` or `room_info`.
fn update_push_room_context(
context: &mut Context,
push_rules: &mut PushConditionRoomCtx,
user_id: &UserId,
room_info: &RoomInfo,
) {
let room_id = &*room_info.room_id;
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
// TODO: Use if let chain once stable
if let Some(AnySyncStateEvent::RoomMember(member)) =
context.state_changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
})
{
push_rules.user_display_name = member
.as_original()
.and_then(|ev| ev.content.displayname.clone())
.unwrap_or_else(|| user_id.localpart().to_owned())
}
if let Some(AnySyncStateEvent::RoomPowerLevels(event)) =
context.state_changes.state.get(room_id).and_then(|types| {
types.get(&StateEventType::RoomPowerLevels)?.get("")?.deserialize().ok()
})
{
push_rules.power_levels = Some(event.power_levels().into());
}
}
/// Get the push context for the given room.
///
/// Tries to get the data from `changes` or the up to date `room_info`.
/// Loads the data from the store otherwise.
///
/// Returns `None` if some data couldn't be found. This should only happen
/// in brand new rooms, while we process its state.
pub async fn get_push_room_context(
context: &mut Context,
room: &Room,
room_info: &RoomInfo,
state_store: &BaseStateStore,
) -> Result<Option<PushConditionRoomCtx>> {
let room_id = room.room_id();
let user_id = room.own_user_id();
let member_count = room_info.active_members_count();
// TODO: Use if let chain once stable
let user_display_name = if let Some(AnySyncStateEvent::RoomMember(member)) =
context.state_changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
}) {
member
.as_original()
.and_then(|ev| ev.content.displayname.clone())
.unwrap_or_else(|| user_id.localpart().to_owned())
} else if let Some(AnyStrippedStateEvent::RoomMember(member)) =
context.state_changes.stripped_state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
})
{
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
member.name().to_owned()
} else {
trace!("Couldn't get push context because of missing own member information");
return Ok(None);
};
let power_levels = if let Some(event) =
context.state_changes.state.get(room_id).and_then(|types| {
types
.get(&StateEventType::RoomPowerLevels)?
.get("")?
.deserialize_as::<RoomPowerLevelsEvent>()
.ok()
}) {
Some(event.power_levels().into())
} else if let Some(event) =
context.state_changes.stripped_state.get(room_id).and_then(|types| {
types
.get(&StateEventType::RoomPowerLevels)?
.get("")?
.deserialize_as::<StrippedRoomPowerLevelsEvent>()
.ok()
})
{
Some(event.power_levels().into())
} else {
state_store
.get_state_event_static::<RoomPowerLevelsEventContent>(room_id)
.await?
.and_then(|e| e.deserialize().ok())
.map(|event| event.power_levels().into())
};
Ok(Some(PushConditionRoomCtx {
user_id: user_id.to_owned(),
room_id: room_id.to_owned(),
member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
user_display_name,
power_levels,
}))
}
@@ -0,0 +1,84 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_crypto::OlmMachine;
use ruma::{
events::{
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
SyncMessageLikeEvent,
},
RoomId,
};
use super::Context;
use crate::Result;
/// Process the given event as a verification event if it is a candidate. The
/// event must be decrypted.
pub async fn process_if_relevant(
context: &mut Context,
event: &AnySyncTimelineEvent,
verification_is_allowed: bool,
olm_machine: Option<&OlmMachine>,
room_id: &RoomId,
) -> Result<()> {
if let AnySyncTimelineEvent::MessageLike(event) = event {
// That's it, we are good, the event has been decrypted successfully.
// However, let's run an additional action. Check if this is a verification
// event (`m.key.verification.*`), and call `verification` accordingly.
if match &event {
// This is an original (i.e. non-redacted) `m.room.message` event and its
// content is a verification request…
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
original_event,
)) => {
matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_))
}
// … or this is verification request event
AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
_ => false,
} {
verification(context, verification_is_allowed, olm_machine, event, room_id).await?;
}
}
Ok(())
}
async fn verification(
_context: &mut Context,
verification_is_allowed: bool,
olm_machine: Option<&OlmMachine>,
event: &AnySyncMessageLikeEvent,
room_id: &RoomId,
) -> Result<()> {
if !verification_is_allowed {
return Ok(());
}
if let Some(olm) = olm_machine {
olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?;
}
Ok(())
}
+2 -2
View File
@@ -12,8 +12,8 @@ use std::{
use bitflags::bitflags;
pub use members::RoomMember;
pub use normal::{
apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomMembersUpdate, RoomState, RoomStateFilter,
apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter,
};
use regex::Regex;
use ruma::{
+175 -68
View File
@@ -272,10 +272,8 @@ pub enum RoomMembersUpdate {
impl Room {
/// The size of the latest_encrypted_events RingBuffer
// SAFETY: `new_unchecked` is safe because 10 is not zero.
#[cfg(feature = "e2e-encryption")]
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize = std::num::NonZeroUsize::new(10).unwrap();
pub(crate) fn new(
own_user_id: &UserId,
@@ -428,16 +426,6 @@ impl Room {
self.inner.read().sync_info != SyncInfo::NoState
}
/// Check if the room has its encryption event synced.
///
/// The encryption event can be missing when the room hasn't appeared in
/// sync yet.
///
/// Returns true if the encryption state is synced, false otherwise.
pub fn is_encryption_state_synced(&self) -> bool {
self.inner.read().encryption_state_synced
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
@@ -533,9 +521,9 @@ impl Room {
self.inner.read().base_info.dm_targets.len()
}
/// Is the room encrypted.
pub fn is_encrypted(&self) -> bool {
self.inner.read().is_encrypted()
/// Get the encryption state of this room.
pub fn encryption_state(&self) -> EncryptionState {
self.inner.read().encryption_state()
}
/// Get the `m.room.encryption` content that enabled end to end encryption
@@ -1578,9 +1566,15 @@ impl RoomInfo {
self.room_state
}
/// Returns whether this is an encrypted room.
pub fn is_encrypted(&self) -> bool {
self.base_info.encryption.is_some()
/// Returns the encryption state of this room.
pub fn encryption_state(&self) -> EncryptionState {
if !self.encryption_state_synced {
EncryptionState::Unknown
} else if self.base_info.encryption.is_some() {
EncryptionState::Encrypted
} else {
EncryptionState::NotEncrypted
}
}
/// Set the encryption event content in this room.
@@ -1588,22 +1582,41 @@ impl RoomInfo {
self.base_info.encryption = event;
}
/// Handle the encryption state.
pub fn handle_encryption_state(
&mut self,
requested_required_states: &[(StateEventType, String)],
) {
if requested_required_states
.iter()
.any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
{
// The `m.room.encryption` event was requested during the sync. Whether we have
// received a `m.room.encryption` event in return doesn't matter: we must mark
// the encryption state as synced; if the event is present, it means the room
// _is_ encrypted, otherwise it means the room _is not_ encrypted.
self.mark_encryption_state_synced();
}
}
/// Handle the given state event.
///
/// Returns true if the event modified the info, false otherwise.
pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
let ret = self.base_info.handle_state_event(event);
// Store the state event in the `BaseRoomInfo` first.
let base_info_has_been_modified = self.base_info.handle_state_event(event);
// If we received an `m.room.encryption` event here, and encryption got enabled,
// then we can be certain that we have synced the encryption state event, so
// mark it here as synced.
if let AnySyncStateEvent::RoomEncryption(_) = event {
if self.is_encrypted() {
self.mark_encryption_state_synced();
}
// The `m.room.encryption` event was or wasn't explicitly requested, we don't
// know here (see `Self::handle_encryption_state`) but we got one in
// return! In this case, we can deduce the room _is_ encrypted, but we cannot
// know if it _is not_ encrypted.
self.mark_encryption_state_synced();
}
ret
base_info_has_been_modified
}
/// Handle the given stripped state event.
@@ -2153,6 +2166,33 @@ fn compute_display_name_from_heroes(
}
}
/// Represents the state of a room encryption.
#[derive(Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum EncryptionState {
/// The room is encrypted.
Encrypted,
/// The room is not encrypted.
NotEncrypted,
/// The state of the room encryption is unknown, probably because the
/// `/sync` did not provide all data needed to decide.
Unknown,
}
impl EncryptionState {
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
pub fn is_encrypted(&self) -> bool {
matches!(self, Self::Encrypted)
}
/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
#[cfg(test)]
mod tests {
use std::{
@@ -2163,6 +2203,7 @@ mod tests {
time::Duration,
};
use assert_matches::assert_matches;
use assign::assign;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_test::{
@@ -2199,11 +2240,17 @@ mod tests {
use similar_asserts::assert_eq;
use stream_assert::{assert_pending, assert_ready};
use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo};
use super::{
compute_display_name_from_heroes, EncryptionState, Room, RoomHero, RoomInfo, RoomState,
SyncInfo,
};
use crate::{
latest_event::LatestEvent,
response_processors as processors,
rooms::RoomNotableTags,
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
store::{
IntoStateStore, MemoryStore, RoomLoadSettings, StateChanges, StateStore, StoreConfig,
},
test_utils::logged_in_base_client,
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta,
@@ -2490,16 +2537,16 @@ mod tests {
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
client
.set_session_meta(
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
@@ -2532,11 +2579,19 @@ mod tests {
.cast();
// When the new tag is handled and applied.
let mut changes = StateChanges::default();
client
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
let mut context = processors::Context::new(StateChanges::default(), Default::default());
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
client.apply_changes(&changes, Default::default());
processors::changes::save_and_apply(
context.clone(),
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
@@ -2554,10 +2609,18 @@ mod tests {
}))
.unwrap()
.cast();
client
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
client.apply_changes(&changes, Default::default());
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
@@ -2570,16 +2633,16 @@ mod tests {
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
client
.set_session_meta(
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
@@ -2612,11 +2675,19 @@ mod tests {
.cast();
// When the new tag is handled and applied.
let mut changes = StateChanges::default();
client
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
let mut context = processors::Context::new(StateChanges::default(), Default::default());
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
client.apply_changes(&changes, Default::default());
processors::changes::save_and_apply(
context.clone(),
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
@@ -2634,10 +2705,18 @@ mod tests {
}))
.unwrap()
.cast();
client
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
.await;
client.apply_changes(&changes, Default::default());
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
@@ -3148,23 +3227,21 @@ mod tests {
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
// Given a room,
let client = BaseClient::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let client =
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
client
.set_session_meta(
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
None,
)
.await
@@ -3184,22 +3261,29 @@ mod tests {
// And I provide a decrypted event to replace the encrypted one,
let event = make_latest_event("$A");
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
let mut context = processors::Context::new(StateChanges::default(), Default::default());
room.on_latest_event_decrypted(
event.clone(),
0,
&mut changes,
&mut room_info_notable_updates,
&mut context.state_changes,
&mut context.room_info_notable_updates,
);
assert!(room_info_notable_updates.contains_key(room_id));
assert!(context.room_info_notable_updates.contains_key(room_id));
// The subscriber isn't notified at this point.
assert!(room_info_notable_update.try_recv().is_err());
// Then updating the room info will store the event,
client.apply_changes(&changes, room_info_notable_updates);
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
// And wake up the subscriber.
@@ -3569,11 +3653,10 @@ mod tests {
}
#[test]
fn test_encryption_is_set_when_encryption_event_is_received() {
fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
let (_store, room) = make_room_test_helper(RoomState::Joined);
assert!(room.is_encryption_state_synced().not());
assert!(room.is_encrypted().not());
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
let encryption_content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
@@ -3591,8 +3674,32 @@ mod tests {
));
receive_state_events(&room, vec![&encryption_event]);
assert!(room.is_encryption_state_synced());
assert!(room.is_encrypted());
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
}
#[test]
fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
let (_store, room) = make_room_test_helper(RoomState::Joined);
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
room.inner.update_if(|info| {
info.mark_encryption_state_synced();
false
});
assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
}
#[test]
fn test_encryption_state() {
assert!(EncryptionState::Unknown.is_unknown());
assert!(EncryptionState::Encrypted.is_unknown().not());
assert!(EncryptionState::NotEncrypted.is_unknown().not());
assert!(EncryptionState::Unknown.is_encrypted().not());
assert!(EncryptionState::Encrypted.is_encrypted());
assert!(EncryptionState::NotEncrypted.is_encrypted().not());
}
#[async_test]
@@ -1,49 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! HTTP types for MSC4186 or MSC3585.
//!
//! This module provides unified namings for types from MSC3575 and
//! MSC4186.
/// HTTP types from MSC3575, renamed to match the MSC4186 namings.
pub mod msc3575 {
use ruma::api::client::sync::sync_events::v4;
pub use v4::{Request, Response};
/// HTTP types related to a `Request`.
pub mod request {
pub use super::v4::{
AccountDataConfig as AccountData, ExtensionsConfig as Extensions,
ReceiptsConfig as Receipts, RoomDetailsConfig as RoomDetails, RoomSubscription,
SyncRequestList as List, SyncRequestListFilters as ListFilters,
ToDeviceConfig as ToDevice, TypingConfig as Typing,
};
}
/// HTTP types related to a `Response`.
pub mod response {
pub use super::v4::{
AccountData, Extensions, Receipts, SlidingSyncRoom as Room,
SlidingSyncRoomHero as RoomHero, SyncList as List, ToDevice, Typing,
};
}
}
/// HTTP types from MSC4186.
pub mod msc4186 {
pub use ruma::api::client::sync::sync_events::v5::*;
}
pub use msc4186::*;
@@ -86,7 +86,7 @@ pub(crate) struct AmbiguityCache {
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
#[instrument(ret)]
#[instrument(ret(level = "trace"))]
pub(crate) fn is_display_name_ambiguous(
display_name: &DisplayName,
users_with_display_name: &BTreeSet<OwnedUserId>,
@@ -6,7 +6,7 @@ use assert_matches::assert_matches;
use assert_matches2::assert_let;
use async_trait::async_trait;
use growable_bloom_filter::GrowableBloomBuilder;
use matrix_sdk_test::test_json;
use matrix_sdk_test::{event_factory::EventFactory, test_json};
use ruma::{
api::MatrixVersion,
event_id,
@@ -22,10 +22,9 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
},
AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, AnyMessageLikeEventContent,
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent,
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
SyncStateEvent,
AnyGlobalAccountDataEvent, AnyMessageLikeEventContent, AnyRoomAccountDataEvent,
AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType, SyncStateEvent,
},
owned_event_id, owned_mxc_uri, room_id,
serde::Raw,
@@ -36,7 +35,7 @@ use serde_json::{json, value::Value as JsonValue};
use super::{
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
ServerCapabilities,
RoomLoadSettings, ServerCapabilities,
};
use crate::{
deserialized_responses::MemberEvent,
@@ -47,7 +46,7 @@ use crate::{
/// `StateStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`statestore_integration_tests!`] macro.
/// `statestore_integration_tests!` macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStoreIntegrationTests {
@@ -95,6 +94,8 @@ pub trait StateStoreIntegrationTests {
async fn test_update_send_queue_dependent(&self);
/// Test saving/restoring server capabilities.
async fn test_server_capabilities_saving(&self);
/// Test fetching room infos based on [`RoomLoadSettings`].
async fn test_get_room_infos(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -174,13 +175,11 @@ impl StateStoreIntegrationTests for DynStateStore {
let invited_member_state_event = invited_member_state_raw.deserialize().unwrap();
changes.add_state_event(room_id, invited_member_state_event, invited_member_state_raw);
let receipt_json: &JsonValue = &test_json::READ_RECEIPT;
let receipt_event =
serde_json::from_value::<AnySyncEphemeralRoomEvent>(receipt_json.clone()).unwrap();
let receipt_content = match receipt_event.content() {
AnyEphemeralRoomEventContent::Receipt(content) => content,
_ => panic!(),
};
let f = EventFactory::new().room(room_id);
let receipt_content = f
.read_receipts()
.add(event_id!("$example"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
changes.add_receipts(room_id, receipt_content);
changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map);
@@ -268,7 +267,11 @@ impl StateStoreIntegrationTests for DynStateStore {
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
assert!(self.get_presence_event(user_id).await?.is_some());
assert_eq!(self.get_room_infos().await?.len(), 2, "Expected to find 2 room infos");
assert_eq!(
self.get_room_infos(&RoomLoadSettings::default()).await?.len(),
2,
"Expected to find 2 room infos"
);
assert!(self
.get_account_data_event(GlobalAccountDataEventType::PushRules)
.await?
@@ -930,7 +933,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let user_id = user_id();
assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none());
assert_eq!(self.get_room_infos().await.unwrap().len(), 0);
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 0);
let mut changes = StateChanges::default();
changes
@@ -946,7 +949,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let member_event =
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
assert!(matches!(member_event, MemberEvent::Sync(_)));
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 1);
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
assert_eq!(members, vec![user_id.to_owned()]);
@@ -959,7 +962,7 @@ impl StateStoreIntegrationTests for DynStateStore {
let member_event =
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
assert!(matches!(member_event, MemberEvent::Stripped(_)));
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 1);
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
assert_eq!(members, vec![user_id.to_owned()]);
@@ -1003,7 +1006,11 @@ impl StateStoreIntegrationTests for DynStateStore {
self.remove_room(room_id).await?;
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
assert_eq!(
self.get_room_infos(&RoomLoadSettings::default()).await?.len(),
1,
"room is still there"
);
assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
assert!(
@@ -1057,7 +1064,10 @@ impl StateStoreIntegrationTests for DynStateStore {
self.remove_room(stripped_room_id).await?;
assert!(self.get_room_infos().await?.is_empty(), "still room info found");
assert!(
self.get_room_infos(&RoomLoadSettings::default()).await?.is_empty(),
"still room info found"
);
Ok(())
}
@@ -1683,6 +1693,54 @@ impl StateStoreIntegrationTests for DynStateStore {
}
);
}
async fn test_get_room_infos(&self) {
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
// There is no room for the moment.
{
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 0);
}
// Save rooms.
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
self.save_changes(&changes).await.unwrap();
// We can find all the rooms with `RoomLoadSettings::All`.
{
let mut all_rooms = self.get_room_infos(&RoomLoadSettings::All).await.unwrap();
// (We need to sort by `room_id` so that the test is stable across all
// `StateStore` implementations).
all_rooms.sort_by(|a, b| a.room_id.cmp(&b.room_id));
assert_eq!(all_rooms.len(), 2);
assert_eq!(all_rooms[0].room_id, room_id_0);
assert_eq!(all_rooms[1].room_id, room_id_1);
}
// We can find a single room with `RoomLoadSettings::One`.
{
let all_rooms =
self.get_room_infos(&RoomLoadSettings::One(room_id_1.to_owned())).await.unwrap();
assert_eq!(all_rooms.len(), 1);
assert_eq!(all_rooms[0].room_id, room_id_1);
}
// `RoomLoadSetting::One` can result in loading zero room if the room is
// unknown.
{
let all_rooms =
self.get_room_infos(&RoomLoadSettings::One(room_id_2.to_owned())).await.unwrap();
assert_eq!(all_rooms.len(), 0);
}
}
}
/// Macro building to allow your StateStore implementation to run the entire
@@ -1847,6 +1905,12 @@ macro_rules! statestore_integration_tests {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_update_send_queue_dependent().await;
}
#[async_test]
async fn test_get_room_infos() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_get_room_infos().await;
}
}
};
}
@@ -39,7 +39,7 @@ use super::{
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
traits::{ComposerDraft, ServerCapabilities},
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
StateChanges, StateStore, StoreError,
RoomLoadSettings, StateChanges, StateStore, StoreError,
};
use crate::{
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
@@ -635,8 +635,18 @@ impl StateStore for MemoryStore {
Ok(get_user_ids_inner(&inner.members, room_id, memberships))
}
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
Ok(self.inner.read().unwrap().room_info.values().cloned().collect())
async fn get_room_infos(&self, room_load_settings: &RoomLoadSettings) -> Result<Vec<RoomInfo>> {
let memory_store_inner = self.inner.read().unwrap();
let room_infos = &memory_store_inner.room_info;
Ok(match room_load_settings {
RoomLoadSettings::All => room_infos.values().cloned().collect(),
RoomLoadSettings::One(room_id) => match room_infos.get(room_id) {
Some(room_info) => vec![room_info.clone()],
None => vec![],
},
})
}
async fn get_users_with_display_name(
+311 -39
View File
@@ -146,9 +146,10 @@ pub type Result<T, E = StoreError> = std::result::Result<T, E>;
/// This adds additional higher level store functionality on top of a
/// `StateStore` implementation.
#[derive(Clone)]
pub(crate) struct Store {
pub(crate) struct BaseStateStore {
pub(super) inner: Arc<DynStateStore>,
session_meta: Arc<OnceCell<SessionMeta>>,
room_load_settings: Arc<RwLock<RoomLoadSettings>>,
/// The current sync token that should be used for the next sync call.
pub(super) sync_token: Arc<RwLock<Option<String>>>,
/// All rooms the store knows about.
@@ -158,12 +159,13 @@ pub(crate) struct Store {
sync_lock: Arc<Mutex<()>>,
}
impl Store {
impl BaseStateStore {
/// Create a new store, wrapping the given `StateStore`
pub fn new(inner: Arc<DynStateStore>) -> Self {
Self {
inner,
session_meta: Default::default(),
room_load_settings: Default::default(),
sync_token: Default::default(),
rooms: Arc::new(StdRwLock::new(ObservableMap::new())),
sync_lock: Default::default(),
@@ -175,11 +177,51 @@ impl Store {
&self.sync_lock
}
/// Load the room infos from the inner `StateStore`.
/// Set the [`SessionMeta`] into [`BaseStateStore::session_meta`].
///
/// Applies migrations to the room infos if needed.
async fn load_room_infos(&self) -> Result<Vec<RoomInfo>> {
let mut room_infos = self.inner.get_room_infos().await?;
/// # Panics
///
/// Panics if called twice.
pub(crate) fn set_session_meta(&self, session_meta: SessionMeta) {
self.session_meta.set(session_meta).expect("`SessionMeta` was already set");
}
/// Loads rooms from the given [`DynStateStore`] (in
/// [`BaseStateStore::new`]) into [`BaseStateStore::rooms`].
pub(crate) async fn load_rooms(
&self,
user_id: &UserId,
room_load_settings: RoomLoadSettings,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
*self.room_load_settings.write().await = room_load_settings.clone();
let room_infos = self.load_and_migrate_room_infos(room_load_settings).await?;
let mut rooms = self.rooms.write().unwrap();
for room_info in room_infos {
let new_room = Room::restore(
user_id,
self.inner.clone(),
room_info,
room_info_notable_update_sender.clone(),
);
let new_room_id = new_room.room_id().to_owned();
rooms.insert(new_room_id, new_room);
}
Ok(())
}
/// Load room infos from the [`StateStore`] and applies migrations onto
/// them.
async fn load_and_migrate_room_infos(
&self,
room_load_settings: RoomLoadSettings,
) -> Result<Vec<RoomInfo>> {
let mut room_infos = self.inner.get_room_infos(&room_load_settings).await?;
let mut migrated_room_infos = Vec::with_capacity(room_infos.len());
for room_info in room_infos.iter_mut() {
@@ -205,40 +247,34 @@ impl Store {
Ok(room_infos)
}
/// Set the meta of the session.
///
/// Restores the state of this `Store` from the given `SessionMeta` and the
/// inner `StateStore`.
///
/// This method panics if it is called twice.
pub async fn set_session_meta(
&self,
session_meta: SessionMeta,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
{
let room_infos = self.load_room_infos().await?;
let mut rooms = self.rooms.write().unwrap();
for room_info in room_infos {
let new_room = Room::restore(
&session_meta.user_id,
self.inner.clone(),
room_info,
room_info_notable_update_sender.clone(),
);
let new_room_id = new_room.room_id().to_owned();
rooms.insert(new_room_id, new_room);
}
}
/// Load sync token from the [`StateStore`], and put it in
/// [`BaseStateStore::sync_token`].
pub(crate) async fn load_sync_token(&self) -> Result<()> {
let token =
self.get_kv_data(StateStoreDataKey::SyncToken).await?.and_then(|s| s.into_sync_token());
*self.sync_token.write().await = token;
self.session_meta.set(session_meta).expect("Session Meta was already set");
Ok(())
}
/// Restore the session meta, sync token and rooms from an existing
/// [`BaseStateStore`].
#[cfg(any(feature = "e2e-encryption", test))]
pub(crate) async fn derive_from_other(
&self,
other: &Self,
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
) -> Result<()> {
let Some(session_meta) = other.session_meta.get() else {
return Ok(());
};
let room_load_settings = other.room_load_settings.read().await.clone();
self.load_rooms(&session_meta.user_id, room_load_settings, room_info_notable_update_sender)
.await?;
self.load_sync_token().await?;
self.set_session_meta(session_meta.clone());
Ok(())
}
@@ -319,7 +355,7 @@ impl Store {
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Store {
impl fmt::Debug for BaseStateStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Store")
.field("inner", &self.inner)
@@ -330,7 +366,7 @@ impl fmt::Debug for Store {
}
}
impl Deref for Store {
impl Deref for BaseStateStore {
type Target = DynStateStore;
fn deref(&self) -> &Self::Target {
@@ -338,6 +374,57 @@ impl Deref for Store {
}
}
/// Configure how many rooms will be restored when restoring the session with
/// `BaseStateStore::load_rooms`.
///
/// <div class="warning">
///
/// # ⚠️ Be careful!
///
/// When loading a single room with [`RoomLoadSettings::One`], the in-memory
/// state may not reflect the store state (in the databases). Thus, when one
/// will get a room that exists in the store state but _not_ in the in-memory
/// state, it will be created from scratch and, when saved, will override the
/// data in the store state (in the databases). This can lead to weird
/// behaviours.
///
/// This option is expected to be used as follows:
///
/// 1. Create a `BaseStateStore` with a [`StateStore`] based on SQLite for
/// example,
/// 2. Restore a session and load one room from the [`StateStore`] (in the case
/// of dealing with a notification for example),
/// 3. Derive the `BaseStateStore`, with `BaseStateStore::derive_from_other`,
/// into another one with an in-memory [`StateStore`], such as
/// [`MemoryStore`],
/// 4. Work on this derived `BaseStateStore`.
///
/// Now, all operations happen in the [`MemoryStore`], not on the original store
/// (SQLite in this example), thus protecting original data.
///
/// From a higher-level point of view, this is what
/// [`BaseClient::clone_with_in_memory_state_store`] does.
///
/// </div>
///
/// [`BaseClient::clone_with_in_memory_state_store`]: crate::BaseClient::clone_with_in_memory_state_store
#[derive(Clone, Debug, Default)]
pub enum RoomLoadSettings {
/// Load all rooms from the [`StateStore`] into the in-memory state store
/// `BaseStateStore`.
///
/// This is the default variant.
#[default]
All,
/// Load a single room from the [`StateStore`] into the in-memory state
/// store `BaseStateStore`.
///
/// Please, be careful with this option. Read the documentation of
/// [`RoomLoadSettings`].
One(OwnedRoomId),
}
/// Store state changes and pass them to the StateStore.
#[derive(Clone, Debug, Default)]
pub struct StateChanges {
@@ -479,7 +566,7 @@ impl StateChanges {
///
/// ```
/// # use matrix_sdk_base::store::StoreConfig;
///
/// #
/// let store_config =
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
/// ```
@@ -545,3 +632,188 @@ impl StoreConfig {
self
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use ruma::{owned_device_id, owned_user_id, room_id, user_id};
use tokio::sync::broadcast;
use super::{BaseStateStore, MemoryStore, RoomLoadSettings};
use crate::{RoomInfo, RoomState, SessionMeta, StateChanges};
#[async_test]
async fn test_set_session_meta() {
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
assert!(store.session_meta.get().is_none());
store.set_session_meta(session_meta.clone());
assert_eq!(store.session_meta.get(), Some(&session_meta));
}
#[async_test]
#[should_panic]
async fn test_set_session_meta_twice() {
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
store.set_session_meta(session_meta.clone());
// Kaboom.
store.set_session_meta(session_meta);
}
#[async_test]
async fn test_derive_from_other() {
// The first store.
let other = BaseStateStore::new(Arc::new(MemoryStore::new()));
let session_meta = SessionMeta {
user_id: owned_user_id!("@mnt_io:matrix.org"),
device_id: owned_device_id!("HELLOYOU"),
};
let (room_info_notable_update_sender, _) = broadcast::channel(1);
let room_id_0 = room_id!("!r0");
other
.load_rooms(
&session_meta.user_id,
RoomLoadSettings::One(room_id_0.to_owned()),
&room_info_notable_update_sender,
)
.await
.unwrap();
other.set_session_meta(session_meta.clone());
// Derive another store.
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
store.derive_from_other(&other, &room_info_notable_update_sender).await.unwrap();
// `SessionMeta` is derived.
assert_eq!(store.session_meta.get(), Some(&session_meta));
// `RoomLoadSettings` is derived.
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::One(ref room_id) => {
assert_eq!(room_id, room_id_0);
});
}
#[test]
fn test_room_load_settings_default() {
assert_matches!(RoomLoadSettings::default(), RoomLoadSettings::All);
}
#[async_test]
async fn test_load_all_rooms() {
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let user_id = user_id!("@mnt_io:matrix.org");
let memory_state_store = Arc::new(MemoryStore::new());
// Initial state.
{
let store = BaseStateStore::new(memory_state_store.clone());
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
store.inner.save_changes(&changes).await.unwrap();
}
// Check a `BaseStateStore` is able to load all rooms.
{
let store = BaseStateStore::new(memory_state_store.clone());
let (room_info_notable_update_sender, _) = broadcast::channel(2);
// Default value.
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
// Load rooms.
store
.load_rooms(user_id, RoomLoadSettings::All, &room_info_notable_update_sender)
.await
.unwrap();
// Check the last room load settings.
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
// Check the loaded rooms.
let mut rooms = store.rooms();
rooms.sort_by(|a, b| a.room_id().cmp(b.room_id()));
assert_eq!(rooms.len(), 2);
assert_eq!(rooms[0].room_id(), room_id_0);
assert_eq!(rooms[0].own_user_id(), user_id);
assert_eq!(rooms[1].room_id(), room_id_1);
assert_eq!(rooms[1].own_user_id(), user_id);
}
}
#[async_test]
async fn test_load_one_room() {
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let user_id = user_id!("@mnt_io:matrix.org");
let memory_state_store = Arc::new(MemoryStore::new());
// Initial state.
{
let store = BaseStateStore::new(memory_state_store.clone());
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
store.inner.save_changes(&changes).await.unwrap();
}
// Check a `BaseStateStore` is able to load one room.
{
let store = BaseStateStore::new(memory_state_store.clone());
let (room_info_notable_update_sender, _) = broadcast::channel(2);
// Default value.
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
// Load rooms.
store
.load_rooms(
user_id,
RoomLoadSettings::One(room_id_1.to_owned()),
&room_info_notable_update_sender,
)
.await
.unwrap();
// Check the last room load settings.
assert_matches!(
*store.room_load_settings.read().await,
RoomLoadSettings::One(ref room_id) => {
assert_eq!(room_id, room_id_1);
}
);
// Check the loaded rooms.
let rooms = store.rooms();
assert_eq!(rooms.len(), 1);
assert_eq!(rooms[0].room_id(), room_id_1);
assert_eq!(rooms[0].own_user_id(), user_id);
}
}
}
@@ -236,7 +236,9 @@ pub enum DependentQueuedRequestKind {
/// the final media event with the remote MXC URIs.
FinishUpload {
/// Local echo for the event (containing the local MXC URIs).
local_echo: RoomMessageEventContent,
///
/// `Box` the local echo so that it reduces the size of the whole enum.
local_echo: Box<RoomMessageEventContent>,
/// Transaction id for the file upload.
file_upload: OwnedTransactionId,
+12 -6
View File
@@ -42,8 +42,8 @@ use serde::{Deserialize, Serialize};
use super::{
send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest,
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, StateChanges,
StoreError,
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind,
RoomLoadSettings, StateChanges, StoreError,
};
use crate::{
deserialized_responses::{
@@ -194,8 +194,11 @@ pub trait StateStore: AsyncTraitDeps {
memberships: RoomMemberships,
) -> Result<Vec<OwnedUserId>, Self::Error>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error>;
/// Get a set of pure `RoomInfo`s the store knows about.
async fn get_room_infos(
&self,
room_load_settings: &RoomLoadSettings,
) -> Result<Vec<RoomInfo>, Self::Error>;
/// Get all the users that use the given display name in the given room.
///
@@ -574,8 +577,11 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
self.0.get_user_ids(room_id, memberships).await.map_err(Into::into)
}
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error> {
self.0.get_room_infos().await.map_err(Into::into)
async fn get_room_infos(
&self,
room_load_settings: &RoomLoadSettings,
) -> Result<Vec<RoomInfo>, Self::Error> {
self.0.get_room_infos(room_load_settings).await.map_err(Into::into)
}
async fn get_users_with_display_name(
+2 -2
View File
@@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize};
use crate::{
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
store::Store,
store::BaseStateStore,
};
/// Generalized representation of a `/sync` response.
@@ -85,7 +85,7 @@ impl RoomUpdates {
/// Update the caches for the rooms that received updates.
///
/// This will only fill the in-memory caches, not save the info on disk.
pub(crate) async fn update_in_memory_caches(&self, store: &Store) {
pub(crate) async fn update_in_memory_caches(&self, store: &BaseStateStore) {
for room in self
.leave
.keys()
+9 -6
View File
@@ -18,23 +18,26 @@
use ruma::{owned_user_id, UserId};
use crate::{store::StoreConfig, BaseClient, SessionMeta};
use crate::{
store::{RoomLoadSettings, 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::with_store_config(StoreConfig::new(
"cross-process-store-locks-holder-name".to_owned(),
));
let client =
BaseClient::new(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
.set_session_meta(
.activate(
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await
.expect("set_session_meta failed!");
.expect("`activate` failed!");
client
}
+12 -3
View File
@@ -6,11 +6,20 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.11.0] - 2025-04-11
### Features
- Add a simple TTL cache implementation. The `TtlCache` struct can be used as a
key/value map that expires items after 15 minutes.
([#4663](https://github.com/matrix-org/matrix-rust-sdk/pull/4663))
## [0.10.0] - 2025-02-04
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type
`TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when
we couldn't compute the push actions, because we lacked some information).
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been
fused into a single type `TimelineEvent`, and its field `push_actions`
has been made `Option`al (it is set to `None` when we couldn't
compute the push actions, because we lacked some information).
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
## [0.9.0] - 2024-12-18
+1 -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.10.0"
version = "0.11.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -302,6 +302,9 @@ pub struct EncryptionInfo {
/// Callers that persist this should mark the state as dirty when a device
/// change is received down the sync.
pub verification_state: VerificationState,
/// The Megolm session ID that was used to encrypt this event, or None if
/// this info was stored before we collected this data.
pub session_id: Option<String>,
}
/// Represents a matrix room event that has been returned from `/sync`,
@@ -540,6 +543,19 @@ impl TimelineEventKind {
TimelineEventKind::PlainText { event } => event,
}
}
/// The Megolm session ID that was used to send this event, if it was
/// encrypted.
pub fn session_id(&self) -> Option<&str> {
match self {
TimelineEventKind::Decrypted(decrypted_room_event) => {
decrypted_room_event.encryption_info.session_id.as_ref()
}
TimelineEventKind::UnableToDecrypt { utd_info, .. } => utd_info.session_id.as_ref(),
TimelineEventKind::PlainText { .. } => None,
}
.map(String::as_str)
}
}
#[cfg(not(tarpaulin_include))]
@@ -1042,6 +1058,7 @@ mod tests {
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
session_id: Some("xyz".to_owned()),
},
unsigned_encryption_info: Some(BTreeMap::from([(
UnsignedEventLocation::RelationsReplace,
@@ -1080,6 +1097,7 @@ mod tests {
}
},
"verification_state": "Verified",
"session_id": "xyz",
},
"unsigned_encryption_info": {
"RelationsReplace": {"UnableToDecrypt": {
@@ -1128,6 +1146,7 @@ mod tests {
event.encryption_info().unwrap().algorithm_info,
AlgorithmInfo::MegolmV1AesSha2 { .. }
);
assert_eq!(event.encryption_info().unwrap().session_id, None);
// Test that the previous format, with an undecryptable unsigned event, can also
// be deserialized.
@@ -1283,49 +1302,57 @@ mod tests {
#[test]
fn snapshot_test_verification_level() {
assert_json_snapshot!(VerificationLevel::VerificationViolation);
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(VerificationLevel::VerificationViolation);
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
});
}
#[test]
fn snapshot_test_verification_states() {
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
assert_json_snapshot!(VerificationState::Unverified(
VerificationLevel::VerificationViolation
));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::InsecureSource,
)));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::MissingDevice,
)));
assert_json_snapshot!(VerificationState::Verified);
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
assert_json_snapshot!(VerificationState::Unverified(
VerificationLevel::VerificationViolation
));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::InsecureSource,
)));
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::MissingDevice,
)));
assert_json_snapshot!(VerificationState::Verified);
});
}
#[test]
fn snapshot_test_shield_states() {
assert_json_snapshot!(ShieldState::None);
assert_json_snapshot!(ShieldState::Red {
code: ShieldStateCode::UnverifiedIdentity,
message: "a message"
});
assert_json_snapshot!(ShieldState::Grey {
code: ShieldStateCode::AuthenticityNotGuaranteed,
message: "authenticity of this message cannot be guaranteed",
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(ShieldState::None);
assert_json_snapshot!(ShieldState::Red {
code: ShieldStateCode::UnverifiedIdentity,
message: "a message"
});
assert_json_snapshot!(ShieldState::Grey {
code: ShieldStateCode::AuthenticityNotGuaranteed,
message: "authenticity of this message cannot be guaranteed",
});
});
}
#[test]
fn snapshot_test_shield_codes() {
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
assert_json_snapshot!(ShieldStateCode::SentInClear);
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
assert_json_snapshot!(ShieldStateCode::SentInClear);
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
});
}
#[test]
@@ -1341,7 +1368,9 @@ mod tests {
]),
};
assert_json_snapshot!(info)
with_settings!({ prepend_module_to_snapshot => false }, {
assert_json_snapshot!(info)
});
}
#[test]
@@ -1354,9 +1383,10 @@ mod tests {
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
session_id: Some("mysessionid76".to_owned()),
};
with_settings!({sort_maps =>true}, {
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
assert_json_snapshot!(info)
})
}
@@ -1383,6 +1413,7 @@ mod tests {
]),
},
verification_state: VerificationState::Verified,
session_id: Some("mysessionid112".to_owned()),
},
unsigned_encryption_info: Some(BTreeMap::from([(
UnsignedEventLocation::RelationsThreadLatestEvent,
@@ -1397,7 +1428,7 @@ mod tests {
push_actions: Default::default(),
};
with_settings!({sort_maps =>true}, {
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
// We use directly the serde_json formatter here, because of a bug in insta
// not serializing custom BTreeMap key enum https://github.com/mitsuhiko/insta/issues/689
assert_json_snapshot! {
+19 -3
View File
@@ -47,13 +47,13 @@ where
let _ = future.await;
});
JoinHandle { remote_handle, abort_handle }
JoinHandle { remote_handle: Some(remote_handle), abort_handle }
}
#[cfg(target_arch = "wasm32")]
#[derive(Debug)]
pub struct JoinHandle<T> {
remote_handle: RemoteHandle<T>,
remote_handle: Option<RemoteHandle<T>>,
abort_handle: AbortHandle,
}
@@ -62,6 +62,20 @@ impl<T> JoinHandle<T> {
pub fn abort(&self) {
self.abort_handle.abort();
}
pub fn is_finished(&self) -> bool {
self.abort_handle.is_aborted()
}
}
#[cfg(target_arch = "wasm32")]
impl<T> Drop for JoinHandle<T> {
fn drop(&mut self) {
// don't abort the spawned future
if let Some(h) = self.remote_handle.take() {
h.forget();
}
}
}
#[cfg(target_arch = "wasm32")]
@@ -72,8 +86,10 @@ impl<T: 'static> Future for JoinHandle<T> {
if self.abort_handle.is_aborted() {
// The future has been aborted. It is not possible to poll it again.
Poll::Ready(Err(JoinError))
} else if let Some(handle) = self.remote_handle.as_mut() {
Pin::new(handle).poll(cx).map(Ok)
} else {
Pin::new(&mut self.remote_handle).poll(cx).map(Ok)
Poll::Ready(Err(JoinError))
}
}
}
+1
View File
@@ -32,6 +32,7 @@ pub mod sleep;
pub mod store_locks;
pub mod timeout;
pub mod tracing_timer;
pub mod ttl_cache;
// We cannot currently measure test coverage in the WASM environment, so
// js_tracing is incorrectly flagged as untested. Disable coverage checking for
@@ -14,6 +14,7 @@
use std::{
collections::VecDeque,
iter::repeat_n,
ops::{ControlFlow, Not},
sync::{Arc, RwLock},
};
@@ -133,7 +134,8 @@ impl UpdateToVectorDiff {
///
/// * [`Update::NewItemsChunk`] and [`Update::NewGapChunk`] are inserting a
/// new pair with a chunk length of 0 at the appropriate index,
/// * [`Update::RemoveChunk`] is removing a pair,
/// * [`Update::RemoveChunk`] is removing a pair, and is potentially
/// emitting [`VectorDiff`],
/// * [`Update::PushItems`] is increasing the length of the appropriate pair
/// by the number of new items, and is potentially emitting
/// [`VectorDiff`],
@@ -264,11 +266,10 @@ impl UpdateToVectorDiff {
| Update::NewGapChunk { previous, new, next, .. } => {
match (previous, next) {
// New chunk at the end.
(Some(previous), None) => {
debug_assert!(
matches!(self.chunks.back(), Some((p, _)) if p == previous),
"Inserting new chunk at the end: The previous chunk is invalid"
);
(Some(_previous), None) => {
// No need to check `previous`. It's possible that the linked chunk is
// lazily loaded, chunk by chunk. The `next` is always reliable, but the
// `previous` might not exist in-memory yet.
self.chunks.push_back((*new, 0));
}
@@ -284,7 +285,7 @@ impl UpdateToVectorDiff {
}
// New chunk is inserted between 2 chunks.
(Some(previous), Some(next)) => {
(Some(_previous), Some(next)) => {
let next_chunk_index = self
.chunks
.iter()
@@ -296,10 +297,9 @@ impl UpdateToVectorDiff {
// or `ObservableUpdates` contain a bug.
.expect("Inserting new chunk: The chunk is not found");
debug_assert!(
matches!(self.chunks.get(next_chunk_index - 1), Some((p, _)) if p == previous),
"Inserting new chunk: The previous chunk is invalid"
);
// No need to check `previous`. It's possible that the linked chunk is
// lazily loaded, chunk by chunk. The `next` is always reliable, but the
// `previous` might not exist in-memory yet.
self.chunks.insert(next_chunk_index, (*new, 0));
}
@@ -319,22 +319,17 @@ impl UpdateToVectorDiff {
}
}
Update::RemoveChunk(expected_chunk_identifier) => {
let chunk_index = self
.chunks
.iter()
.position(|(chunk_identifier, _)| {
chunk_identifier == expected_chunk_identifier
})
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
// assuming `Self::chunks` is correctly initialized, it is not possible to
// remove a chunk that does not exist. If this predicate fails, it means
// `LinkedChunk` or `ObservableUpdates` contain a bug.
.expect("Removing a chunk: The chunk is not found");
Update::RemoveChunk(chunk_identifier) => {
let (offset, (chunk_index, _)) =
self.map_to_offset(&Position(*chunk_identifier, 0));
// It's OK to ignore the result. The `chunk_index` exists because it's been
// found, and we don't care about its associated value.
let _ = self.chunks.remove(chunk_index);
let (_, number_of_items) = self
.chunks
.remove(chunk_index)
.expect("Removing an index out of the bounds");
// Removing at the same index because each `Remove` shifts items to the left.
diffs.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
}
Update::PushItems { at: position, items } => {
@@ -478,7 +473,7 @@ mod tests {
use imbl::{vector, Vector};
use super::{
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
super::{Chunk, ChunkIdentifierGenerator, LinkedChunk, Update},
VectorDiff,
};
@@ -635,10 +630,7 @@ mod tests {
);
let removed_item = linked_chunk
.remove_item_at(
linked_chunk.item_position(|item| *item == 'c').unwrap(),
EmptyChunk::Remove,
)
.remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
.unwrap();
assert_eq!(removed_item, 'c');
assert_items_eq!(
@@ -658,10 +650,7 @@ mod tests {
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
let removed_item = linked_chunk
.remove_item_at(
linked_chunk.item_position(|item| *item == 'z').unwrap(),
EmptyChunk::Remove,
)
.remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap())
.unwrap();
assert_eq!(removed_item, 'z');
assert_items_eq!(
@@ -852,6 +841,99 @@ mod tests {
});
}
#[test]
fn test_as_vector_remove_chunk() {
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let mut as_vector = linked_chunk.as_vector().unwrap();
let mut accumulator = Vector::new();
assert!(as_vector.take().is_empty());
linked_chunk.push_items_back(['a', 'b']);
linked_chunk.push_gap_back(());
linked_chunk.push_items_back(['c']);
linked_chunk.push_gap_back(());
linked_chunk.push_items_back(['d', 'e', 'f', 'g']);
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] [-] ['d', 'e', 'f'] ['g']);
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+
// | a | b | c | d | e | f | g |
// +---+---+---+---+---+---+---+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// |
// new
apply_and_assert_eq(
&mut accumulator,
as_vector.take(),
&[
VectorDiff::Append { values: vector!['a', 'b'] },
VectorDiff::Append { values: vector!['c'] },
VectorDiff::Append { values: vector!['d', 'e', 'f'] },
VectorDiff::Append { values: vector!['g'] },
],
);
// Empty a chunk, and remove it once it is empty.
linked_chunk
.remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
.unwrap();
assert_items_eq!(linked_chunk, ['a', 'b'] [-] [-] ['d', 'e', 'f'] ['g']);
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3 4 5 6
// +---+---+---+---+---+---+
// | a | b | d | e | f | g |
// +---+---+---+---+---+---+
// ^
// |
// `c` has been removed
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 2 }]);
// Remove a gap.
linked_chunk
.remove_empty_chunk_at(linked_chunk.chunk_identifier(Chunk::is_gap).unwrap())
.unwrap();
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d', 'e', 'f'] ['g']);
// From an `ObservableVector` point of view, nothing changes.
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[]);
// Remove a non-empty chunk. This is not possible with the public
// `LinkedChunk` API yet, but let's try.
let d_e_and_f = linked_chunk.item_position(|item| *item == 'f').unwrap().chunk_identifier();
let updates = linked_chunk.updates().unwrap();
updates.push(Update::RemoveChunk(d_e_and_f));
// Note that `linked_chunk` is getting out of sync with `AsVector`
// but it's just a test. Better, it's the end of the test.
// From an `ObservableVector` point of view, it would look like:
//
// 0 1 2 3
// +---+---+---+
// | a | b | g |
// +---+---+---+
// ^
// |
// `d`, `e` and `f` have been removed
apply_and_assert_eq(
&mut accumulator,
as_vector.take(),
&[
VectorDiff::Remove { index: 2 },
VectorDiff::Remove { index: 2 },
VectorDiff::Remove { index: 2 },
],
);
}
#[cfg(not(target_arch = "wasm32"))]
mod proptests {
use proptest::prelude::*;
@@ -917,7 +999,7 @@ mod tests {
continue;
};
linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
linked_chunk.remove_item_at(position).expect("Failed to remove an item");
}
}
}
@@ -1,479 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashSet},
marker::PhantomData,
};
use tracing::error;
use super::{
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
ObservableUpdates, RawChunk,
};
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
///
/// Instead of using linking the chunks with pointers, this uses
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
/// which will get resolved later when re-building the full data structure. This
/// allows using chunks that references other chunks that aren't known yet.
struct TemporaryChunk<Item, Gap> {
previous: Option<ChunkIdentifier>,
next: Option<ChunkIdentifier>,
content: ChunkContent<Item, Gap>,
}
/// A data structure to rebuild a linked chunk from its raw representation.
///
/// A linked chunk can be rebuilt incrementally from its internal
/// representation, with the chunks being added *in any order*, as long as they
/// form a single connected component eventually (viz., there's no
/// subgraphs/sublists isolated from the one final linked list). If they don't,
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
/// error).
#[allow(missing_debug_implementations)]
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
/// Work-in-progress chunks.
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
/// Is the final `LinkedChunk` expected to include an update history, as if
/// it were created with [`LinkedChunk::new_with_update_history`]?
build_with_update_history: bool,
}
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
fn default() -> Self {
Self::new()
}
}
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
/// Create an empty [`LinkedChunkBuilder`] with no update history.
pub fn new() -> Self {
Self { chunks: Default::default(), build_with_update_history: false }
}
/// Stash a gap chunk with its content.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_gap(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
content: Gap,
) {
let chunk = TemporaryChunk { previous, next, content: ChunkContent::Gap(content) };
self.chunks.insert(id, chunk);
}
/// Stash an item chunk with its contents.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_items(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
items: impl IntoIterator<Item = Item>,
) {
let chunk = TemporaryChunk {
previous,
next,
content: ChunkContent::Items(items.into_iter().collect()),
};
self.chunks.insert(id, chunk);
}
/// Request that the resulting linked chunk will have an update history, as
/// if it were created with [`LinkedChunk::new_with_update_history`].
pub fn with_update_history(&mut self) {
self.build_with_update_history = true;
}
/// Run all error checks before reconstructing the full linked chunk.
///
/// Must be called after checking `self.chunks` isn't empty in
/// [`Self::build`].
///
/// Returns the identifier of the first chunk.
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
// Look for the first id.
let first_id =
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
// it's a malformed list.
let Some(first_id) = first_id else {
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
// We're going to iterate from the first to the last chunk.
// Keep track of chunks we've already visited.
let mut visited = HashSet::new();
// Start from the first chunk.
let mut maybe_cur = Some(first_id);
while let Some(cur) = maybe_cur {
// The chunk must be referenced in `self.chunks`.
let Some(chunk) = self.chunks.get(&cur) else {
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
};
if let ChunkContent::Items(items) = &chunk.content {
if items.len() > CAP {
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
}
}
// If it's not the first chunk,
if cur != first_id {
// It must have a previous link.
let Some(prev) = chunk.previous else {
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
first_candidate: first_id,
second_candidate: cur,
});
};
// And we must have visited its predecessor at this point, since we've
// iterated from the first chunk.
if !visited.contains(&prev) {
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
}
}
// Add the current chunk to the list of seen chunks.
if !visited.insert(cur) {
// If we didn't insert, then it was already visited: there's a cycle!
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
}
// Move on to the next chunk. If it's none, we'll quit the loop.
maybe_cur = chunk.next;
}
// If there are more chunks than those we've visited: some of them were not
// linked to the "main" branch of the linked list, so we had multiple connected
// components.
if visited.len() != self.chunks.len() {
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
}
Ok(first_id)
}
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
if self.chunks.is_empty() {
return Ok(None);
}
// Run checks.
let first_id = self.check_consistency()?;
// We're now going to iterate from the first to the last chunk. As we're doing
// this, we're also doing a few other things:
//
// - rebuilding the final `Chunk`s one by one, that will be linked using
// pointers,
// - counting items from the item chunks we'll encounter,
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
let mut max_chunk_id = first_id.index();
// Small helper to graduate a temporary chunk into a final one. As we're doing
// this, we're also updating the maximum chunk id (that will be used to
// set up the id generator), and the number of items in this chunk.
let mut graduate_chunk = |id: ChunkIdentifier| {
let temp = self.chunks.remove(&id)?;
// Update the maximum chunk identifier, while we're around.
max_chunk_id = max_chunk_id.max(id.index());
// Graduate the current temporary chunk into a final chunk.
let chunk_ptr = Chunk::new_leaked(id, temp.content);
Some((temp.next, chunk_ptr))
};
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
let mut prev_chunk_ptr = first_chunk_ptr;
while let Some(id) = next_chunk_id {
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingChunk { id });
};
let chunk = unsafe { chunk_ptr.as_mut() };
// Link the current chunk to its previous one.
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
prev_chunk.next = Some(chunk_ptr);
chunk.previous = Some(prev_chunk_ptr);
// Prepare for the next iteration.
prev_chunk_ptr = chunk_ptr;
next_chunk_id = new_next;
}
debug_assert!(self.chunks.is_empty());
// Maintain the convention that `Ends::last` may be unset.
let last_chunk_ptr = prev_chunk_ptr;
let last_chunk_ptr =
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
let chunk_identifier_generator =
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
max_chunk_id,
));
let updates =
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
}
/// Fills a linked chunk builder from all the given raw parts.
pub fn from_raw_parts(raws: Vec<RawChunk<Item, Gap>>) -> Self {
let mut this = Self::new();
for raw in raws {
match raw.content {
ChunkContent::Gap(gap) => {
this.push_gap(raw.previous, raw.identifier, raw.next, gap);
}
ChunkContent::Items(vec) => {
this.push_items(raw.previous, raw.identifier, raw.next, vec);
}
}
}
this
}
}
#[derive(thiserror::Error, Debug)]
pub enum LinkedChunkBuilderError {
#[error("chunk with id {} is too large", id.index())]
ChunkTooLarge { id: ChunkIdentifier },
#[error("there's no first chunk")]
MissingFirstChunk,
#[error("there are multiple first chunks")]
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
#[error("unable to resolve chunk with id {}", id.index())]
MissingChunk { id: ChunkIdentifier },
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
Cycle { repeated: ChunkIdentifier },
#[error("multiple connected components")]
MultipleConnectedComponents,
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::LinkedChunkBuilder;
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
#[test]
fn test_empty() {
let lcb = LinkedChunkBuilder::<3, char, char>::new();
// Building an empty linked chunk works, and returns `None`.
let lc = lcb.build().unwrap();
assert!(lc.is_none());
}
#[test]
fn test_success() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
// the chunk id space.
let cid3 = ChunkIdentifier::new(3);
// Check that we can successfully create a linked chunk, independently of the
// order in which chunks are added.
//
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
// Adding chunk cid0.
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
// Adding chunk cid3.
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
// Adding chunk cid1.
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
let mut lc =
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
// Check the entire content first.
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
// Run checks on the first chunk.
let mut chunks = lc.chunks();
let first_chunk = chunks.next().unwrap();
{
assert!(first_chunk.previous().is_none());
assert_eq!(first_chunk.identifier(), cid0);
}
// Run checks on the second chunk.
let second_chunk = chunks.next().unwrap();
{
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
assert_eq!(second_chunk.identifier(), cid1);
}
// Run checks on the third chunk.
let third_chunk = chunks.next().unwrap();
{
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
assert!(third_chunk.next().is_none());
assert_eq!(third_chunk.identifier(), cid3);
}
// There's no more chunk.
assert!(chunks.next().is_none());
// The linked chunk had 5 items.
assert_eq!(lc.num_items(), 5);
// Now, if we add a new chunk, its identifier should be the previous one we used
// + 1.
lc.push_gap_back('h');
let last_chunk = lc.chunks().last().unwrap();
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
}
#[test]
fn test_chunk_too_large() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
// Adding a chunk with 4 items will fail, because the max capacity specified in
// the builder generics is 3.
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
assert_eq!(id, cid0);
});
}
#[test]
fn test_missing_first_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
}
#[test]
fn test_multiple_first_chunks() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
// Second chunk lies and pretends to be the first too.
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
assert_eq!(first_candidate, cid0);
assert_eq!(second_candidate, cid1);
});
}
#[test]
fn test_missing_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
assert_eq!(id, cid1);
});
}
#[test]
fn test_cycle() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
assert_eq!(repeated, cid0);
});
}
#[test]
fn test_multiple_connected_components() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
// cid0 and cid1 are linked to each other.
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
// cid2 stands on its own.
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -15,10 +15,15 @@
//! Implementation for a _relational linked chunk_, see
//! [`RelationalLinkedChunk`].
use ruma::{OwnedRoomId, RoomId};
use std::{collections::HashMap, hash::Hash};
use super::{ChunkContent, RawChunk};
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
use ruma::{OwnedEventId, OwnedRoomId, RoomId};
use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
use crate::{
deserialized_responses::TimelineEvent,
linked_chunk::{ChunkIdentifier, Position, Update},
};
/// A row of the [`RelationalLinkedChunk::chunks`].
#[derive(Debug, PartialEq)]
@@ -31,10 +36,10 @@ struct ChunkRow {
/// A row of the [`RelationalLinkedChunk::items`].
#[derive(Debug, PartialEq)]
struct ItemRow<Item, Gap> {
struct ItemRow<ItemId, Gap> {
room_id: OwnedRoomId,
position: Position,
item: Either<Item, Gap>,
item: Either<ItemId, Gap>,
}
/// Kind of item.
@@ -66,23 +71,49 @@ enum Either<Item, Gap> {
///
/// [`LinkedChunk`]: super::LinkedChunk
#[derive(Debug)]
pub struct RelationalLinkedChunk<Item, Gap> {
pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
/// Chunks.
chunks: Vec<ChunkRow>,
/// Items.
items: Vec<ItemRow<Item, Gap>>,
/// Items chunks.
items_chunks: Vec<ItemRow<ItemId, Gap>>,
/// The items' content themselves.
items: HashMap<OwnedRoomId, HashMap<ItemId, Item>>,
}
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
/// The [`IndexableItem`] trait is used to mark items that can be indexed into a
/// [`RelationalLinkedChunk`].
pub trait IndexableItem {
type ItemId: Hash + PartialEq + Eq + Clone;
/// Return the identifier of the item.
fn id(&self) -> Self::ItemId;
}
impl IndexableItem for TimelineEvent {
type ItemId = OwnedEventId;
fn id(&self) -> Self::ItemId {
self.event_id()
.expect("all events saved into a relational linked chunk must have a valid event id")
}
}
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone,
{
/// Create a new relational linked chunk.
pub fn new() -> Self {
Self { chunks: Vec::new(), items: Vec::new() }
Self { chunks: Vec::new(), items_chunks: Vec::new(), items: HashMap::new() }
}
/// Removes all the chunks and items from this relational linked chunk.
pub fn clear(&mut self) {
self.chunks.clear();
self.items_chunks.clear();
self.items.clear();
}
@@ -97,7 +128,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
Update::NewGapChunk { previous, new, next, gap } => {
insert_chunk(&mut self.chunks, room_id, previous, new, next);
self.items.push(ItemRow {
self.items_chunks.push(ItemRow {
room_id: room_id.to_owned(),
position: Position::new(new, 0),
item: Either::Gap(gap),
@@ -108,7 +139,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
let indices_to_remove = self
.items
.items_chunks
.iter()
.enumerate()
.filter_map(
@@ -121,16 +152,21 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
self.items_chunks.remove(index_to_remove);
}
}
Update::PushItems { mut at, items } => {
for item in items {
self.items.push(ItemRow {
let item_id = item.id();
self.items
.entry(room_id.to_owned())
.or_default()
.insert(item_id.clone(), item);
self.items_chunks.push(ItemRow {
room_id: room_id.to_owned(),
position: at,
item: Either::Item(item),
item: Either::Item(item_id),
});
at.increment_index();
}
@@ -138,7 +174,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
Update::ReplaceItem { at, item } => {
let existing = self
.items
.items_chunks
.iter_mut()
.find(|item| item.position == at)
.expect("trying to replace at an unknown position");
@@ -146,14 +182,16 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
matches!(existing.item, Either::Item(..)),
"trying to replace a gap with an item"
);
existing.item = Either::Item(item);
let item_id = item.id();
self.items.entry(room_id.to_owned()).or_default().insert(item_id.clone(), item);
existing.item = Either::Item(item_id);
}
Update::RemoveItem { at } => {
let mut entry_to_remove = None;
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
self.items.iter_mut().enumerate()
self.items_chunks.iter_mut().enumerate()
{
// Filter by room ID.
if room_id != room_id_candidate {
@@ -175,12 +213,13 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
}
}
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
self.items_chunks.remove(entry_to_remove.expect("Remove an unknown item"));
// We deliberately keep the item in the items collection.
}
Update::DetachLastItems { at } => {
let indices_to_remove = self
.items
.items_chunks
.iter()
.enumerate()
.filter_map(
@@ -194,7 +233,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
self.items_chunks.remove(index_to_remove);
}
}
@@ -202,7 +241,8 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
Update::Clear => {
self.chunks.retain(|chunk| chunk.room_id != room_id);
self.items.retain(|chunk| chunk.room_id != room_id);
self.items_chunks.retain(|chunk| chunk.room_id != room_id);
// We deliberately leave the items intact.
}
}
}
@@ -292,116 +332,259 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
}
}
}
/// Return an iterator that yields items of a particular room, in no
/// particular order.
pub fn unordered_room_items<'a>(
&'a self,
room_id: &'a RoomId,
) -> impl Iterator<Item = (&'a Item, Position)> {
self.items_chunks.iter().filter_map(move |item_row| {
if item_row.room_id == room_id {
match &item_row.item {
Either::Item(item_id) => {
Some((self.items.get(room_id)?.get(item_id)?, item_row.position))
}
Either::Gap(..) => None,
}
} else {
None
}
})
}
/// Return an iterator over all items of all rooms, without their actual
/// positions.
///
/// This will include out-of-band items.
pub fn items(&self) -> impl Iterator<Item = (&Item, &RoomId)> {
self.items
.iter()
.flat_map(|(room_id, items)| items.values().map(|item| (item, room_id.as_ref())))
}
/// Save a single item "out-of-band" in the relational linked chunk.
pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
let id = item.id();
self.items.entry(room_id).or_default().insert(id, item);
}
}
impl<Item, Gap> RelationalLinkedChunk<Item, Gap>
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
where
Gap: Clone,
Item: Clone,
ItemId: Hash + PartialEq + Eq,
{
/// Reloads the chunks.
/// Loads all the chunks.
///
/// Return an error result if the data was malformed in the struct, with a
/// string message explaining details about the error.
pub fn reload_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
let mut result = Vec::new();
#[doc(hidden)]
pub fn load_all_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
self.chunks
.iter()
.filter(|chunk| chunk.room_id == room_id)
.map(|chunk_row| load_raw_chunk(self, chunk_row, room_id))
.collect::<Result<Vec<_>, String>>()
}
for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) {
// Find all items that correspond to the chunk.
let mut items = self
.items
.iter()
.filter(|row| {
row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
pub fn load_last_chunk(
&self,
room_id: &RoomId,
) -> Result<(Option<RawChunk<Item, Gap>>, ChunkIdentifierGenerator), String> {
// Find the latest chunk identifier to generate a `ChunkIdentifierGenerator`.
let chunk_identifier_generator = match self
.chunks
.iter()
.filter_map(|chunk_row| (chunk_row.room_id == room_id).then_some(chunk_row.chunk))
.max()
{
Some(last_chunk_identifier) => {
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier)
}
None => ChunkIdentifierGenerator::new_from_scratch(),
};
// Look at the first chunk item type, to reconstruct the chunk at hand.
let Some(first) = items.peek() else {
// The only possibility is that we created an empty items chunk; mark it as
// such, and continue.
result.push(RawChunk {
content: ChunkContent::Items(Vec::new()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
continue;
};
// Find the last chunk.
let mut number_of_chunks = 0;
let mut chunk_row = None;
match &first.item {
Either::Item(_) => {
// Collect all the related items.
let mut collected_items = Vec::new();
for row in items {
match &row.item {
Either::Item(item) => {
collected_items.push((item.clone(), row.position.index()))
}
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
for chunk_row_candidate in &self.chunks {
if chunk_row_candidate.room_id == room_id {
number_of_chunks += 1;
// Sort them by their position.
collected_items.sort_unstable_by_key(|(_item, index)| *index);
if chunk_row_candidate.next_chunk.is_none() {
chunk_row = Some(chunk_row_candidate);
result.push(RawChunk {
content: ChunkContent::Items(
collected_items.into_iter().map(|(item, _index)| item).collect(),
),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
}
Either::Gap(gap) => {
assert!(items.next().is_some(), "we just peeked the gap");
// We shouldn't have more than one item row for this chunk.
if items.next().is_some() {
return Err(format!(
"there shouldn't be more than one item row attached in gap chunk {}",
chunk_row.chunk.index()
));
}
result.push(RawChunk {
content: ChunkContent::Gap(gap.clone()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
break;
}
}
}
Ok(result)
let chunk_row = match chunk_row {
// Chunk has been found, all good.
Some(chunk_row) => chunk_row,
// Chunk is not found and there is zero chunk for this room, this is consistent, all
// good.
None if number_of_chunks == 0 => {
return Ok((None, chunk_identifier_generator));
}
// Chunk is not found **but** there are chunks for this room, this is inconsistent. The
// linked chunk is malformed.
//
// Returning `Ok(None)` would be invalid here: we must return an error.
None => {
return Err(
"last chunk is not found but chunks exist: the linked chunk contains a cycle"
.to_owned(),
);
}
};
// Build the chunk.
load_raw_chunk(self, chunk_row, room_id)
.map(|raw_chunk| (Some(raw_chunk), chunk_identifier_generator))
}
pub fn load_previous_chunk(
&self,
room_id: &RoomId,
before_chunk_identifier: ChunkIdentifier,
) -> Result<Option<RawChunk<Item, Gap>>, String> {
// Find the chunk before the chunk identified by `before_chunk_identifier`.
let Some(chunk_row) = self.chunks.iter().find(|chunk_row| {
chunk_row.room_id == room_id && chunk_row.next_chunk == Some(before_chunk_identifier)
}) else {
// Chunk is not found.
return Ok(None);
};
// Build the chunk.
load_raw_chunk(self, chunk_row, room_id).map(Some)
}
}
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
where
Item: IndexableItem<ItemId = ItemId>,
ItemId: Hash + PartialEq + Eq + Clone,
{
fn default() -> Self {
Self::new()
}
}
fn load_raw_chunk<ItemId, Item, Gap>(
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
chunk_row: &ChunkRow,
room_id: &RoomId,
) -> Result<RawChunk<Item, Gap>, String>
where
Item: Clone,
Gap: Clone,
ItemId: Hash + PartialEq + Eq,
{
// Find all items that correspond to the chunk.
let mut items = relational_linked_chunk
.items_chunks
.iter()
.filter(|item_row| {
item_row.room_id == room_id && item_row.position.chunk_identifier() == chunk_row.chunk
})
.peekable();
let Some(first_item) = items.peek() else {
// No item. It means it is a chunk of kind `Items` and that it is empty!
return Ok(RawChunk {
content: ChunkContent::Items(Vec::new()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
});
};
Ok(match first_item.item {
// This is a chunk of kind `Items`.
Either::Item(_) => {
// Collect all the items.
let mut collected_items = Vec::new();
for item_row in items {
match &item_row.item {
Either::Item(item_id) => {
collected_items.push((item_id, item_row.position.index()))
}
Either::Gap(_) => {
return Err(format!(
"unexpected gap in items chunk {}",
chunk_row.chunk.index()
));
}
}
}
// Sort them by their position.
collected_items.sort_unstable_by_key(|(_item, index)| *index);
RawChunk {
content: ChunkContent::Items(
collected_items
.into_iter()
.filter_map(|(item_id, _index)| {
relational_linked_chunk.items.get(room_id)?.get(item_id).cloned()
})
.collect(),
),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
Either::Gap(ref gap) => {
assert!(items.next().is_some(), "we just peeked the gap");
// We shouldn't have more than one item row for this chunk.
if items.next().is_some() {
return Err(format!(
"there shouldn't be more than one item row attached in gap chunk {}",
chunk_row.chunk.index()
));
}
RawChunk {
content: ChunkContent::Gap(gap.clone()),
previous: chunk_row.previous_chunk,
identifier: chunk_row.chunk,
next: chunk_row.next_chunk,
}
}
})
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use ruma::room_id;
use super::{ChunkIdentifier as CId, *};
use crate::linked_chunk::LinkedChunkBuilder;
use super::{super::lazy_loader::from_all_chunks, ChunkIdentifier as CId, *};
impl IndexableItem for char {
type ItemId = char;
fn id(&self) -> Self::ItemId {
*self
}
}
#[test]
fn test_new_items_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -452,13 +635,13 @@ mod tests {
],
);
// Items have not been modified.
assert!(relational_linked_chunk.items.is_empty());
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_new_gap_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -503,7 +686,7 @@ mod tests {
);
// Items contains the gap.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
@@ -515,7 +698,7 @@ mod tests {
#[test]
fn test_remove_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -555,13 +738,13 @@ mod tests {
],
);
// Items no longer contains the gap.
assert!(relational_linked_chunk.items.is_empty());
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_push_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -599,7 +782,7 @@ mod tests {
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[
ItemRow {
room_id: room_id.to_owned(),
@@ -648,7 +831,7 @@ mod tests {
#[test]
fn test_remove_item() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -679,7 +862,7 @@ mod tests {
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[
ItemRow {
room_id: room_id.to_owned(),
@@ -703,7 +886,7 @@ mod tests {
#[test]
fn test_detach_last_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -744,7 +927,7 @@ mod tests {
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[
ItemRow {
room_id: room_id.to_owned(),
@@ -778,21 +961,21 @@ mod tests {
#[test]
fn test_start_and_end_reattach_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
// Nothing happened.
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items.is_empty());
assert!(relational_linked_chunk.items_chunks.is_empty());
}
#[test]
fn test_clear() {
let r0 = room_id!("!r0:matrix.org");
let r1 = room_id!("!r1:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
r0,
@@ -835,7 +1018,7 @@ mod tests {
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[
ItemRow {
room_id: r0.to_owned(),
@@ -875,7 +1058,7 @@ mod tests {
);
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[ItemRow {
room_id: r1.to_owned(),
position: Position::new(CId::new(0), 0),
@@ -885,20 +1068,20 @@ mod tests {
}
#[test]
fn test_reload_empty_linked_chunk() {
fn test_load_empty_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
// When I reload the linked chunk components from an empty store,
let relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
let result = relational_linked_chunk.reload_chunks(room_id).unwrap();
let relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
let result = relational_linked_chunk.load_all_chunks(room_id).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_reload_linked_chunk_with_empty_items() {
fn test_load_all_chunks_with_empty_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
// When I store an empty items chunks,
relational_linked_chunk.apply_updates(
@@ -907,11 +1090,10 @@ mod tests {
);
// It correctly gets reloaded as such.
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
.build()
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
let lc =
from_all_chunks::<3, _, _>(relational_linked_chunk.load_all_chunks(room_id).unwrap())
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
assert_items_eq!(lc, []);
}
@@ -919,7 +1101,7 @@ mod tests {
#[test]
fn test_rebuild_linked_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -942,11 +1124,10 @@ mod tests {
],
);
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
.build()
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
let lc =
from_all_chunks::<3, _, _>(relational_linked_chunk.load_all_chunks(room_id).unwrap())
.expect("building succeeds")
.expect("this leads to a non-empty linked chunk");
// The linked chunk is correctly reloaded.
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
@@ -955,7 +1136,7 @@ mod tests {
#[test]
fn test_replace_item() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
@@ -982,7 +1163,7 @@ mod tests {
// Items contains the pushed *and* replaced items.
assert_eq!(
relational_linked_chunk.items,
relational_linked_chunk.items_chunks,
&[
ItemRow {
room_id: room_id.to_owned(),
@@ -1002,4 +1183,199 @@ mod tests {
],
);
}
#[test]
fn test_unordered_events() {
let room_id = room_id!("!r0:matrix.org");
let other_room_id = room_id!("!r1:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['d', 'e', 'f'] },
],
);
relational_linked_chunk.apply_updates(
other_room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x', 'y', 'z'] },
],
);
let mut events = relational_linked_chunk.unordered_room_items(room_id);
assert_eq!(events.next().unwrap(), (&'a', Position::new(CId::new(0), 0)));
assert_eq!(events.next().unwrap(), (&'b', Position::new(CId::new(0), 1)));
assert_eq!(events.next().unwrap(), (&'c', Position::new(CId::new(0), 2)));
assert_eq!(events.next().unwrap(), (&'d', Position::new(CId::new(1), 0)));
assert_eq!(events.next().unwrap(), (&'e', Position::new(CId::new(1), 1)));
assert_eq!(events.next().unwrap(), (&'f', Position::new(CId::new(1), 2)));
assert!(events.next().is_none());
}
#[test]
fn test_load_last_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
// Case #1: no last chunk.
{
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(room_id).unwrap();
assert!(last_chunk.is_none());
assert_eq!(chunk_identifier_generator.current(), 0);
}
// Case #2: only one chunk is present.
{
relational_linked_chunk.apply_updates(
room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
Update::PushItems { at: Position::new(CId::new(42), 0), items: vec!['a', 'b'] },
],
);
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(room_id).unwrap();
assert_matches!(last_chunk, Some(last_chunk) => {
assert_eq!(last_chunk.identifier, 42);
assert!(last_chunk.previous.is_none());
assert!(last_chunk.next.is_none());
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 2);
assert_eq!(items, &['a', 'b']);
});
});
assert_eq!(chunk_identifier_generator.current(), 42);
}
// Case #3: more chunks are present.
{
relational_linked_chunk.apply_updates(
room_id,
vec![
Update::NewItemsChunk {
previous: Some(CId::new(42)),
new: CId::new(7),
next: None,
},
Update::PushItems {
at: Position::new(CId::new(7), 0),
items: vec!['c', 'd', 'e'],
},
],
);
let (last_chunk, chunk_identifier_generator) =
relational_linked_chunk.load_last_chunk(room_id).unwrap();
assert_matches!(last_chunk, Some(last_chunk) => {
assert_eq!(last_chunk.identifier, 7);
assert_matches!(last_chunk.previous, Some(previous) => {
assert_eq!(previous, 42);
});
assert!(last_chunk.next.is_none());
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items, &['c', 'd', 'e']);
});
});
assert_eq!(chunk_identifier_generator.current(), 42);
}
}
#[test]
fn test_load_last_chunk_with_a_cycle() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
Update::NewItemsChunk {
// Because `previous` connects to chunk #0, it will create a cycle.
// Chunk #0 will have a `next` set to chunk #1! Consequently, the last chunk
// **does not exist**. We have to detect this cycle.
previous: Some(CId::new(0)),
new: CId::new(1),
next: Some(CId::new(0)),
},
],
);
relational_linked_chunk.load_last_chunk(room_id).unwrap_err();
}
#[test]
fn test_load_previous_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
// Case #1: no chunk at all, equivalent to having an inexistent
// `before_chunk_identifier`.
{
let previous_chunk =
relational_linked_chunk.load_previous_chunk(room_id, CId::new(153)).unwrap();
assert!(previous_chunk.is_none());
}
// Case #2: there is one chunk only: we request the previous on this
// one, it doesn't exist.
{
relational_linked_chunk.apply_updates(
room_id,
vec![Update::NewItemsChunk { previous: None, new: CId::new(42), next: None }],
);
let previous_chunk =
relational_linked_chunk.load_previous_chunk(room_id, CId::new(42)).unwrap();
assert!(previous_chunk.is_none());
}
// Case #3: there is two chunks.
{
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk before the one that exists.
Update::NewItemsChunk {
previous: None,
new: CId::new(7),
next: Some(CId::new(42)),
},
Update::PushItems {
at: Position::new(CId::new(7), 0),
items: vec!['a', 'b', 'c'],
},
],
);
let previous_chunk =
relational_linked_chunk.load_previous_chunk(room_id, CId::new(42)).unwrap();
assert_matches!(previous_chunk, Some(previous_chunk) => {
assert_eq!(previous_chunk.identifier, 7);
assert!(previous_chunk.previous.is_none());
assert_matches!(previous_chunk.next, Some(next) => {
assert_eq!(next, 42);
});
assert_matches!(previous_chunk.content, ChunkContent::Items(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items, &['a', 'b', 'c']);
});
});
}
}
}
@@ -138,6 +138,11 @@ impl<Item, Gap> ObservableUpdates<Item, Gap> {
self.inner.write().unwrap().push(update);
}
/// Clear all pending updates.
pub(super) fn clear_pending(&mut self) {
self.inner.write().unwrap().clear_pending();
}
/// Take new updates.
///
/// Updates that have been taken will not be read again.
@@ -242,6 +247,19 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
}
}
/// Clear all pending updates.
fn clear_pending(&mut self) {
self.updates.clear();
// Reset all the per-reader indices.
for idx in self.last_index_per_reader.values_mut() {
*idx = 0;
}
// No need to wake the wakers; they're waiting for a new update, and we
// just made them all disappear.
}
/// Take new updates; it considers the caller is the main reader, i.e. it
/// will use the [`Self::MAIN_READER_TOKEN`].
///

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