Compare commits

...

679 Commits

Author SHA1 Message Date
Benjamin Bouvier f7329c71bb test: test that getting kicked/banned marks the room as left in sliding sync 2024-04-15 11:41:37 +02:00
Benjamin Bouvier b977a239c3 tests: prefix more tests with test_ 2024-04-15 11:41:37 +02:00
Benjamin Bouvier 3aa62a265d sliding sync: also mark kicked/banned users as leaving a room 2024-04-15 11:41:37 +02:00
Benjamin Bouvier fc4cd530fb event cache: introduce the Paginator API (#3309)
This introduces a new helper object to run arbitrary pagination requests, backwards- or forward-. At the moment they're disconnected from the event cache, although I've put the files there for future convenience, since at some point we'll want to merge the retrieved events with the cache (? maybe).

This little state machine makes it possible to retrieve the initial data, given an initial event id, using the /context endpoint; then allow stateful pagination using a paginator kind of API. Paginating in the timeline indicates whether we've reached the start/end of the timeline.

The test for the state subscription is quite extensive and makes sure the basic functionality works as intended.

Some testing helpers have been (re)introduced in the SDK crate, simplifying the code, and introducing a better `EventFactory` / `EventBuilder` pattern than the existing one in the `matrix-sdk-test` crate. In particular, this can make use of some types in `matrix-sdk`, notably `SyncTimelineEvent` and `TimelineEvent`, and I've found the API to be simpler to use as well.

Part of #3234.
2024-04-12 17:57:10 +02:00
Benjamin Bouvier 7c68096237 multiverse: allow setting a proxy
This is handy when running mitmproxy on multiverse.
2024-04-11 16:45:05 +02:00
Stefan Ceriu e12f917559 ffi: expose method for parsing Matrix URIs and converting them into actual Matrix entities 2024-04-11 16:27:20 +02:00
Stefan Ceriu 20f0346733 ffi: expose method for genering user matrix.to permalinks 2024-04-11 16:27:20 +02:00
Benjamin Bouvier 8e98252be3 event cache: only forward a read marker update once per JoinedRoomUpdates
This protects against the sliding sync proxy (or synapse) sending lots
of duplicate fully read marker events for the same room in case of
reset. This would lead to forwarding lots and lots of
`RoomEventCacheUpdate`s to the timeline, cluttering the channel, and
resulting in a lag. This lag would then clear the timeline, seemingly
cause missing chunks from history.

https://github.com/matrix-org/matrix-rust-sdk/pull/3312 is likely the
long term fix (after a clear(), the timeline should retrieve all the
memoized events from the event cache), but this should prevent the root
cause of the spamming in the first place, getting us back to a safe
state.
2024-04-11 14:02:57 +02:00
Benjamin Bouvier 1053bc9148 sync service: only log sliding sync errors when it's not a sliding sync expiration 2024-04-11 12:12:31 +02:00
Benjamin Bouvier 1dd497e9de sync service(refactor): let start task methods be self-less functions
This removes one (1) level of indent in both tasks and makes the code a
bit simpler to read.
2024-04-11 12:12:31 +02:00
Benjamin Bouvier 81f1292660 sync service: show the termination report debug info in the error logs
We do see lots of "broken channel" log lines for these two log
statements, and since I'm unclear why they happened, I'd like to add a
bit more logging to those.

Also makes the log level consistent, both are set to "warn" instead of
one warn and one error. Usually it's not a big deal because the only
error that may happen is that the channel is broken, indicating the task
died before, so there's no need to stop it manually.
2024-04-11 12:12:31 +02:00
Benjamin Bouvier 5671121b21 timeline: add comments about insertion order to the front of the timeline 2024-04-11 12:11:54 +02:00
Kévin Commaille 88c4dec35f sdk: Upgrade image crate
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-11 11:40:32 +02:00
Kévin Commaille 6d9aa14ccd qrcode: Upgrade qrcode crate
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-11 11:40:32 +02:00
Benjamin Bouvier 16551feea3 timeline: lower log severity of read receipts issues
Those two issues really aren't errors we should be too scared of:

- on the one hand, they don't prevent correct usage of the timeline, but
slightly decrease the UX's quality
- on the other hand, they may indicate that read receipts haven't been
received yet, OR some events are unknown (can be happening if the
previous read receipt refers to an event the timeline doesn't know
about).

So I lowered their severity to debug instead of error, since only
outstanding issues should errors or warnings in my opinion.
2024-04-11 11:26:16 +02:00
Andy Balaam 1e7182e820 Merge pull request #3253 from matrix-org/andybalaam/add-backup_version-arg
crypto: Add a backup_version argument to group session backup methods
2024-04-09 16:44:50 +01:00
Andy Balaam a843125fa4 crypto: Share code to get backup_version in tests 2024-04-09 16:31:13 +01:00
Benjamin Bouvier fcfdaadb25 room(refactor): reuse code to decrypt an event instead of duplicating it 2024-04-08 16:16:44 +02:00
Benjamin Bouvier 222f969e2f integration tests: add tests for /context
Update testing/matrix-sdk-integration-testing/src/tests/room.rs

Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Benjamin Bouvier <public@benj.me>
2024-04-08 16:16:44 +02:00
Benjamin Bouvier 406fd011ff room: extend event_with_context so it returns a fully decrypted /context response 2024-04-08 16:16:44 +02:00
Benjamin Bouvier b903ee4b42 room(style): remove one level of indent with an early let/else statement
Classic Benji.
2024-04-08 16:16:44 +02:00
Stefan Ceriu b9b5286f8a ffi: fix the experimental-room-list-with-unified-invites feature, the sliding sync is_invite field should be None when they are allowed in the room list 2024-04-08 15:37:28 +02:00
dependabot[bot] 2f5d8c212a chore(deps): bump h2 from 0.3.24 to 0.3.26
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 15:20:10 +02:00
Benjamin Bouvier 797532815a room: warn at most once per room if the room version is missing
This should avoid spamming the logs about missing room versions.
2024-04-08 09:41:56 +02:00
Andy Balaam f11aeafd58 crypto: Add an optional backup_version param to inbound_group_session_counts 2024-04-05 16:24:55 +01:00
Kévin Commaille 327e0aef99 sdk: Enable matrix-sdk-base feature conditionally
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-05 13:00:04 +02:00
Benjamin Bouvier 742700b7fa event cache: increase the number of pending updates for a sender
And also log the number of skipped updates, when lagging behind in the
event cache or the timeline.
2024-04-05 10:59:28 +02:00
Benjamin Bouvier 03f4a56bff timeline(optimization): don't call force_update_sender_profiles when there are no member_ambiguity_changes
`force_update_sender_profiles` goes through all the timeline items, even
though the `ambiguity_changes` map should be empty, leading to wasted
work. It might not be a big deal, but it's nice to avoid awaiting locks
when we don't have to.
2024-04-05 10:59:28 +02:00
Benjamin Bouvier d8b0b9e2f4 event cache: better handle room updates lag
First, add a log line, since this is a pretty big deal if we start
lagging behind, and we should understand this clearly in rageshakes.

Second: don't remove existing `RoomEventCache`s, but instead clear them:
we shouldn't re-create new `RoomEventCache`s for the same room id,
otherwise a previous subscriber to updates to `RoomEventCache` would not
get newer updates coming later on.

Third: let consumers know that we cleared the events from the
`RoomEventCaches`.
2024-04-05 10:59:28 +02:00
Benjamin Bouvier dbb9c60d09 linked chunks: cosmetic changes
This tweaks assert and error messages, avoids indent by returning early,
etc.
2024-04-05 10:59:12 +02:00
Benjamin Bouvier b6db3af882 room list service: always include the m.room.create state event in a room subscription 2024-04-05 10:58:55 +02:00
Kévin Commaille 8a8ad22961 ffi: Use async functions in AuthenticationService (#3294)
* ffi: Use async functions in AuthenticationService
* Fix swift tests
* Set async_runtime for uniffi::export attribute
* Rename RwLocks
* Get rid of unnecessary map_err

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-04 15:54:20 +00:00
Benjamin Bouvier b78bbc01a0 timeline: pin all the local echoes to the bottom
Before this patch, only local echoes that were messages not sent or
messages succesfully sent (but not ack'd yet by sync) would be pinned to
the bottom.

This fixes it by also pinning events that failed to send.

Fixes #3287.
2024-04-04 15:02:13 +02:00
Andy Balaam f51865e1ae crypto: Add a backup_version argument to group session backup methods 2024-04-04 11:00:59 +01:00
Stefan Ceriu 682c17c9d8 feat: add support for storing breacrumbs in the state store 2024-04-04 11:23:55 +02:00
Damir Jelić 4da1c01963 chore: Fix some typos 2024-04-04 11:17:18 +02:00
Benjamin Bouvier 96a4b06ca6 timeline: use the day divider adjuster when updating the send state of a message 2024-04-02 16:16:45 +02:00
Benjamin Bouvier ef5b12035d day dividers: explicitly chase trailing day dividers
The algorithm works on the basis that we remove a previous day divider
if it was spurious. But we'd never do this, for a final item that would
be a day divider! So chase these explicitly, also ignore read markers
that would be in the way.
2024-04-02 16:16:45 +02:00
Kévin Commaille 38978dacd7 crypto: Define both bounds in the same place
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-02 15:05:41 +02:00
Kévin Commaille 74ea661438 chore: Use only extern attributes for test modules
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-02 15:05:41 +02:00
Kévin Commaille da2abccc0d chore: Disable clippy::assigning_clones lint
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-02 15:05:41 +02:00
Benjamin Bouvier 6731c52b12 base: extract changes to profile into a small function helper
and detail the comment about why we need to remove a previous profile
first.
2024-04-02 14:30:19 +02:00
Benjamin Bouvier a3ca28f1a5 test: add regression test for #3278 2024-04-02 14:30:19 +02:00
Benjamin Bouvier 450ceaa241 base: remove previous profiles of member when they're invited 2024-04-02 14:30:19 +02:00
Benjamin Bouvier 6cd655ba7c state store: add Changes::profile_to_delete field
So as to remove existing profiles from the storage.
2024-04-02 14:30:19 +02:00
Kévin Commaille f9ab073adf chore: Avoid redundant imports
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-02 13:27:58 +02:00
Kévin Commaille b5d7c40029 tests: Use .contains_key() instead of .get().is_some()/.is_none()
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-02 13:27:58 +02:00
Stefan Ceriu 29f7f88a2c ffi: expose room and event matrix.to permalink builder methods 2024-04-02 10:35:23 +02:00
Benjamin Bouvier db37a1feb5 base: fix typo in comment 2024-03-29 13:04:39 +01:00
Benjamin Bouvier 901024bccc base: remove cognitive overload when passing parameters
The `MemberInfo` struct only existed to regroup parameters, so let's
pass individual parameters instead. One less data structure is good for
the cognitive load.
2024-03-29 13:04:39 +01:00
Benjamin Bouvier 5d1fa986f3 base(logs): remove spammy trace statements in BaseRoom::get_member
and in BaseRoom::member_room_info.
2024-03-29 13:04:39 +01:00
Benjamin Bouvier aa99c7bd38 base(style): avoid repetitions of and_then
Option::and_then(f).and_then(g) is equivalent to Option::and_then(g(f?)).
2024-03-29 13:04:39 +01:00
Benjamin Bouvier 76d484541f test: add test for #3278 2024-03-29 13:04:39 +01:00
Benjamin Bouvier 8482d181b5 base: ensure test_ prefix for all the tests 2024-03-29 13:04:39 +01:00
Benjamin Bouvier 650f210da7 base: copy comment in receive_all_members explaining why it's not fine to set the profile of another user 2024-03-29 11:56:29 +01:00
Benjamin Bouvier 191350a290 Revert "fix: Process profiles for all room members not only state event senders (#3278)"
This reverts commit 99e47ed5d7.
2024-03-29 11:56:29 +01:00
Ivan Enderlin 95baf17c36 fix(sdk): Fix LinkedChunk::insert_items_at.
This patch fixes a bug when inserting items at the end of the
current items of a chunk. The condition was: `if item_index >=
current_items_length`, which is too restrictive. The bug was “hidden”
so far because it's not possible to get the position of “after an item”
with the current API. However, the bug arises if the items are empty and
new items are inserted the position index 0 (index = 0, length = 0).

The fix consists of relaxing the condition, and introducing an
optimisation. If we insert at the end, we do a simple push (like an
append). If we insert at another position, we split, then push the new
items, and finally push the detached items (as it was the case before).

The tests have been updated accordingly.
2024-03-29 11:43:25 +01:00
Benjamin Bouvier 1c1053afe6 event cache: internalize handling of the account data
(By moving handling of the fully read marker into the event cache
itself.)
2024-03-28 17:59:43 +01:00
Andy Balaam 7ac153fd67 Merge pull request #3282 from matrix-org/andybalaam/memorystore-tracked-users-map
memorystore: Fix bug where duplicate tracked users are stored
2024-03-28 16:33:49 +00:00
Andy Balaam 31131146a6 memorystore: Fix bug where duplicate tracked users are stored 2024-03-28 16:19:23 +00:00
Ivan Enderlin ac0bc95c25 feat(sdk): EventCache fully uses RoomEvents/LinkedChunk
feat(sdk): `EventCache` fully uses `RoomEvents`/`LinkedChunk`
2024-03-27 10:23:42 +01:00
Ivan Enderlin 11c3799fa2 doc(sdk): Improve doc of EventCache. 2024-03-27 09:39:22 +01:00
Ivan Enderlin f11cf87326 fix(sdk): Replace unwrap by expect and add SAFETY docs. 2024-03-27 09:35:45 +01:00
Ivan Enderlin 7b092fd174 doc(sdk): Improve documentation of EventCacheInner::multiple_room_updates_lock. 2024-03-27 09:32:33 +01:00
Ivan Enderlin 1fa4bb4cfa chore(sdk): Remove commented code. 2024-03-27 09:32:33 +01:00
Ivan Enderlin dabc7c512c !revert test changes 2024-03-27 09:32:33 +01:00
Ivan Enderlin 8fe27ab582 doc(sdk): Improve documentation of RoomEvents::replace_gap_at. 2024-03-27 09:32:33 +01:00
Ivan Enderlin 8c1d3f4f60 feat(sdk): RoomEventCacheInner::backpaginate always return Ok for unknown token.
Prior to this patch, in `RoomEventCacheInner::backpaginate`, when the
`token` validity was checked, and it was invalid:

* before calling `/messages`, `Err(EventCacheError::UnknownBackpaginationToken)` was returned,
* after calling `/messages`, `Ok(BackPaginationOutput::UnknownBackpaginationToken)` was returned.

This patch tries to uniformize this by only returning
`Ok(BackPaginationOutput::UnknownBackpaginationToken)`.

That's a tradeoff. It will probably be refactor later.

The idea is also to call `/messages` **before** taking the write-lock
of `RoomEvents`, otherwise it can keep the lock for up to 30secs in
this case. Also, checking the validity of the `token` **before** and
**after** `/messages` is not necessary: it can be done only after.
2024-03-27 09:32:33 +01:00
Ivan Enderlin a623215257 chore(sdk): Make Clippy happy. 2024-03-27 09:32:33 +01:00
Ivan Enderlin f61de718b8 fix(sdk): Fix a race-condition in EventCache.
This patch ensures that operations on `RoomEvents` happen in one block,
by sharing the same lock.

2 new methods are created: `replace_all_events_by` and
`append_new_events`.
2024-03-27 09:32:33 +01:00
Ivan Enderlin fa5bbadf57 doc(sdk): Just highlight how important this lock is. 2024-03-27 09:32:15 +01:00
Ivan Enderlin 9319f4fcff test(sdk): Fix test_reset_while_backpaginating.
The test `test_reset_while_backpaginating` was expecting a
race-condition, which no longer exists. It first initially tried to
assert a workaround about this race-condition. It doesn't hold anymore.
Rewrite the test to assert the (correct) new behaviour.
2024-03-27 09:32:15 +01:00
Ivan Enderlin 25fb9ee47d feat(sdk): Update RoomEvents::replace_gap_at to return a &Chunk. 2024-03-27 09:32:15 +01:00
Ivan Enderlin 85538dc3ed feat(sdk): Remove EventCacheStore, TimelineEntry, RoomInfo and MemoryStore. 2024-03-27 09:23:51 +01:00
Ivan Enderlin 5bb2511914 test(sdk): Tests of EventCache uses RoomEvents. 2024-03-27 09:23:25 +01:00
Ivan Enderlin 667ada88e6 feat(sdk): RoomEventCache::subscribe uses RoomEvents. 2024-03-27 09:23:05 +01:00
Ivan Enderlin 022b8a0f38 feat(sdk): EventCache::listen_task uses RoomEvents. 2024-03-27 09:22:43 +01:00
Ivan Enderlin 29caa02ef0 feat(sdk): EventCache::add_initial_events uses RoomEvents. 2024-03-27 09:22:13 +01:00
Ivan Enderlin 9102a9c841 feat(sdk): RoomEventCachecacherInner::oldest_backpagination_token uses RoomEvents. 2024-03-27 09:21:05 +01:00
Stefan Ceriu 99e47ed5d7 fix: Process profiles for all room members not only state event senders (#3278) 2024-03-27 09:13:18 +01:00
Ivan Enderlin 54729ce32b feat(sdk): Start disabling the global store in EventCache. 2024-03-27 09:11:15 +01:00
Ivan Enderlin 36e199c31e feat(sdk): Implement RoomEvents::reset, push_gap, replace_gap_at and events.
This patch implements the following wrapper methods (over
`LinkedChunk`): `push_gap`, `replace_gap_at` and `events`. This patch
also implements the `reset` method that clears/drops all chunks in the
`LinkedChunk`.
2024-03-27 09:07:52 +01:00
Richard van der Hoff ab9e4f73b1 crypto: Add OlmMachine::device_creation_time (#3275)
Turns out this is useful for https://github.com/element-hq/element-meta/issues/2313.
2024-03-26 15:13:55 +00:00
Benjamin Bouvier ce7143b833 integration tests: rewrite test_toggling_reaction so it syncs in the background 2024-03-25 18:03:18 +01:00
Benjamin Bouvier 9480450410 integration tests: attempt to fix test_toggling_reaction
There was a message sent, *then* an attempt to wait for the remote echo later. It's not ideal, because if the time setting up the waiting is high enough, and the server is fast
enough, the remote echo could come *before* we started waiting for it, resulting in timeouts. This fixes it by spawning the waiting task first, and then only sending the message.
Let's see how this helps with this test.
2024-03-25 18:03:18 +01:00
Benjamin Bouvier 4744a994b4 integration tests: enhance testing of test_room_notification_count
This adds additional checks for each room updates, and works around a few race conditions, notably one where the server would send a remote echo for a message, but not update the
computed unread_notification_counts immediately. This tends to make the test more stable, in that each response is well known and now properly tested.
2024-03-25 18:03:18 +01:00
Benjamin Bouvier 1fd5b34fd0 ci: add more logs for test_toggling_reaction 2024-03-25 18:03:18 +01:00
Benjamin Bouvier 1255027d6e git: ignore the code coverage report from the output 2024-03-25 18:03:18 +01:00
Benjamin Bouvier baac38fec5 day dividers: add test for redundant day divider before a read marker 2024-03-25 14:48:07 +01:00
Benjamin Bouvier 94c0322fbe day dividers: soften assertions
It's not worth panic'ing the whole timeline because we removed the wrong item; worst case, users will complain and can send a rageshake that contains all the information that's
needed to debug what went wrong.
2024-03-25 14:48:07 +01:00
Benjamin Bouvier e9a4389a12 day dividers: don't assume the previous item is at the immediate previous position
There could be situations where we have a day divider, then a read marker, then an event. In that case, when looking at the event, if the previous day divider is wrong and needs
to be removed, we would assume the "previous item" (= the day divider) was at position i-1, which could be that of the read marker, and we'd remove the read marker instead of the
day divider.
2024-03-25 14:48:07 +01:00
Andy Balaam 67615fec3a Merge pull request #3252 from matrix-org/andybalaam/run-integration-tests-for-memorystore
crypto: Run the crypto integration tests against MemoryStore
2024-03-25 12:23:52 +00:00
Thomas 95a471b0d2 ffi: Expose discovered sliding sync proxy URL (#3266)
Closes #3265.

There is currently no way to get the URL of a homeserver's sliding sync proxy before logging in on the homeserver.

I suggest exposing the URL via the `HomeserverLoginDetail` struct after configuring the homeserver (through `configure_homeserver`).

Since the homeserver may not declare a corresponding sliding sync proxy, this value is an `Optional`. It is used later in `configure_homeserver` to check if a sliding sync proxy exists and throws an error otherwise. Previously, this check was done against the client's `discovered_sliding_sync_proxy` function.

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

Signed-off-by: Thomas Völkl <thomas@vollkorntomate.de>
Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-03-22 17:21:24 +00:00
Benjamin Bouvier 56f4b3e70e day divider: don't assume events have event id
Local echoes (which haven't received a remote echo yet) can have no event id, so when computing the report, don't unwrap the event id but use a sensible
default instead.

Also tweaks comments from a previous version of another PR. And rename `DayDividerAdjuster::maybe_adjust_day_dividers` to `run`.
2024-03-22 17:00:33 +01:00
Benjamin Bouvier 6aee1f62bd day divider: make it impossible to handle an event without adjusting day dividers (#3267)
The previous API relied on the callers not forgetting about adjusting day dividers after handling an event.

This makes it statically impossible, by requiring that `TimelineEventHandler` takes a `&mut DayDividerAdjuster` when operating, that it marks as not "consumed". Later, the caller must call `DayDividerAdjuster::maybe_adjust_day_dividers()`, to mark it as consumed. When dropping, we check that it's been consumed, otherwise we panic — as it's a developer error to not call `maybe_adjust_day_dividers()`.
2024-03-22 12:14:28 +00:00
Hubert Chathi 8d968604e9 chore: Update Ruma to version that uses web-time crate (#3264) 2024-03-22 12:30:19 +01:00
Andy Balaam 3a7b8fc6a5 crypto: Run the crypto integration tests against MemoryStore 2024-03-22 09:58:13 +00:00
Andy Balaam 7de5d295b6 crypto: Enable testing module in test mode 2024-03-22 09:58:13 +00:00
Andy Balaam 6b394d96bd crypto: Formatting for integration_tests 2024-03-22 09:58:13 +00:00
Richard van der Hoff 97959bbcd0 crypto: Log details about olm session after encryption/decryption (#3242) 2024-03-22 09:43:35 +00:00
Benjamin Bouvier 2883685bcc day divider: adjust instrument macro
also fix two "date dividers" instances
2024-03-22 10:24:57 +01:00
Benjamin Bouvier eef61f87c1 day divider: move code to a new file
Only code motion, no changes in functionality.
2024-03-22 10:24:57 +01:00
Benjamin Bouvier 2c9a088a36 day divider: add test for duplicate date header after matching two local echoes
I checked that the test failed on main, by causing a final state of [DD First DD Second].

Fixes #2590.
2024-03-22 10:24:57 +01:00
Benjamin Bouvier 8912761eb7 day divider algorithm: address review comments
Notably, split the code into smaller functions, and revamp the high-level signatures so the individual handle_ functions don't take a bazillion arguments.
2024-03-22 10:24:57 +01:00
Benjamin Bouvier 3323f37efc day divider: beef up the reports
The reports now include the final state as well as the set of operations to run, so we can really debug all the steps just from looking at a rageshake.
2024-03-22 10:24:57 +01:00
Benjamin Bouvier 515aaf0a8a day divider: fix offset computation
When we're removing or inserting any day divider, we're also updating the offset, so that subsequent operations happen at the right positions.
The previous code made the error to clamp the offset when assigning it, instead of letting it be "out of bounds" and clamping the uses, which is
the correct way to implement this.
2024-03-22 10:24:57 +01:00
Benjamin Bouvier c172ad9191 day divider: only warn if some invariants are broken, in non-debug mode 2024-03-22 10:24:57 +01:00
Benjamin Bouvier 601dce76ef timeline: add instrumentation for the maybe_adjust_day_dividers function 2024-03-22 10:24:57 +01:00
Benjamin Bouvier f704066fbe timeline: group updates of the day dividers when multiple events are added at the same time 2024-03-22 10:24:57 +01:00
Benjamin Bouvier b8174c437f timeline: move the day divider adjusting code into its own data structure 2024-03-22 10:24:57 +01:00
Benjamin Bouvier a7cda30f6a timeline: use push_{front,back} semantics for both messages and day dividers 2024-03-22 10:24:57 +01:00
Benjamin Bouvier 88cd2557f3 timeline: rework the day divider separation as a post-processing algorithm 2024-03-22 10:24:57 +01:00
Benjamin Bouvier bd33c336e7 tests: try bumping the timeout duration in test_room_notification_count
The test has been failing with a timeout recently, several time. Let's see if it was a fluke caused by the low threshold (because the server might be
busy handling other requests from other tests), or an actual issue.
2024-03-21 20:15:05 +01:00
Richard van der Hoff 82bcf48c88 Enable debuginfo for tarpaulin builds
It appears that tarpaulin complains if the symbol information is stripped from
the binary, and as of Rust 1.77, `debug=0` causes Cargo to strip all debug
info.

To fix this, set `debug=1`.
2024-03-21 17:46:30 +01:00
Ivan Enderlin daaf17198c Merge pull request #3257 from Hywan/fix-issue-3213
doc(crypto-ffi): `Device::first_time_seen_ts` has an incorrect unit
2024-03-21 16:24:03 +01:00
Andy Balaam fe39ca47d6 Merge pull request #3223 from matrix-org/andybalaam/adjust-integration-tests
crypto: Refactor integration tests in preparation for them running against MemoryStore
2024-03-21 13:15:45 +00:00
Ivan Enderlin f42c8937da feat(sdk): Improve ChunkIdentifierGenerator
feat(sdk): Improve `ChunkIdentifierGenerator`
2024-03-21 12:56:16 +01:00
Ivan Enderlin 199275ff89 feat(sdk): Rename ChunkIdentifierGenerator::generate_next to next.
This patch renames `ChunkIdentifierGenerator::generate_next` to `next.

This patch also simplifies a `.saturating_add(1)` to a simple `+ 1`,
which is fine because we have checked for overflow just before.
2024-03-21 12:39:18 +01:00
Ivan Enderlin 7df31406dc feat(bindings): added join room by id to ffi
feat(bindings): added join room by id to ffi
2024-03-21 12:18:33 +01:00
Ivan Enderlin 1edfc6cb5e Merge pull request #3261 from matrix-org/kegan/arc-uniffi
uniffi: wrap TaskHandle up in an Arc<>
2024-03-21 12:10:36 +01:00
Ivan Enderlin d4c1b9b8ad chore: Avoid importing types redundantly
chore: Avoid importing types redundantly
2024-03-21 11:56:02 +01:00
Mauro Romito d447f63e33 fixed invalid parsing 2024-03-21 11:53:44 +01:00
Kegan Dougal c13fb7e19f uniffi: wrap TaskHandle up in an Arc<>
Up until uniffi 0.26 it was not possible to send objects
across the boundary unless they were wrapped in an `Arc<>`,
see https://github.com/mozilla/uniffi-rs/pull/1672

The bindings generator used in complement-crypto only supports
up to uniffi 0.25, meaning having a function which returns objects
ends up erroring with:
```
error[E0277]: the trait bound `TaskHandle: LowerReturn<UniFfiTag>` is not satisfied
   --> bindings/matrix-sdk-ffi/src/room_directory_search.rs:109:10
    |
109 |     ) -> TaskHandle {
    |          ^^^^^^^^^^ the trait `LowerReturn<UniFfiTag>` is not implemented for `TaskHandle`
    |
    = help: the following other types implement trait `LowerReturn<UT>`:
              <bool as LowerReturn<UT>>
              <i8 as LowerReturn<UT>>
              <i16 as LowerReturn<UT>>
              <i32 as LowerReturn<UT>>
              <i64 as LowerReturn<UT>>
              <u8 as LowerReturn<UT>>
              <u16 as LowerReturn<UT>>
              <u32 as LowerReturn<UT>>
            and 133 others

error[E0277]: the trait bound `TaskHandle: LowerReturn<_>` is not satisfied
   --> bindings/matrix-sdk-ffi/src/room_directory_search.rs:82:1
    |
82  | #[uniffi::export(async_runtime = "tokio")]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `LowerReturn<_>` is not implemented for `TaskHandle`
    |
    = help: the following other types implement trait `LowerReturn<UT>`:
              <bool as LowerReturn<UT>>
              <i8 as LowerReturn<UT>>
              <i16 as LowerReturn<UT>>
              <i32 as LowerReturn<UT>>
              <i64 as LowerReturn<UT>>
              <u8 as LowerReturn<UT>>
              <u16 as LowerReturn<UT>>
              <u32 as LowerReturn<UT>>
            and 133 others
```

This PR wraps the offending function in an `Arc<>` to make it uniffi 0.25 compatible,
which unbreaks complement crypto.
2024-03-21 10:40:37 +00:00
Mauro Romito 17805cbcd8 feat(bindings): added join room by id to ffi 2024-03-21 11:39:04 +01:00
hanadi92 d2c9ca455d docs: update copyright
Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2024-03-21 09:53:43 +01:00
hanadi92 36c39b837a refactor: create a pusher manager to set and delete
Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2024-03-21 09:53:43 +01:00
hanadi92 b83a644260 fix: use async instead of block on runtime
Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2024-03-21 09:53:43 +01:00
hanadi92 7c4d180297 ffi: add delete_pusher method
Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2024-03-21 09:53:43 +01:00
hanadi92 b2e7ae4310 sdk: add delete_pusher method
Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
2024-03-21 09:53:43 +01:00
Ivan Enderlin 5d5a3044c8 chore: Avoid importing types redundantly.
`TryFrom` and `TryInto` are imported redundantly. They are already
defined in `core::prelude::rust_2021` which is automatically imported.
This is generating warnings on my side. This patch fixes that.
2024-03-21 09:14:56 +01:00
Ivan Enderlin ae170362a5 doc(crypto-ffi): Device::first_time_seen_ts has an incorrect unit.
This patch changes seconds to milliseconds for the description of
`Device::first_time_seen_ts`.
2024-03-21 09:05:30 +01:00
Ivan Enderlin 01c5412951 feat(sdk): ChunkIdentifierGenerator::generate_next panics.
This patch makes `ChunkIdentifierGenerator::generate_next` to panic
if there is no more identifiers available. It was previously returning
a `Result` but we were doing nothing with this `Result` except
`unwrap`ping it. To simplify the API: let's panic.
2024-03-20 21:14:51 +01:00
Ivan Enderlin ab2b5bfa23 feat(sdk): Remove AtomicU64::load in ChunkIdentifierGenerator.
As suggested in https://github.com/matrix-org/matrix-rust-sdk/
pull/3251#discussion_r1532103818 by Poljar, it is possible that the
value of the atomic changes between the `fetch_add` and the `load` (if
and only if it is used in a concurrency model, which is not the case
right now, but anyway… better being correct now!). The idea is not
`load` but repeat the addition manually to compute the “current” value.
2024-03-20 21:08:01 +01:00
Ivan Enderlin c120da79d1 feat(sdk): Optimise how LinkedChunk::insert_gap_at works when inserting at first position
feat(sdk): Optimise how `LinkedChunk::insert_gap_at` works when inserting at first position
2024-03-20 17:22:22 +01:00
Ivan Enderlin 962c0bf4fd doc(sdk): Improve SAFETY paragraphs, and replace unwraps by expects. 2024-03-20 15:57:04 +01:00
Ivan Enderlin 57b68614af doc(sdk): US vs UK strike again. 2024-03-20 15:50:51 +01:00
Ivan Enderlin 8eafaa58fb Merge pull request #3169 from matrix-org/mauroromito/directory_search
Room Directory Search
2024-03-20 15:43:18 +01:00
Ivan Enderlin 96c7b3fc52 doc(sdk): Add a warning.
Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2024-03-20 15:28:24 +01:00
Ivan Enderlin 0a02a41a14 doc(sdk): Fix mark up.
Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2024-03-20 15:28:03 +01:00
Ivan Enderlin a248ec75e2 feat(sdk): Optimise how insert_gap_at works when inserting at first position.
Imagine we have this linked chunk:

```rust
assert_items_eq!(linked_chunk, ['a'] [-] ['b', 'c'] ['d', 'e', 'f']);
```

Before the patch, when we were running:

```rust
let position_of_d = linked_chunk.item_position(|item| *item == 'd').unwrap();
linked_chunk.insert_gap_at((), position_of_d)?;
```

it was taking `['d', 'e', 'f']` to split it at index 0, so `[]` + `['d',
'e', 'f']`, and then was inserting a gap in between, thus resulting
into:

```rust
assert_items_eq!(linked_chunk, ['a'] [-] ['b', 'c'] [] [-] ['d', 'e', 'f']);
```

The problem is that it creates a useless empty chunk. It's a waste of
space, and rapidly, of computation. When used with `EventCache`, this
problem occurs every time a backpagination occurs (because it executes
a `replace_gap_at` to insert the new item, and then executes a
`insert_gap_at` if the backpagination contains another `prev_batch`
token).

With this patch, the result of the operation is now:

```rust
assert_items_eq!(linked_chunk, ['a'] [-] ['b', 'c'] [-] ['d', 'e', 'f']);
```

The `assert_items_eq!` macro has been updated to be able to assert
empty chunks. The `test_insert_gap_at` has been updated to test all
edge cases.
2024-03-20 13:47:53 +01:00
Ivan Enderlin 3aa0a905b2 feat(sdk): LinkedChunk::replace_gap_at returns &Chunk. 2024-03-20 13:47:53 +01:00
Damir Jelić 44443d3b46 crypto: Mac then decrypt in the PkDecryption compat module 2024-03-20 10:34:51 +01:00
Andy Balaam 40d96dbf27 crypto: Refactor integration tests in preparation for them running against MemoryStore
A couple of small changes that allow us to drop locks that would
otherwise persist when running the tests over the MemoryStore, and some
documentation comments for assertions to make it easier to spot which
assertion failed, even though we are inside a macro.
2024-03-20 09:10:02 +00:00
Andy Balaam b2dc21d7d0 Merge pull request #3222 from matrix-org/andybalaam/store-private-identity-in-memorystore
crypto: Save private identity in the MemoryStore
2024-03-20 09:09:22 +00:00
Ivan Enderlin fa5ce1d462 fix(sdk): Various tiny improvements for LinkedChunk
fix(sdk): Various tiny improvements for `LinkedChunk`
2024-03-19 20:41:23 +01:00
Ivan Enderlin e264482954 feat(sdk): Rename ItemPosition to Position.
The “position” can be placed inside a `Gap`, so naming it `Item…` is a
non-sense. This patch removes the `Item` prefix.
2024-03-19 20:22:08 +01:00
Matthias Grandl a1c1b0e157 timeline: make room() public (#3248) 2024-03-19 18:30:10 +01:00
Valere 3ac123db29 crypto: fix BackedUpRoomKey Serialization 2024-03-19 17:10:36 +01:00
Andy Balaam 9caec95c5e crypto: Save private identity in the MemoryStore 2024-03-19 15:36:08 +00:00
Andy Balaam 4bdcedbc66 Merge pull request #3221 from matrix-org/andybalaam/store-trackedusers-in-memorystore
crypto: Store TrackedUsers in MemoryStore
2024-03-19 15:29:13 +00:00
Damir Jelić 3ccd2e9f8f crypto: Remove the KeysBackup variant of the OutgoingRequest enum
Backing up room keys isn't part of the outgoing requests processing
loop, instead the user is supposed to have a separate loop calling
`BackupMachine::backup()`.
2024-03-19 16:23:13 +01:00
Andy Balaam 099c855049 crypto: Store TrackedUsers in MemoryStore 2024-03-19 15:16:53 +00:00
Hubert Chathi 1e35188aec Add a dehydrated flag to device_keys of dehydrated devices (#3164)
Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-03-19 14:33:13 +00:00
Benjamin Bouvier c6da40cf55 ffi: bump opentelemetry crates
This gets rid of multiple duplicate crates in the dependency tree.
2024-03-19 14:21:59 +01:00
Kévin Commaille 0ff9e066fb sdk: Don't enable default features of mas-oidc-client
We don't need the `hyper` feature as we use our own HTTP client,
and the `keystore` will not be used in most cases.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-03-19 14:01:53 +01:00
Damir Jelić c59465c54c crypto: Rotate fallback keys in a time-based manner (#3151)
Fallback keys until now have been rotated on the basis that the
homeserver tells us that a fallback key has been used.

Now this leads to various problems if the server tells us too often that
it has been used, i.e. if we receive the same sync response too often.
It leaves us also open to the homeserver being dishonest and never
telling us that the fallback key has been used.

Let's avoid all these problems by just rotating the fallback key in a
time-based manner.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2024-03-19 13:16:37 +01:00
Jorge Martin Espinosa 0c4b62f664 sdk: move get_profile from Client to Account (#3238)
This also renames and streamlines the existing `Account::get_profile` function to `Account::fetch_profile` which now calls the more general function.
2024-03-19 12:03:32 +00:00
Andy Balaam 8c2831a5da Merge pull request #3220 from matrix-org/andybalaam/save-outbound-sessions-in-memorystore
crypto: Save outbound sessions in MemoryStore
2024-03-19 11:38:59 +00:00
依云 4e8cee162a sdk: make content of RawEvent public (#3239)
This way it can be moved out and converted to other types like ruma::Serde::Raw.

Signed-off-by: lilydjwg <lilydjwg@gmail.com>
2024-03-19 11:28:34 +00:00
Andy Balaam 1e11ac406f crypto: Use a BTreeMap for MemoryStores' OutboundGroupSessions 2024-03-19 11:26:20 +00:00
Marco Antonio Alvarez 10069fbead MSC2530: added the ability to send media with captions (#3226)
Now that there is some support for [MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530), I gave adding sending captions a try. ( This is my first time with Rust 😄  )

I tried it on Element X with a hardcoded caption and it seems to work well
![image](https://github.com/matrix-org/matrix-rust-sdk/assets/683652/597e5ebf-f7f2-498f-97a4-ac98613c1134)

(It even got forwarded through mautrix-whatsapp and the caption was visible on the Whatsapp side)

---

* ffi: Expose filename and formatted body fields for media captions

In relevance to MSC2530

* MSC2530: added the ability to send media with captions

Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com>

* signoff

Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com>

* fixing the import messup

* fix missing parameters in documentation

* fix formatting

* move optional parameters to the end

* more formatting fixes

* more formatting fixes

* rename url parameter to filename in send_attachment and helpers

* fix send_attachment documentation example

* move caption and formatted_caption into attachmentconfig

* fix formatting

* fix formatting

* fix formatting (hopefully the last one)

* updated stale comments

* simplify attachment message comments

---------

Signed-off-by: Marco Antonio Alvarez <surakin@gmail.com>
Co-authored-by: SpiritCroc <dev@spiritcroc.de>
2024-03-19 11:08:47 +01:00
Andy Balaam 56da4a31a4 Merge pull request #3231 from matrix-org/andybalaam/wiremock-as-workspace-dep
build: Make wiremock a workspace dependency
2024-03-19 10:04:04 +00:00
Ivan Enderlin ffacbe8666 feat(sdk): Reverse the indices order of ItemPosition.
Previously, in a chunk with items `a`, `b` and `c`, their indices were
2, 1 and 0. It creates a problem when we push new items: all indices
must be shifted to the left inside the same chunk. That's not optimised.
This patch reverses the order, thus now `a` has index 0, `b` has index 1
and `c` has index 2.

It also removes a possible bug where we use `item_index` without
“reversing” it. This is now obvious that we don't need to re-compute the
`item_index`, we can use it directly.
2024-03-19 10:40:36 +01:00
Andy Balaam ca13be020c build: Make wiremock a workspace dependency 2024-03-19 09:39:31 +00:00
Samy Djemaï baf97c69b1 chore: upgrade uniffi-rs to latest commit
Signed-off-by: Samy Djemaï <53857555+SamyDjemai@users.noreply.github.com>
2024-03-19 10:37:26 +01:00
Ivan Enderlin 628374b8d8 feat(sdk): Optimise LinkedChunk iterators.
This patch optimises `LinkedChunk::rchunks` and `chunks` by _not_ using
`rchunks_from` and `chunks_from`. Indeed, it's faster to not call the
inner `chunk` method + `unwrap`ping the result and so on. Just a tiny
optimisation.

This patch also uses the new `Chunk::last_item_position` method for
`LinkedChunk::items`. Abstraction for the win!
2024-03-19 10:11:58 +01:00
Ivan Enderlin 454d49aa64 feat(sdk): Add Chunk::first_item_position and ::last_item_position.
This patch implements `Chunk::first_item_position` and
`Chunk::last_item_position` as a way to _position_ the
“cursor” (`ItemPosition`) in the correct way when we want to do some
particular insertion (at the beginning or at the end of a chunk).
2024-03-19 10:05:12 +01:00
Ivan Enderlin 06e212c11d !fix rebase issue 2024-03-19 09:48:28 +01:00
Ivan Enderlin 213dac2d30 feat(sdk): Rewrite LinkedChunk::replace_gap_at.
This patch rewrites `LinkedChunk::replace_gap_at`. Instead of inserting
new items on the _previous_ chunk of the gap and doing all the stuff
here, a new items chunk is created _after_ the gap (where items are
pushed), to finally unlink and remove the gap.

First off, it removes an `unwrap`. Second, if the previous chunk was
an items chunk, and had free space, the items would have been added in
there, which is not the intended behaviour. The tests have been updated
accordingly.
2024-03-19 09:48:28 +01:00
Ivan Enderlin 6b754acd81 feat(sdk): Add Chunk::as_ptr.
This patch adds the new `Chunk::as_ptr` method. It simplifies the code a
little bit.
2024-03-19 09:39:20 +01:00
Ivan Enderlin 88f75a85bb feat(sdk): Chunk::is_gap and ::is_items are now public. 2024-03-19 09:39:09 +01:00
Ivan Enderlin a8e522c164 feat(sdk): Implement LinkedChunk::chunks.
This patch implements the `LinkedChunk::chunks` method that returns a
forward iterator over the chunks.
2024-03-19 09:39:01 +01:00
Ivan Enderlin 9dcab4ed30 feat(sdk): ItemPosition has the copy semantics.
This patch implements `Copy` and `Clone` for `ItemPosition`.
2024-03-19 09:38:42 +01:00
Ivan Enderlin 4774cc8e65 feat(sdk): Implement Chunk::content.
This patch implements `Chunk::content` to get an immutable reference to
the content of a chunk.
2024-03-19 09:38:33 +01:00
Ivan Enderlin 9c4318d191 feat(sdk): Convert ChunkIdentifier to ItemPosition.
This patch implements `From<ChunkIdentifier>` for `ItemPosition`.
It's useful when we get a `ChunkIdentifier` and we need to `insert_…
_at(item_position)`.
2024-03-19 09:38:20 +01:00
Ivan Enderlin 2bb07d6a4e feat(sdk): Implement LinkedChunk::items.
This patch implements the new `LinkedChunk::items` method that returns
a forward iterator over items.
2024-03-19 09:38:06 +01:00
Ivan Enderlin 44029009e4 feat(sdk): Implement ChunkIdentifier::to_last_item_position.
This patch is about an internal thing, but it makes the code easier
to understand.
2024-03-19 09:37:52 +01:00
Andy Balaam b2c96b72b0 ci: Add a CI workflow to verify the minimum supported Rust version 2024-03-18 18:39:07 +01:00
Andy Balaam 9d281937d5 build: Add a missing dependency on wiremock even when testing feature is not enabled 2024-03-18 18:39:07 +01:00
Benjamin Bouvier 818a435f9e event cache: rename backpaginate_with_token to backpaginate 2024-03-18 17:02:05 +01:00
Andy Balaam 486b6d6e2b crypto: Save outbound sessions in MemoryStore 2024-03-18 15:21:41 +00:00
Andy Balaam 32edfb1a9f Merge pull request #3219 from matrix-org/andybalaam/fix-warnings-in-integration-tests
crypto: Fix warnings in integration_tests.rs (and a tiny bug)
2024-03-18 15:11:53 +00:00
Andy Balaam 9159a5983b crypto: Fix typo bug in integration tests 2024-03-18 14:54:59 +00:00
Andy Balaam 69ac7e07e6 crypto: Fix warnings in integration tests 2024-03-18 14:54:59 +00:00
Andy Balaam ee23839259 crypto: Remove unused imports from integration_tests.rs
The warnings were hidden because no-one within this crate used this macro.
2024-03-18 14:54:59 +00:00
Ivan Enderlin 555dfe0e77 feat(sdk): LinkedChunk can hold a value for Gaps
feat(sdk): `LinkedChunk` can hold a value for `Gap`s
2024-03-18 12:50:25 +01:00
Ivan Enderlin 7f7d9b8175 chore(sdk): Rename T and U in LinkedChunk.
This patch renames the generic parameters `T` and `U` to `Item` and
`Gap` for the `LinkedChunk` type and siblings.
2024-03-18 12:36:51 +01:00
Benjamin Bouvier 57f6715784 timeline: get rid of the update_timeline_item! macro and replace it with function calls 2024-03-18 12:36:35 +01:00
Benjamin Bouvier b587c064d7 timeline: prefix more tests with test_ 2024-03-18 12:36:35 +01:00
Benjamin Bouvier 52dc64e0db timeline: add doc comments here and there 2024-03-18 12:36:35 +01:00
Andy Balaam 7b7ee980e8 build: Update minimum supported Rust version to 1.76
This reflects the reality of the situation at the moment: we need 1.76.
to compile, and Ruma requires 1.75.
2024-03-18 11:45:02 +01:00
Ivan Enderlin 5591be9a8e feat(sdk): LinkedChunk can hold a value for Gaps.
This patch updates `ChunkContent::Gap` to hold a content `U`. Thus,
`Chunk` and LinkedChunk` both get a new generic parameter `U`. Some
methods like `new_gap` or `insert_gap_at` take a new `content: U`
parameter.

This type `RoomEvents` (that uses `LinkedChunk`) is also updated
accordingly.
2024-03-18 10:52:46 +01:00
Kévin Commaille 876d3237eb sdk: Check if server name points to homeserver during discovery (#3218)
The small first commit reintroduces `sanitize_server_name` in the public API. In Fractal, we use it to validate the string in the input before allowing the user to trigger the discovery.

The main commit changes a bit the discovery process: before, server names like `example.org` would only be checked for the presence of a well-known, and only URLs like `https://example.org` would be checked as a homeserver. Now, providing `example.org` will also check if `https://example.org` is the URL of a homeserver.

Sadly I don't think it's possible to add tests for it as it would require to have a homeserver accessible via HTTPS.

---

* sdk: Restore sanitize_server_name in the public API

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* sdk: Check if a provided server name points to a homeserver during discovery

Before, only URLs like `https://example.org` would be checked as a homeserver.
Providing `example.org` will check if `https://example.org` is the URL of a homeserver.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Refactor to avoid duplication

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-03-15 19:18:33 +00:00
Mauro a308d34d09 Merge branch 'main' into mauroromito/directory_search 2024-03-15 15:08:16 +01:00
Benjamin Bouvier cabab289c9 timeline: get rid of ManuallyDrop in the TimelineInnerStateTransaction 2024-03-15 15:08:10 +01:00
Benjamin Bouvier a98779dfbb timeline: use u64 for all the fields of PaginationOutcome 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 182e84cd3d timeline: get rid of deref/deref_mut from TimelineInnerState to TimelineInnerMetadata 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 5c049d6e2e timeline: rename handle_joined_room_update to handle_sync_events 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 057bca070c timeline: get rid of the synthetic Timeline and JoinedRoomUpdate when updating the timeline 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 75871216d2 timeline: sanitize usage of meta in the TimelineInnerStateTransaction
Before this patch, the meta field would be mutated, even when the transaction would be aborted. This changes the update scheme to meta
with the following:

- when creating the transaction, clone the meta (but keep the pointer location to the previous one)
- all the transaction's methods operate on the WIP meta
- when committing, replace the previous meta with the current one
2024-03-15 15:08:10 +01:00
Benjamin Bouvier 4661ca810a timeline: get rid of deref/deref_mut from TimelineInnerStateTransaction to TimelineInnerMetadata 2024-03-15 15:08:10 +01:00
Benjamin Bouvier f3687dc4c7 timeline: don't return the unused event id in handle_remote_event 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 694fb57c17 timeline: lower the number of methods to add events 2024-03-15 15:08:10 +01:00
Benjamin Bouvier e1b9fe266d timeline: prefix a few tests with test_ 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 35a1596755 timeline: change number of added/updated items to u64 in `HandleManyEventsResult
u64 should be enough for everyone?
2024-03-15 15:08:10 +01:00
Benjamin Bouvier 117307eaff timeline: inline TimelineInnerStateTransaction::handle_live_event 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 52a5a86cf9 event cache: important status update 2024-03-15 15:08:10 +01:00
Benjamin Bouvier 9faa839f56 event cache: don't return the prev_batch token anymore \o/
It's now the event cache's responsibility to handle back-pagination, so the timeline doesn't have to do it anymore.
2024-03-15 15:08:10 +01:00
bitfriend 5f960d889e Append the missed cancel codes 2024-03-15 11:52:08 +01:00
Mauro 655ac0725c Merge branch 'main' into mauroromito/directory_search 2024-03-14 17:56:23 +01:00
Mauro Romito 229105536b docs: updated docs 2024-03-14 17:55:54 +01:00
Mauro Romito 4b85a81347 docs: warning about NSFW content 2024-03-14 16:53:18 +01:00
Richard van der Hoff a328d8787a crypto: Log errors from Olm decryption (#3212)
When we fail to decrypt an olm message, it is useful to know *why* it
failed. Include the details of the failures in the warning message.
2024-03-14 15:22:46 +00:00
Benjamin Bouvier d1e92ece42 timeline: move the back-pagination code into the timeline/pagination.rs file
No changes in behavior, just pure code motion.
2024-03-14 15:47:53 +01:00
Benjamin Bouvier ff4a8f0ba5 timeline: integrate the event cache pagination into the UI timeline 2024-03-14 15:47:53 +01:00
Ivan Enderlin 0a7e28f681 Merge pull request #3166 from Hywan/feat-sdk-event-cache-store-experimental
feat(sdk): Event cache experimental store
2024-03-14 15:27:22 +01:00
Ivan Enderlin e8cf6dcde6 doc(sdk): Update the CHANGELOG.md. 2024-03-14 15:09:22 +01:00
Ivan Enderlin 505fb682af feat(sdk): Introduce the LinkedChunk type.
This patch is a work-in-progress. It explores an experimental data
structure to store events in an efficient way.

Note: in this comment, I will use the term _store_ to mean _database_
or _storage_.

The biggest constraint is the following: events can be ordered in
multiple ways, either topological order, or sync order. The problem is
that, when syncing events (with `/sync`), or when fetching events (with
`/messages`), we **don't know** how to order the newly received events
compared to the already downloaded events. A reconciliation algorithm
must be written (see #3058). However, from the “storage” point of view,
events must be read, written and re-ordered efficiently.

The simplest approach would be to use an `order_index` for example.
Every time a new event is inserted, it uses the position of the last
event, increments it by one, and done.

However, inserting a new event in _the middle_ of existing events would
shift all events on one side of the insertion point: given `a`, `b`,
`c`, `d`, `e`, `f` with `f` being the most recent event, if `g` needs
to be inserted between `b` and `c`, then `c`, `d`, `e`, `f`'s ordering
positions need to be shifted. That's not optimal at all as it would
imply a lot of updates in the store.

Example of a relational database:

| ordering_index | event |
|----------------|-------|
| 0              | `a`   |
| 1              | `b`   |
| 2              | `g`   |
| 3              | `c`   |
| …              | …     |

An insertion can be O(n), and it can happen more frequently than one
can think of. Let's imagine a permalink to an old message: the user
opens it, a couple of events are fetched (with `/messages`), and these
events must be inserted in the store, thus potentially shifting a lot of
existing events. Another example: Imagine the SDK has a search API for
events; as long as no search result is found, the SDK will back-paginate
until reaching the beginning of the room; every time there is a
back-pagination, a block of events will be inserted: there is more and
more events to shift at each back-pagination.

OK, let's forget the `order_index`. Let's use a linked list then? Each
event has a _link_ to the _previous_ and to the _next_ event.

Inserting an event would be at worst O(3) in this case: if the previous
event exists, it must be updated, if the next event exists, it must be
updated, finally, insert the new event.

Example with a relational database:

| previous | id      | event | next    |
|----------|---------|-------|---------|
| null     | `id(a)` | `a`   | `id(b)` |
| `id(a)`  | `id(b)` | `b`   | `id(c)` |
| `id(b)`  | `id(c)` | `c`   | null    |

This approach ensures a fast _writing_, but a terribly slow _reading_.
Indeed, reading N events require N queries in the store. Events aren't
contiguous in the store, and cannot be ordered by the database engine
(e.g. with `ORDER BY` for SQL-based database). So it really requires one
query per event. That's a no-go.

In the two scenarios above, another problem arises. How to represent
a gap? Indeed, when new events are synced (via `/sync`), sometimes the
response contains a `limited` flag, which means that the results are
_partial_.

Let's take the following example: the store contains `a`, `b`, `c`.
After a long offline period (during which the room has been pretty
active), a sync is started, which provides the following events: `x`,
`y`, `z` + the _limited_ flag. The app is killed and reopened later.
The event cache store will contain `a`, `b`, `c`, `x`, `y`, `z`. How
do we know that there is a hole/a gap between `c` and `x`? This is an
important information! When `z`, `y` and `x` are displayed, and the user
would like to scroll up, the SDK must know that it must back-paginate
before providing `c`, `b` and `a`.

So the data structure we use must also represent gaps. This information
is also crucial for the events reconciliation algorithm.

What about a mix between the two? Here is _Linked Chunk_.

A _linked chunk_ is like a linked list, except that each node is either
a _Gap_ or an _Items_. A _Gap_ contains nothing, it's just a gap. An
_Items_ contains _several_ events. A node is called a _Chunk_. A _chunk_
has a maximum size, which is called a _capacity_. When a chunk is full,
a new chunk is created and linked appropriately. Inside a chunk, an
ordering index is used to order events. At this point, it becomes a
trade-off the find the appropriate chunk size to balance the performance
between reading and writing. Nonetheless, if the chunk size is 50, then
reading events is 50 times more efficient with a linked chunk than with
a linked list, and writing events is at worst O(49), compare to the O(n
- 1) of the ordering index.

Example with a relational database. First table is `events`, second
table is `chunks`.

| chunk id | index | event |
|----------|-------|-------|
| `$0`     | 0     | `a`   |
| `$0`     | 1     | `b`   |
| `$0`     | 2     | `c`   |
| `$0`     | 3     | `d`   |
| `$2`     | 0     | `e`   |
| `$2`     | 1     | `f`   |
| `$2`     | 2     | `g`   |
| `$2`     | 3     | `h`   |

| chunk id | type  | previous | next |
|----------|-------|----------|------|
| `$0`     | items | null     | `$1` |
| `$1`     | gap   | `$0`     | `$2` |
| `$2`     | items | `$1`     | null |

Reading the last chunk consists of reading all events where the
`chunk_id` is `$2` for example, and contains events `e`, `f`, `g` and
`h`. We can sort them easily by using the `event_index` column. The
previous chunk is a gap. The previous chunk contains events `a`, `b`,
`c` and `d`.

Being able to read events by chunk clearly limit the amount of reading
and writing in the store. It is also close to what will be really done
in real life with this store. It also allows to represent gaps. We can
replace a gap by new chunk pretty easily with few writings.

A summary:

| Data structure | Reading           | Writing         |
|----------------|-------------------|-----------------|
| Ordering index | “O(1)”[^1] (fast) | O(n - 1) (slow) |
| Linked list    | O(n) (slow)       | O(3) (fast)     |
| Linked chunk   | O(n / capacity)   | O(capacity - 1) |

This patch contains a draft implementation of a linked chunk. It will
strictly only contain the required API for the `EventCache`, understand
it _is not_ designed as a generic data structure type.

[^1]: O(1) because it's simply one query to run; the database engine
does the sorting for us in a very efficient way, particularly if the
`ordering_index` is an unsigned integer.
2024-03-14 15:09:22 +01:00
SpiritCroc 2520804a60 ffi: Expose filename and formatted body fields for media captions
In relevance to MSC2530
2024-03-14 14:43:03 +01:00
Benjamin Bouvier 73684ab57c ui/timeline: allow subscribing to UTDs and late-decrypt events (#3206)
This adds a new mechanism in the UI crate (since re-attempts to decrypt happen in the timeline, as of today — later that'll happen in the event cache) to notify whenever we run into a UTD (an event couldn't be decrypted) or a late-decryption event (after some time, a UTD could be decrypted).

This new hook will deduplicate pings for the same event (identifying events on their event id), and also implements an optional grace period. If an event was a UTD:

- if it's still a UTD after the grace period, then it's reported with a `None` `time_to_decrypt`,
- if it's not a UTD anymore (i.e. it's been decrypted in the meanwhile), then it's reported with a `time_to_decrypt` set to the time it took to decrypt the event (approximate, since it starts counting from the time the timeline receives it, not the time the SDK fails to decrypt it at first).

It's configurable as an optional hook on timeline builders. For the FFI, it's configurable at the sync service's level with a "delegate", and then the sync service will forward the hook to the timelines it creates, and the hook will forward the UTD info to the delegate.

Part of https://github.com/element-hq/element-meta/issues/2300.

---

* ui: add a new module and trait for subscribing to unable-to-decrypt notifications

and late decryptions (i.e. the key came in after the event that required it for decryption).

* timeline: hook in (!) the unable-to-decrypt hook

* timeline: prefix some test names with test_

* utd hook: delay reporting a UTDs

* ffi: add support for configuring the utd hook

* utd hook: switch strategy, have a single hook

And have the data structure contain extra information about late-decryption events.

* utd hook: rename `SmartUtdHook` to `UtdHookManager`

* ffi: configure the UTD hook with a grace period of 60 seconds

And ignore UTDs that have been late-decrypted in less than 4 seconds.

* utd hook: update documentation and satisfy the clippy gods

* ffi: introduce another UnableToDecryptInfo FFI struct that exposes simplified fields from the SDK's version

* review: introduce type alias for pending utd reports

* review: address other review comments
2024-03-14 14:13:44 +01:00
Benjamin Bouvier 7718f90428 event cache: add support for running back-pagination (#3195)
This adds support for back-pagination into the event cache, supporting enough features for integrating with the timeline (which is going to happen in a separate PR).

The idea is to provide two new primitives:

- one to get (or wait, if we don't have any handy) the latest pagination token received by the sync,
- one to run a single back-pagination, given a token (or not — which will backpaginate from the end of the room's timeline)

The timeline code can then use those two primitives in a loop to replicate the current behavior it has (next PR to be open Soon™).

The representation of events in the store is changed, so that a timeline can have *entries*, which are one of two things:

- either an event, as before
- or a gap, identified by a backpagination token (at the moment)

This allows us to avoid a lot of complexity from the back-pagination code in the timeline, where we'd attach the backpagination token to an event that only had an event_id. We don't have to do this here, and I suppose we could even attach the backpagination token to the next event itself.

This doesn't do reconciliation yet; the plan is to add it as a next step.
2024-03-14 09:57:52 +00:00
Mauro 5e692931dd Apply suggestions from code review
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Mauro <34335419+Velin92@users.noreply.github.com>
2024-03-14 10:42:27 +01:00
Richard van der Hoff 5e10ccc248 Add more logging for crypto store generation counter (#3207)
It's a bit unclear whether the crypto-store generation counter is doing the right thing
in terms of causing us to reload the OlmMachine. There is a suspicion that things
 might be keeping hold of references to the old OlmMachine.

This PR attempts to add the generation number to the logging for any operations that 
hold the cross-process lock. It's obviously not bulletproof: for example, it is possible
for the OlmMachine to be replaced without holding the lock; but hopefully this will
at least help us understand what's going on.
2024-03-13 17:09:57 +00:00
Mauro Romito 73b01743a5 improved code spacing 2024-03-13 12:48:32 +01:00
Mauro Romito 2f9b9942c3 docs: removed useless comment 2024-03-13 12:45:50 +01:00
Mauro Romito a52a2329a1 pr suggestions
improved the conversion by using a try from and changed the nex_token to a search state to indicate the current state of the search
2024-03-13 12:43:26 +01:00
Ivan Enderlin 6f9147de86 Merge pull request #3205 from matrix-org/stefan/invites_main_room_list
feat: Expose a room list filter for Invites only
2024-03-13 11:43:10 +01:00
Stefan Ceriu 6a67ff9acf Update invite room list filter tests 2024-03-13 12:30:33 +02:00
Stefan Ceriu 4eb3da6be7 Update crates/matrix-sdk-ui/src/room_list_service/filters/invite.rs
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Stefan Ceriu <stefan.ceriu@gmail.com>
2024-03-13 12:15:16 +02:00
Ivan Enderlin 78889aec8c fix(ruma): Add the compat-tag-info feature
fix(ruma): Add the `compat-tag-info` feature
2024-03-13 11:10:38 +01:00
Ivan Enderlin b0880996fc fix(ruma): Add the compat-tag-info feature.
This patch enables the `compat-tag-info` feature on Ruma, so that
`TagInfo::order` can be deserialized from both a `f64` or a `string`
representing a `f64`[^1].

[^1]: https://github.com/ruma/ruma/blob/f24cae17f50d140a0ff112cb3dc74a2053aa1df4/crates/ruma-events/src/tag.rs#L180-L185
2024-03-13 10:24:18 +01:00
Andy Balaam 88a8a7007c Merge pull request #3200 from matrix-org/andybalaam/feature-flag-for-overriding-expiration-min
crypto: Add a feature flag to disable the minimum session rotation time
2024-03-12 14:45:45 +00:00
Andy Balaam ff1555ed40 crypto: Clarify Durations in outbound tests 2024-03-12 14:29:27 +00:00
Andy Balaam 5c29c08941 crypto: Document that _disable-minimum-rotation-period-ms should not be used 2024-03-12 14:17:31 +00:00
Damir Jelić 3fb8a46c95 test: Add a test for the verification state reset case 2024-03-12 14:36:05 +01:00
Damir Jelić 31d985813a test: Switch to scoped mocks for the /sync mocking
Wiremock doesn't allow overwriting of a mock, so if we want to mock
different sync response bodies for the same path we'll have to mount the
mock in a subscobe.

This also removes the need to access some internal OlmMachine state to
get us notified about changed devices.
2024-03-12 14:36:05 +01:00
Stefan Ceriu eea475854c feat: Expose a verification state listener directly from Encryption 2024-03-12 14:36:05 +01:00
Richard van der Hoff a6c2719976 Remove OlmError::Decryption (#3204)
I believe this is never used, so can be removed
2024-03-12 11:16:41 +00:00
Timo Kösters 2f58cb1620 members: Simplify disambiguation logic when loading member list (#3184)
When all room members are loaded, we do not need an incremental member update. We know that parsing the /members response will only lead to more ambiguous names, not less. And because /members returns the complete list, we can directly use that list as the disambiguation map.

This improves the performance in my emulator from 56s to 9s and on a less performant device from 11mins to 11s (Tested experimentally on Matrix HQ using log statements in element android. If I have time, I will write a proper benchmark tomorrow.

See also https://github.com/matrix-org/matrix-rust-sdk/pull/3184#issuecomment-1986170631 for a more detailed benchmark run.

---

* members: Simplify disambiguation logic

* members: Prevent api misuse for receive_members

* members: Benchmark receive_all_members performance

* sdk: remove unused import

* sdk-base: rename `ApiMisuse` error to `InvalidReceiveMembersParameters`

* benchmarks: extract the member loading benchmark to `room_bench.rs`

* benchmarks: remove wiremock

* sdk-base: fix format

* sdk-base: try fixing tests

* benchmark: Provide some data to the store so the search and disambiguation happen

* benchmark: fix clippy

* benchmark: use a constant for `MEMBERS_IN_ROOM`

* sdk(style): reduce indent in `receive_all_members`

---------

Co-authored-by: Jorge Martín <jorgem@element.io>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-03-12 10:15:30 +00:00
Andy Balaam 0947349ae0 crypto: Add a feature flag to disable the minimum session rotation time
The feature flag is called "disable-minimum-rotation-period-ms" and
should not be used outside of tests, which is why it is largely
undocumented.
2024-03-12 09:40:19 +00:00
Andy Balaam 2f3b85d244 crypto: Split clamping of rotation_period into a separate function 2024-03-12 09:40:19 +00:00
Andy Balaam 919d58f94b crypto: Test that we enforce a minimum rotation_period_ms 2024-03-12 09:40:19 +00:00
Stefan Ceriu 16dcfb2e84 feat: Expose a room list filter for Invites only 2024-03-11 17:41:28 +02:00
Mauro 9ef78a484c Merge branch 'main' into mauroromito/directory_search 2024-03-11 16:30:55 +01:00
Ivan Enderlin 3edaff1364 Merge pull request #3178 from matrix-org/ex-tui
labs: turn `rrrepl` into a timeline client
2024-03-11 14:53:35 +01:00
Ivan Enderlin 45055d80cd Merge pull request #3203 from Hywan/feat-ui-room-list-invite-revisit
feat(ui,ffi): Add a new `experimental-room-list-with-unified-invites` feature
2024-03-11 13:28:09 +01:00
Richard van der Hoff 1ea163271b crypto: Include event timestamp in decryption failure logs
Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-11 13:16:39 +01:00
Andy Balaam 899e4db8d0 crypto: Break up the expiration tests for clarity 2024-03-11 12:41:35 +01:00
Ivan Enderlin e9cca7f68d feat(ui,ffi): Add a new experimental-room-list-with-unified-invites feature.
The idea of this patch is to explore the possibility to unify the
`all_rooms` list with the `invites` list in `RoomListService`.
Since this is entirely experimental, it's behind a new feature
flag. The feature itself can be configured at runtime by using the
new `SyncServiceBuilder::with_unified_invites_in_room_list` builder
method, or directly with `RoomListService::new_with_unified_invites`
constructor.
2024-03-11 12:05:32 +01:00
Kévin Commaille fd709b9d52 workspace: Bump ruma crate
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-03-09 15:18:46 +01:00
Benjamin Bouvier 4ad79d6d44 multiverse: hide the password by using rpassword to prompt it 2024-03-08 17:44:55 +01:00
Benjamin Bouvier e57a02fd91 multiverse: add support for backpagination 2024-03-08 17:37:26 +01:00
Benjamin Bouvier c41f7975b3 labs: turn rrrepl into a timeline client 2024-03-08 16:08:29 +01:00
Jorge Martin Espinosa cb6b420ad0 ffi: add previous power levels to OtherState::RoomPowerLevels (#3199)
This is needed to be able to diff between increases and decreases of power levels ("user Alice was promoted Admin", etc.).

---

* ffi: add `previous` power levels to `OtherState::RoomPowerLevels`

This is needed to be able to diff between increases and decreases of power levels.

* ffi: please clippy

* ffi: inline initialization of `previous` and `users`
2024-03-08 15:03:02 +00:00
Hanadi 724d133cce sdk&ffi: server unstable features support for MSC4028 (#3192)
Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3191

Allows support for fetching the unstable_features from `/_matrix/clients/versions`.
Specifically, to be used for checking the state of org.matrix.msc4028 through ffi to the clients.

---

* sdk: fetch unstable_features supported by homeserver

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* ffi: add can_homeserver_push_encrypted_event_to_device method

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* fix: use copied instead of dereferencing

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Hanadi <hanadi.tamimi@gmail.com>

* fix: move can_homeserver_push_encrypted_event_to_device logic to sdk

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* fix: remove unused unstable features param in client builder

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* fix: use assert instead of asserteq for bool check

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* fix: documentation

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>

* Apply suggestions from code review

Signed-off-by: Benjamin Bouvier <public@benj.me>

---------

Signed-off-by: hanadi92 <hanadi.tamimi@gmail.com>
Signed-off-by: Hanadi <hanadi.tamimi@gmail.com>
Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-03-08 11:28:04 +01:00
Benjamin Bouvier b7d6fd08f1 event cache: enforce unique access on the EventCacheStore 2024-03-07 18:57:57 +01:00
Jorge Martin Espinosa 0469c27b91 ffi: Add methods to get and reset the power levels of a Room
- `Room::build_power_level_changes_from_current()` was replaced by `Room::get_power_levels()`, which now returns an SDK/Ruma `RoomPowerLevels` value containing all the data we need to display these values in UI and not only the customised values.
- `Room::reset_power_levels()` was added to the FFI layer.
2024-03-07 12:46:13 +01:00
Benjamin Kampmann f14c00db82 store: Add a method to set a custom value without reading and returning the old value
This is useful if we don't care about the old value, which lets us avoid unnecessary reads.
2024-03-07 12:02:32 +01:00
Johannes Marbach a204b2994d chore: improve create_room documentation
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-03-06 09:52:23 +01:00
Mauro 3f627f4125 Merge branch 'main' into mauroromito/directory_search 2024-03-05 11:44:15 +01:00
dependabot[bot] 98a68632df chore(deps): bump mio from 0.8.10 to 0.8.11
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 10:42:26 +01:00
Jorge Martin Espinosa 0c98e26a05 sdk: create new users_with_power_levels fn (#3182)
It maps user ids to users' power levels.

Also, make sure it just returns an empty map if this info is not available, instead of crashing. 

Then use it in the FFI side to output updated data for the `RoomInfo`.

---

* sdk: create new `users_with_power_levels` fn which maps user ids to users' power levels

Also, make sure it just returns an empty map if this info is not available, instead of crashing.

* Update crates/matrix-sdk/src/room/mod.rs

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Jorge Martin Espinosa <angel.arasthel@gmail.com>

* Improve tests

---------

Signed-off-by: Jorge Martin Espinosa <angel.arasthel@gmail.com>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-03-04 16:37:16 +01:00
Benjamin Bouvier 4b56ca1841 event cache: remove the RoomNotFound error
Having `EventCache::for_room` return an `Option` avoids the need for the error. One caller can
safely unwrap it, and others can log and ignore rooms that have disappeared (like we do in `call_sync_response_handlers`).
2024-03-04 15:11:14 +01:00
Benjamin Bouvier 8d2e790bca event cache: add a Room::event_cache() method
This keeps the `RoomNotFound` error, which could still happen in theory if the `EventCache` is
being misused internally.
2024-03-04 15:11:14 +01:00
Stefan Ceriu e4be216731 feat: add support for m.call.invite events in the timeline and as a last room message 2024-03-04 14:57:01 +01:00
Benjamin Bouvier 74727e5f84 ci: update codecov action to v4 2024-03-04 12:25:19 +01:00
Andy Balaam 4b711e4b37 Merge pull request #3177 from matrix-org/doug/multi-user-power-levels
FFI: Update the power levels of multiple users at once.
2024-03-04 10:10:28 +00:00
Mauro Romito 4db69647ce Merge branch 'mauroromito/directory_search' of github.com:matrix-org/matrix-rust-sdk into mauroromito/directory_search 2024-03-01 17:25:16 +01:00
Mauro Romito e922a58cc3 tests: fix fmt 2024-03-01 17:25:11 +01:00
Mauro eb0ddbc063 Merge branch 'main' into mauroromito/directory_search 2024-03-01 17:18:08 +01:00
Mauro Romito 9fbc2ab07c mocking library not supported on wasm 2024-03-01 17:17:24 +01:00
Mauro Romito 2f7b2f0451 fix: fmt 2024-03-01 17:07:52 +01:00
Mauro Romito ad1623da58 docs: fixed docs 2024-03-01 15:44:24 +01:00
Mauro Romito 2abe3aba4a improved docs 2024-03-01 15:21:57 +01:00
Andy Balaam 82684d64d4 Merge pull request #3180 from matrix-org/jme/bump-uniffi-referenced-commit-so-android-works-again
ffi: bump the UniFFi referenced commit again
2024-03-01 13:27:53 +00:00
Jorge Martín 7b40daa3cf ffi: bump the UniFFi referenced commit again
At the moment, the SDK can't be built for Android because of these 2 issues:

- https://github.com/mozilla/uniffi-rs/issues/1996
- https://github.com/mozilla/uniffi-rs/issues/1995

Those have been fixed in the new commit.
2024-03-01 13:59:12 +01:00
Timo Kösters 5ee3897f7e ffi: aggregate RoomMember fields early instead of requiring to call getters (#3172)
This does two things:

- raise the timeout value for the `/members` queries, because they can super slow in Synapse because of nasal demons,
- and aggregate all the fields of `RoomMember` early, so users don't have to call getters to read each field.

---

* room_members: Export plain old data for room members to avoid FFI calls

Signed-off-by: Timo Kösters <timo@koesters.xyz>

* room_member: Respond to review feedback

* room_member: Remove previous RoomMember struct

* ffi: rename ExportedRoomMember to RoomMember

---------

Signed-off-by: Timo Kösters <timo@koesters.xyz>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-02-29 17:27:21 +01:00
Benjamin Bouvier 370f4735f7 ffi: simplify the declaration type 2024-02-29 15:03:45 +01:00
Mauro 77ba3010cc Merge branch 'main' into mauroromito/directory_search 2024-02-29 14:29:31 +01:00
Mauro Romito b2b9b5fa12 feat(bindings): reverted some code from ffi 2024-02-29 14:29:03 +01:00
Doug c251f16292 ffi: Update the power levels of multiple users as once. 2024-02-29 13:12:30 +00:00
Mauro Romito 8890bf3cee feat(bindings): improved and fixed ffi code 2024-02-29 14:10:45 +01:00
Mauro Romito 2e3ced1fb2 docs: more documentation 2024-02-29 12:58:34 +01:00
Mauro Romito 2163ab03ec tests: test improvements and added a new test 2024-02-29 12:48:49 +01:00
Mauro 4b1eefca80 Apply suggestions from code review
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Mauro <34335419+Velin92@users.noreply.github.com>
2024-02-29 11:52:31 +01:00
Mauro Romito 4dd7c3093c tests: improved the tests by adding the limit
into the check for the request
2024-02-28 19:41:31 +01:00
Mauro Romito 70466aafb4 docs: more documentation for types and the mod 2024-02-28 19:25:52 +01:00
Mauro Romito 3e35f163b7 docs: updated documentation
and removed code from ffi that will be changed soon
2024-02-28 19:17:58 +01:00
Mauro Romito 690ed4611d tests: unit tests have been completed 2024-02-28 19:01:42 +01:00
Stefan Ceriu 8392ef07cd ffi: bump uniffi to latest git rev to generate open swift classes 2024-02-28 18:46:26 +01:00
Mauro Romito 9c33540af8 tests: improved tests and added a unit test 2024-02-28 16:18:35 +01:00
Doug 0401b995b7 ffi: Expose getting a power level from a role. 2024-02-28 13:27:10 +01:00
Andy Balaam 0521d23e94 Merge pull request #3167 from matrix-org/andybalaam/doc-fixes-e2ee
docs: Punctuation and wording fixes in encryption docs
2024-02-28 09:56:19 +00:00
Mauro Romito 37d95571e9 test: fixed a test by a adding a small delay 2024-02-27 17:40:03 +01:00
Mauro Romito caa9a7d8be tests: code improvement for the filter integration test 2024-02-27 17:31:26 +01:00
Mauro Romito d9231be1ba feat(bindings): listener code 2024-02-27 17:08:55 +01:00
Jorge Martin Espinosa 2068e7f266 sdk & ffi: add user power levels and role getter to RoomInfo (#3170)
Changes:
- sdk: Add `get_user_power_level` and `get_suggested_user_role` functions so we don't need to load the whole room member list to know if a user has some power level/role.
- ffi: add an FFI fn for `get_suggested_user_role`.
- ffi: add `user_power_levels` to `RoomInfo`.

The goal of this PR is being able to fetch a user's power level or role almost immediately and avoid having to load and find the user in the room member list, which can be very slow to load (especially in EX Android).

---

* sdk: Add `get_user_power_level` and `get_suggested_user_role` functions to get the power level for a user without loading the room member list.

* ffi: add `suggested_role_for_user` fn, which calls the new `get_suggested_user_role` fn in Room

* sdk: add test

* ffi: add user power level info to `RoomInfo`

* Add changes to changelog

* sdk: Fix docs formatting

* sdk & ffi: use `&UserId` instead of `OwnedUserId`

Also, simplify error mapping.

* sdk: add extra test

* ffi: fix `OwnedUserId` -> `&UserId` conversion

* sdk: Replace `UserId::parse` with `user_id!` macro for literals in tests

* Update crates/matrix-sdk/tests/integration/room/joined.rs

Signed-off-by: Benjamin Bouvier <public@benj.me>

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-02-27 16:04:08 +01:00
Mauro Romito 26b0b32e55 feat(bindings): ffi layer started implementation
also improved the integration test for the filtered case
2024-02-27 14:37:22 +01:00
Andy Balaam c7f3e2ad1d docs: Punctuation and wording fixes in encryption docs
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-27 10:16:03 +00:00
Mauro Romito a79c5286d7 improved the tests 2024-02-27 10:58:13 +01:00
Mauro Romito 8ac6845607 feat: paginated public room search 2024-02-26 17:32:52 +01:00
Benjamin Bouvier 9f75552c9b event cache: only emit an event update when there are, well, events to propagate
Mostly an optimization that was also revealed by the previous version of the test failing, because it would receive an initial update with empty
events.
2024-02-26 16:05:23 +01:00
Benjamin Bouvier 3ef5214587 event cache: add a few smoke tests
This adds a few basic tests for the event cache, notably one for the `add_initial_events`, for something I identified while working on the code, but
that could've caused bad issues:

The `test_add_initial_events` checks that even if we received updates with meaningful events for a room, the room's events are cleared before we add initial events (since
we have no ways to know where to insert the events, in this case). In the future, we can keep this test as a "smoke test" for basic functionality of
the event cache.
2024-02-26 16:05:23 +01:00
Valere 30640ebb65 sdk: Log the successful room message sending and record the received event ID 2024-02-26 11:40:38 +01:00
Doug 4f3cdfacaa docs: Clarify PR title guidelines. 2024-02-26 10:31:28 +01:00
Johannes Marbach b68bcf9cff fix(examples): Fix typos in getting started example
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-02-25 11:35:59 +01:00
Doug 8e64341176 ffi: Use a custom RollingFileAppender.
The built in hourly preset doesn't let us specify a max file count. Plus this way we can add a file extension too.
2024-02-23 11:49:47 +01:00
Benjamin Bouvier 40ba98b95e test: fix one potential race in test_room_notification_count
We're subscribing to settings updates *after* sending a request to change a setting. In an unlucky scenario, the following sequence of events could happen:

- sending request to change the settings
- response is received
- we set up the receiver to settings updates, but it's too late

The fix would then be to subscribe to the changes *before* we even send the request to update settings.
2024-02-22 18:01:53 +01:00
Benjamin Bouvier 3541d205e0 test: timeout faster in test_room_notification_count 2024-02-22 18:01:53 +01:00
Benjamin Bouvier 2a201d4218 test: move the with_server methods in the sdk crate
It means the `wiremock` dependency is not a `dev-dependency` anymore, but an optional `dependency` enabled only if `testing` is enabled.
It seems like a fine tradeoff to me.
2024-02-22 18:01:29 +01:00
Benjamin Bouvier 43129441db test: rename another logged_in_client to logged_in_client_with_server 2024-02-22 18:01:29 +01:00
Benjamin Bouvier 5ae2d83457 test: rename one logged_in_client to logged_in_client_with_server 2024-02-22 18:01:29 +01:00
Benjamin Bouvier 193f3331e8 test: remove a few copies of logged_in_client
- in sdk-ui, reuse the same implementation everywhere
- in the sdk integration test, make use of the sdk logged_in_client to remove a few lines
2024-02-22 18:01:29 +01:00
Benjamin Bouvier 65fe3c8b5b test: commonize the logged_in_base_client methods in sdk-base 2024-02-22 18:01:29 +01:00
Benjamin Bouvier 85e8771b5a test: rename one logged_in_client to logged_in_base_client
As it returns a `BaseClient`, it should be distinguished from all the other `logged_in_client`s that return... a `Client`.
2024-02-22 18:01:29 +01:00
Benjamin Bouvier d27bfca5e4 test: rename no_retry_test_client to {{SAME}}_with_server as it also returns a mocking server
And use `no_retry_test_client` in there.
2024-02-22 18:01:29 +01:00
Benjamin Bouvier 77bf972b3f test: rename another test_client_builder to {{SAME}}_with_server as it also returns a mocking server
And use `test_client_builder` in there too.
2024-02-22 18:01:29 +01:00
Benjamin Bouvier 307063e571 test: deduplicate test_client_builder methods
And start deduplicating the other twos as well.
2024-02-22 18:01:29 +01:00
Mauro Romito 18c155beb5 feat: room_directory_sync basic sync loop 2024-02-22 17:50:07 +01:00
Stefan Ceriu 0c1b6e45d5 latest event: Remove edits/replacements from the latest room event (#3150) 2024-02-22 15:19:00 +01:00
Ivan Enderlin b5bda577dd chore(sdk): Remove useless imports or fix unused code. 2024-02-22 14:34:56 +01:00
Benjamin Bouvier 6593e32582 ffi: fix one clippy warning about ToOwned 2024-02-22 14:33:13 +01:00
Benjamin Bouvier d3612ce35b event cache: give each RoomEventCache a sdk::Room
To do so, we need to put the weak reference `EventCache -> Client` back into the `EventCache`.

Putting the `sdk::Room` in the `RoomEventCache` will be useful to run room queries like backpagination (aka call `Room::messages()`).
2024-02-22 14:33:13 +01:00
Damir Jelić bd6d0e959a backups: Better logs for decryption errors for backed up room keys 2024-02-21 16:08:32 +01:00
Benjamin Kampmann cced512ad4 Keep the raw notification event around for further processing 2024-02-21 12:48:29 +01:00
Ivan Enderlin 06359b1166 fix(sdk): Race condition, doc, and Wasm in EventCache
fix(sdk): Race condition, doc, and Wasm in `EventCache`
2024-02-21 12:14:41 +01:00
Ivan Enderlin 10098d20c5 test(common): Add tests for spawn and JoinHandle::abort. 2024-02-21 12:01:08 +01:00
Ivan Enderlin e4c8d6b708 chore(test): Remove a useless import. 2024-02-21 12:00:50 +01:00
Ivan Enderlin 6c6a8e2e77 chore(common): Remove useless import. 2024-02-21 10:54:31 +01:00
Andy Balaam dcf0069753 export: Provide a streamed way to export keys
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-20 16:06:24 +01:00
Andy Balaam f0354d1fc5 export: Move existing export_room_keys method to Store
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-20 15:18:52 +01:00
Damir Jelić 6634735065 ffi: Let the auth service use a proxy as well
The ClientBuilder already exposes setting the proxy, but sadly the
AuthService doesn't let you configure the ClientBuilder directly (yet).

So logging in with a proxy wasn't supported until now.
2024-02-20 15:15:49 +01:00
Stefan Ceriu 59c468c758 Switch user ignoring/unignoring methods to full async 2024-02-20 13:51:18 +01:00
Stefan Ceriu c6e93b06a3 Log ignored user list event deserialization errors 2024-02-20 13:51:18 +01:00
Stefan Ceriu 89033cd13a Fixes #3141 - Expose ignored users on the FFI layer 2024-02-20 13:51:18 +01:00
Valere 88a70f472f Discard session API and bindings for Room (#2941)
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-02-20 13:36:55 +01:00
Doug fafd1a403c fix: Use from macro on wrapped error. 2024-02-20 13:32:46 +01:00
Doug 371cc24031 fix: Update Swift test, remove pub access on sanitize_server_name
Removed a test that was pinging matrix.org too.
2024-02-20 13:32:46 +01:00
Doug 2538ba68c5 ffi: Expose more server discovery errors. 2024-02-20 13:32:46 +01:00
Doug 14e93e8c0c ffi: Use server_name_or_homeserver_url directly. 2024-02-20 13:32:46 +01:00
Jorge Martín 54bdb7791c ffi: Add extra logs to Client::process_session_change
This should help us understand why 2 failed requests with invalid access token didn't pass a refreshed token to the client.
2024-02-20 11:44:02 +01:00
Ivan Enderlin 2f97bc2bae feat(sdk): Improve performance of EventCacheInner::for_room. 2024-02-19 20:08:28 +01:00
Ivan Enderlin 7e3e8fff55 feat(common): Implement JoinHandle::abort on wasm32-u-u.
This patch implements `JoinHandle::abort` when `cfg(target_arch =
"wasm32")`.

The idea is to combine `RemoteHandle` and `AbortHandle`.
2024-02-19 18:21:20 +01:00
Ivan Enderlin 8d878b6785 fix(sdk): Fix a race condition in EventCacheInner::for_room.
There is a race condition in `EventCacheInner::for_room`. Before this
patch, it was possible for 2 concurrent execution to do the following:

* Execution 1 takes the read lock; there is no room in `by_room`, so it
  creates the `RoomEventCache`, code is suspended while trying to get
  the write lock.
* Execution 2 takes the read lock; there is no room in `by_room`, so it
  creates the `RoomEventCache`, code is _not_ suspended because _insert
  reason_, the write lock is acquired, and the `RoomEventCache` is saved
  and a shallow clone is returned.
* Execution 1 resumes, the write lock is acquired, the `RoomEventCache`
  overwrites the one from Execution 2, and a shallow clone is returned.

Now Execution 2 holds a `RoomEventCache` that isn't saved in `by_room`.
It's a ghost! Worst, because the store is cloned in both
`RoomEventCache`, 2 versions will overwrite themselves in the store
constantly.

This patch uses a single write lock, and uses the `BTreeMap::entry`
API instead. Thus, the race condition disappears.

This patch also changes the return type of `for_room` to make it
infallible. It was already the case: only the `Ok` value was returned.
2024-02-19 16:40:11 +01:00
Ivan Enderlin add06bf897 doc(sdk): Add some documentation about the EventCache. 2024-02-19 16:40:11 +01:00
Ivan Enderlin e5b07aa827 chore(sdk): Move EventCache::drop_handles inside EventCacheInner.
This patch moves `EventCache::drop_handles` inside `EventCacheInner` for
the sake of simplicity.
2024-02-19 16:40:11 +01:00
Ivan Enderlin 814d78708e feat(sdk): EventCache uses matrix_sdk_common::executor::spawn.
This patch imports the `spawn` function from
`matrix_sdk_common::executor` instead of `tokio`.
`matrix_sdk_common::executor` adds support for WebAssembly.
2024-02-19 16:40:11 +01:00
Ivan Enderlin 88d7a2fe28 feat(ui,ffi): Add the favourite room list filter
feat(ui,ffi): Add the `favourite` room list filter
2024-02-19 13:45:37 +01:00
Benjamin Bouvier 5386e9e838 event cache: have a single EventCache instance per Client, at most (#3136)
* event cache: move it to the main SDK crate

* event cache: add requested Debug impl to `RoomEventCacheUpdate`

Somehow the compiler asked for it now...

* event cache: add missing copyright notice to store file

* event cache: use a weak reference to the client internally

This will make it possible to have the `Client` own an `EventCache` without a reference
cycle.

* event cache: move the spawned task to its own function

* event cache: move RwLock from EventCache::inner to the only mutable field inside EventCacheInner

* event cache: have the Client own *the* event cache

The goal is to have a unique EventCache instance overall, that's available from everywhere in
the SDK, notably when creating timelines for rooms.

Because the event cache only owns a weak reference to the client, it means the Client still
can be dropped, In turn, this will close its sender of `RoomUpdates`, which will gracefully
close the task spawned in `EventCache::new` after it's done handling the latest updates.

* event cache: process room updates one at a time

* timeline: use the client-wide event cache instead of spawning one per timeline

This now means that we're passing the "initial events" to the event cache just before initializing
the timeline. As a result, there might be previous events that the event cache saw (coming from
sync), but now we can't decide where to put them; drop previously known events in that case.

* event cache: hey, turns out we don't even need the weak back-link

Keeping it as a separate commit, to make it easier to revert later.

* event cache: remove unused errors

Keeping the error type and results, though, because we might have store errors soonish.

* fixup! event cache: move the spawned task to its own function

* event cache: manually subscribe to the event cache

It was a bad idea to have it enabled by default, since some users may not be interested in all
updates for all rooms (e.g. bots). Instead, we make it so that the event cache must be
explicitly subscribed to, and we do it in two cases:

- in the UI `TimelineBuilder::build` method, because we're interested in updates to the current
  room,
- in the `RoomListService`, because we *will* be interested in updates to room derived data (e.g.
  unread counts, read receipts, and so on).

This avoids a bit of fiddling when creating the event cache in the client.

This is resilient when a parent Client is forked into a child Client, because the child
`EventCache` share the same subscription as the parent's.
2024-02-19 12:39:31 +00:00
Ivan Enderlin 9e6252cb2d doc(ui): Add missing copyright headers. 2024-02-19 13:33:11 +01:00
Ivan Enderlin 12d5f51051 feat(ffi): Add the favourite room list filter.
This patch implements the `RoomListEntriesDynamicFilterKind::Favourite`
variant.
2024-02-19 13:29:56 +01:00
Ivan Enderlin c7d34bd65e doc(ui): Add documentation for matrix_sdk_ui::room_list_service::filters.
This patch adds missing documentation for the `filters` module.
2024-02-19 13:29:56 +01:00
Ivan Enderlin 900a6d1382 feat(ui): Add the favourite filter.
This patch adds the `favourite` filter, to filter out rooms that are not
marked as favourite.
2024-02-19 13:29:56 +01:00
Doug 9228ad2f59 chore: Update changelog 2024-02-16 14:57:12 +01:00
Doug 7d9ee71245 More rusty 🦀 2024-02-16 14:57:12 +01:00
Doug c1c8bfda4e Fix: Feature flags on CI 2024-02-16 14:57:12 +01:00
Doug 1fb717968e fix: Store the details and the error separately. 2024-02-16 14:57:12 +01:00
Doug e69591dad3 fix: Address PR comments. 2024-02-16 14:57:12 +01:00
Doug ee8e9ef528 sdk: Add tests for server_name_or_homeserver_url 2024-02-16 14:57:12 +01:00
Doug 7cbc3e587d sdk: Add server_name_or_homeserver_url to ClientBuilder 2024-02-16 14:57:12 +01:00
maan2003 aeba46a0eb indexeddb: fix incorrect key used for next batch token
Signed-off-by: maan2003 <manmeetmann2003@gmail.com>
2024-02-16 14:45:21 +01:00
Benjamin Bouvier 11074d8f4d event cache: listen to all the room updates at once! 2024-02-15 18:22:11 +01:00
Benjamin Bouvier a6c133369f sdk: add a mechanism to get all room updates at once
(instead of having to subscribe to a single room in the event cache)
2024-02-15 18:22:11 +01:00
Benjamin Bouvier 6a81ceced0 event cache: move store trait and memory impl to a new store file 2024-02-15 18:22:11 +01:00
Benjamin Bouvier ffc7648ce6 ui: rename "event graph" to "event cache" 2024-02-15 18:22:11 +01:00
Benjamin Bouvier fd395a82c5 sdk: add docs for the DeduplicatingHandler data structure 2024-02-15 18:10:34 +01:00
Benjamin Bouvier 15afd1f690 sdk: deduplicate requests to send a read receipt 2024-02-15 18:10:34 +01:00
Benjamin Bouvier 1e24fbc72d rrrepl: use Timeline::mark_as_read there too 2024-02-15 18:09:57 +01:00
Damir Jelić 32afc56005 ffi: Expose the method to add additional certs in the bindings 2024-02-15 16:19:20 +01:00
Damir Jelić 7e61a6dd31 sdk: Re-expose the reqwest method to add certs through the ClientBuilder 2024-02-15 16:19:20 +01:00
Doug cae3b38c35 ffi: Expose the suggested role constructor. 2024-02-15 11:31:36 +01:00
Doug c36cb1b424 ffi: Include users in the RoomPowerLevels state 2024-02-15 11:31:36 +01:00
Damir Jelić 315a29f568 Reneable the correct backup download strategy for the bindings 2024-02-15 09:19:14 +01:00
ganfra 74931768e5 ffi : expose room_info is_favourite 2024-02-13 16:47:26 +01:00
Benjamin Bouvier 7eb3c30a3c timeline: remove the thread parameter from mark_as_read
It doesn't make sense to set a thread identifier for the receipt of the latest event: the event might not belong to that thread, and the SDK would need to check that,
since the latest event isn't reachable from the outside world (the reason why `mark_as_read` had been introduced). So let's remove it for now, and
add comments related to threaded receipts.
2024-02-13 16:03:49 +01:00
Benjamin Bouvier 7e99e812dd ffi/sdk: rename mark_read into set_unread_flag
This slightly changes the API when interacting from the FFI layer:

- instead of `mark_as_unread` and `mark_as_read`, there's now a single method `set_unread_flag(bool)`, which callers may call with true (i.e. unread) or false (i.e. not unread).
- there's a new method `mark_as_read` which sends a read receipt to the latest event in the timeline, using other commits from the same PR,
- forcing a room as read requires calling first `set_unread_flag(false)` then `mark_as_read()`
2024-02-13 16:03:49 +01:00
Benjamin Bouvier e97b7838c5 ffi: use the Timeline::mark_as_read function in mark_as_read_and_send_read_receipt 2024-02-13 16:03:49 +01:00
Benjamin Bouvier 8eb3fdc9e4 timeline: add a test that sending a reaction to the latest timeline event item isn't the same as marking as read 2024-02-13 16:03:49 +01:00
Benjamin Bouvier 8536e2b2a3 timeline: add a mark_as_read function to send a read receipt for the actual latest event 2024-02-13 16:03:49 +01:00
Benjamin Bouvier 060eaeffc0 key backup: test that the upload moves from the error state back to the idle state immediately 2024-02-12 19:38:03 +01:00
Benjamin Bouvier b177bd9783 sdk: put all the e2ee-related Client fields under a new EncryptionData struct 2024-02-12 19:38:03 +01:00
Benjamin Bouvier 6fa487fad8 sdk: comment each sub-field of ClientInner
and make the code breath a bit more
2024-02-12 19:38:03 +01:00
Denis Kasak 12444d3dc1 docs: Clarify some doc comments. 2024-02-12 17:32:37 +01:00
Denis Kasak ed5d97e052 docs: Fix doc comment for Account::signed_one_time_keys.
It talks about generating OTKs, but that is no longer true since
1c6d85935.
2024-02-12 17:32:37 +01:00
kegsay cff844ac74 Add debug logging when sessions get rotated (#3106)
Critically, explain why the session is being rotated.

Signed-off-by: kegsay <7190048+kegsay@users.noreply.github.com>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-02-12 17:31:41 +01:00
Benjamin Bouvier 70e38755fa recovery: comment and simplify all_known_secrets_available 2024-02-12 17:28:40 +01:00
Benjamin Bouvier c20e6aeca7 ffi: configure encryption settings only when it's needed
It's wrong that the first client, used only to determine how to log in and find the user id, try to run the encryption initialization tasks.
In particular, it should not even try to bootstrap the account, as this may send OTKs to the server, which the client will forget about as soon
as it's respawned as a database-backed client.
2024-02-12 17:28:40 +01:00
Benjamin Bouvier 53d723d149 ffi: avoid having two clients alive at the same time 2024-02-12 17:28:40 +01:00
Timo Kösters 5cb587a60b sliding_sync: Use assert_next_eq for tests
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Benjamin Bouvier 354f9de257 test: refactor integration test code to avoid Box::leak 2024-02-12 14:48:35 +01:00
Timo Kösters 9d04b23f45 sliding_sync: Refactor for code review
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 73edf6b734 sliding_sync: Documentation for RoomInfoUpdate and cargo fmt
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 5114474829 sliding_sync: Only emit manual room list update when late decryption happens
Previously, there were duplicate updates.

Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 91331bea51 sliding_sync: More documentation for roominfo sender/receiver
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 79f97504f8 sliding_sync: Refactor delayed decryption test
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 50ed681a4e sliding_sync: Refactor roominfo sender/receiver code
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 910887fbc6 sliding_sync: Add test for delayed decryption roominfo updates
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters 304bd910f0 sliding_sync: Add test to make sure stream updates are in correct order
I have also changed the priorities so that manual updates are preferred.
This means that duplicate updates do not happen if the room was previously unknown.

Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters e87a7954a0 sliding_sync: Refactor roominfo update receiver
Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Timo Kösters d2b02ec2e8 sliding_sync: Trigger room list update when room info changes
This fixes https://github.com/element-hq/element-x-ios/issues/1847

Signed-off-by: Timo Kösters <timo@koesters.xyz>
2024-02-12 14:48:35 +01:00
Andy Balaam d84387d12e Merge pull request #3118 from progval/shrink-decrypted-media
Preallocate vector storing decrypted files
2024-02-12 10:06:00 +00:00
Val Lorentz 368b585124 Preallocate vector storing decrypted files
Vectors grow in powers of two while reading, which is doubly wasteful:

* causes every byte to be copied in average once
* leaves the vector in average 25% larger than it needs to be, wasting memory.

For example, adding this test to
`crates/matrix-sdk-crypto/src/file_encryption/attachments.rs`:

```
fn encrypt_decrypt_minimize_memory() {
    let data = std::iter::repeat("abcdefg").take(10000).collect::<String>();
    let mut cursor = Cursor::new(data.clone());

    let mut encryptor = AttachmentEncryptor::new(&mut cursor);

    let mut encrypted = Vec::new();

    encryptor.read_to_end(&mut encrypted).unwrap();
    let key = encryptor.finish();
    assert_ne!(encrypted.as_slice(), data.as_bytes());

    let mut cursor = Cursor::new(encrypted);
    let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
    let mut decrypted_data = Vec::new();

    decryptor.read_to_end(&mut decrypted_data).unwrap();

    assert_eq!(
        decrypted_data.len(),
        decrypted_data.capacity(),
        "{} bytes wasted by decrypted_data",
        decrypted_data.capacity() - decrypted_data.len()
    );
}
```

errors with:

```
assertion `left == right` failed: 61072 bytes wasted by decrypted_data
  left: 70000
 right: 131072
```

By initially setting this capacity, the vector should be slightly larger
than needed from the start, avoiding both copying and leftover capacity
after decryption is done.
2024-02-10 10:49:23 +01:00
Val Lorentz 7bbd07cc77 Allow references to MediaEventContent
`matrix_sdk_ui::timeline::Message::msgtype` returns `&MessageType`,
and variants of `MessageType` implement `MediaEventContent`, so it is
allowing references here avoids cloning message content.
2024-02-10 10:20:00 +01:00
Ivan Enderlin 008330a744 Merge pull request #3111 from Hywan/fix-base-notable-tags
fix(base): Rewrite `RoomNotableTags`
2024-02-09 17:37:14 +01:00
Ivan Enderlin 1052c8db15 Merge branch 'main' into fix-base-notable-tags
Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2024-02-09 17:03:30 +01:00
Benjamin Bouvier 3e04590ded sdk: rename Client::handle_sync_response to call_sync_response_handlers
This indicates, in an hopefully clearer way, that it's only calling the handlers, after getting a sync response.
2024-02-09 11:39:46 +01:00
Benjamin Bouvier ce2d8212eb sdk: rename other {Joined,Invited,Left}Room to {0}Update
Since they're updates to rooms, and not the rooms in themselves.
2024-02-09 11:39:46 +01:00
Benjamin Bouvier 5ad9090dc8 sdk: rename sync::Rooms to RoomUpdates
This struct contains updates to rooms, not rooms per se. Make it clear from the name.
2024-02-09 11:39:46 +01:00
Benjamin Bouvier b15829a9fd sdk: add comment and make it clear that some functions are tests 2024-02-09 11:39:46 +01:00
Benjamin Bouvier bebb733607 nit: change favorite to favourite in a few places
Signed-off-by: Benjamin Bouvier <public@benj.me>
2024-02-09 11:37:50 +01:00
Benjamin Bouvier e872babd40 read receipts: add the room id in the instrumentation of send_single_receipt
This didn't show up, and it's quite useful to know for which room we're trying to send a receipt, without having to look up the room based on the
target event id.
2024-02-09 11:21:23 +01:00
Ivan Enderlin bcb125a09b test(sdk): Revisit the tests for notable tags.
This patch rewrites all the integration tests for the notable tags. The
tests were good, but we could merge some together. The names were too
long, they have been shortened.
2024-02-09 09:44:40 +01:00
Ivan Enderlin 65774aa90b chore(base): Remove warnings when e2e-encryption is disabled. 2024-02-09 09:31:47 +01:00
Ivan Enderlin 868bb9a8d9 fix(base) Client::receive_sync_response overwrites RoomInfo.
The workflow inside `Client::receive_sync_response` is a little
bit fragile. When `Client::handle_room_account_data` is called, it
modifies `RoomInfo`. The problem is that a current `RoomInfo` is being
computed before `handle_room_account_data` is called, and saved a
couple lines after, resulting in overwriting the modification made by
`handle_room_account_data`.

This patch ensures that the new `RoomInfo` is saved before
`handle_room_account_data` is called.
2024-02-09 09:29:57 +01:00
Ivan Enderlin 61c7a96e36 doc(base): Add missing documentation. 2024-02-09 09:29:34 +01:00
Ivan Enderlin e6dadf2b0d chore(base): Clean up tests. 2024-02-09 09:29:34 +01:00
Ivan Enderlin 75454de284 test(base): Test Room::is_favourite and ::is_low_priority.
This patch tests the `is_favourite` and `is_low_priority` methods.
2024-02-09 09:29:34 +01:00
Ivan Enderlin 8b298dfd2f chore(base): Rename NotableTags to RoomNotableTags.
Now that `NotableTags` has replaced `RoomNotableTags` entirely, this
patch can rename `NotableTags` to `RoomNotableTags` as it's a better
name for it.

Note that this type is private to `matrix_sdk_base`, so it's fine to
rename it.
2024-02-08 15:27:48 +01:00
Ivan Enderlin 31ba7b82d8 doc(sdk,base,ffi): Improve documentation and rename favorite to favourite.
The Matrix specification uses the `m.favourite` orthography. Let's use
the same in our code. So `set_is_favorite` becomes `set_is_favourite`.
This patch updates this in various places for the sake of consistency.
2024-02-08 15:25:41 +01:00
Ivan Enderlin 71f4af9cdd feat(base,sdk,ffi): Remove RoomNotableTags.
The previous patch has introduced the new `NotableTags` type to replace
the `RoomNotableTags`. This patch removes the latter.

This patch keeps the `Room::set_is_favorite` and `::set_is_low_priority`
methods. However, this patch adds the `Room::is_favourite` and
`::is_low_priority` methods, with the consequence of entirely hiding the
notable tags type from the public API.
2024-02-08 15:17:17 +01:00
Ivan Enderlin fd716bcd81 feat(base): Add the NotableTags type.
This patch is the first step to remove the [`RoomNotableTags`] API. The
idea is to compute the notable tags as a single 8-bit unsigned integer
(`u8`) and to store it inside `RoomInfo`. The benefits are numerous:

1. The type is smaller that `RoomNotableTags`,
2. It's part of `RoomInfo` so:
  - We don't need an async API to fetch the tags from the store and to
    compute the notable tags,
  - It can be put in the store,
  - The observable/subscribe mechanism is supported by `RoomInfo` behind
    observable instead of introducing a new subscribe mechanism.
2024-02-08 15:14:03 +01:00
Ivan Enderlin 385e6933d2 chore(base): Update bitflags. 2024-02-08 14:33:00 +01:00
Ivan Enderlin fab1c1c299 chore(base): Introduce a small helper to clarify the code.
I believe this small refactoring makes the code simpler to read, esp.
when more event type will be added.
2024-02-08 14:33:00 +01:00
Ivan Enderlin 9bf48ef041 feat(ui): Moaaar filters: add all, any, not, unread and category filters
feat(ui): Moaaar filters: add `all`, `any`, `not`, `unread` and `category` filters
2024-02-08 14:28:18 +01:00
Valere 2e9f362ae4 Add a method to the Device to encrypt an event directly for the device (#3091)
This patch exposes the 1-to-1 encryption method that is usually used to share a room key with a device. Users might want to send encrypted custom to-device events to a device directly, so let's expose this functionality. 

Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-02-08 13:15:01 +00:00
Ivan Enderlin fce1140ad1 test(ui): Split tests and improve documentation. 2024-02-08 14:15:01 +01:00
ganfra 0c1d90d901 Tags : introduce set_is_favorite and set_is_low_priority (#3075)
* tags : introduce update_notable_tags method

* tags : replace update_notable_tags by set_is_favorite and set_is_low_priority methods. Also add more tests.

* tags : fix clippy issues

* tags : improve doc
2024-02-08 12:32:24 +01:00
Damir Jelić 717dc1184b Add a integration test that checks if backups get automatically created 2024-02-08 10:47:35 +01:00
Damir Jelić 99e0593f90 Catch the error if we fail to automatically enable backups
We don't want the rest of the method to abort executing because
automatic backup creation failed.

We also move the infallible event handler registration at the top of the
method.

The logic to automatically create a backup uses the following logic:

1. We check if we need to create a backup, this includes a check if a
   backup exists on the server.
2. We conclude that we should create a backup, no backup exists on the
   server.
3. The method to create the backup checks again if a backup exists, this
   was an API change because the method is public and users misused the
   method.
4. Finally, we create the backup.

A race exists between the first time we check if a backup exists and the
second time, i.e. step 1 and 3.

It seems that some users create two Client objects which then make this
race a common occurrence. Clients should not do that, but at least we
don't error out too soon anymore if the automatic creation of the backup
fails.
2024-02-08 10:47:35 +01:00
Valere f6f6cfd844 Indexeddb: Avoid long and suspendable calls for encryption/serialization during the indexeddb transaction (#2966)
Quick performance improvement on `save_change` for indexeddb.
All the serialization/encryption is now done outside the db transaction

---

* do serialization/encryption before db transaction

* clippy

* add changelog for indexeddb crate

* clean comments

* fix typo

* Review: Fix typo in changelog

* Review: refactor, rename DbOperation to PendingOperation

* Review: rename variant PutKeyVal to Put

* Review: fix doc typo

* Review: rename perfrom_operation to apply

* Review: remove unneeded isEmpty checks

* Review: refactor better API for PendingStoreChanges

* Review: Prefer BTreeMap to HashMap

* Refactor: rename IndexeddbChangesKeyValue

* Refactoring: get the list of affected store from PendingIndexeddbChanges

* cleaning

* Review: Better names and comments

* Review: use filter_map instead of filter then map
2024-02-08 09:22:28 +01:00
Ivan Enderlin d905dcc476 doc(sdk): Fix a typo
doc(sdk): Fix a typo
2024-02-08 09:00:41 +01:00
Ivan Enderlin 790fe5f6db doc(sdk): Fix a typo. 2024-02-08 08:36:00 +01:00
Ivan Enderlin f2652ee9f2 Merge pull request #3066 from matrix-org/doug/room-power-levels
Add RoomPowerLevelSettings.
2024-02-07 16:03:10 +01:00
Doug 099bf9c929 sdk: Add RoomPowerLevelSettings.
fix: Preserve the event of any settings that are changed back to the default level.

sdk: Rename get_room_power_levels (drop the get).

chore: Refactor with more sensible naming.

sdk: Clean up the RoomPowerLevelChanges API.
2024-02-07 14:49:03 +00:00
Damir Jelić 5957d9603b Move back to reqwest 0.11.20
The linking error[1] on Mac still isn't fixed.

[1]: https://github.com/seanmonstar/reqwest/issues/2006
2024-02-07 13:57:07 +01:00
Damir Jelić 3d60ac36d2 Replace the usage of IndexMap::remove with IndexMap::swap_remove
The IndexMap::remove method has been deprecated, the documentation[1] on
the method tells us that we can replace the usage of it with
IndexMap::swap_remove:

> NOTE: This is equivalent to .swap_remove(key), replacing this entry’s
> position with the last element, and it is deprecated in favor of
> calling that explicitly.

[1]: https://docs.rs/indexmap/2.2.2/indexmap/map/struct.IndexMap.html#method.remove
2024-02-07 13:57:07 +01:00
Damir Jelić 787d04190e Fix some more new clippy warnings 2024-02-07 13:57:07 +01:00
Damir Jelić e33c44266e Fix a new clippy warning in the crypto crate 2024-02-07 13:57:07 +01:00
Damir Jelić 17e8109ab6 Bump our nightly version for the CI and xtask 2024-02-07 13:57:07 +01:00
Damir Jelić 14246c7094 Update our lock file
The Dalek crates got a new release fixing some build issues on nightly.
This should get rid of those build issues.
2024-02-07 13:57:07 +01:00
Andy Balaam 6a34f54753 Merge pull request #3095 from matrix-org/andybalaam/indexeddb-tidy-migrations
indexeddb: Tidy the migrations code
2024-02-07 12:41:44 +00:00
Andy Balaam 78e3350b17 indexeddb: Use a type alias to clarify do_schema_upgrade
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-07 12:27:36 +00:00
Andy Balaam e3c7e9db9b indexeddb: Fix incorrect link
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-07 12:24:25 +00:00
Ivan Enderlin dc89c00a8c doc(sdk): Fix typos. 2024-02-07 13:22:10 +01:00
Ivan Enderlin 5baf078c4b feat(ui,ffi): Implement the category room list filter.
This patch implements the `category` room list filter. It introduces a
new type: `RoomCategory`, to ensure that “group” and “people” are
mutually exclusives.
2024-02-07 13:22:10 +01:00
Ivan Enderlin f950e67b39 feat(base): Implement Room::direct_targets_length.
This patch implements `Room::direct_targets_length`. It avoids to call
`Room::is_direct` if and only if we don't care about the room's state
and we don't want an async call, and if we don't want to pay the cost of
`Room::direct_targets` which clones the `HashSet` as an alternative way
to get a similar information than `Room::is_direct`.
2024-02-07 10:36:52 +01:00
Ivan Enderlin 2c688fd40f chore(ui): Remove the get_ prefix of an internal filter type.
This patch renames `NonLeftRoomMatcher::get_state` to `::state`. This is
more Rust idiomatic.
2024-02-07 10:36:52 +01:00
Ivan Enderlin e94fd2a7df feat(ui,ffi): Implement the unread room list filter.
This patch implements the `unread` room list filter.
2024-02-07 10:36:52 +01:00
Ivan Enderlin d699b2fa70 feat(ui,ffi): Implement the all and any filters on FFI.
This patch implements the `all` and `any` filters in `matrix-sdk-ffi`.
The `not` filter cannot be implemented because recursive enum isn't
supported by UniFFI (see https://github.com/mozilla/uniffi-rs/issues/396).
2024-02-07 10:23:23 +01:00
Ivan Enderlin 61d3f8d1d9 feat(ui): Rename logical_<name> filters to <name>.
This patch removes the `logical_` prefix of some filters.
2024-02-07 10:23:23 +01:00
Ivan Enderlin 435d74a67a chore(ui): Remove the all room list filter.
This patch removes the `all` room list filter. It's not used anymore
since we have `non_left` which is more correct.
2024-02-07 10:23:22 +01:00
Ivan Enderlin fe2ca34179 feat: Implement logical_all, _any and _not filters.
This patch implements 3 new filters: `logical_all`, `logical_any` and
`logical_not`.
2024-02-07 10:23:22 +01:00
Ivan Enderlin 3b068b592d chore: Rename all_non_left to non_left.
This patch renames the room list filter `all_non_left` to `non_left`.
2024-02-07 10:23:22 +01:00
Ivan Enderlin 344a96a80f feat: Introduce the Filter trait alias.
This patch introduces the
`matrix_sdk_ui::room_list_service::filters::Filter` trait alias.

This patch also cleans up a little bit the filters by renaming some
methods for the sake of consistency across all the existing filters.
2024-02-07 10:23:22 +01:00
Ivan Enderlin 4e8c63e4e0 Merge pull request #3059 from matrix-org/fga/room_typing_notifications
Room typing notifications
2024-02-07 09:52:22 +01:00
Damir Jelić 452cf0f269 Add a missing changelog entry for the Client::sync_token() method 2024-02-07 08:23:13 +01:00
ganfra e78c4b89b4 Merge branch 'main' into fga/room_typing_notifications 2024-02-06 16:54:54 +01:00
Damir Jelić bcf8cc1c57 Fix a clippy warning about a large error variant 2024-02-06 16:17:05 +01:00
Damir Jelić f47e7b3427 Ensure the Olm session state is recorded in both decryption paths 2024-02-06 16:17:05 +01:00
Damir Jelić 8252379f76 Record the Olm session before we try to decrypt
A decryption failure would have prevented the recording of the session
otherwise. We are mostly interested in the state of the session if a
decryption failure happens.
2024-02-06 16:17:05 +01:00
Damir Jelić 2ef1e32f9a Add the message to log lines when we decrypt a Olm message 2024-02-06 16:17:05 +01:00
Damir Jelić 47a4f473a6 Bump vodozemac
This adds more detailed logging for Olm messages
2024-02-06 16:17:05 +01:00
Andy Balaam 9d6d2c3b3c Merge pull request #3097 from matrix-org/doug/swift-task
Improve the handling of targets in the Swift xtask.
2024-02-06 09:22:12 +00:00
Doug bd38dc971b xtask: Tidy-up the swift task. 2024-02-05 16:27:02 +00:00
Andy Balaam 4712c318cc indexeddb: Fix compiler warnings
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 16:05:52 +00:00
Andy Balaam a92140f3ac indexeddb: Use a MigrationDb object to ensure DB is closed
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 88d10210ad indexeddb: Regularise migration method names
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam de9adc8d3d indexeddb: Use utility functions to create indices
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 51704fa093 indexeddb: Move add_nonunique_index method up to the main module
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 0e91c085f9 indexeddb: Clear the inbound_group_sessions2 store during migration
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 49630fdd9f indexeddb: Comments explaining the migration methods and modules
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam a1f1992d7e indexeddb: Extract db_version function
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam d64a5e4dcc indexeddb: Move v0 to c5 migration into own module
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam f0c97d94b2 indexeddb: Re-use do_schema_upgrade for v0 to v5
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 5d0c6a608a indexeddb: Simplify v5 to v7 migration code
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 3dbfdcddf4 indexeddb: Move v7 to v8 migration into separate module
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam e9b11408bb indexeddb: Move InboundGroupSessionIndexedDbObject2 into v7 module
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam d26b8cca1c indexeddb: Move v5 to v7 code into a separate module
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 6c5400896a indexeddb: prepare_data_for_v7 creates its own DB
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 8b7ebece49 indexeddb: Use do_schema_upgrade for v8
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 4def306eb8 indexeddb: use do_schema_upgrade for v6 migration
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 3d4db7e220 indexeddb: Move do_schema_upgrade into migrations to be re-used
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 6fe98c3f43 indexeddb: Split v6 migration out because it is part of v5_to_v7
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 375c6c33c6 indexeddb: Move old_keys into its own file
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam edf23dbd2e indexeddb: move v8_to_v10 migration into its own file
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Andy Balaam 8d87e32f8b indexeddb: Create a migrations directory
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 15:57:07 +00:00
Doug 2f6d0450a7 xtask: Simplify the code used to build targets for Swift. 2024-02-05 15:50:46 +00:00
Doug e07ed14d20 xtask: Add supported swift targets and iterate through them when building. 2024-02-05 15:30:35 +00:00
Ivan Enderlin ae97772909 feat(sdk,ui): Remove SlidingSyncRoom::latest_event
feat(sdk,ui): Remove `SlidingSyncRoom::latest_event`
2024-02-05 12:57:07 +01:00
Ivan Enderlin 8975e6ab5d Merge pull request #3076 from matrix-org/stefan/mark_unread
Add support for MSC2867 - Manually marking rooms as unread
2024-02-05 12:40:52 +01:00
Andy Balaam 32aa784c5f Merge pull request #3094 from matrix-org/andybalaam/fix-incorrect-url
doc: Fix an incorrect URL about Indexed DB
2024-02-05 11:30:51 +00:00
Andy Balaam 56e155300c doc: Fix an incorrect URL about Indexed DB
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-02-05 11:07:08 +00:00
Ivan Enderlin df61037ac5 fix(base,sdk): Fix imports. 2024-02-05 11:59:31 +01:00
Stefan Ceriu 471b470c01 Change where unstable-msc2867 is enabled, try to fix CI tests 2024-02-05 11:59:31 +01:00
Stefan Ceriu 56d9f9d9a8 Add support for MSC2867 - Manually marking rooms as unread
- also fixes how room account data is processed and adds tests for both when rooms and extensions are present or just extensions
2024-02-05 11:59:29 +01:00
Ivan Enderlin 6e685e2e09 Merge pull request #3062 from zecakeh/ambiguity-changes
sdk: Allow to track ambiguity changes per-room
2024-02-05 11:53:10 +01:00
Ivan Enderlin ad5c1202b5 feat(sdk,ui): Remove SlidingSyncRoom::latest_event.
This patch inlines `SlidingSyncRoom::latest_event` into its unique call
site: `SlidingSyncRoomExt::latest_timeline_item`.

`SlidingSyncRoom::latest_event` is not using any data from
`SlidingSyncRoom`, except the `Client`. So it can easily live somewhere
else. Our goal is to clean up `SlidingSyncRoom` as much as possible, and
to remove any kind of logic from it.
2024-02-05 11:09:14 +01:00
Ivan Enderlin 87a07d9ee3 feat(sdk): Remove SlidingSyncRoomInner::inner
feat(sdk): Remove `SlidingSyncRoomInner::inner`
2024-02-05 09:27:44 +01:00
Kévin Commaille e0bda80e7b Merge branch 'main' into ambiguity-changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-02-04 11:55:41 +01:00
Andy Balaam b9ef5d6270 Merge pull request #3090 from matrix-org/andybalaam/indexeddb-clear-store-before-delete
indexeddb: Clear the object store before deleting it
2024-02-02 14:50:01 +00:00
Damir Jelić fa26499a39 Showcase how to use an event handler context in the custom events example 2024-02-02 15:30:26 +01:00
Andy Balaam 916e85f79e indexeddb: Clear the object store before deleting it
Since my investigation found that it significantly speeds up deletion of
a store on both Firefox and Chromium if you clear() it first, do that in
our migration code.
2024-02-02 14:06:07 +00:00
Ivan Enderlin f2b61eb5e2 chore: Remove ununsed imports. 2024-02-02 08:42:06 +01:00
Benjamin Bouvier 40e006658c ffi: add missing variants to ffi::event::MessageLikeEventType and use it in the timeline filter types 2024-02-01 19:02:12 +01:00
Benjamin Bouvier ac2fce8220 ffi: move two event type enums over to matrix-sdk-ffi/event.rs 2024-02-01 19:02:12 +01:00
Benjamin Bouvier 296d613138 ffi: sort event type enums in alphabetical order 2024-02-01 19:02:12 +01:00
Ivan Enderlin 43bed281b1 feat: Replace SlidingSyncRoom::inner by SlidingSyncRoom::prev_batch.
This patch removes the need to store and to update the
`SlidingSyncRoom::inner` field (of type `v4::SlidingSyncRoom`). Now,
we just need to handle the `prev_batch`, which is the only data we care
about.

Bonus: it reduces the size of the frozen sliding sync room quite much.
2024-02-01 17:45:30 +01:00
Ivan Enderlin b951d0d785 feat(sdk,base,ui,ffi): Continue to remove dead code in SlidingSyncRoom
feat(sdk,base,ui,ffi): Continue to remove dead code in `SlidingSyncRoom`
2024-02-01 17:20:36 +01:00
Ivan Enderlin a802b733f8 feat(sdk): Remove the SlidingSyncRoom::name method.
This patch removes the `SlidingSyncRoom::name` method. The name is
already synchronized with the `matrix_sdk_base::Room`. This code is
then useless.
2024-02-01 15:26:25 +01:00
Ivan Enderlin 6acd68cf00 feat(sdk): Remove the SlidingSyncRoom::is_dm method.
This patch removes the `SlidingSyncRoom::is_dm` method. It's not used
anywhere in the SDK, neither by the FFI bindings. Since this code is
still experimental, it's fine to remove public API.
2024-02-01 15:20:56 +01:00
Ivan Enderlin 75365a7774 feat(sdk): Remove the SlidingSyncRoom::is_initial_response method.
This patch removes the `SlidingSyncRoom::is_initial_response` method.
It's not used anywhere in the SDK, neither by the FFI bindings. Since
this code is still experimental, it's fine to remove public API.
2024-02-01 15:13:03 +01:00
Ivan Enderlin 61a5f1b3cf feat(sdk): Remove the SlidingSyncRoom::required_state method
feat(sdk): Remove the `SlidingSyncRoom::required_state` method
2024-02-01 14:47:48 +01:00
Ivan Enderlin 44e438b21b feat(sdk): Remove the SlidingSyncRoom::required_state method.
This patch removes a useless and not used (in the Matrix Rust SDK, along with the FFI bindings) method: `SlidingSyncRoom::required_state`.
2024-02-01 14:35:42 +01:00
Ivan Enderlin 7b49283394 feat(sdk,ui,ffi): Remove SlidingSyncRoom::has_unread_notifications and ::unread_notifications
feat(sdk,ui,ffi): Remove `SlidingSyncRoom::has_unread_notifications` and `::unread_notifications`
2024-02-01 14:07:22 +01:00
Benjamin Bouvier e0384494b6 sdk: use a Mutex instead of a RwLock for the sync lock
I think the reasoning behind using a RwLock for sync, and notably allowing multiple concurrent `.read()` lock taking was flawed: since there's no way
to identify which subpiece of the store we're reading and writing, there could be two concurrent requests to the write to the same thing at the same
time. To get rid of this possibility, this commit changes the lock to a single access only Mutex lock.
2024-02-01 14:07:04 +01:00
ganfra cc236e39d5 typing : filter the current user id 2024-02-01 12:54:10 +01:00
Ivan Enderlin c4d5657162 chore: Remove unused imports. 2024-02-01 12:26:44 +01:00
ganfra 59f9f43f15 Merge branch 'main' into fga/room_typing_notifications 2024-02-01 12:18:26 +01:00
Ivan Enderlin 261eb99614 feat(ffi): Remove room_list::Room::has_unread_notifications and ::unread_notifications.
In the previous commit, these methods have been removed from `matrix_sdk_ui`.
2024-02-01 11:57:11 +01:00
Ivan Enderlin 5ae638047e feat(ui): Remove room_list::Room::has_unread_notifications and ::unread_notifications.
This patch removes the `room_list::Room::has_unread_notifications` and
the `::unread_notifications` methods. Since the previous commits,
the respective `SlidingSyncRoom` methods have been removed too. It's
better to use the `Deref` implementation of `room_list::Room` to
`matrix_sdk::Room` to use the correct methods.
2024-02-01 11:54:48 +01:00
Ivan Enderlin 8ee0021641 feat(sdk): Remove SlidingSyncRoom::has_unread_notifications and ::unread_notifications.
This patch removes the `SlidingSyncRoom::has_unread_notifications`
and the `::unread_notifications` methods. It doesn't hold any relevant
information that `matrix_sdk::Room` can provide.
2024-02-01 11:53:26 +01:00
Ivan Enderlin 39241c5e18 fix(base,sdk,ui): Synchronize sliding sync room's avatar with regular Room
fix(base,sdk,ui): Synchronize sliding sync room's avatar with regular `Room`
2024-02-01 11:17:45 +01:00
Ivan Enderlin 9ef9251936 feat(ui): Remove the fallback mechanism for avatar in room_list_service::Room.
`SlidingSyncRoom` no longer has an `avatar_url` method.
`room_list::Room` no longer needs to check first in sliding sync then in
`Room` as a fallback.

This patch removes the `room_list::Room::avatar_url` method.

This patch also implements `Deref` for `room_list::Room` to
`matrix_sdk::Room`, to make our lifes easier.
2024-02-01 10:58:05 +01:00
Ivan Enderlin 90f1a34855 feat(sdk): Remove the avatar_url logic in SlidingSyncRoom.
With the previous commit, the avatar is properly synchronized with the
`Room`. The result is that `SlidingSyncRoom` no longer needs to hold
the `avatar_url`.
2024-02-01 10:58:05 +01:00
Ivan Enderlin e00532f5d2 feat(base): Update room's avatar from SlidingSyncRoom::avatar.
To update the avatar of a room, one has to look up in the state event.
That's the canonical way to do. For Sliding Sync, it means looking
inside the `required_state` field of the `v4::SlidingSyncRoom`. This
case already works and was tested.

However, a `v4::SlidingSyncRoom` comes with a direct `avatar` field.
It's another way to know the avatar URL of the room. This case was
handled and tested in `matrix_sdk::sliding_sync::SlidingSyncRoom`, but
it was never propagated into the proper sync mechanism. So when the
`avatar` field was set up, `matrix_sdk::sliding_sync::SlidingSyncRoom`
was holding this information, and the `avatar` wasn't defined in the
proper `Room`: `SlidingSyncRoom` has to look up inside the `Room` as
a fallback.

This patch is the first one to fix this “fallback” mechanism.

The `avatar` field of a `v4::SlidingSyncRoom` now triggers an update to
the new `RoomInfo::update_avatar` method (à la `update_name`), via
`process_room_properties`.
2024-02-01 10:58:05 +01:00
Kévin Commaille 8cef7a54d9 Update changelogs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-31 18:49:07 +01:00
Andy Balaam f64af126f1 Merge pull request #3073 from matrix-org/andybalaam/shrink-inbound_group_sessions
indexeddb: Shrink values in inbound_group_sessions store, improving performance all around
2024-01-31 12:29:31 +00:00
Andy Balaam e3da325f29 doc: Add rust,no_run to examples in matrix-sdk-store-encryption 2024-01-31 12:15:43 +00:00
Andy Balaam 860fe4afe0 indexeddb: Measure performance of v8 and v10 inbound_group_session store 2024-01-31 12:15:43 +00:00
Andy Balaam e3ba3366f1 indexeddb: Shrink values in inbound_group_sessions store 2024-01-31 11:59:46 +00:00
Jorge Martin Espinosa e604277e4c ffi: add missing poll events to FilterMessageLikeEventType (#3077)
Added:

- PollEnd
- PollResponse
- PollStart
- UnstablePollStart
- UnstablePollResponse
- UnstablePollEnd
2024-01-31 11:04:26 +01:00
Benjamin Bouvier a356a20c3d fixup! event graph: make TimelineBuilder::build fallible 2024-01-30 23:27:22 +01:00
Benjamin Bouvier 1c1ecf0ab0 ui: inline SlidingSyncRoomExt::timeline into its own caller
It's only used in test code, so it's not worth exposing to the SlidingSyncRoom object (and the Room already has such a timeline function, if needs be).
2024-01-30 23:27:22 +01:00
Benjamin Bouvier fd26cbcfcb ffi: get rid of Room::poll_history as it's slow and likely unused
It's super slow (as it recreates and backpaginates from the start again) and unused in both EX apps, which are our main FFI embedders.
2024-01-30 23:27:22 +01:00
Benjamin Bouvier 3a543f188b event graph: make TimelineBuilder::build fallible 2024-01-30 23:27:22 +01:00
Benjamin Bouvier 06fe8a8f32 event graph: add an initial implementation of the event graph
This is mostly a demonstration of how to plug this with the timeline, and how little it changes as a result.

Remove read receipts
2024-01-30 23:27:22 +01:00
ganfra 6ef33619bf tags : improve code after pr review 2024-01-30 16:46:43 +01:00
ganfra cde6a559ad tags : exposes RoomNotableTags directly from the sdk-base crate 2024-01-30 16:46:43 +01:00
ganfra 3b20cc4444 tags: introduce a new RoomNotablesTags and methods to subscribe to changes on it. 2024-01-30 16:46:43 +01:00
Kévin Commaille 3599e578e2 base: Move ambiguity changes maps from SyncResponse to {Left/Joined}Room
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-30 16:33:34 +01:00
Kévin Commaille bc6c458371 ui: Update user profile when ambiguity changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-30 16:33:34 +01:00
Kévin Commaille f0d722099f sdk: Allow to receive ambiguity changes per-room.
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-30 16:33:34 +01:00
Kévin Commaille bcf1ee408b sdk: Add integration test for ambiguity changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-30 16:33:34 +01:00
Kévin Commaille ef322cc39b base: Add room member ID to AmbiguityChange
Makes AmbiguityChange easier to use, especially outside of SyncResponse,
where we might not have the m.room.member event.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-30 16:33:33 +01:00
Benjamin Bouvier f5f8f47667 integration test: fix racy behavior in the test_toggling_reaction
The room sync could be over, and then the timeline items would not contain the updated item yet, if the timeline didn't get a chance to finish its
internal update. Fix this by:

1. limiting the sync time to 10 seconds
2. giving up to one second for the timeline to update internally, by watching its stream of events (we're ignoring stream updates just after this loop)
2024-01-30 12:42:56 +01:00
Jorge Martin Espinosa c418f0e6ba timeline: Add TimelineEventTypeFilter (#3025)
Adds a `TimelineEventTypeFilter` enum that either returns only events whose event_type is included in a set of allowed event types, or all events but those whose event types are in a list of excluded event types.

Also adds `TimelineEventTypeFilter` so the clients can use it to define those lists of event types, which are then converted to ruma `TimelineEventType` and used for filtering.

---

* matrix-sdk-ui: add `TimelineEventTypeFilter` to filter timeline events by either including only those of some event types or all but the ones that match those event types.

* ffi: add bindings to `TimelineEventTypeFilter` and `FilterTimelineEventType` so we can provide these event types from the FFI clients

* Fix format

* Fix tests

* Fix format again (using nightly toolchain)

* Remove `all_filter_...` functions as there is no right way to support it at the moment and they're just helpers

* Improve tests

* Make `TimelineEventFilterFn` public so it can be used in several layers.

* Make `TimelineEventTypeFilter` a struct in the FFI layer

* Add fns for creating a timeline with cache and event type filters

* Remove dead code

* Fix some review comments

* ffi: create new timeline initialization APIs, modify existing ones.
ui: make `Room::timeline()` return `None` if no timeline exists instead of lazily creating one.

More details:

- Added `init_timeline_with_builder` to `matrix_sdk_ui::room_list_service::Room` so a timeline can be initialized at will given a `TimelineBuilder`.
- Create `is_timeline_initialized()` fns in both the ui and ffi layers to check the status of the timeline.
- Make `matrix_sdk_ui::room_list_service::Room::timeline()` only return a timeline if it's already been initialized.
- Create FFI functions to expose these UI ones.

* Fix tests

* Fix some review comments

* Update bindings/matrix-sdk-ffi/src/room_list.rs

Signed-off-by: Benjamin Bouvier <public@benj.me>

---------

Signed-off-by: Benjamin Bouvier <public@benj.me>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-01-30 11:00:43 +00:00
Benjamin Bouvier cde7dd2ea8 integration test: make test_toggling_reaction fail fast (#3070)
It's started failing with timeouts in multiple PRs, so this PR should help spot where the issues specifically are, since codecov test failures are so hard to reproduce locally.
2024-01-30 09:52:53 +00:00
ganfra debc28fa47 typing : apply some changes after pr review 2024-01-29 18:18:29 +01:00
Kévin Commaille 6e3892528c intergation-testing: Fix syntax of docker-compose.yml
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-29 18:01:33 +01:00
Ivan Enderlin fca455c0bf Merge pull request #3063 from zecakeh/compose-v2
integration-testing: Improve docker-compose.yml compatibility with Podman
2024-01-29 13:52:25 +01:00
Kévin Commaille cd90265eab integration-testing: Update command to clean up docker compose data
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-29 11:50:33 +01:00
Kévin Commaille 25b2b191bb integration-testing: Make sliding-sync proxy depend on synapse in docker compose
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-29 11:48:58 +01:00
Kévin Commaille 3ade16a07d integration-testing: Add newline at end of docker-compose.yml
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-28 17:49:21 +01:00
Kévin Commaille 411dd29244 integration-testing: Use volumes in docker-compose.yml
Podman running unprivileged containers by default, using system folders
can result in permission issues inside the containers.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-28 12:20:43 +01:00
Kévin Commaille 1e74be2b2d integration-testing: Remove links in docker-compose.yml
It is not supported by podman and is not necessary by defaut

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-28 12:18:33 +01:00
Kévin Commaille a77b67eecd integration-testing: Migrate to Compose V2
Compose V1 was deprecated in January 2023 and is not
supported anymore since July 2023

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-28 12:16:50 +01:00
Jonas Platte 18eefcffdb Move matrix-sdk-crypto enum definitions out of UDL 2024-01-27 00:31:42 +01:00
ganfra 2a5dcd562c typing : fix ci 2024-01-26 17:29:05 +01:00
ganfra a3b8ec3640 typing : enable typing extension in room_list_service 2024-01-26 15:37:13 +01:00
ganfra 1fa488ec04 typing : add subscribe_to_typing_notifications and expose through ffi 2024-01-26 15:36:50 +01:00
ganfra 25ecf089aa sliding_sync : fix room account data not being processed when there is no other changes in the room (#3032)
This fixes an issue with the sliding sync processing where room account data are not processed when the associated room has no other changes.

---

* sliding_sync : fix room account data not being processed when there is no other changes in the room

* sliding_sync : properly handle room_account_data from extension

* sliding_sync : add test for processing rooms_account_data

* sliding_sync : fix formatting

* Apply suggestions from code review

Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: ganfra <francois.ganard@gmail.com>

* sliding_sync : avoid cloning room account data events

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: ganfra <francois.ganard@gmail.com>

---------

Signed-off-by: ganfra <francois.ganard@gmail.com>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-01-26 13:34:10 +01:00
Valere 8573417537 indexeddb(perf): don't deserialize data while in an indexeddb transaction (#2969)
A quick performance improvement to not do the deserialization/decryption inside the transaction.

---

* quick perf inbound_group_sessions_for_backup

* log on deserialize error

* Review: Unneeded use of :? for an object with Display trait

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Valere <bill.carson@valrsoft.com>

* Doc: fix capitalization

Co-authored-by: Benjamin Bouvier <public@benj.me>
Signed-off-by: Valere <bill.carson@valrsoft.com>

* Review: better naming

---------

Signed-off-by: Valere <bill.carson@valrsoft.com>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-01-26 11:55:04 +01:00
Doug f20c92a323 sdk: Support getting a room member's role. 2024-01-26 11:36:34 +01:00
Doug be8be9ef04 sdk: Add uniffi scaffolding. 2024-01-26 11:36:34 +01:00
Jorge Martín eccfab8b27 ffi: enable android_cleaner option in uniffi.toml
This allows us to use the native `SystemCleaner` in Android 13+ which is a bit safer regarding deadlocks than the JNA alternative we'd use otherwise.
Note this breaks compatibility with pure JVM environments, meaning the library can only be used in Android.
2024-01-26 11:20:55 +01:00
Benjamin Bouvier 25e4c8d722 read receipts: add lots of tracing logs for easier remote debugging 2024-01-26 10:45:02 +01:00
Benjamin Bouvier a9d6ab7313 read receipts: take implicit receipts into account when computing unread counts 2024-01-26 10:45:02 +01:00
Ivan Enderlin 4d2c3e33fc Merge pull request #3042 from matrix-org/rav/room_settings
crypto: Implement `OlmMachine::{set_,}room_settings`
2024-01-25 16:28:34 +01:00
Jonas Platte 0be2747ccf Use new crates.io release of UniFFI 2024-01-25 16:25:55 +01:00
Richard van der Hoff 0b01a2a53a Merge branch 'main' into rav/room_settings
Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-01-25 15:14:52 +00:00
Benjamin Bouvier d85c7b213f sliding sync: take the sync lock when processing the response 2024-01-25 13:29:55 +01:00
Benjamin Bouvier 52b10e776c read receipts: use comparison instead of manually reporting read receipts have changed
Before, to avoid saving a room info if the read receipts `RoomInfo` field hadn't changed, we used explicit reporting to the caller that a field
changed value. This way was error-prone, and there's been bugs twice in this area: one fixed in a previous PR,
and there was another one in the existing implementation (when the pending list shrinked, it wouldn't tell the caller).

This commit changes the implementation so that it we snapshot the previous read receipts, and after processing unread counts, we compare the
new version with the snapshot, relying on `PartialEq` to do the leg work here. This is more future proof and less error prone.

Also `compute_unread_counts` isn't fallible, so doesn't need to return a Result.
2024-01-25 13:29:55 +01:00
Benjamin Bouvier d88970155d read receipts: rename compute_notifications to compute_unread_counts 2024-01-25 13:29:55 +01:00
Benjamin Bouvier 1139538c76 comments: correct "inflight" to "in-flight" throughout the codebase 2024-01-25 11:58:13 +01:00
Benjamin Bouvier a1a93afbe2 matrix-sdk: add doc comment to Client::handle_sync_response 2024-01-25 11:58:13 +01:00
Benjamin Bouvier 7a107d8fa2 timeline: inline handle_room_encrypted into its one caller TimelineEventHandler::add
It's a one-liner, so not much worth it as a separate function.
2024-01-25 11:58:13 +01:00
Benjamin Bouvier 5117325ef4 comment: make it clear when some account data comes from storage 2024-01-25 11:58:13 +01:00
Benjamin Bouvier 1085b85dfc timeline: replace floating timeline_item function with a ctor on TimelineItem
And add comments here and there.
2024-01-25 11:58:13 +01:00
Benjamin Bouvier 7a7e624e7b timeline: rename and invert meaning of event_should_update_fully_read_marker
This is renamed to `has_up_to_date_read_marker_item`, and inverted in meaning, compared to its previous meaning.
It's simpler to think of it as the presence of the read marker item: if it's not there, then that's the only case where we'd need to worry about adding one.
2024-01-25 11:58:13 +01:00
Benjamin Bouvier 384d8b0e19 timeline: add comments around read marker handling 2024-01-25 11:58:13 +01:00
Benjamin Bouvier 68c9e5e98d timeline: inline find_read_marker that's used only once 2024-01-25 11:58:13 +01:00
Richard van der Hoff ff89315b6e crypto: add test for 4S keys with no mac or iv (#3048)
A regression test for https://github.com/matrix-org/matrix-rust-sdk/issues/2932

Also a changelog update.
2024-01-23 11:15:49 +00:00
Ivan Enderlin eec52d7977 chore: Update Ruma to 684ffc
chore: Update Ruma to 684ffc
2024-01-22 15:13:01 +01:00
Ivan Enderlin 8515def61b fix(crypto): It's OK to have iv and mac missing.
This patch updates `SecretStorageKey::check_zero_message` to assume
that a missing `iv` and/or `mac` is valid, instead of an error, as the
spec suggests.
2024-01-22 15:00:23 +01:00
Richard van der Hoff 75ea0ad283 Rename session_rotation_period_msgs 2024-01-22 13:36:09 +00:00
Richard van der Hoff ac24f62d8c Address review comments 2024-01-22 12:49:54 +00:00
Benjamin Bouvier 76cd7ab649 test: reenable the unread count test in code coverage (#3044)
The real reason why the test wasn't passing was the same root cause as #3031. The client's room member information was missing, meaning we couldn't compute a push context, so we couldn't compute notifications/mentions either. The fix is in the testing code itself, the sliding sync should request the `$ME` room member state event to work correctly and non-racily.
2024-01-22 12:49:28 +00:00
Ivan Enderlin eab35c1289 chore: Update Ruma to 684ffc.
This patch updates Ruma to the latest commit on its `main` branch.

This is useful for https://github.com/matrix-org/matrix-rust-sdk/issues/2932.
2024-01-22 13:42:00 +01:00
Benjamin Bouvier 1d6c01fb34 Bump matrix-sdk to 0.7.1 2024-01-22 11:56:58 +01:00
Benjamin Bouvier b680c50035 labs: introduce rrrrepl, a client specialized for showing and sending read receipts
This was quite handy during development of the client-side computation of read-receipts, to analyze some bugs.

It might be not useful to have it checked in for long, but I would love to make sure it keeps on compiling
until we have a more stable handling of read receipts in general.
2024-01-22 11:01:26 +01:00
Ivan Enderlin 20b8c41727 Merge pull request #3031 from matrix-org/sliding-sync-add-$me-to-required-state-for-all-rooms
room list service: add `$ME` as a required state for the `all_rooms` sliding sync list
2024-01-22 10:50:47 +01:00
Ivan Enderlin e9d2b1f27a feat: RingBuffer takes a NonZeroUsize
feat: `RingBuffer` takes a `NonZeroUsize`
2024-01-22 10:47:46 +01:00
Ivan Enderlin f90d1678e2 Merge pull request #3038 from matrix-org/cleanup-registrations-again
oidc: cleanup `read_registration_data()` again
2024-01-22 10:46:24 +01:00
Ivan Enderlin 92b7598e4b Merge pull request #3041 from matrix-org/rav/stfu_dead_code
crypto: Silence compiler warning
2024-01-22 10:35:56 +01:00
Ivan Enderlin 10779481b7 feat: RingBuffer takes a NonZeroUsize.
The default implementation of `RingBuffer` was setting a capacity of 0.
This was incorrect as it wasn't possible to insert any items. This patch
updates it to take a `NonZeroUsize` so that it's impossible to set a
negative or zero capacity.
2024-01-22 10:34:49 +01:00
Richard van der Hoff 2314a74cdb crypto: Implement OlmMachine::{get,set}_room_settings
We need to make sure we guard against servers downgrading the encryption
settings, or breaking them with unsupported algorithms. It's not *entirely*
clear what the expectation is here, but legacy crypto in matrix-js-sdk blocks
any changes to the settings at all, so we'll follow suit for now.
2024-01-19 19:15:44 +00:00
Richard van der Hoff 5e0e752fcd memorystore: implement room settings saving 2024-01-19 19:15:44 +00:00
Richard van der Hoff b4b460ad27 crypto: expand RoomSettings to cover session rotation settings. 2024-01-19 19:00:56 +00:00
Richard van der Hoff 6d5f617355 crypto: Silence compiler warning 2024-01-19 17:36:50 +00:00
dependabot[bot] 9f0a1e0ce7 build(deps): bump h2 from 0.3.22 to 0.3.24
---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 17:54:43 +01:00
Benjamin Bouvier d6484a012c oidc: cleanup read_registration_data() again 2024-01-19 17:03:00 +01:00
Benjamin Bouvier f7cb253b1c test: add test that after a first time sync with the room list service, we're able to compute notifications
trying to simplify test
2024-01-19 16:44:32 +01:00
Doug 2dbd4a28a1 oidc: Remove public registration errors that are only used internally. 2024-01-19 13:57:43 +01:00
Doug 5c3edfcf6a authentication: Merge the UI crate's authentication module into the SDK. 2024-01-19 13:57:43 +01:00
Benjamin Bouvier 20b97fc716 room list service: add $ME as a required state for the all_rooms sliding sync list
Without this, we wouldn't ever have the member information for the logged-in user, the first time they log in, if they don't happen to be the
last user sending a message in a room (a case covered by `$LAZY`).
2024-01-19 13:02:23 +01:00
Kévin Commaille b3f4e658c5 base-client: Support notifications in invited rooms (#2907)
Necessary for the `.m.rule.invite_for_me` rule that should only happen in invited rooms.

Requires to create a `Notification` type that accepts stripped state events. It is simpler than Ruma's type because it lacks fields with data that is made up or redundant.

Tested locally with Fractal.

Fixes #1912.

---

* Upgrade Ruma

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* base-client: Support notifications in invited rooms

Necessary for the .m.rule.invite_for_me rule that should
only happen in invited rooms.
Requires to create a Notification type that accepts stripped state
events.
It also removes fields with data that is made up or redundant.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* matrix-sdk-test: Add macros to construct raw sync or stripped state events

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* client: Add tests for register_notification_handler

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Upgrade Ruma

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Fix base changelog

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Fix methods on Room

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Fix FFI

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Fix FFI RoomMember

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

* Simplify and_then chain

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-19 10:30:35 +00:00
Benjamin Bouvier a3fe9c357b read receipts: address PR comments (renamings + comments) 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 2fcd28ab0c read receipts: include implementation details in the module's doc comment 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 5e029aa7a0 read receipts: slightly simplify the new active receipt case 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 95edb9f864 read receipts: rename account_event to process_event
and address some other review comments.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier 7116ed4848 ring buffer: show the behavior of a ring buffer with 0 capacity with a test 2024-01-18 16:48:47 +01:00
Benjamin Bouvier c00f9e9312 read receipts: use a ring buffer to limit the number of pending receipts 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 26896599de ring buffer: Add retain() to the ring buffer implementation 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 3b5238135d read receipts: tracing improvements to help debugging 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 0e5cc44ffa read receipts: use a lax comparison when selecting better receipts
Before, restoring a timeline from the cache, for which we had a receipt for the last element, would not mark that receipt as "interesting", thus
considering that all the events are new. This is incorrect, and another way to fix that would be to set `best_receipt` to the initial read receipt;
but having the information that we had a change is more useful in general, so the comparison is changed from strict to lax here.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier ca47e6d5f9 read receipts: dismiss old events when they contain common events with the new set
When restoring a timeline from the disk cache, or when we get a timeline from a limited sync, we can have events in common to the two sets, messing
up with the count. In that case, forget about previous events, since we don't try to stitch timelines yet.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier 75f1aaced6 ui timeline: add a note about a proxy workaround for duplicate timeline items 2024-01-18 16:48:47 +01:00
Benjamin Bouvier de316acc11 read receipts: don't get confused when receiving multiple times the same event
This is a workaround because the proxy may send the same event multiple times in a sync timeline. When that happens and we had the read receipt
on the duplicated event, it may cause some events to be counted twice, resulting in invalid counts. This patch fixes it by resetting the count
*every* time we see the matching event.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier 706e477e02 read receipts(chore): clippy/rustfmt/typos 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 6716ad6374 read receipts: have compute_notifications return true when stashing pending receipts 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 92df885474 read receipts: introduce ReceiptSelector helper and add many unit tests
The `ReceiptSelector` splits the tasks of computing a better new receipt into small parts, making it trivial to test in isolation.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier ddb44d35cd read receipts: extract create_sync_index out of compute_notifications and unit-test it 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 8de6345d0f test: add test for multiple receipts coming in the same sync 2024-01-18 16:48:47 +01:00
Benjamin Bouvier e6d33eaf90 test: use existing make_receipt_event_content test helper 2024-01-18 16:48:47 +01:00
Benjamin Bouvier fa74602c65 test: introduce test helper sync_timeline_message 2024-01-18 16:48:47 +01:00
Benjamin Bouvier 5cbc34811f read receipts: use sync order
No caching. Step 1 make it correct, etc.
2024-01-18 16:48:47 +01:00
Benjamin Bouvier 2d60611a44 read receipts: don't repeat iterating over older events if the server sent us the same receipt twice 2024-01-18 16:48:47 +01:00
Andy Balaam ff387609c3 Merge pull request #3012 from matrix-org/andybalaam/batch-indexeddb-fetch-for-get_inbound_group_sessions
indexeddb: Attempt to fix export crashes by batching DB operations in get_inbound_group_sessions
2024-01-18 13:40:20 +00:00
Andy Balaam b05a0ecb3b indexeddb: Batch DB access in get_inbound_group_sessions
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-01-18 13:28:14 +00:00
Valere 784c7459fe refactor(bootstrap): init cross-signing with other e2e initial task (#3024)
In existing code the bootstraping is done in different places:
  - in `bootstrap_cross_signing_if_needed` 
  - or in `run_intialization_tasks`

And both are based on the same EncryptionSettings.
`bootstrap_cross_signing_if_needed` was done by `LoginBuilder` but `run_intialization_tasks` by `MatrixAuth`. 

I propose to move all that under `run_intialization_tasks` that has already support to do it in background, and to call it in one place only: `MatrixAuth`

Also Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/2763

## Notes

- Some tests have been updated to properly `wait_for_e2ee_initialization_tasks`

- `bootstrap_cross_signing_if_needed` might require re-authentication. Which I suppose was the original reason to have that in a seperate place. But given that the need to re-auth will soon be deprecated for bootstrap (when this [MSC](https://github.com/matrix-org/matrix-spec-proposals/pull/3967) will land); so for now we pass the authentication info to `run_intialization_tasks` if any. 

---

* refactor(bootstrap): init cross-signing with other e2e initial task

* fix compilation warning for NoEncryption feature set

* Doc and Formatting: Improve doc and formatting

* Fix missing import with encryption feature flag
2024-01-18 14:25:50 +01:00
Benjamin Bouvier 18065cb42e ffi: remove unused RoomListItem::full_room_blocking() function
The Kotlin leaks have been fixed nowadays.
2024-01-18 12:30:47 +01:00
Richard van der Hoff 01d11888f4 RELEASE.md: remove spurious backticks
Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-01-18 10:31:36 +01:00
Richard van der Hoff 8aef687dcb Merge pull request #3026 from matrix-org/rav/remove_base_dep
indexeddb, sqlite: Avoid dependency on `matrix-sdk-base`
2024-01-17 09:19:15 +00:00
Richard van der Hoff 79bb716298 Inline matrix_sdk_sqlite::make_store_config
... mostly for parity with `matrix_sdk_indexeddb::make_store_config`, but
again, simplifying the dependency graph.
2024-01-16 18:13:51 +00:00
Richard van der Hoff 8ef09b4044 indexeddb: Replace make_store_config
Replace `make_store_config` with a pair of funtions `open_stores_with_name` and
`open_state_store`. This allows us to remove a dependency on
`matrix-sdk-base/e2e-encryption`, and generally simplifies things.
2024-01-16 18:13:51 +00:00
Richard van der Hoff 5f435a86ee Factor out build_store_config function 2024-01-16 12:57:18 +00:00
Andy Balaam 58be7b0f3d Merge pull request #3013 from matrix-org/andybalaam/retain-in-export_room_keys
crypto: Improve memory overhead of export_room_keys by using Vec::retain
2024-01-15 11:59:33 +00:00
Andy Balaam 7034104ef1 fixup! crypto: Improve memory overhead of export_room_keys by using Vec::retain 2024-01-15 10:05:42 +00:00
Denis Kasak c4113060b2 refactor: Simplify construction of main crate UserIdentity.
Instead of needing three methods, we now only need one: since we use the
crypto crate enum directly, we no longer need to convert between the
crypto crate and main crate enums.

Signed-off-by: Denis Kasak <dkasak@termina.org.uk>
2024-01-14 18:29:57 +01:00
Denis Kasak 3d1de1f583 refactor: Simplify main crate user identities types.
Specifically, this removes the following redundant types in the main
crate:

- enum UserIdentities
- struct OwnUserIdentity
- struct OtherUserIdentity

This is possible because `OwnUserIdentity` and `OtherUserIdentity` are
exactly the same as their crypto crate counterparts, except they also
wrap a `Client`.

As a consequence, the main crate enum `UserIdentities` is isomorphic to
a struct containing:

1. the crypto crate `UserIdentities`
2. a `Client`

So this commit performs that transformation.

Signed-off-by: Denis Kasak <dkasak@termina.org.uk>
2024-01-14 18:29:57 +01:00
Sami J. Mäkinen 147f1d3f6c Upgrade aquamarine dependency 2024-01-14 11:20:12 +00:00
Kévin Commaille e3af74739d ui: Improve logging of toggle reaction failures
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-13 21:19:55 +01:00
Kévin Commaille 8d0867a1a8 ui: Forward server failures to toggle reaction to user
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-01-13 21:19:55 +01:00
Benjamin Bouvier cfe3bb7cef sdk: expose SqliteStateStore for symmetry with the crypto store export 2024-01-13 09:25:05 +01:00
Benjamin Bouvier 95b95aae5f Apply suggestions from code review
Co-authored-by: Jonas Platte <jplatte@matrix.org>
Signed-off-by: Benjamin Bouvier <public@benj.me>
2024-01-12 17:20:10 +01:00
Benjamin Bouvier 83d7cd5430 docs: explain how to release the SDK in RELEASE.md 2024-01-12 17:20:10 +01:00
Benjamin Bouvier d0e5d84bff docs: add commit messages and PR titles guidelines 2024-01-12 17:20:10 +01:00
Mauro fb4b5ea48f ffi bindings: Expose members_no_sync (#3004)
As it is useful to allow fetching members from the store in offline contexts.
2024-01-12 17:01:05 +01:00
Andy Balaam 985f9f9b07 crypto: Improve memory overhead of export_room_keys by using Vec::retain
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-01-12 15:05:13 +00:00
Mauro c7f7828368 ffi: Expose is_name_ambiguous for notification item 2024-01-12 10:28:48 +00:00
Benjamin Bouvier 3fea9ae51a crypto: rename Account::generate_one_time_keys_helper to generate_one_time_keys 2024-01-11 16:19:12 +01:00
Benjamin Bouvier 3c4b892129 crypto: rename Account::generate_one_time_keys to Account::generate_one_time_keys_if_needed 2024-01-11 16:19:12 +01:00
Benjamin Bouvier a7d2ff327d style: reduce indent in generate_one_time_keys 2024-01-11 16:19:12 +01:00
Benjamin Bouvier 90c1858389 style: don't use a large outer Ok with ? in it 2024-01-11 16:19:12 +01:00
Benjamin Bouvier 289268a60b crypto: make it clearer receive_to_device_event is infallible 2024-01-11 16:19:12 +01:00
Benjamin Bouvier 04d1fd4b8a test: rename create_session_for to create_session_for_test_helper
To make it easier to see it's a test function when looking up callers/callees.
2024-01-11 16:19:12 +01:00
Ivan Enderlin 97b2a20338 Merge pull request #3007 from matrix-org/revert-3006-revert-2995-rav/indexeddb_optional_base
Revert "Revert "matrix-sdk-indexeddb: make `matrix-sdk-base` an optional dependency""
2024-01-11 15:21:05 +01:00
Ivan Enderlin 62641adfeb Revert "Revert "matrix-sdk-indexeddb: make matrix-sdk-base an optional dependency"" 2024-01-11 15:20:38 +01:00
Ivan Enderlin 6e407989e8 Merge pull request #3006 from matrix-org/revert-2995-rav/indexeddb_optional_base
Revert "matrix-sdk-indexeddb: make `matrix-sdk-base` an optional dependency"
2024-01-11 15:18:27 +01:00
Ivan Enderlin 1521ec6cb2 Revert "matrix-sdk-indexeddb: make matrix-sdk-base an optional dependency" 2024-01-11 15:05:00 +01:00
Ivan Enderlin b3ada6cb37 Merge pull request #3003 from matrix-org/mauroromito/better_naming_for_msc4028
Improving documentation: Can push encrypted events to device
2024-01-11 14:13:28 +01:00
Mauro 21b7e54a59 Merge branch 'main' into mauroromito/better_naming_for_msc4028 2024-01-11 12:16:20 +01:00
Benjamin Bouvier 463b0dfb91 sliding sync: make the maximum number of room facultative when updating a room 2024-01-11 11:38:26 +01:00
Benjamin Bouvier f48695cc95 Test that an extension causing a room update will mark the room as updated 2024-01-11 11:38:26 +01:00
Benjamin Bouvier 395a39e039 sliding sync: mark room/list as updated whenever a room is updated by an extension
Some sliding sync responses may include extension data that will cause an update to the room, but the room itself may not be in the set of
rooms as returned in the top-level `rooms` fields of the response. This may cause missed updates for rooms, since notifiers won't be notified
about those.

This fixes that, by making sure that a `RoomInfo`-only update will cause the room (and the lists that contain it) to be marked as updated.
2024-01-11 11:38:26 +01:00
Mauro Romito e37f5e5ac3 ffi docs: renamed push encrypted event FFI API
and improved the documentation
2024-01-11 11:35:21 +01:00
Benjamin Bouvier 1bc921f373 integration tests: remove useless qualifications 2024-01-11 10:30:16 +01:00
Benjamin Bouvier 36e69f30ec crypto ffi: use a u64 for timestamps
Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/2974
2024-01-11 10:30:16 +01:00
Ivan Enderlin 82c60116ef Merge pull request #2995 from matrix-org/rav/indexeddb_optional_base
matrix-sdk-indexeddb: make `matrix-sdk-base` an optional dependency
2024-01-10 10:52:29 +01:00
Ivan Enderlin 840cb97c21 Merge pull request #3001 from matrix-org/andybalaam/tests-for-inbound-group-session-serialization
Tests for serialization of InboundGroupSessionIndexedDbObject
2024-01-10 10:23:22 +01:00
Benjamin Bouvier ae15595af1 cleanup: move homeserver overriding to the SendRequest struct 2024-01-09 16:54:58 +01:00
Andy Balaam f89ed01182 Formatting 2024-01-09 15:38:29 +00:00
Andy Balaam 2e803c3a3d Improve name of variable in tests 2024-01-09 15:35:25 +00:00
Andy Balaam 7a3a3edc9d Tests for serialization of InboundGroupSessionIndexedDbObject
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
2024-01-09 15:31:24 +00:00
Mauro Romito 13718c6d49 fix 2024-01-08 17:44:52 +01:00
Benjamin Kampmann 767b45dcf7 Fix: Registration-login-pattern leads to double devices (#2888)
When using the same pattern of `client.register(); client.login_*` that is used by tests in [our Acter app, strangely showed that more than one device/session had been opened](https://github.com/acterglobal/a3/issues/938).

Turns out, if the server is set up for it, according [to the spec](https://spec.matrix.org/v1.8/client-server-api/#post_matrixclientv3register) registration _is_ already a login and both device id and access token will be returned ... which are then straight-up ignored by the matrix-sdk Client and without any way of setting it from the outside, one _must_ login again, creating a second unnecessary device/session.

This PR adds an integration test case that checks that upon the minimal registration-login-flow only one device/session is found (as is to be expected from the outside), surfacing the error. It also contains a "somewhat fix" (as we need this in the app right now), but as it a) changes the behavior of the current API and b) isn't fully implementing the encryption-bootstrapping-pattern (and thus fails a different test), I'd leave it open to discussion whether that was appropriate way to go.

---

* Test showing registration-login-pattern leads to too many devices

* Fix too-many-devices-bug

* Do not panic on failure to set session, refactor and add docs

* refactor bootstrap crosssinging

* Fixup

* Use new post_login_cross_signing feature from Oidc, too

* unwrap at construction for less verbose code

* remove comment from test

* make new fn pub(crate) to fix build
2024-01-08 15:24:56 +01:00
Richard van der Hoff b690db1e05 matrix-sdk-crypto: enable matrix-sdk-common/js 2024-01-08 13:31:31 +00:00
Richard van der Hoff 5ab397e8d9 Enable state-store on matrix-sdk-indexeddb 2024-01-08 11:56:03 +00:00
Richard van der Hoff 0d938d94aa matrix-sdk-indexeddb: make matrix-sdk-base an optional dependency
If we're not using the StateStore, then we don't need a dependency on
`matrix-sdk-base`. Let's make it turn-off-able.
2024-01-05 17:29:07 +00:00
311 changed files with 27093 additions and 7203 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-02-06
components: rustfmt
- name: Run Benchmarks
+4 -7
View File
@@ -210,11 +210,8 @@ jobs:
- name: '[m]-common'
cmd: matrix-sdk-common
- name: '[m]-indexeddb, no crypto'
cmd: indexeddb-no-crypto
- name: '[m]-indexeddb, with crypto'
cmd: indexeddb-with-crypto
- name: '[m]-indexeddb'
cmd: indexeddb
- name: '[m], no-default, wasm-flags'
cmd: matrix-sdk-no-default
@@ -281,7 +278,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-02-06
components: rustfmt
- name: Cargo fmt
@@ -318,7 +315,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-02-06
components: clippy
- name: Load cache
+2 -2
View File
@@ -105,13 +105,13 @@ jobs:
--features experimental-widgets,testing
env:
CARGO_PROFILE_COV_INHERITS: 'dev'
CARGO_PROFILE_COV_DEBUG: 'false'
CARGO_PROFILE_COV_DEBUG: 1
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
# Otherwise not required for public repos.
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-02-06
- name: Install Node.js
uses: actions/setup-node@v3
+21
View File
@@ -0,0 +1,21 @@
name: Rust version
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
+2
View File
@@ -7,6 +7,8 @@ emsdk-*
.build
.swiftpm
/Package.swift
# code coverage report
cobertura.xml
## User settings
xcuserdata/
+5 -3
View File
@@ -22,12 +22,14 @@ sing = "sign"
singed = "signed"
singing = "signing"
Nd = "Nd"
ratatui = "ratatui"
[files]
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
# automatically ignored, we ignore them here.
extend-exclude = [
# Our json files contain a bunch of base64 encoded ed25519 keys.
"*.json",
# We are using some fuzzy match patterns that can be understood as typos confusingly.
# Fuzzy match patterns that can be understood as typos confusingly.
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
# Hand-crafted base64 session keys that are understood as typos.
"crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs",
]
+38
View File
@@ -29,6 +29,44 @@ integration tests that need a running synapse instance. These tests reside in
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
synapse for testing purposes.
## Commit messages and PR title guidelines
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit
should have a *good commit message*.
An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes
tests. This is useful to make the review process easier (help your reviewer), but also when running
bisections, helping identifying which commit introduced a regression.
A *good commit message* should be composed of:
- a prefix to indicate which area/feature is related by the commit
- a short description that would give sufficient context for a reviewer to guess what the commit is
about.
Examples of commit messages that aren't so useful:
- “add new method“
- “enhance performance“
- “fix receipts“
Examples of good commit messages:
- “ffi: Add new method `frobnicate_the_foos`
- “indexeddb: Break up the request inside `get_inbound_group_sessions`
- “read_receipts: Store receipts locally, fixing #12345
A *proper PR title* would be a one-liner summary of the changes in the PR, following the
same guidelines of a good commit message, including the area/feature prefix. Something like
`FFI: Allow logs files to be pruned.` would be a good PR title.
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
branch name.)
Having good commit messages and PR titles also helps with reviews, scanning the `git log` of
the project, and writing the [*This week in
Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK.
## Sign off
In order to have a concrete record that your contribution is intentional
Generated
+987 -586
View File
File diff suppressed because it is too large Load Diff
+23 -8
View File
@@ -6,15 +6,16 @@ members = [
"crates/*",
"testing/*",
"examples/*",
"labs/*",
"uniffi-bindgen",
"xtask",
]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*"]
default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.70"
rust-version = "1.76"
[workspace.dependencies]
anyhow = "1.0.68"
@@ -32,11 +33,21 @@ eyeball-im = { version = "0.4.1", features = ["tracing"] }
eyeball-im-util = "0.5.1"
futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.26", default-features = false, features = [
"alloc",
] }
http = "0.2.6"
imbl = "2.0.0"
itertools = "0.12.0"
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
ruma-common = "0.12.0"
ruma = { git = "https://github.com/ruma/ruma", rev = "4c00bd010dbdca6005bd599b52e90a0b7015d056", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"unstable-msc3401",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "4c00bd010dbdca6005bd599b52e90a0b7015d056" }
once_cell = "1.16.0"
rand = "0.8.5"
serde = "1.0.151"
@@ -49,9 +60,10 @@ tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
vodozemac = "0.5.0"
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" }
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" }
vodozemac = { git="https://github.com/matrix-org/vodozemac", rev = "0c75746fc8a5eda4a0e490d345d1798b4c6cbd67" }
wiremock = "0.5.21"
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
@@ -95,3 +107,6 @@ opt-level = 3
[patch.crates-io]
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
[workspace.lints.clippy]
assigning_clones = "allow"
+21
View File
@@ -0,0 +1,21 @@
# Releasing `matrix-rust-sdk`
- Make sure to bump all the crates to *the same version number*, and commit that (along with the
changes to the `Cargo.lock` file).
- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`.
- Push the tag: `git push origin 0.7.0`
- Publish all the crates, in topological order of the dependency tree:
```
cargo publish -p matrix-sdk-test-macros
cargo publish -p matrix-sdk-test
cargo publish -p matrix-sdk-common
cargo publish -p matrix-sdk-qrcode
cargo publish -p matrix-sdk-store-encryption
cargo publish -p matrix-sdk-crypto
cargo publish -p matrix-sdk-base
cargo publish -p matrix-sdk-sqlite
cargo publish -p matrix-sdk-indexeddb
cargo publish -p matrix-sdk
cargo publish -p matrix-sdk-ui
```
+5
View File
@@ -15,6 +15,7 @@ matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test = { workspace = true }
matrix-sdk = { workspace = true }
ruma = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = "3.3.0"
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
@@ -29,3 +30,7 @@ harness = false
[[bench]]
name = "store_bench"
harness = false
[[bench]]
name = "room_bench"
harness = false
+2 -2
View File
@@ -194,7 +194,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
}
machine.invalidate_group_session(room_id).await.unwrap();
machine.discard_room_key(room_id).await.unwrap();
})
});
@@ -225,7 +225,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
}
machine.invalidate_group_session(room_id).await.unwrap();
machine.discard_room_key(room_id).await.unwrap();
})
});
+114
View File
@@ -0,0 +1,114 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::utils::IntoRawStateEventContent;
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::EventBuilder;
use ruma::{
api::client::membership::get_member_events,
device_id,
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
owned_room_id,
serde::Raw,
user_id, OwnedUserId,
};
use serde_json::json;
use tokio::runtime::Builder;
pub fn receive_all_members_benchmark(c: &mut Criterion) {
const MEMBERS_IN_ROOM: usize = 100000;
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let ev_builder = EventBuilder::new();
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
let member_content_json = json!({
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"membership": "join",
"reason": "Looking for support",
});
let member_content: Raw<RoomMemberEventContent> =
member_content_json.into_raw_state_event_content().cast();
for i in 0..MEMBERS_IN_ROOM {
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
let state_key = user_id.to_string();
let event: Raw<RoomMemberEvent> = ev_builder
.make_state_event(
&user_id,
&room_id,
&state_key,
member_content.deserialize().unwrap(),
None,
)
.cast();
member_events.push(event);
}
// Create a fake list of changes, and a session to recover from.
let mut changes = StateChanges::default();
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
for member_event in member_events.iter() {
let event = member_event.clone().cast();
changes.add_state_event(&room_id, event.deserialize().unwrap(), event);
}
// Sqlite
let sqlite_dir = tempfile::tempdir().unwrap();
let sqlite_store = runtime.block_on(SqliteStateStore::open(sqlite_dir.path(), None)).unwrap();
runtime
.block_on(sqlite_store.save_changes(&changes))
.expect("initial filling of sqlite failed");
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
runtime
.block_on(base_client.set_session_meta(SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
}))
.expect("Could not set session meta");
base_client.get_or_create_room(&room_id, RoomState::Joined);
let request = get_member_events::v3::Request::new(room_id.clone());
let response = get_member_events::v3::Response::new(member_events);
let count = MEMBERS_IN_ROOM;
let name = format!("{count} members");
let mut group = c.benchmark_group("Test");
group.throughput(Throughput::Elements(count as u64));
group.sample_size(50);
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
b.to_async(&runtime).iter(|| async {
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
});
});
{
let _guard = runtime.enter();
drop(base_client);
}
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 = room;
config = criterion();
targets = receive_all_members_benchmark,
}
criterion_main!(room);
@@ -2,9 +2,9 @@ import XCTest
@testable import MatrixRustSDK
final class ClientTests: XCTestCase {
func testBuildingWithHomeserverURL() {
func testBuildingWithHomeserverURL() async {
do {
_ = try ClientBuilder()
_ = try await ClientBuilder()
.homeserverUrl(url: "https://localhost:8008")
.build()
} catch {
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
}
}
func testBuildingWithHomeserverURLAndUserAgent() {
func testBuildingWithHomeserverURLAndUserAgent() async {
do {
_ = try ClientBuilder()
_ = try await ClientBuilder()
.homeserverUrl(url: "https://localhost:8008")
.userAgent(userAgent: "golden-eye/007")
.build()
@@ -23,24 +23,14 @@ final class ClientTests: XCTestCase {
}
}
func testBuildingWithUsername() {
func testBuildingWithInvalidUsername() async {
do {
_ = try ClientBuilder()
.username(username: "@test:matrix.org")
.build()
} catch {
XCTFail("The client should build successfully when given a username.")
}
}
func testBuildingWithInvalidUsername() {
do {
_ = try ClientBuilder()
_ = try await ClientBuilder()
.username(username: "@test:invalid")
.build()
XCTFail("The client should not build when given an invalid username.")
} catch ClientError.Generic(let message) {
} catch ClientBuildError.Sdk(let message) {
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
} catch {
XCTFail("Not expecting any other kind of exception")
+4 -2
View File
@@ -41,7 +41,7 @@ features = ["lax_deserialize"]
[dependencies.matrix-sdk-crypto]
workspace = true
features = ["qrcode", "automatic-room-key-forwarding"]
features = ["qrcode", "automatic-room-key-forwarding", "uniffi"]
[dependencies.matrix-sdk-sqlite]
workspace = true
@@ -53,9 +53,11 @@ default_features = false
features = ["rt-multi-thread"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
[dev-dependencies]
tempfile = "3.8.0"
assert_matches2 = { workspace = true }
[lints]
workspace = true
-1
View File
@@ -31,7 +31,6 @@ fn setup_x86_64_android_workaround() {
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
uniffi::generate_scaffolding("./src/olm.udl")?;
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
+2 -2
View File
@@ -25,8 +25,8 @@ pub struct Device {
/// Is our cross signing identity trusted and does the identity trust the
/// device.
pub cross_signing_trusted: bool,
/// The first time this device was seen in local timestamp, seconds since
/// epoch.
/// The first time this device was seen in local timestamp, milliseconds
/// since epoch.
pub first_time_seen_ts: u64,
}
+28 -22
View File
@@ -16,9 +16,9 @@ mod responses;
mod users;
mod verification;
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use anyhow::Context;
use anyhow::Context as _;
pub use backup_recovery_key::{
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
};
@@ -31,11 +31,10 @@ pub use logger::{set_logger, Logger};
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
use matrix_sdk_crypto::{
backups::SignatureState,
olm::{IdentityKeys, InboundGroupSession, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
EncryptionSettings as RustEncryptionSettings, LocalTrust,
EncryptionSettings as RustEncryptionSettings,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
pub use responses::{
@@ -129,10 +128,10 @@ pub struct PickledSession {
pub sender_key: String,
/// Was the session created using a fallback key.
pub created_using_fallback_key: bool,
/// The Unix timestamp when the session was created.
pub creation_time: String,
/// The Unix timestamp when the session was last used.
pub last_use_time: String,
/// Unix timestamp (in seconds) when the session was created.
pub creation_time: u64,
/// Unix timestamp (in seconds) when the session was last used.
pub last_use_time: u64,
}
/// A pickled version of an `InboundGroupSession`.
@@ -244,9 +243,11 @@ async fn migrate_data(
user_id: parse_user_id(&data.account.user_id)?,
device_id: device_id.clone(),
pickle,
dehydrated: false, // dehydrated devices are never involved in migration
shared: data.account.shared,
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
creation_local_time: MilliSecondsSinceUnixEpoch(UInt::default()),
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
fallback_key_creation_timestamp: Some(MilliSecondsSinceUnixEpoch::now()),
};
let account = matrix_sdk_crypto::olm::Account::from_pickle(pickled_account)?;
@@ -423,8 +424,12 @@ fn collect_sessions(
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
.pickle();
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
let creation_time = SecondsSinceUnixEpoch(
UInt::new(session_pickle.creation_time).context("invalid creation timestamp")?,
);
let last_use_time = SecondsSinceUnixEpoch(
UInt::new(session_pickle.last_use_time).context("invalid last use timestamp")?,
);
let pickle = matrix_sdk_crypto::olm::PickledSession {
pickle,
@@ -839,6 +844,7 @@ impl From<RoomSettings> for RustRoomSettings {
Self {
algorithm: value.algorithm.into(),
only_allow_trusted_devices: value.only_allow_trusted_devices,
..RustRoomSettings::default()
}
}
}
@@ -881,7 +887,7 @@ fn vodozemac_version() -> String {
vodozemac::VERSION.to_owned()
}
uniffi::include_scaffolding!("olm");
uniffi::setup_scaffolding!();
#[cfg(test)]
mod tests {
@@ -901,36 +907,35 @@ mod tests {
"pickle":"FFGTGho89T3Xgd56l+EedOPV37s09RR8aYnS9305qPKF66LG+ly29YpCibjJOvkwm0dZwN9A2bOH/z7WscriqwZn/p0GE6YSNwLzffCy5iROzYzpYzFe0HtiyJmCQWCezvLc5lHV8YsfD00C1pKGX2R9M1wwp3/n4/3VjtTyPsdnmtwAPu4WdcPSkisCaQ3a6JaSKqv8zYzUjnpzgcpXHvPUR5d5+TzXgrVz3BeCOe8NEOWIW6xYUxFtGteYP0BczOkkJ22t7Css0tSMSrYgCll4zZUGNrd6D9b/z7KwcDnb978epsZ16DcZ/aaTxPdM5uDIkHgF/qHWerfxcaqsqs4EQfJdSgOTeqhjHBw1k0uWF2bByJLK+n7sGkYXEAuTzc4+0XvSFvu3Qp+1bHZuT7QejngRZzyxznORyBxd8la3/JjeJlehSK80OL7zSmohoYZD59S6i3tFWfopjQThJ0/eIyVOhEN/c3tfIcVr3lFEQeokgpCRNOVldhPcQWq994NHaL7jtb6yhUqT1gShY4zYayFL/VRz6nBSXXYwzrC9jho67knqXSri3lIKYevP9aOi384IvzbkinQdumc804dYwiCbs5hZppfEnfhfgiDDm+kVrJ9WaPRF4SySCTlS8jdGmBeL2CfCQ5IcZ5nK6X7tZM3tmtYwva0RuQiTNltp3XTfbMa0EoaEBximv25165hFTpzrWgoszBTpZPfgsMuWENWBcIX4AcLSk0CJ0qzPDeUwvmRcFStstGYV4drs5u5HEqovFSI48CoHPSEZfwwERCI4c/0efZ0CVEfnm8VcMv3AbnAfedD7v3QNdVwWOEhz/fGR76BQi2WjZP4MWvYRJ/vsLO5hcVWUvaJGQs5kANUFZMWuJQeJv3DmkV9kKKXnyfFUerlQ4Uk/5tp2mXiG+adHjuRp/Eeh5V/biCcIaX3rNuIY6MJaPz6SOwlFe79MMBaNwaS3j4Kh/Aq9BRw0QXdjO4CqMI4p2xCE1N5QTPdeaRTHTZ3r7mLkHX3FpZMxitc8vDl9L2FRoSOMMh/sRD1boBCkjrsty9rvTUGYY3li05jBuTXnYMjA4zj79dC9TGo4g+/wi+h537EhtP5+170LwqnIzfHt8yfjbsMMC7iwLpC1C57sTwxpMkNo3nQEvZOfqCxjq+ihiGuL9iN5lSstu9/C4qP2tQll86ASXf1axxRZQlUB0hlLHbEW6/7O7xOU6FTs4yXAZC04souRkggmfhDzZ9kQmN/zRTbqlATFI7l9/0VGxwLOVnCMUhgiDX5yL8CYK9I4ENMLf5zOuO6P3GbYISjEoHC7fUOzQ6OwGgLyI0wCEVdSJzQcdKh+W15VV+eDjhE/qEJHQWx024hTQFTKYHlDn95+lMmRI9BJLP1HU2JW6onVWsTsE5zSYu9jLj739EKfV4gS/pWzoQDRa7a9ZG6+m+RrwyJhCso3gkUekDNobhFlDX6YeH+Btj91N0uS3F9qr8lbo491s/z2fNV42zT4NYObzgrAYDQAV/2WYF8tXtxLV/Jzk8AMmyr/cfNaT2dXxVJKWq+nN2BYHBmg9CCWPJ2aB/1WWIcHfcDOlngtH991gP6246f/DEaVC/Ayxz7bPtSH5tlZ4Xbpc2P4BYxaRp/yxhhQ2C9H2I/PTt3mnNNgky/t8PZrN3W5+eiSVE9sONF8G3mYsa4XFqM+KxfbPUqsrEnrRBmvmJ250hpTPkFcIF775RvvRRKALXdlTKs+S4HKDW7KoP0Dm9+r4RlO0UHpWND9w0WSMItvWQyo0VViXJgZfBjYtWDoO0Ud+Kc7PLWNX6RUKY7RlDjXadJTC4adH6CN3UBC/ouqqfTrYvPOkyd2oKf4RLjEVcFAUIftFbLy+WBcWv8072nnAFJIlm3CxGq++80TyjqFR45P+qfIJavxQNIt5zhHPfMgHjX27OA3+l7rHDxqfMLBPxhtARwlyF+qx1IJiSWbmlHkdz2ylD9unoLSpf+DmmFvvgTj+3EEP4bY2jA/t91XFeG3uaTQSy3ryDvhbX21U7G2HGOEl9rCkmz+hG0YRB/6KxZZ0eMIDr7OWfpPEuHV8oYwDNYbsT9zCGsR1hHxBJtdo60b36mjMemtf761DhJ/oQZ4eU738yzx1hvVS3aCJsfyp70H5u+pUjgrA565uG2lEMNLu4T4NFVw0UdrVudyrhmT8P7vF4v+mR4pp+OzRbLf8AtZrKmHlMqRst+/wOHUHug/Tpz6EwZPDWGiQyFyPUkjHWW7ACouegBFOWFabsk+zCDhyxoSNrSMCtdB1L+qK72jRPGOvXk8p/1kBOIJfAjaK1ZWz8hTc30hOSWYxkRP296zPHiQF0ibNYSPNZ9tNxgq9nV/cEQ68TsNr3SULfDr0TSjCPf4AfmJ0k1k5xphSYv/TtGIbjg/9yGVFqclg4Y/6rrfkApbx36PQEBNxLiRsZ4hGpCfVU6h0jOekk8TV6CAguXVX/G31UqsAEa4sOD2g10Ir+5JD7bdd3JE/999kHGdiCqc0DNcgSqWYbq2QYwrN/mb+mMUbiQSNMcc34kK1n+7dGxppnt7YN7UsJqBWJdH0Lw1Epxi11ViTeVma9bqioJYXi6N5exdpZTT7KmcGYFsoTqO958EX6AppgcML7N9oP3TO8qSgCpV3Bbbemq4bvjV43aM6Rdx17pC4GZo0jjU97p4K8jE4PvgoHlYkuPwSJDOSAdnYPh+Inq/vCk48UfIlup0ATJFVUXD7uf84v9roZSwZPXZ5j/88+MkHBIJwPv8cugmz5uN2EuBW5IScMuEqG7Cmk72SU3/QA39G79S0Gpw7iPhTos5LXxhfvohGcnSaNEvfNeecQf7fpVciTdHwuvcgqJizUKpSFg2P+LDBiO44mJD15RNAaT37Rrj5P06YITO4PDj+FMdc6gx+JQUFbcSRhScE/0gfsVm0P1BYIH5q0k/QDgEVoerf/n19lITTzPib1F2OHP4hyF3BEq1pd9NwuPhhsVVqTVTK5MzFwFIOH7cwJyY7aBykmsWBavdb2J7UA5wjKqMHl1auUGPlNL+lZjqG4tw05bchtFAF+PGWQXJhJCtRSkkzTOCrLRyYyyI9mWYEjoc23cGLanlIs7WA1Nd0Jz+5RSNlf9Gtnd65yQp/W1eqY6yzURPHUUa7FrynyORmjaR9adT9utSQkXy8++IeDNzhMtFr+SqQ/gKECLe0GeuyTs6E5bImUtqpN+xopBXnEeq8wp+bvLf76d98qPE5ibTRwlsSyCE4c1Y7vrJrlc15Yc2R9ciIuKUS8rUKLSdGBFe/TD4R3cPhCKAnnRLGWnJiPPgxoTVwHVZMISdsAjNaWblBmiAOzFcu7443d3PCLyXVcfR9xgvW51HTumo91t5Qyx4HIXGoZxayZYFm2hrhSlieUqLnDL2j2gYgGU5NGoQl4OnEY2QqobpRUF4xJ4HhLzYbLrBeXmTDPvj0MasC3kKsRlm/HrsRRWZ2iPSMw9601tLvDfyjG53ddPISiVNnkdXcaAN5np7dwipdBOC1s4a0sEmKakNbkkDb8LsGBNte/g6UYs5yYaKr0bnXlDjMCznHQa7pypBjE7S55T3UeRpwo3IvZ1tfIGdb+z9RIA/PDvUksxJ3Xq3lqtZzkZJF5aeedfIOekGS/G0LiCSYsELgRceH5veknHqoGoL6xi4Q6/VjmfpZVXT19bDcTNtaR9Dlaq4LDjpQl9rl5C3O/X1hgADvJUuINCiLrD114sLY1DG/TDXE0sp+TK7utnjLAoHuAuj+6anY5vN66CSbwyUNmvo+m8li/AMkRYdtSDoPWkV7Y1ixMBPcua0Llwn2HSKKwnCjvhDIDIIVwbWwb1s6b9cztH81WF5RWUgFujewPvTElM1Sy10y7BcZohKw28uLRFVsKunc9yX2PiQoTSB4PHBHRA4U5dEQV3GHQJ93nee7VT3oeQPMVebWhuhOhi34Z33LQajzpCF3OjIbJb0tOPP6L6N/ODqkNsYViI3kgCnkNhexadOuGFWIqen2Q8iv2uOZWbPirt0YEeKZIk2dpND07L8Q3OsoQCk2rjpnw9LuFrjgu7gN9gFyPq25HJRBn7PM/lS60DF+xVkJq94PwN+CiZWC43SVcBGx65DFZIs/N78MZCUzZbFlsS7FsIrDJt878cp9eZdq/Ai4LZhL8QYHpVUrQxRxZGSqooA755N6nOxw66JkA1VPnjECCMgoNNtWox0JzhMe8PBdh2ZliXf8yQ6/eTvsG6FD84F+49pc7m0L99pfWHb9ClyO3KRHscp/MOIC1MJmqoB4dNxV20U+z8/lSTIvcmM8DiaAZj/yxlst90drlGydlyPjQzYd/XtIYcO5gHoeD1KUCZRapE5dkyk5vh97WZJn/JkR8hsslU3D6x3rNGwJbQVRu0IiA3PpeAQNZBNAJHHfv8IzIYxPhMJdYq0YqLIGSUYu87D04cDOxJY7hgawYs+ExOWb7XkbpuRoITQd8zpwVDFlSCS+wFO+qah3Vn8RBTc6cXHO5xRWfUNj+NrEtPdVmax+9EXqXtHQyFpxaauvL96RH+mGwpKHOk3aisXbZ6gLE2mF4egGjjJOIJdHyb2ZR+kj+4GIvkoBwipDgUfr4UBXY8pvFxQOxRgtI4LgOY9Z1Aco7Mwp6qi1KoMFJW8d+gJwsgM3cMsyEeYH1n/mdpJW6VDbIWzOHkP5n+OKKNm2vJTkQFFwF9eOtGy9fNBtS4qo4jvOUJnnAPsrPbGMbBYd1dMC3daHLEwvIKCAVBn7q1Z2c4zAD5eEoY0EwZj/j8x8lGQ8TswFT81ZotW7ZBDai/YtV8mkGfuaWJRI5yHc/bV7GWLF+yrMji/jicBF5jy2UoqwxseqjgTut49FRgBH3h1qwnfYbXD3FvQljyAAgBCiZV726pFRG+sZv0FjDbq0iCKILVSEUDZgmQ",
"shared":true,
"uploaded_signed_key_count":50
},
"sessions":[
{
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4s+SdrKUYAMUdGcYD7QukrPklEOy7fJho9YGK/jV04QdA8JABiOfD+ngJTR4V8eZdmDuG08+Q5EL79V81hQwU2fKndP0y/9nAXPUIADYq0Zrg4EsOnXz7aE+hAeBAm0IBog1s8RYUvynZ15uwjbd/OTLP+gpqpX33DwVg2leiBkQetiUSpOpZCuQ8CcZwIA0MoGCqvaT7h76VHX9JxJx+2fCMhsJMx1nhd99WJH1W9ge5CtdbC4KUP92OSxIrPOnMrNcOPJPp/paZP+HFNQ3PDL+z8pGKXmCnrXGSbd7iPHurPYESrVkBzr",
"sender_key":"WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
"created_using_fallback_key":false,
"creation_time":"1649425011424",
"last_use_time":"1649425011424"
"creation_time": 1649425011424u64,
"last_use_time": 1649425011424u64
},
{
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4t2W/lowyrV6SXVZp+uG59im0AAfNSKjhjZuiOpQlX7MS+AOJkCNvyujJ2g3KSjLZ94IkoHxkBDHLWSjwaLPu40rfOzJPDpm0XZsR6bQrsxKOmXLGEw2qw5jOTouzMVL2gvuuTix97nSYSU8j3XvTMRUoh0AF/tUpRLcvEFZeGrdUYmTMlyTv4na+FVUalUZ+jrk8t1/sM99JNq3SY1IBSjrBq/0rCOHieiippz0sw2fe2b87id4rqj1g3R9w2MWTWEdOz3ugjMGYF1YDBQZA1tJZ/hmgppk2AU2xKQXE2X3DgSC6fC66D4",
"sender_key":"RzRROfmHNlBfzxnNCUYBfn/5oZNQ11XYjDg59hS+mV0",
"created_using_fallback_key":false,
"creation_time":"1649425011503",
"last_use_time":"1649425011503"
"creation_time": 1649425011503u64,
"last_use_time": 1649425011503u64
},
{
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4titbL3SS12PYHpcBPJc6hXnOnZXqrjtjYOD545fck+3utEo8cqqwWubc9tsvxGW3tOWPttLBdAW30Vn8V1M8ebqVCNVWEAb1GKjV4ni8xG7G9SlEcCjLjnF4lJpddSZkqVMFoN0ITr9aSz/eJwXpc3HLreUFXwc8LuQp7krQ4Vt1e5EE/klduqsdurZf5V14RHsmWz2lKjt7nVgtIz/dhtF5F/sGJdg8kCGaHIMSbGAPuPPpa4/Laicb/5otrYt4pg4W4KdFpSGJIcvUQNjXaOZMx3cu/RPJIOyNhx7whG1QiYAUBqAJvr",
"sender_key":"IXSZugAHig1v8MowE1jxi2wDDDfuZBeJynHlegJVwUc",
"created_using_fallback_key":false,
"creation_time":"1649425011566",
"last_use_time":"1649425011566"
"creation_time": 1649425011566u64,
"last_use_time": 1649425011566u64
},
{
"pickle":"SmkDiFZjNukiarQ7XHQo25FILHsuhNOnxy56cMSQU/Y71jaGbJes4YrvN4Dfy4RSONfejEDXDkbW2JudlHHRP/rWEmnfJiGbK6ArbrG2puqIZgOecPnOUgPfCisr49p1Gmf36dPaO5lm/ZSrngfSoxahoeJJE/CcJN98sYM15XytRk2LBwc+CyYDqr4V1qxfsBt6tzJ4+tsAZeRdD0UtipQgysgH56o8N7nKTCkaZz5lfpYCl3FEgwXpLJ0MGQvtQmbORFvOLqR1jZ/EbmNGKiqDDIYsqG0sf78ii1jqfpLDBXLuYDccsg",
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
"created_using_fallback_key":false,
"creation_time":"1649542063182",
"last_use_time":"1649542063182"
"creation_time": 1649542063182u64,
"last_use_time": 1649542063182u64
}
],
"inbound_group_sessions":[
@@ -1010,7 +1015,8 @@ mod tests {
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
);
let room_keys = machine.runtime.block_on(machine.inner.export_room_keys(|_| true))?;
let room_keys =
machine.runtime.block_on(machine.inner.store().export_room_keys(|_| true))?;
assert_eq!(room_keys.len(), 2);
let cross_signing_status = machine.cross_signing_status();
+51 -3
View File
@@ -17,7 +17,7 @@ use matrix_sdk_crypto::{
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities,
};
use ruma::{
api::{
@@ -40,6 +40,7 @@ use ruma::{
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
@@ -800,6 +801,53 @@ impl OlmMachine {
Ok(serde_json::to_string(&encrypted_content)?)
}
/// Encrypt the given event with the given type and content for the given
/// device. This method is used to send an event to a specific device.
///
/// # Arguments
///
/// * `user_id` - The ID of the user who owns the target device.
/// * `device_id` - The ID of the device to which the message will be sent.
/// * `event_type` - The event type.
/// * `content` - The serialized content of the event.
///
/// # Returns
/// A `Result` containing the request to be sent out if the encryption was
/// successful. If the device is not found, the result will be `Ok(None)`.
///
/// The caller should ensure that there is an olm session (see
/// `get_missing_sessions`) with the target device before calling this
/// method.
pub fn create_encrypted_to_device_request(
&self,
user_id: String,
device_id: String,
event_type: String,
content: String,
) -> Result<Option<Request>, CryptoStoreError> {
let user_id = parse_user_id(&user_id)?;
let device_id = device_id.as_str().into();
let content = serde_json::from_str(&content)?;
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
if let Some(device) = device {
let encrypted_content =
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
let request = ToDeviceRequest::new(
user_id.as_ref(),
DeviceIdOrAllDevices::DeviceId(device_id.to_owned()),
"m.room.encrypted",
encrypted_content.cast(),
);
Ok(Some(request.into()))
} else {
Ok(None)
}
}
/// Decrypt the given event that was sent in the given room.
///
/// # Arguments
@@ -917,7 +965,7 @@ impl OlmMachine {
passphrase: String,
rounds: i32,
) -> Result<String, CryptoStoreError> {
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
let keys = self.runtime.block_on(self.inner.store().export_room_keys(|_| true))?;
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
.map_err(CryptoStoreError::Serialization)?;
@@ -976,7 +1024,7 @@ impl OlmMachine {
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
let room_id = RoomId::parse(room_id)?;
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
self.runtime.block_on(self.inner.discard_room_key(&room_id))?;
Ok(())
}
@@ -1,15 +0,0 @@
namespace matrix_sdk_crypto_ffi {};
enum LocalTrust {
"Verified",
"BlackListed",
"Ignored",
"Unset",
};
enum SignatureState {
"Missing",
"Invalid",
"ValidButNotTrusted",
"ValidAndTrusted",
};
@@ -164,7 +164,6 @@ impl From<OutgoingRequest> for Request {
},
RoomMessage(r) => Request::from(r),
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
KeysBackup(b) => (r.request_id().to_owned(), b.clone()).into(),
}
}
}
+10 -5
View File
@@ -29,12 +29,12 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-core = { workspace = true }
futures-util = { workspace = true }
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi", "experimental-room-list-with-unified-invites"] }
mime = "0.3.16"
once_cell = { workspace = true }
opentelemetry = "0.21.0"
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
opentelemetry = "0.22.0"
opentelemetry_sdk = { version = "0.22.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.15.0", features = ["tokio", "reqwest-client", "http-proto"] }
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
sanitize-filename-reader-friendly = "2.2.1"
serde = { workspace = true }
@@ -42,7 +42,7 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-core = { workspace = true }
tracing-opentelemetry = "0.22.0"
tracing-opentelemetry = "0.23.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
@@ -69,6 +69,7 @@ features = [
"rustls-tls", # note: differ from block below
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
@@ -83,4 +84,8 @@ features = [
"native-tls", # note: differ from block above
"socks",
"sqlite",
"uniffi",
]
[lints]
workspace = true
@@ -1,10 +1,11 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
sync::{Arc, RwLock as StdRwLock},
};
use matrix_sdk::{
oidc::{
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
types::{
client_credentials::ClientCredentials,
errors::ClientErrorCode::AccessDenied,
@@ -15,30 +16,39 @@ use matrix_sdk::{
},
AuthorizationResponse, Oidc, OidcError,
},
AuthSession,
AuthSession, ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError,
};
use matrix_sdk_ui::authentication::oidc::{ClientId, OidcRegistrations, OidcRegistrationsError};
use ruma::{
api::client::discovery::discover_homeserver::AuthenticationServerInfo, IdParseError,
api::{
client::discovery::discover_homeserver::AuthenticationServerInfo,
error::{DeserializationError, FromHttpResponseError},
},
OwnedUserId,
};
use tokio::sync::RwLock as AsyncRwLock;
use url::Url;
use zeroize::Zeroize;
use super::{client::Client, client_builder::ClientBuilder, RUNTIME};
use crate::{client::ClientSessionDelegate, client_builder::UrlScheme, error::ClientError};
use super::{client::Client, client_builder::ClientBuilder};
use crate::{
client::ClientSessionDelegate,
client_builder::{CertificateBytes, ClientBuildError},
error::ClientError,
};
#[derive(uniffi::Object)]
pub struct AuthenticationService {
base_path: String,
passphrase: Option<String>,
user_agent: Option<String>,
client: RwLock<Option<Arc<Client>>>,
homeserver_details: RwLock<Option<Arc<HomeserverLoginDetails>>>,
client: AsyncRwLock<Option<Client>>,
homeserver_details: StdRwLock<Option<Arc<HomeserverLoginDetails>>>,
oidc_configuration: Option<OidcConfiguration>,
custom_sliding_sync_proxy: RwLock<Option<String>>,
custom_sliding_sync_proxy: StdRwLock<Option<String>>,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<CertificateBytes>,
proxy: Option<String>,
}
impl Drop for AuthenticationService {
@@ -52,14 +62,23 @@ impl Drop for AuthenticationService {
pub enum AuthenticationError {
#[error("A successful call to configure_homeserver must be made first.")]
ClientMissing,
#[error("{message}")]
InvalidServerName { message: String },
#[error("The supplied server name is invalid.")]
InvalidServerName,
#[error(transparent)]
ServerUnreachable(HttpError),
#[error(transparent)]
WellKnownLookupFailed(RumaApiError),
#[error(transparent)]
WellKnownDeserializationError(DeserializationError),
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
SlidingSyncNotAvailable,
#[error("Login was successful but is missing a valid Session to configure the file store.")]
SessionMissing,
#[error("Failed to use the supplied base path.")]
InvalidBasePath,
#[error(
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
)]
@@ -74,6 +93,7 @@ pub enum AuthenticationError {
OidcCancelled,
#[error("An error occurred with OIDC: {message}")]
OidcError { message: String },
#[error("An error occurred: {message}")]
Generic { message: String },
}
@@ -84,9 +104,27 @@ impl From<anyhow::Error> for AuthenticationError {
}
}
impl From<IdParseError> for AuthenticationError {
fn from(e: IdParseError) -> AuthenticationError {
AuthenticationError::InvalidServerName { message: e.to_string() }
impl From<ClientBuildError> for AuthenticationError {
fn from(e: ClientBuildError) -> AuthenticationError {
match e {
ClientBuildError::Sdk(MatrixClientBuildError::InvalidServerName) => {
AuthenticationError::InvalidServerName
}
ClientBuildError::Sdk(MatrixClientBuildError::Http(e)) => {
AuthenticationError::ServerUnreachable(e)
}
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
FromHttpResponseError::Server(e),
)) => AuthenticationError::WellKnownLookupFailed(e),
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
FromHttpResponseError::Deserialization(e),
)) => AuthenticationError::WellKnownDeserializationError(e),
_ => AuthenticationError::Generic { message: e.to_string() },
}
}
}
@@ -150,6 +188,7 @@ impl OidcAuthenticationData {
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
url: String,
sliding_sync_proxy: Option<String>,
supports_oidc_login: bool,
supports_password_login: bool,
}
@@ -161,6 +200,12 @@ impl HomeserverLoginDetails {
self.url.clone()
}
/// The URL of the discovered or manually set sliding sync proxy,
/// if any.
pub fn sliding_sync_proxy(&self) -> Option<String> {
self.sliding_sync_proxy.clone()
}
/// Whether the current homeserver supports login using OIDC.
pub fn supports_oidc_login(&self) -> bool {
self.supports_oidc_login
@@ -172,14 +217,20 @@ impl HomeserverLoginDetails {
}
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl AuthenticationService {
/// Creates a new service to authenticate a user with.
#[uniffi::constructor]
// TODO: This has too many arguments, even clippy agrees. Many of these methods are the same as
// for the `ClientBuilder`. We should let people pass in a `ClientBuilder` and possibly convert
// this to a builder pattern as well.
#[allow(clippy::too_many_arguments)]
pub fn new(
base_path: String,
passphrase: Option<String>,
user_agent: Option<String>,
additional_root_certificates: Vec<Vec<u8>>,
proxy: Option<String>,
oidc_configuration: Option<OidcConfiguration>,
custom_sliding_sync_proxy: Option<String>,
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
@@ -189,12 +240,14 @@ impl AuthenticationService {
base_path,
passphrase,
user_agent,
client: RwLock::new(None),
homeserver_details: RwLock::new(None),
client: AsyncRwLock::new(None),
homeserver_details: StdRwLock::new(None),
oidc_configuration,
custom_sliding_sync_proxy: RwLock::new(custom_sliding_sync_proxy),
custom_sliding_sync_proxy: StdRwLock::new(custom_sliding_sync_proxy),
session_delegate: session_delegate.map(Into::into),
cross_process_refresh_lock_id,
additional_root_certificates,
proxy,
})
}
@@ -204,93 +257,65 @@ impl AuthenticationService {
/// Updates the service to authenticate with the homeserver for the
/// specified address.
pub fn configure_homeserver(
pub async fn configure_homeserver(
&self,
server_name_or_homeserver_url: String,
) -> Result<(), AuthenticationError> {
let mut builder = self.new_client_builder();
builder = builder.server_name_or_homeserver_url(server_name_or_homeserver_url);
// Attempt discovery as a server name first.
let result = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url);
let client = builder.build_inner().await?;
let details = self.details_from_client(&client).await?;
match result {
Ok(server_name) => {
let protocol = if server_name_or_homeserver_url.starts_with("http://") {
UrlScheme::Http
} else {
UrlScheme::Https
};
builder = builder.server_name_with_protocol(server_name.to_string(), protocol);
}
Err(e) => {
// When the input isn't a valid server name check it is a URL.
// If this is the case, build the client with a homeserver URL.
if Url::parse(&server_name_or_homeserver_url).is_ok() {
builder = builder.homeserver_url(server_name_or_homeserver_url.clone());
} else {
return Err(e.into());
}
}
}
let client = builder.build_inner().or_else(|e| {
if !server_name_or_homeserver_url.starts_with("http://")
&& !server_name_or_homeserver_url.starts_with("https://")
{
return Err(e);
}
// When discovery fails, fallback to the homeserver URL if supplied.
let mut builder = self.new_client_builder();
builder = builder.homeserver_url(server_name_or_homeserver_url);
builder.build_inner()
})?;
let details = RUNTIME.block_on(self.details_from_client(&client))?;
// Now we've verified that it's a valid homeserver, make sure
// there's a sliding sync proxy available one way or another.
// Make sure there's a sliding sync proxy available.
if self.custom_sliding_sync_proxy.read().unwrap().is_none()
&& client.discovered_sliding_sync_proxy().is_none()
&& details.sliding_sync_proxy().is_none()
{
return Err(AuthenticationError::SlidingSyncNotAvailable);
}
*self.client.write().unwrap() = Some(client);
*self.client.write().await = Some(client);
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
Ok(())
}
/// Performs a password login using the current homeserver.
pub fn login(
pub async fn login(
&self,
username: String,
password: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<Arc<Client>, AuthenticationError> {
let Some(client) = self.client.read().unwrap().clone() else {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
// Login and ask the server for the full user ID as this could be different from
// the username that was entered.
client.login(username, password, initial_device_name, device_id).map_err(|e| match e {
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
})?;
let whoami = client.whoami()?;
client.login(username, password, initial_device_name, device_id).await.map_err(
|e| match e {
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
},
)?;
let whoami = client.whoami().await?;
let session =
client.inner.matrix_auth().session().ok_or(AuthenticationError::SessionMissing)?;
self.finalize_client(client, session, whoami.user_id)
drop(client_guard);
self.finalize_client(session, whoami.user_id).await
}
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns.
pub fn url_for_oidc_login(&self) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
let Some(client) = self.client.read().unwrap().clone() else {
pub async fn url_for_oidc_login(
&self,
) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
@@ -307,25 +332,24 @@ impl AuthenticationService {
let oidc = client.inner.oidc();
RUNTIME.block_on(async {
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
let mut data_builder = oidc.login(redirect_url, None)?;
// TODO: Add a check for the Consent prompt when MAS is updated.
data_builder = data_builder.prompt(vec![Prompt::Consent]);
let data = data_builder.build().await?;
let mut data_builder = oidc.login(redirect_url, None)?;
// TODO: Add a check for the Consent prompt when MAS is updated.
data_builder = data_builder.prompt(vec![Prompt::Consent]);
let data = data_builder.build().await?;
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
})
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
}
/// Completes the OIDC login process.
pub fn login_with_oidc_callback(
pub async fn login_with_oidc_callback(
&self,
authentication_data: Arc<OidcAuthenticationData>,
callback_url: String,
) -> Result<Arc<Client>, AuthenticationError> {
let Some(client) = self.client.read().unwrap().clone() else {
let client_guard = self.client.read().await;
let Some(client) = client_guard.as_ref() else {
return Err(AuthenticationError::ClientMissing);
};
@@ -354,18 +378,18 @@ impl AuthenticationService {
return Err(AuthenticationError::OidcCallbackUrlInvalid);
};
RUNTIME.block_on(async move {
oidc.finish_authorization(code).await?;
oidc.finish_authorization(code).await?;
oidc.finish_login()
.await
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })
})?;
oidc.finish_login()
.await
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?;
let user_id = client.inner.user_id().unwrap().to_owned();
let session =
client.inner.oidc().full_session().ok_or(AuthenticationError::SessionMissing)?;
self.finalize_client(client, session, user_id)
drop(client_guard);
self.finalize_client(session, user_id).await
}
}
@@ -379,19 +403,31 @@ impl AuthenticationService {
builder = builder.user_agent(user_agent);
}
if let Some(proxy) = &self.proxy {
builder = builder.proxy(proxy.to_owned())
}
builder = builder.add_root_certificates(self.additional_root_certificates.clone());
builder
}
/// Get the homeserver login details from a client.
async fn details_from_client(
&self,
client: &Arc<Client>,
client: &Client,
) -> Result<HomeserverLoginDetails, AuthenticationError> {
let supports_oidc_login = client.discovered_authentication_server().is_some();
let supports_password_login = client.supports_password_login().await.ok().unwrap_or(false);
let sliding_sync_proxy = client.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
let url = client.homeserver();
Ok(HomeserverLoginDetails { url, supports_oidc_login, supports_password_login })
Ok(HomeserverLoginDetails {
url,
sliding_sync_proxy,
supports_oidc_login,
supports_password_login,
})
}
/// Handle any necessary configuration in order for login via OIDC to
@@ -550,30 +586,53 @@ impl AuthenticationService {
}
/// Creates a new client to setup the store path now the user ID is known.
fn finalize_client(
async fn finalize_client(
&self,
client: Arc<Client>,
session: impl Into<AuthSession>,
user_id: OwnedUserId,
) -> Result<Arc<Client>, AuthenticationError> {
// Take ownership of the client. This means that further attempts to
// `finalize_client` may fail, but we want to make sure that there
// aren't two clients at any point later.
let Some(client) = self.client.write().await.take() else {
return Err(AuthenticationError::ClientMissing);
};
let homeserver_url = client.homeserver();
let sliding_sync_proxy: Option<String>;
if let Some(custom_proxy) = self.custom_sliding_sync_proxy.read().unwrap().clone() {
sliding_sync_proxy = Some(custom_proxy);
} else if let Some(discovered_proxy) = client.discovered_sliding_sync_proxy() {
sliding_sync_proxy = Some(discovered_proxy.to_string());
} else {
sliding_sync_proxy = None;
}
let sliding_sync_proxy = self
.custom_sliding_sync_proxy
.read()
.unwrap()
.clone()
.or_else(|| client.sliding_sync_proxy().map(|url| url.to_string()));
// Wait for the parent client to finish running its initialization tasks.
client.inner.encryption().wait_for_e2ee_initialization_tasks().await;
// Drop the parent client. Both clients shouldn't be alive at the same time, or
// it may cause issues (when trying to initialize encryption-related tasks at
// the same time).
drop(client);
// Construct the final client.
let mut client = self
.new_client_builder()
.passphrase(self.passphrase.clone())
.homeserver_url(homeserver_url)
.sliding_sync_proxy(sliding_sync_proxy)
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
auto_enable_cross_signing: true,
backup_download_strategy:
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
auto_enable_backups: true,
})
.username(user_id.to_string());
if let Some(proxy) = &self.proxy {
client = client.proxy(proxy.to_owned())
}
if let Some(id) = &self.cross_process_refresh_lock_id {
let Some(ref session_delegate) = self.session_delegate else {
return Err(AuthenticationError::OidcError {
@@ -586,12 +645,12 @@ impl AuthenticationService {
client = client.set_session_delegate_inner(session_delegate.clone());
}
let client = client.build_inner()?;
let client = client.build_inner().await?;
// Restore the client using the session from the login request.
client.restore_session_inner(session)?;
Ok(client)
Ok(Arc::new(client))
}
}
+118 -54
View File
@@ -32,7 +32,7 @@ use matrix_sdk::{
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, TransactionId, UInt, UserId,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
};
@@ -40,7 +40,10 @@ use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotifi
use mime::Mime;
use ruma::{
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
events::room::power_levels::RoomPowerLevelsEventContent,
events::{
ignored_user_list::IgnoredUserListEventContent,
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
};
use serde::{Deserialize, Serialize};
@@ -55,6 +58,7 @@ use crate::{
encryption::Encryption,
notification::NotificationClientBuilder,
notification_settings::NotificationSettings,
room_directory_search::RoomDirectorySearch,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
ClientError,
@@ -179,7 +183,7 @@ impl Client {
sdk_client: MatrixClient,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
) -> Result<Arc<Self>, ClientError> {
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
@@ -193,11 +197,11 @@ impl Client {
}
});
let client = Arc::new(Client {
let client = Client {
inner: ManuallyDrop::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
});
};
if let Some(process_id) = cross_process_refresh_lock_id {
if session_delegate.is_none() {
@@ -239,24 +243,22 @@ impl Client {
#[uniffi::export(async_runtime = "tokio")]
impl Client {
/// Login using a username and password.
pub fn login(
pub async fn login(
&self,
username: String,
password: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
})
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
}
pub async fn get_media_file(
@@ -325,9 +327,10 @@ impl Client {
self.inner.oidc().authentication_server_info().cloned()
}
/// The sliding sync proxy that is trusted by the homeserver. `None` when
/// not configured.
pub fn discovered_sliding_sync_proxy(&self) -> Option<Url> {
/// The sliding sync proxy of the homeserver. It is either set automatically
/// during discovery or manually via `set_sliding_sync_proxy` or `None`
/// when not configured.
pub fn sliding_sync_proxy(&self) -> Option<Url> {
self.inner.sliding_sync_proxy()
}
@@ -342,9 +345,8 @@ impl Client {
}
/// Gets information about the owner of a given access token.
pub(crate) fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
RUNTIME
.block_on(async move { self.inner.whoami().await.map_err(|e| anyhow!(e.to_string())) })
pub(crate) async fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
Ok(self.inner.whoami().await?)
}
}
@@ -604,7 +606,7 @@ impl Client {
}
/// Registers a pusher with given parameters
pub fn set_pusher(
pub async fn set_pusher(
&self,
identifiers: PusherIdentifiers,
kind: PusherKind,
@@ -613,20 +615,24 @@ impl Client {
profile_tag: Option<String>,
lang: String,
) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let ids = identifiers.into();
let ids = identifiers.into();
let pusher_init = PusherInit {
ids,
kind: kind.try_into()?,
app_display_name,
device_display_name,
profile_tag,
lang,
};
self.inner.set_pusher(pusher_init.into()).await?;
Ok(())
})
let pusher_init = PusherInit {
ids,
kind: kind.try_into()?,
app_display_name,
device_display_name,
profile_tag,
lang,
};
self.inner.pusher().set(pusher_init.into()).await?;
Ok(())
}
/// Deletes a pusher of given pusher ids
pub async fn delete_pusher(&self, identifiers: PusherIdentifiers) -> Result<(), ClientError> {
self.inner.pusher().delete(identifiers.into()).await?;
Ok(())
}
/// The homeserver this client is configured to use.
@@ -645,22 +651,6 @@ impl Client {
Ok(dm)
}
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user_id = UserId::parse(user_id)?;
self.inner.account().ignore_user(&user_id).await?;
Ok(())
})
}
pub fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user_id = UserId::parse(user_id)?;
self.inner.account().unignore_user(&user_id).await?;
Ok(())
})
}
pub fn search_users(
&self,
search_term: String,
@@ -675,7 +665,7 @@ impl Client {
pub fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
RUNTIME.block_on(async move {
let owned_user_id = UserId::parse(user_id.clone())?;
let response = self.inner.get_profile(&owned_user_id).await?;
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
let user_profile = UserProfile {
user_id,
@@ -710,6 +700,75 @@ impl Client {
pub fn encryption(&self) -> Arc<Encryption> {
Arc::new(self.inner.encryption().into())
}
// Ignored users
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
if let Some(raw_content) = self
.inner
.account()
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
.await?
{
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
let user_ids: Vec<String> =
content.ignored_users.keys().map(|id| id.to_string()).collect();
return Ok(user_ids);
}
Ok(vec![])
}
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;
self.inner.account().ignore_user(&user_id).await?;
Ok(())
}
pub async fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;
self.inner.account().unignore_user(&user_id).await?;
Ok(())
}
pub fn subscribe_to_ignored_users(
&self,
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 {
while let Some(user_ids) = subscriber.next().await {
listener.call(user_ids);
}
})))
}
pub fn room_directory_search(&self) -> Arc<RoomDirectorySearch> {
Arc::new(RoomDirectorySearch::new(
matrix_sdk::room_directory_search::RoomDirectorySearch::new((*self.inner).clone()),
))
}
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
Ok(Arc::new(Room::new(room)))
}
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
Ok(self.inner.account().get_recently_visited_rooms().await?)
}
pub async fn track_recently_visited_room(&self, room: String) -> Result<(), ClientError> {
self.inner.account().track_recently_visited_room(room).await?;
Ok(())
}
}
#[uniffi::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
fn call(&self, ignored_user_ids: Vec<String>);
}
#[derive(uniffi::Enum)]
@@ -766,6 +825,7 @@ impl From<&search_users::v3::User> for UserProfile {
impl Client {
fn process_session_change(&self, session_change: SessionChange) {
if let Some(delegate) = self.delegate.read().unwrap().clone() {
debug!("Applying session change: {session_change:?}");
RUNTIME.spawn_blocking(move || match session_change {
SessionChange::UnknownToken { soft_logout } => {
delegate.did_receive_auth_error(soft_logout);
@@ -774,6 +834,10 @@ impl Client {
delegate.did_refresh_tokens();
}
});
} else {
debug!(
"No client delegate found, session change couldn't be applied: {session_change:?}"
);
}
}
+138 -71
View File
@@ -2,31 +2,69 @@ use std::{fs, path::PathBuf, sync::Arc};
use matrix_sdk::{
encryption::{BackupDownloadStrategy, EncryptionSettings},
reqwest::Certificate,
ruma::{
api::{error::UnknownVersionError, MatrixVersion},
ServerName, UserId,
},
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
Client as MatrixClient, ClientBuildError as MatrixClientBuildError,
ClientBuilder as MatrixClientBuilder, IdParseError,
};
use sanitize_filename_reader_friendly::sanitize;
use url::Url;
use zeroize::Zeroizing;
use super::{client::Client, RUNTIME};
use super::client::Client;
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
#[derive(Clone)]
pub(crate) enum UrlScheme {
Http,
Https,
/// A list of bytes containing a certificate in DER or PEM form.
pub type CertificateBytes = Vec<u8>;
#[derive(Debug, Clone)]
enum HomeserverConfig {
Url(String),
ServerName(String),
ServerNameOrUrl(String),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum ClientBuildError {
#[error(transparent)]
Sdk(#[from] MatrixClientBuildError),
#[error("Failed to build the client: {message}")]
Generic { message: String },
}
impl From<IdParseError> for ClientBuildError {
fn from(e: IdParseError) -> ClientBuildError {
ClientBuildError::Generic { message: format!("{e:#}") }
}
}
impl From<std::io::Error> for ClientBuildError {
fn from(e: std::io::Error) -> ClientBuildError {
ClientBuildError::Generic { message: format!("{e:#}") }
}
}
impl From<url::ParseError> for ClientBuildError {
fn from(e: url::ParseError) -> ClientBuildError {
ClientBuildError::Generic { message: format!("{e:#}") }
}
}
impl From<ClientError> for ClientBuildError {
fn from(e: ClientError) -> ClientBuildError {
ClientBuildError::Generic { message: format!("{e:#}") }
}
}
#[derive(Clone, uniffi::Object)]
pub struct ClientBuilder {
base_path: Option<String>,
username: Option<String>,
server_name: Option<(String, UrlScheme)>,
homeserver_url: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
server_versions: Option<Vec<String>>,
passphrase: Zeroizing<Option<String>>,
user_agent: Option<String>,
@@ -37,13 +75,33 @@ pub struct ClientBuilder {
inner: MatrixClientBuilder,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
Arc::new(Self {
base_path: None,
username: None,
homeserver_cfg: None,
server_versions: None,
passphrase: Zeroizing::new(None),
user_agent: None,
sliding_sync_proxy: None,
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
inner: MatrixClient::builder().with_encryption_settings(EncryptionSettings {
auto_enable_cross_signing: false,
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
auto_enable_backups: false,
}),
cross_process_refresh_lock_id: None,
session_delegate: None,
additional_root_certificates: Default::default(),
})
}
pub fn enable_cross_process_refresh_lock(
@@ -83,14 +141,19 @@ impl ClientBuilder {
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
// Assume HTTPS if no protocol is provided.
builder.server_name = Some((server_name, UrlScheme::Https));
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
Arc::new(builder)
}
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_url = Some(url);
builder.homeserver_cfg = Some(HomeserverConfig::Url(url));
Arc::new(builder)
}
pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
Arc::new(builder)
}
@@ -130,12 +193,31 @@ impl ClientBuilder {
Arc::new(builder)
}
pub fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientError> {
Ok(self.build_inner()?)
pub fn add_root_certificates(
self: Arc<Self>,
certificates: Vec<CertificateBytes>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.additional_root_certificates = certificates;
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
Ok(Arc::new(self.build_inner().await?))
}
}
impl ClientBuilder {
pub(crate) fn with_encryption_settings(
self: Arc<Self>,
settings: EncryptionSettings,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.inner = builder.inner.with_encryption_settings(settings);
Arc::new(builder)
}
pub(crate) fn enable_cross_process_refresh_lock_inner(
self: Arc<Self>,
process_id: String,
@@ -156,17 +238,7 @@ impl ClientBuilder {
Arc::new(builder)
}
pub(crate) fn server_name_with_protocol(
self: Arc<Self>,
server_name: String,
protocol: UrlScheme,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.server_name = Some((server_name, protocol));
Arc::new(builder)
}
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
pub(crate) async fn build_inner(self: Arc<Self>) -> Result<Client, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = builder.inner;
@@ -179,23 +251,45 @@ impl ClientBuilder {
}
// Determine server either from URL, server name or user ID.
if let Some(homeserver_url) = builder.homeserver_url {
inner_builder = inner_builder.homeserver_url(homeserver_url);
} else if let Some((server_name, protocol)) = builder.server_name {
let server_name = ServerName::parse(server_name)?;
inner_builder = match protocol {
UrlScheme::Http => inner_builder.insecure_server_name_no_tls(&server_name),
UrlScheme::Https => inner_builder.server_name(&server_name),
};
} else if let Some(username) = builder.username {
let user = UserId::parse(username)?;
inner_builder = inner_builder.server_name(user.server_name());
} else {
anyhow::bail!(
"Failed to build: One of homeserver_url, server_name or username must be called."
);
inner_builder = match builder.homeserver_cfg {
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
Some(HomeserverConfig::ServerName(server_name)) => {
let server_name = ServerName::parse(server_name)?;
inner_builder.server_name(&server_name)
}
Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
inner_builder.server_name_or_homeserver_url(server_name_or_url)
}
None => {
if let Some(username) = builder.username {
let user = UserId::parse(username)?;
inner_builder.server_name(user.server_name())
} else {
return Err(ClientBuildError::Generic {
message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
});
}
}
};
let mut certificates = Vec::new();
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
if let Ok(cert) = Certificate::from_der(&certificate) {
certificates.push(cert);
} else {
let cert =
Certificate::from_pem(&certificate).map_err(|e| ClientBuildError::Generic {
message: format!("Failed to add a root certificate {e:?}"),
})?;
certificates.push(cert);
}
}
inner_builder = inner_builder.add_root_certificates(certificates);
if let Some(proxy) = builder.proxy {
inner_builder = inner_builder.proxy(proxy);
}
@@ -217,11 +311,12 @@ impl ClientBuilder {
server_versions
.iter()
.map(|s| MatrixVersion::try_from(s.as_str()))
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()?,
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
);
}
let sdk_client = RUNTIME.block_on(async move { inner_builder.build().await })?;
let sdk_client = inner_builder.build().await?;
// At this point, `sdk_client` might contain a `sliding_sync_proxy` that has
// been configured by the homeserver (if it's a `ServerName` and the
@@ -247,31 +342,3 @@ impl ClientBuilder {
)?)
}
}
impl Default for ClientBuilder {
fn default() -> Self {
let encryption_settings = EncryptionSettings {
auto_enable_cross_signing: true,
auto_enable_backups: true,
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
};
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
Self {
base_path: None,
username: None,
server_name: None,
homeserver_url: None,
server_versions: None,
passphrase: Zeroizing::new(None),
user_agent: None,
sliding_sync_proxy: None,
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
inner,
cross_process_refresh_lock_id: None,
session_delegate: None,
}
}
}
+42 -1
View File
@@ -1,7 +1,10 @@
use std::sync::Arc;
use futures_util::StreamExt;
use matrix_sdk::encryption::{backups, recovery};
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use thiserror::Error;
use zeroize::Zeroize;
@@ -34,6 +37,11 @@ pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[uniffi::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
fn on_update(&self, status: VerificationState);
}
#[derive(uniffi::Enum)]
pub enum BackupUploadState {
Waiting,
@@ -186,6 +194,23 @@ impl From<recovery::EnableProgress> for EnableRecoveryProgress {
}
}
#[derive(uniffi::Enum)]
pub enum VerificationState {
Unknown,
Verified,
Unverified,
}
impl From<encryption::VerificationState> for VerificationState {
fn from(value: encryption::VerificationState) -> Self {
match &value {
encryption::VerificationState::Unknown => Self::Unknown,
encryption::VerificationState::Verified => Self::Verified,
encryption::VerificationState::Unverified => Self::Unverified,
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl Encryption {
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
@@ -326,4 +351,20 @@ impl Encryption {
Ok(result?)
}
pub fn verification_state(&self) -> VerificationState {
self.inner.verification_state().get().into()
}
pub fn verification_state_listener(
self: Arc<Self>,
listener: Box<dyn VerificationStateListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.verification_state();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
while let Some(verification_state) = subscriber.next().await {
listener.on_update(verification_state.into());
}
})))
}
}
+8 -2
View File
@@ -1,8 +1,8 @@
use std::fmt::Display;
use matrix_sdk::{
self, encryption::CryptoStoreError, oidc::OidcError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
IdParseError, NotificationSettingsError as SdkNotificationSettingsError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use uniffi::UnexpectedUniFFICallbackError;
@@ -115,6 +115,12 @@ impl From<RoomError> for ClientError {
}
}
impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
+110
View File
@@ -216,3 +216,113 @@ where
event.as_original().context("Failed to get original content")?.content.clone();
Ok(original_content)
}
#[derive(Clone, uniffi::Enum)]
pub enum StateEventType {
CallMember,
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomJoinRules,
RoomMemberEvent,
RoomName,
RoomPinnedEvents,
RoomPowerLevels,
RoomServerAcl,
RoomThirdPartyInvite,
RoomTombstone,
RoomTopic,
SpaceChild,
SpaceParent,
}
impl From<StateEventType> for ruma::events::StateEventType {
fn from(val: StateEventType) -> Self {
match val {
StateEventType::CallMember => Self::CallMember,
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
StateEventType::RoomAliases => Self::RoomAliases,
StateEventType::RoomAvatar => Self::RoomAvatar,
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
StateEventType::RoomCreate => Self::RoomCreate,
StateEventType::RoomEncryption => Self::RoomEncryption,
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
StateEventType::RoomJoinRules => Self::RoomJoinRules,
StateEventType::RoomMemberEvent => Self::RoomMember,
StateEventType::RoomName => Self::RoomName,
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
StateEventType::RoomServerAcl => Self::RoomServerAcl,
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
StateEventType::RoomTombstone => Self::RoomTombstone,
StateEventType::RoomTopic => Self::RoomTopic,
StateEventType::SpaceChild => Self::SpaceChild,
StateEventType::SpaceParent => Self::SpaceParent,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum MessageLikeEventType {
CallAnswer,
CallCandidates,
CallHangup,
CallInvite,
KeyVerificationAccept,
KeyVerificationCancel,
KeyVerificationDone,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationReady,
KeyVerificationStart,
PollEnd,
PollResponse,
PollStart,
Reaction,
RoomEncrypted,
RoomMessage,
RoomRedaction,
Sticker,
UnstablePollEnd,
UnstablePollResponse,
UnstablePollStart,
}
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
fn from(val: MessageLikeEventType) -> Self {
match val {
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallHangup => Self::CallHangup,
MessageLikeEventType::CallCandidates => Self::CallCandidates,
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
MessageLikeEventType::Reaction => Self::Reaction,
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
MessageLikeEventType::RoomMessage => Self::RoomMessage,
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
MessageLikeEventType::Sticker => Self::Sticker,
MessageLikeEventType::PollEnd => Self::PollEnd,
MessageLikeEventType::PollResponse => Self::PollResponse,
MessageLikeEventType::PollStart => Self::PollStart,
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
}
}
}
+2
View File
@@ -32,6 +32,7 @@ mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_directory_search;
mod room_info;
mod room_list;
mod room_member;
@@ -40,6 +41,7 @@ mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod timeline_event_filter;
mod tracing;
mod utils;
mod widget;
@@ -21,6 +21,7 @@ pub enum NotificationEvent {
pub struct NotificationSenderInfo {
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub is_name_ambiguous: bool,
}
#[derive(uniffi::Record)]
@@ -63,6 +64,7 @@ impl NotificationItem {
sender_info: NotificationSenderInfo {
display_name: item.sender_display_name,
avatar_url: item.sender_avatar_url,
is_name_ambiguous: item.is_sender_name_ambiguous,
},
room_info: NotificationRoomInfo {
display_name: item.room_display_name,
@@ -297,10 +297,10 @@ impl NotificationSettings {
Ok(enabled)
}
/// Check if [MSC 4028 push rule][rule] is enabled.
/// Returns true if [MSC 4028 push rule][rule] is supported and enabled.
///
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
pub async fn can_push_encrypted_event_to_device(&self) -> bool {
let notification_settings = self.sdk_notification_settings.read().await;
// Check stable identifier
if let Ok(enabled) = notification_settings
@@ -319,6 +319,13 @@ impl NotificationSettings {
}
}
/// Check whether [MSC 4028 push rule][rule] is enabled on the homeserver.
///
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
self.sdk_client.can_homeserver_push_encrypted_event_to_device().await.unwrap()
}
/// Set whether user mentions are enabled.
pub async fn set_user_mention_enabled(
&self,
+17 -1
View File
@@ -6,6 +6,7 @@ use opentelemetry::KeyValue;
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
use tokio::runtime::Handle;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_core::Subscriber;
use tracing_subscriber::{
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
@@ -202,13 +203,26 @@ where
}
let file_layer = config.write_to_files.map(|c| {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&c.file_prefix);
if let Some(max_files) = c.max_files {
builder = builder.max_log_files(max_files as usize)
};
if let Some(file_suffix) = c.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
fmt::layer()
.event_format(EventFormatter::new())
// EventFormatter doesn't support ANSI colors anyways, but the
// default field formatter does, which is unhelpful for iOS +
// Android logs, but enabled by default.
.with_ansi(false)
.with_writer(tracing_appender::rolling::hourly(c.path, c.file_prefix))
.with_writer(writer)
});
Layer::and_then(
@@ -237,6 +251,8 @@ where
pub struct TracingFileConfiguration {
path: String,
file_prefix: String,
file_suffix: Option<String>,
max_files: Option<u64>,
}
#[derive(uniffi::Record)]
+225 -37
View File
@@ -1,14 +1,23 @@
use std::{convert::TryFrom, sync::Arc};
use std::sync::Arc;
use anyhow::{Context, Result};
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
use matrix_sdk::{
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::RoomExt;
use mime::Mime;
use ruma::{
api::client::room::report_content,
assign,
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
EventId, UserId,
events::{
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
},
TimelineEventType,
},
EventId, Int, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
@@ -17,10 +26,11 @@ use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
error::{ClientError, MediaInfoError, RoomError},
event::{MessageLikeEventType, StateEventType},
room_info::RoomInfo,
room_member::{MessageLikeEventType, RoomMember, StateEventType},
room_member::RoomMember,
ruma::ImageInfo,
timeline::{EventTimelineItem, Timeline},
timeline::{EventTimelineItem, ReceiptType, Timeline},
utils::u64_to_uint,
TaskHandle,
};
@@ -124,34 +134,37 @@ impl Room {
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
}
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
pub fn inviter(&self) -> Option<RoomMember> {
if self.inner.state() == RoomState::Invited {
RUNTIME.block_on(async move {
self.inner
.invite_details()
.await
.ok()
.and_then(|a| a.inviter)
.map(|m| Arc::new(RoomMember::new(m)))
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
})
} else {
None
}
}
pub async fn timeline(&self) -> Arc<Timeline> {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
timeline.clone()
} else {
let timeline = Timeline::new(self.inner.timeline().await);
*write_guard = Some(timeline.clone());
timeline
}
/// Forces the currently active room key, which is used to encrypt messages,
/// to be rotated.
///
/// A new room key will be crated and shared with all the room members the
/// next time a message will be sent. You don't have to call this method,
/// room keys will be rotated automatically when necessary. This method is
/// still useful for debugging purposes.
pub async fn discard_room_key(&self) -> Result<(), ClientError> {
self.inner.discard_room_key().await?;
Ok(())
}
pub async fn poll_history(&self) -> Arc<Timeline> {
Timeline::new(self.inner.poll_history().await)
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
Ok(timeline.clone())
} else {
let timeline = Timeline::new(self.inner.timeline().await?);
*write_guard = Some(timeline.clone());
Ok(timeline)
}
}
pub fn display_name(&self) -> Result<String, ClientError> {
@@ -171,10 +184,16 @@ impl Room {
Ok(Arc::new(RoomMembersIterator::new(self.inner.members(RoomMemberships::empty()).await?)))
}
pub async fn member(&self, user_id: String) -> Result<Arc<RoomMember>, ClientError> {
pub async fn members_no_sync(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
Ok(Arc::new(RoomMembersIterator::new(
self.inner.members_no_sync(RoomMemberships::empty()).await?,
)))
}
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = self.inner.get_member(&user_id).await?.context("No user found")?;
Ok(Arc::new(RoomMember::new(member)))
Ok(member.into())
}
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
@@ -250,6 +269,24 @@ impl Room {
})))
}
pub async fn set_is_favourite(
&self,
is_favourite: bool,
tag_order: Option<f64>,
) -> Result<(), ClientError> {
self.inner.set_is_favourite(is_favourite, tag_order).await?;
Ok(())
}
pub async fn set_is_low_priority(
&self,
is_low_priority: bool,
tag_order: Option<f64>,
) -> Result<(), ClientError> {
self.inner.set_is_low_priority(is_low_priority, tag_order).await?;
Ok(())
}
/// Redacts an event from the room.
///
/// # Arguments
@@ -317,13 +354,11 @@ impl Room {
///
/// # Arguments
///
/// * `event_id` - The ID of the user to ignore.
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user_id = UserId::parse(user_id)?;
self.inner.client().account().ignore_user(&user_id).await?;
Ok(())
})
/// * `user_id` - The ID of the user to ignore.
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
let user_id = UserId::parse(user_id)?;
self.inner.client().account().ignore_user(&user_id).await?;
Ok(())
}
/// Leave this room.
@@ -414,9 +449,14 @@ impl Room {
})
}
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
pub async fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_redact(&user_id).await?)
Ok(self.inner.can_user_redact_own(&user_id).await?)
}
pub async fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.can_user_redact_other(&user_id).await?)
}
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
@@ -494,6 +534,140 @@ impl Room {
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
Ok(self.inner.typing_notice(is_typing).await?)
}
pub fn subscribe_to_typing_notifications(
self: Arc<Self>,
listener: Box<dyn TypingNotificationsListener>,
) -> Arc<TaskHandle> {
Arc::new(TaskHandle::new(RUNTIME.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 {
let typing_user_ids =
typing_user_ids.into_iter().map(|user_id| user_id.to_string()).collect();
listener.call(typing_user_ids);
}
})))
}
/// Set (or unset) a flag on the room to indicate that the user has
/// explicitly marked it as unread.
pub async fn set_unread_flag(&self, new_value: bool) -> Result<(), ClientError> {
Ok(self.inner.set_unread_flag(new_value).await?)
}
/// Mark a room as read, by attaching a read receipt on the latest event.
///
/// Note: this does NOT unset the unread flag; it's the caller's
/// responsibility to do so, if needs be.
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
let timeline = self.timeline().await?;
timeline.mark_as_read(receipt_type).await?;
Ok(())
}
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
let power_levels = self.inner.room_power_levels().await?;
Ok(RoomPowerLevels::from(power_levels))
}
pub async fn apply_power_level_changes(
&self,
changes: RoomPowerLevelChanges,
) -> Result<(), ClientError> {
self.inner.apply_power_level_changes(changes).await?;
Ok(())
}
pub async fn update_power_levels_for_users(
&self,
updates: Vec<UserPowerLevelUpdate>,
) -> Result<(), ClientError> {
let updates = updates
.iter()
.map(|update| {
let user_id: &UserId = update.user_id.as_str().try_into()?;
let power_level = Int::new(update.power_level).context("Invalid power level")?;
Ok((user_id, power_level))
})
.collect::<Result<Vec<_>>>()?;
self.inner
.update_power_levels(updates)
.await
.map_err(|e| ClientError::Generic { msg: e.to_string() })?;
Ok(())
}
pub async fn suggested_role_for_user(
&self,
user_id: String,
) -> Result<RoomMemberRole, ClientError> {
let user_id = UserId::parse(&user_id)?;
Ok(self.inner.get_suggested_user_role(&user_id).await?)
}
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
}
pub async fn matrix_to_permalink(&self) -> Result<String, ClientError> {
Ok(self.inner.matrix_to_permalink().await?.to_string())
}
pub async fn matrix_to_event_permalink(&self, event_id: String) -> Result<String, ClientError> {
let event_id = EventId::parse(event_id)?;
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
}
}
#[derive(uniffi::Record)]
pub struct RoomPowerLevels {
/// The level required to ban a user.
pub ban: i64,
/// The level required to invite a user.
pub invite: i64,
/// The level required to kick a user.
pub kick: i64,
/// The level required to redact an event.
pub redact: i64,
/// The default level required to send message events.
pub events_default: i64,
/// The default level required to send state events.
pub state_default: i64,
/// The default power level for every user in the room.
pub users_default: i64,
/// The level required to change the room's name.
pub room_name: i64,
/// The level required to change the room's avatar.
pub room_avatar: i64,
/// The level required to change the room's topic.
pub room_topic: i64,
}
impl From<RumaPowerLevels> for RoomPowerLevels {
fn from(value: RumaPowerLevels) -> Self {
fn state_event_level_for(
power_levels: &RumaPowerLevels,
event_type: &TimelineEventType,
) -> i64 {
let default_state: i64 = power_levels.state_default.into();
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
}
Self {
ban: value.ban.into(),
invite: value.invite.into(),
kick: value.kick.into(),
redact: value.redact.into(),
events_default: value.events_default.into(),
state_default: value.state_default.into(),
users_default: value.users_default.into(),
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
}
}
}
#[uniffi::export(callback_interface)]
@@ -501,6 +675,11 @@ pub trait RoomInfoListener: Sync + Send {
fn call(&self, room_info: RoomInfo);
}
#[uniffi::export(callback_interface)]
pub trait TypingNotificationsListener: Sync + Send {
fn call(&self, typing_user_ids: Vec<String>);
}
#[derive(uniffi::Object)]
pub struct RoomMembersIterator {
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
@@ -518,13 +697,22 @@ impl RoomMembersIterator {
self.chunk_iterator.len()
}
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<Arc<RoomMember>>> {
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
self.chunk_iterator
.next(chunk_size)
.map(|members| members.into_iter().map(RoomMember::new).map(Arc::new).collect())
.map(|members| members.into_iter().map(|m| m.into()).collect())
}
}
/// An update for a particular user's power level within the room.
#[derive(uniffi::Record)]
pub struct UserPowerLevelUpdate {
/// The user ID of the user to update.
user_id: String,
/// The power level to assign to the user.
power_level: i64,
}
impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
type Error = MediaInfoError;
@@ -0,0 +1,175 @@
// Copyright 2024 Mauro Romito
// 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::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use tokio::sync::RwLock;
use super::RUNTIME;
use crate::{error::ClientError, task_handle::TaskHandle};
#[derive(uniffi::Enum)]
pub enum PublicRoomJoinRule {
Public,
Knock,
}
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
type Error = String;
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
match value {
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
rule => Err(format!("unsupported join rule: {rule:?}")),
}
}
}
#[derive(uniffi::Record)]
pub struct RoomDescription {
pub room_id: String,
pub name: Option<String>,
pub topic: Option<String>,
pub alias: Option<String>,
pub avatar_url: Option<String>,
pub join_rule: Option<PublicRoomJoinRule>,
pub is_world_readable: bool,
pub joined_members: u64,
}
impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
Self {
room_id: value.room_id.to_string(),
name: value.name,
topic: value.topic,
alias: value.alias.map(|alias| alias.to_string()),
avatar_url: value.avatar_url.map(|url| url.to_string()),
join_rule: value.join_rule.try_into().ok(),
is_world_readable: value.is_world_readable,
joined_members: value.joined_members,
}
}
}
#[derive(uniffi::Object)]
pub struct RoomDirectorySearch {
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
}
impl RoomDirectorySearch {
pub fn new(inner: SdkRoomDirectorySearch) -> Self {
Self { inner: RwLock::new(inner) }
}
}
#[uniffi::export(async_runtime = "tokio")]
impl RoomDirectorySearch {
pub async fn next_page(&self) -> Result<(), ClientError> {
let mut inner = self.inner.write().await;
inner.next_page().await?;
Ok(())
}
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
let mut inner = self.inner.write().await;
inner.search(filter, batch_size).await?;
Ok(())
}
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
let inner = self.inner.read().await;
Ok(inner.loaded_pages() as u32)
}
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
let inner = self.inner.read().await;
Ok(inner.is_at_last_page())
}
pub async fn results(
&self,
listener: Box<dyn RoomDirectorySearchEntriesListener>,
) -> 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(),
}]);
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
}
})))
}
}
#[derive(uniffi::Record)]
pub struct RoomDirectorySearchEntriesResult {
pub entries_stream: Arc<TaskHandle>,
}
#[derive(uniffi::Enum)]
pub enum RoomDirectorySearchEntryUpdate {
Append { values: Vec<RoomDescription> },
Clear,
PushFront { value: RoomDescription },
PushBack { value: RoomDescription },
PopFront,
PopBack,
Insert { index: u32, value: RoomDescription },
Set { index: u32, value: RoomDescription },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<RoomDescription> },
}
impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
for RoomDirectorySearchEntryUpdate
{
fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: value.into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
}
}
}
}
#[uniffi::export(callback_interface)]
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+16 -5
View File
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::RoomState;
use ruma::OwnedMxcUri;
@@ -18,19 +18,23 @@ pub struct RoomInfo {
is_public: bool,
is_space: bool,
is_tombstoned: bool,
is_favourite: bool,
canonical_alias: Option<String>,
alternative_aliases: Vec<String>,
membership: Membership,
latest_event: Option<Arc<EventTimelineItem>>,
inviter: Option<Arc<RoomMember>>,
inviter: Option<RoomMember>,
active_members_count: u64,
invited_members_count: u64,
joined_members_count: u64,
user_power_levels: HashMap<String, i64>,
highlight_count: u64,
notification_count: u64,
user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
/// Whether this room has been explicitly marked as unread
is_marked_unread: bool,
/// "Interesting" messages received in that room, independently of the
/// notification settings.
num_unread_messages: u64,
@@ -50,6 +54,12 @@ impl RoomInfo {
) -> matrix_sdk::Result<Self> {
let unread_notification_counts = room.unread_notification_counts();
let power_levels_map = room.users_with_power_levels().await;
let mut user_power_levels = HashMap::<String, i64>::new();
for (id, level) in power_levels_map.iter() {
user_power_levels.insert(id.to_string(), *level);
}
Ok(Self {
id: room.room_id().to_string(),
name: room.name(),
@@ -59,19 +69,19 @@ impl RoomInfo {
is_public: room.is_public(),
is_space: room.is_space(),
is_tombstoned: room.is_tombstoned(),
is_favourite: room.is_favourite(),
canonical_alias: room.canonical_alias().map(Into::into),
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
membership: room.state().into(),
latest_event,
inviter: match room.state() {
RoomState::Invited => {
room.invite_details().await?.inviter.map(|inner| Arc::new(RoomMember { inner }))
}
RoomState::Invited => room.invite_details().await?.inviter.map(|m| m.into()),
_ => None,
},
active_members_count: room.active_members_count(),
invited_members_count: room.invited_members_count(),
joined_members_count: room.joined_members_count(),
user_power_levels,
highlight_count: unread_notification_counts.highlight_count,
notification_count: unread_notification_counts.notification_count,
user_defined_notification_mode: room
@@ -84,6 +94,7 @@ impl RoomInfo {
.iter()
.map(|u| u.to_string())
.collect(),
is_marked_unread: room.is_marked_unread(),
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
+136 -40
View File
@@ -1,7 +1,7 @@
use std::{fmt::Debug, sync::Arc, time::Duration};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{pin_mut, StreamExt, TryFutureExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::{
@@ -12,9 +12,18 @@ use matrix_sdk::{
},
RoomListEntry as MatrixRoomListEntry,
};
use matrix_sdk_ui::room_list_service::filters::{
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
new_filter_normalized_match_room_name,
use matrix_sdk_ui::{
room_list_service::{
filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_non_left,
new_filter_none, new_filter_normalized_match_room_name, new_filter_unread,
RoomCategory,
},
BoxedFilterFn,
},
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use tokio::sync::RwLock;
@@ -23,6 +32,7 @@ use crate::{
room::Room,
room_info::RoomInfo,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
TaskHandle, RUNTIME,
};
@@ -38,6 +48,14 @@ pub enum RoomListError {
RoomNotFound { room_name: String },
#[error("invalid room ID: {error}")]
InvalidRoomId { error: String },
#[error("A timeline instance already exists for room {room_name}")]
TimelineAlreadyExists { room_name: String },
#[error("A timeline instance hasn't been initialized for room {room_name}")]
TimelineNotInitialized { room_name: String },
#[error("Timeline couldn't be initialized: {error}")]
InitializingTimeline { error: String },
#[error("Event cache ran into an error: {error}")]
EventCache { error: String },
}
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
@@ -49,6 +67,13 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
UnknownList(list_name) => Self::UnknownList { list_name },
InputCannotBeApplied(_) => Self::InputCannotBeApplied,
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
TimelineAlreadyExists(room_id) => {
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
}
InitializingTimeline(source) => {
Self::InitializingTimeline { error: source.to_string() }
}
EventCache(error) => Self::EventCache { error: error.to_string() },
}
}
}
@@ -83,6 +108,7 @@ impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
#[derive(uniffi::Object)]
pub struct RoomListService {
pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
@@ -104,6 +130,7 @@ impl RoomListService {
Ok(Arc::new(RoomListItem {
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
utd_hook: self.utd_hook.clone(),
}))
}
@@ -193,7 +220,10 @@ impl RoomList {
listener: Box<dyn RoomListEntriesListener>,
) -> RoomListEntriesWithDynamicAdaptersResult {
let (entries_stream, dynamic_entries_controller) =
self.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
self.inner.entries_with_dynamic_adapters(
page_size.try_into().unwrap(),
self.room_list_service.inner.client().roominfo_update_receiver(),
);
RoomListEntriesWithDynamicAdaptersResult {
controller: Arc::new(RoomListDynamicEntriesController::new(
@@ -375,19 +405,8 @@ impl RoomListDynamicEntriesController {
#[uniffi::export]
impl RoomListDynamicEntriesController {
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
use RoomListEntriesDynamicFilterKind as Kind;
match kind {
Kind::All => self.inner.set_filter(new_filter_all()),
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
Kind::None => self.inner.set_filter(new_filter_none()),
Kind::NormalizedMatchRoomName { pattern } => {
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
}
Kind::FuzzyMatchRoomName { pattern } => {
self.inner.set_filter(new_filter_fuzzy_match_room_name(&self.client, &pattern))
}
}
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
self.inner.set_filter(filter)
}
fn add_one_page(&self) {
@@ -401,16 +420,68 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All,
AllNonLeft,
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonLeft,
Unread,
Favourite,
Invite,
Category { expect: RoomListFilterCategory },
None,
NormalizedMatchRoomName { pattern: String },
FuzzyMatchRoomName { pattern: String },
}
#[derive(uniffi::Enum)]
pub enum RoomListFilterCategory {
Group,
People,
}
impl From<RoomListFilterCategory> for RoomCategory {
fn from(value: RoomListFilterCategory) -> Self {
match value {
RoomListFilterCategory::Group => Self::Group,
RoomListFilterCategory::People => Self::People,
}
}
}
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
/// a `BoxedFilterFn`.
struct FilterWrapper(BoxedFilterFn);
impl FilterWrapper {
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
use RoomListEntriesDynamicFilterKind as Kind;
match value {
Kind::All { filters } => Self(Box::new(new_filter_all(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
Kind::Any { filters } => Self(Box::new(new_filter_any(
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
))),
Kind::NonLeft => Self(Box::new(new_filter_non_left(client))),
Kind::Unread => Self(Box::new(new_filter_unread(client))),
Kind::Favourite => Self(Box::new(new_filter_favourite(client))),
Kind::Invite => Self(Box::new(new_filter_invite(client))),
Kind::Category { expect } => Self(Box::new(new_filter_category(client, expect.into()))),
Kind::None => Self(Box::new(new_filter_none())),
Kind::NormalizedMatchRoomName { pattern } => {
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))
}
Kind::FuzzyMatchRoomName { pattern } => {
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
}
}
}
}
#[derive(uniffi::Object)]
pub struct RoomListItem {
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
@@ -441,20 +512,53 @@ impl RoomListItem {
Ok(RoomInfo::new(self.inner.inner_room(), avatar_url, latest_event).await?)
}
/// Building a `Room`.
///
/// Be careful that building a `Room` builds its entire `Timeline` at the
/// same time.
async fn full_room(&self) -> Arc<Room> {
Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
))
/// Building a `Room`. If its internal timeline hasn't been initialized
/// it'll fail.
async fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if let Some(timeline) = self.inner.timeline() {
Ok(Arc::new(Room::with_timeline(
self.inner.inner_room().clone(),
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
)))
} else {
Err(RoomListError::TimelineNotInitialized {
room_name: self.inner.inner_room().room_id().to_string(),
})
}
}
// Temporary workaround for coroutine leaks on Kotlin.
fn full_room_blocking(&self) -> Arc<Room> {
RUNTIME.block_on(async move { self.full_room().await })
/// Checks whether the Room's timeline has been initialized before.
fn is_timeline_initialized(&self) -> bool {
self.inner.is_timeline_initialized()
}
/// Initializes the timeline for this room using the provided parameters.
///
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
/// used to filter timeline events besides the default timeline filter. If
/// `None` is passed, only the default timeline filter will be used.
async fn init_timeline(
&self,
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
) -> Result<(), RoomListError> {
let mut timeline_builder = self
.inner
.default_room_timeline_builder()
.await
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
if let Some(event_type_filter) = event_type_filter {
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
});
}
if let Some(utd_hook) = self.utd_hook.clone() {
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
}
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
}
fn subscribe(&self, settings: Option<RoomSubscription>) {
@@ -468,14 +572,6 @@ impl RoomListItem {
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
}
fn has_unread_notifications(&self) -> bool {
self.inner.has_unread_notifications()
}
fn unread_notifications(&self) -> Arc<UnreadNotificationsCount> {
Arc::new(self.inner.unread_notifications().into())
}
}
#[derive(Clone, Debug, uniffi::Enum)]
+39 -185
View File
@@ -1,7 +1,7 @@
use matrix_sdk::room::RoomMember as SdkRoomMember;
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::UserId;
use super::RUNTIME;
use crate::ClientError;
use crate::error::ClientError;
#[derive(Clone, uniffi::Enum)]
pub enum MembershipState {
@@ -35,203 +35,57 @@ impl From<matrix_sdk::ruma::events::room::member::MembershipState> for Membershi
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
MembershipState::Leave
}
_ => todo!(
_ => unimplemented!(
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomMember {
pub(crate) inner: SdkRoomMember,
#[uniffi::export]
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
RoomMemberRole::suggested_role_for_power_level(power_level)
}
#[uniffi::export]
impl RoomMember {
pub fn user_id(&self) -> String {
self.inner.user_id().to_string()
}
pub fn display_name(&self) -> Option<String> {
self.inner.display_name().map(|d| d.to_owned())
}
pub fn avatar_url(&self) -> Option<String> {
self.inner.avatar_url().map(ToString::to_string)
}
pub fn membership(&self) -> MembershipState {
self.inner.membership().to_owned().into()
}
pub fn is_name_ambiguous(&self) -> bool {
self.inner.name_ambiguous()
}
pub fn power_level(&self) -> i64 {
self.inner.power_level()
}
pub fn normalized_power_level(&self) -> i64 {
self.inner.normalized_power_level()
}
pub fn is_ignored(&self) -> bool {
self.inner.is_ignored()
}
pub fn is_account_user(&self) -> bool {
self.inner.is_account_user()
}
/// Adds the room member to the current account data's ignore list
/// which will ignore the user across all rooms.
pub fn ignore(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.ignore().await?;
Ok(())
})
}
/// Removes the room member from the current account data's ignore list
/// which will unignore the user across all rooms.
pub fn unignore(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.unignore().await?;
Ok(())
})
}
pub fn can_ban(&self) -> bool {
self.inner.can_ban()
}
pub fn can_invite(&self) -> bool {
self.inner.can_invite()
}
pub fn can_kick(&self) -> bool {
self.inner.can_kick()
}
pub fn can_redact(&self) -> bool {
self.inner.can_redact()
}
pub fn can_send_state(&self, state_event: StateEventType) -> bool {
self.inner.can_send_state(state_event.into())
}
pub fn can_send_message(&self, event: MessageLikeEventType) -> bool {
self.inner.can_send_message(event.into())
}
pub fn can_trigger_room_notification(&self) -> bool {
self.inner.can_trigger_room_notification()
}
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
// It's not possible to expose methods on an Enum through Uniffi ☹️
role.suggested_power_level()
}
impl RoomMember {
pub fn new(room_member: SdkRoomMember) -> Self {
RoomMember { inner: room_member }
}
/// Generates a `matrix.to` permalink from to the given userID.
#[uniffi::export]
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
let user_id = UserId::parse(user_id)?;
Ok(user_id.matrix_to_uri().to_string())
}
#[derive(Clone, uniffi::Enum)]
pub enum StateEventType {
CallMember,
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomJoinRules,
RoomMemberEvent,
RoomName,
RoomPinnedEvents,
RoomPowerLevels,
RoomServerAcl,
RoomThirdPartyInvite,
RoomTombstone,
RoomTopic,
SpaceChild,
SpaceParent,
#[derive(uniffi::Record)]
pub struct RoomMember {
pub user_id: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub membership: MembershipState,
pub is_name_ambiguous: bool,
pub power_level: i64,
pub normalized_power_level: i64,
pub is_ignored: bool,
pub suggested_role_for_power_level: RoomMemberRole,
}
impl From<StateEventType> for ruma::events::StateEventType {
fn from(val: StateEventType) -> Self {
match val {
StateEventType::CallMember => Self::CallMember,
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
StateEventType::RoomAliases => Self::RoomAliases,
StateEventType::RoomAvatar => Self::RoomAvatar,
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
StateEventType::RoomCreate => Self::RoomCreate,
StateEventType::RoomEncryption => Self::RoomEncryption,
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
StateEventType::RoomJoinRules => Self::RoomJoinRules,
StateEventType::RoomMemberEvent => Self::RoomMember,
StateEventType::RoomName => Self::RoomName,
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
StateEventType::RoomServerAcl => Self::RoomServerAcl,
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
StateEventType::RoomTombstone => Self::RoomTombstone,
StateEventType::RoomTopic => Self::RoomTopic,
StateEventType::SpaceChild => Self::SpaceChild,
StateEventType::SpaceParent => Self::SpaceParent,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum MessageLikeEventType {
CallAnswer,
CallInvite,
CallHangup,
CallCandidates,
KeyVerificationReady,
KeyVerificationStart,
KeyVerificationCancel,
KeyVerificationAccept,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationDone,
ReactionSent,
RoomEncrypted,
RoomMessage,
RoomRedaction,
Sticker,
}
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
fn from(val: MessageLikeEventType) -> Self {
match val {
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallHangup => Self::CallHangup,
MessageLikeEventType::CallCandidates => Self::CallCandidates,
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
MessageLikeEventType::ReactionSent => Self::Reaction,
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
MessageLikeEventType::RoomMessage => Self::RoomMessage,
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
MessageLikeEventType::Sticker => Self::Sticker,
impl From<SdkRoomMember> for RoomMember {
fn from(m: SdkRoomMember) -> Self {
RoomMember {
user_id: m.user_id().to_string(),
display_name: m.display_name().map(|s| s.to_owned()),
avatar_url: m.avatar_url().map(|a| a.to_string()),
membership: m.membership().clone().into(),
is_name_ambiguous: m.name_ambiguous(),
power_level: m.power_level(),
normalized_power_level: m.normalized_power_level(),
is_ignored: m.is_ignored(),
suggested_role_for_power_level: m.suggested_role_for_power_level(),
}
}
}
+114 -18
View File
@@ -44,8 +44,9 @@ use ruma::{
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
},
},
matrix_uri::MatrixId as RumaMatrixId,
serde::JsonObject,
OwnedUserId, UInt, UserId,
MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId,
};
use tracing::info;
@@ -55,6 +56,72 @@ use crate::{
utils::u64_to_uint,
};
/// Parse a matrix entity from a given URI, be it either
/// a `matrix.to` link or a `matrix:` URI
#[uniffi::export]
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
return Some(MatrixEntity {
id: matrix_uri.id().into(),
via: matrix_uri.via().iter().map(|via| via.to_string()).collect(),
});
}
if let Ok(matrix_to_uri) = MatrixToUri::parse(&uri) {
return Some(MatrixEntity {
id: matrix_to_uri.id().into(),
via: matrix_to_uri.via().iter().map(|via| via.to_string()).collect(),
});
}
None
}
/// A Matrix entity that can be a room, room alias, user, or event, and a list
/// of via servers.
#[derive(uniffi::Record)]
pub struct MatrixEntity {
id: MatrixId,
via: Vec<String>,
}
/// A Matrix ID that can be a room, room alias, user, or event.
#[derive(Clone, uniffi::Enum)]
pub enum MatrixId {
Room { id: String },
RoomAlias { alias: String },
User { id: String },
EventOnRoomId { room_id: String, event_id: String },
EventOnRoomAlias { alias: String, event_id: String },
}
impl From<&RumaMatrixId> for MatrixId {
fn from(value: &RumaMatrixId) -> Self {
match value {
RumaMatrixId::User(id) => MatrixId::User { id: id.to_string() },
RumaMatrixId::Room(id) => MatrixId::Room { id: id.to_string() },
RumaMatrixId::RoomAlias(id) => MatrixId::RoomAlias { alias: id.to_string() },
RumaMatrixId::Event(room_id_or_alias, event_id) => {
if room_id_or_alias.is_room_id() {
MatrixId::EventOnRoomId {
room_id: room_id_or_alias.to_string(),
event_id: event_id.to_string(),
}
} else if room_id_or_alias.is_room_alias_id() {
MatrixId::EventOnRoomAlias {
alias: room_id_or_alias.to_string(),
event_id: event_id.to_string(),
}
} else {
panic!("Unexpected MatrixId type: {:?}", room_id_or_alias)
}
}
_ => panic!("Unexpected MatrixId type: {:?}", value),
}
}
}
#[uniffi::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
@@ -172,23 +239,38 @@ impl TryFrom<MessageType> for RumaMessageType {
formatted: content.formatted.map(Into::into),
}))
}
MessageType::Image { content } => Self::Image(
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Audio { content } => Self::Audio(
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Video { content } => Self::Video(
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::File { content } => Self::File(
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
.filename(content.filename)
.info(content.info.map(Into::into).map(Box::new)),
),
MessageType::Image { content } => {
let mut event_content =
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
Self::Image(event_content)
}
MessageType::Audio { content } => {
let mut event_content =
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
Self::Audio(event_content)
}
MessageType::Video { content } => {
let mut event_content =
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
Self::Video(event_content)
}
MessageType::File { content } => {
let mut event_content =
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
Self::File(event_content)
}
MessageType::Notice { content } => {
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
formatted: content.formatted.map(Into::into),
@@ -221,6 +303,8 @@ impl From<RumaMessageType> for MessageType {
RumaMessageType::Image(c) => MessageType::Image {
content: ImageMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
@@ -228,6 +312,8 @@ impl From<RumaMessageType> for MessageType {
RumaMessageType::Audio(c) => MessageType::Audio {
content: AudioMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
@@ -237,6 +323,8 @@ impl From<RumaMessageType> for MessageType {
RumaMessageType::Video(c) => MessageType::Video {
content: VideoMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
@@ -244,6 +332,7 @@ impl From<RumaMessageType> for MessageType {
RumaMessageType::File(c) => MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
formatted: c.formatted.as_ref().map(Into::into),
filename: c.filename.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
@@ -295,6 +384,8 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<ImageInfo>,
}
@@ -302,6 +393,8 @@ pub struct ImageMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<AudioInfo>,
pub audio: Option<UnstableAudioDetailsContent>,
@@ -311,6 +404,8 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<VideoInfo>,
}
@@ -318,6 +413,7 @@ pub struct VideoMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
+99 -8
View File
@@ -12,13 +12,18 @@
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc};
use std::{fmt::Debug, sync::Arc, time::Duration};
use futures_util::pin_mut;
use matrix_sdk::Client;
use matrix_sdk_ui::sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
use matrix_sdk_ui::{
sync_service::{
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
SyncServiceBuilder as MatrixSyncServiceBuilder,
},
unable_to_decrypt_hook::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
},
};
use crate::{
@@ -53,12 +58,16 @@ pub trait SyncServiceStateObserver: Send + Sync + Debug {
#[derive(uniffi::Object)]
pub struct SyncService {
pub(crate) inner: Arc<MatrixSyncService>,
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
impl SyncService {
pub fn room_list_service(&self) -> Arc<RoomListService> {
Arc::new(RoomListService { inner: self.inner.room_list_service() })
Arc::new(RoomListService {
inner: self.inner.room_list_service(),
utd_hook: self.utd_hook.clone(),
})
}
pub async fn start(&self) {
@@ -85,24 +94,106 @@ impl SyncService {
#[derive(Clone, uniffi::Object)]
pub struct SyncServiceBuilder {
builder: MatrixSyncServiceBuilder,
utd_hook: Option<Arc<UtdHookManager>>,
}
impl SyncServiceBuilder {
pub(crate) fn new(client: Client) -> Arc<Self> {
Arc::new(Self { builder: MatrixSyncService::builder(client) })
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook: None })
}
}
#[uniffi::export(async_runtime = "tokio")]
impl SyncServiceBuilder {
pub fn with_unified_invites_in_room_list(
self: Arc<Self>,
with_unified_invites: bool,
) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_unified_invites_in_room_list(with_unified_invites);
Arc::new(Self { builder, utd_hook: this.utd_hook })
}
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock(app_identifier);
Arc::new(Self { builder })
Arc::new(Self { builder, utd_hook: this.utd_hook })
}
pub fn with_utd_hook(self: Arc<Self>, delegate: Box<dyn UnableToDecryptDelegate>) -> Arc<Self> {
// UTDs detected before this duration may be reclassified as "late decryption"
// events (or discarded, if they get decrypted fast enough).
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
let this = unwrap_or_clone_arc(self);
let utd_hook = Some(Arc::new(
UtdHookManager::new(Arc::new(UtdHook { delegate }))
.with_max_delay(UTD_HOOK_GRACE_PERIOD),
));
Arc::new(Self { builder: this.builder, utd_hook })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
Ok(Arc::new(SyncService {
inner: Arc::new(this.builder.build().await?),
utd_hook: this.utd_hook,
}))
}
}
#[uniffi::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
struct UtdHook {
delegate: Box<dyn UnableToDecryptDelegate>,
}
impl std::fmt::Debug for UtdHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UtdHook").finish_non_exhaustive()
}
}
impl UnableToDecryptHook for UtdHook {
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
// not considered UTDs.
if let Some(duration) = &info.time_to_decrypt {
if *duration < IGNORE_UTD_PERIOD {
return;
}
}
// Report the UTD to the client.
self.delegate.on_utd(info.into());
}
}
#[derive(uniffi::Record)]
pub struct UnableToDecryptInfo {
/// The identifier of the event that couldn't get decrypted.
event_id: String,
/// If the event could be decrypted late (that is, the event was encrypted
/// at first, but could be decrypted later on), then this indicates the
/// time it took to decrypt the event. If it is not set, this is
/// considered a definite UTD.
///
/// If set, this is in milliseconds.
pub time_to_decrypt_ms: Option<u64>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
fn from(value: SdkUnableToDecryptInfo) -> Self {
Self {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
}
}
}
@@ -14,6 +14,7 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use tracing::warn;
@@ -40,6 +41,7 @@ impl TimelineItemContent {
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
@@ -112,6 +114,7 @@ pub enum TimelineItemContentKind {
end_time: Option<u64>,
has_been_edited: bool,
},
CallInvite,
UnableToDecrypt {
msg: EncryptedMessage,
},
@@ -307,7 +310,7 @@ pub enum OtherState {
RoomJoinRules,
RoomName { name: Option<String> },
RoomPinnedEvents,
RoomPowerLevels,
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
RoomServerAcl,
RoomThirdPartyInvite { display_name: Option<String> },
RoomTombstone,
@@ -350,7 +353,20 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Self::RoomName { name }
}
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
Content::RoomPowerLevels(c) => match c {
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
users: power_level_user_changes(content, prev_content)
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
previous: prev_content.as_ref().map(|prev_content| {
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
}),
},
FullContent::Redacted(_) => {
Self::RoomPowerLevels { users: Default::default(), previous: None }
}
},
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
let display_name = match c {
+47 -10
View File
@@ -52,19 +52,21 @@ use tokio::{
use tracing::{error, info, warn};
use uuid::Uuid;
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
use crate::{
client::ProgressWatcher,
error::{ClientError, RoomError},
helpers::unwrap_or_clone_arc,
ruma::{AssetType, AudioInfo, FileInfo, ImageInfo, PollKind, ThumbnailInfo, VideoInfo},
ruma::{
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo,
VideoInfo,
},
task_handle::TaskHandle,
RUNTIME,
};
mod content;
pub use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
#[derive(uniffi::Object)]
#[repr(transparent)]
pub struct Timeline {
@@ -106,12 +108,12 @@ impl Timeline {
async fn send_attachment(
&self,
url: String,
filename: String,
mime_type: Mime,
attachment_config: AttachmentConfig,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<(), RoomError> {
let request = self.inner.send_attachment(url, mime_type, attachment_config);
let request = self.inner.send_attachment(filename, mime_type, attachment_config);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
RUNTIME.spawn(async move {
@@ -196,6 +198,17 @@ impl Timeline {
})
}
/// Mark the room as read by trying to attach an *unthreaded* read receipt
/// to the latest room event.
///
/// This works even if the latest event belongs to a thread, as a threaded
/// reply also belongs to the unthreaded timeline. No threaded receipt
/// will be sent here (see also #3123).
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
self.inner.mark_as_read(receipt_type.into()).await?;
Ok(())
}
pub fn send(self: Arc<Self>, msg: Arc<RoomMessageEventContentWithoutRelation>) {
RUNTIME.spawn(async move {
self.inner.send((*msg).to_owned().with_relation(None).into()).await;
@@ -207,6 +220,8 @@ impl Timeline {
url: String,
thumbnail_url: Option<String>,
image_info: ImageInfo,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
@@ -227,7 +242,9 @@ impl Timeline {
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
};
}
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
@@ -238,6 +255,8 @@ impl Timeline {
url: String,
thumbnail_url: Option<String>,
video_info: VideoInfo,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
@@ -258,7 +277,9 @@ impl Timeline {
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
_ => AttachmentConfig::new().info(attachment_info),
};
}
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
@@ -268,6 +289,8 @@ impl Timeline {
self: Arc<Self>,
url: String,
audio_info: AudioInfo,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
@@ -280,7 +303,10 @@ impl Timeline {
.map_err(|_| RoomError::InvalidAttachmentData)?;
let attachment_info = AttachmentInfo::Audio(base_audio_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
@@ -291,6 +317,8 @@ impl Timeline {
url: String,
audio_info: AudioInfo,
waveform: Vec<u16>,
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Arc<SendAttachmentJoinHandle> {
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
@@ -304,7 +332,10 @@ impl Timeline {
let attachment_info =
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
let attachment_config = AttachmentConfig::new().info(attachment_info);
let attachment_config = AttachmentConfig::new()
.info(attachment_info)
.caption(caption)
.formatted_caption(formatted_caption.map(Into::into));
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
}))
@@ -534,6 +565,12 @@ impl Timeline {
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
})
}
pub async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
let latest_event = self.inner.latest_event().await;
latest_event.map(|item| Arc::new(EventTimelineItem(item)))
}
}
#[derive(uniffi::Record)]
@@ -991,7 +1028,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
fn from(value: ReceiptType) -> Self {
match value {
ReceiptType::Read => Self::Read,
ReceiptType::ReadPrivate => Self::Read,
ReceiptType::ReadPrivate => Self::ReadPrivate,
ReceiptType::FullyRead => Self::FullyRead,
}
}
@@ -0,0 +1,55 @@
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,
}
#[uniffi::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 -1
View File
@@ -1,3 +1,4 @@
[bindings.kotlin]
package_name = "org.matrix.rustcomponents.sdk"
cdylib_name = "matrix_sdk_ffi"
cdylib_name = "matrix_sdk_ffi"
android_cleaner = true
+6
View File
@@ -1,3 +1,9 @@
# unreleased
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
- `AmbiguityCache` contains the room member's user ID
# 0.7.0
- Rename `RoomType` to `RoomState`
+8 -2
View File
@@ -23,6 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
message-ids = ["matrix-sdk-crypto?/message-ids"]
experimental-sliding-sync = ["ruma/unstable-msc3575"]
uniffi = ["dep:uniffi"]
# helpers for testing features build upon this
testing = [
@@ -38,7 +39,7 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = "2.1.0"
bitflags = { version = "2.4.0", features = ["serde"] }
eyeball = { workspace = true }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
@@ -48,12 +49,13 @@ matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption = { workspace = true }
matrix-sdk-test = { workspace = true, optional = true }
once_cell = { workspace = true }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867"] }
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uniffi = { workspace = true, optional = true }
[dev-dependencies]
assert_matches = { workspace = true }
@@ -63,9 +65,13 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
web-time = "1.1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.33"
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+2 -47
View File
@@ -14,14 +14,10 @@
//! Helpers for creating `std::fmt::Debug` implementations.
use std::{collections::BTreeMap, fmt};
use std::fmt;
pub use matrix_sdk_common::debug::*;
use ruma::{
api::client::{push::get_notifications::v3::Notification, sync::sync_events::v3::InvitedRoom},
serde::Raw,
OwnedRoomId,
};
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
/// that only prints the event type of each item.
@@ -36,47 +32,6 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
}
}
/// A wrapper around a notification map as found in `/sync` responses that
/// implements `Debug` in a way that only prints the event ID and event type
/// for the raw events contained in each notification.
pub struct DebugNotificationMap<'a>(pub &'a BTreeMap<OwnedRoomId, Vec<Notification>>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugNotificationMap<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut map = f.debug_map();
map.entries(self.0.iter().map(|(room_id, raw)| (room_id, DebugNotificationList(raw))));
map.finish()
}
}
struct DebugNotificationList<'a>(&'a [Notification]);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugNotificationList<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(DebugNotification));
list.finish()
}
}
struct DebugNotification<'a>(&'a Notification);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugNotification<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Notification")
.field("actions", &self.0.actions)
.field("event", &DebugRawEvent(&self.0.event))
.field("profile_tag", &self.0.profile_tag)
.field("read", &self.0.read)
.field("room_id", &self.0.room_id)
.field("ts", &self.0.ts)
.finish()
}
}
/// A wrapper around an invited room as found in `/sync` responses that
/// implements `Debug` in a way that only prints the event ID and event type for
/// the raw events contained in `invite_state`.
@@ -14,7 +14,7 @@
//! SDK-specific variations of response types from Ruma.
use std::{collections::BTreeMap, fmt};
use std::{collections::BTreeMap, fmt, iter};
pub use matrix_sdk_common::deserialized_responses::*;
use ruma::{
@@ -23,7 +23,7 @@ use ruma::{
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
AnyStrippedStateEvent, AnySyncStateEvent, EventContentFromType,
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
},
@@ -34,9 +34,12 @@ use serde::Serialize;
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AmbiguityChange {
/// The user ID of the member that is contained in the state key of the
/// `m.room.member` event.
pub member_id: OwnedUserId,
/// Is the member that is contained in the state key of the `m.room.member`
/// event itself ambiguous because of the event.
pub member_ambiguous: bool,
@@ -46,6 +49,15 @@ pub struct AmbiguityChange {
pub ambiguated_member: Option<OwnedUserId>,
}
impl AmbiguityChange {
/// Get an iterator over the user IDs listed in this `AmbiguityChange`.
pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
iter::once(&*self.member_id)
.chain(self.disambiguated_member.as_deref())
.chain(self.ambiguated_member.as_deref())
}
}
/// Collection of ambiguity changes that room member events trigger.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
@@ -66,6 +78,16 @@ pub struct MembersResponse {
pub ambiguity_changes: AmbiguityChanges,
}
/// Wrapper around both versions of any event received via sync.
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum RawAnySyncOrStrippedTimelineEvent {
/// An event from a room in joined or left state.
Sync(Raw<AnySyncTimelineEvent>),
/// An event from a room in invited state.
Stripped(Raw<AnyStrippedStateEvent>),
}
/// Wrapper around both versions of any raw state event.
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
+5
View File
@@ -56,4 +56,9 @@ pub enum Error {
#[cfg(feature = "e2e-encryption")]
#[error(transparent)]
MegolmError(#[from] MegolmError),
/// An error caused by calling the `BaseClient::receive_all_members`
/// function with invalid parameters
#[error("receive_all_members function was called with invalid parameters")]
InvalidReceiveMembersParameters,
}
+93 -13
View File
@@ -9,7 +9,10 @@ use ruma::events::{
poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent,
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
};
use ruma::{MxcUri, OwnedEventId};
use ruma::{
events::{call::invite::SyncCallInviteEvent, relation::RelationType},
MxcUri, OwnedEventId,
};
use serde::{Deserialize, Serialize};
use crate::MinimalRoomMemberEvent;
@@ -25,11 +28,15 @@ pub enum PossibleLatestEvent<'a> {
YesRoomMessage(&'a SyncRoomMessageEvent),
/// This message is suitable - it is a poll
YesPoll(&'a SyncUnstablePollStartEvent),
/// This message is suitable - it is a call invite
YesCallInvite(&'a SyncCallInviteEvent),
// Later: YesState(),
// Later: YesReaction(),
/// Not suitable - it's a state event
NoUnsupportedEventType,
/// Not suitable - it's not an m.room.message
/// Not suitable - it's not a m.room.message or an edit/replacement
NoUnsupportedMessageLikeType,
/// Not suitable - it's encrypted
NoEncrypted,
@@ -40,15 +47,37 @@ pub enum PossibleLatestEvent<'a> {
#[cfg(feature = "e2e-encryption")]
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
match event {
// Suitable - we have an m.room.message that was not redacted
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
PossibleLatestEvent::YesRoomMessage(message)
// Check if this is a replacement for another message. If it is, ignore it
if let Some(original_message) = message.as_original() {
let is_replacement =
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
relation_type == RelationType::Replacement
} else {
false
}
});
if is_replacement {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
} else {
return PossibleLatestEvent::YesRoomMessage(message);
}
}
return PossibleLatestEvent::YesRoomMessage(message);
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
PossibleLatestEvent::YesPoll(poll)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
PossibleLatestEvent::YesCallInvite(invite)
}
// Encrypted events are not suitable
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
PossibleLatestEvent::NoEncrypted
@@ -225,10 +254,15 @@ mod tests {
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use ruma::{
events::{
call::{
invite::{CallInviteEventContent, SyncCallInviteEvent},
SessionDescription,
},
poll::unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
UnstablePollStartContentBlock,
},
relation::Replacement,
room::{
encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
@@ -236,7 +270,7 @@ mod tests {
},
message::{
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
RoomMessageEventContent, SyncRoomMessageEvent,
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
},
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
ImageInfo, MediaSource,
@@ -249,14 +283,14 @@ mod tests {
},
owned_event_id, owned_mxc_uri, owned_user_id,
serde::Raw,
MilliSecondsSinceUnixEpoch, UInt,
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
};
use serde_json::json;
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
#[test]
fn room_messages_are_suitable() {
fn test_room_messages_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomMessageEventContent::new(MessageType::Image(
@@ -280,7 +314,7 @@ mod tests {
}
#[test]
fn polls_are_suitable() {
fn test_polls_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
@@ -303,7 +337,29 @@ mod tests {
}
#[test]
fn different_types_of_messagelike_are_unsuitable() {
fn test_call_invites_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
content: CallInviteEventContent::new(
"call_id".into(),
UInt::new(123).unwrap(),
SessionDescription::new("".into(), "".into()),
VoipVersionId::V1,
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
);
}
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
content: StickerEventContent::new(
@@ -325,7 +381,7 @@ mod tests {
}
#[test]
fn redacted_messages_are_suitable() {
fn test_redacted_messages_are_suitable() {
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
"content": {},
@@ -353,7 +409,7 @@ mod tests {
}
#[test]
fn encrypted_messages_are_unsuitable() {
fn test_encrypted_messages_are_unsuitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomEncryptedEventContent::new(
@@ -373,7 +429,7 @@ mod tests {
}
#[test]
fn state_events_are_unsuitable() {
fn test_state_events_are_unsuitable() {
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
content: RoomTopicEventContent::new("".to_owned()),
@@ -392,7 +448,31 @@ mod tests {
}
#[test]
fn deserialize_latest_event() {
fn test_replacement_events_are_unsuitable() {
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
owned_event_id!("$1"),
RoomMessageEventContent::text_plain("Hello, world!").into(),
)));
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: event_content,
event_id: owned_event_id!("$2"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_matches!(
is_suitable_for_latest_event(&event),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[test]
fn test_deserialize_latest_event() {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct TestStruct {
latest_event: LatestEvent,
+8 -3
View File
@@ -31,15 +31,20 @@ pub mod latest_event;
pub mod media;
mod rooms;
mod read_receipts;
pub mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;
pub mod store;
pub mod sync;
#[cfg(any(test, feature = "testing"))]
mod test_utils;
mod utils;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
pub use client::BaseClient;
#[cfg(any(test, feature = "testing"))]
pub use http;
@@ -47,8 +52,8 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomMember, RoomMemberships,
RoomState, RoomStateFilter,
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomInfoUpdate, RoomMember,
RoomMemberships, RoomState, RoomStateFilter,
};
pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError};
pub use utils::{
File diff suppressed because it is too large Load Diff
+21 -15
View File
@@ -52,8 +52,12 @@ pub struct RoomMember {
}
impl RoomMember {
pub(crate) fn from_parts(member_info: MemberInfo, room_info: &MemberRoomInfo<'_>) -> Self {
let MemberInfo { event, profile, presence } = member_info;
pub(crate) fn from_parts(
event: MemberEvent,
profile: Option<MinimalRoomMemberEvent>,
presence: Option<PresenceEvent>,
room_info: &MemberRoomInfo<'_>,
) -> Self {
let MemberRoomInfo {
power_levels,
max_power_level,
@@ -122,7 +126,8 @@ impl RoomMember {
/// Get the normalized power level of this member.
///
/// The normalized power level depends on the maximum power level that can
/// be found in a certain room, it's always in the range of 0-100.
/// be found in a certain room, positive values are always in the range of
/// 0-100.
pub fn normalized_power_level(&self) -> i64 {
if self.max_power_level > 0 {
(self.power_level() * 100) / self.max_power_level
@@ -160,11 +165,19 @@ impl RoomMember {
self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
}
/// Whether this user can redact events based on the power levels.
/// Whether this user can redact their own events based on the power levels.
///
/// Same as `member.can_do(PowerLevelAction::Redact)`.
pub fn can_redact(&self) -> bool {
self.can_do_impl(|pls| pls.user_can_redact(self.user_id()))
/// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
pub fn can_redact_own(&self) -> bool {
self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
}
/// Whether this user can redact events of other users based on the power
/// levels.
///
/// Same as `member.can_do(PowerLevelAction::RedactOther)`.
pub fn can_redact_other(&self) -> bool {
self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
}
/// Whether this user can send message events based on the power levels.
@@ -222,14 +235,7 @@ impl RoomMember {
}
}
// Information about a room member.
pub(crate) struct MemberInfo {
pub event: MemberEvent,
pub(crate) profile: Option<MinimalRoomMemberEvent>,
pub(crate) presence: Option<PresenceEvent>,
}
// Information about a the room a member is in.
// Information about the room a member is in.
pub(crate) struct MemberRoomInfo<'a> {
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
pub(crate) max_power_level: i64,
+78 -2
View File
@@ -11,7 +11,7 @@ use std::{
use bitflags::bitflags;
pub use members::RoomMember;
pub use normal::{Room, RoomInfo, RoomState, RoomStateFilter};
pub use normal::{Room, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter};
use ruma::{
assign,
events::{
@@ -30,6 +30,7 @@ use ruma::{
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
tag::{TagName, Tags},
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
},
@@ -104,6 +105,15 @@ pub struct BaseRoomInfo {
/// memberships.
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
/// Whether this room has been manually marked as unread.
#[serde(default)]
pub(crate) is_marked_unread: bool,
/// Some notable tags.
///
/// We are not interested by all the tags. Some tags are more important than
/// others, and this field collects them.
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
pub(crate) notable_tags: RoomNotableTags,
}
impl BaseRoomInfo {
@@ -285,6 +295,36 @@ impl BaseRoomInfo {
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
}
}
pub fn handle_notable_tags(&mut self, tags: &Tags) {
let mut notable_tags = RoomNotableTags::empty();
if tags.contains_key(&TagName::Favorite) {
notable_tags.insert(RoomNotableTags::FAVOURITE);
}
if tags.contains_key(&TagName::LowPriority) {
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
}
self.notable_tags = notable_tags;
}
}
bitflags! {
/// Notable tags, i.e. subset of tags that we are more interested by.
///
/// We are not interested by all the tags. Some tags are more important than
/// others, and this struct describes them.
#[repr(transparent)]
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub(crate) struct RoomNotableTags: u8 {
/// The `m.favourite` tag.
const FAVOURITE = 0b0000_0001;
/// THe `m.lowpriority` tag.
const LOW_PRIORITY = 0b0000_0010;
}
}
trait OptionExt {
@@ -317,6 +357,8 @@ impl Default for BaseRoomInfo {
tombstone: None,
topic: None,
rtc_member: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
}
}
}
@@ -520,7 +562,11 @@ impl RoomMemberships {
#[cfg(test)]
mod tests {
use super::{calculate_room_name, DisplayName};
use std::ops::Not;
use ruma::events::tag::{TagInfo, TagName, Tags};
use super::{calculate_room_name, BaseRoomInfo, DisplayName, RoomNotableTags};
#[test]
fn test_calculate_room_name() {
@@ -554,4 +600,34 @@ mod tests {
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
}
#[test]
fn test_handle_notable_tags_favourite() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::Favorite, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
}
#[test]
fn test_handle_notable_tags_low_priority() {
let mut base_room_info = BaseRoomInfo::default();
let mut tags = Tags::new();
tags.insert(TagName::LowPriority, TagInfo::default());
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
tags.clear();
base_room_info.handle_notable_tags(&tags);
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
}
}
+292 -56
View File
@@ -17,7 +17,7 @@ use std::sync::RwLock as SyncRwLock;
use std::{
collections::{BTreeMap, HashSet},
mem,
sync::Arc,
sync::{atomic::AtomicBool, Arc},
};
use bitflags::bitflags;
@@ -34,6 +34,7 @@ use ruma::{
ignored_user_list::IgnoredUserListEventContent,
receipt::{Receipt, ReceiptThread, ReceiptType},
room::{
avatar::RoomAvatarEventContent,
encryption::RoomEncryptionEventContent,
guest_access::GuestAccess,
history_visibility::HistoryVisibility,
@@ -53,11 +54,12 @@ use ruma::{
RoomId, RoomVersionId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, field::debug, info, instrument, trace, warn};
use tokio::sync::broadcast;
use tracing::{debug, field::debug, info, instrument, warn};
use super::{
members::{MemberInfo, MemberRoomInfo},
BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent, RoomMember,
members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent,
RoomMember, RoomNotableTags,
};
#[cfg(feature = "experimental-sliding-sync")]
use crate::latest_event::LatestEvent;
@@ -69,6 +71,20 @@ use crate::{
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
};
/// A summary of changes to room information.
///
/// It also indicates whether this update should update the room list.
#[derive(Debug, Clone)]
pub struct RoomInfoUpdate {
/// The room which was updated.
pub room_id: OwnedRoomId,
/// Whether this event should trigger the room list to update.
///
/// If the change is minor or if another action already causes the room list
/// to update, this should be false to avoid duplicate updates.
pub trigger_room_list_update: bool,
}
/// The underlying room data structure collecting state for joined, left and
/// invited rooms.
#[derive(Debug, Clone)]
@@ -76,6 +92,7 @@ pub struct Room {
room_id: OwnedRoomId,
own_user_id: OwnedUserId,
inner: SharedObservable<RoomInfo>,
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
store: Arc<DynStateStore>,
/// The most recent few encrypted events. When the keys come through to
@@ -133,23 +150,27 @@ impl From<&MembershipState> for RoomState {
impl Room {
/// The size of the latest_encrypted_events RingBuffer
// SAFETY: `new_unchecked` is safe because 10 is not zero.
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
const MAX_ENCRYPTED_EVENTS: usize = 10;
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
pub(crate) fn new(
own_user_id: &UserId,
store: Arc<DynStateStore>,
room_id: &RoomId,
room_state: RoomState,
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
) -> Self {
let room_info = RoomInfo::new(room_id, room_state);
Self::restore(own_user_id, store, room_info)
Self::restore(own_user_id, store, room_info, roominfo_update_sender)
}
pub(crate) fn restore(
own_user_id: &UserId,
store: Arc<DynStateStore>,
room_info: RoomInfo,
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
) -> Self {
Self {
own_user_id: own_user_id.into(),
@@ -160,6 +181,7 @@ impl Room {
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
Self::MAX_ENCRYPTED_EVENTS,
))),
roominfo_update_sender,
}
}
@@ -196,6 +218,11 @@ impl Room {
self.inner.read().read_receipts.num_unread
}
/// Get the detailed information about read receipts for the room.
pub fn read_receipts(&self) -> RoomReadReceipts {
self.inner.read().read_receipts.clone()
}
/// Get the number of unread notifications (computed client-side).
///
/// This might be more precise than [`Self::unread_notification_counts`] for
@@ -335,6 +362,12 @@ impl Room {
self.inner.read().base_info.dm_targets.clone()
}
/// If this room is a direct message, returns the number of members that
/// we're sharing the room with.
pub fn direct_targets_length(&self) -> usize {
self.inner.read().base_info.dm_targets.len()
}
/// Is the room encrypted.
pub fn is_encrypted(&self) -> bool {
self.inner.read().is_encrypted()
@@ -514,10 +547,7 @@ impl Room {
for event in member_events {
let profile = profiles.remove(event.user_id());
let presence = presences.remove(event.user_id());
let member_info = MemberInfo { event, profile, presence };
members.push(RoomMember::from_parts(member_info, &room_info))
members.push(RoomMember::from_parts(event, profile, presence, &room_info))
}
Ok(members)
@@ -630,10 +660,17 @@ impl Room {
self.inner.get()
}
/// Update the inner summary with the given RoomInfo, and notify
/// subscribers.
pub fn set_room_info(&self, room_info: RoomInfo) {
/// Update the summary with given RoomInfo.
///
/// This also triggers an update for room info observers if
/// `trigger_room_list_update` is true.
pub fn set_room_info(&self, room_info: RoomInfo, trigger_room_list_update: bool) {
self.inner.set(room_info);
// Ignore error if no receiver exists.
let _ = self
.roominfo_update_sender
.send(RoomInfoUpdate { room_id: self.room_id.clone(), trigger_room_list_update });
}
/// Get the `RoomMember` with the given `user_id`.
@@ -641,30 +678,23 @@ impl Room {
/// Returns `None` if the member was never part of this room, otherwise
/// return a `RoomMember` that can be in a joined, invited, left, banned
/// state.
#[instrument(skip(self))]
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
trace!("Fetching member event");
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
debug!("Member event not found in state store");
debug!(%user_id, "Member event not found in state store");
return Ok(None);
};
trace!("Deserializing member event");
let event = raw_event.deserialize()?;
trace!("Fetching presence event");
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
trace!("Fetching profile");
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let display_names = [event.display_name().to_owned()];
let room_info = self.member_room_info(&display_names).await?;
trace!("Got all member information");
let member_info = MemberInfo { event, profile, presence };
Ok(Some(RoomMember::from_parts(member_info, &room_info)))
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
}
/// The current `MemberRoomInfo` for this room.
@@ -675,18 +705,15 @@ impl Room {
let max_power_level = self.max_power_level();
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
trace!("Fetching power levels");
let power_levels = self
.store
.get_state_event_static(self.room_id())
.await?
.and_then(|e| e.deserialize().ok());
trace!("Fetching users based on display names");
let users_display_names =
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
trace!("Fetching ignored users");
let ignored_users = self
.store
.get_account_data_event_static::<IgnoredUserListEventContent>()
@@ -718,6 +745,21 @@ impl Room {
}
}
/// Check whether the room is marked as favourite.
///
/// A room is considered favourite if it has received the `m.favourite` tag.
pub fn is_favourite(&self) -> bool {
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
}
/// Check whether the room is marked as low priority.
///
/// A room is considered low priority if it has received the `m.lowpriority`
/// tag.
pub fn is_low_priority(&self) -> bool {
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
}
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
/// `receipt_type`, `thread` and `user_id` in this room.
pub async fn load_user_receipt(
@@ -742,6 +784,12 @@ impl Room {
.get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
.await
}
/// Returns a boolean indicating if this room has been manually marked as
/// unread
pub fn is_marked_unread(&self) -> bool {
self.inner.read().base_info.is_marked_unread
}
}
/// The underlying pure data structure for joined and left rooms.
@@ -787,6 +835,12 @@ pub struct RoomInfo {
/// Base room info which holds some basic event contents important for the
/// room state.
pub(crate) base_info: Box<BaseRoomInfo>,
/// Did we already warn about an unknown room version in
/// [`RoomInfo::room_version_or_default`]? This is done to avoid
/// spamming about unknown room versions in the log for the same room.
#[serde(skip)]
pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -822,6 +876,7 @@ impl RoomInfo {
latest_event: None,
read_receipts: Default::default(),
base_info: Box::new(BaseRoomInfo::new()),
warned_about_unknown_room_version: Arc::new(false.into()),
}
}
@@ -938,7 +993,7 @@ impl RoomInfo {
#[cfg(feature = "experimental-sliding-sync")]
if let Some(latest_event) = &mut self.latest_event {
trace!("Checking if redaction applies to latest event");
tracing::trace!("Checking if redaction applies to latest event");
if latest_event.event_id().as_deref() == Some(redacts) {
match apply_redaction(&latest_event.event().event, _raw, room_version) {
Some(redacted) => {
@@ -964,6 +1019,16 @@ impl RoomInfo {
}));
}
/// Update the room avatar
pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
self.base_info.avatar = url.map(|url| {
let mut content = RoomAvatarEventContent::new();
content.url = Some(url);
MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
});
}
/// Update the notifications count
pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
self.notification_counts = notification_counts;
@@ -1037,6 +1102,26 @@ impl RoomInfo {
self.base_info.room_version()
}
/// Get the room version of this room, or a sensible default.
///
/// Will warn (at most once) if the room creation event is missing from this
/// [`RoomInfo`].
pub fn room_version_or_default(&self) -> RoomVersionId {
use std::sync::atomic::Ordering;
self.base_info.room_version().cloned().unwrap_or_else(|| {
if self
.warned_about_unknown_room_version
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
warn!("Unknown room version, falling back to v10");
}
RoomVersionId::V10
})
}
/// Get the room type of this room.
pub fn room_type(&self) -> Option<&RoomType> {
match self.base_info.create.as_ref()? {
@@ -1140,6 +1225,12 @@ impl RoomInfo {
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.active_room_call_memberships().iter().map(|(user_id, _)| user_id.clone()).collect()
}
/// Returns the latest (decrypted) event recorded for this room.
#[cfg(feature = "experimental-sliding-sync")]
pub fn latest_event(&self) -> Option<&LatestEvent> {
self.latest_event.as_deref()
}
}
#[cfg(feature = "experimental-sliding-sync")]
@@ -1230,10 +1321,9 @@ impl RoomStateFilter {
#[cfg(test)]
mod tests {
use std::{
ops::Sub,
ops::{Not, Sub},
str::FromStr,
sync::Arc,
time::{Duration, SystemTime},
};
use assign::assign;
@@ -1262,6 +1352,8 @@ mod tests {
user_id, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId,
};
use serde_json::json;
use stream_assert::{assert_pending, assert_ready};
use web_time::{Duration, SystemTime};
#[cfg(feature = "experimental-sliding-sync")]
use super::SyncInfo;
@@ -1270,12 +1362,12 @@ mod tests {
use crate::latest_event::LatestEvent;
use crate::{
store::{MemoryStore, StateChanges, StateStore},
DisplayName, MinimalStateEvent, OriginalMinimalStateEvent,
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta,
};
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn room_info_serialization() {
fn test_room_info_serialization() {
// This test exists to make sure we don't accidentally change the
// serialized format for `RoomInfo`.
@@ -1303,6 +1395,7 @@ mod tests {
))),
base_info: Box::new(BaseRoomInfo::new()),
read_receipts: Default::default(),
warned_about_unknown_room_version: Arc::new(false.into()),
};
let info_json = json!({
@@ -1337,6 +1430,7 @@ mod tests {
"encryption": null,
"guest_access": null,
"history_visibility": null,
"is_marked_unread": false,
"join_rules": null,
"max_power_level": 100,
"name": null,
@@ -1347,7 +1441,8 @@ mod tests {
"num_unread": 0,
"num_mentions": 0,
"num_notifications": 0,
"latest_read_receipt_event_id": null,
"latest_active": null,
"pending": []
}
});
@@ -1356,7 +1451,7 @@ mod tests {
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn room_info_deserialization_without_optional_items() {
fn test_room_info_deserialization_without_optional_items() {
// Ensure we can still deserialize RoomInfos before we added things to its
// schema
@@ -1422,12 +1517,153 @@ mod tests {
assert!(info.base_info.topic.is_none());
}
#[async_test]
async fn test_is_favourite() {
// Given a room,
let client = BaseClient::new();
client
.set_session_meta(SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
})
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// Sanity checks to ensure the room isn't marked as favourite.
assert!(room.is_favourite().not());
// Subscribe to the `RoomInfo`.
let mut room_info_subscriber = room.subscribe_info();
assert_pending!(room_info_subscriber);
// Create the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {
"m.favourite": {
"order": 0.0
},
},
},
"type": "m.tag",
}))
.unwrap()
.cast();
// When the new tag is handled and applied.
let mut changes = StateChanges::default();
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
client.apply_changes(&changes, false);
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as favourite.
assert!(room.is_favourite());
// Now, let's remove the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {},
},
"type": "m.tag"
}))
.unwrap()
.cast();
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
client.apply_changes(&changes, false);
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as _not_ favourite.
assert!(room.is_favourite().not());
}
#[async_test]
async fn test_is_low_priority() {
// Given a room,
let client = BaseClient::new();
client
.set_session_meta(SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
})
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// Sanity checks to ensure the room isn't marked as low priority.
assert!(!room.is_low_priority());
// Subscribe to the `RoomInfo`.
let mut room_info_subscriber = room.subscribe_info();
assert_pending!(room_info_subscriber);
// Create the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {
"m.lowpriority": {
"order": 0.0
},
}
},
"type": "m.tag"
}))
.unwrap()
.cast();
// When the new tag is handled and applied.
let mut changes = StateChanges::default();
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
client.apply_changes(&changes, false);
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as low priority.
assert!(room.is_low_priority());
// Now, let's remove the tag.
let tag_raw = Raw::new(&json!({
"content": {
"tags": {},
},
"type": "m.tag"
}))
.unwrap()
.cast();
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
client.apply_changes(&changes, false);
// The `RoomInfo` is getting notified.
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// The room is now marked as _not_ low priority.
assert!(room.is_low_priority().not());
}
fn make_room(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
let user_id = user_id!("@me:example.org");
let room_id = room_id!("!test:localhost");
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
(store.clone(), Room::new(user_id, store, room_id, room_type))
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
}
fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
@@ -1459,13 +1695,13 @@ mod tests {
}
#[async_test]
async fn display_name_for_joined_room_is_empty_if_no_info() {
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
let (_, room) = make_room(RoomState::Joined);
assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty);
}
#[async_test]
async fn display_name_for_joined_room_uses_canonical_alias_if_available() {
async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
let (_, room) = make_room(RoomState::Joined);
room.inner
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
@@ -1473,7 +1709,7 @@ mod tests {
}
#[async_test]
async fn display_name_for_joined_room_prefers_name_over_alias() {
async fn test_display_name_for_joined_room_prefers_name_over_alias() {
let (_, room) = make_room(RoomState::Joined);
room.inner
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
@@ -1484,13 +1720,13 @@ mod tests {
}
#[async_test]
async fn display_name_for_invited_room_is_empty_if_no_info() {
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
let (_, room) = make_room(RoomState::Invited);
assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty);
}
#[async_test]
async fn display_name_for_invited_room_is_empty_if_room_name_empty() {
async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
let (_, room) = make_room(RoomState::Invited);
let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
@@ -1503,7 +1739,7 @@ mod tests {
}
#[async_test]
async fn display_name_for_invited_room_uses_canonical_alias_if_available() {
async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
let (_, room) = make_room(RoomState::Invited);
room.inner
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
@@ -1511,7 +1747,7 @@ mod tests {
}
#[async_test]
async fn display_name_for_invited_room_prefers_name_over_alias() {
async fn test_display_name_for_invited_room_prefers_name_over_alias() {
let (_, room) = make_room(RoomState::Invited);
room.inner
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
@@ -1668,7 +1904,7 @@ mod tests {
}
#[test]
fn setting_the_name_on_room_info_creates_a_fake_event() {
fn test_setting_the_name_on_room_info_creates_a_fake_event() {
// Given a room
let mut room_info = RoomInfo::new(room_id!("!r:e.uk"), RoomState::Joined);
@@ -1689,10 +1925,11 @@ mod tests {
#[cfg(feature = "experimental-sliding-sync")]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_update() {
// Given a room,
let client = crate::BaseClient::new();
let client = BaseClient::new();
client
.set_session_meta(crate::SessionMeta {
.set_session_meta(SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
})
@@ -1717,21 +1954,20 @@ mod tests {
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
// The subscriber isn't notified at this point.
stream_assert::assert_pending!(room_info_subscriber);
assert_pending!(room_info_subscriber);
// Then updating the room info will store the event,
client.apply_changes(&changes);
client.apply_changes(&changes, false);
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
// And wake up the subscriber.
use futures_util::FutureExt as _;
assert!(room_info_subscriber.next().now_or_never().is_some());
stream_assert::assert_pending!(room_info_subscriber);
assert_ready!(room_info_subscriber);
assert_pending!(room_info_subscriber);
}
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
// Given a room with an encrypted event
let (_store, room) = make_room(RoomState::Joined);
add_encrypted_event(&room, "$A");
@@ -1742,7 +1978,7 @@ mod tests {
let event = make_latest_event("$A");
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
// Then is it stored
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
@@ -1750,7 +1986,7 @@ mod tests {
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
// Given a room with some encrypted events and a latest event
let (_store, room) = make_room(RoomState::Joined);
room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
@@ -1764,7 +2000,7 @@ mod tests {
let new_event_index = 1;
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(new_event.clone(), new_event_index, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
// Then the encrypted events list is shortened to only newer events
let enc_evs = room.latest_encrypted_events();
@@ -1778,7 +2014,7 @@ mod tests {
#[test]
#[cfg(feature = "experimental-sliding-sync")]
fn replacing_the_newest_event_leaves_none_left() {
fn test_replacing_the_newest_event_leaves_none_left() {
// Given a room with some encrypted events
let (_store, room) = make_room(RoomState::Joined);
add_encrypted_event(&room, "$0");
@@ -1791,7 +2027,7 @@ mod tests {
let new_event_index = 3;
let mut changes = StateChanges::default();
room.on_latest_event_decrypted(new_event, new_event_index, &mut changes);
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
// Then the encrypted events list ie empty
let enc_evs = room.latest_encrypted_events();
@@ -1900,7 +2136,7 @@ mod tests {
}
#[test]
fn show_correct_active_call_state() {
fn test_show_correct_active_call_state() {
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
// This check also tests the ordering.
@@ -1914,7 +2150,7 @@ mod tests {
}
#[test]
fn active_call_is_false_when_everyone_left() {
fn test_active_call_is_false_when_everyone_left() {
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
let b_empty_membership = call_member_state_event(Vec::new(), "$1234_1", &BOB);
+306 -137
View File
@@ -20,16 +20,13 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::AnyToDeviceEvent;
use ruma::{
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{
v3::{self, InvitedRoom, RoomSummary},
v4,
},
api::client::sync::sync_events::{
v3::{self, InvitedRoom, RoomSummary},
v4,
},
events::{AnySyncStateEvent, AnySyncTimelineEvent},
events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent},
serde::Raw,
OwnedRoomId, RoomId,
JsOption, OwnedRoomId, RoomId,
};
use tracing::{instrument, trace, warn};
@@ -39,12 +36,11 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
deserialized_responses::AmbiguityChanges,
error::Result,
read_receipts::{compute_notifications, PreviousEventsProvider},
read_receipts::{compute_unread_counts, PreviousEventsProvider},
rooms::RoomState,
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse},
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
Room, RoomInfo,
};
@@ -100,7 +96,7 @@ impl BaseClient {
trace!("ready to submit changes to store");
self.store.save_changes(&changes).await?;
self.apply_changes(&changes);
self.apply_changes(&changes, true);
trace!("applied changes");
Ok(to_device)
@@ -153,15 +149,16 @@ impl BaseClient {
self.handle_account_data(&account_data.global, &mut changes).await;
}
let mut new_rooms = Rooms::default();
let mut new_rooms = RoomUpdates::default();
let mut notifications = Default::default();
let mut rooms_account_data = account_data.rooms.clone();
for (room_id, response_room_data) in rooms {
let (room_info, joined_room, left_room, invited_room) = self
.process_sliding_sync_room(
room_id,
response_room_data,
account_data,
&mut rooms_account_data,
&store,
&mut changes,
&mut notifications,
@@ -207,7 +204,7 @@ impl BaseClient {
new_rooms
.join
.entry(room_id.to_owned())
.or_insert_with(JoinedRoom::default)
.or_insert_with(JoinedRoomUpdate::default)
.ephemeral
.push(raw.clone().cast());
}
@@ -217,11 +214,34 @@ impl BaseClient {
new_rooms
.join
.entry(room_id.to_owned())
.or_insert_with(JoinedRoom::default)
.or_insert_with(JoinedRoomUpdate::default)
.ephemeral
.push(raw.clone().cast());
}
// Handle room account data
for (room_id, raw) in &rooms_account_data {
self.handle_room_account_data(room_id, raw, &mut changes).await;
if let Some(room) = self.store.get_room(room_id) {
match room.state() {
RoomState::Joined => new_rooms
.join
.entry(room_id.to_owned())
.or_insert_with(JoinedRoomUpdate::default)
.account_data
.append(&mut raw.to_vec()),
RoomState::Left => new_rooms
.leave
.entry(room_id.to_owned())
.or_insert_with(LeftRoomUpdate::default)
.account_data
.append(&mut raw.to_vec()),
RoomState::Invited => {}
}
}
}
// Rooms in `new_rooms.join` either have a timeline update, or a new read
// receipt. Update the read receipt accordingly.
let user_id = &self.session_meta().expect("logged in user").user_id;
@@ -233,14 +253,18 @@ impl BaseClient {
.cloned()
.or_else(|| self.get_room(room_id).map(|r| r.clone_info()))
{
if compute_notifications(
let prev_read_receipts = room_info.read_receipts.clone();
compute_unread_counts(
user_id,
room_id,
changes.receipts.get(room_id),
previous_events_provider,
previous_events_provider.for_room(room_id),
&joined_room_update.timeline.events,
&mut room_info.read_receipts,
)? {
);
if prev_read_receipts != room_info.read_receipts {
changes.add_room(room_info);
}
}
@@ -268,12 +292,11 @@ impl BaseClient {
trace!("ready to submit changes to store");
store.save_changes(&changes).await?;
self.apply_changes(&changes);
self.apply_changes(&changes, false);
trace!("applied changes");
Ok(SyncResponse {
rooms: new_rooms,
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
notifications,
// FIXME not yet supported by sliding sync.
presence: Default::default(),
@@ -287,26 +310,29 @@ impl BaseClient {
&self,
room_id: &RoomId,
room_data: &v4::SlidingSyncRoom,
account_data: &v4::AccountData,
rooms_account_data: &mut BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
store: &Store,
changes: &mut StateChanges,
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
ambiguity_cache: &mut AmbiguityCache,
) -> Result<(RoomInfo, Option<JoinedRoom>, Option<LeftRoom>, Option<InvitedRoom>)> {
let mut state_events = Self::deserialize_state_events(&room_data.required_state);
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
) -> Result<(RoomInfo, Option<JoinedRoomUpdate>, Option<LeftRoomUpdate>, Option<InvitedRoom>)>
{
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = {
let mut state_events = Vec::new();
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = state_events.into_iter().unzip();
// Read state events from the `required_state` field.
state_events.extend(Self::deserialize_state_events(&room_data.required_state));
// Read state events from the `timeline` field.
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
state_events.into_iter().unzip()
};
// Find or create the room in the store
#[allow(unused_mut)] // Required for some feature flag combinations
let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership(
room_data,
&state_events,
store,
room_id,
changes,
);
let (mut room, mut room_info, invited_room) =
self.process_sliding_sync_room_membership(room_data, &state_events, store, room_id);
room_info.mark_state_partially_synced();
@@ -323,17 +349,22 @@ impl BaseClient {
Default::default()
};
let room_account_data = if let Some(events) = account_data.rooms.get(room_id) {
self.handle_room_account_data(room_id, events, changes).await;
Some(events.to_vec())
} else {
None
};
let push_rules = self.get_push_rules(changes).await?;
if let Some(invite_state) = &room_data.invite_state {
self.handle_invited_state(
&room,
invite_state,
&push_rules,
&mut room_info,
changes,
notifications,
)
.await?;
}
process_room_properties(room_data, &mut room_info);
let push_rules = self.get_push_rules(changes).await?;
let timeline = self
.handle_timeline(
&room,
@@ -375,6 +406,9 @@ impl BaseClient {
let notification_count = room_data.unread_notifications.clone().into();
room_info.update_notification_count(notification_count);
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
let room_account_data = rooms_account_data.get(room_id).cloned();
match room_info.state() {
RoomState::Joined => {
// Ephemeral events are added separately, because we might not
@@ -384,12 +418,13 @@ impl BaseClient {
Ok((
room_info,
Some(JoinedRoom::new(
Some(JoinedRoomUpdate::new(
timeline,
raw_state_events,
room_account_data.unwrap_or_default(),
ephemeral,
notification_count,
ambiguity_changes,
)),
None,
None,
@@ -399,10 +434,11 @@ impl BaseClient {
RoomState::Left => Ok((
room_info,
None,
Some(LeftRoom::new(
Some(LeftRoomUpdate::new(
timeline,
raw_state_events,
room_account_data.unwrap_or_default(),
ambiguity_changes,
)),
None,
)),
@@ -422,10 +458,13 @@ impl BaseClient {
state_events: &[AnySyncStateEvent],
store: &Store,
room_id: &RoomId,
changes: &mut StateChanges,
) -> (Room, RoomInfo, Option<InvitedRoom>) {
if let Some(invite_state) = &room_data.invite_state {
let room = store.get_or_create_room(room_id, RoomState::Invited);
let room = store.get_or_create_room(
room_id,
RoomState::Invited,
self.roominfo_update_sender.clone(),
);
let mut room_info = room.clone_info();
// We don't actually know what events are inside invite_state. In theory, they
@@ -441,15 +480,17 @@ impl BaseClient {
// no content at all.
room_info.mark_as_invited();
self.handle_invited_state(invite_state.as_slice(), &mut room_info, changes);
(
room,
room_info,
Some(v3::InvitedRoom::from(v3::InviteState::from(invite_state.clone()))),
)
} else {
let room = store.get_or_create_room(room_id, RoomState::Joined);
let room = store.get_or_create_room(
room_id,
RoomState::Joined,
self.roominfo_update_sender.clone(),
);
let mut room_info = room.clone_info();
// We default to considering this room joined if it's not an invite. If it's
@@ -487,7 +528,7 @@ impl BaseClient {
if let AnySyncStateEvent::RoomMember(member) = &event {
// If this event updates the current user's membership, record that in the
// room_info.
if member.sender() == meta.user_id && member.state_key() == meta.user_id.as_str() {
if member.state_key() == meta.user_id.as_str() {
room_info.set_state(member.membership().into());
break;
}
@@ -544,8 +585,10 @@ async fn cache_latest_events(
for event in events.iter().rev() {
if let Ok(timeline_event) = event.event.deserialize() {
match is_suitable_for_latest_event(&timeline_event) {
PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) => {
// m.room.message or m.poll.start - we found one! Store it.
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_) => {
// We found a suitable latest event. Store it.
// In order to make the latest event fast to read, we want to keep the
// associated sender in cache. This is a best-effort to gather enough
@@ -639,10 +682,25 @@ async fn cache_latest_events(
}
fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut RoomInfo) {
// Handle the room's name.
if let Some(name) = &room_data.name {
room_info.update_name(name.to_owned());
}
// Handle the room's avatar.
//
// It can be updated via the state events, or via the `SlidingSyncRoom::avatar`
// field. This part of the code handles the latter case. The former case is
// handled by [`BaseClient::handle_state`].
match &room_data.avatar {
// A new avatar!
JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
// Avatar must be removed.
JsOption::Null => room_info.update_avatar(None),
// Nothing to do.
JsOption::Undefined => {}
}
// Sliding sync doesn't have a room summary, nevertheless it contains the joined
// and invited member counts. It likely will never have a heroes concept since
// it calculates the room display name for us.
@@ -672,7 +730,7 @@ mod tests {
use matrix_sdk_test::async_test;
use ruma::{
api::client::sync::sync_events::{v4, UnreadNotificationsCount},
assign, device_id, event_id,
assign, event_id,
events::{
direct::DirectEventContent,
room::{
@@ -686,16 +744,18 @@ mod tests {
},
mxc_uri, room_alias_id, room_id,
serde::Raw,
uint, user_id, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
uint, user_id, JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
};
use serde_json::json;
use super::cache_latest_events;
use crate::{store::MemoryStore, BaseClient, Room, RoomState, SessionMeta};
use crate::{
store::MemoryStore, test_utils::logged_in_base_client, BaseClient, Room, RoomState,
};
#[async_test]
async fn test_notification_count_set() {
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let mut response = v4::Response::new("42".to_owned());
let room_id = room_id!("!room:example.org");
@@ -724,16 +784,16 @@ mod tests {
}
#[async_test]
async fn can_process_empty_sliding_sync_response() {
let client = logged_in_client().await;
async fn test_can_process_empty_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let empty_response = v4::Response::new("5".to_owned());
client.process_sliding_sync(&empty_response, &()).await.expect("Failed to process sync");
}
#[async_test]
async fn room_with_unspecified_state_is_added_to_client_and_joined_list() {
async fn test_room_with_unspecified_state_is_added_to_client_and_joined_list() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
// When I send sliding sync response containing a room (with identifiable data
@@ -751,15 +811,15 @@ mod tests {
assert_eq!(client_room.state(), RoomState::Joined);
// And it is added to the list of joined rooms only.
assert!(sync_resp.rooms.join.get(room_id).is_some());
assert!(sync_resp.rooms.leave.get(room_id).is_none());
assert!(sync_resp.rooms.invite.get(room_id).is_none());
assert!(sync_resp.rooms.join.contains_key(room_id));
assert!(!sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
}
#[async_test]
async fn room_name_is_found_when_processing_sliding_sync_response() {
async fn test_room_name_is_found_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
// When I send sliding sync response containing a room with a name
@@ -775,15 +835,15 @@ mod tests {
assert_eq!(client_room.state(), RoomState::Joined);
// And it is added to the list of joined rooms only.
assert!(sync_resp.rooms.join.get(room_id).is_some());
assert!(sync_resp.rooms.leave.get(room_id).is_none());
assert!(sync_resp.rooms.invite.get(room_id).is_none());
assert!(sync_resp.rooms.join.contains_key(room_id));
assert!(!sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
}
#[async_test]
async fn invited_room_name_is_found_when_processing_sliding_sync_response() {
async fn test_invited_room_name_is_found_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@w:e.uk");
@@ -801,15 +861,15 @@ mod tests {
assert_eq!(client_room.state(), RoomState::Invited);
// And it is added to the list of invited rooms only.
assert!(sync_resp.rooms.join.get(room_id).is_none());
assert!(sync_resp.rooms.leave.get(room_id).is_none());
assert!(sync_resp.rooms.invite.get(room_id).is_some());
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(!sync_resp.rooms.leave.contains_key(room_id));
assert!(sync_resp.rooms.invite.contains_key(room_id));
}
#[async_test]
async fn left_a_room_from_required_state_event() {
async fn test_left_a_room_from_required_state_event() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -830,16 +890,53 @@ mod tests {
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
// And it is added to the list of invited rooms only.
assert!(sync_resp.rooms.join.get(room_id).is_none());
assert!(sync_resp.rooms.leave.get(room_id).is_some());
assert!(sync_resp.rooms.invite.get(room_id).is_none());
// And it is added to the list of left rooms only.
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
}
#[async_test]
async fn left_a_room_from_timeline_state_event() {
async fn test_kick_or_ban_updates_room_to_left() {
for membership in [MembershipState::Leave, MembershipState::Ban] {
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let client = logged_in_base_client(Some(user_a_id)).await;
// When I join…
let mut room = v4::SlidingSyncRoom::new();
set_room_joined(&mut room, user_a_id);
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
// And then get kicked/banned with a `required_state` state event…
let mut room = v4::SlidingSyncRoom::new();
room.required_state.push(make_state_event(
user_b_id,
user_a_id.as_str(),
RoomMemberEventContent::new(membership),
None,
));
let response = response_with_room(room_id, room).await;
let sync_resp =
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// The room is left.
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
// And it is added to the list of left rooms only.
assert!(!sync_resp.rooms.join.contains_key(room_id));
assert!(sync_resp.rooms.leave.contains_key(room_id));
assert!(!sync_resp.rooms.invite.contains_key(room_id));
}
}
#[async_test]
async fn test_left_a_room_from_timeline_state_event() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -861,11 +958,11 @@ mod tests {
}
#[async_test]
async fn can_be_reinvited_to_a_left_room() {
async fn test_can_be_reinvited_to_a_left_room() {
// See https://github.com/matrix-org/matrix-rust-sdk/issues/1834
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -896,13 +993,13 @@ mod tests {
}
#[async_test]
async fn other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
async fn test_other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
// Given we have a DM with B, who is joined
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
// (Sanity: B is a direct target, and is in Join state)
@@ -920,14 +1017,14 @@ mod tests {
}
#[async_test]
async fn other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets(
async fn test_other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets(
) {
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
// Given I have invited B to a DM
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
// (Sanity: B is a direct target, and is in Invite state)
@@ -945,13 +1042,13 @@ mod tests {
}
#[async_test]
async fn members_count_in_a_dm_where_other_person_has_joined() {
async fn test_members_count_in_a_dm_where_other_person_has_joined() {
let room_id = room_id!("!r:bar.org");
let user_a_id = user_id!("@a:bar.org");
let user_b_id = user_id!("@b:bar.org");
// Given we have a DM with B, who is joined
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
// (Sanity: A is in Join state)
@@ -969,13 +1066,13 @@ mod tests {
}
#[async_test]
async fn members_count_in_a_dm_where_other_person_is_invited() {
async fn test_members_count_in_a_dm_where_other_person_is_invited() {
let room_id = room_id!("!r:bar.org");
let user_a_id = user_id!("@a:bar.org");
let user_b_id = user_id!("@b:bar.org");
// Given we have a DM with B, who is joined
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
// (Sanity: A is in Join state)
@@ -993,9 +1090,89 @@ mod tests {
}
#[async_test]
async fn avatar_is_found_when_processing_sliding_sync_response() {
async fn test_avatar_is_found_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
// When I send sliding sync response containing a room with an avatar
let room = {
let mut room = v4::SlidingSyncRoom::new();
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
room
};
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has the avatar
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
}
#[async_test]
async fn test_avatar_can_be_unset_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
// Set the avatar.
// When I send sliding sync response containing a room with an avatar
let room = {
let mut room = v4::SlidingSyncRoom::new();
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
room
};
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has the avatar
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
// No avatar. Still here.
// When I send sliding sync response containing no avatar.
let room = v4::SlidingSyncRoom::new();
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client still has the avatar
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
// Avatar is unset.
// When I send sliding sync response containing an avatar set to `null` (!).
let room = {
let mut room = v4::SlidingSyncRoom::new();
room.avatar = JsOption::Null;
room
};
let response = response_with_room(room_id, room).await;
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
// Then the room in the client has no more avatar
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.avatar_url().is_none());
}
#[async_test]
async fn test_avatar_is_found_from_required_state_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -1013,9 +1190,9 @@ mod tests {
}
#[async_test]
async fn invitation_room_is_added_to_client_and_invite_list() {
async fn test_invitation_room_is_added_to_client_and_invite_list() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -1033,13 +1210,13 @@ mod tests {
// And it is added to the list of invited rooms, not the joined ones
assert!(!sync_resp.rooms.invite[room_id].invite_state.is_empty());
assert!(sync_resp.rooms.join.get(room_id).is_none());
assert!(!sync_resp.rooms.join.contains_key(room_id));
}
#[async_test]
async fn avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
async fn test_avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
@@ -1058,9 +1235,10 @@ mod tests {
}
#[async_test]
async fn canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response() {
async fn test_canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response(
) {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let room_alias_id = room_alias_id!("#myroom:e.uk");
@@ -1077,9 +1255,9 @@ mod tests {
}
#[async_test]
async fn display_name_from_sliding_sync_overrides_alias() {
async fn test_display_name_from_sliding_sync_overrides_alias() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let room_alias_id = room_alias_id!("#myroom:e.uk");
@@ -1100,9 +1278,9 @@ mod tests {
}
#[async_test]
async fn last_event_from_sliding_sync_is_cached() {
async fn test_last_event_from_sliding_sync_is_cached() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let event_a = json!({
"sender":"@alice:example.com",
@@ -1134,9 +1312,9 @@ mod tests {
}
#[async_test]
async fn cached_latest_event_can_be_redacted() {
async fn test_cached_latest_event_can_be_redacted() {
// Given a logged-in client
let client = logged_in_client().await;
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let event_a = json!({
"sender": "@alice:example.com",
@@ -1187,14 +1365,14 @@ mod tests {
}
#[async_test]
async fn when_no_events_we_dont_cache_any() {
async fn test_when_no_events_we_dont_cache_any() {
let events = &[];
let chosen = choose_event_to_cache(events).await;
assert!(chosen.is_none());
}
#[async_test]
async fn when_only_one_event_we_cache_it() {
async fn test_when_only_one_event_we_cache_it() {
let event1 = make_event("m.room.message", "$1");
let events = &[event1.clone()];
let chosen = choose_event_to_cache(events).await;
@@ -1202,7 +1380,7 @@ mod tests {
}
#[async_test]
async fn with_multiple_events_we_cache_the_last_one() {
async fn test_with_multiple_events_we_cache_the_last_one() {
let event1 = make_event("m.room.message", "$1");
let event2 = make_event("m.room.message", "$2");
let events = &[event1, event2.clone()];
@@ -1211,7 +1389,7 @@ mod tests {
}
#[async_test]
async fn cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
let event1 = make_event("m.room.message", "$1");
let event2 = make_event("m.room.message", "$2");
let event3 = make_event("m.room.powerlevels", "$3");
@@ -1222,7 +1400,7 @@ mod tests {
}
#[async_test]
async fn prefer_to_cache_nothing_rather_than_irrelevant_events() {
async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() {
let event1 = make_event("m.room.power_levels", "$1");
let events = &[event1];
let chosen = choose_event_to_cache(events).await;
@@ -1230,7 +1408,7 @@ mod tests {
}
#[async_test]
async fn cache_encrypted_events_that_are_after_latest_message() {
async fn test_cache_encrypted_events_that_are_after_latest_message() {
// Given two message events followed by two encrypted
let event1 = make_event("m.room.message", "$1");
let event2 = make_event("m.room.message", "$2");
@@ -1249,7 +1427,7 @@ mod tests {
rawev_id(event2.clone())
);
room.set_room_info(room_info);
room.set_room_info(room_info, false);
assert_eq!(
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
rawev_id(event2)
@@ -1260,7 +1438,7 @@ mod tests {
}
#[async_test]
async fn dont_cache_encrypted_events_that_are_before_latest_message() {
async fn test_dont_cache_encrypted_events_that_are_before_latest_message() {
// Given an encrypted event before and after the message
let event1 = make_encrypted_event("$1");
let event2 = make_event("m.room.message", "$2");
@@ -1271,7 +1449,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// The latest message is stored
assert_eq!(
@@ -1284,7 +1462,7 @@ mod tests {
}
#[async_test]
async fn skip_irrelevant_events_eg_receipts_even_if_after_message() {
async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() {
// Given two message events followed by two encrypted, with a receipt in the
// middle
let event1 = make_event("m.room.message", "$1");
@@ -1298,7 +1476,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// The latest message is stored, ignoring the receipt
assert_eq!(
@@ -1311,7 +1489,7 @@ mod tests {
}
#[async_test]
async fn only_store_the_max_number_of_encrypted_events() {
async fn test_only_store_the_max_number_of_encrypted_events() {
// Given two message events followed by lots of encrypted and other irrelevant
// events
let evente = make_event("m.room.message", "$e");
@@ -1351,7 +1529,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// The latest message is stored, ignoring encrypted and receipts
assert_eq!(
@@ -1369,7 +1547,7 @@ mod tests {
}
#[async_test]
async fn dont_overflow_capacity_if_previous_encrypted_events_exist() {
async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() {
// Given a RoomInfo with lots of encrypted events already inside it
let room = make_room();
let mut room_info = room.clone_info();
@@ -1392,7 +1570,7 @@ mod tests {
None,
)
.await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// Sanity: room_info has 10 encrypted events inside it
assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10);
@@ -1401,7 +1579,7 @@ mod tests {
let eventa = make_encrypted_event("$a");
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, &[eventa], None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// The oldest event is gone
assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned()));
@@ -1411,7 +1589,7 @@ mod tests {
}
#[async_test]
async fn existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
// Given a RoomInfo with some encrypted events already inside it
let room = make_room();
let mut room_info = room.clone_info();
@@ -1423,13 +1601,13 @@ mod tests {
None,
)
.await;
room.set_room_info(room_info.clone());
room.set_room_info(room_info.clone(), false);
// When I ask to cache an unencrypted event, and some more encrypted events
let eventa = make_event("m.room.message", "$a");
let eventb = make_encrypted_event("$b");
cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
// The only encrypted events stored are the ones after the decrypted one
assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]);
@@ -1442,7 +1620,7 @@ mod tests {
let room = make_room();
let mut room_info = room.clone_info();
cache_latest_events(&room, &mut room_info, events, None, None).await;
room.set_room_info(room_info);
room.set_room_info(room_info, false);
room.latest_event().map(|latest_event| latest_event.event().clone())
}
@@ -1463,11 +1641,14 @@ mod tests {
}
fn make_room() -> Room {
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
Room::new(
user_id!("@u:e.co"),
Arc::new(MemoryStore::new()),
room_id!("!r:e.co"),
RoomState::Joined,
sender,
)
}
@@ -1587,18 +1768,6 @@ mod tests {
.push(make_global_account_data_event(DirectEventContent(direct_content)));
}
async fn logged_in_client() -> BaseClient {
let client = BaseClient::new();
client
.set_session_meta(SessionMeta {
user_id: user_id!("@u:e.uk").to_owned(),
device_id: device_id!("XYZ").to_owned(),
})
.await
.expect("Failed to set session meta");
client
}
async fn response_with_room(room_id: &RoomId, room: v4::SlidingSyncRoom) -> v4::Response {
let mut response = v4::Response::new("5".to_owned());
response.rooms.insert(room_id.to_owned(), room);
@@ -118,6 +118,7 @@ impl AmbiguityCache {
self.update(room_id, old_map, new_map);
let change = AmbiguityChange {
member_id: member_event.state_key().clone(),
disambiguated_member,
ambiguated_member,
member_ambiguous: ambiguous,
@@ -159,12 +160,9 @@ impl AmbiguityCache {
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
use MembershipState::*;
let old_event = if let Some(m) = changes
.state
.get(room_id)
.and_then(|events| events.get(&StateEventType::RoomMember))
.and_then(|m| m.get(member_event.state_key().as_str()))
{
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
}) {
Some(RawMemberEvent::Sync(m.clone().cast()))
} else {
self.store.get_member_event(room_id, member_event.state_key()).await?
@@ -173,20 +171,15 @@ impl AmbiguityCache {
// FIXME: Use let chains once stable
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
if matches!(event.membership(), Join | Invite) {
let display_name = if let Some(d) = changes
.profiles
.get(room_id)
.and_then(|p| p.get(member_event.state_key()))
.and_then(|p| p.as_original())
.and_then(|p| p.content.displayname.as_deref())
{
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
}) {
Some(d.to_owned())
} else if let Some(d) = self
.store
.get_profile(room_id, member_event.state_key())
.await?
.and_then(|p| p.into_original())
.and_then(|p| p.content.displayname)
.and_then(|p| p.into_original()?.content.displayname)
{
Some(d)
} else {
@@ -41,8 +41,8 @@ use crate::{
/// `StateStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the [``]
/// macro.
/// This trait is not meant to be used directly, but will be used with the
/// [`statestore_integration_tests!`] macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStoreIntegrationTests {
@@ -74,6 +74,8 @@ pub trait StateStoreIntegrationTests {
async fn test_stripped_non_stripped(&self) -> Result<()>;
/// Test room removal.
async fn test_room_removal(&self) -> Result<()>;
/// Test profile removal.
async fn test_profile_removal(&self) -> Result<()>;
/// Test presence saving.
async fn test_presence_saving(&self);
/// Test display names saving.
@@ -1058,6 +1060,77 @@ impl StateStoreIntegrationTests for DynStateStore {
Ok(())
}
async fn test_profile_removal(&self) -> Result<()> {
let room_id = room_id();
// Both the user id and invited user id get a profile in populate().
let user_id = user_id();
let invited_user_id = invited_user_id();
self.populate().await?;
let new_invite_member_json = json!({
"content": {
"avatar_url": "mxc://localhost/SEsfnsuifSDFSSEG",
"displayname": "example after update",
"membership": "invite",
"reason": "Looking for support"
},
"event_id": "$143273582443PhrSm:localhost",
"origin_server_ts": 1432735824,
"room_id": room_id,
"sender": user_id,
"state_key": invited_user_id,
"type": "m.room.member",
});
let new_invite_member_event: SyncRoomMemberEvent =
serde_json::from_value(new_invite_member_json.clone()).unwrap();
let mut changes = StateChanges {
// Both get their profiles deleted…
profiles_to_delete: [(
room_id.to_owned(),
vec![user_id.to_owned(), invited_user_id.to_owned()],
)]
.into(),
// …but the invited user get a new profile.
profiles: {
let mut map = BTreeMap::default();
map.insert(
room_id.to_owned(),
[(invited_user_id.to_owned(), new_invite_member_event.into())]
.into_iter()
.collect(),
);
map
},
..StateChanges::default()
};
let raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(new_invite_member_json)
.expect("can create sync-state-event for topic");
let event = raw.deserialize().unwrap();
changes.add_state_event(room_id, event, raw);
self.save_changes(&changes).await.unwrap();
// The profile for user has been removed.
assert!(self.get_profile(room_id, user_id).await?.is_none());
assert!(self.get_member_event(room_id, user_id).await?.is_some());
// The profile for the invited user has been updated.
let invited_member_event = self.get_profile(room_id, invited_user_id).await?.unwrap();
assert_eq!(
invited_member_event.as_original().unwrap().content.displayname.as_deref(),
Some("example after update")
);
assert!(self.get_member_event(room_id, invited_user_id).await?.is_some());
Ok(())
}
async fn test_presence_saving(&self) {
let user_id = user_id();
let second_user_id = user_id!("@second:localhost");
@@ -1288,6 +1361,12 @@ macro_rules! statestore_integration_tests {
store.test_room_removal().await
}
#[async_test]
async fn test_profile_removal() -> StoreResult<()> {
let store = get_store().await?.into_state_store();
store.test_profile_removal().await
}
#[async_test]
async fn test_presence_saving() {
let store = get_store().await.expect("creating store failed").into_state_store();
@@ -14,6 +14,7 @@
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
num::NonZeroUsize,
sync::RwLock as StdRwLock,
};
@@ -45,8 +46,9 @@ use crate::{
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct MemoryStore {
recently_visited_rooms: StdRwLock<HashMap<String, Vec<String>>>,
user_avatar_url: StdRwLock<HashMap<String, String>>,
sync_token: StdRwLock<Option<String>>,
filters: StdRwLock<HashMap<String, String>>,
@@ -82,10 +84,38 @@ pub struct MemoryStore {
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
impl Default for MemoryStore {
fn default() -> Self {
Self {
recently_visited_rooms: Default::default(),
user_avatar_url: Default::default(),
sync_token: Default::default(),
filters: Default::default(),
account_data: Default::default(),
profiles: Default::default(),
display_names: Default::default(),
members: Default::default(),
room_info: Default::default(),
room_state: Default::default(),
room_account_data: Default::default(),
stripped_room_state: Default::default(),
stripped_members: Default::default(),
presence: Default::default(),
room_user_receipts: Default::default(),
room_event_receipts: Default::default(),
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
custom: Default::default(),
}
}
}
impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Self { media: StdRwLock::new(RingBuffer::new(20)), ..Default::default() }
Self::default()
}
fn get_user_room_receipt_event_impl(
@@ -149,6 +179,13 @@ impl StateStore for MemoryStore {
.get(user_id.as_str())
.cloned()
.map(StateStoreDataValue::UserAvatarUrl),
StateStoreDataKey::RecentlyVisitedRooms(user_id) => self
.recently_visited_rooms
.read()
.unwrap()
.get(user_id.as_str())
.cloned()
.map(StateStoreDataValue::RecentlyVisitedRooms),
})
}
@@ -174,6 +211,14 @@ impl StateStore for MemoryStore {
value.into_user_avatar_url().expect("Session data not a user avatar url"),
);
}
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
self.recently_visited_rooms.write().unwrap().insert(
user_id.to_string(),
value
.into_recently_visited_rooms()
.expect("Session data not a list of recently visited rooms"),
);
}
}
Ok(())
@@ -188,6 +233,9 @@ impl StateStore for MemoryStore {
StateStoreDataKey::UserAvatarUrl(user_id) => {
self.filters.write().unwrap().remove(user_id.as_str());
}
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
self.recently_visited_rooms.write().unwrap().remove(user_id.as_str());
}
}
Ok(())
}
@@ -199,14 +247,25 @@ impl StateStore for MemoryStore {
*self.sync_token.write().unwrap() = Some(s.to_owned());
}
for (room, users) in &changes.profiles {
for (user_id, profile) in users {
self.profiles
.write()
.unwrap()
.entry(room.clone())
.or_default()
.insert(user_id.clone(), profile.clone());
{
let mut profiles = self.profiles.write().unwrap();
for (room, users) in &changes.profiles_to_delete {
let Some(room_profiles) = profiles.get_mut(room) else {
continue;
};
for user in users {
room_profiles.remove(user);
}
}
for (room, users) in &changes.profiles {
for (user_id, profile) in users {
profiles
.entry(room.clone())
.or_default()
.insert(user_id.clone(), profile.clone());
}
}
}
@@ -14,7 +14,10 @@
//! Data migration helpers for StateStore implementations.
use std::collections::{BTreeMap, HashSet};
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
#[cfg(feature = "experimental-sliding-sync")]
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
@@ -44,7 +47,7 @@ use crate::{
deserialized_responses::SyncOrStrippedState,
rooms::{
normal::{RoomSummary, SyncInfo},
BaseRoomInfo,
BaseRoomInfo, RoomNotableTags,
},
sync::UnreadNotificationsCount,
MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
@@ -120,6 +123,7 @@ impl RoomInfoV1 {
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
read_receipts: Default::default(),
base_info: base_info.migrate(create),
warned_about_unknown_room_version: Arc::new(false.into()),
}
}
}
@@ -204,6 +208,8 @@ impl BaseRoomInfoV1 {
tombstone,
topic,
rtc_member: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
})
}
}
+30 -12
View File
@@ -51,13 +51,13 @@ use ruma::{
serde::Raw,
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
};
use tokio::sync::RwLock;
use tokio::sync::{broadcast, Mutex, RwLock};
/// BoxStream of owned Types
pub type BoxStream<T> = Pin<Box<dyn futures_util::Stream<Item = T> + Send>>;
use crate::{
rooms::{RoomInfo, RoomState},
rooms::{normal::RoomInfoUpdate, RoomInfo, RoomState},
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
};
@@ -144,11 +144,8 @@ pub(crate) struct Store {
pub(super) sync_token: Arc<RwLock<Option<String>>>,
rooms: Arc<StdRwLock<BTreeMap<OwnedRoomId, Room>>>,
/// A lock to synchronize access to the store, such that data by the sync is
/// never overwritten. The sync processing is supposed to use write access,
/// such that only it is currently accessing the store overall. Other things
/// might acquire read access, such that access to different rooms can be
/// parallelized.
sync_lock: Arc<RwLock<()>>,
/// never overwritten.
sync_lock: Arc<Mutex<()>>,
}
impl Store {
@@ -164,7 +161,7 @@ impl Store {
}
/// Get access to the syncing lock.
pub fn sync_lock(&self) -> &RwLock<()> {
pub fn sync_lock(&self) -> &Mutex<()> {
&self.sync_lock
}
@@ -174,9 +171,18 @@ impl Store {
/// inner `StateStore`.
///
/// This method panics if it is called twice.
pub async fn set_session_meta(&self, session_meta: SessionMeta) -> Result<()> {
pub async fn set_session_meta(
&self,
session_meta: SessionMeta,
roominfo_update_sender: &broadcast::Sender<RoomInfoUpdate>,
) -> Result<()> {
for info in self.inner.get_room_infos().await? {
let room = Room::restore(&session_meta.user_id, self.inner.clone(), info);
let room = Room::restore(
&session_meta.user_id,
self.inner.clone(),
info,
roominfo_update_sender.clone(),
);
self.rooms.write().unwrap().insert(room.room_id().to_owned(), room);
}
@@ -217,7 +223,12 @@ impl Store {
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
/// yet in the store
pub fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomState) -> Room {
pub fn get_or_create_room(
&self,
room_id: &RoomId,
room_type: RoomState,
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
) -> Room {
let user_id =
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
@@ -225,7 +236,9 @@ impl Store {
.write()
.unwrap()
.entry(room_id.to_owned())
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
.or_insert_with(|| {
Room::new(user_id, self.inner.clone(), room_id, room_type, roominfo_update_sender)
})
.clone()
}
}
@@ -264,6 +277,11 @@ pub struct StateChanges {
/// `MinimalRoomMemberEvent`.
pub profiles: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, MinimalRoomMemberEvent>>,
/// A mapping of room profiles to delete.
///
/// These are deleted *before* other room profiles are inserted.
pub profiles_to_delete: BTreeMap<OwnedRoomId, Vec<OwnedUserId>>,
/// A mapping of `RoomId` to a map of event type string to a state key and
/// `AnySyncStateEvent`.
pub state:
+38 -1
View File
@@ -302,7 +302,8 @@ pub trait StateStore: AsyncTraitDeps {
/// * `key` - The key to fetch data for
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
/// Put arbitrary data into the custom store
/// Put arbitrary data into the custom store, return the data previously
/// stored
///
/// # Arguments
///
@@ -315,6 +316,27 @@ pub trait StateStore: AsyncTraitDeps {
value: Vec<u8>,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Put arbitrary data into the custom store, do not attempt to read any
/// previous data
///
/// Optimization option for set_custom_values for stores that would perform
/// better withouts the extra read and the caller not needing that data
/// returned. Otherwise this just wraps around `set_custom_data` and
/// discards the result.
///
/// # Arguments
///
/// * `key` - The key to insert data into
///
/// * `value` - The value to insert
async fn set_custom_value_no_read(
&self,
key: &[u8],
value: Vec<u8>,
) -> Result<(), Self::Error> {
self.set_custom_value(key, value).await.map(|_| ())
}
/// Remove arbitrary data from the custom store and return it if existed
///
/// # Arguments
@@ -783,6 +805,9 @@ pub enum StateStoreDataValue {
/// The user avatar url
UserAvatarUrl(String),
/// A list of recently visited room identifiers for the current user
RecentlyVisitedRooms(Vec<String>),
}
impl StateStoreDataValue {
@@ -800,6 +825,11 @@ impl StateStoreDataValue {
pub fn into_user_avatar_url(self) -> Option<String> {
as_variant!(self, Self::UserAvatarUrl)
}
/// Get this value if it is a list of recently visited rooms.
pub fn into_recently_visited_rooms(self) -> Option<Vec<String>> {
as_variant!(self, Self::RecentlyVisitedRooms)
}
}
/// A key for key-value data.
@@ -813,6 +843,9 @@ pub enum StateStoreDataKey<'a> {
/// Avatar URL
UserAvatarUrl(&'a UserId),
/// Recently visited room identifiers
RecentlyVisitedRooms(&'a UserId),
}
impl StateStoreDataKey<'_> {
@@ -823,4 +856,8 @@ impl StateStoreDataKey<'_> {
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
/// variant.
pub const USER_AVATAR_URL: &'static str = "user_avatar_url";
/// Key prefix to use for the
/// [`RecentlyVisitedRooms`][Self::RecentlyVisitedRooms] variant.
pub const RECENTLY_VISITED_ROOMS: &'static str = "recently_visited_rooms";
}
+67 -34
View File
@@ -16,46 +16,41 @@
use std::{collections::BTreeMap, fmt};
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
use ruma::{
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{
v3::InvitedRoom, UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
api::client::sync::sync_events::{
v3::InvitedRoom as InvitedRoomUpdate,
UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
events::{
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
},
push::Action,
serde::Raw,
OwnedRoomId,
OwnedEventId, OwnedRoomId,
};
use serde::{Deserialize, Serialize};
use crate::{
debug::{
DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId, DebugNotificationMap,
},
deserialized_responses::AmbiguityChanges,
debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
};
/// Internal representation of a `/sync` response.
/// Generalized representation of a `/sync` response.
///
/// This type is intended to be applicable regardless of the endpoint used for
/// syncing.
#[derive(Clone, Default)]
pub struct SyncResponse {
/// Updates to rooms.
pub rooms: Rooms,
pub rooms: RoomUpdates,
/// Updates to the presence status of other users.
pub presence: Vec<Raw<PresenceEvent>>,
/// The global private data created by this user.
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
/// Messages sent directly between devices.
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
/// New notifications per room.
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
@@ -67,37 +62,36 @@ impl fmt::Debug for SyncResponse {
.field("rooms", &self.rooms)
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
.field("to_device", &DebugListOfRawEventsNoId(&self.to_device))
.field("ambiguity_changes", &self.ambiguity_changes)
.field("notifications", &DebugNotificationMap(&self.notifications))
.field("notifications", &self.notifications)
.finish_non_exhaustive()
}
}
/// Updates to rooms in a [`SyncResponse`].
#[derive(Clone, Default)]
pub struct Rooms {
pub struct RoomUpdates {
/// The rooms that the user has left or been banned from.
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
pub leave: BTreeMap<OwnedRoomId, LeftRoomUpdate>,
/// The rooms that the user has joined.
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
pub join: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
/// The rooms that the user has been invited to.
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Rooms {
impl fmt::Debug for RoomUpdates {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rooms")
.field("leave", &self.leave)
.field("join", &self.join)
.field("invite", &DebugInvitedRooms(&self.invite))
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
.finish()
}
}
/// Updates to joined rooms.
#[derive(Clone, Default)]
pub struct JoinedRoom {
pub struct JoinedRoomUpdate {
/// Counts of unread notifications for this room.
pub unread_notifications: UnreadNotificationsCount,
/// The timeline of messages and state changes in the room.
@@ -112,10 +106,15 @@ pub struct JoinedRoom {
/// The ephemeral events in the room that aren't recorded in the timeline or
/// state of the room. e.g. typing.
pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
/// Collection of ambiguity changes that room member events trigger.
///
/// This is a map of event ID of the `m.room.member` event to the
/// details of the ambiguity change.
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for JoinedRoom {
impl fmt::Debug for JoinedRoomUpdate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JoinedRoom")
.field("unread_notifications", &self.unread_notifications)
@@ -123,19 +122,21 @@ impl fmt::Debug for JoinedRoom {
.field("state", &DebugListOfRawEvents(&self.state))
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
.field("ephemeral", &self.ephemeral)
.field("ambiguity_changes", &self.ambiguity_changes)
.finish()
}
}
impl JoinedRoom {
impl JoinedRoomUpdate {
pub(crate) fn new(
timeline: Timeline,
state: Vec<Raw<AnySyncStateEvent>>,
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
unread_notifications: UnreadNotificationsCount,
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
) -> Self {
Self { unread_notifications, timeline, state, account_data, ephemeral }
Self { unread_notifications, timeline, state, account_data, ephemeral, ambiguity_changes }
}
}
@@ -159,8 +160,8 @@ impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
}
/// Updates to left rooms.
#[derive(Clone)]
pub struct LeftRoom {
#[derive(Clone, Default)]
pub struct LeftRoomUpdate {
/// The timeline of messages and state changes in the room up to the point
/// when the user left.
pub timeline: Timeline,
@@ -171,25 +172,32 @@ pub struct LeftRoom {
pub state: Vec<Raw<AnySyncStateEvent>>,
/// The private data that this user has attached to this room.
pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
/// Collection of ambiguity changes that room member events trigger.
///
/// This is a map of event ID of the `m.room.member` event to the
/// details of the ambiguity change.
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
}
impl LeftRoom {
impl LeftRoomUpdate {
pub(crate) fn new(
timeline: Timeline,
state: Vec<Raw<AnySyncStateEvent>>,
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
) -> Self {
Self { timeline, state, account_data }
Self { timeline, state, account_data, ambiguity_changes }
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for LeftRoom {
impl fmt::Debug for LeftRoomUpdate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JoinedRoom")
.field("timeline", &self.timeline)
.field("state", &DebugListOfRawEvents(&self.state))
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
.field("ambiguity_changes", &self.ambiguity_changes)
.finish()
}
}
@@ -215,11 +223,36 @@ impl Timeline {
}
}
struct DebugInvitedRooms<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoom>);
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugInvitedRooms<'a> {
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
}
}
/// A notification triggered by a sync response.
#[derive(Clone)]
pub struct Notification {
/// The actions to perform when the conditions for this rule are met.
pub actions: Vec<Action>,
/// The event that triggered the notification.
pub event: RawAnySyncOrStrippedTimelineEvent,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Notification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let event_debug = match &self.event {
RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => DebugRawEvent(ev.cast_ref()),
};
f.debug_struct("Notification")
.field("actions", &self.actions)
.field("event", &event_debug)
.finish()
}
}
+34
View File
@@ -0,0 +1,34 @@
// 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.
//! Testing utilities - DO NOT USE IN PRODUCTION.
#![allow(dead_code)]
use ruma::{owned_user_id, UserId};
use crate::{BaseClient, SessionMeta};
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
/// one otherwise.
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
let client = BaseClient::new();
let user_id =
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
client
.set_session_meta(SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() })
.await
.expect("set_session_meta failed!");
client
}
+3
View File
@@ -45,3 +45,6 @@ wasm-bindgen-test = "0.3.33"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
js-sys = "0.3.64"
[lints]
workspace = true
+57 -8
View File
@@ -15,8 +15,6 @@
//! Abstraction over an executor so we can spawn tasks under WASM the same way
//! we do usually.
#[cfg(target_arch = "wasm32")]
pub use std::convert::Infallible as JoinError;
#[cfg(target_arch = "wasm32")]
use std::{
future::Future,
@@ -25,7 +23,12 @@ use std::{
};
#[cfg(target_arch = "wasm32")]
use futures_util::{future::RemoteHandle, FutureExt};
pub use futures_util::future::Aborted as JoinError;
#[cfg(target_arch = "wasm32")]
use futures_util::{
future::{AbortHandle, Abortable, RemoteHandle},
FutureExt,
};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::task::{spawn, JoinError, JoinHandle};
@@ -34,16 +37,31 @@ pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
F: Future<Output = T> + 'static,
{
let (fut, handle) = future.remote_handle();
wasm_bindgen_futures::spawn_local(fut);
let (future, remote_handle) = future.remote_handle();
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let future = Abortable::new(future, abort_registration);
JoinHandle { handle }
wasm_bindgen_futures::spawn_local(async {
// Poll the future, and ignore the result (either it's `Ok(())`, or it's
// `Err(Aborted)`).
let _ = future.await;
});
JoinHandle { remote_handle, abort_handle }
}
#[cfg(target_arch = "wasm32")]
#[derive(Debug)]
pub struct JoinHandle<T> {
handle: RemoteHandle<T>,
remote_handle: RemoteHandle<T>,
abort_handle: AbortHandle,
}
#[cfg(target_arch = "wasm32")]
impl<T> JoinHandle<T> {
pub fn abort(&self) {
self.abort_handle.abort();
}
}
#[cfg(target_arch = "wasm32")]
@@ -51,6 +69,37 @@ impl<T: 'static> Future for JoinHandle<T> {
type Output = Result<T, JoinError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handle).poll(cx).map(Ok)
if self.abort_handle.is_aborted() {
// The future has been aborted. It is not possible to poll it again.
Poll::Ready(Err(JoinError))
} else {
Pin::new(&mut self.remote_handle).poll(cx).map(Ok)
}
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use super::spawn;
#[async_test]
async fn test_spawn() {
let future = async { 42 };
let join_handle = spawn(future);
assert_matches!(join_handle.await, Ok(42));
}
#[async_test]
async fn test_abort() {
let future = async { 42 };
let join_handle = spawn(future);
join_handle.abort();
assert!(join_handle.await.is_err());
}
}
+38 -23
View File
@@ -17,10 +17,11 @@ use std::{
vec_deque::{Drain, Iter},
VecDeque,
},
num::NonZeroUsize,
ops::RangeBounds,
};
use serde::{self, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
/// A simple fixed-size ring buffer implementation.
///
@@ -35,8 +36,8 @@ pub struct RingBuffer<T> {
impl<T> RingBuffer<T> {
/// Create a ring buffer with the supplied capacity, reserving it so we
/// never need to reallocate.
pub fn new(size: usize) -> Self {
Self { inner: VecDeque::with_capacity(size) }
pub fn new(size: NonZeroUsize) -> Self {
Self { inner: VecDeque::with_capacity(size.into()) }
}
/// Returns the number of items that are stored in this ring buffer.
@@ -105,6 +106,14 @@ impl<T> RingBuffer<T> {
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
/// Retains only the elements specified by the predicate.
pub fn retain<F>(&mut self, predicate: F)
where
F: FnMut(&T) -> bool,
{
self.inner.retain(predicate)
}
}
impl<U> Extend<U> for RingBuffer<U> {
@@ -115,21 +124,15 @@ impl<U> Extend<U> for RingBuffer<U> {
}
}
impl<T> Default for RingBuffer<T> {
fn default() -> Self {
Self { inner: Default::default() }
}
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use std::{num::NonZeroUsize, ops::Not};
use super::RingBuffer;
#[test]
pub fn test_fixed_size() {
let mut ring_buffer = RingBuffer::new(5);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
assert!(ring_buffer.is_empty());
@@ -163,7 +166,7 @@ mod tests {
#[test]
pub fn test_push_and_pop_and_remove_and_length() {
let mut ring_buffer = RingBuffer::new(3);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.push(1);
assert_eq!(ring_buffer.len(), 1);
@@ -220,7 +223,7 @@ mod tests {
#[test]
fn test_drain() {
let mut ring_buffer = RingBuffer::new(5);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
ring_buffer.push(1);
ring_buffer.push(2);
@@ -241,7 +244,7 @@ mod tests {
#[test]
fn clear_on_empty_buffer_is_a_noop() {
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(3);
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.clear();
assert_eq!(ring_buffer.len(), 0);
}
@@ -249,7 +252,7 @@ mod tests {
#[test]
fn clear_removes_all_items() {
// Given a RingBuffer that has been used
let mut ring_buffer = RingBuffer::new(3);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.push(4);
ring_buffer.push(5);
ring_buffer.push(6);
@@ -269,7 +272,7 @@ mod tests {
#[test]
fn clear_does_not_affect_capacity() {
// Given a RingBuffer that has been used
let mut ring_buffer = RingBuffer::new(3);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.push(4);
ring_buffer.push(5);
ring_buffer.push(6);
@@ -287,7 +290,7 @@ mod tests {
#[test]
fn capacity_is_what_we_passed_to_new() {
// Given a RingBuffer
let ring_buffer = RingBuffer::<i32>::new(13);
let ring_buffer = RingBuffer::<i32>::new(NonZeroUsize::new(13).unwrap());
// When I ask for its capacity I get what I provided at the start
assert_eq!(ring_buffer.capacity(), 13);
}
@@ -295,7 +298,7 @@ mod tests {
#[test]
fn capacity_is_not_affected_by_overflowing() {
// Given a RingBuffer that has been used
let mut ring_buffer = RingBuffer::new(3);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.push(4);
ring_buffer.push(5);
ring_buffer.push(6);
@@ -317,7 +320,7 @@ mod tests {
#[test]
fn roundtrip_serialization() {
// Given a RingBuffer
let mut ring_buffer = RingBuffer::new(3);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
ring_buffer.push("1".to_owned());
ring_buffer.push("2".to_owned());
@@ -337,7 +340,7 @@ mod tests {
#[test]
fn extending_an_empty_ringbuffer_adds_the_items() {
// Given a RingBuffer
let mut ring_buffer = RingBuffer::new(5);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
// When I extend it
ring_buffer.extend(vec!["a".to_owned(), "b".to_owned()]);
@@ -349,7 +352,7 @@ mod tests {
#[test]
fn extend_adds_items_to_the_end() {
// Given a RingBuffer with something in it
let mut ring_buffer = RingBuffer::new(5);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
ring_buffer.push("1".to_owned());
ring_buffer.push("2".to_owned());
@@ -366,7 +369,7 @@ mod tests {
#[test]
fn extend_does_not_overflow_max_length() {
// Given a RingBuffer with something in it
let mut ring_buffer = RingBuffer::new(5);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
ring_buffer.push("1".to_owned());
ring_buffer.push("2".to_owned());
@@ -389,7 +392,7 @@ mod tests {
#[test]
fn extending_a_full_ringbuffer_preserves_max_length() {
// Given a full RingBuffer with something in it
let mut ring_buffer = RingBuffer::new(2);
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
ring_buffer.push("1".to_owned());
ring_buffer.push("2".to_owned());
@@ -405,4 +408,16 @@ mod tests {
// Then only the last N items remain
assert_eq!(ring_buffer.iter().map(String::as_str).collect::<Vec<_>>(), vec!["6", "7"]);
}
#[test]
fn test_retain() {
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
ring_buffer.push(1);
ring_buffer.push(2);
ring_buffer.retain(|v| v % 2 == 0);
assert_eq!(ring_buffer.len(), 1);
assert_eq!(ring_buffer.get(0).copied().unwrap(), 2);
}
}
+58
View File
@@ -1,3 +1,61 @@
# UNRELEASED
Changed:
- Fallback keys are rotated in a time-based manner, instead of waiting for the
server to tell us that a fallback key got used.
([#3151](https://github.com/matrix-org/matrix-rust-sdk/pull/3151))
Breaking changes:
- Add a `backup_version` argument to `CryptoStore`'s
`inbound_group_sessions_for_backup`,
`mark_inbound_group_sessions_as_backed_up` and
`inbound_group_session_counts` methods.
([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
- Rename the `OlmMachine::invalidate_group_session` method to
`OlmMachine::discard_room_key`
- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
(Call it with `olm_machine.store().export_room_keys(...)`.)
- Add new `dehydrated` property to `olm::account::PickledAccount`.
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
Additions:
- Expose new method `OlmMachine::device_creation_time`.
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
- Log more details about the Olm session after encryption and decryption.
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
- When Olm message decryption fails, report the error code(s) from the failure.
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
- Expose new methods `OlmMachine::set_room_settings` and
`OlmMachine::get_room_settings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Add new properties `session_rotation_period` and
`session_rotation_period_msgs` to `store::RoomSettings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
keys whose metadata lacked check fields.
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
- Add new API `Device::encrypt_event_raw` that allows
to encrypt an event to a specific device.
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
- Add new API `store::Store::export_room_keys_stream` that provides room
keys on demand.
- Include event timestamps on logs from event decryption.
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
# 0.7.0
- Add method to mark a list of inbound group sessions as backed up:
+9 -1
View File
@@ -17,10 +17,12 @@ rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
automatic-room-key-forwarding = []
js = ["ruma/js", "vodozemac/js"]
js = ["ruma/js", "vodozemac/js", "matrix-sdk-common/js"]
qrcode = ["dep:matrix-sdk-qrcode"]
message-ids = ["dep:ulid"]
experimental-algorithms = []
uniffi = ["dep:uniffi"]
_disable-minimum-rotation-period-ms = []
# Testing helpers for implementations based upon this
testing = ["dep:http"]
@@ -41,6 +43,7 @@ hkdf = "0.12.3"
hmac = "0.12.1"
http = { workspace = true, optional = true } # feature = testing only
itertools = { workspace = true }
js_option = "0.1.1"
matrix-sdk-qrcode = { workspace = true, optional = true }
matrix-sdk-common = { workspace = true }
pbkdf2 = { version = "0.12.2", default-features = false }
@@ -51,11 +54,13 @@ serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
subtle = "2.5.0"
time = { version = "0.3.34", features = ["formatting"] }
tokio-stream = { workspace = true, features = ["sync"] }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
ulid = { version = "1.0.0", optional = true }
uniffi = { workspace = true, optional = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -78,3 +83,6 @@ proptest = { version = "1.0.0", default-features = false, features = ["std"] }
# required for async_test macro
stream_assert = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
[lints]
workspace = true
+3 -1
View File
@@ -70,4 +70,6 @@ The following crate feature flags are available:
* `qrcode`: Enbles QRcode generation and reading code
* `testing`: provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
@@ -106,12 +106,12 @@ impl PkDecryption {
let keys = Keys::new(shared_secret);
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
let hmac = keys.hmac();
hmac.verify_truncated_left(&message.mac)?;
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
Ok(decrypted)
}
@@ -17,7 +17,6 @@ use std::{
ops::DerefMut,
};
use bs58;
use ruma::api::client::backup::EncryptedSessionData;
use thiserror::Error;
use vodozemac::Curve25519PublicKey;
@@ -45,7 +44,7 @@ pub enum DecodeError {
/// The recovery key has an invalid length.
#[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
Length(usize, usize),
/// The recovry key isn't valid base58.
/// The recovery key isn't valid base58.
#[error(transparent)]
Base58(#[from] bs58::decode::Error),
/// The recovery key isn't valid base64.
+28 -15
View File
@@ -39,8 +39,7 @@ use crate::{
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
SignatureError,
CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError,
};
mod keys;
@@ -70,12 +69,6 @@ struct PendingBackup {
sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
}
impl From<PendingBackup> for OutgoingRequest {
fn from(b: PendingBackup) -> Self {
OutgoingRequest { request_id: b.request_id, request: Arc::new(b.request.into()) }
}
}
/// The result of a signature verification of a signed JSON object.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SignatureVerification {
@@ -108,6 +101,7 @@ impl SignatureVerification {
/// The result of a signature check.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum SignatureState {
/// The signature is missing.
#[default]
@@ -403,7 +397,8 @@ impl BackupMachine {
/// Get the number of backed up room keys and the total number of room keys.
pub async fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> {
self.store.inbound_group_session_counts().await
let backup_version = self.backup_key.read().await.as_ref().and_then(|k| k.backup_version());
self.store.inbound_group_session_counts(backup_version.as_deref()).await
}
/// Disable and reset our backup state.
@@ -482,7 +477,12 @@ impl BackupMachine {
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
self.store.mark_inbound_group_sessions_as_backed_up(&room_and_session_ids).await?;
self.store
.mark_inbound_group_sessions_as_backed_up(
&r.request.version,
&room_and_session_ids,
)
.await?;
trace!(
request_id = ?r.request_id,
@@ -520,7 +520,7 @@ impl BackupMachine {
};
let sessions =
self.store.inbound_group_sessions_for_backup(Self::BACKUP_BATCH_SIZE).await?;
self.store.inbound_group_sessions_for_backup(&version, Self::BACKUP_BATCH_SIZE).await?;
if sessions.is_empty() {
trace!(?backup_key, "No room keys need to be backed up");
@@ -625,6 +625,7 @@ mod tests {
use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
use serde_json::json;
use super::BackupMachine;
use crate::{
olm::BackedUpRoomKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError,
OlmMachine,
@@ -664,7 +665,10 @@ mod tests {
async fn backup_flow(machine: OlmMachine) -> Result<(), OlmError> {
let backup_machine = machine.backup_machine();
let counts = backup_machine.store.inbound_group_session_counts().await?;
let backup_version = current_backup_version(backup_machine).await;
let counts =
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
assert_eq!(counts.total, 0, "Initially no keys exist");
assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
@@ -672,7 +676,8 @@ mod tests {
machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
let counts = backup_machine.store.inbound_group_session_counts().await?;
let counts =
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
@@ -691,8 +696,10 @@ mod tests {
);
backup_machine.mark_request_as_sent(&request_id).await?;
let backup_version = current_backup_version(backup_machine).await;
let counts = backup_machine.store.inbound_group_session_counts().await?;
let counts =
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
assert_eq!(counts.total, 2);
assert_eq!(counts.backed_up, 2, "All room keys have been backed up");
@@ -702,8 +709,10 @@ mod tests {
);
backup_machine.disable_backup().await?;
let backup_version = current_backup_version(backup_machine).await;
let counts = backup_machine.store.inbound_group_session_counts().await?;
let counts =
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
assert_eq!(counts.total, 2);
assert_eq!(
counts.backed_up, 0,
@@ -713,6 +722,10 @@ mod tests {
Ok(())
}
async fn current_backup_version(backup_machine: &BackupMachine) -> Option<String> {
backup_machine.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
}
#[async_test]
async fn memory_store_backups() -> Result<(), OlmError> {
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
@@ -95,7 +95,7 @@ impl DehydratedDevices {
let user_id = self.inner.user_id();
let user_identity = self.inner.store().private_identity();
let account = Account::new(user_id);
let account = Account::new_dehydrated(user_id);
let store = Arc::new(CryptoStoreWrapper::new(user_id, MemoryStore::new()));
let verification_machine = VerificationMachine::new(
@@ -318,7 +318,7 @@ impl DehydratedDevice {
let mut transaction = self.store.transaction().await;
let account = transaction.account().await?;
account.generate_fallback_key_helper();
account.generate_fallback_key_if_needed();
let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
@@ -378,6 +378,7 @@ fn expand_pickle_key(key: &[u8; 32], device_id: &DeviceId) -> Box<[u8; 32]> {
mod tests {
use std::{collections::BTreeMap, iter};
use js_option::JsOption;
use matrix_sdk_test::async_test;
use ruma::{
api::client::keys::get_keys::v3::Response as KeysQueryResponse, assign,
@@ -390,7 +391,7 @@ mod tests {
create_session, get_prepared_machine_test_helper, to_device_requests_to_content,
},
olm::OutboundGroupSession,
types::events::ToDeviceEvent,
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
utilities::json_convert,
EncryptionSettings, OlmMachine,
};
@@ -477,6 +478,13 @@ mod tests {
!request.fallback_keys.is_empty(),
"The dehydrated device creation request should contain some fallback keys"
);
let device_keys: DeviceKeysType = request.device_keys.deserialize_as().unwrap();
assert_eq!(
device_keys.dehydrated,
JsOption::Some(true),
"The device keys of the dehydrated device should be marked as dehydrated."
);
}
#[async_test]
+22 -7
View File
@@ -38,10 +38,6 @@ pub enum OlmError {
#[error(transparent)]
JsonError(#[from] SerdeError),
/// The event could not have been decrypted.
#[error(transparent)]
Decryption(#[from] vodozemac::olm::DecryptionError),
/// The received room key couldn't be converted into a valid Megolm session.
#[error(transparent)]
SessionCreation(#[from] SessionCreationError),
@@ -263,11 +259,11 @@ pub enum SessionCreationError {
)]
InvalidSignature {
/// The one-time key that failed the signature verification.
one_time_key: SignedKey,
one_time_key: Box<SignedKey>,
/// The key that was used to verify the signature.
signing_key: Option<Ed25519PublicKey>,
signing_key: Option<Box<Ed25519PublicKey>>,
/// The exact error describing why the signature verification failed.
error: SignatureError,
error: Box<SignatureError>,
},
/// The user's device is missing a curve25519 key.
@@ -289,3 +285,22 @@ pub enum SessionCreationError {
#[error(transparent)]
InboundCreation(#[from] vodozemac::olm::SessionCreationError),
}
/// Errors that can be returned by
/// [`crate::machine::OlmMachine::set_room_settings`].
#[derive(Debug, Error)]
pub enum SetRoomSettingsError {
/// The changes are rejected because they conflict with the previous
/// settings for this room.
#[error("the new settings would cause a downgrade of encryption security")]
EncryptionDowngrade,
/// The changes are rejected because we would be unable to use them to
/// encrypt events.
#[error("the new settings are invalid")]
InvalidSettings,
/// The store ran into an error.
#[error(transparent)]
Store(#[from] CryptoStoreError),
}
@@ -130,7 +130,7 @@ pub fn decrypt_room_key_export(
/// # async {
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// let room_id = room_id!("!test:localhost");
/// let exported_keys = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
/// let exported_keys = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
/// # };
/// ```
@@ -293,7 +293,7 @@ mod tests {
let room_id = room_id!("!test:localhost");
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
let export = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
let export = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
assert!(!export.is_empty());
@@ -313,7 +313,7 @@ impl GossipMachine {
let device =
self.inner.store.get_device(&event.sender, &event.content.requesting_device_id).await?;
Ok(if let Some(device) = device {
if let Some(device) = device {
if device.user_id() == self.user_id() {
if device.is_verified() {
info!(
@@ -338,7 +338,7 @@ impl GossipMachine {
Ok(None)
}
Err(e) => Err(e),
}?
}
} else {
info!(
user_id = ?device.user_id(),
@@ -347,7 +347,7 @@ impl GossipMachine {
"Received a secret request that we won't serve, the device isn't trusted",
);
None
Ok(None)
}
} else {
info!(
@@ -357,7 +357,7 @@ impl GossipMachine {
"Received a secret request that we won't serve, the device doesn't belong to us",
);
None
Ok(None)
}
} else {
warn!(
@@ -374,8 +374,8 @@ impl GossipMachine {
.mark_user_as_changed(&event.sender)
.await?;
None
})
Ok(None)
}
}
/// Try to encrypt the given `InboundGroupSession` for the given `Device` as
@@ -1247,7 +1247,8 @@ mod tests {
.with_transaction(|mut btr| async {
let alice_account = atr.account().await?;
let bob_account = btr.account().await?;
let sessions = alice_account.create_session_for(bob_account).await;
let sessions =
alice_account.create_session_for_test_helper(bob_account).await;
Ok((btr, sessions))
})
.await?;
@@ -1845,7 +1846,7 @@ mod tests {
.with_transaction(|mut tr| async {
let alice_account = tr.account().await?;
let (alice_session, _) =
alice_account.create_session_for(&mut second_account).await;
alice_account.create_session_for_test_helper(&mut second_account).await;
Ok((tr, alice_session))
})
.await
@@ -2068,7 +2069,8 @@ mod tests {
.with_transaction(|mut btr| async {
let alice_account = atr.account().await?;
let bob_account = btr.account().await?;
let sessions = alice_account.create_session_for(bob_account).await;
let sessions =
alice_account.create_session_for_test_helper(bob_account).await;
Ok((btr, sessions))
})
.await?;
@@ -14,7 +14,6 @@
use std::{
collections::{BTreeMap, HashMap},
convert::{TryFrom, TryInto},
ops::Deref,
sync::{
atomic::{AtomicBool, Ordering},
@@ -30,12 +29,13 @@ use ruma::{
OwnedDeviceKeyId, UInt, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::Mutex;
use tracing::{instrument, trace, warn};
use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey};
use super::{atomic_bool_deserializer, atomic_bool_serializer};
#[cfg(any(test, feature = "testing"))]
#[cfg(any(test, feature = "testing", doc))]
use crate::OlmMachine;
use crate::{
error::{EventError, OlmError, OlmResult, SignatureError},
@@ -440,6 +440,51 @@ impl Device {
self.encrypt(event_type, content).await
}
/// Encrypt an event for this device.
///
/// Beware that the 1-to-1 session must be established prior to this
/// call by using the [`OlmMachine::get_missing_sessions`] method.
///
/// Notable limitation: The caller is responsible for sending the encrypted
/// event to the target device, this encryption method supports out-of-order
/// messages to a certain extent (2000 messages), if multiple messages are
/// encrypted using this method they should be sent in the same order as
/// they are encrypted.
///
/// *Note*: To instead encrypt an event meant for a room use the
/// [`OlmMachine::encrypt_room_event()`] method instead.
///
/// # Arguments
/// * `event_type` - The type of the event to be sent.
/// * `content` - The content of the event to be sent. This should be a type
/// that implements the `Serialize` trait.
///
/// # Returns
///
/// The encrypted raw content to be shared with your preferred transport
/// layer (usually to-device), [`OlmError::MissingSession`] if there is
/// no established session with the device.
pub async fn encrypt_event_raw(
&self,
event_type: &str,
content: &Value,
) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
let (used_session, raw_encrypted) = self.encrypt(event_type, content).await?;
// perist the used session
self.verification_machine
.store
.save_changes(Changes { sessions: vec![used_session], ..Default::default() })
.await?;
Ok(raw_encrypted)
}
/// Whether or not the device is a dehydrated device.
pub fn is_dehydrated(&self) -> bool {
self.inner.inner.dehydrated.unwrap_or(false)
}
}
/// A read only view over all devices belonging to a user.
@@ -500,8 +545,9 @@ impl UserDevices {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
/// The local trust state of a device.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum LocalTrust {
/// The device has been verified and is trusted.
Verified = 0,
@@ -651,7 +697,7 @@ impl ReadOnlyDevice {
}
} else {
warn!(
"Trying to find a Olm session of a device, but the device doesn't have a \
"Trying to find an Olm session of a device, but the device doesn't have a \
Curve25519 key",
);
@@ -744,7 +790,6 @@ impl ReadOnlyDevice {
recipient_device = ?self.device_id(),
recipient_key = ?self.curve25519_key(),
event_type,
session,
message_id,
))
]
@@ -772,7 +817,6 @@ impl ReadOnlyDevice {
if let Some(mut session) = session {
let message = session.encrypt(self, event_type, content, message_id).await?;
trace!("Successfully encrypted an event");
Ok((session, message))
} else {
trace!("Trying to encrypt an event for a device, but no Olm session is found.");
@@ -933,10 +977,10 @@ impl PartialEq for ReadOnlyDevice {
}
}
/// Testing Facilities for Device Management
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
pub(crate) mod testing {
//! Testing Facilities for Device Management
#![allow(dead_code)]
use serde_json::json;
use crate::{identities::ReadOnlyDevice, types::DeviceKeys};
@@ -1014,8 +1014,8 @@ impl IdentityManager {
}
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
pub(crate) mod testing {
#![allow(dead_code)]
use std::sync::Arc;
use ruma::{
@@ -699,10 +699,10 @@ impl ReadOnlyOwnUserIdentity {
}
}
/// Testing Facilities
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
pub(crate) mod testing {
//! Testing Facilities
#![allow(dead_code)]
use ruma::{api::client::keys::get_keys::v3::Response as KeyQueryResponse, user_id};
use super::{ReadOnlyOwnUserIdentity, ReadOnlyUserIdentity};
+7 -2
View File
@@ -33,7 +33,7 @@ pub mod types;
mod utilities;
mod verification;
#[cfg(feature = "testing")]
#[cfg(any(test, feature = "testing"))]
/// Testing facilities and helpers for crypto tests
pub mod testing {
pub use crate::identities::{
@@ -70,7 +70,9 @@ impl RoomKeyImportResult {
}
}
pub use error::{EventError, MegolmError, OlmError, SessionCreationError, SignatureError};
pub use error::{
EventError, MegolmError, OlmError, SessionCreationError, SetRoomSettingsError, SignatureError,
};
pub use file_encryption::{
decrypt_room_key_export, encrypt_room_key_export, AttachmentDecryptor, AttachmentEncryptor,
DecryptorError, KeyExportError, MediaEncryptionInfo,
@@ -105,3 +107,6 @@ pub static VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
matrix_sdk_test::init_tracing_for_tests!();
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
+355 -90
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{BTreeMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
sync::{Arc, RwLock as StdRwLock},
time::Duration,
};
@@ -40,8 +40,8 @@ use ruma::{
AnyToDeviceEvent, MessageLikeEventContent,
},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId,
RoomId, TransactionId, UInt, UserId,
DeviceId, DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedDeviceKeyId,
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::Mutex;
@@ -58,7 +58,7 @@ use vodozemac::{
use crate::{
backups::{BackupMachine, MegolmV1BackupKey},
dehydrated_devices::{DehydratedDevices, DehydrationError},
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult},
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult, SetRoomSettingsError},
gossiping::GossipMachine,
identities::{user::UserIdentities, Device, IdentityManager, UserDevices},
olm::{
@@ -70,8 +70,8 @@ use crate::{
session_manager::{GroupSessionManager, SessionManager},
store::{
Changes, CryptoStoreWrapper, DeviceChanges, IdentityChanges, IntoCryptoStore, MemoryStore,
PendingChanges, Result as StoreResult, RoomKeyInfo, SecretImportError, Store, StoreCache,
StoreTransaction,
PendingChanges, Result as StoreResult, RoomKeyInfo, RoomSettings, SecretImportError, Store,
StoreCache, StoreTransaction,
},
types::{
events::{
@@ -86,8 +86,9 @@ use crate::{
},
ToDeviceEvents,
},
Signatures,
EventEncryptionAlgorithm, Signatures,
},
utilities::timestamp_to_iso8601,
verification::{Verification, VerificationMachine, VerificationRequest},
CrossSigningKeyExport, CryptoStoreError, KeysQueryRequest, LocalTrust, ReadOnlyDevice,
RoomKeyImportResult, SignatureError, ToDeviceRequest,
@@ -345,6 +346,16 @@ impl OlmMachine {
&self.inner.device_id
}
/// The time at which the `Account` backing this `OlmMachine` was created.
///
/// An [`Account`] is created when an `OlmMachine` is first instantiated
/// against a given [`Store`], at which point it creates identity keys etc.
/// This method returns the timestamp, according to the local clock, at
/// which that happened.
pub fn device_creation_time(&self) -> MilliSecondsSinceUnixEpoch {
self.inner.store.static_account().creation_local_time()
}
/// Get the public parts of our Olm identity keys.
pub fn identity_keys(&self) -> IdentityKeys {
let account = self.inner.store.static_account();
@@ -894,12 +905,17 @@ impl OlmMachine {
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
}
/// Invalidate the currently active outbound group session for the given
/// room.
/// Forces the currently active room key, which is used to encrypt messages,
/// to be rotated.
///
/// A new room key will be crated and shared with all the room members the
/// next time a message will be sent. You don't have to call this method,
/// room keys will be rotated automatically when necessary. This method is
/// still useful for debugging purposes.
///
/// Returns true if a session was invalidated, false if there was no session
/// to invalidate.
pub async fn invalidate_group_session(&self, room_id: &RoomId) -> StoreResult<bool> {
pub async fn discard_room_key(&self, room_id: &RoomId) -> StoreResult<bool> {
self.inner.group_session_manager.invalidate_group_session(room_id).await
}
@@ -1101,7 +1117,7 @@ impl OlmMachine {
transaction: &mut StoreTransaction,
changes: &mut Changes,
mut raw_event: Raw<AnyToDeviceEvent>,
) -> OlmResult<Raw<AnyToDeviceEvent>> {
) -> Raw<AnyToDeviceEvent> {
Self::record_message_id(&raw_event);
let event: ToDeviceEvents = match raw_event.deserialize_as() {
@@ -1110,7 +1126,7 @@ impl OlmMachine {
// Skip invalid events.
warn!("Received an invalid to-device event: {e}");
return Ok(raw_event);
return raw_event;
}
};
@@ -1135,7 +1151,7 @@ impl OlmMachine {
}
}
return Ok(raw_event);
return raw_event;
}
};
@@ -1172,7 +1188,7 @@ impl OlmMachine {
e => self.handle_to_device_event(changes, &e).await,
}
Ok(raw_event)
raw_event
}
/// Handle a to-device and one-time key counts from a sync response.
@@ -1247,8 +1263,7 @@ impl OlmMachine {
for raw_event in sync_changes.to_device_events {
let raw_event =
Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event))
.await?;
Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event)).await;
events.push(raw_event);
}
@@ -1392,7 +1407,7 @@ impl OlmMachine {
let secret_requests = GossipMachine::request_missing_secrets(self.user_id(), secrets);
// Check if there are already inflight requests for these secrets?
// Check if there are already in-flight requests for these secrets?
let unsent_request = self.store().get_unsent_secret_requests().await?;
let not_yet_requested = secret_requests
.into_iter()
@@ -1530,7 +1545,7 @@ impl OlmMachine {
/// * `event` - The event that should be decrypted.
///
/// * `room_id` - The ID of the room where the event was sent to.
#[instrument(skip_all, fields(?room_id, event_id, sender, algorithm, session_id, sender_key))]
#[instrument(skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, sender_key))]
pub async fn decrypt_room_event(
&self,
event: &Raw<EncryptedEvent>,
@@ -1541,6 +1556,11 @@ impl OlmMachine {
tracing::Span::current()
.record("sender", debug(&event.sender))
.record("event_id", debug(&event.event_id))
.record(
"origin_server_ts",
timestamp_to_iso8601(event.origin_server_ts)
.unwrap_or_else(|| "<out of range>".to_owned()),
)
.record("algorithm", debug(event.content.algorithm()));
let content: SupportedEventEncryptionSchemes<'_> = match &event.content.scheme {
@@ -1803,50 +1823,6 @@ impl OlmMachine {
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
}
/// Export the keys that match the given predicate.
///
/// # Arguments
///
/// * `predicate` - A closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true` the `InboundGroupSession` will be included in the export,
/// if the closure returns `false` it will not be included.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_room_key_export};
/// # use ruma::{device_id, user_id, room_id};
/// # let alice = user_id!("@alice:example.org");
/// # async {
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// let room_id = room_id!("!test:localhost");
/// let exported_keys = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
/// # };
/// ```
pub async fn export_room_keys(
&self,
mut predicate: impl FnMut(&InboundGroupSession) -> bool,
) -> StoreResult<Vec<ExportedRoomKey>> {
let mut exported = Vec::new();
let sessions: Vec<InboundGroupSession> = self
.store()
.get_inbound_group_sessions()
.await?
.into_iter()
.filter(|s| predicate(s))
.collect();
for session in sessions {
let export = session.export().await;
exported.push(export);
}
Ok(exported)
}
/// Get the status of the private cross signing keys.
///
/// This can be used to check which private cross signing keys we have
@@ -1961,6 +1937,8 @@ impl OlmMachine {
None => 0,
};
tracing::debug!("Initialising crypto store generation at {}", gen);
self.inner
.store
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, gen.to_le_bytes().to_vec())
@@ -1973,19 +1951,32 @@ impl OlmMachine {
/// If needs be, update the local and on-disk crypto store generation.
///
/// Returns true whether another user has modified the internal generation
/// counter, and as such we've incremented and updated it in the
/// database.
///
/// ## Requirements
///
/// - This assumes that `initialize_crypto_store_generation` has been called
/// beforehand.
/// - This requires that the crypto store lock has been acquired.
pub async fn maintain_crypto_store_generation(
&self,
///
/// # Arguments
///
/// * `generation` - The in-memory generation counter (or rather, the
/// `Mutex` wrapping it). This defines the "expected" generation on entry,
/// and, if we determine an update is needed, is updated to hold the "new"
/// generation.
///
/// # Returns
///
/// A tuple containing:
///
/// * A `bool`, set to `true` if another process has updated the generation
/// number in the `Store` since our expected value, and as such we've
/// incremented and updated it in the database. Otherwise, `false`.
///
/// * The (possibly updated) generation counter.
pub async fn maintain_crypto_store_generation<'a>(
&'a self,
generation: &Mutex<Option<u64>>,
) -> StoreResult<bool> {
) -> StoreResult<(bool, u64)> {
let mut gen_guard = generation.lock().await;
// The database value must be there:
@@ -2007,10 +1998,10 @@ impl OlmMachine {
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
})?);
let expected_gen = match gen_guard.as_ref() {
let new_gen = match gen_guard.as_ref() {
Some(expected_gen) => {
if actual_gen == *expected_gen {
return Ok(false);
return Ok((false, actual_gen));
}
// Increment the biggest, and store it everywhere.
actual_gen.max(*expected_gen).wrapping_add(1)
@@ -2026,22 +2017,19 @@ impl OlmMachine {
"Crypto store generation mismatch: previously known was {:?}, actual is {:?}, next is {}",
*gen_guard,
actual_gen,
expected_gen
new_gen
);
// Update known value.
*gen_guard = Some(expected_gen);
*gen_guard = Some(new_gen);
// Update value in database.
self.inner
.store
.set_custom_value(
Self::CURRENT_GENERATION_STORE_KEY,
expected_gen.to_le_bytes().to_vec(),
)
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, new_gen.to_le_bytes().to_vec())
.await?;
Ok(true)
Ok((true, new_gen))
}
/// Manage dehydrated devices.
@@ -2049,6 +2037,89 @@ impl OlmMachine {
DehydratedDevices { inner: self.to_owned() }
}
/// Get the stored encryption settings for the given room, such as the
/// encryption algorithm or whether to encrypt only for trusted devices.
///
/// These settings can be modified via [`OlmMachine::set_room_settings`].
pub async fn room_settings(&self, room_id: &RoomId) -> StoreResult<Option<RoomSettings>> {
// There's not much to do here: it's just exposed for symmetry with
// `set_room_settings`.
self.inner.store.get_room_settings(room_id).await
}
/// Store encryption settings for the given room.
///
/// This method checks if the new settings are "safe" -- ie, that they do
/// not represent a downgrade in encryption security from any previous
/// settings. Attempts to downgrade security will result in a
/// [`SetRoomSettingsError::EncryptionDowngrade`].
///
/// If the settings are valid, they will be persisted to the crypto store.
/// These settings are not used directly by this library, but the saved
/// settings can be retrieved via [`OlmMachine::room_settings`].
pub async fn set_room_settings(
&self,
room_id: &RoomId,
new_settings: &RoomSettings,
) -> Result<(), SetRoomSettingsError> {
let store = &self.inner.store;
// We want to make sure that we do not race against a second concurrent call to
// `set_room_settings`. By way of an easy way to do so, we start a
// StoreTransaction. There's no need to commit() it: we're just using it as a
// lock guard.
let _store_transaction = store.transaction().await;
let old_settings = store.get_room_settings(room_id).await?;
// We want to make sure that the change to the room settings does not represent
// a downgrade in security. The [E2EE implementation guide] recommends:
//
// > This flag should **not** be cleared if a later `m.room.encryption` event
// > changes the configuration.
//
// (However, it doesn't really address how to handle changes to the rotation
// parameters, etc.) For now at least, we are very conservative here:
// any new settings are rejected if they differ from the existing settings.
// merit improvement (cf https://github.com/element-hq/element-meta/issues/69).
//
// [E2EE implementation guide]: https://matrix.org/docs/matrix-concepts/end-to-end-encryption/#handling-an-m-room-encryption-state-event
if let Some(old_settings) = old_settings {
if old_settings != *new_settings {
return Err(SetRoomSettingsError::EncryptionDowngrade);
} else {
// nothing to do here
return Ok(());
}
}
// Make sure that the new settings are valid
match new_settings.algorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2 => (),
#[cfg(feature = "experimental-algorithms")]
EventEncryptionAlgorithm::MegolmV2AesSha2 => (),
_ => {
warn!(
?room_id,
"Rejecting invalid encryption algorithm {}", new_settings.algorithm
);
return Err(SetRoomSettingsError::InvalidSettings);
}
}
// The new settings are acceptable, so let's save them.
store
.save_changes(Changes {
room_settings: HashMap::from([(room_id.to_owned(), new_settings.clone())]),
..Default::default()
})
.await?;
Ok(())
}
#[cfg(any(feature = "testing", test))]
/// Returns whether this `OlmMachine` is the same another one.
///
@@ -2108,8 +2179,8 @@ pub struct EncryptionSyncChanges<'a> {
}
#[cfg(any(feature = "testing", test))]
#[allow(dead_code)]
pub(crate) mod testing {
#![allow(dead_code)]
use http::Response;
pub fn response_from_file(json: &serde_json::Value) -> Response<Vec<u8>> {
@@ -2156,10 +2227,11 @@ pub(crate) mod tests {
},
room_id,
serde::Raw,
to_device::DeviceIdOrAllDevices,
uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
OwnedDeviceKeyId, SecondsSinceUnixEpoch, TransactionId, UserId,
};
use serde_json::json;
use serde_json::{json, value::to_raw_value};
use vodozemac::{
megolm::{GroupSession, SessionConfig},
Curve25519PublicKey, Ed25519PublicKey,
@@ -2167,10 +2239,10 @@ pub(crate) mod tests {
use super::{testing::response_from_file, CrossSigningBootstrapRequests};
use crate::{
error::EventError,
error::{EventError, SetRoomSettingsError},
machine::{EncryptionSyncChanges, OlmMachine},
olm::{InboundGroupSession, OutboundGroupSession, VerifyJson},
store::Changes,
store::{Changes, RoomSettings},
types::{
events::{
room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent},
@@ -2243,9 +2315,9 @@ pub(crate) mod tests {
.store()
.with_transaction(|mut tr| async {
let account = tr.account().await.unwrap();
account.generate_fallback_key_helper();
account.generate_fallback_key_if_needed();
account.update_uploaded_key_count(0);
account.generate_one_time_keys();
account.generate_one_time_keys_if_needed();
let request = machine
.keys_for_upload(account)
.await
@@ -2273,7 +2345,7 @@ pub(crate) mod tests {
(machine, otk)
}
async fn get_machine_pair(
pub async fn get_machine_pair(
alice: &UserId,
bob: &UserId,
use_fallback_key: bool,
@@ -2348,8 +2420,13 @@ pub(crate) mod tests {
#[async_test]
async fn test_create_olm_machine() {
let test_start_ts = MilliSecondsSinceUnixEpoch::now();
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let device_creation_time = machine.device_creation_time();
assert!(device_creation_time <= MilliSecondsSinceUnixEpoch::now());
assert!(device_creation_time >= test_start_ts);
let cache = machine.store().cache().await.unwrap();
let account = cache.account().await.unwrap();
assert!(!account.shared());
@@ -2371,7 +2448,7 @@ pub(crate) mod tests {
.store()
.with_transaction(|mut tr| async {
let account = tr.account().await.unwrap();
assert!(account.generate_one_time_keys().is_some());
assert!(account.generate_one_time_keys_if_needed().is_some());
Ok((tr, ()))
})
.await
@@ -2385,7 +2462,7 @@ pub(crate) mod tests {
.store()
.with_transaction(|mut tr| async {
let account = tr.account().await.unwrap();
assert!(account.generate_one_time_keys().is_some());
assert!(account.generate_one_time_keys_if_needed().is_some());
Ok((tr, ()))
})
.await
@@ -2399,7 +2476,7 @@ pub(crate) mod tests {
.store()
.with_transaction(|mut tr| async {
let account = tr.account().await.unwrap();
assert!(account.generate_one_time_keys().is_none());
assert!(account.generate_one_time_keys_if_needed().is_none());
Ok((tr, ()))
})
@@ -2437,7 +2514,7 @@ pub(crate) mod tests {
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
assert!(machine.inner.group_session_manager.get_outbound_group_session(room_id).is_some());
machine.invalidate_group_session(room_id).await.unwrap();
machine.discard_room_key(room_id).await.unwrap();
assert!(machine
.inner
@@ -2467,7 +2544,7 @@ pub(crate) mod tests {
fn test_one_time_key_signing() {
let mut account = Account::with_device_id(user_id(), alice_device_id());
account.update_uploaded_key_count(49);
account.generate_one_time_keys();
account.generate_one_time_keys_if_needed();
let mut one_time_keys = account.signed_one_time_keys();
let ed25519_key = account.identity_keys().ed25519;
@@ -4071,4 +4148,192 @@ pub(crate) mod tests {
// The waiting should successfully complete.
wait.await.unwrap();
}
#[async_test]
async fn room_settings_returns_none_for_unknown_room() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let settings = machine.room_settings(room_id!("!test2:localhost")).await.unwrap();
assert!(settings.is_none());
}
#[async_test]
async fn stores_and_returns_room_settings() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let room_id = room_id!("!test:localhost");
let settings = RoomSettings {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
only_allow_trusted_devices: true,
session_rotation_period: Some(Duration::from_secs(10)),
session_rotation_period_messages: Some(1234),
};
machine.set_room_settings(room_id, &settings).await.unwrap();
assert_eq!(machine.room_settings(room_id).await.unwrap(), Some(settings));
}
#[async_test]
async fn set_room_settings_rejects_invalid_algorithms() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let room_id = room_id!("!test:localhost");
let err = machine
.set_room_settings(
room_id,
&RoomSettings {
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
..Default::default()
},
)
.await
.unwrap_err();
assert_matches!(err, SetRoomSettingsError::InvalidSettings)
}
#[async_test]
async fn set_room_settings_rejects_changes() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let room_id = room_id!("!test:localhost");
// Initial settings
machine
.set_room_settings(
room_id,
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
)
.await
.unwrap();
// Now, modifying the settings should be rejected
let err = machine
.set_room_settings(
room_id,
&RoomSettings {
session_rotation_period_messages: Some(1000),
..Default::default()
},
)
.await
.unwrap_err();
assert_matches!(err, SetRoomSettingsError::EncryptionDowngrade);
}
#[async_test]
async fn set_room_settings_accepts_noop_changes() {
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
let room_id = room_id!("!test:localhost");
// Initial settings
machine
.set_room_settings(
room_id,
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
)
.await
.unwrap();
// Same again; should be fine.
machine
.set_room_settings(
room_id,
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
)
.await
.unwrap();
}
#[async_test]
async fn test_send_encrypted_to_device() {
let (alice, bob) = get_machine_pair_with_session(alice_id(), user_id(), false).await;
let custom_event_type = "m.new_device";
let custom_content = json!({
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
});
let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
let raw_encrypted = device
.encrypt_event_raw(custom_event_type, &custom_content)
.await
.expect("Should have encryted the content");
let request = ToDeviceRequest::new(
bob.user_id(),
DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned()),
"m.room.encrypted",
raw_encrypted.cast(),
);
assert_eq!("m.room.encrypted", request.event_type.to_string());
let messages = &request.messages;
assert_eq!(1, messages.len());
assert!(messages.get(bob.user_id()).is_some());
let target_devices = messages.get(bob.user_id()).unwrap();
assert_eq!(1, target_devices.len());
assert!(target_devices
.get(&DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned()))
.is_some());
let event = ToDeviceEvent::new(
alice.user_id().to_owned(),
to_device_requests_to_content(vec![request.clone().into()]),
);
let event = json_convert(&event).unwrap();
let sync_changes = EncryptionSyncChanges {
to_device_events: vec![event],
changed_devices: &Default::default(),
one_time_keys_counts: &Default::default(),
unused_fallback_keys: None,
next_batch_token: None,
};
let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
assert_eq!(1, decrypted.len());
let decrypted_event = decrypted[0].deserialize().unwrap();
assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned());
let decrypted_value = to_raw_value(&decrypted[0]).unwrap();
let decrypted_value = serde_json::to_value(decrypted_value).unwrap();
assert_eq!(
decrypted_value.get("content").unwrap().get("device_id").unwrap().as_str().unwrap(),
custom_content.get("device_id").unwrap().as_str().unwrap(),
);
assert_eq!(
decrypted_value.get("content").unwrap().get("rooms").unwrap().as_array().unwrap(),
custom_content.get("rooms").unwrap().as_array().unwrap(),
);
}
#[async_test]
async fn test_send_encrypted_to_device_no_session() {
let (alice, bob, _) = get_machine_pair(alice_id(), user_id(), false).await;
let custom_event_type = "m.new_device";
let custom_content = json!({
"device_id": "XYZABCDE",
"rooms": ["!726s6s6q:example.com"]
});
let encryption_result = alice
.get_device(bob.user_id(), bob_device_id(), None)
.await
.unwrap()
.unwrap()
.encrypt_event_raw(custom_event_type, &custom_content)
.await;
assert_matches!(encryption_result, Err(OlmError::MissingSession));
}
}
+190 -90
View File
@@ -17,8 +17,10 @@ use std::{
fmt,
ops::{Deref, Not as _},
sync::Arc,
time::Duration,
};
use js_option::JsOption;
use ruma::{
api::client::{
dehydrated_device::{DehydratedDeviceData, DehydratedDeviceV1},
@@ -161,6 +163,8 @@ pub struct StaticAccountData {
pub device_id: OwnedDeviceId,
/// The associated identity keys.
pub identity_keys: Arc<IdentityKeys>,
/// Whether the account is for a dehydrated device.
pub dehydrated: bool,
// The creation time of the account in milliseconds since epoch.
creation_local_time: MilliSecondsSinceUnixEpoch,
}
@@ -281,13 +285,17 @@ impl StaticAccountData {
),
]);
DeviceKeys::new(
let mut ret = DeviceKeys::new(
(*self.user_id).to_owned(),
(*self.device_id).to_owned(),
Self::ALGORITHMS.iter().map(|a| (**a).clone()).collect(),
keys,
Default::default(),
)
);
if self.dehydrated {
ret.dehydrated = JsOption::Some(true);
}
ret
}
/// Get the user id of the owner of the account.
@@ -327,6 +335,14 @@ pub struct Account {
/// needs to set this for us, depending on the count we will suggest the
/// client to upload new keys.
uploaded_signed_key_count: u64,
/// The timestamp of the last time we generated a fallback key. Fallback
/// keys are rotated in a time-based manner. This field records when we
/// either generated our first fallback key or rotated one.
///
/// Will be `None` if we never created a fallback key, or if we're migrating
/// from a `AccountPickle` that didn't use time-based fallback key
/// rotation.
fallback_creation_timestamp: Option<MilliSecondsSinceUnixEpoch>,
}
impl Deref for Account {
@@ -352,12 +368,18 @@ pub struct PickledAccount {
pub pickle: AccountPickle,
/// Was the account shared.
pub shared: bool,
/// Whether this is for a dehydrated device
#[serde(default)]
pub dehydrated: bool,
/// The number of uploaded one-time keys we have on the server.
pub uploaded_signed_key_count: u64,
/// The local time creation of this account (milliseconds since epoch), used
/// as creation time of own device
#[serde(default = "default_account_creation_time")]
pub creation_local_time: MilliSecondsSinceUnixEpoch,
/// The timestamp of the last time we generated a fallback key.
#[serde(default)]
pub fallback_key_creation_timestamp: Option<MilliSecondsSinceUnixEpoch>,
}
fn default_account_creation_time() -> MilliSecondsSinceUnixEpoch {
@@ -399,11 +421,13 @@ impl Account {
user_id: user_id.into(),
device_id: device_id.into(),
identity_keys: Arc::new(identity_keys),
dehydrated: false,
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
},
inner: Box::new(account),
shared: false,
uploaded_signed_key_count: 0,
fallback_creation_timestamp: None,
}
}
@@ -424,6 +448,17 @@ impl Account {
Self::new_helper(account, user_id, &device_id)
}
/// Create a new random Olm Account for a dehydrated device
pub fn new_dehydrated(user_id: &UserId) -> Self {
let account = InnerAccount::new();
let device_id: OwnedDeviceId =
base64_encode(account.identity_keys().curve25519.as_bytes()).into();
let mut ret = Self::new_helper(account, user_id, &device_id);
ret.static_data.dehydrated = true;
ret
}
/// Get the immutable data for this account.
pub fn static_data(&self) -> &StaticAccountData {
&self.static_data
@@ -464,7 +499,7 @@ impl Account {
}
/// Generate count number of one-time keys.
pub fn generate_one_time_keys_helper(&mut self, count: usize) -> OneTimeKeyGenerationResult {
pub fn generate_one_time_keys(&mut self, count: usize) -> OneTimeKeyGenerationResult {
self.inner.generate_one_time_keys(count)
}
@@ -493,14 +528,14 @@ impl Account {
}
self.update_uploaded_key_count(count);
self.generate_one_time_keys();
self.generate_one_time_keys_if_needed();
}
if let Some(unused) = unused_fallback_keys {
if !unused.contains(&DeviceKeyAlgorithm::SignedCurve25519) {
// Generate a new fallback key if we don't have one.
self.generate_fallback_key_helper();
}
// If the server supports fallback keys or if it did so in the past, shown by
// the existence of a fallback creation timestamp, generate a new one if
// we don't have one, or if the current fallback key expired.
if unused_fallback_keys.is_some() || self.fallback_creation_timestamp.is_some() {
self.generate_fallback_key_if_needed();
}
}
@@ -513,47 +548,91 @@ impl Account {
/// Generally `Some` means that keys should be uploaded, while `None` means
/// that keys should not be uploaded.
#[instrument(skip_all)]
pub fn generate_one_time_keys(&mut self) -> Option<u64> {
pub fn generate_one_time_keys_if_needed(&mut self) -> Option<u64> {
// Only generate one-time keys if there aren't any, otherwise the caller
// might have failed to upload them the last time this method was
// called.
if self.one_time_keys().is_empty() {
let count = self.uploaded_key_count();
let max_keys = self.max_one_time_keys();
if count >= max_keys as u64 {
return None;
}
let key_count = (max_keys as u64) - count;
let key_count: usize = key_count.try_into().unwrap_or(max_keys);
let result = self.generate_one_time_keys_helper(key_count);
debug!(
count = key_count,
discarded_keys = ?result.removed,
created_keys = ?result.created,
"Generated new one-time keys"
);
Some(key_count as u64)
} else {
Some(0)
if !self.one_time_keys().is_empty() {
return Some(0);
}
let count = self.uploaded_key_count();
let max_keys = self.max_one_time_keys();
if count >= max_keys as u64 {
return None;
}
let key_count = (max_keys as u64) - count;
let key_count: usize = key_count.try_into().unwrap_or(max_keys);
let result = self.generate_one_time_keys(key_count);
debug!(
count = key_count,
discarded_keys = ?result.removed,
created_keys = ?result.created,
"Generated new one-time keys"
);
Some(key_count as u64)
}
pub(crate) fn generate_fallback_key_helper(&mut self) {
if self.inner.fallback_key().is_empty() {
/// Generate a new fallback key iff a unpublished one isn't already inside
/// of vodozemac and if the currently active one expired.
///
/// The former is checked using [`Account::fallback_key().is_empty()`],
/// which is a hashmap that gets cleared by the
/// [`Account::mark_keys_as_published()`] call.
pub(crate) fn generate_fallback_key_if_needed(&mut self) {
if self.inner.fallback_key().is_empty() && self.fallback_key_expired() {
let removed_fallback_key = self.inner.generate_fallback_key();
self.fallback_creation_timestamp = Some(MilliSecondsSinceUnixEpoch::now());
debug!(
?removed_fallback_key,
"No unused fallback keys were found on the server, generated a new fallback key.",
"The fallback key either expired or we didn't have one: generated a new fallback key.",
);
}
}
/// Check if our most recent fallback key has expired.
///
/// We consider the fallback key to be expired if it's older than a week.
/// This is the lower bound for the recommended signed pre-key bundle
/// rotation interval in the X3DH spec[1].
///
/// [1]: https://signal.org/docs/specifications/x3dh/#publishing-keys
fn fallback_key_expired(&self) -> bool {
const FALLBACK_KEY_MAX_AGE: Duration = Duration::from_secs(3600 * 24 * 7);
if let Some(time) = self.fallback_creation_timestamp {
// `to_system_time()` returns `None` if the the UNIX_EPOCH + `time` doesn't fit
// into a i64. This will likely never happen, but let's rotate the
// key in case the values are messed up for some other reason.
let Some(system_time) = time.to_system_time() else {
return true;
};
// `elapsed()` errors if the `system_time` is in the future, this should mean
// that our clock has changed to the past, let's rotate just in case
// and then we'll get to a normal time.
let Ok(elapsed) = system_time.elapsed() else {
return true;
};
// Alright, our times are normal and we know how much time elapsed since the
// last time we created/rotated a fallback key.
//
// If the key is older than a week, then we rotate it.
elapsed > FALLBACK_KEY_MAX_AGE
} else {
// We never created a fallback key, or we're migrating to the time-based
// fallback key rotation, so let's generate a new fallback key.
true
}
}
fn fallback_key(&self) -> HashMap<KeyId, Curve25519PublicKey> {
self.inner.fallback_key()
}
@@ -593,8 +672,10 @@ impl Account {
device_id: self.device_id().to_owned(),
pickle,
shared: self.shared(),
dehydrated: self.static_data.dehydrated,
uploaded_signed_key_count: self.uploaded_key_count(),
creation_local_time: self.static_data.creation_local_time,
fallback_key_creation_timestamp: self.fallback_creation_timestamp,
}
}
@@ -646,11 +727,13 @@ impl Account {
user_id: (*pickle.user_id).into(),
device_id: (*pickle.device_id).into(),
identity_keys: Arc::new(identity_keys),
dehydrated: pickle.dehydrated,
creation_local_time: pickle.creation_local_time,
},
inner: Box::new(account),
shared: pickle.shared,
uploaded_signed_key_count: pickle.uploaded_signed_key_count,
fallback_creation_timestamp: pickle.fallback_key_creation_timestamp,
})
}
@@ -729,9 +812,9 @@ impl Account {
self.inner.sign_json(json)
}
/// Generate, sign and prepare one-time keys to be uploaded.
/// Sign and prepare one-time keys to be uploaded.
///
/// If no one-time keys need to be uploaded returns an empty BTreeMap.
/// If no one-time keys need to be uploaded, returns an empty `BTreeMap`.
pub fn signed_one_time_keys(
&self,
) -> BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>> {
@@ -900,9 +983,9 @@ impl Account {
PrekeyBundle::Olm3DH { key } => {
device.verify_one_time_key(&key).map_err(|error| {
SessionCreationError::InvalidSignature {
signing_key: device.ed25519_key(),
one_time_key: key.clone(),
error,
signing_key: device.ed25519_key().map(Box::new),
one_time_key: key.clone().into(),
error: error.into(),
}
})?;
@@ -937,28 +1020,19 @@ impl Account {
///
/// * `message` - A pre-key Olm message that was sent to us by the other
/// account.
#[instrument(
skip_all,
fields(
message,
session_id = message.session_id(),
session,
)
)]
pub fn create_inbound_session(
&mut self,
their_identity_key: Curve25519PublicKey,
message: &PreKeyMessage,
) -> Result<InboundCreationResult, SessionCreationError> {
debug!("Creating a new Olm session from a pre-key message");
Span::current().record("session_id", debug(message.session_id()));
trace!("Creating a new Olm session from a pre-key message");
let result = self.inner.create_inbound_session(their_identity_key, message)?;
let now = SecondsSinceUnixEpoch::now();
let session_id = result.session.session_id();
Span::current().record("session", debug(&result.session));
trace!("Olm session created successfully");
debug!(session=?result.session, "Decrypted an Olm message from a new Olm session");
let session = Session {
user_id: self.static_data.user_id.clone(),
@@ -980,10 +1054,13 @@ impl Account {
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
/// Testing only helper to create a session for the given Account
pub async fn create_session_for(&mut self, other: &mut Account) -> (Session, Session) {
pub async fn create_session_for_test_helper(
&mut self,
other: &mut Account,
) -> (Session, Session) {
use ruma::events::dummy::ToDeviceDummyEventContent;
other.generate_one_time_keys_helper(1);
other.generate_one_time_keys(1);
let one_time_map = other.signed_one_time_keys();
let device = ReadOnlyDevice::from_account(other);
@@ -1140,32 +1217,33 @@ impl Account {
match message {
OlmMessage::Normal(_) => {
let session_ids = if let Some(sessions) = existing_sessions {
let mut errors_by_olm_session = Vec::new();
if let Some(sessions) = existing_sessions {
let sessions = &mut *sessions.lock().await;
// Try to decrypt the message using each Session we share with the
// given curve25519 sender key.
for session in sessions.iter_mut() {
if let Ok(p) = session.decrypt(message).await {
// success!
return Ok((SessionType::Existing(session.clone()), p));
} else {
// An error here is completely normal, after all we don't know
// which session was used to encrypt a message. We will log a
// warning if no session was able to decrypt the message.
continue;
match session.decrypt(message).await {
Ok(p) => {
// success!
return Ok((SessionType::Existing(session.clone()), p));
}
Err(e) => {
// An error here is completely normal, after all we don't know
// which session was used to encrypt a message.
// We keep hold of the error, so that if *all* sessions fail to
// decrypt, we can log something useful.
errors_by_olm_session.push((session.session_id().to_owned(), e));
}
}
}
// decryption wasn't successful with any of the sessions. Collect a list of
// session IDs to log.
sessions.iter().map(|s| s.session_id().to_owned()).collect()
} else {
vec![]
};
}
warn!(
?session_ids,
?errors_by_olm_session,
"Failed to decrypt a non-pre-key message with all available sessions"
);
Err(OlmError::SessionWedged(sender.to_owned(), sender_key))
@@ -1237,7 +1315,7 @@ impl Account {
/// Decrypt an Olm message, creating a new Olm session if necessary, and
/// parse the result.
#[instrument(skip(self, store, message))]
#[instrument(skip(self, store), fields(session, session_id))]
async fn decrypt_and_parse_olm_message(
&mut self,
store: &Store,
@@ -1248,15 +1326,7 @@ impl Account {
let (session, plaintext) =
self.decrypt_olm_message(store, sender, sender_key, message).await?;
{
let session_id = match &session {
SessionType::New(s) => s.session_id(),
SessionType::Existing(s) => s.session_id(),
};
Span::current().record("session_id", session_id);
trace!("Successfully decrypted an Olm message");
}
trace!("Successfully decrypted an Olm message");
match self.parse_decrypted_to_device_event(store, sender, sender_key, plaintext).await {
Ok(result) => Ok((session, result)),
@@ -1383,6 +1453,7 @@ mod tests {
use std::{
collections::{BTreeMap, BTreeSet},
ops::Deref,
time::Duration,
};
use anyhow::Result;
@@ -1427,13 +1498,13 @@ mod tests {
account.mark_keys_as_published();
account.update_uploaded_key_count(50);
account.generate_one_time_keys();
account.generate_one_time_keys_if_needed();
let (_, third_one_time_keys, _) = account.keys_for_upload();
assert!(third_one_time_keys.is_empty());
account.update_uploaded_key_count(0);
account.generate_one_time_keys();
account.generate_one_time_keys_if_needed();
let (_, fourth_one_time_keys, _) = account.keys_for_upload();
assert!(!fourth_one_time_keys.is_empty());
@@ -1454,7 +1525,10 @@ mod tests {
// We don't create fallback keys since we don't know if the server
// supports them, we need to receive a sync response to decide if we're
// going to create them or not.
assert!(fallback_keys.is_empty());
assert!(
fallback_keys.is_empty(),
"We should not upload fallback keys until we know if the server supports them."
);
let one_time_keys = BTreeMap::from([(DeviceKeyAlgorithm::SignedCurve25519, 50u8.into())]);
@@ -1462,7 +1536,11 @@ mod tests {
// fallback key gets uploaded.
account.update_key_counts(&one_time_keys, None);
let (_, _, fallback_keys) = account.keys_for_upload();
assert!(fallback_keys.is_empty());
assert!(
fallback_keys.is_empty(),
"We should not upload a fallback key if we're certain that the server doesn't support \
them."
);
// The empty array means that the server supports fallback keys but
// there isn't a unused fallback key on the server. This time we upload
@@ -1470,14 +1548,36 @@ mod tests {
let unused_fallback_keys = &[];
account.update_key_counts(&one_time_keys, Some(unused_fallback_keys.as_ref()));
let (_, _, fallback_keys) = account.keys_for_upload();
assert!(!fallback_keys.is_empty());
assert!(
!fallback_keys.is_empty(),
"We should upload the initial fallback key if the server supports them."
);
account.mark_keys_as_published();
// There's an unused fallback key on the server, nothing to do here.
let unused_fallback_keys = &[DeviceKeyAlgorithm::SignedCurve25519];
// There's no unused fallback key on the server, but our initial fallback key
// did not yet expire.
let unused_fallback_keys = &[];
account.update_key_counts(&one_time_keys, Some(unused_fallback_keys.as_ref()));
let (_, _, fallback_keys) = account.keys_for_upload();
assert!(fallback_keys.is_empty());
assert!(
fallback_keys.is_empty(),
"We should not upload new fallback keys unless our current fallback key expires."
);
let fallback_key_timestamp =
account.fallback_creation_timestamp.unwrap().to_system_time().unwrap()
- Duration::from_secs(3600 * 24 * 30);
account.fallback_creation_timestamp =
Some(MilliSecondsSinceUnixEpoch::from_system_time(fallback_key_timestamp).unwrap());
account.update_key_counts(&one_time_keys, None);
let (_, _, fallback_keys) = account.keys_for_upload();
assert!(
!fallback_keys.is_empty(),
"Now that our fallback key has expired, we should try to upload a new one, even if the \
server supposedly doesn't support fallback keys anymore"
);
Ok(())
}
@@ -136,6 +136,11 @@ pub struct BackedUpRoomKey {
/// Chain of Curve25519 keys through which this session was forwarded, via
/// m.forwarded_room_key events.
#[serde(
default,
deserialize_with = "deserialize_curve_key_vec",
serialize_with = "serialize_curve_key_vec"
)]
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
}
@@ -245,3 +250,33 @@ impl TryFrom<ForwardedRoomKeyContent> for ExportedRoomKey {
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::BackedUpRoomKey;
#[test]
fn test_deserialize_backed_up_key() {
let data = json!({
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": ["DBPC2zr6c9qimo9YRFK3RVr0Two/I6ODb9mbsToZN3Q", "bBc/qzZFOOKshMMT+i4gjS/gWPDoKfGmETs9yfw9430"]
});
let backed_up_room_key: BackedUpRoomKey = serde_json::from_value(data)
.expect("We should be able to deserialize the backed up room key.");
assert_eq!(
backed_up_room_key.forwarding_curve25519_key_chain.len(),
2,
"The number of forwarding Curve25519 chains should be two."
);
}
}
@@ -59,7 +59,10 @@ use crate::{
ReadOnlyDevice, ToDeviceRequest,
};
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
const ONE_WEEK: Duration = Duration::from_secs(60 * 60 * 24 * 7);
const ROTATION_PERIOD: Duration = ONE_WEEK;
const ROTATION_MESSAGES: u64 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -415,15 +418,27 @@ impl OutboundGroupSession {
fn elapsed(&self) -> bool {
let creation_time = Duration::from_secs(self.creation_time.get().into());
let now = Duration::from_secs(SecondsSinceUnixEpoch::now().get().into());
// Since the encryption settings are provided by users and not
// checked someone could set a really low rotation period so
// clamp it to an hour.
now.checked_sub(creation_time)
.map(|elapsed| elapsed >= max(self.settings.rotation_period, Duration::from_secs(3600)))
.map(|elapsed| elapsed >= self.safe_rotation_period())
.unwrap_or(true)
}
/// Returns the rotation_period_ms that was set for this session, clamped
/// to be no less than one hour.
///
/// This is to prevent a malicious or careless user causing sessions to be
/// rotated very frequently.
///
/// The feature flag `_disable-minimum-rotation-period-ms` can
/// be used to prevent this behaviour (which can be useful for tests).
fn safe_rotation_period(&self) -> Duration {
if cfg!(feature = "_disable-minimum-rotation-period-ms") {
self.settings.rotation_period
} else {
max(self.settings.rotation_period, ONE_HOUR)
}
}
/// Check if the session has expired and if it should be rotated.
///
/// A session will expire after some time or if enough messages have been
@@ -735,20 +750,16 @@ pub struct PickledOutboundGroupSession {
#[cfg(test)]
mod tests {
use std::{sync::atomic::Ordering, time::Duration};
use std::time::Duration;
use matrix_sdk_test::async_test;
use ruma::{
device_id,
events::room::{
encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
message::RoomMessageEventContent,
},
room_id, uint, user_id, EventEncryptionAlgorithm,
uint, EventEncryptionAlgorithm,
};
use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD};
use crate::{Account, MegolmError};
#[test]
fn test_encryption_settings_conversion() {
@@ -768,78 +779,208 @@ mod tests {
assert_eq!(settings.rotation_period_msgs, 500);
}
#[async_test]
#[cfg(any(target_os = "linux", target_os = "macos", target_arch = "wasm32"))]
async fn test_expiration() -> Result<(), MegolmError> {
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
mod expiration {
use std::{sync::atomic::Ordering, time::Duration};
let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() };
let account =
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
.static_data;
let (session, _) = account
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
.await
.unwrap();
assert!(!session.expired());
let _ = session
.encrypt(
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
.await;
assert!(session.expired());
let settings = EncryptionSettings {
rotation_period: Duration::from_millis(100),
..Default::default()
use matrix_sdk_test::async_test;
use ruma::{
device_id, events::room::message::RoomMessageEventContent, room_id, serde::Raw, uint,
user_id, SecondsSinceUnixEpoch,
};
let (mut session, _) = account
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
.await
.unwrap();
use crate::{olm::OutboundGroupSession, Account, EncryptionSettings, MegolmError};
assert!(!session.expired());
const TWO_HOURS: Duration = Duration::from_secs(60 * 60 * 2);
let now = SecondsSinceUnixEpoch::now();
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3600));
assert!(session.expired());
let settings = EncryptionSettings { rotation_period_msgs: 0, ..Default::default() };
let (session, _) = account
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
.await
.unwrap();
assert!(!session.expired());
let _ = session
.encrypt(
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
#[async_test]
async fn session_is_not_expired_if_no_messages_sent_and_no_time_passed() {
// Given a session that expires after one message
let session = create_session(EncryptionSettings {
rotation_period_msgs: 1,
..Default::default()
})
.await;
assert!(session.expired());
let settings = EncryptionSettings { rotation_period_msgs: 100_000, ..Default::default() };
// When we send no messages at all
let (session, _) = account
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
.await
.unwrap();
// Then it is not expired
assert!(!session.expired());
}
assert!(!session.expired());
session.message_count.store(1000, Ordering::SeqCst);
assert!(!session.expired());
session.message_count.store(9999, Ordering::SeqCst);
assert!(!session.expired());
session.message_count.store(10_000, Ordering::SeqCst);
assert!(session.expired());
#[async_test]
async fn session_is_expired_if_we_rotate_every_message_and_one_was_sent(
) -> Result<(), MegolmError> {
// Given a session that expires after one message
let session = create_session(EncryptionSettings {
rotation_period_msgs: 1,
..Default::default()
})
.await;
Ok(())
// When we send a message
let _ = session
.encrypt(
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
.await;
// Then the session is expired
assert!(session.expired());
Ok(())
}
#[async_test]
async fn session_with_rotation_period_is_not_expired_after_no_time() {
// Given a session with a 2h expiration
let session = create_session(EncryptionSettings {
rotation_period: TWO_HOURS,
..Default::default()
})
.await;
// When we don't allow any time to pass
// Then it is not expired
assert!(!session.expired());
}
#[async_test]
async fn session_is_expired_after_rotation_period() {
// Given a session with a 2h expiration
let mut session = create_session(EncryptionSettings {
rotation_period: TWO_HOURS,
..Default::default()
})
.await;
// When 3 hours have passed
let now = SecondsSinceUnixEpoch::now();
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(10800));
// Then the session is expired
assert!(session.expired());
}
#[async_test]
#[cfg(not(feature = "_disable-minimum-rotation-period-ms"))]
async fn session_does_not_expire_under_one_hour_even_if_we_ask_for_shorter() {
// Given a session with a 100ms expiration
let mut session = create_session(EncryptionSettings {
rotation_period: Duration::from_millis(100),
..Default::default()
})
.await;
// When less than an hour has passed
let now = SecondsSinceUnixEpoch::now();
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
// Then the session is not expired: we enforce a minimum of 1 hour
assert!(!session.expired());
// But when more than an hour has passed
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3601));
// Then the session is expired
assert!(session.expired());
}
#[async_test]
#[cfg(feature = "_disable-minimum-rotation-period-ms")]
async fn with_disable_minrotperiod_feature_sessions_can_expire_quickly() {
// Given a session with a 100ms expiration
let mut session = create_session(EncryptionSettings {
rotation_period: Duration::from_millis(100),
..Default::default()
})
.await;
// When less than an hour has passed
let now = SecondsSinceUnixEpoch::now();
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
// Then the session is expired: the feature flag has prevented us enforcing a
// minimum
assert!(session.expired());
}
#[async_test]
async fn session_with_zero_msgs_rotation_is_not_expired_initially() {
// Given a session that is supposed to expire after zero messages
let session = create_session(EncryptionSettings {
rotation_period_msgs: 0,
..Default::default()
})
.await;
// When we send no messages
// Then the session is not expired: we are protected against this nonsensical
// setup
assert!(!session.expired());
}
#[async_test]
async fn session_with_zero_msgs_rotation_expires_after_one_message(
) -> Result<(), MegolmError> {
// Given a session that is supposed to expire after zero messages
let session = create_session(EncryptionSettings {
rotation_period_msgs: 0,
..Default::default()
})
.await;
// When we send a message
let _ = session
.encrypt(
"m.room.message",
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
)
.await;
// Then the session is expired: we treated rotation_period_msgs=0 as if it were
// =1
assert!(session.expired());
Ok(())
}
#[async_test]
async fn session_expires_after_10k_messages_even_if_we_ask_for_more() {
// Given we asked to expire after 100K messages
let session = create_session(EncryptionSettings {
rotation_period_msgs: 100_000,
..Default::default()
})
.await;
// Sanity: it does not expire after <10K messages
assert!(!session.expired());
session.message_count.store(1000, Ordering::SeqCst);
assert!(!session.expired());
session.message_count.store(9999, Ordering::SeqCst);
assert!(!session.expired());
// When we have sent >= 10K messages
session.message_count.store(10_000, Ordering::SeqCst);
// Then it is considered expired: we enforce a maximum of 10K messages before
// rotation.
assert!(session.expired());
}
async fn create_session(settings: EncryptionSettings) -> OutboundGroupSession {
let account =
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
.static_data;
let (session, _) = account
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
.await
.unwrap();
session
}
}
}
+3 -3
View File
@@ -85,7 +85,7 @@ pub(crate) mod tests {
let alice = Account::with_device_id(alice_id(), alice_device_id());
let mut bob = Account::with_device_id(bob_id(), bob_device_id());
bob.generate_one_time_keys_helper(1);
bob.generate_one_time_keys(1);
let one_time_key = *bob.one_time_keys().values().next().unwrap();
let sender_key = bob.identity_keys().curve25519;
let session = alice.create_outbound_session_helper(
@@ -116,7 +116,7 @@ pub(crate) mod tests {
assert!(!one_time_keys.is_empty());
assert_ne!(account.max_one_time_keys(), 0);
account.generate_one_time_keys_helper(10);
account.generate_one_time_keys(10);
let one_time_keys = account.one_time_keys();
assert_ne!(one_time_keys.values().len(), 0);
@@ -133,7 +133,7 @@ pub(crate) mod tests {
let mut alice = Account::with_device_id(alice_id(), alice_device_id());
let bob = Account::with_device_id(bob_id(), bob_device_id());
let alice_keys = alice.identity_keys();
alice.generate_one_time_keys_helper(1);
alice.generate_one_time_keys(1);
let one_time_keys = alice.one_time_keys();
alice.mark_keys_as_published();
+5 -8
View File
@@ -18,7 +18,7 @@ use ruma::{serde::Raw, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::sync::Mutex;
use tracing::{field::debug, instrument, trace, Span};
use tracing::{debug, Span};
use vodozemac::{
olm::{DecryptionError, OlmMessage, Session as InnerSession, SessionConfig, SessionPickle},
Curve25519PublicKey,
@@ -79,13 +79,12 @@ impl Session {
/// # Arguments
///
/// * `message` - The Olm message that should be decrypted.
#[instrument(skip_all, fields(session))]
pub async fn decrypt(&mut self, message: &OlmMessage) -> Result<String, DecryptionError> {
let mut inner = self.inner.lock().await;
let plaintext = inner.decrypt(message)?;
Span::current().record("session_id", inner.session_id());
Span::current().record("session", debug(inner));
trace!("Decrypted a Olm message");
let plaintext = inner.decrypt(message)?;
debug!(session=?inner, "Decrypted an Olm message");
let plaintext = String::from_utf8_lossy(&plaintext).to_string();
@@ -126,11 +125,9 @@ impl Session {
/// * `plaintext` - The plaintext that should be encrypted.
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
let mut session = self.inner.lock().await;
Span::current().record("session", debug(&session));
let message = session.encrypt(plaintext);
self.last_use_time = SecondsSinceUnixEpoch::now();
debug!(?session, "Successfully encrypted an event");
message
}
-8
View File
@@ -218,8 +218,6 @@ pub enum OutgoingRequests {
/// A room message request, usually for sending in-room interactive
/// verification events.
RoomMessage(RoomMessageRequest),
/// A request that will back up a batch of room keys to the server.
KeysBackup(KeysBackupRequest),
}
#[cfg(test)]
@@ -235,12 +233,6 @@ impl From<KeysQueryRequest> for OutgoingRequests {
}
}
impl From<KeysBackupRequest> for OutgoingRequests {
fn from(r: KeysBackupRequest) -> Self {
Self::KeysBackup(r)
}
}
impl From<KeysClaimRequest> for OutgoingRequests {
fn from(r: KeysClaimRequest) -> Self {
Self::KeysClaim(r)
+53 -24
View File
@@ -245,29 +245,40 @@ impl SecretStorageKey {
fn check_zero_message(&self) -> Result<(), DecodeError> {
match &self.storage_key_info.algorithm {
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(properties) => {
if properties.iv.as_bytes().len() != IV_SIZE {
Err(DecodeError::IvLength(IV_SIZE, properties.iv.as_bytes().len()))
} else {
let mut iv_array = [0u8; 16];
iv_array.copy_from_slice(properties.iv.as_bytes());
// I'm not particularly convinced that this couldn't have been done simpler. Why
// do we need to reproduce the ciphertext? Couldn't we just generate the MAC tag
// using the `ZERO_MESSAGE`?
let (Some(iv), Some(mac)) = (&properties.iv, &properties.mac) else {
// The IV and/or MAC are missing from the account data
// content. As the [spec] says, we have to assume that the
// key is valid.
//
// If someone is reading this and is designing a new secret encryption
// algorithm, please consider the above suggestion.
let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, "");
let ciphertext = key.apply_keystream(Self::ZERO_MESSAGE.to_vec(), &iv_array);
let expected_mac = HmacSha256Mac::from_slice(properties.mac.as_bytes())
.ok_or_else(|| {
DecodeError::MacLength(MAC_SIZE, properties.mac.as_bytes().len())
})?;
// [spec]: https://spec.matrix.org/unstable/client-server-api/#msecret_storagev1aes-hmac-sha2
return Ok(());
};
key.verify_mac(&ciphertext, expected_mac.as_bytes())?;
let iv = iv.as_bytes();
let iv_length = iv.len();
Ok(())
if iv_length != IV_SIZE {
return Err(DecodeError::IvLength(IV_SIZE, iv_length));
}
let mut iv_array = [0u8; 16];
iv_array.copy_from_slice(iv);
// I'm not particularly convinced that this couldn't have been done simpler.
// Why do we need to reproduce the ciphertext?
// Couldn't we just generate the MAC tag
// using the `ZERO_MESSAGE`?
//
// If someone is reading this and is designing a new secret encryption
// algorithm, please consider the above suggestion.
let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, "");
let ciphertext = key.apply_keystream(Self::ZERO_MESSAGE.to_vec(), &iv_array);
let expected_mac = HmacSha256Mac::from_slice(mac.as_bytes())
.ok_or_else(|| DecodeError::MacLength(MAC_SIZE, mac.as_bytes().len()))?;
key.verify_mac(&ciphertext, expected_mac.as_bytes())?;
Ok(())
}
custom => Err(DecodeError::UnsupportedAlgorithm(custom.algorithm().to_owned())),
}
@@ -283,7 +294,7 @@ impl SecretStorageKey {
SecretStorageKeyEventContent::new(
key_id,
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
SecretStorageV1AesHmacSha2Properties::new(iv, mac),
SecretStorageV1AesHmacSha2Properties::new(Some(iv), Some(mac)),
),
)
}
@@ -743,6 +754,24 @@ mod test {
);
}
/// The `iv` and `mac` properties within the `m.secret_storage.key.*`
/// content are optional, and the spec says we must assume the
/// passphrase is correct in that case.
#[test]
fn accepts_any_passphrase_if_mac_and_iv_are_missing() {
let mut content = SecretStorageKeyEventContent::new(
"my_new_key_id".to_owned(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
SecretStorageV1AesHmacSha2Properties::new(None, None),
),
);
content.passphrase =
Some(PassPhrase::new("salty goodness".to_owned(), UInt::new_saturating(100)));
SecretStorageKey::from_account_data("It's a secret to nobody", content.to_owned())
.expect("Should accept any passphrase");
}
#[test]
fn base58_parsing() {
const DECODED_KEY: [u8; 32] = [
@@ -862,8 +891,8 @@ mod test {
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
SecretStorageV1AesHmacSha2Properties::new(
Base64::new(vec![0u8; 14]),
Base64::new(vec![0u8; 32]),
Some(Base64::new(vec![0u8; 14])),
Some(Base64::new(vec![0u8; 32])),
),
),
);
@@ -878,8 +907,8 @@ mod test {
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
SecretStorageV1AesHmacSha2Properties::new(
Base64::new(vec![0u8; 16]),
Base64::new(vec![0u8; 10]),
Some(Base64::new(vec![0u8; 16])),
Some(Base64::new(vec![0u8; 10])),
),
),
);
@@ -343,6 +343,7 @@ impl GroupSessionManager {
/// Returns information indicating whether the session needs to be rotated
/// and the list of users/devices that should receive or not the session
/// (with withheld reason).
#[instrument(skip_all)]
pub async fn collect_session_recipients(
&self,
users: impl Iterator<Item = &UserId>,
@@ -436,6 +437,12 @@ impl GroupSessionManager {
shared.difference(&recipient_device_ids).collect::<BTreeSet<_>>();
should_rotate = !newly_deleted_or_blacklisted.is_empty();
if should_rotate {
debug!(
"Rotating a room key due to these devices being deleted/blacklisted {:?}",
newly_deleted_or_blacklisted,
);
}
};
}
@@ -443,6 +450,15 @@ impl GroupSessionManager {
withheld_devices.extend(withheld_recipients);
}
if should_rotate {
debug!(
should_rotate,
user_left,
visibility_changed,
algorithm_changed,
"Rotating room key to protect room history",
);
}
trace!(should_rotate, "Done calculating group session recipients");
Ok(CollectRecipientsResult { should_rotate, devices, withheld_devices })
@@ -718,7 +718,7 @@ mod tests {
assert!(request.one_time_keys.contains_key(bob.user_id()));
bob.generate_one_time_keys_helper(1);
bob.generate_one_time_keys(1);
let one_time = bob.signed_one_time_keys();
assert!(!one_time.is_empty());
bob.mark_keys_as_published();
@@ -854,7 +854,7 @@ mod tests {
.store
.with_transaction(|mut tr| async {
let manager_account = tr.account().await.unwrap();
let res = bob.create_session_for(manager_account).await;
let res = bob.create_session_for_test_helper(manager_account).await;
Ok((tr, res))
})
.await
@@ -882,7 +882,7 @@ mod tests {
assert!(request.one_time_keys.contains_key(bob.user_id()));
bob.generate_one_time_keys_helper(1);
bob.generate_one_time_keys(1);
let one_time = bob.signed_one_time_keys();
assert!(!one_time.is_empty());
bob.mark_keys_as_published();
@@ -1009,7 +1009,7 @@ mod tests {
// Since alice is timed out, we won't claim keys for her.
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
alice_account.generate_one_time_keys_helper(1);
alice_account.generate_one_time_keys(1);
let one_time = alice_account.signed_one_time_keys();
assert!(!one_time.is_empty());
@@ -4,44 +4,40 @@ macro_rules! cryptostore_integration_tests {
() => {
mod cryptostore_integration_tests {
use std::collections::{BTreeMap, HashMap};
use std::time::Duration;
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use ruma::{
device_id,
encryption::SignedKey,
events::secret::request::SecretName,
room_id,
serde::{Base64, Raw},
to_device::DeviceIdOrAllDevices,
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId
device_id, events::secret::request::SecretName, room_id, serde::Raw,
to_device::DeviceIdOrAllDevices, user_id, DeviceId, RoomId, TransactionId, UserId,
};
use serde_json::value::to_raw_value;
use $crate::{
olm::{
Curve25519PublicKey, InboundGroupSession, OlmMessageHash,
PrivateCrossSigningIdentity, Account, Session,
Account, Curve25519PublicKey, InboundGroupSession, OlmMessageHash,
PrivateCrossSigningIdentity, Session,
},
store::{
BackupKeys, Changes, CryptoStore, DeviceChanges,
GossipRequest, IdentityChanges, BackupDecryptionKey, RoomSettings, PendingChanges,
BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, GossipRequest,
IdentityChanges, PendingChanges, RoomSettings,
},
testing::{get_device, get_other_identity, get_own_identity},
types::{
events::{
dummy::DummyEventContent,
olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
room_key_request::MegolmV1AesSha2Content,
room_key_withheld::{
CommonWithheldCodeContent, MegolmV1AesSha2WithheldContent,
RoomKeyWithheldContent, WithheldCode,
},
olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
secret_send::SecretSendContent,
ToDeviceEvent,
},
EventEncryptionAlgorithm,
},
ReadOnlyDevice, SecretInfo, ToDeviceRequest, TrackedUser, GossippedSecret,
GossippedSecret, ReadOnlyDevice, SecretInfo, ToDeviceRequest, TrackedUser,
};
use super::get_store;
@@ -79,16 +75,15 @@ macro_rules! cryptostore_integration_tests {
let alice = Account::with_device_id(alice_id(), alice_device_id());
let mut bob = Account::with_device_id(bob_id(), bob_device_id());
bob.generate_one_time_keys_helper(1);
bob.generate_one_time_keys(1);
let one_time_key = *bob.one_time_keys().values().next().unwrap();
let sender_key = bob.identity_keys().curve25519;
let session = alice
.create_outbound_session_helper(
Default::default(),
sender_key,
one_time_key,
false,
);
let session = alice.create_outbound_session_helper(
Default::default(),
sender_key,
one_time_key,
false,
);
(alice, session)
}
@@ -101,7 +96,7 @@ macro_rules! cryptostore_integration_tests {
let account = get_account();
store
.save_pending_changes(PendingChanges { account: Some(account), })
.save_pending_changes(PendingChanges { account: Some(account) })
.await
.expect("Can't save account");
assert!(store.get_static_account().is_some());
@@ -114,7 +109,10 @@ macro_rules! cryptostore_integration_tests {
assert!(store.load_account().await.unwrap().is_none());
let account = get_account();
store.save_pending_changes(PendingChanges { account: Some(account), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account) })
.await
.expect("Can't save account");
assert!(store.get_static_account().is_some());
}
@@ -123,7 +121,10 @@ macro_rules! cryptostore_integration_tests {
let store = get_store("load_account", None).await;
let account = get_account();
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
.await
.expect("Can't save account");
let loaded_account = store.load_account().await.expect("Can't load account");
let loaded_account = loaded_account.unwrap();
@@ -137,7 +138,10 @@ macro_rules! cryptostore_integration_tests {
get_store("load_account_with_passphrase", Some("secret_passphrase")).await;
let account = get_account();
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
.await
.expect("Can't save account");
let loaded_account = store.load_account().await.expect("Can't load account");
let loaded_account = loaded_account.unwrap();
@@ -150,12 +154,18 @@ macro_rules! cryptostore_integration_tests {
let store = get_store("save_and_share_account", None).await;
let mut account = get_account();
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
.await
.expect("Can't save account");
account.mark_as_shared();
account.update_uploaded_key_count(50);
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
.await
.expect("Can't save account");
let loaded_account = store.load_account().await.expect("Can't load account");
let loaded_account = loaded_account.unwrap();
@@ -168,7 +178,10 @@ macro_rules! cryptostore_integration_tests {
async fn load_sessions() {
let store = get_store("load_sessions", None).await;
let (account, session) = get_account_and_session().await;
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
store
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
.await
.expect("Can't save account");
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
@@ -187,26 +200,37 @@ macro_rules! cryptostore_integration_tests {
#[async_test]
async fn add_and_save_session() {
let store_name = "add_and_save_session";
let store = get_store(store_name, None).await;
let (account, session) = get_account_and_session().await;
let sender_key = session.sender_key.to_base64();
let session_id = session.session_id().to_owned();
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
// Given we created a session and saved it in the store
let (session_id, account, sender_key) = {
let store = get_store(store_name, None).await;
let (account, session) = get_account_and_session().await;
let sender_key = session.sender_key.to_base64();
let session_id = session.session_id().to_owned();
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
store.save_changes(changes).await.unwrap();
store
.save_pending_changes(PendingChanges {
account: Some(account.deep_clone()),
})
.await
.expect("Can't save account");
let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap();
let sessions_lock = sessions.lock().await;
let session = &sessions_lock[0];
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
store.save_changes(changes).await.unwrap();
assert_eq!(session_id, session.session_id());
let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap();
let sessions_lock = sessions.lock().await;
let session = &sessions_lock[0];
drop(store);
assert_eq!(session_id, session.session_id());
(session_id, account, sender_key)
};
// When we reload the store
let store = get_store(store_name, None).await;
// Then the same account and session info was reloaded
let loaded_account = store.load_account().await.unwrap().unwrap();
assert_eq!(account, loaded_account);
@@ -220,36 +244,50 @@ macro_rules! cryptostore_integration_tests {
#[async_test]
async fn load_outbound_group_session() {
let dir = "load_outbound_group_session";
let (account, store) = get_loaded_store(dir.clone()).await;
let room_id = room_id!("!test:localhost");
assert!(store.get_outbound_group_session(&room_id).await.unwrap().is_none());
let (session, _) = account.create_group_session_pair_with_defaults(&room_id).await;
// Given we saved an outbound group session
{
let (account, store) = get_loaded_store(dir.clone()).await;
assert!(
store.get_outbound_group_session(&room_id).await.unwrap().is_none(),
"Initially there should be no outbound group session"
);
let user_id = user_id!("@example:localhost");
let request = ToDeviceRequest::new(
user_id,
DeviceIdOrAllDevices::AllDevices,
"m.dummy",
Raw::from_json(to_raw_value(&DummyEventContent::new()).unwrap()),
);
let (session, _) =
account.create_group_session_pair_with_defaults(&room_id).await;
session.add_request(TransactionId::new(), request.into(), Default::default());
let user_id = user_id!("@example:localhost");
let request = ToDeviceRequest::new(
user_id,
DeviceIdOrAllDevices::AllDevices,
"m.dummy",
Raw::from_json(to_raw_value(&DummyEventContent::new()).unwrap()),
);
let changes = Changes {
outbound_group_sessions: vec![session.clone()],
..Default::default()
};
session.add_request(TransactionId::new(), request.into(), Default::default());
store.save_changes(changes).await.expect("Can't save group session");
let changes = Changes {
outbound_group_sessions: vec![session.clone()],
..Default::default()
};
drop(store);
store.save_changes(changes).await.expect("Can't save group session");
assert!(
store.get_outbound_group_session(&room_id).await.unwrap().is_some(),
"Sanity: after we've saved one, there should be an outbound_group_session"
);
}
// When we reload the account
let store = get_store(dir, None).await;
store.load_account().await.unwrap();
assert!(store.get_outbound_group_session(&room_id).await.unwrap().is_some());
// Then the saved session is restored
assert!(
store.get_outbound_group_session(&room_id).await.unwrap().is_some(),
"The outbound_group_session should have been loaded"
);
}
#[async_test]
@@ -285,10 +323,10 @@ macro_rules! cryptostore_integration_tests {
.unwrap();
assert_eq!(session, loaded_session);
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
assert_eq!(store.inbound_group_session_counts().await.unwrap().backed_up, 0);
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().backed_up, 0);
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
let to_back_up = store.inbound_group_sessions_for_backup("bkpver", 1).await.unwrap();
assert_eq!(to_back_up, vec![session])
}
@@ -299,19 +337,15 @@ macro_rules! cryptostore_integration_tests {
get_loaded_store("mark_inbound_group_sessions_as_backed_up").await;
let room_id = &room_id!("!test:localhost");
let mut sessions: Vec<InboundGroupSession> = Vec::with_capacity(10);
for i in 0..10 {
for _i in 0..10 {
sessions.push(account.create_group_session_pair_with_defaults(room_id).await.1);
}
let changes = Changes { inbound_group_sessions: sessions.clone(), ..Default::default() };
store.save_changes(changes).await.expect("Can't save group session");
assert_eq!(store.inbound_group_sessions_for_backup(100).await.unwrap().len(), 10);
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
(&session.room_id(), &session.session_id())
}
assert_eq!(store.inbound_group_sessions_for_backup("bkpver", 100).await.unwrap().len(), 10);
// When I mark some as backed up
let x = store.mark_inbound_group_sessions_as_backed_up(&[
store.mark_inbound_group_sessions_as_backed_up("bkpver", &[
session_info(&sessions[1]),
session_info(&sessions[3]),
session_info(&sessions[5]),
@@ -319,9 +353,8 @@ macro_rules! cryptostore_integration_tests {
session_info(&sessions[9]),
]).await.expect("Failed to mark sessions as backed up");
// And ask which still need backing up
let to_back_up = store.inbound_group_sessions_for_backup(10).await.unwrap();
let to_back_up = store.inbound_group_sessions_for_backup("bkpver", 10).await.unwrap();
let needs_backing_up = |i: usize| to_back_up.iter().any(|s| s.session_id() == sessions[i].session_id());
// Then the sessions we said were backed up no longer need backing up
@@ -344,27 +377,35 @@ macro_rules! cryptostore_integration_tests {
async fn reset_inbound_group_session_for_backup() {
let (account, store) =
get_loaded_store("reset_inbound_group_session_for_backup").await;
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 0);
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 0);
let room_id = &room_id!("!test:localhost");
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
session.mark_as_backed_up();
let changes =
Changes { inbound_group_sessions: vec![session.clone()], ..Default::default() };
store.save_changes(changes).await.expect("Can't save group session");
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
assert_eq!(store.inbound_group_session_counts().await.unwrap().backed_up, 1);
// Given we have backed up our session
store
.mark_inbound_group_sessions_as_backed_up("bkpver1", &[session_info(&session)])
.await
.expect("Failed to mark_inbound_group_sessions_as_backed_up.");
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
assert_eq!(store.inbound_group_session_counts(Some("bkpver1")).await.unwrap().total, 1);
assert_eq!(store.inbound_group_session_counts(Some("bkpver1")).await.unwrap().backed_up, 1);
// Sanity: before resetting, we have nothing to back up
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
assert_eq!(to_back_up, vec![]);
// When we reset the backup
store.reset_backup_state().await.unwrap();
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
// Then after resetting, even if we supply the same backup version number, we need
// to back up the session
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
assert_eq!(to_back_up, vec![session]);
}
@@ -377,7 +418,7 @@ macro_rules! cryptostore_integration_tests {
let room_id = &room_id!("!test:localhost");
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
let mut export = session.export().await;
let export = session.export().await;
let session = InboundGroupSession::from_export(&export).unwrap();
@@ -398,10 +439,10 @@ macro_rules! cryptostore_integration_tests {
.unwrap()
.unwrap();
assert_eq!(session, loaded_session);
let export = loaded_session.export().await;
loaded_session.export().await;
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
}
#[async_test]
@@ -426,13 +467,13 @@ macro_rules! cryptostore_integration_tests {
let loaded_alice =
loaded.get(alice).expect("Alice should be in the store as a tracked user");
let loaded_bob =
loaded.get(alice).expect("Bob should be in the store as as tracked user");
loaded.get(bob).expect("Bob should be in the store as as tracked user");
assert!(!loaded.contains_key(candy), "Candy shouldn't be part of the store");
assert_eq!(loaded.len(), 2, "Candy shouldn't be part of the store");
assert!(loaded_alice.dirty, "Alice should be considered to be dirty");
assert!(loaded_alice.dirty, "Bob should not be considered to be dirty");
assert!(!loaded_bob.dirty, "Bob should not be considered to be dirty");
};
let loaded = store.load_tracked_users().await.unwrap();
@@ -849,18 +890,21 @@ macro_rules! cryptostore_integration_tests {
#[async_test]
async fn room_settings_saving() {
let (account, store) = get_loaded_store("room_settings_saving").await;
let (_, store) = get_loaded_store("room_settings_saving").await;
let room_1 = room_id!("!test_1:localhost");
let settings_1 = RoomSettings {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
only_allow_trusted_devices: true,
session_rotation_period: Some(Duration::from_secs(10)),
session_rotation_period_messages: Some(123),
};
let room_2 = room_id!("!test_2:localhost");
let settings_2 = RoomSettings {
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
only_allow_trusted_devices: false,
..Default::default()
};
let room_3 = room_id!("!test_3:localhost");
@@ -887,7 +931,7 @@ macro_rules! cryptostore_integration_tests {
#[async_test]
async fn backup_keys_saving() {
let (account, store) = get_loaded_store("backup_keys_saving").await;
let (_account, store) = get_loaded_store("backup_keys_saving").await;
let restored = store.load_backup_keys().await.unwrap();
assert!(restored.decryption_key.is_none(), "Initially no backup decryption key should be present");
@@ -911,7 +955,7 @@ macro_rules! cryptostore_integration_tests {
#[async_test]
async fn custom_value_saving() {
let (account, store) = get_loaded_store("custom_value_saving").await;
let (_, store) = get_loaded_store("custom_value_saving").await;
store.set_custom_value("A", "Hello".as_bytes().to_vec()).await.unwrap();
let loaded_1 = store.get_custom_value("A").await.unwrap();
@@ -920,6 +964,10 @@ macro_rules! cryptostore_integration_tests {
let loaded_2 = store.get_custom_value("B").await.unwrap();
assert_eq!(None, loaded_2);
}
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
(&session.room_id(), &session.session_id())
}
}
};
}
+383 -14
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
convert::Infallible,
sync::{Arc, RwLock as StdRwLock},
time::{Duration, Instant},
@@ -25,7 +25,6 @@ use ruma::{
OwnedUserId, RoomId, TransactionId, UserId,
};
use tokio::sync::{Mutex, RwLock};
use tracing::warn;
use super::{
caches::{DeviceStore, GroupSessionStore, SessionStore},
@@ -55,6 +54,9 @@ pub struct MemoryStore {
account: StdRwLock<Option<Account>>,
sessions: SessionStore,
inbound_group_sessions: GroupSessionStore,
outbound_group_sessions: StdRwLock<BTreeMap<OwnedRoomId, OutboundGroupSession>>,
private_identity: StdRwLock<Option<PrivateCrossSigningIdentity>>,
tracked_users: StdRwLock<HashMap<OwnedUserId, TrackedUser>>,
olm_hashes: StdRwLock<HashMap<String, HashSet<String>>>,
devices: DeviceStore,
identities: StdRwLock<HashMap<OwnedUserId, ReadOnlyUserIdentities>>,
@@ -66,6 +68,7 @@ pub struct MemoryStore {
secret_inbox: StdRwLock<HashMap<String, Vec<GossippedSecret>>>,
backup_keys: RwLock<BackupKeys>,
next_batch_token: RwLock<Option<String>>,
room_settings: StdRwLock<HashMap<OwnedRoomId, RoomSettings>>,
}
impl Default for MemoryStore {
@@ -74,6 +77,9 @@ impl Default for MemoryStore {
account: Default::default(),
sessions: SessionStore::new(),
inbound_group_sessions: GroupSessionStore::new(),
outbound_group_sessions: Default::default(),
private_identity: Default::default(),
tracked_users: Default::default(),
olm_hashes: Default::default(),
devices: DeviceStore::new(),
identities: Default::default(),
@@ -85,6 +91,7 @@ impl Default for MemoryStore {
backup_keys: Default::default(),
secret_inbox: Default::default(),
next_batch_token: Default::default(),
room_settings: Default::default(),
}
}
}
@@ -118,6 +125,17 @@ impl MemoryStore {
self.inbound_group_sessions.add(session);
}
}
fn save_outbound_group_sessions(&self, sessions: Vec<OutboundGroupSession>) {
self.outbound_group_sessions
.write()
.unwrap()
.extend(sessions.into_iter().map(|s| (s.room_id().to_owned(), s)));
}
fn save_private_identity(&self, private_identity: Option<PrivateCrossSigningIdentity>) {
*self.private_identity.write().unwrap() = private_identity;
}
}
type Result<T> = std::result::Result<T, Infallible>;
@@ -132,7 +150,7 @@ impl CryptoStore for MemoryStore {
}
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
Ok(None)
Ok(self.private_identity.read().unwrap().clone())
}
async fn next_batch_token(&self) -> Result<Option<String>> {
@@ -150,6 +168,8 @@ impl CryptoStore for MemoryStore {
async fn save_changes(&self, changes: Changes) -> Result<()> {
self.save_sessions(changes.sessions).await;
self.save_inbound_group_sessions(changes.inbound_group_sessions);
self.save_outbound_group_sessions(changes.outbound_group_sessions);
self.save_private_identity(changes.private_identity);
self.save_devices(changes.devices.new);
self.save_devices(changes.devices.changed);
@@ -213,6 +233,11 @@ impl CryptoStore for MemoryStore {
*self.next_batch_token.write().await = Some(next_batch_token);
}
if !changes.room_settings.is_empty() {
let mut settings = self.room_settings.write().unwrap();
settings.extend(changes.room_settings);
}
Ok(())
}
@@ -245,7 +270,10 @@ impl CryptoStore for MemoryStore {
Ok(self.inbound_group_sessions.get_all())
}
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts> {
async fn inbound_group_session_counts(
&self,
_backup_version: Option<&str>,
) -> Result<RoomKeyCounts> {
let backed_up =
self.get_inbound_group_sessions().await?.into_iter().filter(|s| s.backed_up()).count();
@@ -254,6 +282,7 @@ impl CryptoStore for MemoryStore {
async fn inbound_group_sessions_for_backup(
&self,
_backup_version: &str,
limit: usize,
) -> Result<Vec<InboundGroupSession>> {
Ok(self
@@ -267,6 +296,7 @@ impl CryptoStore for MemoryStore {
async fn mark_inbound_group_sessions_as_backed_up(
&self,
_backup_version: &str,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<()> {
for (room_id, session_id) in room_and_session_ids {
@@ -291,15 +321,22 @@ impl CryptoStore for MemoryStore {
Ok(self.backup_keys.read().await.to_owned())
}
async fn get_outbound_group_session(&self, _: &RoomId) -> Result<Option<OutboundGroupSession>> {
Ok(None)
async fn get_outbound_group_session(
&self,
room_id: &RoomId,
) -> Result<Option<OutboundGroupSession>> {
Ok(self.outbound_group_sessions.read().unwrap().get(room_id).cloned())
}
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
Ok(Vec::new())
Ok(self.tracked_users.read().unwrap().values().cloned().collect())
}
async fn save_tracked_users(&self, _: &[(&UserId, bool)]) -> Result<()> {
async fn save_tracked_users(&self, tracked_users: &[(&UserId, bool)]) -> Result<()> {
self.tracked_users.write().unwrap().extend(tracked_users.iter().map(|(user_id, dirty)| {
let user_id: OwnedUserId = user_id.to_owned().into();
(user_id.clone(), TrackedUser { user_id, dirty: *dirty })
}));
Ok(())
}
@@ -393,9 +430,8 @@ impl CryptoStore for MemoryStore {
Ok(())
}
async fn get_room_settings(&self, _room_id: &RoomId) -> Result<Option<RoomSettings>> {
warn!("Method not implemented");
Ok(None)
async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
Ok(self.room_settings.read().unwrap().get(room_id).cloned())
}
async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
@@ -454,12 +490,15 @@ impl CryptoStore for MemoryStore {
#[cfg(test)]
mod tests {
use matrix_sdk_test::async_test;
use ruma::room_id;
use ruma::{room_id, user_id};
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
use crate::{
identities::device::testing::get_device,
olm::{tests::get_account_and_session_test_helper, InboundGroupSession, OlmMessageHash},
olm::{
tests::get_account_and_session_test_helper, InboundGroupSession, OlmMessageHash,
PrivateCrossSigningIdentity,
},
store::{memorystore::MemoryStore, Changes, CryptoStore, PendingChanges},
};
@@ -482,7 +521,7 @@ mod tests {
}
#[async_test]
async fn test_group_session_store() {
async fn test_inbound_group_session_store() {
let (account, _) = get_account_and_session_test_helper();
let room_id = room_id!("!test:localhost");
let curve_key = "Nn0L2hkcCMFKqynTjyGsJbth7QrVmX3lbrksMkrGOAw";
@@ -506,6 +545,67 @@ mod tests {
assert_eq!(inbound, loaded_session);
}
#[async_test]
async fn test_outbound_group_session_store() {
// Given an outbound session
let (account, _) = get_account_and_session_test_helper();
let room_id = room_id!("!test:localhost");
let (outbound, _) = account.create_group_session_pair_with_defaults(room_id).await;
// When we save it to the store
let store = MemoryStore::new();
store.save_outbound_group_sessions(vec![outbound.clone()]);
// Then we can get it out again
let loaded_session = store.get_outbound_group_session(room_id).await.unwrap().unwrap();
assert_eq!(
serde_json::to_string(&outbound.pickle().await).unwrap(),
serde_json::to_string(&loaded_session.pickle().await).unwrap()
);
}
#[async_test]
async fn test_tracked_users_are_stored_once_per_user_id() {
// Given a store containing 2 tracked users, both dirty
let user1 = user_id!("@user1:s");
let user2 = user_id!("@user2:s");
let user3 = user_id!("@user3:s");
let store = MemoryStore::new();
store.save_tracked_users(&[(user1, true), (user2, true)]).await.unwrap();
// When we mark one as clean and add another
store.save_tracked_users(&[(user2, false), (user3, false)]).await.unwrap();
// Then we can get them out again and their dirty flags are correct
let loaded_tracked_users =
store.load_tracked_users().await.expect("failed to load tracked users");
let tracked_contains = |user_id, dirty| {
loaded_tracked_users.iter().any(|u| u.user_id == user_id && u.dirty == dirty)
};
assert!(tracked_contains(user1, true));
assert!(tracked_contains(user2, false));
assert!(tracked_contains(user3, false));
assert_eq!(loaded_tracked_users.len(), 3);
}
#[async_test]
async fn test_private_identity_store() {
// Given a private identity
let private_identity = PrivateCrossSigningIdentity::empty(user_id!("@u:s"));
// When we save it to the store
let store = MemoryStore::new();
store.save_private_identity(Some(private_identity.clone()));
// Then we can get it out again
let loaded_identity =
store.load_identity().await.expect("failed to load private identity").unwrap();
assert_eq!(loaded_identity.user_id(), user_id!("@u:s"));
}
#[async_test]
async fn test_device_store() {
let device = get_device();
@@ -546,3 +646,272 @@ mod tests {
assert!(store.is_message_known(&hash).await.unwrap());
}
}
#[cfg(test)]
mod integration_tests {
use std::{
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
};
use async_trait::async_trait;
use ruma::{
events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId,
};
use super::MemoryStore;
use crate::{
cryptostore_integration_tests, cryptostore_integration_tests_time,
olm::{
InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity,
StaticAccountData,
},
store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings},
types::events::room_key_withheld::RoomKeyWithheldEvent,
Account, GossipRequest, GossippedSecret, ReadOnlyDevice, ReadOnlyUserIdentities,
SecretInfo, Session, TrackedUser,
};
/// Holds on to a MemoryStore during a test, and moves it back into STORES
/// when this is dropped
#[derive(Clone, Debug)]
struct PersistentMemoryStore(Arc<MemoryStore>);
impl PersistentMemoryStore {
fn new() -> Self {
Self(Arc::new(MemoryStore::new()))
}
fn get_static_account(&self) -> Option<StaticAccountData> {
self.0.get_static_account()
}
}
impl MemoryStore {
fn get_static_account(&self) -> Option<StaticAccountData> {
self.account.read().unwrap().as_ref().map(|acc| acc.static_data().clone())
}
}
/// Return a clone of the store for the test with the supplied name. Note:
/// dropping this store won't destroy its data, since
/// [PersistentMemoryStore] is a reference-counted smart pointer
/// to an underlying [MemoryStore].
async fn get_store(name: &str, _passphrase: Option<&str>) -> PersistentMemoryStore {
// Holds on to one [PersistentMemoryStore] per test, so even if the test drops
// the store, we keep its data alive. This simulates the behaviour of
// the other stores, which keep their data in a real DB, allowing us to
// test MemoryStore using the same code.
static STORES: OnceLock<Mutex<HashMap<String, PersistentMemoryStore>>> = OnceLock::new();
let stores = STORES.get_or_init(|| Mutex::new(HashMap::new()));
stores
.lock()
.unwrap()
.entry(name.to_owned())
.or_insert_with(PersistentMemoryStore::new)
.clone()
}
/// Forwards all methods to the underlying [MemoryStore].
#[async_trait]
impl CryptoStore for PersistentMemoryStore {
type Error = <MemoryStore as CryptoStore>::Error;
async fn load_account(&self) -> Result<Option<Account>, Self::Error> {
self.0.load_account().await
}
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>, Self::Error> {
self.0.load_identity().await
}
async fn save_changes(&self, changes: Changes) -> Result<(), Self::Error> {
self.0.save_changes(changes).await
}
async fn save_pending_changes(&self, changes: PendingChanges) -> Result<(), Self::Error> {
self.0.save_pending_changes(changes).await
}
async fn get_sessions(
&self,
sender_key: &str,
) -> Result<Option<Arc<tokio::sync::Mutex<Vec<Session>>>>, Self::Error> {
self.0.get_sessions(sender_key).await
}
async fn get_inbound_group_session(
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<InboundGroupSession>, Self::Error> {
self.0.get_inbound_group_session(room_id, session_id).await
}
async fn get_withheld_info(
&self,
room_id: &RoomId,
session_id: &str,
) -> Result<Option<RoomKeyWithheldEvent>, Self::Error> {
self.0.get_withheld_info(room_id, session_id).await
}
async fn get_inbound_group_sessions(
&self,
) -> Result<Vec<InboundGroupSession>, Self::Error> {
self.0.get_inbound_group_sessions().await
}
async fn inbound_group_session_counts(
&self,
backup_version: Option<&str>,
) -> Result<RoomKeyCounts, Self::Error> {
self.0.inbound_group_session_counts(backup_version).await
}
async fn inbound_group_sessions_for_backup(
&self,
backup_version: &str,
limit: usize,
) -> Result<Vec<InboundGroupSession>, Self::Error> {
self.0.inbound_group_sessions_for_backup(backup_version, limit).await
}
async fn mark_inbound_group_sessions_as_backed_up(
&self,
backup_version: &str,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<(), Self::Error> {
self.0
.mark_inbound_group_sessions_as_backed_up(backup_version, room_and_session_ids)
.await
}
async fn reset_backup_state(&self) -> Result<(), Self::Error> {
self.0.reset_backup_state().await
}
async fn load_backup_keys(&self) -> Result<BackupKeys, Self::Error> {
self.0.load_backup_keys().await
}
async fn get_outbound_group_session(
&self,
room_id: &RoomId,
) -> Result<Option<OutboundGroupSession>, Self::Error> {
self.0.get_outbound_group_session(room_id).await
}
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>, Self::Error> {
self.0.load_tracked_users().await
}
async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<(), Self::Error> {
self.0.save_tracked_users(users).await
}
async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<ReadOnlyDevice>, Self::Error> {
self.0.get_device(user_id, device_id).await
}
async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<HashMap<OwnedDeviceId, ReadOnlyDevice>, Self::Error> {
self.0.get_user_devices(user_id).await
}
async fn get_user_identity(
&self,
user_id: &UserId,
) -> Result<Option<ReadOnlyUserIdentities>, Self::Error> {
self.0.get_user_identity(user_id).await
}
async fn is_message_known(
&self,
message_hash: &OlmMessageHash,
) -> Result<bool, Self::Error> {
self.0.is_message_known(message_hash).await
}
async fn get_outgoing_secret_requests(
&self,
request_id: &TransactionId,
) -> Result<Option<GossipRequest>, Self::Error> {
self.0.get_outgoing_secret_requests(request_id).await
}
async fn get_secret_request_by_info(
&self,
secret_info: &SecretInfo,
) -> Result<Option<GossipRequest>, Self::Error> {
self.0.get_secret_request_by_info(secret_info).await
}
async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>, Self::Error> {
self.0.get_unsent_secret_requests().await
}
async fn delete_outgoing_secret_requests(
&self,
request_id: &TransactionId,
) -> Result<(), Self::Error> {
self.0.delete_outgoing_secret_requests(request_id).await
}
async fn get_secrets_from_inbox(
&self,
secret_name: &SecretName,
) -> Result<Vec<GossippedSecret>, Self::Error> {
self.0.get_secrets_from_inbox(secret_name).await
}
async fn delete_secrets_from_inbox(
&self,
secret_name: &SecretName,
) -> Result<(), Self::Error> {
self.0.delete_secrets_from_inbox(secret_name).await
}
async fn get_room_settings(
&self,
room_id: &RoomId,
) -> Result<Option<RoomSettings>, Self::Error> {
self.0.get_room_settings(room_id).await
}
async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_custom_value(key).await
}
async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<(), Self::Error> {
self.0.set_custom_value(key, value).await
}
async fn remove_custom_value(&self, key: &str) -> Result<(), Self::Error> {
self.0.remove_custom_value(key).await
}
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
async fn next_batch_token(&self) -> Result<Option<String>, Self::Error> {
self.0.next_batch_token().await
}
}
cryptostore_integration_tests!();
cryptostore_integration_tests_time!();
}
+186 -1
View File
@@ -526,7 +526,7 @@ pub struct Changes {
}
/// A user for which we are tracking the list of devices.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TrackedUser {
/// The user ID of the user.
pub user_id: OwnedUserId,
@@ -833,9 +833,18 @@ pub(crate) enum UserKeyQueryResult {
pub struct RoomSettings {
/// The encryption algorithm that should be used in the room.
pub algorithm: EventEncryptionAlgorithm,
/// Should untrusted devices receive the room key, or should they be
/// excluded from the conversation.
pub only_allow_trusted_devices: bool,
/// The maximum time an encryption session should be used for, before it is
/// rotated.
pub session_rotation_period: Option<Duration>,
/// The maximum number of messages an encryption session should be used for,
/// before it is rotated.
pub session_rotation_period_messages: Option<usize>,
}
impl Default for RoomSettings {
@@ -843,6 +852,8 @@ impl Default for RoomSettings {
Self {
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
only_allow_trusted_devices: false,
session_rotation_period: None,
session_rotation_period_messages: None,
}
}
}
@@ -1583,6 +1594,87 @@ impl Store {
pub(crate) fn crypto_store(&self) -> Arc<CryptoStoreWrapper> {
self.inner.store.clone()
}
/// Export the keys that match the given predicate.
///
/// # Arguments
///
/// * `predicate` - A closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true` the `InboundGroupSession` will be included in the export,
/// if the closure returns `false` it will not be included.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_room_key_export};
/// # use ruma::{device_id, user_id, room_id};
/// # let alice = user_id!("@alice:example.org");
/// # async {
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// let room_id = room_id!("!test:localhost");
/// let exported_keys = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
/// # };
/// ```
pub async fn export_room_keys(
&self,
predicate: impl FnMut(&InboundGroupSession) -> bool,
) -> Result<Vec<ExportedRoomKey>> {
let mut exported = Vec::new();
let mut sessions = self.get_inbound_group_sessions().await?;
sessions.retain(predicate);
for session in sessions {
let export = session.export().await;
exported.push(export);
}
Ok(exported)
}
/// Export room keys matching a predicate, providing them as an async
/// `Stream`.
///
/// # Arguments
///
/// * `predicate` - A closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true` the `InboundGroupSession` will be included in the export,
/// if the closure returns `false` it will not be included.
///
/// # Examples
///
/// ```no_run
/// use std::pin::pin;
///
/// use matrix_sdk_crypto::{olm::ExportedRoomKey, OlmMachine};
/// use ruma::{device_id, room_id, user_id};
/// use tokio_stream::StreamExt;
/// # async {
/// let alice = user_id!("@alice:example.org");
/// let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// let room_id = room_id!("!test:localhost");
/// let mut keys = pin!(machine
/// .store()
/// .export_room_keys_stream(|s| s.room_id() == room_id)
/// .await
/// .unwrap());
/// while let Some(key) = keys.next().await {
/// println!("{}", key.room_id);
/// }
/// # };
/// ```
pub async fn export_room_keys_stream(
&self,
predicate: impl FnMut(&InboundGroupSession) -> bool,
) -> Result<impl Stream<Item = ExportedRoomKey>> {
// TODO: if/when there is a get_inbound_group_sessions_stream, use that here.
let sessions = self.get_inbound_group_sessions().await?;
Ok(futures_util::stream::iter(sessions.into_iter().filter(predicate))
.then(|session| async move { session.export().await }))
}
}
impl Deref for Store {
@@ -1611,3 +1703,96 @@ impl matrix_sdk_common::store_locks::BackingStore for LockableCryptoStore {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use futures_util::StreamExt;
use matrix_sdk_test::async_test;
use ruma::{room_id, user_id};
use crate::{machine::tests::get_machine_pair, types::EventEncryptionAlgorithm};
#[async_test]
async fn export_room_keys_provides_selected_keys() {
// Given an OlmMachine with room keys in it
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
let room1_id = room_id!("!room1:localhost");
let room2_id = room_id!("!room2:localhost");
let room3_id = room_id!("!room3:localhost");
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
alice.create_outbound_group_session_with_defaults_test_helper(room3_id).await.unwrap();
// When I export some of the keys
let keys = alice
.store()
.export_room_keys(|s| s.room_id() == room2_id || s.room_id() == room3_id)
.await
.unwrap();
// Then the requested keys were provided
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
assert_eq!(keys[1].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
assert_eq!(keys[0].room_id, "!room2:localhost");
assert_eq!(keys[1].room_id, "!room3:localhost");
assert_eq!(keys[0].session_key.to_base64().len(), 220);
assert_eq!(keys[1].session_key.to_base64().len(), 220);
}
#[async_test]
async fn export_room_keys_stream_can_provide_all_keys() {
// Given an OlmMachine with room keys in it
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
let room1_id = room_id!("!room1:localhost");
let room2_id = room_id!("!room2:localhost");
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
// When I export the keys as a stream
let mut keys = pin!(alice.store().export_room_keys_stream(|_| true).await.unwrap());
// And collect them
let mut collected = vec![];
while let Some(key) = keys.next().await {
collected.push(key);
}
// Then all the keys were provided
assert_eq!(collected.len(), 2);
assert_eq!(collected[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
assert_eq!(collected[1].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
assert_eq!(collected[0].room_id, "!room1:localhost");
assert_eq!(collected[1].room_id, "!room2:localhost");
assert_eq!(collected[0].session_key.to_base64().len(), 220);
assert_eq!(collected[1].session_key.to_base64().len(), 220);
}
#[async_test]
async fn export_room_keys_stream_can_provide_a_subset_of_keys() {
// Given an OlmMachine with room keys in it
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
let room1_id = room_id!("!room1:localhost");
let room2_id = room_id!("!room2:localhost");
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
// When I export the keys as a stream
let mut keys =
pin!(alice.store().export_room_keys_stream(|s| s.room_id() == room1_id).await.unwrap());
// And collect them
let mut collected = vec![];
while let Some(key) = keys.next().await {
collected.push(key);
}
// Then all the keys matching our predicate were provided, and no others
assert_eq!(collected.len(), 1);
assert_eq!(collected[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
assert_eq!(collected[0].room_id, "!room1:localhost");
assert_eq!(collected[0].session_key.to_base64().len(), 220);
}
}
+40 -12
View File
@@ -105,22 +105,43 @@ pub trait CryptoStore: AsyncTraitDeps {
/// Get the number inbound group sessions we have and how many of them are
/// backed up.
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts, Self::Error>;
async fn inbound_group_session_counts(
&self,
backup_version: Option<&str>,
) -> Result<RoomKeyCounts, Self::Error>;
/// Get all the inbound group sessions we have not backed up yet.
/// Return a batch of ['InboundGroupSession'] ("room keys") that have not
/// yet been backed up in the supplied backup version.
///
/// The size of the returned `Vec` is <= `limit`.
///
/// Note: some implementations ignore `backup_version` and assume the
/// current backup version, which is normally the same.
async fn inbound_group_sessions_for_backup(
&self,
backup_version: &str,
limit: usize,
) -> Result<Vec<InboundGroupSession>, Self::Error>;
/// Mark the inbound group sessions with the supplied room and session IDs
/// as backed up
/// Store the fact that the supplied sessions were backed up into the backup
/// with version `backup_version`.
///
/// Note: some implementations ignore `backup_version` and assume the
/// current backup version, which is normally the same.
async fn mark_inbound_group_sessions_as_backed_up(
&self,
backup_version: &str,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<(), Self::Error>;
/// Reset the backup state of all the stored inbound group sessions.
///
/// Note: this is mostly implemented by stores that ignore the
/// `backup_version` argument on `inbound_group_sessions_for_backup` and
/// `mark_inbound_group_sessions_as_backed_up`. Implementations that
/// pay attention to the supplied backup version probably don't need to
/// update their storage when the current backup version changes, so have
/// empty implementations of this method.
async fn reset_backup_state(&self) -> Result<(), Self::Error>;
/// Get the backup keys we have stored.
@@ -133,11 +154,14 @@ pub trait CryptoStore: AsyncTraitDeps {
room_id: &RoomId,
) -> Result<Option<OutboundGroupSession>, Self::Error>;
/// Load the list of users whose devices we are keeping track of.
/// Provide the list of users whose devices we are keeping track of, and
/// whether they are considered dirty/outdated.
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>, Self::Error>;
/// Save a list of users and their respective dirty/outdated flags to the
/// store.
/// Update the list of users whose devices we are keeping track of, and
/// whether they are considered dirty/outdated.
///
/// Replaces any existing entry with a matching user ID.
async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<(), Self::Error>;
/// Get the device for the given user with the given device ID.
@@ -328,23 +352,27 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
self.0.get_inbound_group_sessions().await.map_err(Into::into)
}
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts> {
self.0.inbound_group_session_counts().await.map_err(Into::into)
async fn inbound_group_session_counts(
&self,
backup_version: Option<&str>,
) -> Result<RoomKeyCounts> {
self.0.inbound_group_session_counts(backup_version).await.map_err(Into::into)
}
async fn inbound_group_sessions_for_backup(
&self,
backup_version: &str,
limit: usize,
) -> Result<Vec<InboundGroupSession>> {
self.0.inbound_group_sessions_for_backup(limit).await.map_err(Into::into)
self.0.inbound_group_sessions_for_backup(backup_version, limit).await.map_err(Into::into)
}
async fn mark_inbound_group_sessions_as_backed_up(
&self,
backup_version: &str,
room_and_session_ids: &[(&RoomId, &str)],
) -> Result<()> {
self.0
.mark_inbound_group_sessions_as_backed_up(room_and_session_ids)
.mark_inbound_group_sessions_as_backed_up(backup_version, room_and_session_ids)
.await
.map_err(Into::into)
}
@@ -20,6 +20,7 @@
use std::collections::BTreeMap;
use js_option::JsOption;
use ruma::{
serde::Raw, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedUserId,
};
@@ -52,6 +53,10 @@ pub struct DeviceKeys {
/// Signatures for the device key object.
pub signatures: Signatures,
/// Whether the device is a dehydrated device or not
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
pub dehydrated: JsOption<bool>,
/// Additional data added to the device key information by intermediate
/// servers, and not covered by the signatures.
#[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")]
@@ -77,6 +82,7 @@ impl DeviceKeys {
algorithms,
keys,
signatures,
dehydrated: JsOption::Undefined,
unsigned: Default::default(),
other: BTreeMap::new(),
}
@@ -182,6 +188,8 @@ struct DeviceKeyHelper {
pub device_id: OwnedDeviceId,
pub algorithms: Vec<EventEncryptionAlgorithm>,
pub keys: BTreeMap<OwnedDeviceKeyId, String>,
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
pub dehydrated: JsOption<bool>,
pub signatures: Signatures,
#[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")]
pub unsigned: UnsignedDeviceInfo,
@@ -216,6 +224,7 @@ impl TryFrom<DeviceKeyHelper> for DeviceKeys {
device_id: value.device_id,
algorithms: value.algorithms,
keys: keys?,
dehydrated: value.dehydrated,
signatures: value.signatures,
unsigned: value.unsigned,
other: value.other,
@@ -233,6 +242,7 @@ impl From<DeviceKeys> for DeviceKeyHelper {
device_id: value.device_id,
algorithms: value.algorithms,
keys,
dehydrated: value.dehydrated,
signatures: value.signatures,
unsigned: value.unsigned,
other: value.other,
+2 -2
View File
@@ -246,10 +246,10 @@ impl<T: Ord> SigningKeys<T> {
}
/// Get a `SigningKey` with the given `DeviceKeyId`.
pub fn get<Q: ?Sized>(&self, key_id: &Q) -> Option<&SigningKey>
pub fn get<Q>(&self, key_id: &Q) -> Option<&SigningKey>
where
T: Borrow<Q>,
Q: Ord,
Q: Ord + ?Sized,
{
self.0.get(key_id)
}
+62
View File
@@ -12,6 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::num::NonZeroU8;
use ruma::MilliSecondsSinceUnixEpoch;
use time::{
format_description::well_known::{iso8601, Iso8601},
OffsetDateTime,
};
#[cfg(test)]
pub(crate) fn json_convert<T, U>(value: &T) -> serde_json::Result<U>
where
@@ -21,3 +29,57 @@ where
let json = serde_json::to_string(value)?;
serde_json::from_str(&json)
}
const ISO8601_WITH_MILLIS: iso8601::EncodedConfig = iso8601::Config::DEFAULT
.set_time_precision(iso8601::TimePrecision::Second { decimal_digits: NonZeroU8::new(3) })
.encode();
/// Format the given timestamp into a human-readable timestamp.
///
/// # Returns
///
/// Provided the timestamp fits within an `OffsetDateTime` (ie, it is on or
/// before year 9999), a string that looks like `1970-01-01T00:00:00.000Z`.
/// Otherwise, `None`.
pub fn timestamp_to_iso8601(ts: MilliSecondsSinceUnixEpoch) -> Option<String> {
let nanos_since_epoch = i128::from(ts.get()) * 1_000_000;
// OffsetDateTime has a max year of 9999, whereas MilliSecondsSinceUnixEpoch has
// a max year of 285427, so `from_unix_timestamp_nanos` can overflow for very
// large timestamps. (The Y10K problem!)
let dt = OffsetDateTime::from_unix_timestamp_nanos(nanos_since_epoch).ok()?;
// SAFETY: `format` can fail if:
// * The input lacks information on a component we have asked it to format
// (eg, it is given a `Time` and we ask it for a date), or
// * The input contains an invalid component (eg 30th February), or
// * An `io::Error` is raised internally.
//
// The first two cannot occur because we know we are giving it a valid
// OffsetDateTime that has all the components we are asking it to print.
//
// The third should not occur because we are formatting a short string to an
// in-memory buffer.
Some(dt.format(&Iso8601::<ISO8601_WITH_MILLIS>).unwrap())
}
#[cfg(test)]
pub(crate) mod tests {
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
use super::timestamp_to_iso8601;
#[test]
fn test_timestamp_to_iso8601() {
assert_eq!(
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(0))),
Some("1970-01-01T00:00:00.000Z".to_owned())
);
assert_eq!(
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(1709657033012))),
Some("2024-03-05T16:43:53.012Z".to_owned())
);
assert_eq!(timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::MAX)), None);
}
}
@@ -12,10 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
};
use std::collections::BTreeMap;
use as_variant::as_variant;
use ruma::{
@@ -771,7 +768,6 @@ impl TryFrom<OutgoingRequest> for OutgoingContent {
match value.request() {
crate::OutgoingRequests::KeysUpload(_)
| crate::OutgoingRequests::KeysQuery(_)
| crate::OutgoingRequests::KeysBackup(_)
| crate::OutgoingRequests::SignatureUpload(_)
| crate::OutgoingRequests::KeysClaim(_) => Err("Invalid request type".to_owned()),
crate::OutgoingRequests::ToDeviceRequest(r) => Self::try_from(r.clone()),

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