Compare commits

..

974 Commits

Author SHA1 Message Date
Ivan Enderlin eddaf961a1 Merge pull request #3462 from matrix-org/valere/binding_ffi_crypto_expose_clear_caches
FFI Bindings: Expose CryptoStore clear caches
2024-05-28 10:50:27 +02:00
Valere 0e456cded2 review: cargo fmt 2024-05-27 16:14:49 +02:00
Valere a042b3d409 FFI Bindings: Review, remove runtime block_on, use await
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Valere <bill.carson@valrsoft.com>
2024-05-27 12:49:59 +02:00
Ivan Enderlin 16891ff937 feat(sdk): Mutation-based testing and property-based testing for LinkedChunk
feat(sdk): Mutation-based testing and property-based testing for `LinkedChunk`
2024-05-27 11:35:49 +02:00
Benjamin Bouvier 70403b5d9a nit: address typo
Signed-off-by: Benjamin Bouvier <public@benj.me>
2024-05-27 10:32:19 +02:00
Benjamin Bouvier 6a8dd30f26 fixup! rooms: use the heroes names if provided to compute the display name 2024-05-27 10:32:19 +02:00
Benjamin Bouvier 75d6a048b9 room names: better handle the unfortunate case where there are no heroes 2024-05-27 10:32:19 +02:00
Benjamin Bouvier d186e735d3 rooms: use the heroes names if provided to compute the display name 2024-05-27 10:32:19 +02:00
Benjamin Bouvier 75c4cebdb6 sliding sync: also save the heroes' names from sync 2024-05-27 10:32:19 +02:00
Benjamin Bouvier 56be9d5461 sdk-base: store heroes as OwnedUserIds in our own RoomSummary
This is the right goal, and Ruma will be updated to reflect that the
`heroes` field should contain `OwnedUserId` too in [1].

This simplifies the code a bit, and avoids a round-trip encoding
user-ids into a string then decoding them from a string later, in the
case of sliding sync and room name computation.

[1] https://github.com/ruma/ruma/pull/1822
2024-05-27 10:32:19 +02:00
Benjamin Bouvier 2851a42fed sliding sync: save UserIds in the room_summary.heroes field 2024-05-27 10:32:19 +02:00
Benjamin Bouvier 58cbc0ffaa multiverse: add support for room names
Since rendering is sync, and I want the computed display name (which
retrieval is async), I have to fetch the display name early, just after
getting the room. Oh well :)
2024-05-27 10:32:19 +02:00
Ivan Enderlin 93fa01f7f5 feat(ui): Enable include_heroes for all_rooms and visible_rooms. 2024-05-27 10:32:19 +02:00
Ivan Enderlin 4892fce37c feat(sdk): Add include_heroes as a sticky parameter for SlidingSyncList. 2024-05-27 10:32:19 +02:00
Ivan Enderlin cedcda5372 feat(base+ffi): SlidingSync consumes heroes from a response.
This patch does 3 things:

1. It updates Ruma to the latest revision at the time of writing,
2. It updates `matrix-sdk-ffi` to provide the
   `RoomSubscription::include_heroes` field,
3. It updates `matrix-sdk-base` so that SlidingSync consumes `heroes`
   from the response and update the `RoomSummary` accordingly.

A test has been added to ensure the `RoomSummary` is updated as expected.
2024-05-27 10:32:19 +02:00
Ivan Enderlin 78433d3dc7 test(sdk): Do not run proptest on wasm32. 2024-05-27 10:05:43 +02:00
Ivan Enderlin db9e6518ed Merge pull request #3463 from matrix-org/valere/fix_up_crypto_0.7.1_release_branch_not_merged
chore(crypto): Fixup version and changelog from patch release
2024-05-27 09:44:01 +02:00
Ivan Enderlin 4c9398f6bd test(sdk): Use property-based testing to test AsVector.
This patch adds property-based testing to test `AsVector`. In this case
it works pretty well.
2024-05-27 09:21:43 +02:00
Ivan Enderlin 3ef8cb2f92 chore(sdk): Add missing derive(Debug). 2024-05-27 09:21:43 +02:00
Ivan Enderlin bbe9d75bd6 fix(sdk): Drain Updates when building AsVector.
This patch fixes a bug found with the `as_vector` fuzz test where the
`Updates` weren't drained when building `AsVector`.
2024-05-27 09:21:43 +02:00
Ivan Enderlin 8d6f88f612 test(sdk) Add a test for Chunk::is_first_chunk and Chunk::is_last_chunk.
This patch is testing two more methods on `Chunk`: `is_first_chunk` and
`is_last_chunk`. It's been detected by a mutant with `cargo-mutants`.
2024-05-27 09:21:40 +02:00
Valere 8cf52972bb ffi-crypto-bindings: Improve comments 2024-05-24 16:13:31 +02:00
Valere e99b67c281 chore(crypto): Fixup version and changelog from patch release 2024-05-24 16:03:03 +02:00
Valere 1985eede8f Changelog: Update crypto crate changelog 2024-05-24 15:09:31 +02:00
Valere 0f5f71f07a FFI Bindings: Expose CryptoStore clear caches 2024-05-24 14:59:35 +02:00
Richard van der Hoff 1dc370942b Merge pull request #3456 from matrix-org/rav/fix_fix_backup_import
crypto: Wire up `save_inbound_group_sessions` to `room_keys_received` stream
2024-05-24 10:32:49 +01:00
Benjamin Bouvier e0d7f4d9c6 timeline: make live_back_pagination_status available only on live timelines
It doesn't make sense to expose it for a focused timeline.

Note there's no way for timeline to change focus, at the moment; this
would require special handling here, because one could subscribe to the
back pagination status on a live timeline first, then switch the
timeline to the focus mode. The stream wrapper could then observe other
states that aren't expected by the converter method.
2024-05-24 11:21:34 +02:00
Benjamin Bouvier aebfaf58a3 timeline: reintroduce a custom pagination status that indicates if we've hit the timeline start
This is actually required for one use case: if the event cache triggers
back-pagination in the background *before* a timeline is opened, it's
possible the timeline observes pagination is running at the beginning,
and doesn't know if more back-pagination is required or not.

This wraps the `Paginator::status` stream by reading the
hit_timeline_start() from the room pagination and adding it to the
`Idle` state.
2024-05-24 11:21:34 +02:00
Benjamin Bouvier 0d4c5c7f49 paginator: add a method to retrieve the current pagination status
i.e. did we hit the timeline start/end, after paginating sufficiently?
2024-05-24 11:21:34 +02:00
Damir Jelić 2de2bc658c feat(sdk): Return the Curve25519Public key type instead of a string (#3450) 2024-05-24 11:17:56 +02:00
Damir Jelić 15c4ba2fb9 feat(crypto): Allow the explicit upload of the device keys (#3457) 2024-05-24 11:15:33 +02:00
Damir Jelić bb05b904cf feat(crypto): Allow the creation of an OlmMachine using a custom Account (#3451) 2024-05-24 10:54:21 +02:00
Damir Jelić 110d67cc62 feat(crypto): If available, sign the new device keys using the cross-signing keys (#3453) 2024-05-24 10:38:43 +02:00
Stefan Ceriu 03069f7acf fix: fix what permissions get_element_call_required_permissions returns and have them match what Element Call actually expects 2024-05-24 10:25:04 +02:00
Johannes Marbach f7aee0ee36 Update JS bindings link
Signed-off-by: Johannes Marbach <johannesm@element.io>
2024-05-23 19:54:47 +02:00
Ivan Enderlin e9dc02ae17 Merge pull request #3392 from Hywan/feat-sdk-linked-chunk-subscribe-as-vector 2024-05-23 19:02:45 +02:00
Benjamin Bouvier 11d66979d6 crypto(nit): remove unnecessary path qualifier in a test
The compiler has been pestering me with this, so here's a fix.
2024-05-23 18:52:14 +02:00
Richard van der Hoff 7e44fbca79 crypto: Wire up save_inbound_group_sessions to room_keys_received stream
https://github.com/matrix-org/matrix-rust-sdk/pull/3448 added a new method
`save_inbound_group_sessions` to `CrytoStore`, but forgot to wire it up to the
`room_keys_received` stream.
2024-05-23 17:08:58 +01:00
Richard van der Hoff dcc32da55a Merge pull request #3448 from matrix-org/rav/fix_backup_import
Crypto: fix backed-up keys being re-backed-up
2024-05-23 16:20:32 +01:00
Ivan Enderlin 52c0614199 doc(sdk): Fix tiny typos. 2024-05-23 16:54:23 +02:00
Richard van der Hoff 0777aa6ece crypto: Remove Olm::import_room_keys altogether 2024-05-23 15:53:21 +01:00
Richard van der Hoff 3945071446 crypto: Update changelog 2024-05-23 15:53:16 +01:00
Richard van der Hoff 05e4d7a502 ffi: Deprecate import_decrypted_room_keys
... and expose a new method `import_room_keys_from_backup` which does the right
thing.
2024-05-23 15:52:22 +01:00
Richard van der Hoff ba38d0c1de sdk: avoid deprecated BackupMachine::import_backed_up_room_keys
... and use its replacement, `Store::import_room_keys`
2024-05-23 15:52:22 +01:00
Richard van der Hoff f7cc17e1e0 crypto: Deprecate BackupMachine::import_backed_up_room_keys
This whole method is a bit broken. It assumes that, when we did an import from
backup, the backup that they came from is the same as the "current" version.

We *could* add another argument, but to be honest I find the whole method a bit
pointless. It's not particularly tied to the `BackupMachine` abstraction. Let's
instead expose `Store::import_room_keys` and
`ExportedRooomKey::from_backed_up_room_key` publicly, and tell callers to use
that directly.
2024-05-23 15:52:22 +01:00
Richard van der Hoff f77d2cd83f crypto: Use new CryptoStore::save_inbound_group_sessions method
When we are importing a batch of room keys, use the newly-added
`CryptoStore::save_inbound_group_sessions` method instead of
`CryptoStore::save_changes`.

To do this, we need to pass the backup version into `Store::import_room_keys`
instead of just `from_backup` flag.
2024-05-23 15:52:22 +01:00
Richard van der Hoff 4cd619ccdd crypto: New method CryptoStore::save_inbound_group_sessions
When we add a batch of inbound group sessions to the store, if they came from a
backup, we need to record which backup version they came from.
`CryptoStore::save_changes` doesn't give us an easy way to set the backup
version (we could add it to the `Changes` struct, but ugh).

So, we add a new method `save_inbound_group_sessions` which takes the backup
version as a separate param. This works fine, because whenever we import
sessions there are no other changes to store.

The new method isn't used outside of tests yet -- that comes in a follow-up
commit.
2024-05-23 15:52:22 +01:00
Richard van der Hoff 66aae2a0b5 crypto: Add a failing test demonstrating our problem 2024-05-23 15:52:22 +01:00
Damir Jelić 7fb57ea271 chore: Move the cross-process lock enabling into a separate method 2024-05-23 15:31:25 +02:00
Damir Jelić 4d9e41871e chore: The e2ee initialization tasks method doesn't need to return an error 2024-05-23 15:31:25 +02:00
Stefan Ceriu f672f17fcf feat(calls): add support for sending Matrix RTC call notifications 2024-05-23 15:07:13 +02:00
Ivan Enderlin 39a6a2eb8c chore(sdk): Rename Update::PushItems::position_hint to …::at. 2024-05-23 13:40:08 +02:00
Benjamin Bouvier 13ceb3e745 sdk: make use of the WeakClient in the encryption/ directory too 2024-05-23 12:20:17 +02:00
Benjamin Bouvier 6250677493 sdk: move the WeakRoom into room/mod.rs
This moves code around, and tweaks the behavior:

- `WeakRoom::get()` returns an `Option`, that will be None if the client
is missing or the room is missing.
- `PaginableRoom` for `WeakRoom` will now return a default response if
the room could not be reconstructed from the weak room. It's fine to do
so because we're in a shutdown context.
2024-05-23 12:20:17 +02:00
Benjamin Bouvier f1dcfd6332 event cache: keep a weak link to the room when paginating 2024-05-23 12:20:17 +02:00
Benjamin Bouvier c40b3e768e sdk: add failing test for the strong Client <-> EventCache cycle 2024-05-23 12:20:17 +02:00
Benjamin Bouvier 3eaac27789 sdk: introduce a WeakClient data structure 2024-05-23 12:20:17 +02:00
Benjamin Bouvier affb5d195e day dividers: remove all the trailing day dividers if needs be
multipletrailing
2024-05-23 12:19:07 +02:00
Benjamin Bouvier 7995c046b4 day dividers: tweak invariant
It's possible the timeline starts with a read marker, in which case the
first item won't be a day divider; in that case, check for the next
item, if present.

test_start_with_read_marker
2024-05-23 12:19:07 +02:00
Benjamin Bouvier 612ed3b603 day dividers: don't remove the final entry if it was already scheduled for removal 2024-05-23 12:19:07 +02:00
Ivan Enderlin ba1c797db4 chore(sdk): Rename Updates to ObservableUpdates. 2024-05-23 11:45:34 +02:00
Ivan Enderlin e3fdf19843 doc(sdk): Improve the documentation of AsVector. 2024-05-23 10:43:13 +02:00
Ivan Enderlin c1061150a9 chore(sdk): derive(Clone) is implemented if all parameters are Clone. 2024-05-23 10:06:18 +02:00
Ivan Enderlin c5f4168dd6 Merge pull request #3442 from matrix-org/rav/no_device_update_on_unchanged
crypto: emit no `identities_stream` items on no-op changes
2024-05-23 09:31:15 +02:00
Ivan Enderlin 7ea804bd2a Merge pull request #3443 from matrix-org/valere/apple_binding_update_podspec
Apple Bindings | Update SDKCrypto podspec files
2024-05-23 09:23:55 +02:00
Valere 748c3d514a Apple Bindings | Update SDKCrypto podspec files 2024-05-22 19:50:37 +02:00
Richard van der Hoff 818778cdf3 Add a test for invalid responses
This was untested before, and it seems the coverage gate is now sad.
2024-05-22 17:23:11 +01:00
Richard van der Hoff 2a605deaa5 changelog 2024-05-22 13:42:57 +01:00
Richard van der Hoff a154cd4640 crypto: emit no stream update for unchanged devices
There is no need to emit a notification from `identities_stream` when all
devices are unchanged since last time.
2024-05-22 13:42:57 +01:00
Richard van der Hoff eee0dc4e87 crypto: indicate when a device was updated
update `ReadOnlyDevice::update_device` to return a bool indicating whether
anything is changing.
2024-05-22 13:42:57 +01:00
Ivan Enderlin 28a8ce1732 chore(sdk): Make explicit a hidden lifetime. 2024-05-22 13:59:39 +02:00
Ivan Enderlin 3d3639a923 chore(sdk): Replace into_iter by iter. 2024-05-22 13:55:03 +02:00
Ivan Enderlin 57d454c557 chore(sdk): pin-project-lite is no longer required. 2024-05-22 13:54:00 +02:00
Ivan Enderlin 94fe6a9876 test(sdk): More tests for AsVector.
This patch adds another check to ensure `AsVector` generates
`VectorDiff`s that, once combined, produce an expected `Vector`. It
avoids errors when unit testing `VectorDiff` alone.
2024-05-22 13:28:29 +02:00
Ivan Enderlin c9086a436b fix(sdk): Ensure an underflow is not possible.
This patch ensures that an underflow is not possible when the length
of `chunks` is 0. In practise, it's not possible because there is
_always_ one chunk inside `LinkedChunk`, but it's better to have good
code habits.
2024-05-22 12:15:48 +02:00
Ivan Enderlin 87370abea4 chore(sdk): Remove a useless Option. 2024-05-22 12:15:00 +02:00
Ivan Enderlin fa554455b7 Merge pull request #3334 from matrix-org/valere/fix_crypto_binding_apple_script
crypto: Apple Crypto Bindings | Fix crypto xcframework script
2024-05-22 11:43:47 +02:00
Valere b430d95c0a Workflow | only add the needed target for script 2024-05-22 11:29:01 +02:00
Ivan Enderlin b4b2ee4716 chore(sdk): Move code inside the same module. 2024-05-22 10:24:36 +02:00
Ivan Enderlin ea6e15086e feat(sdk): as_vectors are no longer unsafe.
This patch removes the `unsafe` part of `as_vector`. The idea is to
pass `Iter` (the forward iterator of `Chunk`) to `AsVector` so that it
internally computes `initial_chunk_lengths`. The shape of this data must
no longer be guaranteed by the caller.

This patch goes a bit further: `UpdateToVectorDiff` has
a new constructor which consumes this `Iter` and builds
`initial_chunk_lengths` itself. Even better!

Finally, `Updates::as_vector` is removed. It's clearly no longer
necessary and it was creating borrowing issues anyway with the new code
structure.
2024-05-22 10:16:02 +02:00
Richard van der Hoff d7a887766c indexeddb: expose new method IndexeddbCryptoStore::open_with_key (#3423)
Allow applications to skip the PBKDF2 operation if they already have a cryptographically secure key,
instead using a simple HKDF to derive a key.

In order to maintain compatibility for existing element-web sessions, if we discover that we have an
existing store that was encrypted with a key derived from PBKDF2, then we reconstruct what
element-web used to do: specifically, we base64-encode the key to obtain the "passphrase" that
was previously passed in. If that matches, we know we've got the right key, and can update the
meta store accordingly.

Part of a resolution to element-hq/element-web#26821.

Signed-off-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-05-22 09:07:45 +01:00
Valere ee040aba60 split for do in several lines 2024-05-22 09:55:25 +02:00
Valere f7e06d0c20 Generate ffi files only using aarch64-apple-ios 2024-05-22 09:55:25 +02:00
Valere d2ecf461d0 fix script 2024-05-22 09:55:25 +02:00
Valere a1bbe9d810 reduce build time of CI check 2024-05-22 09:55:25 +02:00
Valere 20ada4014d fix target name 2024-05-22 09:55:24 +02:00
Valere 604190d3ca CI: Test CrypoFFI XCFramework generation 2024-05-22 09:55:24 +02:00
Valere 6ad26fff5c remove unneeded \ at end of script 2024-05-22 09:54:43 +02:00
Valere de989b2c51 fmt 2024-05-22 09:54:43 +02:00
Valere ea4737269f Fix crypto xcframework apple bindings 2024-05-22 09:54:43 +02:00
Ivan Enderlin bf39976d21 doc(sdk): Add a useful inline documentation. 2024-05-22 09:36:15 +02:00
Ivan Enderlin ad9c2acef1 chore(sdk): Rename ReattachItems and ReattachItemsDone.
This patch renames `ReattachItems` to `StartReattachItems` and
`ReattachItemsDone` to `EndReattachItems`. This naming conveys better
the idea of a _state transition_.
2024-05-22 09:33:36 +02:00
Ivan Enderlin 5ac5e04fb8 doc(sdk): Fix or add missing documentation. 2024-05-22 09:23:42 +02:00
Kegan Dougal 794b11a0ce ci: Add a step running complement crypto (#3400)
Add a CI step running complement crypto, automatically matching the complement-crypto branch name based on the current branch name, if needs be.

Signed-off-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com>
2024-05-21 16:34:39 +02:00
Benjamin Bouvier dba7cf39e6 day dividers: record the insert position when applying an operation based on the previous item
We could end up, like in the regression test, with a sequence of
operations like that:

- remove day divider @ i+1 (because it's redundant with one @ i)
- remove day divider @ i (because it's useless, since the event before
the day divider and after the day divider use the same date).

In that case, it would break the non-decreasing invariant: we'd apply an
operation on the array @ i+1, then @ i, which troubles the offset
computation.

Instead, when doing an operation based on the "prev_item" (now with a
small helper struct, to facilitate understanding of each field), we also
record the insertion order for the operation itself: it's always "at the
end of the operation order, at the time we're looking at it", so
equivalent to a "push_back" if there's no operation in between; but that
ensures that we'll do the operation in a non-decreasing order. For
instance in the above test case, the Remove(i) is now inserted before
the Remove(i+1), instead of after.
2024-05-21 12:45:59 +02:00
Benjamin Bouvier 1a872ac383 day dividers: only run the poisoning check for DayDividerAdjuster::drop if we're not already panicking 2024-05-21 12:45:59 +02:00
Andy Balaam 7bce12ca70 Merge pull request #3320 from matrix-org/andybalaam/fast-backup-reset-in-memorystore2
crypto: MemoryStore uses backup versions to track which sessions are backed up
2024-05-21 09:57:26 +01:00
Andy Balaam 2652b77a1b crypto: MemoryStore uses backup versions to track which sessions are backed up 2024-05-21 09:44:39 +01:00
Andy Balaam f310db44a3 crypto: Test for resetting backups by asking for new version 2024-05-21 09:43:40 +01:00
Benjamin Bouvier 5df53d7338 timeline queue refactoring: address review comments 2024-05-20 11:18:43 +02:00
Benjamin Bouvier 9575ee92d4 timeline queue: unify updating event send state into a single place 2024-05-20 11:18:43 +02:00
Benjamin Bouvier e49d62988b timeline queue: tiny refactorings
A few renamings here and there, making use of `as_variant!` a bit more,
adding a few comments,…
2024-05-20 11:18:43 +02:00
Benjamin Bouvier 8867a03c07 memory state store: correctly save user avatar url
With a regression test.

Fixes #3432.
2024-05-20 11:03:07 +02:00
Kévin Commaille 6c18bcf748 sdk: Improvements around generate_image_thumbnail (#3415)
* sdk: Return a Thumbnail from generate_image_thumbnail

We have already all the data for it.
Also fixes an error where the thumbnail format was assumed to always be
JPEG.

* sdk: Allow to select the format of the generated thumbnail

Sending an attachment could often fail if the image crate
cannot encode the thumbnail to the same format as the original.
This allows to select a known supported format to always
be able to generate a thumbnail.

* sdk: Do not return error of thumbnail generation for SendAttachment

Since the thumbnail is optional, failing to generate it should not
stop us from sending the attachment.

* Apply code review fixes
* sdk: Split attachment tests in separate file
* sdk: Add integration tests for generating thumbnails
* Revert wiremock debug log level

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-17 16:48:59 +02:00
Benjamin Bouvier 4cc67e9002 room list/notification services: don't request the room avatar since it's always provided
It's always provided in the `avatar` response in the field, which
sliding sync's using by default, so there's no need to request the
`m.room.avatar` event too.
2024-05-17 15:14:47 +02:00
Benjamin Bouvier bac0654a63 sliding sync: don't store the server-computed name in place of the raw name event
If we want to be able to note the absence of a room name, we shouldn't
take the name returned by the server (which may be computed, and thus
always be non-null). This way, we can expose both the name contained in
the m.room.name event as well as the computed name everywhere.

A consequence of this is that the room list service must now ask for the
m.room.name event as part of the required state for every room.
2024-05-17 15:14:47 +02:00
Valere 6111beded7 ffi: Expose ed25519/curve25519 keys in bindings (#3420) 2024-05-17 13:55:18 +02:00
Benjamin Bouvier b0558a002b day divider: don't recall a previous item if it's scheduled for deletion
With a regression test that didn't pass on main, and would incorrectly
remove the read marker, because it tried to replace the day divider at
(4), that was just scheduled for deletion in the previous loop
iteration.
2024-05-17 11:59:40 +02:00
Benjamin Bouvier c6f4ca09f5 day divider: simplify control flow in handle_event 2024-05-17 11:59:40 +02:00
Benjamin Bouvier 532f3a3ee8 day divider: add new invariant, a read marker should not disappear 2024-05-17 11:59:40 +02:00
Jorge Martín 3914e31461 ffi: Add the user's display name to RoomMembership timeline content
This is useful to properly format membership state events.
2024-05-17 10:45:48 +02:00
Damir Jelić a48d604b3f chore: Downgrade prost-derive, since the latest version was yanked 2024-05-17 10:40:31 +02:00
Benjamin Bouvier e486da4b08 cross-process lock: add a log line when aborting the previous renew task 2024-05-16 17:16:15 +02:00
Ivan Enderlin 67e5820643 feat(sdk): AsVectorSubscriber becomes AsVector.
This patch removes the possibility to have `LinkedChunk` as an
`ObservableVector` via a `Stream`, which was initially the goal of
`AsVectorSubscriber`. Having a `Stream` was more complex than required
for the integration inside `EventCache`. This patch keeps the same code
but renames `AsVectorSubscriber` into `AsVector`. A new internal type,
`UpdateToVectorDiff`, applies the algorithm itself and maintains its
own state, making it possible to re-introduce a `Stream` later if we
want to.
2024-05-16 16:57:01 +02:00
Benjamin Bouvier 7ae0bcecfd room preview: rejigger public API to pass a RoomOrAliasId in place of a RoomId to get_room_preview 2024-05-16 10:58:41 +02:00
Benjamin Bouvier 1fd29f7b6d room preview: allow passing through a list of servers to discover a room with MSC3266
Fixes #3395.`
2024-05-16 10:58:41 +02:00
Benjamin Bouvier c8f6fe4f6d event cache/timeline: reuse the Paginator when running back-paginations (#3373)
* event cache: reuse the paginator internally

Fixes #3355.

* event cache: move the `pagination_token_notifier` into the `RoomPaginationData` as well

* event cache: introduce a `RoomPagination` API object and move code around

Only code motion. No changes in functionality.

* event cache: remove "paginate" (et al.) in `RoomPagination` method names

No changes in functionality, just renamings.

* event_cache/timeline: have the event cache handle restarting a back-pagination that failed under our feet

When a timeline reset happens while we're back-paginating, the event
cache method to run back pagination would return an success result
indicating that the pagination token disappeared. After thinking about
it, it's not the best API in the world; ideally, the backpagination
mechanism would restart automatically.

Now, this was handled in the timeline before, and the reason it was
handled there was because it was possible to back-paginate and ask for a
certain number of events. I've removed that feature, so that
back-pagination on a live timeline matches the capabilities of a
focused-timeline back-pagination: one can only ask for a given number of
*events*, not timeline items.

As a matter of fact, this simplifies the code a lot by removing many
data structures, that were also exposed (and unused, since recent
changes) in the FFI layer.

* Address review comments
2024-05-16 10:22:05 +02:00
Damir Jelić 21804ab313 chore(crypto): Add a missing changelog entry 2024-05-16 09:33:12 +02:00
Ivan Enderlin 14252807a2 Merge pull request #3418 from zecakeh/attachment-mentions
sdk: Allow to send mentions with attachments
2024-05-16 09:06:01 +02:00
Ivan Enderlin d1fad85c5f chore(sdk): Make Clippy happy. 2024-05-16 08:47:42 +02:00
Ivan Enderlin 38932e7e03 doc(sdk): Add moooooar documentation.
This patch adds documentation that explains how `AsVectorSubscriber`
works.
2024-05-15 22:16:18 +02:00
Ivan Enderlin 397a970636 doc+test(sdk): Add documentation for AsVectorSubscriber + improve tests.
This patch add more documentation for `AsVectorSubscriber` and improve
the tests.
2024-05-15 22:16:18 +02:00
Ivan Enderlin 8d2db9d35a chore(sdk): assert_items_eq is shared amongst several test modules.
This patch moves the `assert_items_eq` macro in a place where it's
accessible to several test modules. This is going to be useful for next
patches.
2024-05-15 22:16:18 +02:00
Ivan Enderlin 8ee0bbb22e feat(sdk): Add Update::ReattachItems and Update::ReattachItemsDone.
This patch adds 2 new updates: `ReattachItems` and `ReattachItemsDone`.
It's useful to optimise insertion, esp. in `AsVectorSubscriber` but
almost maybe in database.
2024-05-15 22:16:18 +02:00
Ivan Enderlin 672bb0229b feat(sdk): Rename Update::TruncateItems to Update::DetachLastItems.
This patch renames `TruncateItems` to `DetachLastItems`. It's
technically the same things, but the fields are different: `chunk:
ChunkIdentifier` and `length: usize` become a unique `at: Position`. The
spirit is the same but the semantics is a bit different.

This patch, with this renaming, also prepares the next commits.
2024-05-15 22:09:01 +02:00
Ivan Enderlin b894b700c7 feat(sdk): Rename Update::InsertItems to Update::PushItems.
First off, this patch renames `Update::InsertItems` to
`Update::PushItems` because all items insertions happen at the end of
a chunk. Let's reflect that. `InsertItems` would have meant that it's
possible to insert at any position, but it's not what happens by design.

Second, this patch renames `Update::PushItems::at` to
`Update::PushItems::position_hint`. Indeed, the `at` field would have
meant that the position is specific whilst it's not. It's just a hint to
make the life of update readers simpler. It's also a good way to improve
performance in some cases: since items are pushed, to know the new
position of the items, one has to read the position of the previous last
item and compute the new position from there. Having `position_hint`
help to prevent this dance and save the cost of a reading. That's also
why the field has been renamed to `position_hint`, it's a hint.
2024-05-15 22:08:54 +02:00
Ivan Enderlin 9af57c7a60 feat(sdk) AsVectorSubscriber::new now receives the initial chunk lengths.
This patch updates the constructor of `AsVectorSubscriber` to receive
the initial chunk lengths so that this type doesn't have to guess what
to do when receiving an unknown chunk identifier. `AsVectorSubscriber`
is de facto synchronized with its source `LinkedChunk` when created.
2024-05-15 22:08:27 +02:00
Ivan Enderlin 725d7bbc97 !tmp First draft of the subscribe_as_vector API. 2024-05-15 22:05:52 +02:00
Ivan Enderlin 2d10ea4dc9 chore(sdk): Use pin-project-lite in matrix-sdk.
This patch moves the `pin-project-lite` dependency from `matrix-sdk-ui`
to the workspace, and uses it in `matrix-sdk`.
2024-05-15 22:05:52 +02:00
Kévin Commaille e50b574f12 sdk: Allow to send mentions with attachments
Particularly useful for captions

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-15 16:40:06 +02:00
Damir Jelić 35173347fd chore: Bump the vodozemac version
This gives us the necessary primitives for the QR code login feature.
2024-05-15 15:01:53 +02:00
Ivan Enderlin 64c5a83e33 Merge pull request #3413 from zecakeh/send-attachment-path
ui: Take a `PathBuf` to send an attachment
2024-05-15 13:24:09 +02:00
Kévin Commaille 8da3922ff0 sdk: Rename SendAttachment's url field to filename
It is name filename before and after, and it is not a url.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-15 13:00:49 +02:00
Kévin Commaille eba5efc829 Merge branch 'main' into send-attachment-path
Signed-off-by: Kévin Commaille <76261501+zecakeh@users.noreply.github.com>
2024-05-15 10:57:08 +02:00
Ivan Enderlin 7ed6db9758 Merge pull request #3406 from zecakeh/edit-without-relation
ui: Make Timeline::edit take a RoomMessageEventContentWithoutRelation
2024-05-15 10:55:21 +02:00
Kévin Commaille 32c7a1ab78 ui: Take a PathBuf to send an attachment
I found the previous API confusing. Firstly, the parameter name was "filename" when we want a file path.
Secondly, we take a `String` when we want a `Path`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-15 10:50:26 +02:00
Damir Jelić cbb92cacce feat(crypto): Add support to import/export the newly added secrets bundle 2024-05-14 11:46:56 +02:00
Damir Jelić 402d620608 feat(crypto): Add a SecretsBundle type 2024-05-14 11:46:56 +02:00
Doug b57dbd8d1a xtask: Allow passing multiple targets to Swift's build-framework.
* Fix CI
2024-05-14 11:33:13 +02:00
Damir Jelić aa6bbc1a0f feat(crypto): Add data types to parse QR codes from MSC4108 2024-05-14 10:47:22 +02:00
Damir Jelić 6672302684 chore: Make the url crate a workspace dependency 2024-05-14 10:47:22 +02:00
Kévin Commaille 9e4125bb39 ui: Make Timeline::edit take a RoomMessageEventContentWithoutRelation
Like send_reply.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-13 20:25:44 +02:00
Kévin Commaille cb452802bb chore: Upgrade ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-13 20:25:43 +02:00
Benjamin Bouvier da249bbc51 crypto: remove the fake crypto store test for olm session wedging
Two reviewers were suspicious the test wasn't very useful, so I propose
to hereby remove it.
2024-05-13 18:30:25 +02:00
Benjamin Bouvier c16bfd3234 timeline: reuse internal id when adding pending replied-to details to an item
Fixes #3379.
2024-05-13 18:29:34 +02:00
Benjamin Bouvier 83427b325c ci: use the latest synapse-service image with msc3266
And re-enable the room preview test there.
2024-05-13 18:11:07 +02:00
Andy Balaam b99e103704 Merge pull request #3409 from matrix-org/andybalaam/disable-flaking-nse-test
Ignore a flaking test
2024-05-13 16:55:17 +01:00
Andy Balaam 6d6f470e11 Ignore a flaking test 2024-05-13 16:42:22 +01:00
Benjamin Bouvier 94d3758a0d base: sort the heroes in the computed display name alphabetically 2024-05-13 15:11:57 +02:00
Andy Balaam 4b1d03f229 Merge pull request #3343 from matrix-org/andybalaam/test-store-cache-drop
crypto: Add a standalone integration test for the NSE race
2024-05-13 14:09:40 +01:00
Andy Balaam 06825c6385 crypto: Test for the NSE race bug #3110
Adds a test for https://github.com/matrix-org/matrix-rust-sdk/issues/3110
that fails before the fix and passes afterwards.
2024-05-13 13:55:04 +01:00
Ivan Enderlin 2987bd1f6d chore(cargo) Update dependencies
chore(cargo) Update dependencies
2024-05-13 14:32:05 +02:00
Ivan Enderlin 099cd8d6a0 chore(sdk): Remove the need for cfg_vis.
It introduces more dependencies for —apparently— no useful need here.
2024-05-13 14:17:45 +02:00
Ivan Enderlin f669b3a530 chore(test): Use workspace dependencies. 2024-05-13 14:16:36 +02:00
Ivan Enderlin da8588787f chore(cargo): Declare reqwest as a workspace dependency. 2024-05-13 14:15:35 +02:00
Ivan Enderlin e21e412aae chore(cargo): Update dependencies. 2024-05-13 14:12:32 +02:00
Andy Balaam 749ed2c3e0 crypto: Allow duplicating a Client in tests 2024-05-13 12:17:11 +01:00
Benjamin Bouvier e709cca2f5 test utils: prefer From impl to Into impl
Thanks clippy, i guess?
2024-05-13 12:36:44 +02:00
Benjamin Bouvier f661b0d728 event cache: add a regression test for ignoring/unignoring at the event cache level 2024-05-13 12:36:44 +02:00
Benjamin Bouvier cef4409dda timeline: make use of the EventFactory in more tests 2024-05-13 12:36:44 +02:00
Benjamin Bouvier ac6fa7abb1 event cache: move reacting to (un)blocks in the event cache 2024-05-13 12:36:44 +02:00
Damir Jelić 9ef465fdf4 chore: Fix the formatting 2024-05-13 12:22:35 +02:00
Denis Kasak 11de0449fa Merge pull request from GHSA-9ggc-845v-gcgv
Avoid incorrect usage of private backup key
2024-05-13 10:07:10 +00:00
Ivan Enderlin 964553952d Merge pull request #3399 from matrix-org/stefan/invitesListCleanup
feat(ui): enable `room-list-with-unified-invites` by default, remove …
2024-05-13 11:51:10 +02:00
Ivan Enderlin 5c52014c4b Merge pull request #3386 from matrix-org/stefan/joinedRoomListFilter
feat(ui): add room list filter for excluding non-joined rooms
2024-05-13 11:24:13 +02:00
Stefan Ceriu 85d09c3c56 feat(ui): enable room-list-with-unified-invites by default, remove old invites sliding sync list. 2024-05-13 11:55:38 +03:00
Ivan Enderlin c0924c87af feat(sdk): Introduce linked_chunk::Updates
feat(sdk): Introduce `linked_chunk::Updates`
2024-05-12 17:42:29 +02:00
Kévin Commaille 733665ddcc ui: Expose message mentions
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-11 10:53:52 +02:00
Andy Balaam 2c57557a3a ci: Quieten hyper logs 2024-05-10 17:24:30 +01:00
Andy Balaam d5df58496b Merge pull request #3396 from zecakeh/ci-nightly
ci: Bump version of rust nightly
2024-05-10 16:27:14 +01:00
Valere df5e8e724e Review: better comments
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Valere <bill.carson@valrsoft.com>
2024-05-10 15:49:20 +02:00
Valere a7cc3777d1 Review: better comment
Co-authored-by: Denis Kasak <dkasak@termina.org.uk>
Signed-off-by: Valere <bill.carson@valrsoft.com>
2024-05-10 15:49:02 +02:00
Kévin Commaille 4724115e8d chore: Fix docs warning
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-10 12:16:42 +02:00
Kévin Commaille e355a6aa39 ci: Bump version of rust nightly
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-10 11:35:19 +02:00
Kévin Commaille 99ae06be68 chore: Fix cfg options
Cargo nightly now checks whether a cfg option exists.
We need to declare the custom
options we use
and fix those that are wrong.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-10 11:34:59 +02:00
Kévin Commaille 787b2d31cd chore: Fix warnings during compilation
cargo says that default_features will not be supported anymore in 2024 edition

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-05-10 11:32:05 +02:00
Doug 23f17614e5 ffi: Allow joining a room ID with server names. 2024-05-09 19:32:16 +02:00
Doug eebbb74be7 ffi: Include servers when resolving a room alias.
Rename result.
2024-05-09 19:32:16 +02:00
Damir Jelić fac5ba5bae chore(crypto): Refactor the PK signing subkey constructors
This makes the public key fields private to ensure that we don't
accidentally swap them out. It also moves the construction of the
subkeys into the master key type.
2024-05-09 18:03:55 +02:00
Damir Jelić c57d2c68a1 fixup! chore(crypto): Refactor the cross-signing key wrappers 2024-05-09 17:41:32 +02:00
Damir Jelić 6b1ef484f2 chore(crypto): Refactor the cross-signing key wrappers
Since the master/self-signing/user-signing public key types are used for
public user identities as well as for the private key type we have, and
we'd like to sign the public key types it makes sense that the types
itself aren't using an Arc.

Let's instead put the Arc inside the user identity structs.

This will allow us later on to more easily sign the public key types.
2024-05-09 17:41:32 +02:00
Damir Jelić aeb85ba836 chore: Add a better debug implementation for the backup recover/decryption key 2024-05-09 17:40:51 +02:00
Damir Jelić afa3808752 chore: Use the released version of Ruma 2024-05-09 17:34:16 +02:00
Ivan Enderlin 1f26822a64 chore(sdk): Split linked_chunk into 2 modules. 2024-05-08 14:47:45 +02:00
Ivan Enderlin 44a78ba9f6 chore(sdk): Thanks Clippy. 2024-05-08 13:58:49 +02:00
Ivan Enderlin 1493dc2c6a chore(sdk): Remove the LinkedChunk prefix from type names. 2024-05-08 13:45:45 +02:00
Ivan Enderlin 9531a4041e feat(sdk): Allow LinkedChunkUpdatesInner to have multiple readers.
This patch removes the notion of `take` vs. `peek` from
`LinkedChunkUpdatesInner` and widens the problem to a more general
approach: `LinkedChunkUpdatesInner` must support multiple readers, not
only two (`take` was the first reader, `peek` was the second reader,
kind of).

Why do we need multiple readers? `LinkedChunkUpdates::take` is
clearly the first reader, it's part of the public API. But the private
API needs to read the updates too, without consuming them, like
`LinkedChunkUpdatesSubscriber`. `peek` was nice for that, but it's
possible to have multiple `LinkedChunkUpdatesSubscriber` at the same
time! Hence the need to widen the approach from 2 readers to many
readers.

This patch introduces a `ReaderToken` to identify readers. The last
indexes are now all stored in a `HashMap<ReaderToken, usize>`. The rest
of the modifications are the consequence of that.

The test `test_updates_take_and_peek` has been entirely rewritten to be
`test_updates_take_and_garbage_collector` where it tests 2 readers and
see how the garbage collector reacts to that.
2024-05-08 13:45:45 +02:00
Stefan Ceriu 1ce27d6eec feat(ui): add room list filter for excluding non-joined rooms 2024-05-08 12:48:14 +03:00
Ivan Enderlin 73ae1cc6da feat(sdk): Add LinkedChunkUpdatesSubscriber.
This patch implements `LinkedChunkUpdates::subscribe` and
`LinkedChunkUpdateSubscriber`, which itself implements `Stream`.

This patch splits `LinkedChunkUpdates` into `LinkedChunkUpdatesInner`,
so that the latter can be shared with `LinkedChunkUpdatesSubscriber`.
2024-05-08 09:23:20 +02:00
Michael Hollister 74b79d8212 ffi: Added dehydrated flag to Device
Signed-off-by: Michael Hollister <michael@futo.org>
2024-05-07 22:28:18 +02:00
Valere d0776819c7 Review: Add assert error message
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Valere <bill.carson@valrsoft.com>
2024-05-07 14:43:15 +02:00
Valere 4e9edcb0db Review: better comment
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
Signed-off-by: Valere <bill.carson@valrsoft.com>
2024-05-07 14:42:52 +02:00
Valere e3dc094be4 Add minimal reproducing test 2024-05-07 10:20:19 +02:00
Valere f4772c9b0e Add FIXME comment 2024-05-07 10:20:08 +02:00
Ivan Enderlin 76210686c4 feat(sdk): Implement Clone on LinkedChunkUpdate.
`LinkedChunkUpdate` implements `Clone` if and only if `Item` and `Gap`
both implement `Clone`.
2024-05-06 22:22:32 +02:00
Ivan Enderlin 3a00271af0 chore(sdk): Hmmmm. 2024-05-06 20:15:35 +02:00
Ivan Enderlin d6915793c1 feat(sdk) Add the LinkedChunkUpdates::peek.
This patch adds the `LinkedChunkUpdates::peek` method. This is
a new channel to read the updates without consuming them, like
`LinkedChunkUpdates::take` does.

The complexity is: when do we clear/drop the updates then? We don't
want to keep them in memory forever. Initially `take` was clearing
the updates, but now that we can read them with `peek` too, who's
responsible to clear them? Enter `garbage_collect`. First off,
we already need to maintain 2 index, resp. `last_taken_index` and
`last_peeked_index` so that `take` and `peek` don't return already
returned updates. They respectively know the index of the last update
that has been read. We can use this information to know which updates
must be garbage collected: that's all updates below the two index.
Tadaa. Simple. The only _drawback_ (if it can be considered as such)
is that the garbage collection happens on the next call to `take` or
`peek` (because of the borrow checker). That's not really a big deal in
practise. We could make it happens immediately when calling `take` or
`peek` but it needs more pointer arithmetic and a less straighforward
code.
2024-05-06 20:13:42 +02:00
Damir Jelić 121dedee4e chore: Use a released version of vodozemac 2024-05-06 15:24:21 +02:00
Damir Jelić 8eebb9bb39 chore: Use a released version of uniffi (#3382)
The commit we were using has been part of the 0.27.1 release, so let's
use it:

https://github.com/mozilla/uniffi-rs/commit/789a9023b522562a95618443cee5a0d4f111c4c7
2024-05-06 14:32:47 +02:00
Ivan Enderlin 9e2e28d57a feat(sdk) Change the update history from a Vec<T> to a new LinkedChunkUpdates type.
This patch updates the `LinkedChunk::update_history` field from
a simple `Vec<LinkedChunkUpdate<Item, Gap>>` type to the new
`LinkedChunkUpdates<Item, Gap>` type (note the plural).

This is going to be helpul for the next patches.
2024-05-06 14:22:51 +02:00
Ivan Enderlin 2255cd5a43 chore(ffi): Remove support for opentelemetry
chore(ffi): Remove support for opentelemetry
2024-05-03 20:17:50 +02:00
Ivan Enderlin 12a231675c chore(ffi): Remove support for opentelemetry.
This patch removes support for OpenTelemetry because it's not used
anymore by anybody. It also adds multiple duplicated dependencies (like
`reqwest`). Anyway. Farewell.
2024-05-03 19:52:17 +02:00
Denis Kasak 60017241b2 Tweak log messages when rejecting devices (DeviceKeys structs) received from the server.
Preferring to use "reject" wording rather than "failed to
create/update". The latter can be easily misinterpreted as a failure of
the local client to create an entirely new device from scratch, rather
than refusal to instantiate a new local device representation of an
(invalid) device definition received from the server.
2024-05-03 14:08:36 +02:00
Denis Kasak 03fe9feb69 docs: Expand docs for the DeviceKeys struct. 2024-05-03 14:08:36 +02:00
Damir Jelić 6f2d8e0e50 chore: Fix some clippy warnings 2024-05-02 17:12:19 +02:00
Damir Jelić 56aa86da8b chore: Depend on a released version of mas-oidc-client 2024-05-02 17:12:19 +02:00
Ivan Enderlin 67e2842f84 feat(sdk): Introduce LinkedChunkUpdate
feat(sdk): Introduce `LinkedChunkUpdate`
2024-05-02 14:46:19 +02:00
Ivan Enderlin 443647a1ba feat(sdk): Make update history of LinkedChunk optional.
This patch makes the `LinkedChunk::update_history` field optional,
so that it doesn't require the user to drain it to avoid eating the
universe.

The `new` constructor disabled the update history, the
`new_with_update_history` enables it.
2024-05-02 14:30:19 +02:00
Ivan Enderlin b9ea6ff300 chore(sdk): Rename LinkedChunkLinks to LinkedChunkEnds. 2024-05-02 14:30:19 +02:00
Ivan Enderlin c219c727bb feat(sdk) Remove LinkedChunkListener.
This patch is a turn around about the `LinkedChunkListener`. Many
patches have been removed because `LinkedChunkListener` needed to
support I/O, so errors and async code. The whole code was affected by
that, resulting in a complex API. The idea of this patch is to decoupled
this. Here is how.

First off, `LinkedChunkListener` is removed. So it's one less generic
parameter on `LinkedChunk`. It's also one less trait, so less
implementations.

Second, now `LinkedChunk` accumulates/collects all “updates” under the
form of a new enum `LinkedChunkUpdate`. These updates can be read with
`LinkedChunk::updates(&mut self) -> &Vec<LinkedChunkUpdate>`. The reader
can simply read them, or even drain them. The reader is responsible
to handle these updates and to dispatch them in a storage or whatever.
`LinkedChunk` is no longer responsible to do that, removing the need to
support errors and to be async.

Third, the simplification has led to an optimisation by introducing a
new type `LinkedChunkLinks`. The documentation explains what it does
and why it was needed. The benefit of this type is: it doesn't increase
the size of `LinkedChunk`, but it simplifies the code: no more `Arc`,
no more `Mutex` (it was required because with I/O and async), no more
borrow checker trick, and the code stays as safe as before.
2024-05-02 14:30:19 +02:00
Ivan Enderlin db0f9b19be feat(sdk) LinkedChunkError is a real error now. 2024-05-02 14:30:19 +02:00
Ivan Enderlin 343416653d feat(sdk): Create LinkedChunkListener.
This patch creates the `LinkedChunkListener` trait.

This patch also updates `LinkedChunk` to be able to use a
`LinkedChunkListener`.
2024-05-02 14:30:19 +02:00
Ivan Enderlin 17b3cb6b31 fix(store-encryption): Remove the displaydoc dependency
fix(store-encryption): Remove the `displaydoc` dependency
2024-05-02 09:41:43 +02:00
Ivan Enderlin f8a6f90664 Merge pull request #3371 from matrix-org/bnjbvr/ansi-pedantic-sync-builder
tests: rename ev_builder to sync_builder + add test_ prefix to test functions
2024-05-02 09:36:00 +02:00
Ivan Enderlin 8ac51c19f5 Merge pull request #3369 from matrix-org/bnjbvr/get-rid-of-notificationclientbuilder
notification client: get rid of builder
2024-05-02 09:34:32 +02:00
Ivan Enderlin ad2e8336f6 fix(store-encryption): Remove the displaydoc dep.
This patch removes the `displaydoc` dependency. Why?

1. It creates a warning in rustc nightly:

```
warning: non-local `impl` definition, they should be avoided as they go against expectation
  --> crates/matrix-sdk-store-encryption/src/lib.rs:49:17
   |
49 | #[derive(Debug, Display, thiserror::Error)]
   |                 ^^^^^^^
   |
   = help: move this `impl` block outside the of the current constant `_DERIVE_Display_FOR_Error`
   = note: an `impl` definition is non-local if it is nested inside an item and may impact type checking outside of that item. This can be the case if neither the trait or the self type are at the same nesting level as the `impl`
   = note: one exception to the rule are anon-const (`const _: () = { ... }`) at top-level module and anon-const at the same nesting as the trait or type
   = note: this lint may become deny-by-default in the edition 2024 and higher, see the tracking issue <https://github.com/rust-lang/rust/issues/120363>
   = note: the derive macro `Display` may come from an old version of the `displaydoc` crate, try updating your dependency with `cargo update -p displaydoc`
   = note: `#[warn(non_local_definitions)]` on by default
   = note: this warning originates in the derive macro `Display` (in Nightly builds, run with -Z macro-backtrace for more info)
```

2. `thiserror` is already used, which seems to provide a similar issue.
3. That's less dependency, and less proc-macro, which will improve the
   compilation time in general.
2024-05-02 09:27:17 +02:00
Benjamin Bouvier a3f6e0fb5a ffi: add back a raw_name() Room method and RoomInfo field for the name defined in the raw state event
And rename "name" to "display_name" everywhere, duh.
2024-05-01 14:32:38 +02:00
Benjamin Bouvier f997256c73 ffi: revert a few methods back to sync
And sprinkle useful comments here and there.
2024-05-01 14:32:38 +02:00
Benjamin Bouvier e56d092b4a ffi: simplify RoomInfo::new() by getting the room avatar url internally
Before, it was computed externally and passed as a parameter.
2024-05-01 14:32:38 +02:00
Benjamin Bouvier dedfc2649a ffi: get rid of name(), and use the computed_display_name() everywhere
This should make it more regular, in all the places, to use the same
string:
- Room
- RoomListItem
- RoomInfo
2024-05-01 14:32:38 +02:00
Benjamin Bouvier 90bed18415 ffi: make the name method sync again
Also:

- rename `display_name` to `computed_display_name` in several places,
and reflect that change into a few callers
- simplify slightly the `computed_display_name()` method
2024-05-01 14:32:38 +02:00
Benjamin Bouvier a3061eb39a ffi: make RoomListItem::is_direct sync again
And comment why some methods it's calling are async under the hood.
2024-05-01 14:32:38 +02:00
Benjamin Bouvier f69db1d169 notification client(bugfix): don't filter out the notification if we couldn't compute push actions with /context
This is in line with what the other method using sliding sync does. This
wasn't tested before, because this required `filter_by_push_rules()` to
be enabled in the notification client; now that it's the default, the
test revealed the bug, and so it could be fixed.
2024-05-01 13:13:14 +02:00
Benjamin Bouvier 0ba4e42161 notification client: get rid of builder
The builder had only one meaningful method, `filter_by_push_rules`,
which was always called by the applications — and in fact should always
be true. It was designed as an extra method because it was experimental
at the time, but it's stabilized sufficiently that we can enable this
behavior by default now, considering that a notification that is not
wanted by the user shouldn't be kept, to respect their intent. (This is
in the UI crate, which is opinionated, so it's fine to assume such
intents by design.)
2024-05-01 13:13:14 +02:00
Benjamin Bouvier 7cf36ee9f6 tests: rename ev_builder to sync_builder + add test_ prefix to test functions 2024-05-01 12:37:55 +02:00
Benjamin Bouvier ff40ef0176 ffi: replace some block_on asyncs by async() functions 2024-05-01 11:11:04 +02:00
Benjamin Bouvier d02125ba21 ffi: simplify notification settings locks
This gets rid of a few calls to `RUNTIME.block_on`.
2024-05-01 11:11:04 +02:00
Ivan Enderlin 76200c1007 Merge pull request #3367 from zecakeh/upgrade-crates
chore: Upgrade dependencies
2024-05-01 09:39:28 +02:00
Kévin Commaille b4c3ab38b8 chore: Upgrade more crates
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 20:05:34 +02:00
Kévin Commaille f4b0ebdb95 ui: Do not enable matrix_sdk experimental-oidc feature
The feature should have been removed when the authentication
module was moved.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 19:01:00 +02:00
Kévin Commaille 6488be671e Fix check of nonce length
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 18:44:47 +02:00
Kévin Commaille 5eaf10e9f8 chore: Upgrade base64 crate
This matches the version used in ruma and vodozemac

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 17:46:59 +02:00
Benjamin Bouvier 177e31cf9a timeline: reset pagination status if a live back-pagination is aborted 2024-04-30 16:10:30 +02:00
Kévin Commaille dc2b9ed89c ci: Upgrade most actions (#3364)
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 15:04:54 +02:00
Damir Jelić fb9982fb48 oidc: Use the correct types to compare the status codes in the oidc example (#3363) 2024-04-30 13:02:29 +00:00
Kévin Commaille 856dd01009 Upgrade http, ruma, reqwest and wiremock dependencies (#3362)
They need to be updated together
because the latters depend on the former.

matrix-authentication-service is still using http 0.2
so we need to add a conversion layer between both major versions
for OIDC requests.

We need to update vodozemac too because of a dependency resolution issue.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 14:04:56 +02:00
Kévin Commaille ea1a01000f sdk: Use the GET /auth_issuer endpoint for OIDC
The well-known method is deprecated.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-30 12:33:31 +02:00
Benjamin Bouvier 0ebedfe286 clippy: disable the box_default lint, take 2 2024-04-30 12:04:29 +02:00
Benjamin Bouvier e0b4e2c35d fixup! paginator: reset the paginator's state if the task is cancelled 2024-04-30 12:04:29 +02:00
Benjamin Bouvier 1f2459478f paginator: reset the paginator's state if the task is cancelled
This makes all the requests (/context and /messages) cancellation-safe
by making two changes:

- first, use sync locks instead of async locks for the prev/next batch
tokens. This ensures that we can't get cancelled after receiving a
response and managing internal state, i.e. we keep run-to-completion
semantics until the end of the function, once we got a response.
- second, introduce a RAII guard that will reset the state to a given
value when dropped. Then use this to reset the state to Initial (resp.
Idle) in /context (resp. /messages) when the async call is aborted. We
use `mem::forget` once the response has been returned, so as to not call
the `Drop` implementation of the guard later on.

A regression test has also been introduced.
2024-04-30 12:04:29 +02:00
Ivan Enderlin 0c0342b994 doc(sdk): Add doc for assert_items_eq!. 2024-04-29 15:08:34 +02:00
Ivan Enderlin 3e261188d3 chore(sdk): Move CHUNK_CAPACITY as first generics of LinkedChunk.
This patch changes the signature of `LinkedChunk<Item, Gap, const
CHUNK_CAPACITY = usize>` to `LinkedChunk<const CHUNK_CAPACITY = usize,
Item, Gap>`. It allows to add more generic parameters if needed, without
conflicting with generic constants.
2024-04-29 15:08:34 +02:00
Benjamin Bouvier 16eb449c6b clippy: disable the box_default lint
The clippy website explains that `Box::new(Default::default())` can be
shortened to `Box::default()` and that it's more readable. That's true…
when you don't have a generic parameter in the mix (which may be
required if rustc can't infer the type of the boxee), in which case it
just looks bad, e.g. `Box::<MyType>::default()` instead of
`Box::new(MyType::default())`.

I do strongly prefer the latter, and propose to get rid of the lint, as
a result.
2024-04-29 14:32:08 +02:00
Doug 4618d7ca7d ffi: Allow creation of a matrix.to link for any room alias. 2024-04-29 12:00:36 +02:00
Ivan Enderlin 5e2a2465c9 Merge pull request #3357 from zecakeh/use-axum
sdk: Replace hyper with axum for local servers
2024-04-29 09:35:33 +02:00
Kévin Commaille 0e0a406cb1 Replace hyper with axum for oidc_cli example
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-27 19:37:46 +02:00
Kévin Commaille 04e5643422 Lock axum to 0.7.4
Version 0.7.5 triggers a warning in
our version of rust nightly in CI,
but we can't update it to a recent
version because it triggers a warning
caused by displaydoc

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-27 18:34:37 +02:00
Kévin Commaille d2bb17acd5 sdk: Replace hyper with axum for SSO login
hyper::Server was dropped in hyper 1, without a replacement.
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-04-27 17:12:10 +02:00
Ivan Enderlin dddc607e07 timeline: add event focus mode for permalinks
timeline: add event focus mode for permalinks
2024-04-25 16:23:28 +02:00
Ivan Enderlin 25f893b0bb Merge branch 'main' into bnjbvr/permalink-mvp 2024-04-25 16:02:23 +02:00
Andy Balaam 415617080a Merge pull request #3337 from matrix-org/andybalaam/utd-type-info
crypto: UtdCause enum in reporting hooks and encryption event
2024-04-25 08:55:50 +01:00
Andy Balaam 2c7afc201f Merge pull request #3326 from matrix-org/andybalaam/allow-setting-encryption-settings
ffi: Expose encryption settings via FFI
2024-04-25 08:55:43 +01:00
Andy Balaam 89abb75d4d crypto: Include UTD cause in FFI EncryptedMessage 2024-04-24 12:16:17 +01:00
Andy Balaam fd5666f55f crypto: Support unstable prefix for MSC4115 2024-04-24 08:31:31 +01:00
Benjamin Bouvier 397a26e00b ffi: add bindings for the timeline focus mode and associated functions 2024-04-23 19:02:59 +02:00
Benjamin Bouvier a6c42404a6 tests: add integration tests for the new timeline focus mode 2024-04-23 19:02:45 +02:00
Benjamin Bouvier 7856dab56d timeline: add support for a focus mode in the timeline
This introduces the `TimelineFocus`, a new enum to declare if the
timeline is "live" aka looking at events from sync and displaying them
as they come in, or focused on an event (e.g. after clicking a
permalink).

When in the second mode, the timeline can paginate forwards and
backwards, without interacting with the event cache (as this would
require some complicated reconciliation of known events with events
received from pagination, with no guarantee that those events are event
connected in whatever way).

An event-focused timeline will also show edits/reactions/redactions in
real-time (as the events are received from the sync), but will not show
new timeline items, be they for local echoes or events received from the
sync.
2024-04-23 19:02:27 +02:00
Valere 149606e148 Avoid incorrect usage of private backup key
This fixes instances of key backup corruption and prevents inadvertently logging the private backup key to the logs.
2024-04-23 17:46:53 +02:00
Andy Balaam ebcf1c434c ffi: Expose encryption_settings via FFI 2024-04-23 15:45:29 +01:00
Andy Balaam b5e2eb6831 crypto: Add a UtdCause and code to determine it for membership 2024-04-23 15:44:39 +01:00
Andy Balaam 9d20a02a12 build: Ensure uniffi feature is properly passed on where needed 2024-04-23 15:26:51 +01:00
Benjamin Bouvier 8f5e1f3dfc timeline: include the remote event's origin in the timeline position/end
and don't assume inserting to the end means it's coming from sync — as
it won't be true for forward pagination anymore.
2024-04-23 13:45:01 +02:00
Benjamin Bouvier d9864373f3 timeline: add the ability to set a prefix for internal IDs 2024-04-23 13:19:32 +02:00
Benjamin Bouvier e4331ac9b8 timeline: use a string instead of a u64 to identify timeline items
This will allow to define a prefix later, to distinguish detached
timelines from live timelines.
2024-04-23 13:19:32 +02:00
Benjamin Bouvier 9547e3cee6 timeline: move populate_initial_user_receipt to TimelineInnerState
This makes the API less weird, and is more consistent with other read
receipts methods.
2024-04-23 13:19:14 +02:00
Benjamin Bouvier ec45ce3aa6 timeline: prevent deadlock in populate_initial_user_receipt
Follow-up to 13cc7962.
2024-04-23 13:19:14 +02:00
Benjamin Bouvier 5916192fb6 ffi: don't abort RoomInfo creation if the room member invite event is missing
`Room::invite_details()` can return an error if the room member event
(the invite) is missing from the store. Usually that would be a sign
that the state is semi-broken, since there should always be such an
event for an invited room. But if it's missing, it shouldn't break the
creation of a `RoomInfo`, and just be missing from the struct.
2024-04-23 11:05:44 +02:00
Ivan Enderlin 5e347ce135 fix(ui): Timeline::send_reply correctly sets up m.mentions
fix(ui): `Timeline::send_reply` correctly sets up `m.mentions`
2024-04-23 09:43:05 +02:00
Ivan Enderlin 97ce5742e1 fix(ui): Timeline::send_reply correctly sets up m.mentions.
In https://github.com/matrix-org/matrix-rust-sdk/pull/2691, I suppose
the way `add_mentions` is computed is… wrong. `AddMentions` is used to
automatically infer the `m.mentions` of the reply event based on the
replied event. The way it was computed was based on the reply event
`mentions`, which seems wrong: if the reply contains mentions, then the
sender should be part of it? Nah. That's a bug. We want the reply event
to automatically mention the sender of the replied event if and only
if it's not the same as the current user, i.e. the sender of the reply
event.

This patch fixes the `add_mentions` calculation. This patch also updates
a test and adds another test to ensure that `m.mentions` is correctly
defined when replying to an event.
2024-04-23 09:30:21 +02:00
Benjamin Bouvier 13cc7962e7 timeline: prevent deadlock in replace_with_initial_events
The `state` lock was taken at the top level of this function, and
indirectly implicitly in the `set_fully_read_event` function. This fixes
it, and adds a regression test.
2024-04-22 20:49:12 +02:00
Benjamin Bouvier c471ee42ab paginator: select how many events to retrieve from the /messages query 2024-04-22 16:58:37 +02:00
Benjamin Bouvier 8e2fdd9200 paginator: select how many events to retrieve from the initial /context query 2024-04-22 16:58:37 +02:00
Benjamin Bouvier 289e2ac92b test: silently skip the room summary test on CI, if the server doesn't support it 2024-04-22 14:55:47 +02:00
Benjamin Bouvier 90c35b6b34 room preview: add support for MSC3266, room summary 2024-04-22 14:55:47 +02:00
Benjamin Bouvier fd96360228 dependencies: bump ruma
Honestly, I'm not sure how the retry-after that's a fixed point in time
in the future should be handled, open to suggestions here…
2024-04-22 14:55:47 +02:00
Benjamin Bouvier bd65c77534 dependencies: use a fork of Ruma for the time being 2024-04-22 14:55:47 +02:00
Benjamin Bouvier 4398052d94 timeline: after an event cache update lag, fetch previous events back from the cache
Fixes #3311.

When the timeline is lagging behind the event cache, it should not only
clear what it contains (because it may be lagging some information
coming from the event cache), it should also retrieve the events the
cache knows about, and adds them as if they were "initial" events.

This makes sure that the event cache's events and the timeline's events
are always the same, and that, in the case of a lag, there won't be any
missing chunks (caused by the event cache back-pagination being further
away in the past, than what's displayed in the timeline).

The lag behind a bit too hard to reproduce, I've not included a test
here; but I think this should be the right move.
2024-04-22 11:18:19 +02:00
Ivan Enderlin b03ae97580 Merge pull request #3346 from matrix-org/dependabot/cargo/rustls-0.21.11
chore(deps): bump rustls from 0.21.10 to 0.21.11
2024-04-22 10:48:02 +02:00
dependabot[bot] a157f2eb97 chore(deps): bump rustls from 0.21.10 to 0.21.11
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.10 to 0.21.11.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.10...v/0.21.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 19:53:20 +00:00
Benjamin Bouvier db5151d612 timeline: rename variables around reaction redaction 2024-04-19 15:15:11 +02:00
Benjamin Bouvier 621b47d763 timeline: refactor handling of local redactions too 2024-04-19 15:15:11 +02:00
Benjamin Bouvier 650281b534 timeline: refactor handling of local events to use a single method 2024-04-19 15:15:11 +02:00
Benjamin Bouvier 25ee247fe9 timeline: simplify redaction
Redaction was requiring the `RoomRedactionEventContent`, which was
eventually unused in all the code paths; this removes it.

Also adds lots of documentation for the different types of reaction that
can happen in the timeline.
2024-04-19 15:15:11 +02:00
Andy Balaam 1f524f2dec Merge pull request #3327 from matrix-org/andybalaam/expose-wait-e2ee
ffi: Expose Encryption::wait_for_e2ee_initialization_tasks
2024-04-19 11:26:06 +01:00
Andy Balaam a3e6a070d5 Merge pull request #3338 from matrix-org/kegan/drop-store
bugfix: ensure the SessionStore is cleared when regenerating the OlmMachine
2024-04-19 09:05:01 +01:00
Andy Balaam 381c02d21e crypto: Test that the sqlite store empties its session cache when asked 2024-04-18 13:47:36 +01:00
Andy Balaam 558d133e14 crypto: Test that regenerate_olm clears the store cache 2024-04-18 11:50:22 +01:00
Kegan Dougal 3a99d52e84 Apply suggestions from code review
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com>
2024-04-18 10:34:54 +01:00
Benjamin Bouvier 4325812b05 test: try a different strategy for waiting for the sync to stabilize 2024-04-18 11:29:24 +02:00
Benjamin Bouvier 845f65400a test: add integration test for a room preview 2024-04-18 11:29:24 +02:00
Benjamin Bouvier d5cbf77b84 tests: prefix some more tests with test_ 2024-04-18 11:29:24 +02:00
Benjamin Bouvier f322dcd200 sdk: give the ability to get a room's preview 2024-04-18 11:29:24 +02:00
Kegan Dougal 8cb778e9d9 CHANGELOG 2024-04-18 10:11:52 +01:00
Kegan Dougal 09955cf0db bugfix: ensure the SessionStore is cleared when regenerating the OlmMachine
This fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3110
2024-04-18 10:08:15 +01:00
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
Andy Balaam 8a313df6a4 ffi: Expose Encryption::wait_for_e2ee_initialization_tasks 2024-04-12 14:20:17 +01: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
370 changed files with 38944 additions and 11519 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/audit-check@v1
- uses: actions/checkout@v4
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -12,12 +12,12 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-05-09
components: rustfmt
- name: Run Benchmarks
+51 -5
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -52,7 +52,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -69,7 +69,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
@@ -94,7 +94,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -108,4 +108,50 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios
complement-crypto:
name: "Run Complement Crypto tests"
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_rust_sdk: "." # use local checkout
use_complement_crypto: "MATCHING_BRANCH"
test-crypto-apple-framework-generation:
name: Generate Crypto FFI Apple XCFramework
runs-on: macos-12
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout
uses: actions/checkout@v3
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Add rust targets
run: |
rustup target add aarch64-apple-ios
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
- name: Delete cargo config
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run the Build Framework script
run: ./bindings/apple/build_crypto_xcframework.sh -i
- name: Is XCFramework generated?
if: ${{ hashFiles('generated/MatrixSDKCryptoFFI.zip') != '' }}
run: echo "XCFramework exists"
+23 -25
View File
@@ -41,10 +41,11 @@ jobs:
- markdown
- socks
- sso-login
- image-proc
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -64,7 +65,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -82,7 +83,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -96,7 +97,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -114,7 +115,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -128,7 +129,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -161,7 +162,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -210,11 +211,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
@@ -227,7 +225,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -236,7 +234,7 @@ jobs:
components: clippy
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
uses: qmaru/wasm-pack-action@v0.5.0
with:
version: v0.10.3
@@ -255,7 +253,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -276,12 +274,12 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-05-09
components: rustfmt
- name: Cargo fmt
@@ -295,10 +293,10 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.17.0
uses: crate-ci/typos@v1.20.10
clippy:
name: Run clippy
@@ -308,7 +306,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -318,7 +316,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-05-09
components: clippy
- name: Load cache
@@ -327,7 +325,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -378,7 +376,7 @@ jobs:
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./coverage.yml
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -387,7 +385,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -402,7 +400,7 @@ jobs:
- name: Test
env:
RUST_LOG: "hyper=trace"
RUST_LOG: "info,matrix_sdk=trace"
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
+6 -6
View File
@@ -62,7 +62,7 @@ jobs:
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
# latter does not provide networking for services to communicate with it.
synapse:
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./ci.yml
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -71,7 +71,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -94,7 +94,7 @@ jobs:
tool: cargo-tarpaulin
# set up backend for integration tests
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.8
@@ -102,16 +102,16 @@ jobs:
run: |
rustup run stable cargo tarpaulin \
--skip-clean --profile cov --out xml \
--features experimental-widgets,testing
--features experimental-widgets,testing,image-proc
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.
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -37,10 +37,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2023-11-08
toolchain: nightly-2024-05-09
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
@@ -60,11 +60,11 @@ jobs:
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: './target/doc/'
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
id: deployment
uses: actions/deploy-pages@v2
uses: actions/deploy-pages@v4
+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 -2
View File
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Calculate cache key
id: cachekey
@@ -53,7 +53,7 @@ jobs:
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@v3
uses: actions/cache@v4
id: xtask-cache
with:
path: target/debug/xtask
+2
View File
@@ -7,6 +7,8 @@ emsdk-*
.build
.swiftpm
/Package.swift
# code coverage report
cobertura.xml
## User settings
xcuserdata/
+7 -3
View File
@@ -21,13 +21,17 @@ WeeChat = "WeeChat"
sing = "sign"
singed = "signed"
singing = "signing"
ratatui = "ratatui"
# base64 false positives
Nd = "Nd"
Abl = "Abl"
[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
+1610 -1293
View File
File diff suppressed because it is too large Load Diff
+31 -10
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"
@@ -25,20 +26,34 @@ async-rx = "0.1.3"
async-stream = "0.3.3"
async-trait = "0.1.60"
as_variant = "1.2.0"
base64 = "0.21.0"
base64 = "0.22.0"
byteorder = "1.4.3"
eyeball = { version = "0.8.7", features = ["tracing"] }
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"] }
http = "0.2.6"
futures-util = { version = "0.3.26", default-features = false, features = [
"alloc",
] }
http = "1.1.0"
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"
once_cell = "1.16.0"
pin-project-lite = "0.2.9"
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
ruma = { git = "https://github.com/Hywan/ruma", branch = "feat-sliding-sync-list-include-heroes", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
"compat-arbitrary-length-ids",
"compat-tag-info",
"unstable-msc3401",
"unstable-msc3266",
"unstable-msc4075"
] }
ruma-common = { git = "https://github.com/Hywan/ruma", branch = "feat-sliding-sync-list-include-heroes" }
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
@@ -49,9 +64,11 @@ 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 = { version = "0.27.1" }
uniffi_bindgen = { version = "0.27.1" }
url = "2.5.0"
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "826d0aa22a9b5405535927c7691492db4b92a43b" }
wiremock = "0.6.0"
zeroize = "1.6.0"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
@@ -95,3 +112,7 @@ 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"
box_default = "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
+12 -9
View File
@@ -76,8 +76,9 @@ pub fn keys_query(c: &mut Criterion) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
let machine =
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
b.to_async(&runtime)
@@ -134,7 +135,7 @@ pub fn keys_claiming(c: &mut Criterion) {
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store))
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
runtime
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
@@ -194,7 +195,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();
})
});
@@ -203,8 +204,9 @@ pub fn room_key_sharing(c: &mut Criterion) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
let machine =
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
@@ -225,7 +227,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();
})
});
@@ -267,8 +269,9 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
let machine =
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
let machine = runtime
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
.unwrap();
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).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 -2
View File
@@ -11,7 +11,7 @@ maintained by the owners of the Matrix Rust SDK project.
There are also external bindings in other repositories:
* [`matrix-sdk-crypto-js`], JavaScript bindings of the
* [`matrix-sdk-crypto-wasm`], JavaScript / WebAssembly bindings of the
[`matrix-sdk-crypto`] crate,
* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the
[`matrix-sdk-crypto`] crate
@@ -22,7 +22,7 @@ There are also external bindings in other repositories:
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
[`matrix-sdk`]: ../crates/matrix-sdk
[`matrix-sdk-crypto-js`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-web
[`matrix-sdk-crypto-wasm`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm
[`matrix-sdk-crypto-nodejs`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs
## Contributing
+4 -4
View File
@@ -3,14 +3,14 @@
Pod::Spec.new do |s|
s.name = "MatrixSDKCrypto"
s.version = "0.0.1"
s.version = "0.4.1"
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
s.author = { "matrix.org" => "support@matrix.org" }
s.ios.deployment_target = "11.0"
s.osx.deployment_target = "10.10"
s.ios.deployment_target = "13.0"
s.osx.deployment_target = "10.15"
s.swift_versions = ['5.1', '5.2']
+4 -4
View File
@@ -1,16 +1,16 @@
Pod::Spec.new do |s|
s.name = "MatrixSDKCrypto"
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
s.version = "0.4.1" # Version is only incremented manually and locally before pushing to CocoaPods
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
s.author = { "matrix.org" => "support@matrix.org" }
s.ios.deployment_target = "11.0"
s.osx.deployment_target = "10.10"
s.ios.deployment_target = "13.0"
s.osx.deployment_target = "10.15"
s.swift_versions = ['5.0']
s.swift_versions = ['5.1', '5.2']
s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" }
s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework"
@@ -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")
+72 -39
View File
@@ -1,6 +1,22 @@
#!/usr/bin/env bash
set -eEu
helpFunction() {
echo ""
echo "Usage: $0 -only_ios"
echo -e "\t-i Option to build only for iOS. Default will build for all targets."
exit 1
}
only_ios='false'
while getopts ':i' 'opt'; do
case ${opt} in
'i') only_ios='true' ;;
?) helpFunction ;;
esac
done
cd "$(dirname "$0")"
# Path to the repo root
@@ -22,52 +38,63 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
# Required by olm-sys crate
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
# iOS
echo -e "Building for iOS [1/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
if ${only_ios}; then
# iOS
echo -e "Building only for iOS"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
else
# iOS
echo -e "Building for iOS [1/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
# MacOS
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
echo -e "\nBuilding for macOS (Intel) [3/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
# MacOS
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
echo -e "\nBuilding for macOS (Intel) [3/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
# iOS Simulator
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
# iOS Simulator
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
fi
echo -e "\nCreating XCFramework"
# Lipo together the libraries for the same platform
# MacOS
lipo -create \
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
if ! ${only_ios}; then
echo "Lipo together the libraries for the same platform"
# MacOS
lipo -create \
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
# iOS Simulator
lipo -create \
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
# iOS Simulator
lipo -create \
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
fi
# Generate uniffi files
cargo uniffi-bindgen generate \
cd ../matrix-sdk-crypto-ffi && cargo run --bin matrix_sdk_crypto_ffi generate \
--language swift \
--lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
--out-dir ${GENERATED_DIR} \
"${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl"
--library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
--out-dir ${GENERATED_DIR}
# Move headers to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
# Rename and merge the modulemap files into a single file to the right place
for f in ${GENERATED_DIR}/*.modulemap
do
cat $f; echo;
done > ${HEADERS_DIR}/module.modulemap
rm ${GENERATED_DIR}/*.modulemap
# Move source files to the right place
SWIFT_DIR="${GENERATED_DIR}/Sources"
@@ -77,15 +104,21 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
if ${only_ios}; then
xcodebuild -create-xcframework \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
else
xcodebuild -create-xcframework \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
fi
# Cleanup
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
+11 -4
View File
@@ -12,6 +12,10 @@ publish = false
[lib]
crate-type = ["cdylib", "staticlib"]
[[bin]]
name = "matrix_sdk_crypto_ffi"
path = "uniffi-bindgen.rs"
[features]
default = ["bundled-sqlite"]
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
@@ -31,7 +35,7 @@ sha2 = { workspace = true }
thiserror = { workspace = true }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
uniffi = { workspace = true }
uniffi = { workspace = true , features = ["cli"]}
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -41,7 +45,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
@@ -49,13 +53,16 @@ features = ["crypto-store"]
[dependencies.tokio]
version = "1.33.0"
default_features = false
default-features = false
features = ["rt-multi-thread"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
uniffi = { workspace = true, features = ["build"] }
[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()?;
+5 -2
View File
@@ -25,9 +25,11 @@ 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,
/// Whether or not the device is a dehydrated device.
pub dehydrated: bool,
}
impl From<InnerDevice> for Device {
@@ -42,6 +44,7 @@ impl From<InnerDevice> for Device {
locally_trusted: d.is_locally_trusted(),
cross_signing_trusted: d.is_cross_signing_trusted(),
first_time_seen_ts: d.first_time_seen_ts().0.into(),
dehydrated: d.is_dehydrated(),
}
}
}
+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();
+107 -12
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};
@@ -207,8 +208,12 @@ impl OlmMachine {
passphrase.zeroize();
let inner =
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
let inner = runtime.block_on(InnerMachine::with_store(
&user_id,
device_id,
Arc::new(store),
None,
))?;
Ok(Arc::new(OlmMachine { inner: ManuallyDrop::new(inner), runtime }))
}
@@ -800,6 +805,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 +969,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)?;
@@ -943,7 +995,7 @@ impl OlmMachine {
) -> Result<KeysImportResult, KeyImportError> {
let keys = Cursor::new(keys);
let keys = decrypt_room_key_export(keys, &passphrase)?;
self.import_room_keys_helper(keys, false, progress_listener)
self.import_room_keys_helper(keys, None, progress_listener)
}
/// Import room keys from the given serialized unencrypted key export.
@@ -953,6 +1005,9 @@ impl OlmMachine {
/// should be used if the room keys are coming from the server-side backup,
/// the method will mark all imported room keys as backed up.
///
/// **Note**: This has been deprecated. Use
/// [`OlmMachine::import_room_keys_from_backup`] instead.
///
/// # Arguments
///
/// * `keys` - The serialized version of the unencrypted key export.
@@ -964,11 +1019,38 @@ impl OlmMachine {
keys: String,
progress_listener: Box<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> {
// Assume that the keys came from the current backup version.
let backup_version = self.runtime.block_on(self.inner.backup_machine().backup_version());
let keys: Vec<Value> = serde_json::from_str(&keys)?;
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
self.import_room_keys_helper(keys, backup_version.as_deref(), progress_listener)
}
self.import_room_keys_helper(keys, true, progress_listener)
/// Import room keys from the given serialized unencrypted key export.
///
/// This method is the same as [`OlmMachine::import_room_keys`] but the
/// decryption step is skipped and should be performed by the caller. This
/// should be used if the room keys are coming from the server-side backup.
/// The method will mark all imported room keys as backed up.
///
/// # Arguments
///
/// * `keys` - The serialized version of the unencrypted key export.
///
/// * `backup_version` - The version of the backup that these keys came
/// from.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the key import.
pub fn import_room_keys_from_backup(
&self,
keys: String,
backup_version: String,
progress_listener: Box<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> {
let keys: Vec<Value> = serde_json::from_str(&keys)?;
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
self.import_room_keys_helper(keys, Some(&backup_version), progress_listener)
}
/// Discard the currently active room key for the given room if there is
@@ -976,7 +1058,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(())
}
@@ -1452,22 +1534,35 @@ impl OlmMachine {
}
.into()
}
/// Clear any in-memory caches because they may be out of sync with the
/// underlying data store.
///
/// The crypto store layer is caching olm sessions for a given device.
/// When used in a multi-process context this cache will get outdated.
/// If the machine is used by another process, the cache must be
/// invalidating when the main process is resumed.
pub async fn clear_crypto_cache(&self) {
self.inner.clear_crypto_cache().await
}
}
impl OlmMachine {
fn import_room_keys_helper(
&self,
keys: Vec<ExportedRoomKey>,
from_backup: bool,
from_backup_version: Option<&str>,
progress_listener: Box<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> {
let listener = |progress: usize, total: usize| {
progress_listener.on_progress(progress as i32, total as i32)
};
#[allow(deprecated)]
let result =
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
let result = self.runtime.block_on(self.inner.store().import_room_keys(
keys,
from_backup_version,
listener,
))?;
Ok(KeysImportResult {
imported: result.imported_count as i64,
@@ -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(),
}
}
}
@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}
+6 -7
View File
@@ -24,17 +24,12 @@ vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
anyhow = { workspace = true }
as_variant = { workspace = true }
async-compat = "0.2.1"
base64 = "0.21"
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"] }
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"] }
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,13 +37,12 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-core = { workspace = true }
tracing-opentelemetry = "0.22.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = { version = "0.2.2" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = { workspace = true, features = ["time"] }
uniffi = { workspace = true, features = ["tokio"] }
url = "2.2.2"
url = { workspace = true }
zeroize = { workspace = true }
uuid = { version = "1.4.1", features = ["v4"] }
language-tags = "0.3.2"
@@ -69,6 +63,7 @@ features = [
"rustls-tls", # note: differ from block below
"socks",
"sqlite",
"uniffi",
]
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
@@ -83,4 +78,8 @@ features = [
"native-tls", # note: differ from block above
"socks",
"sqlite",
"uniffi",
]
[lints]
workspace = true
-8
View File
@@ -2,14 +2,6 @@
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
# OpenTelemetry support
The bindings have support for OpenTelemetry, allowing for the upload of traces to
any OpenTelemetry collector. This support is provided through the
[`opentelemetry`](https://docs.rs/opentelemetry/latest/opentelemetry/)
crate, which requires the [`protoc`] binary to be installed during the build
process.
## Platforms
### Swift/iOS sync
@@ -1,10 +1,12 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
sync::{Arc, RwLock as StdRwLock},
};
use matrix_sdk::{
encryption::BackupDownloadStrategy,
oidc::{
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
types::{
client_credentials::ClientCredentials,
errors::ClientErrorCode::AccessDenied,
@@ -15,30 +17,37 @@ use matrix_sdk::{
},
AuthorizationResponse, Oidc, OidcError,
},
AuthSession,
reqwest::StatusCode,
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::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 +61,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 +92,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 +103,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 +187,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 +199,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 +216,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 +239,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,98 +256,82 @@ 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);
};
let Some(authentication_server) = client.discovered_authentication_server() else {
return Err(AuthenticationError::OidcNotSupported);
let oidc = client.inner.oidc();
let issuer = match oidc.fetch_authentication_issuer().await {
Ok(issuer) => issuer,
Err(error) => {
if error
.as_client_api_error()
.is_some_and(|err| err.status_code == StatusCode::NOT_FOUND)
{
return Err(AuthenticationError::OidcNotSupported);
} else {
return Err(AuthenticationError::ServerUnreachable(error));
}
}
};
let Some(oidc_configuration) = &self.oidc_configuration else {
@@ -305,27 +341,24 @@ impl AuthenticationService {
let redirect_url = Url::parse(&oidc_configuration.redirect_uri)
.map_err(|_e| AuthenticationError::OidcMetadataInvalid)?;
let oidc = client.inner.oidc();
self.configure_oidc(&oidc, issuer, oidc_configuration).await?;
RUNTIME.block_on(async {
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 +387,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 +412,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_oidc_login = client.inner.oidc().fetch_authentication_issuer().await.is_ok();
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
@@ -401,7 +446,7 @@ impl AuthenticationService {
async fn configure_oidc(
&self,
oidc: &Oidc,
authentication_server: AuthenticationServerInfo,
issuer: String,
configuration: &OidcConfiguration,
) -> Result<(), AuthenticationError> {
if oidc.client_credentials().is_some() {
@@ -411,22 +456,20 @@ impl AuthenticationService {
let oidc_metadata = self.oidc_metadata(configuration)?;
if self.load_client_registration(oidc, &authentication_server, oidc_metadata.clone()).await
{
if self.load_client_registration(oidc, issuer.clone(), oidc_metadata.clone()).await {
tracing::info!("OIDC configuration loaded from disk.");
return Ok(());
}
tracing::info!("Registering this client for OIDC.");
let registration_response = oidc
.register_client(&authentication_server.issuer, oidc_metadata.clone(), None)
.await?;
let registration_response =
oidc.register_client(&issuer, oidc_metadata.clone(), None).await?;
// The format of the credentials changes according to the client metadata that
// was sent. Public clients only get a client ID.
let credentials =
ClientCredentials::None { client_id: registration_response.client_id.clone() };
oidc.restore_registered_client(authentication_server, oidc_metadata, credentials);
oidc.restore_registered_client(issuer, oidc_metadata, credentials);
tracing::info!("Persisting OIDC registration data.");
self.store_client_registration(oidc).await?;
@@ -468,11 +511,11 @@ impl AuthenticationService {
async fn load_client_registration(
&self,
oidc: &Oidc,
authentication_server: &AuthenticationServerInfo,
issuer: String,
oidc_metadata: VerifiedClientMetadata,
) -> bool {
let Ok(issuer) = Url::parse(&authentication_server.issuer) else {
tracing::error!("Failed to parse {:?}", authentication_server.issuer);
let Ok(issuer_url) = Url::parse(&issuer) else {
tracing::error!("Failed to parse {issuer:?}");
return false;
};
let Some(registrations) = OidcRegistrations::new(
@@ -483,12 +526,12 @@ impl AuthenticationService {
.ok() else {
return false;
};
let Some(client_id) = registrations.client_id(&issuer) else {
let Some(client_id) = registrations.client_id(&issuer_url) else {
return false;
};
oidc.restore_registered_client(
authentication_server.clone(),
issuer,
oidc_metadata,
ClientCredentials::None { client_id: client_id.0 },
);
@@ -550,30 +593,50 @@ 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)
.auto_enable_cross_signing(true)
.backup_download_strategy(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 +649,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)?;
client.restore_session_inner(session).await?;
Ok(client)
Ok(Arc::new(client))
}
}
+366 -204
View File
@@ -8,13 +8,14 @@ use anyhow::{anyhow, Context as _};
use matrix_sdk::{
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
oidc::{
requests::account_management::AccountManagementActionFull,
types::{
client_credentials::ClientCredentials,
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
},
OidcAccountManagementAction, OidcSession,
OidcSession,
},
ruma::{
api::client::{
@@ -32,16 +33,23 @@ use matrix_sdk::{
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, TransactionId, UInt, UserId,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotificationProcessSetup;
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
NotificationProcessSetup as MatrixNotificationProcessSetup,
};
use mime::Mime;
use ruma::{
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
events::room::power_levels::RoomPowerLevelsEventContent,
api::client::{alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -53,8 +61,10 @@ use super::{room::Room, session_verification::SessionVerificationController, RUN
use crate::{
client,
encryption::Encryption,
notification::NotificationClientBuilder,
notification::NotificationClient,
notification_settings::NotificationSettings,
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
ClientError,
@@ -175,11 +185,11 @@ impl Drop for Client {
}
impl Client {
pub fn new(
pub async fn new(
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 +203,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() {
@@ -205,9 +215,7 @@ impl Client {
"missing session delegates when enabling the cross-process lock"
))?;
}
RUNTIME.block_on(async {
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await
})?;
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
}
if let Some(session_delegate) = session_delegate {
@@ -239,24 +247,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(
@@ -286,11 +292,11 @@ impl Client {
}
/// Restores the client from a `Session`.
pub fn restore_session(&self, session: Session) -> Result<(), ClientError> {
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
let sliding_sync_proxy = session.sliding_sync_proxy.clone();
let auth_session: AuthSession = session.try_into()?;
self.restore_session_inner(auth_session)?;
self.restore_session_inner(auth_session).await?;
if let Some(sliding_sync_proxy) = sliding_sync_proxy {
let sliding_sync_proxy = Url::parse(&sliding_sync_proxy)
@@ -305,29 +311,18 @@ impl Client {
impl Client {
/// Restores the client from an `AuthSession`.
pub(crate) fn restore_session_inner(
pub(crate) async fn restore_session_inner(
&self,
session: impl Into<AuthSession>,
) -> anyhow::Result<()> {
RUNTIME.block_on(async move {
self.inner.restore_session(session).await?;
Ok(())
})
self.inner.restore_session(session).await?;
Ok(())
}
/// The homeserver's trusted OIDC Provider that was discovered in the
/// well-known.
///
/// This will only be set if the homeserver supports authenticating via
/// OpenID Connect and this `Client` was constructed using auto-discovery by
/// setting the homeserver with [`ClientBuilder::server_name()`].
pub(crate) fn discovered_authentication_server(&self) -> Option<AuthenticationServerInfo> {
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 +337,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?)
}
}
@@ -375,15 +369,15 @@ impl Client {
})
}
pub fn session(&self) -> Result<Session, ClientError> {
RUNTIME.block_on(async move { Self::session_inner((*self.inner).clone()).await })
pub async fn session(&self) -> Result<Session, ClientError> {
Self::session_inner((*self.inner).clone()).await
}
pub fn account_url(
pub async fn account_url(
&self,
action: Option<AccountManagementAction>,
) -> Result<Option<String>, ClientError> {
match self.inner.oidc().account_management_url(action.map(Into::into)) {
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
Ok(url) => Ok(url.map(|u| u.to_string())),
Err(e) => {
tracing::error!("Failed retrieving account management URL: {e}");
@@ -397,57 +391,43 @@ impl Client {
Ok(user_id.to_string())
}
pub fn display_name(&self) -> Result<String, ClientError> {
let l = self.inner.clone();
RUNTIME.block_on(async move {
let display_name = l.account().get_display_name().await?.context("No User ID found")?;
Ok(display_name)
})
pub async fn display_name(&self) -> Result<String, ClientError> {
let display_name =
self.inner.account().get_display_name().await?.context("No User ID found")?;
Ok(display_name)
}
pub fn set_display_name(&self, name: String) -> Result<(), ClientError> {
let client = self.inner.clone();
RUNTIME.block_on(async move {
client
.account()
.set_display_name(Some(name.as_str()))
.await
.context("Unable to set display name")?;
Ok(())
})
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
self.inner
.account()
.set_display_name(Some(name.as_str()))
.await
.context("Unable to set display name")?;
Ok(())
}
pub fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
let client = self.inner.clone();
RUNTIME.block_on(async move {
let mime: Mime = mime_type.parse()?;
client.account().upload_avatar(&mime, data).await?;
Ok(())
})
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
let mime: Mime = mime_type.parse()?;
self.inner.account().upload_avatar(&mime, data).await?;
Ok(())
}
pub fn remove_avatar(&self) -> Result<(), ClientError> {
let client = self.inner.clone();
RUNTIME.block_on(async move {
client.account().set_avatar_url(None).await?;
Ok(())
})
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
self.inner.account().set_avatar_url(None).await?;
Ok(())
}
pub fn avatar_url(&self) -> Result<Option<String>, ClientError> {
let l = self.inner.clone();
RUNTIME.block_on(async move {
let avatar_url = l.account().get_avatar_url().await?;
Ok(avatar_url.map(|u| u.to_string()))
})
/// Sends a request to retrieve the avatar URL. Will fill the cache used by
/// [`Self::cached_avatar_url`] on success.
pub async fn avatar_url(&self) -> Result<Option<String>, ClientError> {
let avatar_url = self.inner.account().get_avatar_url().await?;
Ok(avatar_url.map(|u| u.to_string()))
}
/// Retrieves an avatar cached from a previous call to [`Self::avatar_url`].
pub fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
let l = self.inner.clone();
RUNTIME.block_on(async move {
let url = l.account().get_cached_avatar_url().await?;
Ok(url)
})
Ok(RUNTIME.block_on(self.inner.account().get_cached_avatar_url())?)
}
pub fn device_id(&self) -> Result<String, ClientError> {
@@ -455,35 +435,31 @@ impl Client {
Ok(device_id.to_string())
}
pub fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let client = self.inner.clone();
RUNTIME.block_on(async move {
let response = client.create_room(request.into()).await?;
Ok(String::from(response.room_id()))
})
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let response = self.inner.create_room(request.into()).await?;
Ok(String::from(response.room_id()))
}
/// Get the content of the event of the given type out of the account data
/// store.
///
/// It will be returned as a JSON string.
pub fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
RUNTIME.block_on(async move {
let event = self.inner.account().account_data_raw(event_type.into()).await?;
Ok(event.map(|e| e.json().get().to_owned()))
})
pub async fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
let event = self.inner.account().account_data_raw(event_type.into()).await?;
Ok(event.map(|e| e.json().get().to_owned()))
}
/// Set the given account data content for the given event type.
///
/// It should be supplied as a JSON string.
pub fn set_account_data(&self, event_type: String, content: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let raw_content = Raw::from_json_string(content)?;
self.inner.account().set_account_data_raw(event_type.into(), raw_content).await?;
Ok(())
})
pub async fn set_account_data(
&self,
event_type: String,
content: String,
) -> Result<(), ClientError> {
let raw_content = Raw::from_json_string(content)?;
self.inner.account().set_account_data_raw(event_type.into(), raw_content).await?;
Ok(())
}
pub async fn upload_media(
@@ -547,37 +523,35 @@ impl Client {
.await?)
}
pub fn get_session_verification_controller(
pub async fn get_session_verification_controller(
&self,
) -> Result<Arc<SessionVerificationController>, ClientError> {
RUNTIME.block_on(async move {
if let Some(session_verification_controller) =
&*self.session_verification_controller.read().await
{
return Ok(Arc::new(session_verification_controller.clone()));
}
let user_id = self.inner.user_id().context("Failed retrieving current user_id")?;
let user_identity = self
.inner
.encryption()
.get_user_identity(user_id)
.await?
.context("Failed retrieving user identity")?;
if let Some(session_verification_controller) =
&*self.session_verification_controller.read().await
{
return Ok(Arc::new(session_verification_controller.clone()));
}
let user_id = self.inner.user_id().context("Failed retrieving current user_id")?;
let user_identity = self
.inner
.encryption()
.get_user_identity(user_id)
.await?
.context("Failed retrieving user identity")?;
let session_verification_controller =
SessionVerificationController::new(self.inner.encryption(), user_identity);
let session_verification_controller =
SessionVerificationController::new(self.inner.encryption(), user_identity);
*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
*self.session_verification_controller.write().await =
Some(session_verification_controller.clone());
Ok(Arc::new(session_verification_controller))
})
Ok(Arc::new(session_verification_controller))
}
/// Log out the current user. This method returns an optional URL that
/// should be presented to the user to complete logout (in the case of
/// Session having been authenticated using OIDC).
pub fn logout(&self) -> Result<Option<String>, ClientError> {
pub async fn logout(&self) -> Result<Option<String>, ClientError> {
let Some(auth_api) = self.inner.auth_api() else {
return Err(anyhow!("Missing authentication API").into());
};
@@ -585,12 +559,13 @@ impl Client {
match auth_api {
AuthApi::Matrix(a) => {
tracing::info!("Logging out via the homeserver.");
RUNTIME.block_on(a.logout())?;
a.logout().await?;
Ok(None)
}
AuthApi::Oidc(api) => {
tracing::info!("Logging out via OIDC.");
let end_session_builder = RUNTIME.block_on(api.logout())?;
let end_session_builder = api.logout().await?;
if let Some(builder) = end_session_builder {
let url = builder.build()?.url;
@@ -604,7 +579,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 +588,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,53 +624,36 @@ 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(
pub async fn search_users(
&self,
search_term: String,
limit: u64,
) -> Result<SearchUsersResults, ClientError> {
RUNTIME.block_on(async move {
let response = self.inner.search_users(&search_term, limit).await?;
Ok(SearchUsersResults::from(response))
let response = self.inner.search_users(&search_term, limit).await?;
Ok(SearchUsersResults::from(response))
}
pub async fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
let owned_user_id = UserId::parse(user_id.clone())?;
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
Ok(UserProfile {
user_id,
display_name: response.displayname.clone(),
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
})
}
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 user_profile = UserProfile {
user_id,
display_name: response.displayname.clone(),
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
};
Ok(user_profile)
})
}
pub fn notification_client(
pub async fn notification_client(
self: Arc<Self>,
process_setup: NotificationProcessSetup,
) -> Result<Arc<NotificationClientBuilder>, ClientError> {
NotificationClientBuilder::new(self.clone(), process_setup.into())
) -> Result<Arc<NotificationClient>, ClientError> {
Ok(Arc::new(NotificationClient {
inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
.await?,
_client: self.clone(),
}))
}
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
@@ -699,17 +661,164 @@ impl Client {
}
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
RUNTIME.block_on(async move {
Arc::new(NotificationSettings::new(
(*self.inner).clone(),
self.inner.notification_settings().await,
))
})
let inner = RUNTIME.block_on(self.inner.notification_settings());
Arc::new(NotificationSettings::new((*self.inner).clone(), inner))
}
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()),
))
}
/// Join a room by its ID.
///
/// Use this method when the homeserver already knows of the given room ID.
/// Otherwise use `join_room_by_id_or_alias` so you can pass a list of
/// server names for the homeserver to find the room.
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)))
}
/// Join a room by its ID or alias.
///
/// When supplying the room's ID, you can also supply a list of server names
/// for the homeserver to find the room. Typically these server names
/// come from a permalink's `via` parameters, or from resolving a room's
/// alias into an ID.
pub async fn join_room_by_id_or_alias(
&self,
room_id_or_alias: String,
server_names: Vec<String>,
) -> Result<Arc<Room>, ClientError> {
let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
let server_names = server_names
.iter()
.map(|name| OwnedServerName::try_from(name.as_str()))
.collect::<Result<Vec<_>, _>>()?;
let room =
self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
Ok(Arc::new(Room::new(room)))
}
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(())
}
/// Resolves the given room alias to a room ID (and a list of servers), if
/// possible.
pub async fn resolve_room_alias(
&self,
room_alias: String,
) -> Result<ResolvedRoomAlias, ClientError> {
let room_alias = RoomAliasId::parse(&room_alias)?;
let response = self.inner.resolve_room_alias(&room_alias).await?;
Ok(response.into())
}
/// Given a room id, get the preview of a room, to interact with it.
///
/// The list of `via_servers` must be a list of servers that know
/// about the room and can resolve it, and that may appear as a `via`
/// parameter in e.g. a permalink URL. This list can be empty.
pub async fn get_room_preview_from_room_id(
&self,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
let via_servers = via_servers
.into_iter()
.map(ServerName::parse)
.collect::<Result<Vec<_>, _>>()
.context("at least one `via` server name is invalid")?;
// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
// rustc win that one fight.
let room_id: &RoomId = &room_id;
let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
}
/// Given a room alias, get the preview of a room, to interact with it.
pub async fn get_room_preview_from_room_alias(
&self,
room_alias: String,
) -> Result<RoomPreview, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
// rustc win that one fight.
let room_alias: &RoomAliasId = &room_alias;
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
}
}
#[uniffi::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
fn call(&self, ignored_user_ids: Vec<String>);
}
#[derive(uniffi::Enum)]
@@ -733,6 +842,24 @@ impl From<NotificationProcessSetup> for MatrixNotificationProcessSetup {
}
}
/// Information about a room, that was resolved from a room alias.
#[derive(uniffi::Record)]
pub struct ResolvedRoomAlias {
/// The room ID that the alias resolved to.
pub room_id: String,
/// A list of servers that can be used to find the room by its room ID.
pub servers: Vec<String>,
}
impl From<get_alias::v3::Response> for ResolvedRoomAlias {
fn from(value: get_alias::v3::Response) -> Self {
Self {
room_id: value.room_id.to_string(),
servers: value.servers.iter().map(ToString::to_string).collect(),
}
}
}
#[derive(uniffi::Record)]
pub struct SearchUsersResults {
pub results: Vec<UserProfile>,
@@ -766,6 +893,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 +902,10 @@ impl Client {
delegate.did_refresh_tokens();
}
});
} else {
debug!(
"No client delegate found, session change couldn't be applied: {session_change:?}"
);
}
}
@@ -1062,7 +1194,7 @@ impl Session {
refresh_token,
latest_id_token,
},
issuer_info,
issuer,
} = api.user_session().context("Missing session")?;
let client_id = api
.client_credentials()
@@ -1075,7 +1207,7 @@ impl Session {
client_id,
client_metadata,
latest_id_token: latest_id_token.map(|t| t.to_string()),
issuer_info,
issuer,
};
let oidc_data = serde_json::to_string(&oidc_data).ok();
@@ -1128,7 +1260,7 @@ impl TryFrom<Session> for AuthSession {
refresh_token,
latest_id_token,
},
issuer_info: oidc_data.issuer_info,
issuer: oidc_data.issuer,
};
let session = OidcSession {
@@ -1163,17 +1295,18 @@ pub(crate) struct OidcSessionData {
client_id: String,
client_metadata: VerifiedClientMetadata,
latest_id_token: Option<String>,
issuer_info: AuthenticationServerInfo,
issuer: String,
}
/// Represents an unverified client registration against an OpenID Connect
/// authentication issuer. Call `validate` on this to use it for restoration.
#[derive(Deserialize)]
#[serde(try_from = "OidcUnvalidatedSessionDataDeHelper")]
pub(crate) struct OidcUnvalidatedSessionData {
client_id: String,
client_metadata: ClientMetadata,
latest_id_token: Option<String>,
issuer_info: AuthenticationServerInfo,
issuer: String,
}
impl OidcUnvalidatedSessionData {
@@ -1183,30 +1316,59 @@ impl OidcUnvalidatedSessionData {
client_id: self.client_id,
client_metadata: self.client_metadata.validate()?,
latest_id_token: self.latest_id_token,
issuer_info: self.issuer_info,
issuer: self.issuer,
})
}
}
#[derive(Deserialize)]
struct OidcUnvalidatedSessionDataDeHelper {
client_id: String,
client_metadata: ClientMetadata,
latest_id_token: Option<String>,
issuer_info: Option<AuthenticationServerInfo>,
issuer: Option<String>,
}
impl TryFrom<OidcUnvalidatedSessionDataDeHelper> for OidcUnvalidatedSessionData {
type Error = String;
fn try_from(value: OidcUnvalidatedSessionDataDeHelper) -> Result<Self, Self::Error> {
let OidcUnvalidatedSessionDataDeHelper {
client_id,
client_metadata,
latest_id_token,
issuer_info,
issuer,
} = value;
let issuer = issuer
.or(issuer_info.map(|info| info.issuer))
.ok_or_else(|| "missing field `issuer`".to_owned())?;
Ok(Self { client_id, client_metadata, latest_id_token, issuer })
}
}
#[derive(uniffi::Enum)]
pub enum AccountManagementAction {
Profile,
SessionsList,
SessionView { device_id: String },
SessionEnd { device_id: String },
AccountDeactivate,
CrossSigningReset,
}
impl From<AccountManagementAction> for OidcAccountManagementAction {
impl From<AccountManagementAction> for AccountManagementActionFull {
fn from(value: AccountManagementAction) -> Self {
match value {
AccountManagementAction::Profile => Self::Profile,
AccountManagementAction::SessionsList => Self::SessionsList,
AccountManagementAction::SessionView { device_id } => {
Self::SessionView { device_id: device_id.into() }
}
AccountManagementAction::SessionEnd { device_id } => {
Self::SessionEnd { device_id: device_id.into() }
}
AccountManagementAction::SessionView { device_id } => Self::SessionView { device_id },
AccountManagementAction::SessionEnd { device_id } => Self::SessionEnd { device_id },
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
}
}
}
+165 -76
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,36 @@ pub struct ClientBuilder {
inner: MatrixClientBuilder,
cross_process_refresh_lock_id: Option<String>,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
encryption_settings: EncryptionSettings,
}
#[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(),
cross_process_refresh_lock_id: None,
session_delegate: None,
additional_root_certificates: Default::default(),
encryption_settings: EncryptionSettings {
auto_enable_cross_signing: false,
backup_download_strategy:
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
auto_enable_backups: false,
},
})
}
pub fn enable_cross_process_refresh_lock(
@@ -83,14 +144,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,8 +196,47 @@ 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 fn auto_enable_cross_signing(
self: Arc<Self>,
auto_enable_cross_signing: bool,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
Arc::new(builder)
}
/// Select a strategy to download room keys from the backup. By default
/// we download after a decryption failure.
///
/// Take a look at the [`BackupDownloadStrategy`] enum for more options.
pub fn backup_download_strategy(
self: Arc<Self>,
backup_download_strategy: BackupDownloadStrategy,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.backup_download_strategy = backup_download_strategy;
Arc::new(builder)
}
/// Automatically create a backup version if no backup exists.
pub fn auto_enable_backups(self: Arc<Self>, auto_enable_backups: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.encryption_settings.auto_enable_backups = auto_enable_backups;
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
Ok(Arc::new(self.build_inner().await?))
}
}
@@ -156,17 +261,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 +274,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 +334,14 @@ 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 })?;
inner_builder = inner_builder.with_encryption_settings(builder.encryption_settings);
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
@@ -240,38 +360,7 @@ impl ClientBuilder {
sdk_client.set_sliding_sync_proxy(Some(Url::parse(&sliding_sync_proxy)?));
}
Ok(Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)?)
}
}
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,
}
Ok(Client::new(sdk_client, builder.cross_process_refresh_lock_id, builder.session_delegate)
.await?)
}
}
+60 -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,8 +194,37 @@ 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 {
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
pub async fn ed25519_key(&self) -> Option<String> {
self.inner.ed25519_key().await
}
/// Get the public curve25519 key of our own device in base64. This is
/// usually what is called the identity key of the device.
pub async fn curve25519_key(&self) -> Option<String> {
self.inner.curve25519_key().await.map(|k| k.to_base64())
}
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
let mut stream = self.inner.backups().state_stream();
@@ -326,4 +363,26 @@ 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());
}
})))
}
/// Waits for end-to-end encryption initialization tasks to finish, if any
/// was running in the background.
pub async fn wait_for_e2ee_initialization_tasks(&self) {
self.inner.wait_for_e2ee_initialization_tasks().await;
}
}
+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 {
+122 -1
View File
@@ -5,7 +5,11 @@ use ruma::events::{
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
};
use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
use crate::{
room_member::MembershipState,
ruma::{MessageType, NotifyType},
ClientError,
};
#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
@@ -119,6 +123,7 @@ pub enum MessageLikeEventContent {
CallInvite,
CallHangup,
CallCandidates,
CallNotify { notify_type: NotifyType },
KeyVerificationReady,
KeyVerificationStart,
KeyVerificationCancel,
@@ -143,6 +148,12 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
AnySyncMessageLikeEvent::CallNotify(content) => {
let original_content = get_message_like_event_original_content(content)?;
MessageLikeEventContent::CallNotify {
notify_type: original_content.notify_type.into(),
}
}
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
MessageLikeEventContent::KeyVerificationReady
}
@@ -216,3 +227,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,
}
}
}
+3
View File
@@ -32,14 +32,17 @@ mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_directory_search;
mod room_info;
mod room_list;
mod room_member;
mod room_preview;
mod ruma;
mod session_verification;
mod sync_service;
mod task_handle;
mod timeline;
mod timeline_event_filter;
mod tracing;
mod utils;
mod widget;
+18 -57
View File
@@ -1,15 +1,11 @@
use std::sync::Arc;
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
NotificationClientBuilder as MatrixNotificationClientBuilder,
NotificationItem as MatrixNotificationItem, NotificationProcessSetup,
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
};
use ruma::{EventId, RoomId};
use crate::{
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
};
use crate::{client::Client, error::ClientError, event::TimelineEvent};
#[derive(uniffi::Enum)]
pub enum NotificationEvent {
@@ -21,6 +17,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,9 +60,10 @@ 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,
display_name: item.room_computed_display_name,
avatar_url: item.room_avatar_url,
canonical_alias: item.room_canonical_alias,
joined_members_count: item.joined_members_count,
@@ -78,74 +76,37 @@ impl NotificationItem {
}
}
#[derive(Clone, uniffi::Object)]
pub struct NotificationClientBuilder {
client: Arc<Client>,
builder: MatrixNotificationClientBuilder,
}
impl NotificationClientBuilder {
pub(crate) fn new(
client: Arc<Client>,
process_setup: NotificationProcessSetup,
) -> Result<Arc<Self>, ClientError> {
let builder = RUNTIME.block_on(async {
MatrixNotificationClient::builder((*client.inner).clone(), process_setup).await
})?;
Ok(Arc::new(Self { builder, client }))
}
}
#[uniffi::export]
impl NotificationClientBuilder {
/// Filter out the notification event according to the push rules present in
/// the event.
pub fn filter_by_push_rules(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.filter_by_push_rules();
Arc::new(Self { builder, client: this.client })
}
pub fn finish(self: Arc<Self>) -> Arc<NotificationClient> {
let this = unwrap_or_clone_arc(self);
Arc::new(NotificationClient { inner: this.builder.build(), _client: this.client })
}
}
#[derive(uniffi::Object)]
pub struct NotificationClient {
inner: MatrixNotificationClient,
pub(crate) inner: MatrixNotificationClient,
/// A reference to the FFI client.
///
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
/// would need to repeat the hack done in the FFI `Client::drop` method.
_client: Arc<Client>,
pub(crate) _client: Arc<Client>,
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl NotificationClient {
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
pub fn get_notification(
pub async fn get_notification(
&self,
room_id: String,
event_id: String,
) -> Result<Option<NotificationItem>, ClientError> {
let room_id = RoomId::parse(room_id)?;
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async move {
let item = self
.inner
.get_notification(&room_id, &event_id)
.await
.map_err(ClientError::from)?;
if let Some(item) = item {
Ok(Some(NotificationItem::from_inner(item)))
} else {
Ok(None)
}
})
let item =
self.inner.get_notification(&room_id, &event_id).await.map_err(ClientError::from)?;
if let Some(item) = item {
Ok(Some(NotificationItem::from_inner(item)))
} else {
Ok(None)
}
}
}
@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::sync::{Arc, RwLock};
use matrix_sdk::{
event_handler::EventHandlerHandle,
@@ -13,9 +13,8 @@ use ruma::{
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
RoomId,
};
use tokio::sync::RwLock;
use tokio::sync::RwLock as AsyncRwLock;
use super::RUNTIME;
use crate::error::NotificationSettingsError;
/// Enum representing the push notification modes for a room.
@@ -73,7 +72,7 @@ impl RoomNotificationSettings {
#[derive(Clone, uniffi::Object)]
pub struct NotificationSettings {
sdk_client: MatrixClient,
sdk_notification_settings: Arc<RwLock<SdkNotificationSettings>>,
sdk_notification_settings: Arc<AsyncRwLock<SdkNotificationSettings>>,
pushrules_event_handler: Arc<RwLock<Option<EventHandlerHandle>>>,
}
@@ -82,10 +81,9 @@ impl NotificationSettings {
sdk_client: MatrixClient,
sdk_notification_settings: SdkNotificationSettings,
) -> Self {
let sdk_notification_settings = Arc::new(RwLock::new(sdk_notification_settings));
Self {
sdk_client,
sdk_notification_settings,
sdk_notification_settings: Arc::new(AsyncRwLock::new(sdk_notification_settings)),
pushrules_event_handler: Arc::new(RwLock::new(None)),
}
}
@@ -94,11 +92,9 @@ impl NotificationSettings {
impl Drop for NotificationSettings {
fn drop(&mut self) {
// Remove the event handler on the sdk_client.
RUNTIME.block_on(async move {
if let Some(event_handler) = self.pushrules_event_handler.read().await.as_ref() {
self.sdk_client.remove_event_handler(event_handler.clone());
}
});
if let Some(event_handler) = self.pushrules_event_handler.read().unwrap().as_ref() {
self.sdk_client.remove_event_handler(event_handler.clone());
}
}
}
@@ -114,18 +110,14 @@ impl NotificationSettings {
delegate.settings_did_change();
});
RUNTIME.block_on(async move {
*self.pushrules_event_handler.write().await = Some(event_handler);
});
*self.pushrules_event_handler.write().unwrap() = Some(event_handler);
} else {
// Remove the event handler if there is no delegate
RUNTIME.block_on(async move {
let event_handler = &mut *self.pushrules_event_handler.write().await;
if let Some(event_handler) = event_handler {
self.sdk_client.remove_event_handler(event_handler.clone());
}
*event_handler = None;
});
let event_handler = &mut *self.pushrules_event_handler.write().unwrap();
if let Some(event_handler) = event_handler {
self.sdk_client.remove_event_handler(event_handler.clone());
}
*event_handler = None;
}
}
@@ -143,9 +135,11 @@ impl NotificationSettings {
is_encrypted: bool,
is_one_to_one: bool,
) -> Result<RoomNotificationSettings, NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let parsed_room_id = RoomId::parse(&room_id)
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
let notification_settings = self.sdk_notification_settings.read().await;
// Get the current user defined mode for this room
if let Some(mode) =
notification_settings.get_user_defined_room_notification_mode(&parsed_room_id).await
@@ -158,6 +152,7 @@ impl NotificationSettings {
let mode = notification_settings
.get_default_room_notification_mode(is_encrypted.into(), is_one_to_one.into())
.await;
Ok(RoomNotificationSettings::new(mode.into(), true))
}
@@ -167,10 +162,15 @@ impl NotificationSettings {
room_id: String,
mode: RoomNotificationMode,
) -> Result<(), NotificationSettingsError> {
let notification_settings = self.sdk_notification_settings.read().await;
let parsed_room_id = RoomId::parse(&room_id)
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
notification_settings.set_room_notification_mode(&parsed_room_id, mode.into()).await?;
self.sdk_notification_settings
.read()
.await
.set_room_notification_mode(&parsed_room_id, mode.into())
.await?;
Ok(())
}
@@ -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 -117
View File
@@ -1,11 +1,4 @@
use std::{collections::HashMap, fmt::Debug, pin::Pin};
use base64::{engine::general_purpose::STANDARD, Engine};
use futures_core::future::BoxFuture;
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},
@@ -15,80 +8,6 @@ use tracing_subscriber::{
EnvFilter, Layer,
};
use crate::RUNTIME;
#[derive(Clone, Debug)]
struct TokioRuntime {
runtime: Handle,
}
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
type Interval = tokio_stream::wrappers::IntervalStream;
type Delay = Pin<Box<tokio::time::Sleep>>;
fn interval(&self, period: std::time::Duration) -> Self::Interval {
let _guard = self.runtime.enter();
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
}
fn spawn(&self, future: BoxFuture<'static, ()>) {
#[allow(clippy::let_underscore_future)]
let _ = self.runtime.spawn(future);
}
fn delay(&self, duration: std::time::Duration) -> Self::Delay {
let _guard = self.runtime.enter();
Box::pin(tokio::time::sleep(duration))
}
}
impl RuntimeChannel for TokioRuntime {
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
fn batch_message_channel<T: Debug + Send>(
&self,
capacity: usize,
) -> (Self::Sender<T>, Self::Receiver<T>) {
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
}
}
pub fn create_otlp_tracer(
user: String,
password: String,
otlp_endpoint: String,
client_name: String,
) -> anyhow::Result<Tracer> {
let runtime = RUNTIME.handle().to_owned();
let auth = STANDARD.encode(format!("{user}:{password}"));
let headers = HashMap::from([("Authorization".to_owned(), format!("Basic {auth}"))]);
let http_client = matrix_sdk::reqwest::ClientBuilder::new().build()?;
let exporter = opentelemetry_otlp::new_exporter()
.http()
.with_http_client(http_client)
.with_protocol(Protocol::HttpBinary)
.with_endpoint(otlp_endpoint)
.with_headers(headers);
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
let _guard = runtime.enter();
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(exporter)
.with_trace_config(
opentelemetry_sdk::trace::config()
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
)
.install_batch(tracer_runtime)?;
Ok(tracer)
}
#[cfg(target_os = "android")]
pub fn log_panics() {
std::env::set_var("RUST_BACKTRACE", "1");
@@ -202,13 +121,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 +169,8 @@ where
pub struct TracingFileConfiguration {
path: String,
file_prefix: String,
file_suffix: Option<String>,
max_files: Option<u64>,
}
#[derive(uniffi::Record)]
@@ -258,37 +192,3 @@ pub fn setup_tracing(config: TracingConfiguration) {
.with(text_layers(config))
.init();
}
#[derive(uniffi::Record)]
pub struct OtlpTracingConfiguration {
client_name: String,
user: String,
password: String,
otlp_endpoint: String,
filter: String,
/// Controls whether to print to stdout or, equivalent, the system logs on
/// Android.
write_to_stdout_or_system: bool,
write_to_files: Option<TracingFileConfiguration>,
}
#[uniffi::export]
pub fn setup_otlp_tracing(config: OtlpTracingConfiguration) {
#[cfg(target_os = "android")]
log_panics();
let otlp_tracer =
create_otlp_tracer(config.user, config.password, config.otlp_endpoint, config.client_name)
.expect("Couldn't configure the OpenTelemetry tracer");
let otlp_layer = tracing_opentelemetry::layer().with_tracer(otlp_tracer);
tracing_subscriber::registry()
.with(EnvFilter::new(&config.filter))
.with(text_layers(TracingConfiguration {
filter: config.filter,
write_to_stdout_or_system: config.write_to_stdout_or_system,
write_to_files: config.write_to_files,
}))
.with(otlp_layer)
.init();
}
+415 -139
View File
@@ -1,14 +1,25 @@
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_ui::timeline::RoomExt;
use matrix_sdk::{
event_cache::paginator::PaginatorError,
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
use mime::Mime;
use ruma::{
api::client::room::report_content,
assign,
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
EventId, UserId,
events::{
call::notify,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
},
TimelineEventType,
},
EventId, Int, RoomAliasId, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
@@ -17,10 +28,11 @@ use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
error::{ClientError, MediaInfoError, RoomError},
event::{MessageLikeEventType, StateEventType},
room_info::RoomInfo,
room_member::{MessageLikeEventType, RoomMember, StateEventType},
ruma::ImageInfo,
timeline::{EventTimelineItem, Timeline},
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{EventTimelineItem, FocusEventError, ReceiptType, Timeline},
utils::u64_to_uint,
TaskHandle,
};
@@ -66,7 +78,15 @@ impl Room {
self.inner.room_id().to_string()
}
pub fn name(&self) -> Option<String> {
/// Returns the room's name from the state event if available, otherwise
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
pub fn display_name(&self) -> Result<String, ClientError> {
Ok(RUNTIME.block_on(self.inner.computed_display_name())?.to_string())
}
/// The raw name as present in the room state event.
pub fn raw_name(&self) -> Option<String> {
self.inner.name()
}
@@ -79,7 +99,7 @@ impl Room {
}
pub fn is_direct(&self) -> bool {
RUNTIME.block_on(async move { self.inner.is_direct().await.unwrap_or(false) })
RUNTIME.block_on(self.inner.is_direct()).unwrap_or(false)
}
pub fn is_public(&self) -> bool {
@@ -124,82 +144,119 @@ impl Room {
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
}
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
/// For rooms one is invited to, retrieves the room member information for
/// the user who invited the logged-in user to a room.
pub async fn inviter(&self) -> Option<RoomMember> {
if self.inner.state() == RoomState::Invited {
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> {
/// 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 timeline(&self) -> Result<Arc<Timeline>, ClientError> {
let mut write_guard = self.timeline.write().await;
if let Some(timeline) = &*write_guard {
timeline.clone()
Ok(timeline.clone())
} else {
let timeline = Timeline::new(self.inner.timeline().await);
let timeline = Timeline::new(self.inner.timeline().await?);
*write_guard = Some(timeline.clone());
timeline
Ok(timeline)
}
}
pub async fn poll_history(&self) -> Arc<Timeline> {
Timeline::new(self.inner.poll_history().await)
}
/// Returns a timeline focused on the given event.
///
/// Note: this timeline is independent from that returned with
/// [`Self::timeline`], and as such it is not cached.
pub async fn timeline_focused_on_event(
&self,
event_id: String,
num_context_events: u16,
internal_id_prefix: Option<String>,
) -> Result<Arc<Timeline>, FocusEventError> {
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
})?;
pub fn display_name(&self) -> Result<String, ClientError> {
let r = self.inner.clone();
RUNTIME.block_on(async move { Ok(r.display_name().await?.to_string()) })
let room = &self.inner;
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
if let Some(internal_id_prefix) = internal_id_prefix {
builder = builder.with_internal_id_prefix(internal_id_prefix);
}
let timeline = match builder
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
.build()
.await
{
Ok(t) => t,
Err(err) => {
if let matrix_sdk_ui::timeline::Error::PaginationError(
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
) = err
{
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
}
return Err(FocusEventError::Other { msg: err.to_string() });
}
};
Ok(Timeline::new(timeline))
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
let room = self.inner.clone();
RUNTIME.block_on(async move {
let is_encrypted = room.is_encrypted().await?;
Ok(is_encrypted)
})
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
}
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
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)))
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
Ok(member.into())
}
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
let room = self.inner.clone();
RUNTIME.block_on(async move {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = room.get_member(&user_id).await?.context("No user found")?;
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
Ok(avatar_url_string)
})
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
Ok(avatar_url_string)
}
pub fn member_display_name(&self, user_id: String) -> Result<Option<String>, ClientError> {
let room = self.inner.clone();
RUNTIME.block_on(async move {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = room.get_member(&user_id).await?.context("No user found")?;
let avatar_url_string = member.display_name().map(|m| m.to_owned());
Ok(avatar_url_string)
})
pub async fn member_display_name(
&self,
user_id: String,
) -> Result<Option<String>, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
let avatar_url_string = member.display_name().map(|m| m.to_owned());
Ok(avatar_url_string)
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
let avatar_url = self.inner.avatar_url();
// Look for a local event in the `Timeline`.
//
// First off, let's see if a `Timeline` exists…
@@ -210,7 +267,6 @@ impl Room {
if timeline_last_event.is_local_echo() {
return Ok(RoomInfo::new(
&self.inner,
avatar_url,
Some(Arc::new(EventTimelineItem(timeline_last_event))),
)
.await?);
@@ -218,7 +274,8 @@ impl Room {
}
}
// Otherwise, fallback to the classical path.
// Otherwise, create a synthetic [`EventTimelineItem`] using the classical
// [`Room`] path.
let latest_event = match self.inner.latest_event() {
Some(latest_event) => matrix_sdk_ui::timeline::EventTimelineItem::from_latest_event(
self.inner.client(),
@@ -230,7 +287,8 @@ impl Room {
.map(Arc::new),
None => None,
};
Ok(RoomInfo::new(&self.inner, avatar_url, latest_event).await?)
Ok(RoomInfo::new(&self.inner, latest_event).await?)
}
pub fn subscribe_to_room_info_updates(
@@ -250,6 +308,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
@@ -258,12 +334,14 @@ impl Room {
///
/// * `reason` - The reason for the event being redacted (optional).
/// its transaction ID (optional). If not given one is created.
pub fn redact(&self, event_id: String, reason: Option<String>) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let event_id = EventId::parse(event_id)?;
self.inner.redact(&event_id, reason.as_deref(), None).await?;
Ok(())
})
pub async fn redact(
&self,
event_id: String,
reason: Option<String>,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
self.inner.redact(&event_id, reason.as_deref(), None).await?;
Ok(())
}
pub fn active_members_count(&self) -> u64 {
@@ -288,78 +366,66 @@ impl Room {
///
/// * `score` - The score to rate this content as where -100 is most
/// offensive and 0 is inoffensive (optional).
pub fn report_content(
pub async fn report_content(
&self,
event_id: String,
score: Option<i32>,
reason: Option<String>,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
let int_score = score.map(|value| value.into());
RUNTIME.block_on(async move {
let event_id = EventId::parse(event_id)?;
self.inner
.client()
.send(
report_content::v3::Request::new(
self.inner.room_id().into(),
event_id,
int_score,
reason,
),
None,
)
.await?;
Ok(())
})
self.inner
.client()
.send(
report_content::v3::Request::new(
self.inner.room_id().into(),
event_id,
int_score,
reason,
),
None,
)
.await?;
Ok(())
}
/// Ignores a user.
///
/// # 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.
///
/// Only invited and joined rooms can be left.
pub fn leave(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.leave().await?;
Ok(())
})
pub async fn leave(&self) -> Result<(), ClientError> {
self.inner.leave().await?;
Ok(())
}
/// Join this room.
///
/// Only invited and left rooms can be joined via this method.
pub fn join(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.join().await?;
Ok(())
})
pub async fn join(&self) -> Result<(), ClientError> {
self.inner.join().await?;
Ok(())
}
/// Sets a new name to the room.
pub fn set_name(&self, name: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.set_name(name).await?;
Ok(())
})
pub async fn set_name(&self, name: String) -> Result<(), ClientError> {
self.inner.set_name(name).await?;
Ok(())
}
/// Sets a new topic in the room.
pub fn set_topic(&self, topic: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.set_room_topic(&topic).await?;
Ok(())
})
pub async fn set_topic(&self, topic: String) -> Result<(), ClientError> {
self.inner.set_room_topic(&topic).await?;
Ok(())
}
/// Upload and set the room's avatar.
@@ -375,48 +441,47 @@ impl Room {
/// * `data` - The raw data that will be uploaded to the homeserver's
/// content repository
/// * `media_info` - The media info used as avatar image info.
pub fn upload_avatar(
pub async fn upload_avatar(
&self,
mime_type: String,
data: Vec<u8>,
media_info: Option<ImageInfo>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let mime: Mime = mime_type.parse()?;
self.inner
.upload_avatar(
&mime,
data,
media_info
.map(TryInto::try_into)
.transpose()
.map_err(|_| RoomError::InvalidMediaInfo)?,
)
.await?;
Ok(())
})
let mime: Mime = mime_type.parse()?;
self.inner
.upload_avatar(
&mime,
data,
media_info
.map(TryInto::try_into)
.transpose()
.map_err(|_| RoomError::InvalidMediaInfo)?,
)
.await?;
Ok(())
}
/// Removes the current room avatar
pub fn remove_avatar(&self) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
self.inner.remove_avatar().await?;
Ok(())
})
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
self.inner.remove_avatar().await?;
Ok(())
}
pub fn invite_user_by_id(&self, user_id: String) -> Result<(), ClientError> {
RUNTIME.block_on(async move {
let user = <&UserId>::try_from(user_id.as_str())
.context("Could not create user from string")?;
self.inner.invite_user_by_id(user).await?;
Ok(())
})
pub async fn invite_user_by_id(&self, user_id: String) -> Result<(), ClientError> {
let user =
<&UserId>::try_from(user_id.as_str()).context("Could not create user from string")?;
self.inner.invite_user_by_id(user).await?;
Ok(())
}
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 +559,191 @@ 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())
}
/// This will only send a call notification event if appropriate.
///
/// This function is supposed to be called whenever the user creates a room
/// call. It will send a `m.call.notify` event if:
/// - there is not yet a running call.
/// It will configure the notify type: ring or notify based on:
/// - is this a DM room -> ring
/// - is this a group with more than one other member -> notify
pub async fn send_call_notification_if_needed(&self) -> Result<(), ClientError> {
self.inner.send_call_notification_if_needed().await?;
Ok(())
}
/// Send a call notification event in the current room.
///
/// This is only supposed to be used in **custom** situations where the user
/// explicitly chooses to send a `m.call.notify` event to invite/notify
/// someone explicitly in unusual conditions. The default should be to
/// use `send_call_notification_if_necessary` just before a new room call is
/// created/joined.
///
/// One example could be that the UI allows to start a call with a subset of
/// users of the room members first. And then later on the user can
/// invite more users to the call.
pub async fn send_call_notification(
&self,
call_id: String,
application: RtcApplicationType,
notify_type: NotifyType,
mentions: Mentions,
) -> Result<(), ClientError> {
self.inner
.send_call_notification(
call_id,
application.into(),
notify_type.into(),
mentions.into(),
)
.await?;
Ok(())
}
}
/// Generates a `matrix.to` permalink to the given room alias.
#[uniffi::export]
pub fn matrix_to_room_alias_permalink(
room_alias: String,
) -> std::result::Result<String, ClientError> {
let room_alias = RoomAliasId::parse(room_alias)?;
Ok(room_alias.matrix_to_uri().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 +751,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 +773,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;
@@ -549,3 +813,15 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
}))
}
}
#[derive(uniffi::Enum)]
pub enum RtcApplicationType {
Call,
}
impl From<RtcApplicationType> for notify::ApplicationType {
fn from(value: RtcApplicationType) -> Self {
match value {
RtcApplicationType::Call => notify::ApplicationType::Call,
}
}
}
@@ -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>);
}
+34 -10
View File
@@ -1,7 +1,6 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::RoomState;
use ruma::OwnedMxcUri;
use crate::{
notification_settings::RoomNotificationMode, room::Membership, room_member::RoomMember,
@@ -11,26 +10,39 @@ use crate::{
#[derive(uniffi::Record)]
pub struct RoomInfo {
id: String,
name: Option<String>,
/// The room's name from the room state event if received from sync, or one
/// that's been computed otherwise.
display_name: Option<String>,
/// Room name as defined by the room state event only.
raw_name: Option<String>,
topic: Option<String>,
avatar_url: Option<String>,
is_direct: bool,
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>>,
/// Member who invited the current user to a room that's in the invited
/// state.
///
/// Can be missing if the room membership invite event is missing from the
/// store.
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,
@@ -45,33 +57,44 @@ pub struct RoomInfo {
impl RoomInfo {
pub(crate) async fn new(
room: &matrix_sdk::Room,
avatar_url: Option<OwnedMxcUri>,
latest_event: Option<Arc<EventTimelineItem>>,
) -> 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(),
display_name: room.computed_display_name().await.ok().map(|name| name.to_string()),
raw_name: room.name(),
topic: room.topic(),
avatar_url: avatar_url.map(Into::into),
avatar_url: room.avatar_url().map(Into::into),
is_direct: room.is_direct().await?,
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
.ok()
.and_then(|details| details.inviter)
.map(Into::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 +107,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(),
+162 -58
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_joined,
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")]
@@ -99,11 +125,12 @@ impl RoomListService {
})))
}
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
async fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
Ok(Arc::new(RoomListItem {
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
inner: Arc::new(self.inner.room(room_id).await?),
utd_hook: self.utd_hook.clone(),
}))
}
@@ -114,13 +141,6 @@ impl RoomListService {
}))
}
async fn invites(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
Ok(Arc::new(RoomList {
room_list_service: self.clone(),
inner: Arc::new(self.inner.invites().await.map_err(RoomListError::from)?),
}))
}
async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> {
self.inner.apply_input(input.into()).await.map(|_| ()).map_err(Into::into)
}
@@ -152,7 +172,7 @@ pub struct RoomList {
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
}
#[uniffi::export]
#[uniffi::export(async_runtime = "tokio")]
impl RoomList {
fn loading_state(
&self,
@@ -193,7 +213,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(
@@ -210,8 +233,8 @@ impl RoomList {
}
}
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
self.room_list_service.room(room_id)
async fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
self.room_list_service.room(room_id).await
}
}
@@ -375,19 +398,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 +413,70 @@ impl RoomListDynamicEntriesController {
#[derive(uniffi::Enum)]
pub enum RoomListEntriesDynamicFilterKind {
All,
AllNonLeft,
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
NonLeft,
Joined,
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::Joined => Self(Box::new(new_filter_joined(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")]
@@ -419,8 +485,11 @@ impl RoomListItem {
self.inner.id().to_string()
}
fn name(&self) -> Option<String> {
RUNTIME.block_on(async { self.inner.name().await })
/// Returns the room's name from the state event if available, otherwise
/// compute a room name based on the room's nature (DM or not) and number of
/// members.
fn display_name(&self) -> Option<String> {
RUNTIME.block_on(self.inner.computed_display_name())
}
fn avatar_url(&self) -> Option<String> {
@@ -428,7 +497,7 @@ impl RoomListItem {
}
fn is_direct(&self) -> bool {
RUNTIME.block_on(async { self.inner.inner_room().is_direct().await.unwrap_or(false) })
RUNTIME.block_on(self.inner.inner_room().is_direct()).unwrap_or(false)
}
fn canonical_alias(&self) -> Option<String> {
@@ -436,25 +505,66 @@ impl RoomListItem {
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
let avatar_url = self.inner.avatar_url();
let latest_event = self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new);
Ok(RoomInfo::new(self.inner.inner_room(), avatar_url, latest_event).await?)
Ok(RoomInfo::new(self.inner.inner_room(), latest_event).await?)
}
/// Building a `Room`.
/// 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(),
})
}
}
/// 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.
///
/// 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)))),
))
}
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
/// used to filter timeline events besides the default timeline filter. If
/// `None` is passed, only the default timeline filter will be used.
/// * `internal_id_prefix` - An optional String that will be prepended to
/// all the timeline item's internal IDs, making it possible to
/// distinguish different timeline instances from each other.
async fn init_timeline(
&self,
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
internal_id_prefix: Option<String>,
) -> Result<(), RoomListError> {
let mut timeline_builder = self
.inner
.default_room_timeline_builder()
.await
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
// Temporary workaround for coroutine leaks on Kotlin.
fn full_room_blocking(&self) -> Arc<Room> {
RUNTIME.block_on(async move { self.full_room().await })
if let Some(event_type_filter) = event_type_filter {
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
});
}
if let Some(internal_id_prefix) = internal_id_prefix {
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
}
if let Some(utd_hook) = self.utd_hook.clone() {
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
}
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
}
fn subscribe(&self, settings: Option<RoomSubscription>) {
@@ -468,14 +578,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)]
@@ -513,6 +615,7 @@ pub struct RequiredState {
pub struct RoomSubscription {
pub required_state: Option<Vec<RequiredState>>,
pub timeline_limit: Option<u32>,
pub include_heroes: Option<bool>,
}
impl From<RoomSubscription> for RumaRoomSubscription {
@@ -521,7 +624,8 @@ impl From<RoomSubscription> for RumaRoomSubscription {
required_state: val.required_state.map(|r|
r.into_iter().map(|s| (s.key.into(), s.value)).collect()
).unwrap_or_default(),
timeline_limit: val.timeline_limit.map(|u| u.into())
timeline_limit: val.timeline_limit.map(|u| u.into()),
include_heroes: val.include_heroes,
})
}
}
+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 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(),
}
}
}
@@ -0,0 +1,53 @@
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
use ruma::space::SpaceRoomJoinRule;
/// The preview of a room, be it invited/joined/left, or not.
#[derive(uniffi::Record)]
pub struct RoomPreview {
/// The room id for this room.
pub room_id: String,
/// The canonical alias for the room.
pub canonical_alias: Option<String>,
/// The room's name, if set.
pub name: Option<String>,
/// The room's topic, if set.
pub topic: Option<String>,
/// The MXC URI to the room's avatar, if set.
pub avatar_url: Option<String>,
/// The number of joined members.
pub num_joined_members: u64,
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: Option<String>,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
/// Is the room joined by the current user?
pub is_joined: bool,
/// Is the current user invited to this room?
pub is_invited: bool,
/// is the join rule public for this room?
pub is_public: bool,
/// Can we knock (or restricted-knock) to this room?
pub can_knock: bool,
}
impl RoomPreview {
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
Self {
room_id: preview.room_id.to_string(),
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
name: preview.name,
topic: preview.topic,
avatar_url: preview.avatar_url.map(|url| url.to_string()),
num_joined_members: preview.num_joined_members,
room_type: preview.room_type.map(|room_type| room_type.to_string()),
is_history_world_readable: preview.is_world_readable,
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
can_knock: matches!(
preview.join_rule,
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
),
}
}
}
+139 -18
View File
@@ -21,6 +21,7 @@ use matrix_sdk::attachment::{
use ruma::{
assign,
events::{
call::notify::NotifyType as RumaNotifyType,
location::AssetType as RumaAssetType,
poll::start::PollKind as RumaPollKind,
room::{
@@ -44,8 +45,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 +57,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 +240,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 +304,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 +313,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 +324,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 +333,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),
@@ -286,6 +376,30 @@ impl From<RumaMessageType> for MessageType {
}
}
#[derive(Clone, uniffi::Enum)]
pub enum NotifyType {
Ring,
Notify,
}
impl From<RumaNotifyType> for NotifyType {
fn from(val: RumaNotifyType) -> Self {
match val {
RumaNotifyType::Ring => Self::Ring,
_ => Self::Notify,
}
}
}
impl From<NotifyType> for RumaNotifyType {
fn from(value: NotifyType) -> Self {
match value {
NotifyType::Ring => RumaNotifyType::Ring,
NotifyType::Notify => RumaNotifyType::Notify,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct EmoteMessageContent {
pub body: String,
@@ -295,6 +409,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 +418,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 +429,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 +438,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>,
+96 -9
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::{crypto::types::events::UtdCause, Client};
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,11 +94,13 @@ 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 })
}
}
@@ -98,11 +109,87 @@ impl SyncServiceBuilder {
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>,
/// What we know about what caused this UTD. E.g. was this event sent when
/// we were not a member of this room?
pub cause: UtdCause,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
fn from(value: SdkUnableToDecryptInfo) -> Self {
Self {
event_id: value.event_id.to_string(),
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
cause: value.cause,
}
}
}
@@ -14,7 +14,9 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
use ruma::events::FullStateEventContent;
use tracing::warn;
use super::ProfileDetails;
@@ -40,11 +42,19 @@ impl TimelineItemContent {
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: if let FullStateEventContent::Original { content, .. } =
membership.content()
{
content.displayname.clone()
} else {
None
},
change: membership.change().map(Into::into),
},
Content::ProfileChange(profile) => {
@@ -112,11 +122,13 @@ pub enum TimelineItemContentKind {
end_time: Option<u64>,
has_been_edited: bool,
},
CallInvite,
UnableToDecrypt {
msg: EncryptedMessage,
},
RoomMembership {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
},
ProfileChange {
@@ -211,6 +223,10 @@ pub enum EncryptedMessage {
MegolmV1AesSha2 {
/// The ID of the session used to encrypt the message.
session_id: String,
/// What we know about what caused this UTD. E.g. was this event sent
/// when we were not a member of this room?
cause: UtdCause,
},
Unknown,
}
@@ -224,9 +240,9 @@ impl EncryptedMessage {
let sender_key = sender_key.clone();
Self::OlmV1Curve25519AesSha2 { sender_key }
}
Message::MegolmV1AesSha2 { session_id, .. } => {
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
let session_id = session_id.clone();
Self::MegolmV1AesSha2 { session_id }
Self::MegolmV1AesSha2 { session_id, cause: *cause }
}
Message::Unknown => Self::Unknown,
}
@@ -307,7 +323,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 +366,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 {
+131 -112
View File
@@ -17,12 +17,14 @@ use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
use anyhow::{Context, Result};
use as_variant::as_variant;
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{pin_mut, StreamExt as _};
use matrix_sdk::attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
};
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin, Profile, TimelineDetails};
use matrix_sdk_ui::timeline::{
EventItemOrigin, LiveBackPaginationStatus, Profile, TimelineDetails,
};
use mime::Mime;
use ruma::{
events::{
@@ -52,19 +54,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 +110,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 {
@@ -158,15 +162,19 @@ impl Timeline {
self.inner.fetch_members().await
}
pub fn subscribe_to_back_pagination_status(
pub async fn subscribe_to_back_pagination_status(
&self,
listener: Box<dyn BackPaginationStatusListener>,
listener: Box<dyn PaginationStatusListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let mut subscriber = self.inner.back_pagination_status();
let (initial, mut subscriber) = self
.inner
.live_back_pagination_status()
.await
.context("can't subscribe to the back-pagination status on a focused timeline")?;
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Send the current state even if it hasn't changed right away.
listener.on_update(subscriber.next_now());
listener.on_update(initial);
while let Some(status) = subscriber.next().await {
listener.on_update(status);
@@ -174,26 +182,41 @@ impl Timeline {
}))))
}
/// Loads older messages into the timeline.
/// Paginate backwards, whether we are in focused mode or in live mode.
///
/// Raises an exception if there are no timeline listeners.
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
RUNTIME.block_on(async { Ok(self.inner.paginate_backwards(opts.into()).await?) })
/// Returns whether we hit the end of the timeline or not.
pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
Ok(self.inner.paginate_backwards(num_events).await?)
}
pub fn send_read_receipt(
/// Paginate forwards, when in focused mode.
///
/// Returns whether we hit the end of the timeline or not.
pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
Ok(self.inner.focused_paginate_forwards(num_events).await?)
}
pub async fn send_read_receipt(
&self,
receipt_type: ReceiptType,
event_id: String,
) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
self.inner
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
.await?;
Ok(())
}
RUNTIME.block_on(async {
self.inner
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
.await?;
Ok(())
})
/// 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>) {
@@ -207,6 +230,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 +252,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 +265,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 +287,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 +299,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 +313,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 +327,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 +342,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
}))
@@ -391,29 +432,27 @@ impl Timeline {
Ok(())
}
pub fn send_reply(
pub async fn send_reply(
&self,
msg: Arc<RoomMessageEventContentWithoutRelation>,
reply_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
anyhow::Ok(())
})?;
self.inner
.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes)
.await
.map_err(|err| anyhow::anyhow!(err))?;
Ok(())
}
pub fn edit(
pub async fn edit(
&self,
new_content: Arc<RoomMessageEventContentWithoutRelation>,
edit_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
RUNTIME.block_on(async {
self.inner.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
anyhow::Ok(())
})?;
self.inner
.edit((*new_content).clone(), &edit_item.0)
.await
.map_err(|err| anyhow::anyhow!(err))?;
Ok(())
}
@@ -426,14 +465,10 @@ impl Timeline {
edit_item: Arc<EventTimelineItem>,
) -> Result<(), ClientError> {
let poll_data = PollData { question, answers, max_selections, poll_kind };
RUNTIME.block_on(async {
self.inner
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
.await?;
anyhow::Ok(())
})?;
self.inner
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
.await
.map_err(|err| anyhow::anyhow!(err))?;
Ok(())
}
@@ -464,20 +499,16 @@ impl Timeline {
self.send(Arc::new(room_message_event_content))
}
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
pub async fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
Ok(())
})
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
Ok(())
}
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
let event_id = <&EventId>::try_from(event_id.as_str())?;
RUNTIME.block_on(async {
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
Ok(())
})
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
Ok(())
}
pub fn retry_send(self: Arc<Self>, txn_id: String) {
@@ -496,44 +527,58 @@ impl Timeline {
});
}
pub fn get_event_timeline_item_by_event_id(
pub async fn get_event_timeline_item_by_event_id(
&self,
event_id: String,
) -> Result<Arc<EventTimelineItem>, ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
})
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(EventTimelineItem(item)))
}
pub fn get_timeline_event_content_by_event_id(
pub async fn get_timeline_event_content_by_event_id(
&self,
event_id: String,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
let event_id = EventId::parse(event_id)?;
RUNTIME.block_on(async {
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
let msgtype = item
.content()
.as_message()
.context("Item with given event ID is not a message")?
.msgtype()
.to_owned();
let item = self
.inner
.item_by_event_id(&event_id)
.await
.context("Item with given event ID not found")?;
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
})
let msgtype = item
.content()
.as_message()
.context("Item with given event ID is not a message")?
.msgtype()
.to_owned();
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(Debug, thiserror::Error, uniffi::Error)]
pub enum FocusEventError {
#[error("the event id parameter {event_id} is incorrect: {err}")]
InvalidEventId { event_id: String, err: String },
#[error("the event {event_id} could not be found")]
EventNotFound { event_id: String },
#[error("error when trying to focus on an event: {msg}")]
Other { msg: String },
}
#[derive(uniffi::Record)]
@@ -548,8 +593,8 @@ pub trait TimelineListener: Sync + Send {
}
#[uniffi::export(callback_interface)]
pub trait BackPaginationStatusListener: Sync + Send {
fn on_update(&self, status: BackPaginationStatus);
pub trait PaginationStatusListener: Sync + Send {
fn on_update(&self, status: LiveBackPaginationStatus);
}
#[derive(Clone, uniffi::Object)]
@@ -709,8 +754,8 @@ impl TimelineItem {
}
}
pub fn unique_id(&self) -> u64 {
self.0.unique_id()
pub fn unique_id(&self) -> String {
self.0.unique_id().to_owned()
}
pub fn fmt_debug(&self) -> String {
@@ -939,32 +984,6 @@ impl SendAttachmentJoinHandle {
}
}
#[derive(uniffi::Enum)]
pub enum PaginationOptions {
SimpleRequest { event_limit: u16, wait_for_token: bool },
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
}
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
fn from(value: PaginationOptions) -> Self {
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
let (wait_for_token, mut opts) = match value {
PaginationOptions::SimpleRequest { event_limit, wait_for_token } => {
(wait_for_token, Opts::simple_request(event_limit))
}
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
(wait_for_token, Opts::until_num_items(event_limit, items))
}
};
if wait_for_token {
opts = opts.wait_for_token();
}
opts
}
}
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
#[derive(uniffi::Enum)]
pub enum VirtualTimelineItem {
@@ -991,7 +1010,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()
}
}
}
}
+8 -4
View File
@@ -260,11 +260,12 @@ pub fn new_virtual_element_call_widget(
/// but should only be done as temporal workarounds until this function is
/// adjusted
#[uniffi::export]
pub fn get_element_call_required_permissions() -> WidgetCapabilities {
pub fn get_element_call_required_permissions(own_user_id: String) -> WidgetCapabilities {
use ruma::events::StateEventType;
WidgetCapabilities {
read: vec![
WidgetEventFilter::StateWithType { event_type: "org.matrix.msc3401.call".to_owned() },
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
WidgetEventFilter::MessageLikeWithType {
@@ -275,11 +276,14 @@ pub fn get_element_call_required_permissions() -> WidgetCapabilities {
},
],
send: vec![
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
WidgetEventFilter::StateWithType {
WidgetEventFilter::StateWithTypeAndStateKey {
event_type: StateEventType::CallMember.to_string(),
state_key: own_user_id.clone(),
},
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
WidgetEventFilter::StateWithType {
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
+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", "matrix-sdk-crypto?/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
+7
View File
@@ -0,0 +1,7 @@
fn main() {
// Prevent unnecessary rerunning of this build script
println!("cargo:rerun-if-changed=build.rs");
// Prevent nightly CI from erroring on tarpaulin_include attribute
println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)");
}
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,
+95 -19
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 {
@@ -116,13 +126,9 @@ impl BaseRoomInfo {
&self,
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<RoomMember>,
heroes: Vec<&str>,
) -> DisplayName {
calculate_room_name(
joined_member_count,
invited_member_count,
heroes.iter().take(3).map(|mem| mem.name()).collect::<Vec<&str>>(),
)
calculate_room_name(joined_member_count, invited_member_count, heroes)
}
/// Get the room version of this room.
@@ -285,6 +291,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,32 +353,35 @@ impl Default for BaseRoomInfo {
tombstone: None,
topic: None,
rtc_member: BTreeMap::new(),
is_marked_unread: false,
notable_tags: RoomNotableTags::empty(),
}
}
}
/// Calculate room name according to step 3 of the [naming algorithm.]
///
/// [naming algorithm]: https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room
fn calculate_room_name(
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<&str>,
mut heroes: Vec<&str>,
) -> DisplayName {
let heroes_count = heroes.len() as u64;
let num_heroes = heroes.len() as u64;
let invited_joined = invited_member_count + joined_member_count;
let invited_joined_minus_one = invited_joined.saturating_sub(1);
let names = if heroes_count >= invited_joined_minus_one {
let mut names = heroes;
// stabilize ordering
names.sort_unstable();
names.join(", ")
} else if heroes_count < invited_joined_minus_one && invited_joined > 1 {
let mut names = heroes;
names.sort_unstable();
// Stabilize ordering.
heroes.sort_unstable();
let names = if num_heroes == 0 && invited_joined > 1 {
format!("{} people", invited_joined)
} else if num_heroes >= invited_joined_minus_one {
heroes.join(", ")
} else if num_heroes < invited_joined_minus_one && invited_joined > 1 {
// TODO: What length does the spec want us to use here and in
// the `else`?
format!("{}, and {} others", names.join(", "), (invited_joined - heroes_count))
format!("{}, and {} others", heroes.join(", "), (invited_joined - num_heroes))
} else {
"".to_owned()
};
@@ -520,7 +559,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() {
@@ -536,6 +579,9 @@ mod tests {
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
actual = calculate_room_name(5, 0, vec![]);
assert_eq!(DisplayName::Calculated("5 people".to_owned()), actual);
actual = calculate_room_name(0, 0, vec![]);
assert_eq!(DisplayName::Empty, actual);
@@ -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());
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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 {
@@ -58,6 +58,8 @@ pub trait StateStoreIntegrationTests {
async fn test_member_saving(&self);
/// Test filter saving.
async fn test_filter_saving(&self);
/// Test saving a user avatar URL.
async fn test_user_avatar_url_saving(&self);
/// Test sync token saving.
async fn test_sync_token_saving(&self);
/// Test stripped room member saving.
@@ -74,6 +76,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.
@@ -535,6 +539,30 @@ impl StateStoreIntegrationTests for DynStateStore {
assert_matches!(self.get_kv_data(StateStoreDataKey::Filter(filter_name)).await, Ok(None));
}
async fn test_user_avatar_url_saving(&self) {
let user_id = user_id!("@alice:example.org");
let url = "https://example.org";
self.set_kv_data(
StateStoreDataKey::UserAvatarUrl(user_id),
StateStoreDataValue::UserAvatarUrl(url.to_owned()),
)
.await
.unwrap();
assert_let!(
Ok(Some(StateStoreDataValue::UserAvatarUrl(stored_url))) =
self.get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await
);
assert_eq!(stored_url, url);
self.remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await.unwrap();
assert_matches!(
self.get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await,
Ok(None)
);
}
async fn test_sync_token_saving(&self) {
let sync_token_1 = "t392-516_47314_0_7_1";
let sync_token_2 = "t392-516_47314_0_7_2";
@@ -1058,6 +1086,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");
@@ -1240,6 +1339,12 @@ macro_rules! statestore_integration_tests {
store.test_filter_saving().await
}
#[async_test]
async fn test_user_avatar_url_saving() {
let store = get_store().await.unwrap().into_state_store();
store.test_user_avatar_url_saving().await
}
#[async_test]
async fn test_sync_token_saving() {
let store = get_store().await.unwrap().into_state_store();
@@ -1288,6 +1393,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),
})
}
@@ -169,11 +206,19 @@ impl StateStore for MemoryStore {
);
}
StateStoreDataKey::UserAvatarUrl(user_id) => {
self.filters.write().unwrap().insert(
self.user_avatar_url.write().unwrap().insert(
user_id.to_string(),
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(())
@@ -186,7 +231,10 @@ impl StateStore for MemoryStore {
self.filters.write().unwrap().remove(filter_name);
}
StateStoreDataKey::UserAvatarUrl(user_id) => {
self.filters.write().unwrap().remove(user_id.as_str());
self.user_avatar_url.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(),
})
}
}
+32 -14
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:
@@ -401,8 +419,8 @@ pub struct StoreConfig {
}
#[cfg(not(tarpaulin_include))]
impl std::fmt::Debug for StoreConfig {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
impl fmt::Debug for StoreConfig {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
fmt.debug_struct("StoreConfig").finish()
}
}
+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
+6
View File
@@ -1,6 +1,12 @@
use std::{env, process};
fn main() {
// Prevent unnecessary rerunning of this build script
println!("cargo:rerun-if-changed=build.rs");
// Prevent nightly CI from erroring on tarpaulin_include attribute
println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)");
let is_wasm = env::var_os("CARGO_CFG_TARGET_ARCH").is_some_and(|arch| arch == "wasm32");
if is_wasm && env::var_os("CARGO_FEATURE_JS").is_none() {
eprintln!(
+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);
}
}
+11 -8
View File
@@ -47,7 +47,7 @@ use std::{
};
use tokio::{sync::Mutex, time::sleep};
use tracing::instrument;
use tracing::{debug, error, info, instrument, trace};
use crate::{
executor::{spawn, JoinHandle},
@@ -180,7 +180,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
// decrement `num_holders`. That's fine because that means the lock
// was taken by at least one thread, and after this call it will be
// taken by at least one thread.
tracing::trace!("We already had the lock, incrementing holder count");
trace!("We already had the lock, incrementing holder count");
self.num_holders.fetch_add(1, atomic::Ordering::SeqCst);
let guard = CrossProcessStoreLockGuard { num_holders: self.num_holders.clone() };
return Ok(Some(guard));
@@ -193,11 +193,11 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
.map_err(|err| LockStoreError::BackingStoreError(Box::new(err)))?;
if !acquired {
tracing::trace!("Couldn't acquire the lock immediately.");
trace!("Couldn't acquire the lock immediately.");
return Ok(None);
}
tracing::trace!("Acquired the lock, spawning the lease extension task.");
trace!("Acquired the lock, spawning the lease extension task.");
// This is the first time we've acquired the lock. We're going to spawn the task
// that will renew the lease.
@@ -214,7 +214,10 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
if let Some(_prev) = renew_task.take() {
#[cfg(not(target_arch = "wasm32"))]
_prev.abort();
if !_prev.is_finished() {
info!("aborting the previous renew task");
_prev.abort();
}
}
// Restart a new one.
@@ -234,7 +237,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
// If there are no more users, we can quit.
if this.num_holders.load(atomic::Ordering::SeqCst) == 0 {
tracing::info!("exiting the lease extension loop");
info!("exiting the lease extension loop");
// Cancel the lease with another 0ms lease.
// If we don't get the lock, that's (weird but) fine.
@@ -250,7 +253,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
let fut = this.store.try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder);
if let Err(err) = fut.await {
tracing::error!("error when extending lock lease: {err:#}");
error!("error when extending lock lease: {err:#}");
// Exit the loop.
break;
}
@@ -307,7 +310,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
}
};
tracing::debug!("Waiting {wait} before re-attempting to take the lock");
debug!("Waiting {wait} before re-attempting to take the lock");
sleep(Duration::from_millis(wait.into())).await;
}
}
+109
View File
@@ -1,3 +1,112 @@
# UNRELEASED
Changes:
- Sign the device keys with the user-identity (i.e. cross-signing keys) if
we're uploading the device keys and if the cross-signing keys are available.
This approach eliminates the need to upload signatures in a separate request,
ensuring that other users/devices will never encounter this device without a
signature from their user identity. Consequently, they will never see the
device as unverified.
([#3453](https://github.com/matrix-org/matrix-rust-sdk/pull/3453))
- Avoid emitting entries from `identities_stream_raw` and `devices_stream` when
we receive a `/keys/query` response which shows that no devices changed.
([#3442](https://github.com/matrix-org/matrix-rust-sdk/pull/3442))
- 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 `custom_account` argument to the `OlmMachine::with_store()` method, this
allows users to learn their identity keys before they get access to the user
and device ID.
([#3451](https://github.com/matrix-org/matrix-rust-sdk/pull/3451))
- Add a `backup_version` argument to `CryptoStore`'s
`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))
- Remove deprecated `OlmMachine::import_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
Deprecations:
- Deprecate `BackupMachine::import_backed_up_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
Additions:
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
- Expose new method `OlmMachine::upload_device_keys()`.
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
- Expose new method `CryptoStore::import_room_keys`.
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
- Expose new method `BackupMachine::backup_version`.
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
- Add data types to parse the QR code data for the QR code login defined in
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
- Expose new method `CryptoStore::clear_caches`.
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
- Expose new method `OlmMachine::device_creation_time`.
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
- Log more details about the Olm session after encryption and decryption.
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
- When Olm message decryption fails, report the error code(s) from the failure.
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
- Expose new methods `OlmMachine::set_room_settings` and
`OlmMachine::get_room_settings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Add new properties `session_rotation_period` and
`session_rotation_period_msgs` to `store::RoomSettings`.
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
keys whose metadata lacked check fields.
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
- Add new API `Device::encrypt_event_raw` that allows
to encrypt an event to a specific device.
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
- Add new API `store::Store::export_room_keys_stream` that provides room
keys on demand.
- Include event timestamps on logs from event decryption.
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
## 0.7.1
Security fixes:
- Don't log the private part of the backup key, introduced in [#71136e4](https://github.com/matrix-org/matrix-rust-sdk/commit/71136e44c03c79f80d6d1a2446673bc4d53a2067).
# 0.7.0
- Add method to mark a list of inbound group sessions as backed up:
+12 -2
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.7.1"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -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,14 @@ 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"] }
url = { workspace = true }
ulid = { version = "1.0.0", optional = true }
uniffi = { workspace = true, optional = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -75,6 +81,10 @@ indoc = "2.0.1"
matrix-sdk-test = { workspace = true }
olm-rs = { version = "2.2.0", features = ["serde"] }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
similar-asserts = "1.5.0"
# 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.
+7
View File
@@ -0,0 +1,7 @@
fn main() {
// Prevent unnecessary rerunning of this build script
println!("cargo:rerun-if-changed=build.rs");
// Prevent nightly CI from erroring on tarpaulin_include attribute
println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)");
}
@@ -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.
+98 -21
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]
@@ -159,7 +153,7 @@ impl BackupMachine {
match self.store.static_account().has_signed_raw(signatures, auth_data) {
Ok(_) => SignatureState::ValidAndTrusted,
Err(e) => match e {
crate::SignatureError::NoSignatureFound => SignatureState::Missing,
SignatureError::NoSignatureFound => SignatureState::Missing,
_ => SignatureState::Invalid,
},
}
@@ -185,7 +179,7 @@ impl BackupMachine {
}
}
Err(e) => match e {
crate::SignatureError::NoSignatureFound => SignatureState::Missing,
SignatureError::NoSignatureFound => SignatureState::Missing,
_ => SignatureState::Invalid,
},
}
@@ -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.
@@ -424,6 +419,13 @@ impl BackupMachine {
Ok(())
}
/// Provide the `backup_version` of the current `backup_key`, or None if
/// there is no current key, or the key is not used with any backup
/// version.
pub async fn backup_version(&self) -> Option<String> {
self.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
}
/// Store the backup decryption key in the crypto store.
///
/// This is useful if the client wants to support gossiping of the backup
@@ -482,7 +484,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 +527,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");
@@ -593,6 +600,7 @@ impl BackupMachine {
///
/// Returns a [`RoomKeyImportResult`] containing information about room keys
/// which were imported.
#[deprecated(note = "Use the OlmMachine::store::import_room_keys method instead")]
pub async fn import_backed_up_room_keys(
&self,
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
@@ -612,7 +620,15 @@ impl BackupMachine {
}
}
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
// FIXME: This method is a bit flawed: we have no real idea which backup version
// these keys came from. For example, we might have reset the backup
// since the keys were downloaded. For now, let's assume they came from
// the "current" backup version.
let backup_version = self.backup_version().await;
self.store
.import_room_keys(decrypted_room_keys, backup_version.as_deref(), progress_listener)
.await
}
}
@@ -625,9 +641,12 @@ 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,
olm::BackedUpRoomKey,
store::{BackupDecryptionKey, Changes, CryptoStore, MemoryStore},
types::RoomKeyBackupInfo,
OlmError, OlmMachine,
};
fn room_key() -> BackedUpRoomKey {
@@ -664,7 +683,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 +694,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 +714,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 +727,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 +740,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;
@@ -804,6 +835,12 @@ mod tests {
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
let backup_machine = machine.backup_machine();
// We set up a backup key, so that we can test `backup_machine.backup()` later.
let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
let backup_key = decryption_key.megolm_v1_public_key();
backup_key.set_version("1".to_owned());
backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
let room_key = room_key();
@@ -817,19 +854,29 @@ mod tests {
assert!(session.is_none(), "Initially we should not have the session in the store");
#[allow(deprecated)]
backup_machine
.import_backed_up_room_keys(room_keys, |_, _| {})
.await
.expect("We should be able to import a room key");
// Now check that the session was correctly imported, and that it is marked as
// backed up
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
assert_let!(Some(session) = session);
assert!(
session.backed_up(),
"If a session was imported from a backup, it should be considered to be backed up"
);
assert!(session.has_been_imported());
// Also check that it is not returned by a backup request.
let backup_request =
backup_machine.backup().await.expect("We should be able to create a backup request");
assert!(
backup_request.is_none(),
"If a session was imported from backup, it should not be backed up again."
);
}
#[async_test]
@@ -850,4 +897,34 @@ mod tests {
assert!(result.trusted());
}
#[async_test]
async fn test_fix_backup_key_mismatch() {
let store = MemoryStore::new();
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
store
.save_changes(Changes {
backup_decryption_key: Some(backup_decryption_key.clone()),
backup_version: Some("1".to_owned()),
..Default::default()
})
.await
.unwrap();
// Create the machine using `with_store` and without a call to enable_backup_v1,
// like regenerate_olm would do
let alice =
OlmMachine::with_store(alice_id(), alice_device_id(), store, None).await.unwrap();
let binding = alice.backup_machine().backup_key.read().await;
let machine_backup_key = binding.as_ref().unwrap();
assert_eq!(
machine_backup_key.to_base64(),
backup_decryption_key.megolm_v1_public_key().to_base64(),
"The OlmMachine loaded the wrong backup key."
);
}
}
@@ -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),
}
@@ -74,7 +74,7 @@ pub enum KeyExportError {
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
/// # let export = Cursor::new("".to_owned());
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
/// machine.store().import_room_keys(exported_keys, None, |_, _| {}).await.unwrap();
/// # };
/// ```
pub fn decrypt_room_key_export(
@@ -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.");
@@ -804,7 +848,12 @@ impl ReadOnlyDevice {
}
/// Update a device with a new device keys struct.
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) -> Result<(), SignatureError> {
///
/// Returns `true` if any changes were made to the data.
pub(crate) fn update_device(
&mut self,
device_keys: &DeviceKeys,
) -> Result<bool, SignatureError> {
self.verify_device_keys(device_keys)?;
if self.user_id() != device_keys.user_id || self.device_id() != device_keys.device_id {
@@ -814,10 +863,13 @@ impl ReadOnlyDevice {
self.ed25519_key().map(Box::new),
device_keys.ed25519_key().map(Box::new),
))
} else {
} else if self.inner.as_ref() != device_keys {
self.inner = device_keys.clone().into();
Ok(())
Ok(true)
} else {
// no changes needed
Ok(false)
}
}
@@ -933,10 +985,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};
@@ -1022,9 +1074,11 @@ pub(crate) mod tests {
let mut device_keys = device_keys();
device_keys.unsigned.device_display_name = Some(display_name.clone());
device.update_device(&device_keys).unwrap();
assert!(device.update_device(&device_keys).unwrap());
assert_eq!(&display_name, device.display_name().as_ref().unwrap());
// A second call to `update_device` with the same data should return `false`.
assert!(!device.update_device(&device_keys).unwrap());
}
#[test]
@@ -227,17 +227,18 @@ impl IdentityManager {
store.get_readonly_device(&device_keys.user_id, &device_keys.device_id).await?;
if let Some(mut device) = old_device {
if let Err(e) = device.update_device(&device_keys) {
warn!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
error = ?e,
"Failed to update device keys",
);
Ok(DeviceChange::None)
} else {
Ok(DeviceChange::Updated(device))
match device.update_device(&device_keys) {
Err(e) => {
warn!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
error = ?e,
"Rejecting device update",
);
Ok(DeviceChange::None)
}
Ok(true) => Ok(DeviceChange::Updated(device)),
Ok(false) => Ok(DeviceChange::None),
}
} else {
match ReadOnlyDevice::try_from(&device_keys) {
@@ -279,7 +280,7 @@ impl IdentityManager {
user_id = ?device_keys.user_id,
device_id = ?device_keys.device_id,
error = ?e,
"Failed to create a new device",
"Rejecting a previously unseen device",
);
Ok(DeviceChange::None)
@@ -1014,8 +1015,8 @@ impl IdentityManager {
}
#[cfg(any(test, feature = "testing"))]
#[allow(dead_code)]
pub(crate) mod testing {
#![allow(dead_code)]
use std::sync::Arc;
use ruma::{
@@ -1668,6 +1669,67 @@ pub(crate) mod tests {
assert_eq!(devices.devices().next().unwrap().device_id(), "LVWOVGOXME");
}
#[async_test]
async fn test_invalid_key_response() {
let my_user_id = user_id();
let my_device_id = device_id();
let manager = manager_test_helper(my_user_id, my_device_id).await;
// First of all, populate the store with good data
let (reqid, _) = manager.build_key_query_for_users(vec![user_id()]);
let (device_changes, identity_changes) =
manager.receive_keys_query_response(&reqid, &own_key_query()).await.unwrap();
assert_eq!(device_changes.new.len(), 1);
let test_device_id = device_changes.new.first().unwrap().device_id().to_owned();
use crate::store::Changes;
let changes =
Changes { devices: device_changes, identities: identity_changes, ..Changes::default() };
manager.store.save_changes(changes).await.unwrap();
// Now provide an invalid update
let (reqid, _) = manager.build_key_query_for_users(vec![my_user_id]);
let response_data = response_from_file(&json!({
"device_keys": {
my_user_id: {
test_device_id.as_str(): {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
],
"device_id": test_device_id.as_str(),
"keys": {
format!("curve25519:{}", test_device_id): "wnip2tbJBJxrFayC88NNJpm61TeSNgYcqBH4T9yEDhU",
format!("ed25519:{}", test_device_id): "lQ+eshkhgKoo+qp9Qgnj3OX5PBoWMU5M9zbuEevwYqE"
},
"signatures": {
my_user_id: {
// Not a valid signature.
format!("ed25519:{}", test_device_id): "imadethisup",
}
},
"user_id": my_user_id,
}
}
}
}));
let response =
ruma::api::client::keys::get_keys::v3::Response::try_from_http_response(response_data)
.expect("Can't parse the `/keys/upload` response");
let (device_changes, identity_changes) =
manager.receive_keys_query_response(&reqid, &response).await.unwrap();
// The result should be empty
assert_eq!(device_changes.new.len(), 0);
assert_eq!(device_changes.changed.len(), 0);
assert_eq!(device_changes.deleted.len(), 0);
assert_eq!(identity_changes.new.len(), 0);
// And the device should not have been updated.
let device =
manager.store.get_user_devices(my_user_id).await.unwrap().get(&test_device_id).unwrap();
assert_eq!(device.algorithms().len(), 2);
}
#[async_test]
async fn test_devices_stream() {
let manager = manager_test_helper(user_id(), device_id()).await;
@@ -1723,7 +1785,7 @@ pub(crate) mod tests {
manager.as_ref().unwrap().build_key_query_for_users(vec![user_id()]);
// A second `/keys/query` response with the same result shouldn't fire a change
// notification: the identity should be unchanged.
// notification: the identity and device should be unchanged.
manager
.as_ref()
.unwrap()
@@ -1731,10 +1793,7 @@ pub(crate) mod tests {
.await
.unwrap();
let (identity_update_2, _) = assert_ready!(stream);
assert_eq!(identity_update_2.new.len(), 0);
assert_eq!(identity_update_2.changed.len(), 0);
assert_eq!(identity_update_2.unchanged.len(), 1);
assert_pending!(stream);
// dropping the manager (and hence dropping the store) should close the stream
manager.take();
+29 -24
View File
@@ -148,7 +148,7 @@ impl OwnUserIdentity {
let cache = self.store.cache().await?;
let account = cache.account().await?;
account.sign_master_key(self.master_key.clone())
account.sign_master_key(&self.master_key)
}
/// Send a verification request to our other devices.
@@ -369,8 +369,8 @@ impl ReadOnlyUserIdentities {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReadOnlyUserIdentity {
user_id: OwnedUserId,
pub(crate) master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
pub(crate) master_key: Arc<MasterPubkey>,
self_signing_key: Arc<SelfSigningPubkey>,
}
impl PartialEq for ReadOnlyUserIdentity {
@@ -411,14 +411,19 @@ impl ReadOnlyUserIdentity {
) -> Result<Self, SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
Ok(Self { user_id: master_key.user_id().into(), master_key, self_signing_key })
Ok(Self {
user_id: master_key.user_id().into(),
master_key: master_key.into(),
self_signing_key: self_signing_key.into(),
})
}
#[cfg(test)]
pub(crate) async fn from_private(identity: &crate::olm::PrivateCrossSigningIdentity) -> Self {
let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key.clone();
let master_key =
identity.master_key.lock().await.as_ref().unwrap().public_key().clone().into();
let self_signing_key =
identity.self_signing_key.lock().await.as_ref().unwrap().public_key.clone();
identity.self_signing_key.lock().await.as_ref().unwrap().public_key().clone().into();
Self { user_id: identity.user_id().into(), master_key, self_signing_key }
}
@@ -494,9 +499,9 @@ impl ReadOnlyUserIdentity {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadOnlyOwnUserIdentity {
user_id: OwnedUserId,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
user_signing_key: UserSigningPubkey,
master_key: Arc<MasterPubkey>,
self_signing_key: Arc<SelfSigningPubkey>,
user_signing_key: Arc<UserSigningPubkey>,
#[serde(
serialize_with = "atomic_bool_serializer",
deserialize_with = "atomic_bool_deserializer"
@@ -551,26 +556,26 @@ impl ReadOnlyOwnUserIdentity {
Ok(Self {
user_id: master_key.user_id().into(),
master_key,
self_signing_key,
user_signing_key,
master_key: master_key.into(),
self_signing_key: self_signing_key.into(),
user_signing_key: user_signing_key.into(),
verified: Arc::new(AtomicBool::new(false)),
})
}
#[cfg(test)]
pub(crate) async fn from_private(identity: &crate::olm::PrivateCrossSigningIdentity) -> Self {
let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key.clone();
let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key().clone();
let self_signing_key =
identity.self_signing_key.lock().await.as_ref().unwrap().public_key.clone();
identity.self_signing_key.lock().await.as_ref().unwrap().public_key().clone();
let user_signing_key =
identity.user_signing_key.lock().await.as_ref().unwrap().public_key.clone();
identity.user_signing_key.lock().await.as_ref().unwrap().public_key().clone();
Self {
user_id: identity.user_id().into(),
master_key,
self_signing_key,
user_signing_key,
master_key: master_key.into(),
self_signing_key: self_signing_key.into(),
user_signing_key: user_signing_key.into(),
verified: Arc::new(AtomicBool::new(false)),
}
}
@@ -672,14 +677,14 @@ impl ReadOnlyOwnUserIdentity {
let old = self.clone();
self.self_signing_key = self_signing_key;
self.user_signing_key = user_signing_key;
self.self_signing_key = self_signing_key.into();
self.user_signing_key = user_signing_key.into();
if self.master_key != master_key {
if self.master_key.as_ref() != &master_key {
self.verified.store(false, Ordering::SeqCst);
}
self.master_key = master_key;
self.master_key = master_key.into();
Ok(old != *self)
}
@@ -699,10 +704,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!();
File diff suppressed because it is too large Load Diff
+196 -92
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 {
@@ -378,7 +400,11 @@ pub type OneTimeKeys = BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeK
pub type FallbackKeys = OneTimeKeys;
impl Account {
fn new_helper(mut account: InnerAccount, user_id: &UserId, device_id: &DeviceId) -> Self {
pub(crate) fn new_helper(
mut account: InnerAccount,
user_id: &UserId,
device_id: &DeviceId,
) -> Self {
let identity_keys = account.identity_keys();
// Let's generate some initial one-time keys while we're here. Since we know
@@ -399,11 +425,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 +452,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 +503,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 +532,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 +552,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 +676,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 +731,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,
})
}
@@ -702,7 +789,7 @@ impl Account {
/// Sign the given Master Key
pub fn sign_master_key(
&self,
master_key: MasterPubkey,
master_key: &MasterPubkey,
) -> Result<SignatureUploadRequest, SignatureError> {
let public_key =
master_key.get_first_key().ok_or(SignatureError::MissingSigningKey)?.to_base64().into();
@@ -729,9 +816,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 +987,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 +1024,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 +1058,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 +1221,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 +1319,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 +1330,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 +1457,7 @@ mod tests {
use std::{
collections::{BTreeMap, BTreeSet},
ops::Deref,
time::Duration,
};
use anyhow::Result;
@@ -1427,13 +1502,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 +1529,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 +1540,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 +1552,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(())
}
@@ -98,7 +98,10 @@ pub struct ExportedRoomKey {
}
impl ExportedRoomKey {
pub(crate) fn from_backed_up_room_key(
/// Create an `ExportedRoomKey` from a `BackedUpRoomKey`.
///
/// This can be used when importing the keys from a backup into the store.
pub fn from_backed_up_room_key(
room_id: OwnedRoomId,
session_id: String,
room_key: BackedUpRoomKey,
@@ -136,6 +139,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 +253,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."
);
}
}

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