Compare commits

...

1022 Commits

Author SHA1 Message Date
Damir Jelić 4c46e42201 chore: Release matrix-sdk version 0.10.0 2025-02-04 16:32:55 +01:00
Damir Jelić 0d4bc65e28 chore: Enable releases for the test crates 2025-02-04 16:32:55 +01:00
Jorge Martín 5e1bae02fe feat(ffi): Add RoomPreview::forget action in the FFI layer 2025-02-04 16:26:15 +01:00
Ivan Enderlin 77a67de7df fix(ui): Fix performance of ReadReceiptTimelineUpdate::apply.
This patch improves the performance of
`ReadReceiptTimelineUpdate::apply`, which does 2 things: it calls
`remove_old_receipt` and `add_new_receipt`. Both of them need an
timeline item position. Until this patch, `rfind_event_by_id` was used
and was the bottleneck. The improvement is twofold as is as follows.

First off, when collecting data to create `ReadReceiptTimelineUpdate`,
the timeline item position can be known ahead of time by using
`EventMeta::timeline_item_index`. This data is not always available, for
example if the timeline item isn't created yet. But let's try to collect
these data if there are some.

Second, inside `ReadReceiptTimelineUpdate::remove_old_receipt`, we use
the timeline item position collected from `EventMeta` if it exists.
Otherwise, let's fallback to a similar `rfind_event_by_id` pattern,
without using intermediate types. It's more straightforward here: we
don't need an `EventTimelineItemWithId`, we only need the position.
Once the position is known, it is stored in `Self` (!), this is the
biggest improvement here. Le't see why.

Finally, inside `ReadReceiptTimelineUpdate::add_new_receipt`, we use
the timeline item position collected from `EventMeta` if it exists,
similarly to what `remove_old_receipt` does. Otherwise, let's fallback
to an iterator to find the position. However, instead of iterating over
**all** items, we can skip the first ones, up to the position of the
timeline item holding the old receipt, so up to the position found by
`remove_old_receipt`.

I'm testing this patch with the `test_lazy_back_pagination` test in
https://github.com/matrix-org/matrix-rust-sdk/pull/4594. With 10_000
events in the sync, the `ReadReceipts::maybe_update_read_receipt` method
was taking 52% of the whole execution time. With this patch, it takes
8.1%.
2025-02-04 16:02:29 +01:00
JoFrost f27eb4d1c8 fix[oidc]: fix docstring in oidc module 2025-02-04 15:33:28 +01:00
Jorge Martín 05814c5559 refactor(ffi): Map client API errors to ClientError::MatrixApi, containing the error kind, their error code and the associated message 2025-02-04 12:25:51 +01:00
Kévin Commaille d5d9898fb4 feat: Upgrade Ruma to 0.12.1
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-04 12:00:40 +01:00
Ivan Enderlin 3f71d9a379 fix(sdk): Improve performance of RoomEvents::maybe_apply_new_redaction.
This patch improves the performance of
`RoomEvents::maybe_apply_new_redaction`. This method deserialises
all the events it receives, entirely. If the event is not an
`m.room.redaction`, then the method returns early. Most of the time,
the event is deserialised for nothing because most events aren't of kind
`m.room.redaction`!

This patch first uses `Raw::get_field("type")` to detect the type of
the event. If it's a `m.room.redaction`, then the event is entirely
deserialized, otherwise the method returns.

When running the `test_lazy_back_pagination` from
https://github.com/matrix-org/matrix-rust-sdk/pull/4594 with 10'000
events, prior to this patch, this method takes 11% of the execution
time. With this patch, this method takes 2.5%.
2025-02-04 09:49:58 +01:00
Jorge Martín b077f45e78 test(room_preview): Add tests for where get_room_preview gets its data from for each room state 2025-02-04 09:33:31 +01:00
Jorge Martín 648d527f2f fix(room_preview): Return room preview info based on local data for banned rooms too
Any remote endpoint would just return a `403` status code so we have no other choice than trusting the local room info we already have.
2025-02-04 09:33:31 +01:00
Jorge Martín 8513547e92 feat(ffi): Add FFI bindings for Room::forget.
Also make sure rooms the user has been banned from can also be forgotten, not only left ones.
2025-02-03 19:48:27 +01:00
dependabot[bot] d18669e8d9 chore(deps): bump crate-ci/typos from 1.29.4 to 1.29.5
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.29.4 to 1.29.5.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.29.4...v1.29.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 16:52:27 +01:00
Benjamin Bouvier a0426251a3 test(timeline): test that editing a replied-to doesn't lose the latest edit JSON 2025-02-03 16:52:12 +01:00
Benjamin Bouvier 2739c5bf27 test(timeline): test that adding a response or ending a poll doesn't clear the latest edit JSON 2025-02-03 16:52:12 +01:00
Benjamin Bouvier 381f4d419f fix(timeline): don't clear the latest_edit_json under certain conditions 2025-02-03 16:52:12 +01:00
Ivan Enderlin 9ab5547065 fix(ui): Fix performance of AllRemoteEvents::(in|de)crement_all_timeline_item_index_after.
This patch fixes the performance of
`AllRemoteEvents::increment_all_timeline_item_index_after` and
`decrment_all_timeline_item_index_after`.

It appears that the code was previously iterating over all items. This
is a waste of time. This patch updates the code to iterate over all
items in reverse order:

- if `new_timeline_item_index` is 0, we need to shift all items anyways,
  so all items must be traversed, the iterator direction doesn't matter,
- otherwise, it's unlikely we want to traverse all items: the item has
  been either inserted or pushed back, so there is no need to traverse
  the first items; we can also break the iteration as soon as all
  timeline item index after `new_timeline_item_index` has been updated.

I'm testing this patch with the `test_lazy_back_pagination` test in
https://github.com/matrix-org/matrix-rust-sdk/pull/4594. With 10_000
events in the sync, the `ObservableItems::push_back` method (that uses
`AllRemoteEvents::increment_all_timeline_item_index_after`) was taking
7% of the whole execution time. With this patch, it takes 0.7%.
2025-02-03 11:25:48 +01:00
Kévin Commaille df3cb002a5 chore: Add changelog entries
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:22:23 +01:00
Kévin Commaille 6ebd4295b9 feat(sqlite): Limit size of WAL file
The WAL file can grow depending on the transactions that are run. A
critical case is VACUUM which basically writes the content of the DB
file to the WAL file before writing it back to the DB file.

SQLite doesn't try to reduce the size of the file after that unless we
set an explicit limit,
so we could end up taking twice the size of the database on the
filesystem.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:22:23 +01:00
Kévin Commaille c5104d68fd feat(sqlite): Run PRAGMA optimize regularly
As recommended by the SQLite docs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:22:23 +01:00
Kévin Commaille 0064839283 fix(sqlite): Vaccum the SqliteStateStore
It should have been done in the migration of version 7, to reduce the
size of the database on the filesystem after the media cache was moved
to the SqliteEventCacheStore. Better late than never.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-02-03 11:22:23 +01:00
Kévin Commaille 2727d72916 fix(timeline): Do not filter out own receipts in load_read_receipts_for_event
Fixes #4517.

It turns out that the bugs found in that test were due to 2 causes:

- First commit: `TestRoomDataProvider` didn't use `initial_user_receipts` but returned hardcoded values.
- Second commit: Our own read receipts were ignored in `TimelineStateTransaction::load_read_receipts_for_event`, although we need to process all read receipts via `ReadReceipts::maybe_update_read_receipt` because it knows how to filter out our own read receipts were needed.
2025-02-03 10:15:25 +00:00
Ivan Enderlin 33a2cc3031 chore(cargo): Bump the minimum stable rust version (MSRV). 2025-02-03 10:27:45 +01:00
Ivan Enderlin 38097f90b2 fix(ui): Fix performance of TimelineEventHandler::deduplicate_local_timeline_item.
This patch drastically improves the performance of
`TimelineEventHandler::deduplicate_local_timeline_item`.

Before this patch, `rfind_event_item` was used to iterate over all
timeline items: for each item in reverse order, if it was an event
timeline item, and if it was a local event timeline item, and if it was
matching the event ID or transaction ID, then a duplicate was found.

The problem is the following: all items are traversed.

However, local event timeline items are always at the back of the items.
Even virtual timeline items are before local event timeline items. Thus,
it is not necessary to traverse all items. It is possible to stop the
iteration as soon as (i) a non event timeline item is met, or (ii) a non
local event timeline item is met.

This patch updates
`TimelineEventHandler::deduplicate_local_timeline_item` to replace to
use of `rfind_event_item` by a custom iterator that stops as soon as a
non event timeline item, or a non local event timeline item, is met, or
—of course— when a local event timeline item is a duplicate.

To do so, [`Iterator::try_fold`] is probably the best companion.
[`Iterator::try_find`] would have been nice, but it is available on
nightlies, not on stable versions of Rust. However, many methods in
`Iterator` are using `try_fold`, like `find` or any other methods that
need to do a “short-circuit”. Anyway, `try_fold` works pretty nice here,
and does exactly what we need.

Our use of `try_fold` is to return a `ControlFlow<Option<(usize,
TimelineItem)>, ()>`. After `try_fold`, we call
`ControlFlow::break_value`, which returns an `Option`. Hence the need
to call `Option::flatten` at the end to get a single `Option` instead of
having an `Option<Option<(usize, TimelineItem)>>`.

I'm testing this patch with the `test_lazy_back_pagination` test in
https://github.com/matrix-org/matrix-rust-sdk/pull/4594. With 10_000
events in the sync, the test was taking 13s to run on my machine. With
this patch, it takes 10s to run. It's a 23% improvement. This
`deduplicate_local_timeline_item` method was taking a large part of the
computation according to the profiler. With this patch, this method is
barely visible in the profiler it is so small.

[`Iterator::try_fold`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.try_fold
[`Iterator::try_find`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.try_find
2025-02-03 10:27:45 +01:00
Ivan Enderlin 47d08683a2 fix(security): Update OpenSSL.
See this note https://rustsec.org/advisories/RUSTSEC-2025-0004.

This patch updates OpenSSL to 0.10.70.
2025-02-03 09:42:58 +01:00
Damir Jelić 57919f5480 chore: Bump most of our deps 2025-01-31 17:14:37 +01:00
Damir Jelić b8949cfe26 chore: Bump vodozemac 2025-01-31 17:14:37 +01:00
Damir Jelić 8d27b0c811 test: Simplify some tests using the assert_next_with_timeout macro 2025-01-31 14:15:18 +01:00
Damir Jelić 3707d2fb81 test: Make the timeout parameter in the assert_next_with_timeout macro optional 2025-01-31 14:15:18 +01:00
Damir Jelić eaaa5e17a0 chore: Fix a doc example in the MatrixMockServer 2025-01-31 14:15:18 +01:00
Ivan Enderlin e3958b754c chore(crypto-ffi): Done is a unit type, no need for { .. }. 2025-01-31 14:07:43 +01:00
Ivan Enderlin 78d9e1292f chore(sdk): Do not iterate over the entire iterator when we can reach back.
This patch uses `next_back()` instead of `last()`, which is equivalent
but `last()` requires to iterate over the entire iterator, while
`next_back()` is a single operation.
2025-01-31 14:07:43 +01:00
Ivan Enderlin d594b4dad7 chore(sdk): Remove a useless type conversion.
This patch removes a useless type conversion. The `Room::event()` method
returns a `TimelineEvent`, so calling `Into::into` is useless: we map
`TimelineEvent` to `TimelineEvent`.
2025-01-31 14:07:43 +01:00
Ivan Enderlin 3f40ad83a5 chore(sdk): Remove a useless type conversion.
This patch removes a useless type conversion. The iterator produces
`TimelineEvent`, so mapping to `TimelineEvent::from` is useless: we map
`TimelineEvent` to `TimelineEvent`.
2025-01-31 14:07:43 +01:00
Ivan Enderlin 5049d1a3b6 chore(sqlite): Use repeat_n(…, n) instead of repeat(…).take(n).
Thanks Clippy!
2025-01-31 14:07:43 +01:00
Damir Jelić 29862fc9bd refactor: Add the assert_next_eq_with_timeout test helper
This test helper is the same as the assert_next_eq helper, but it waits
for the stream to be ready for a certain amount of time instead of
expecting it to be ready right away.
2025-01-31 09:58:55 +01:00
Damir Jelić 585224b2fa chore(ui): Replace the unit type with an empty block 2025-01-31 09:58:55 +01:00
Damir Jelić 0dc5e69ace fix: Retry the sync even in case of network errors 2025-01-31 09:58:55 +01:00
Damir Jelić b323802ab0 fix(ui): Enable retries for network failures of the /versions check in the SyncService 2025-01-31 09:58:55 +01:00
Damir Jelić 252786d2ef refactor(ui): Make SyncService::stop infallible
The `SyncService::stop()` method could fail for the following reasons:

1. The supervisor was not properly started up, this is a programmer error.
2. The supervisor task wouldn't shut down and instead it returns a JoinError.
3. We couldn't notify the supervisor task that it should shutdown due the channel being closed.

All of those cases shouldn't ever happen and the supervisor task will be
stopped in all of them.

1. Since there is no supervisor to be stopped, we can safely just log an
   error, our tests ensure that a `SyncService::start()` does create a
   supervisor.

2. A JoinError can be returned if the task has been cancelled or if the
   supervisor task has panicked. Since we never cancel the task, nor
   have any panics in the supervisor task, we can assume that this won't
   happen.

3. The supervisor task holds on to a reference to the receiving end of
   the channel, as long as the task is alive the channel can not be
   closed.

In conclusion, it doesn't seem to be useful to forward these error cases
to the user.
2025-01-31 09:58:55 +01:00
Damir Jelić 97cbe57d3f docs: Document the offline mode for the SyncService 2025-01-31 09:58:55 +01:00
Damir Jelić 0d8ad159c3 test(ui): Write tests for the SyncService offline mode 2025-01-31 09:58:55 +01:00
Damir Jelić 9d732395ce feat(ui): Introduce a "offline" mode for the SyncService 2025-01-31 09:58:55 +01:00
Damir Jelić 6a772d1c56 test: Add a method to mock the /versions endpoint to the MatrixMockServer 2025-01-31 09:58:55 +01:00
Damir Jelić b71499ffe6 refactor(ui): Move the start/stop implementations under the inner SyncService
This will allow us to more easily implement a restart method.
2025-01-31 09:58:55 +01:00
Damir Jelić 4dadf8581a refactor(ui): Move the creation of the child tasks in the SyncService around
This patch moves the creations of the child tasks of the SyncService
into the supervisor tasks itself. This should make it easier to let the
supervisor recreate tasks.

This will become useful once we introduce a offline mode where the
supervisor task becomes responsible to restart syncing once we notice
that the server is back online.
2025-01-31 09:58:55 +01:00
Benjamin Bouvier 4d4cd61363 chore: update copyright years for files newly introduced 2025-01-31 08:50:41 +01:00
Richard van der Hoff 6f42b0a67b crypto: withhold outgoing messages to unsigned dehydrated devices
Per https://github.com/matrix-org/matrix-rust-sdk/issues/4313, we should not
send outgoing messages to dehydrated devices that are not signed by the current
pinned/verified identity.
2025-01-29 17:37:36 +00:00
Richard van der Hoff e3b348761e test: test helpers in share_strategy tests
Split the existing `set_up_test_machine` into two parts, so we can set up
the test OlmMachine without importing data for other users
2025-01-29 17:37:36 +00:00
Ivan Enderlin d755a8a3aa chore(ui): Move EventMeta inside the metadata module.
This patch moves the `EventMeta` type from `state.rs` to `metadata.rs`.
2025-01-29 11:44:27 +01:00
Ivan Enderlin 66e3ddec47 chore(ui): Move TimelineMetadata into its own module.
This patch moves `TimelineMetadata`, its implementation and companion
types (like `RelativePosition`) into its own module. The idea is to
reduce the size of the `state.rs` module.
2025-01-29 11:44:27 +01:00
Ivan Enderlin 720d443452 chore(ui): Move TimelineStateTransaction into its own module.
This patch moves `TimelineStateTransaction` and its implementation into
its own module. The idea is to reduce the size of the `state.rs` module.
2025-01-29 11:44:27 +01:00
Ivan Enderlin 33d691a58e Merge pull request #4571 from zecakeh/media-retention-policy
feat: Add MediaRetentionPolicy to the EventCacheStore, take 2
2025-01-28 17:27:02 +01:00
Kévin Commaille c2f39c1086 Merge branch 'main' into media-retention-policy 2025-01-28 16:53:52 +01:00
Kévin Commaille df9c355aed Merge branch 'main' into media-retention-policy
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:46:55 +01:00
Kévin Commaille 0334ff3f64 chore(sdk): Add changelog entry for MediaRetentionPolicy
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:44 +01:00
Kévin Commaille 8262726369 chore(sqlite): Add changelog entry for EventCacheStore changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:44 +01:00
Kévin Commaille a4a6bf540d chore(base): Add changelog entry for MediaRetentionPolicy and associated changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:44 +01:00
Kévin Commaille 9b38f38aea fix(base): Organize changelog the same way as other crates
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:44 +01:00
Kévin Commaille 2e05cc74bf feat(sdk): Add methods to Media to interact with MediaRetentionPolicy
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:44 +01:00
Kévin Commaille eb9b86971a feat(base): Add methods for MediaRetentionPolicy to EventCacheStore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:44:42 +01:00
Ivan Enderlin 31e7ec182c chore(ui): Remove TimelineNewItemPosition.
This patch removes the `TimelineNewItemPosition` type a sit is no longer
used.
2025-01-28 15:43:06 +01:00
Ivan Enderlin 5e8f3b2bc8 chore(ui): Remove HandleManyEventsResult.
This patch removes the `HandleManyEventsResult` type as it is no longer
used.
2025-01-28 15:43:06 +01:00
Ivan Enderlin c0b91c4b0e chore(ui): Remove TimelineState(Transaction)::add_remote_events_at.
This patch removes `TimelineState::add_remote_events_at` and
`TimelineStateTransaction::add_remote_events_at` as they are no longer
used.
2025-01-28 15:43:06 +01:00
Ivan Enderlin 46d90afa9c refactor(ui): Use handle_remote_events_with_diffs instead of add_remote_events.
This patch replaces a call to `add_remote_events` by
`handle_remote_events_with_diffs`.

The idea is to remove all calls to `add_remote_events`.
2025-01-28 15:43:06 +01:00
Ivan Enderlin 29e19b729b chore(ui): Remove TimelineController::add_events_at.
This patch removes `TimelineController::add_events_at` since it's a
non-public method and it's unused.
2025-01-28 15:43:06 +01:00
Ivan Enderlin 20c1eff391 refactor(ui): The Timeline no longer use add_events_at.
This patch replaces all uses of `TimelineController::add_events_at` by
`TimelineController::handle_remote_events_with_diffs` in the `Timeline`
itself.

The idea is to remove `add_events_at`, as we currently have 2 ways to
add or to handle remote events in the `Timeline`. We want a single one,
because it's simpler!
2025-01-28 15:43:06 +01:00
Ivan Enderlin 3e40db3d7f test(ui): Tests no longer use add_events_at.
This patch replaces all uses of `TimelineController::add_events_at` by
`TimelineController::handle_remote_events_with_diffs` in the tests.

The idea is to remove `add_events_at`, as we currently have 2 ways to
add or to handle remote events in the `Timeline`. We want a single one,
because it's simpler!
2025-01-28 15:43:06 +01:00
Kévin Commaille 8ca5983093 feat(sqlite): Implement EventCacheStoreMedia for SqliteEventCacheStore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:38:18 +01:00
Kévin Commaille 144f568a5c feat(base): Implement EventCacheStoreMedia for MemoryStore
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:37:30 +01:00
Kévin Commaille 34c7dd48ae refactor(base): Use struct for media content in the MemoryStore
It is already a 3-tuple and we want to add more data so it will be clearer to use a stuct with named fields.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:36:16 +01:00
Kévin Commaille 6c7d8c16bb feat(base): Add macro for integration tests of EventCacheStoreMedia
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:36:16 +01:00
Kévin Commaille 2c930df8aa feat(base): Add MediaService
This is an API to handle the MediaRetentionPolicy with a lower level
EventCacheStoreMedia trait.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:36:06 +01:00
Kévin Commaille 834bed2b1a feat(base): Add MediaRetentionPolicy
This will be used as a configuration to decide whether or not to keep
media in the cache, allowing to do periodic cleanups to avoid to have
the size of the media cache grow indefinitely.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-28 15:35:26 +01:00
Valere 8d530ef220 Merge pull request #4558 from matrix-org/valere/memory_store_consistent_with_other_stores
refactor(crypto): Make memory store behave more like other stores
2025-01-28 15:27:03 +01:00
Valere 542d68dcda fix(test): Properly set up test crypto memory store by saving own device 2025-01-28 15:02:30 +01:00
Valere 50696a0d74 refact(mem_store) Add a global save change lock similar to other stores 2025-01-28 15:02:30 +01:00
Valere 182fc6fd8f refact(mem_store): Ser/Deser inbound group sessions 2025-01-28 15:02:30 +01:00
Valere fe85cddf88 refact(mem_store): Serialize/Deserialise olm sessions 2025-01-28 15:02:30 +01:00
Valere 9ff3761cac refact(mem_store): Serialize account and cache static account data 2025-01-28 15:02:30 +01:00
Valere a311dcbd3e refact(mem_store): Replace unwrap with expect 2025-01-28 15:02:30 +01:00
Andy Balaam 447bd67fe1 feat(crypto): Ignore to-device messages from dehydrated devices 2025-01-28 12:57:30 +00:00
Andy Balaam d8ba2b521c refactor(crypto): extract a method from a test that I will re-use later 2025-01-28 12:57:30 +00:00
Damir Jelić 8de15429fb chore: Fix a typo 2025-01-28 12:48:55 +01:00
Damir Jelić 3f398d8934 chore(ui): Move the SyncService stop logic out of the State::Running branch 2025-01-28 12:48:55 +01:00
Damir Jelić f8ec957193 doc(ui): Reword the doc comment for the is_supervisor_task_running method 2025-01-28 12:48:55 +01:00
Damir Jelić 7cc121ab38 docs(ui): Clarify that the supervisor encapsulates the child tasks in its own task 2025-01-28 12:48:55 +01:00
Damir Jelić 8a4918309a refactor(ui): Rename the abortion sender in the SyncService
Termination aligns better with the existing terminology.
2025-01-28 12:48:55 +01:00
Damir Jelić 30d7fac927 refactor(ui): Remove some unneeded references from the SyncServiceInner 2025-01-28 12:48:55 +01:00
Damir Jelić be71c6df56 docs: Document that the SyncService requires MSC4186 2025-01-28 12:48:55 +01:00
Damir Jelić 06ad67f99c docs(ui): Polish the documentation of the SyncService a bit 2025-01-28 12:48:55 +01:00
Damir Jelić 28fb6f7c27 fix(ui): Shutdown the child tasks if the channel got closed in the supervisor 2025-01-28 12:48:55 +01:00
Damir Jelić 842d32d41b refactor(ui): Prettify the two sync tasks a bit 2025-01-28 12:48:55 +01:00
Damir Jelić b52cf8327a refactor(ui): Remove some early returns from the sync service
Now that the various match branches in the start and stop method of the
SyncService are minimized we can remove the early returns.

This should allow us to more easily add new branches.
2025-01-28 12:48:55 +01:00
Damir Jelić d14526f161 refactor(ui): Move the task spawning functions under the supervisor 2025-01-28 12:48:55 +01:00
Damir Jelić 1b8a6b705c refactor(ui): Move the expiration of the sync services closer to the action 2025-01-28 12:48:55 +01:00
Damir Jelić 7c2b15fe86 refactor(ui): Move the spawning of the child tasks into the supervisor 2025-01-28 12:48:55 +01:00
Damir Jelić 3085f05d51 refactor(ui): Create a Supervisor for the SyncService
The supervisor is defined as two optional fields that are set and
removed at the same time.

This patch converts the two optional fields into a single optional
struct. The fields inside the struct now aren't anymore optional. This
ensures that they are always set and destroyed at the same time.
2025-01-28 12:48:55 +01:00
Damir Jelić 4344e06707 refactor(ui): Rename the scheduler task to supervisor task
From cambridge a scheduler is defined as:
    > someone whose job is to create or work with schedules

While supervisor is defined as:
    > a person whose job is to supervise someone or something

Well ok, that doesn't tell us much, supervise is defined as:
    > to watch a person or activity to make certain that everything is done correctly, safely, etc.:

In conclusion, supervising a task is the more common and better
understood terminology here I would say.
2025-01-28 12:48:55 +01:00
Damir Jelić 173ec75bb3 refactor(ui): Move common data of the SyncService under a lock
Previously we had a lock protecting an empty value, but the logic wants
to protect a bunch of data in the SyncService.

Let's do the usual thing and create a SyncServiceInner which holds the
data and protect that with a lock.
2025-01-28 12:48:55 +01:00
Ivan Enderlin 1d3f8bf898 doc(ui): Update the CHANGELOG. 2025-01-28 09:54:31 +01:00
Ivan Enderlin 5b3b87d3e2 chore(ui): Rename Timeline::subscribe_batched to ::subscribe.
This patch renames `Timeline::subscribe_batched` to
`Timeline::subscribe`. Since the `Timeline::subscribe` method has been
removed because unused, it no longer makes sense to have a “batched”
variant here. Let's simplify things!
2025-01-28 09:54:31 +01:00
Ivan Enderlin 6dc5b33d87 chore(ui): Remove useless trace!.
This patch removes useless `trace!` calls.
2025-01-28 09:54:31 +01:00
Richard van der Hoff 408b843156 test: fix cross-signing in legacy dehydrated device test
We missed a call to `sign_device_keys`. Pull out a test helper to make it more
obvious where this code is coming from.
2025-01-27 17:49:47 +00:00
Ivan Enderlin 0820170261 doc(ui): Update the CHANGELOG. 2025-01-27 17:02:09 +01:00
Ivan Enderlin 254ac6f2ce refactor(ui): Unify the Timeline pagination API.
This patch simplifies the `Timeline` pagination API as follows:

- a unique `paginate_backwards` method (no more
  `focused_paginate_backwards` and `live_paginate_backwards`),
- a unique `paginate_forwards` method (no more
  `focused_paginate_forwards`, the `live` variant was absent).

The idea is to unify pagination by hiding the `live` and `focused` mode.
It was already partially the case with `paginate_backards`, but the
`live` and `focused` variants were also present. I believe it creates
an unnecessary confusion.
2025-01-27 17:02:09 +01:00
Ivan Enderlin 468a7ac883 doc(ffi): Remove useless #[cfg(doc)] imports.
This patch removes 2 useless imports that are behind a `#[cfg(doc)]` but
never used.
2025-01-27 17:02:09 +01:00
Richard van der Hoff 3e610c80e1 Merge pull request #4581 from matrix-org/rav/refactor_share_keys
crypto: refactor the room key sharing strategies
2025-01-27 15:51:04 +00:00
Richard van der Hoff f43edbd31f refactor(crypto): split up split_devices_for_user_for_error_on_verified_user_problem_strategy
to make it easier to grok, I hope
2025-01-27 15:34:43 +00:00
Richard van der Hoff 7c57f2cee4 crypto: split out new device collection strategies
Rather than a bunch of flags on `DeviceBasedStrategy`, separate the strategies
properly.
2025-01-27 15:34:43 +00:00
Richard van der Hoff 8d612eca46 crypto: break up split_devices_for_user
handle the separate flags with separate methods.
2025-01-27 15:34:43 +00:00
Richard van der Hoff 98f4d55aa0 test: check serialization format of DeviceBasedStrategy 2025-01-27 15:34:43 +00:00
Richard van der Hoff 709b09c4ec test: factor out test helpers in share_strategy tests
Some helpers for creating common `EncryptionSettings`
2025-01-27 15:34:43 +00:00
Richard van der Hoff 818876a22e crypto: factor out common code between device collection cases
Firstly, build a `CollectRecipientsResult` as we go, rather than building its
components separately and then assembling it at the end.

Then, factor the common code between the two code paths into a method to update
the `CollectRecipientsResult`.
2025-01-27 15:34:43 +00:00
Stefan Ceriu 2657eb7866 feat(ui): expose a method for checking whether a message contains only emojis and should be boosted (use a bigger font size) (#4577)
- supports only text room message types
- enumerates through their body's grapheme clusters and check that every
single one of them is an emoji
- part of the `LazyTimelineItemProvider` so that it can be opt in
2025-01-27 14:00:01 +00:00
torrybr aaecbf07f2 refactor: dont panic if beacon_info is not found 2025-01-27 11:05:24 +01:00
torrybr f336638a17 refactor: move subscribe into arc 2025-01-27 11:05:24 +01:00
torrybr 839fbe477c feat(beacons): expose ffi functions to start, stop and subscribe 2025-01-27 11:05:24 +01:00
Richard van der Hoff 35ad5441d3 crypto: split common struct out of device collection results
`split_devices_for_user` returns a superset of the results of
`split_recipients_withhelds_for_user_based_on_identity`: let's reflect that in
the return types so we can start to share code.

Also, rename `split_recipients_withhelds_for_user_based_on_identity` to
`split_devices_for_user_for_identity_based_strategy` while we are here.
2025-01-26 22:50:15 +00:00
Kevin Boos 756dec264d Upgrade imbl, eyeball-im, eyeball-im-util to fix bounds check
A bounds check was recently relaxed in `imbl`'s `Focus::narrow()`
function: https://github.com/jneem/imbl/pull/89,
which fixed a bug that would cause a panic if the downstream user
of `matrix-sdk-ui` attempted to narrow a focus of Timeline items
using a range that included the last item in the Timeline.
Example: https://github.com/project-robius/robrix/issues/330

This fix has been incorporated in `eyeball-im` and `eyeball-im-util`
and has been tested by me to no longer trigger upon the aforementioned
conditions.
2025-01-25 02:13:22 -05:00
Doug 87983ab610 chore: Remove an old todo
This was already done by moving the methods into Client.
2025-01-24 18:06:05 +01:00
Neil Johnson 66ffc3448e Update README.md
style

Signed-off-by: Neil Johnson <neil@matrix.org>
2025-01-24 11:20:20 +01:00
Neil Johnson c6e308717d update maturity and contribution call to action 2025-01-24 10:56:05 +01:00
maan2003 4c4dd03411 fix(wasm): don't use tokio::time::{timeout,sleep} (#4573)
Tokio timeout and sleep don't work on wasm so provide alternative versions

---------

Signed-off-by: Manmeet Singh <manmeetmann2003@gmail.com>
Signed-off-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-01-23 08:57:11 +00:00
Benjamin Bouvier 2d0f873342 refactor: send respects to multiple automated lints and checks 2025-01-22 20:24:48 +01:00
Benjamin Bouvier 041627ec4a test: rename EventFactory::into_sync to into_event 2025-01-22 20:24:48 +01:00
Benjamin Bouvier da4b8004f2 docs: add changelog entries 2025-01-22 20:24:48 +01:00
Benjamin Bouvier 3428494468 refactor: rename SyncTimelineEvent to TimelineEvent 2025-01-22 20:24:48 +01:00
Benjamin Bouvier 0c2046f93b refactor: have SyncTimelineEvent::push_actions be optional 2025-01-22 20:24:48 +01:00
Benjamin Bouvier eb31f035e6 refactor: turn TimelineEvent into SyncTimelineEvent
As the comment noted, they're essentially doing the same thing. A
`TimelineEvent` may not have computed push actions, and in that regard
it seemed more correct than `SyncTimelineEvent`, so another commit will
make the field optional.
2025-01-22 20:24:48 +01:00
Kévin Commaille df51404a14 chore(sdk): Add changelog for move of matrix_auth and oidc
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-22 20:22:13 +01:00
Kévin Commaille 3e78e441d4 refactor(sdk): Move oidc module to authentication::oidc
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-22 20:22:13 +01:00
Kévin Commaille 02c2e55855 refactor(sdk): Move matrix_auth module to authentication::matrix
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-22 20:22:13 +01:00
Stefan Ceriu a528624274 chore(ffi): replace all the different timeline builder methods with one taking a configuration (#4561) 2025-01-22 13:56:53 +02:00
Ivan Enderlin 1d83d42e9f chore(ui): Remove Timeline::subscribe.
This patch removes `Timeline::subscribe`. There is
`Timeline::subscribe_batched`, which is the only useful API.
`subscribe` was only used by our tests, and `matrix-sdk-ffi` uses
`subscribe_batched` only.
2025-01-22 11:55:23 +01:00
Ivan Enderlin 4684cfb780 chore: Replace Timeline::subscribe by Timeline::subscribe_batched.
This patch changes all calls to `Timeline::subscribe` to replace them by
`Timeline::subscribe_batched`. Most of them are in tests. It's the first
step of a plan to remove `Timeline::subscribe`.

The rest of the patch updates all the tests to use
`Timeline::subscribe_batched`.
2025-01-22 11:55:23 +01:00
Stefan Ceriu 991c9ad610 chore(ci): simplify formatting checks by using xtask instead 2025-01-22 12:20:47 +02:00
Hubert Chathi e826c54a42 Use the dehydrated device format implemented by vodozemac (#4421)
Signed-off-by: Hubert Chathi <hubertc@matrix.org>
2025-01-22 09:38:48 +01:00
Kévin Commaille 9ae658c1b9 feat(sdk): Enable HTTP/2 support
It became an optional default feature in reqwest 0.12, and we disable
the default features,
so I don't think it was meant to be disabled when the crate was
upgraded.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-22 09:08:00 +01:00
Benjamin Bouvier 4341aaf65c test: remove some uses of sync_timeline_event! in the base and sdk crates (#4565)
Part of #3716.
2025-01-21 14:33:22 +00:00
Jorge Martín ad847a82c8 refactor: Remove TestHelper in pinned_events.rs
Use helper functions instead.
2025-01-21 12:56:19 +01:00
Jorge Martín dad3e6839f test: update the code in pinned_events integration tests
This is done so the tests there use the new APIs based on `MatrixMockServer`.
2025-01-21 12:56:19 +01:00
Jorge Martín d078ef6155 fix: fix mock_room_event not being able to properly filter out event ids since the regex was incorrectly parsed by wiremock 2025-01-21 12:56:19 +01:00
Benjamin Bouvier 210c5749f1 test: minimize usage of EventFactory::state_key
It was used in places where we could make use of other helpers, in some
cases. Also introduces the `room_avatar` helper to create the room
avatar state event.
2025-01-21 10:50:29 +01:00
Benjamin Bouvier 0c74abbc50 test: get rid of EventBuilder (#4560)
This gets rid of `EventBuilder`, and makes more usage of the
`EventFactory`, which is more ergonomic to create test events.

A large part of
https://github.com/matrix-org/matrix-rust-sdk/issues/3716.
2025-01-21 09:23:03 +00:00
Jorge Martín dbadfe19b0 fix(timeline): avoid adding non-pinned events to the pinned events timeline when back paginating 2025-01-20 17:38:50 +01:00
dependabot[bot] f3e43dbfa4 chore(deps): bump malinskiy/action-android/install-sdk@release/0.1.4
Bumps [malinskiy/action-android/install-sdk@release/0.1.4](https://github.com/malinskiy/action-android) from 0.1.4 to 0.1.7.
- [Release notes](https://github.com/malinskiy/action-android/releases)
- [Commits](https://github.com/malinskiy/action-android/compare/release/0.1.4...release/0.1.7)

---
updated-dependencies:
- dependency-name: malinskiy/action-android/install-sdk@release/0.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 16:22:38 +01:00
dependabot[bot] b846a6dd81 chore(deps): bump crate-ci/typos from 1.28.3 to 1.29.4
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.3 to 1.29.4.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.3...v1.29.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 16:22:31 +01:00
Ivan Enderlin c82e469fc3 chore(cargo): Update Ruma to include https://github.com/ruma/ruma/pull/1995. 2025-01-20 15:02:53 +01:00
Kévin Commaille f2c9a8f723 fix(ui): Fix latest_edit_json for live edits
This was a regression introduced in
f0d98602a9.

`latest_edit_json` was first set by the call to
`EventTimelineItem::with_content()`.
It was overwritten in the next section because of the
`if let EventTimelineItemKind::Remote(remote_event)`
that uses `item` instead of `new_item`.
It means that the updated `RemoteEventTimelineItem`
inside`new_item` was replaced by the outdated one inside `item`,
so `latest_edit_json` goes back to its previous value.

I believe that part of why that went unnoticed is that the code looks
more
complicated due to the need to set an inner field, so I decided to
change the API and
move `with_encryption_info` to `EventTimelineItem`, which makes the code
look cleaner.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-20 13:07:13 +01:00
torrybr 47fc073b70 refactor(live_location_share): exclude live location events of own user (#4535)
This change ensures that the user's own live location events are
excluded from the location sharing stream. Since the user's location is
already represented on the map by the blue dot, processing their own
events is redundant and unnecessary.
2025-01-17 14:56:10 +00:00
Jonas Platte 160600e8c0 chore(ui): Copy some attributes from matrix-sdk
The others should likely be copied at some point as well, but including
the readme as crate documentation is not currently useful since the
readme for the ui crate is empty, and warning about missing docs or
debug implementations would make CI fail without substantial extra work.
2025-01-17 15:35:28 +01:00
Jonas Platte 993c103270 ci: Add wasm job for matrix-sdk-ui 2025-01-17 15:35:28 +01:00
Jonas Platte e077980ba2 ci: Shorten job name
The extra 'wasm-flags' does not seem to add clarity.
2025-01-17 15:35:28 +01:00
Jonas Platte 63d14b798b ci: Reorder workflow matrix items to match ci.rs 2025-01-17 15:35:28 +01:00
Jonas Platte 077d63a9fc chore(ui): Re-export matrix-sdk's js feature 2025-01-17 15:35:28 +01:00
Ivan Enderlin 453c4e12db doc(sdk): Simplify documentation of RoomEventCache::subscribe.
This patch removes a `XXX` (which I believe is a TODO) in the
documentation of `RoomEventCache::subscribe`. We are not going to change
this API anytime soon.
2025-01-17 15:32:00 +01:00
Jonas Platte f7db52e069 Use Send-less BoxFuture for HttpClient Service impl on wasm 2025-01-17 12:21:21 +01:00
Richard van der Hoff 2bd8c56e64 crypto: add some more documentation to DeviceKeys
This confused me for a while, so I thought more documentation might help.
2025-01-16 15:13:25 +00:00
Richard van der Hoff f231c74314 test: simplify examples for KeyQueryResponseTemplate
Generating keys from slices rather than base64 is easier.

Also, s/builder/template/.
2025-01-16 15:13:11 +00:00
Stefan Ceriu 2cb6ee8e6d chore(ffi): silence useless logs coming out of the ffi crate
Setting the default log level to `debug` results in logs like:

```
log: log_event
log: latest_event
log: log_event
log: log_event
log: room_info
log: latest_event
log: log_event
log: room_info
```

Presumably they're coming out of the custom tracing configuration and we definitely don't need them.
2025-01-16 15:17:24 +02:00
Richard van der Hoff c24770a774 test: add support for dehydrated devices to KeyQueryResponseTemplate (#4540)
#4476 added some test helpers to generate `/keys/query` responses. We're
going to need to test dehydrated devices, so this PR adds support for
that.
2025-01-16 11:26:52 +00:00
Benjamin Bouvier 7fa06cb028 refactor(timeline): rename TimelineItemPosition::UpdateDecrypted to UpdateAt 2025-01-16 12:26:32 +01:00
Benjamin Bouvier 50383098ff feat(event cache): redact events in the database whenever they're redacted 2025-01-16 12:26:32 +01:00
Benjamin Bouvier 6f780a499c test(timeline): use assert_let_timeout more in the timeline's code 2025-01-16 12:26:32 +01:00
Benjamin Bouvier 425e48a46d feat(linked chunk): add LinkedChunk::replace_item_at to replace an item from a given position 2025-01-16 12:26:32 +01:00
Richard van der Hoff 3dd81fbe2c test: rename snapshots not to contain :
Windows doens't allow you to have `:` in its filenames
2025-01-16 11:11:38 +00:00
Benjamin Bouvier 6a0333e812 test: replace Option::default() by None 2025-01-16 11:34:42 +01:00
Benjamin Bouvier b3a789af90 test: get rid of the synced_client helper
Not running a large sync with many events make for simpler test cases,
with a more focused scope.
2025-01-16 11:34:42 +01:00
Benjamin Bouvier 560e582e41 test: get rid of mock_redaction and replace it with the holy MatrixMockServer 2025-01-16 11:34:42 +01:00
Benjamin Bouvier de7397a20e feat(event cache): handle redacted redactions in the AllEventsCache
This is unlikely that it will affect us, so not worth adding a test IMO,
but for the sake of completeness: this handles redacted redactions in
the `AllEventsCache` too.
2025-01-16 10:05:45 +01:00
Andy Balaam 8bd94318c0 fix(tests): Fix a flaky test by marking a room's members as synced.
This is intended to prevent the test
`test_when_user_in_verification_violation_becomes_verified_we_report_it`
flaking. I found that sometimes when it called `Room::members` the
result was empty due to it trying to fetch the answer from the server.
This change prevents that behaviour. I don't know why the behaviour was
inconsistent before.
2025-01-15 15:24:49 +00:00
Richard van der Hoff fe3cc09ae0 test: add examples for the new builder 2025-01-15 10:42:21 +00:00
Richard van der Hoff 3a3cc54067 test: generate dan's data dynamically 2025-01-15 10:42:21 +00:00
Richard van der Hoff 47f8b32ea1 test: give Dan new keys
Regenerate Dan's data with new cross-signing and device keys, for which I know
the private keys.

The signatures are manually calculated for now; this will be improved in a
later commit.
2025-01-15 10:42:21 +00:00
Richard van der Hoff 49748dbd4b test: factor out common parts of dan_keys_query_response{_loggedout} 2025-01-15 10:42:21 +00:00
Richard van der Hoff 25ea5fdd73 test: use builder for some more test data 2025-01-15 10:42:21 +00:00
Richard van der Hoff 5fadde5a6d test: implement test user data builder type
... and use it for some simple data
2025-01-15 10:42:21 +00:00
Richard van der Hoff b6be4d5170 test: remove redundant sig on master key
Our test helper won't do this, and it's redundant
2025-01-15 10:42:21 +00:00
Richard van der Hoff c9bac4ff2b test: snapshot the generated object rather than the JSON
We're going to be switching away from JSON-twiddling, so let's snapshot the
real object rather than the JSON.
2025-01-15 10:42:21 +00:00
Richard van der Hoff fedf7d214f test: add some snapshot tests before we change anything 2025-01-15 10:42:21 +00:00
Valere c969f903b7 Merge pull request #4526 from matrix-org/valere/test_encrypted_crypto_sql_snapshot
tests: Add an encrypted snapshot of a SQLite db for regression tests
2025-01-15 09:37:27 +01:00
Jorge Martín bd5d7aafee feat(ffi): Add FFI bindings for fn Room::own_membership_details.
Also add `membership_change_reason` field to `ffi::RoomMember`.
2025-01-14 16:23:51 +01:00
Jorge Martín e015a531da feat(room): Add fn Room::own_membership_details
This will retrieve the room member info of both the current user and the info for the sender of the current user's room member event.
2025-01-14 16:23:51 +01:00
Benjamin Bouvier b9014a5e2a test: keep a single sync in test_delayed_invite_response_and_sent_message_decryption()
This removes one sync that happens in the background, because it's
likely spurious and may be confusing the server about what's been seen
by the current client.
2025-01-14 15:07:10 +01:00
Jorge Martín e9487b0851 fix(timeline): Add UTDs to the timeline conditionally 2025-01-14 12:25:49 +01:00
Damir Jelić c60bfb877a chore: Add some missing links to the changelog 2025-01-13 19:27:44 +01:00
Valere ee32b1f600 tests: Add an encrypted snapshot of a SQLite db for regression tests 2025-01-13 17:50:50 +01:00
Daniel Salinas 9641aa9082 feat(send queue): Add an enqueued time to to-be-sent events (#4385)
Add a new created_at to the send_queue_events and
dependent_send_queue_events stored records. This will allow clients to
understand how stale a pending message might be in the event that the
queue encounters and error and becomes wedged.

This change is exposed through the FFI on the `EventTimelineItem` struct
as a new optional field named `local_created_at`. It will be `None` for
any Remote event, and `Some` for Local events (except for those that
were enqueued before the migrations were run).

Signed-off-by: Daniel Salinas

---------

Signed-off-by: Daniel Salinas <zzorba@users.noreply.github.com>
Co-authored-by: Daniel Salinas <danielsalinas@daniels-mbp-2.myfiosgateway.com>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
Co-authored-by: Daniel Salinas <danielsalinas@Daniels-MBP-2.attlocal.net>
2025-01-13 16:41:05 +00:00
Benjamin Bouvier a8ca77f4fc feat(base): remove cached events when forgetting about a room 2025-01-13 17:36:33 +01:00
Benjamin Bouvier e647ff935e feat(event cache store): allow removing an entire room at once 2025-01-13 17:36:33 +01:00
Benjamin Bouvier 7f04a9a18b fix(memory chunk): only remove a given room's events when clearing a roo 2025-01-13 17:36:33 +01:00
Damir Jelić 67d2cb790d chore: Fix a couple of typos 2025-01-13 17:25:00 +01:00
Benjamin Bouvier 279c78b3e2 chore!(encryption): rename are_we_the_last_man_standing to is_last_device
While the former name is arguably more fun, the latter is more
descriptive of what the function does.
2025-01-13 16:51:33 +01:00
Benjamin Bouvier 9514388108 tests: rename RoomMessagesResponse to RoomMessagesResponseTemplate 2025-01-13 14:50:21 +01:00
Benjamin Bouvier e6dc10933c tests: add helper to delay a room /messages response
This removes a few manual uses of `ResponseTemplate`, which is sweet and
guarantees some better typing for those responses overall.
2025-01-13 14:50:21 +01:00
Benjamin Bouvier c456356424 tests: add a helper to create a room /messages response 2025-01-13 14:50:21 +01:00
Benjamin Bouvier 5af326b36e fix(event cache): keep the previous-batch token when we haven't enabled storage 2025-01-13 14:50:21 +01:00
Jorge Martín 5548f38393 feat(ffi): Add FFI bindings for the new room privacy settings feature. 2025-01-13 11:29:10 +01:00
Jorge Martín d9c1188f87 test(room): Add integration tests for publishing and removing room aliases 2025-01-13 11:29:10 +01:00
Jorge Martín 588702756b feat(room): Add fn RoomPrivacySettings::remove_room_alias_in_room_directory. 2025-01-13 11:29:10 +01:00
Jorge Martín d6a74d389d feat(room): Add fn RoomPrivacySettings::publish_room_alias_in_room_directory.
This also needs some new mocks for resolving room aliases.
2025-01-13 11:29:10 +01:00
Jorge Martín d807d71e22 feat(room): Add fn RoomPrivacySettings::update_room_visibility. 2025-01-13 11:29:10 +01:00
Jorge Martín 587545ae82 feat(room): Add fn RoomPrivacySettings::get_room_visibility. 2025-01-13 11:29:10 +01:00
Jorge Martín 49985e5476 feat(room): Add fn RoomPrivacySettings::update_join_rule. 2025-01-13 11:29:10 +01:00
Jorge Martín 4fbe79a27d feat(room): Add fn RoomPrivacySettings::update_room_history_visibility. 2025-01-13 11:29:10 +01:00
Jorge Martín f61ad19ae6 feat(room): Add RoomPrivacySettings helper struct.
This can be accessed through `fn Room::privacy_settings` and will wrap the functionality related to a room's access and privacy settings.

This commit includes the `fn RoomPrivacySettings::update_canonical_alias` to modify the canonical alias of a room.
2025-01-13 11:29:10 +01:00
Benjamin Bouvier c9a49006f6 chore(xtask): tweak the TWiM report to include only merged PRs, not created PRs
As an outsider, I am mostly interested in features and new developments
that have happened, not those that *may* happen. An open-but-not-merged
PR may not get merged in the end, or it may not get merged any time
soon, creating false expectations. Merged PRs, on the other hand, have
definitely happened (even if they get undone, that happens via other PRs
that will get merged later). As such, I think it brings more value to
outsiders.
2025-01-13 11:03:08 +01:00
Kévin Commaille ca9eb70db5 Add PR link to changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-13 08:46:10 +01:00
Kévin Commaille f173aea6e4 feat(sdk): Expose Client::server_versions publicly
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-13 08:46:10 +01:00
Jonas Platte e37ad11b47 refactor(ui): Use RPITIT / AFIT for RoomDataProvider 2025-01-11 13:19:16 -05:00
Damir Jelić d6c2a63f5c refactor: Use the simplified locks in the encryption tasks 2025-01-11 09:33:33 +01:00
Damir Jelić 4ebf5056be chore: Remove our ancient upgrade guide 2025-01-10 20:22:27 +01:00
Valere a79d409f9d task(bindings): Expose withdraw_verification in UserIdentity 2025-01-10 15:18:12 +01:00
Kévin Commaille 5941495e68 feat(sdk): Implement Default for AttachmentInfo types
Since all of their fields are optional, it simplifies their
construction.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-10 14:37:56 +01:00
Kévin Commaille b3491582d0 feat(sdk): Allow to set and check whether an image is animated
Using MSC4230.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-10 14:37:56 +01:00
Damir Jelić def4bbbed2 fix(store-encryption): Remove an unwrap that snuck in (#4506) 2025-01-10 14:13:10 +01:00
Valere 1dd2b2c9e8 test: Test the KnownSenderData migration with optimised [u8] serialization 2025-01-10 14:12:32 +01:00
Damir Jelić e4b269e0de fix: Implement visit_bytes for the Ed25519PublicKey deserialization
This fixes the deserialization of the SenderData since it switched to
the base64 encoding for serialization of the master key in one of its
variants.

The issue was introduced in 5ff556f6c3.
2025-01-10 14:12:32 +01:00
Kévin Commaille cb72d4375f chore: Upgrade Ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-10 11:31:56 +01:00
Andy Balaam 7ec384c61a fix: Fix incorrect debug_struct calls in several places 2025-01-10 09:27:31 +01:00
Jonas Platte 7466f77eae refactor(ui): Replace tokio::spawn with matrix_sdk::executor::spawn 2025-01-10 03:02:37 -05:00
Jonas Platte 526b5c4630 refactor(ui): Relax some Send constraints on WASM 2025-01-10 03:02:37 -05:00
Jonas Platte 4043f9bf5d refactor(sdk): Un-cfg SendAttachment::with_send_progress_observable
It (now) compiles on WASM just fine.
2025-01-10 03:02:37 -05:00
Jonas Platte ff5dcbf631 refactor(common): Warn if LinkedChunk::updates() return value is not used 2025-01-09 16:20:51 -05:00
Jonas Platte 6c053a86bf chore: Fix new nightly warnings 2025-01-09 16:20:51 -05:00
Benjamin Bouvier 0cae54cc3f chore(ui): rename "utils" to "algorithms"
It only contains functions used to search items in the timeline now \o/
2025-01-09 17:27:53 +01:00
Benjamin Bouvier 692aceba50 chore(ui): move RelativePosition in timeline/controller 2025-01-09 17:27:53 +01:00
Benjamin Bouvier c4a86a3d0a chore(ui): move timeline/read_receipts to timeline/controller/read_receipts
Read receipts only make sense in the context of the timeline controller.
2025-01-09 17:27:53 +01:00
Benjamin Bouvier 5f5aa81174 chore(ui): move date and timestamp functionality to timeline/date_dividers.rs
`util.rs` files are… not the best thing. These types and functions were
only used by the date dividers file, so let's move them there.
2025-01-09 17:27:53 +01:00
Benjamin Bouvier 6e0f258a39 chore(sdk): move send_queue.rs to send_queue/mod.rs 2025-01-09 17:27:53 +01:00
Stefan Ceriu c4bfbd0f44 feat(ffi): move tracing setup from the final client to the ffi layer (#4492)
Having the final clients define the tracing filters / log levels proved
to be tricky to keep in check resulting missing logs (e.g. recently
introduced modules like the event cache) or unexpected behaviors (e.g.
missing panics because we don't set a global filter). As such we decided
to move the tracing setup and default definitions over to the rust side
and let rust developers have full control over them.

We will now take a general log level and optional extra targets and 
apply them on top of the rust side defined defaults. Targets that log
more than the requested by default will remain unchanged while the
others will increase their log levels to match. Certain targets like
`hyper` will be ignored in this step as they're too verbose others 
like `matrix_sdk` because they're too generic.
2025-01-09 18:08:44 +02:00
Benjamin Bouvier 8e0ee47637 refactor(event cache): eliminate intermediate function append_events_locked
and replace it with an inlined call to `append_events_locked_impl`,
that's then renamed `append_events_locked`.
2025-01-09 15:36:36 +01:00
Benjamin Bouvier 0915eeed51 chore(event cache): simplify and add logs to RoomEventCacheState::propagate_changes 2025-01-09 15:36:36 +01:00
Benjamin Bouvier fb54e869e9 chore(event cache): add more logs when the event cache tasks are shutting down 2025-01-09 15:36:36 +01:00
Benjamin Bouvier 9e97ed3134 test(event cache): add a regression test for not deleting a gap that wasn't inserted 2025-01-09 12:12:12 +01:00
Benjamin Bouvier b926c4287a refactor(event cache): use a more fine-grained check for the gap removal 2025-01-09 12:12:12 +01:00
Ivan Enderlin ddf4d575b7 fix(sdk): Ensure a gap has been inserted before removing it.
This patch fixes a bug where the code assumes a gap has been inserted,
and thus, is always present. But this isn't the case. If `prev_batch`
is `None`, a gap is not inserted, and so we cannot remove it. This patch
checks that `prev_batch` is `Some(_)`, which means the invariant is
correct, and the code can remove the gap.
2025-01-09 12:12:12 +01:00
Damir Jelić 2a954e3ce3 fix(base): Correctly name the LeftRoomUpdate in its debug implementation (#4487)
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2025-01-09 11:10:33 +00:00
Jonas Platte b837865226 refactor(ui): Inherit Send / Sync bounds on RoomDataProvider from super traits 2025-01-09 09:26:55 +01:00
Jonas Platte eac5a5eb35 refactor(ui): Fix unused import on wasm 2025-01-09 09:26:55 +01:00
Jonas Platte d6c64027f6 refactor(sdk): Un-cfg Client::rooms_stream
It compiles on WASM too.
2025-01-09 09:26:55 +01:00
Ivan Enderlin df4b69666c chore: Make Clippy and wasm-pack happy. 2025-01-08 21:30:41 +01:00
Ivan Enderlin 5675ac7f46 refactor(sdk): Remove SlidingSyncRoomInner::client.
This patch removes `SlidingSyncRoomInner::client` because, first
off, it's not `Send`, and second, it's useless. Nobody uses it, it's
basically dead code… annoying dead code… bad dead code!
2025-01-08 21:30:41 +01:00
Ivan Enderlin 6b2233f8c4 fix(sdk): Use spawn from matrix_sdk_common to make it compatible with wasm32-u-u. 2025-01-08 21:30:41 +01:00
Ivan Enderlin 61dd560499 feat: Remove the experimental-sliding-sync feature flag.
Sliding sync is no longer experimental. It has a solid MSC4186, along
with a solid implementation inside Synapse. It's time to consider it
mature.

The SDK continues to support the old MSC3575 in addition to MSC4186.
This patch only removes the `experimental-sliding-sync` feature flag.
2025-01-08 21:30:41 +01:00
Damir Jelić 62567ca6eb refactor(crypto): Use the simplified locks across the crypto crate 2025-01-08 18:59:22 +01:00
Damir Jelić 46dc2a9c5e refactor: Use the simplified locks in the failures cache 2025-01-08 18:59:22 +01:00
Damir Jelić 891583b70e refactor: Add Mutex and RwLock wrappers that panic on poison 2025-01-08 18:59:22 +01:00
Ivan Enderlin e19bdbfd59 test(ui): Adjust tests according to the new Timeline behaviour. 2025-01-08 17:04:58 +01:00
Ivan Enderlin 14d0cc1935 refactor(ui): Remove TimelineSettings::vectordiffs_as_inputs.
From now on, this patch considers that `VectorDiff`s are the official
input type for the `Timeline`, via `RoomEventCacheUpdate` (notably
`::UpdateTimelineEvents`).

This patch removes `TimelineSettings::vectordiffs_as_inputs`. It thus
removes all deduplication logics, as it is supposed to be managed by the
`EventCache` itself.
2025-01-08 17:04:58 +01:00
Ivan Enderlin b8d0384da7 refactor: Remove RoomEventCacheUpdate::AddTimelineEvents.
This patch removes the `AddTimelineEvents` variant from
`RoomEventCacheUpdate` since it is replaced by `UpdateTimelineEvents`
which shares `VectorDiff`.

This patch also tests all uses of `UpdateTimelineEvents` in existing
tests.
2025-01-08 17:04:58 +01:00
Ivan Enderlin 4e0a6d15ca chore(sdk): Merge imports for the sake of clarity. 2025-01-08 17:04:58 +01:00
Ivan Enderlin 251433382f chore(test): Remove a warning.
`owned_user_id` is only used by a test behind the
`experimental-sliding-sync` feature flag.
2025-01-08 17:04:58 +01:00
Benjamin Bouvier 34e993435d fix(ffi): ensure the log level for panic is always set (#4485)
If it's present, we just let it untouched. Otherwise, we set it to
`error` if it's missing. See code comment explaining why we need this.

This makes sure we log panics at the FFI layer, since the `log-panics`
crate will use the `panic` target at the error level.
2025-01-08 15:51:14 +00:00
Benjamin Bouvier dc2775e194 chore!(ffi): rename thumbnail_url to thumbnail_path
This is a breaking change because uniffi may use foreign-language named
parameters based on the Rust parameter name.
2025-01-08 14:20:06 +01:00
Benjamin Bouvier 45c3752cae refactor!(ffi): common out more code in send_attachment and distinguish early from late errors
Some errors can be handled immediately and don't need a request to be
spawned, e.g. invalid mimetype and so on. The returned task handle still
deals about "late" errors about the upload failing (for sync uploads) or
the send queue failing to push the media upload (for async uploads).
2025-01-08 14:20:06 +01:00
Benjamin Bouvier ed178602d7 chore!(ffi): group parameters to upload in UploadParameters
Note: `Box<dyn ProgressWatcher>` couldn't be put in a `Record`, so
doesn't belong in `UploadParameters` as a result.
2025-01-08 14:20:06 +01:00
Benjamin Bouvier 35a03278c3 chore(ffi): rename url to filename in the FFI methods for sending attachments 2025-01-08 14:20:06 +01:00
Valere 9e69b631ee Merge pull request #4450 from matrix-org/valere/serialize_known_sender_data_b64
fix(crypto): Serialize sender data msk in base64 instead of numbers
2025-01-08 14:06:13 +01:00
Valere 5ff556f6c3 fix(crypto): Serialize sender data msk in base64 instead of numbers 2025-01-08 13:22:37 +01:00
Ivan Enderlin d64960679f refactor(sdk): Rename RoomEvents::filter_duplicated_events.
This patch renames `RoomEvents::filter_duplicated_events` to
`collect_valid_and_duplicated_events` as I believe it improves the
understanding of the code. The variables named `unique_events` are
renamed `events` as all (valid) events are returned, not only the unique
ones.
2025-01-08 13:01:59 +01:00
Ivan Enderlin 7ff1170681 chore: Re-indent. 2025-01-08 11:47:24 +01:00
Ivan Enderlin 55e25a3717 feat(sdk,ui): Add EventsOrigin::Pagination.
This patch adds the `Pagination` variant to the `EventsOrigin` enum.
Not something really mandatory and that is likely to fix a bug, but it's
now correct.
2025-01-08 11:43:03 +01:00
Joe Groocock 3f977b79fa feat(timeline): allow sending mentions along with media
Since 8205da898e it has been possible to
attach (intentional) mentions to _edited_ media captions, but the
send_$mediatype() timeline APIs provided no way to send them with the
initial event. This fixes that.

Signed-off-by: Joe Groocock <me@frebib.net>
2025-01-08 10:43:43 +01:00
Benjamin Bouvier aca8c8b8ee chore: remove some allow(dead_code) annotations and associated dead code (#4472)
We have quite a few `allow(dead_code)` annotations. While it's OK to use
in situations where the Cargo-feature combination explodes and makes it
hard to reason about when something is actually used or not, in other
situations it can be avoided, and show actual, dead code.
2025-01-08 10:37:18 +01:00
Kévin Commaille 47c24b9a17 fix(sdk): Fix test now that Ruma is fixed
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-08 10:35:28 +01:00
Kévin Commaille 47445b10f1 chore: Upgrade Ruma
This is using the ruma-0.12 branch where non-breaking changes are backported.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-08 10:35:28 +01:00
Jonas Platte c5a9a1e215 Clean up some imports
With experimental-sliding-sync enabled and e2e-encryption disabled,
there were a bunch of warnings about unused imports. This fixes them
(but a few warnings about other unused items remain).
2025-01-08 09:18:56 +01:00
Benjamin Bouvier 2ef14ded41 refactor(event cache): a few AllEventsCache refactorings (#4471)
I was investigating a potential deadlock with the event cache storage,
and only found a few places where to make the code a bit more idiomatic
and more readable.
2025-01-07 17:25:52 +01:00
Benjamin Bouvier 8205da898e feat(send queue): allow setting intentional mentions in media captions edits
Fixes #4302.
2025-01-07 16:52:53 +01:00
Benjamin Bouvier 618e47250d feat!(base): reintroduce Room::display_name
`compute_display_name` is made private again, and used only within the
base crate. A new public counterpart `Room::display_name` is introduced,
which returns a cached value for, or computes (and fills in cache) the
display name. This is simpler to use, and likely what most users expect
anyways.
2025-01-07 15:25:32 +01:00
Benjamin Bouvier 5110aa64aa doc(base): update lying doc comment of compute_display_name
It claimed that it would immediately return when the cached display name
value was computed, but that's absolutely not the case.

Spotted while reviewing a PR updating `iamb` to the latest version of
the SDK.
2025-01-07 14:39:07 +01:00
Benjamin Bouvier bcad0a3059 test(timeline): rewrite a test to use the MatrixMockServer instead 2025-01-07 11:58:34 +01:00
Benjamin Bouvier b7b88f58d2 feat!(send queue): make unrecoverable errors stop the sending queue
Instead of keeping on handling unwedged events from the sending queue,
it's now required to re-enable the send queue manually for the room that
encountered the sending error, all the time. This is more consistent,
and avoids weird behavior when a user would 1. send an event for which
sending fails, in an unrecoverable manner, 2. send an event that's
actually sendable.
2025-01-07 11:58:34 +01:00
Damir Jelić 412fcab4dc test: Await the device creation in the notification client redecryption test 2025-01-07 11:06:28 +01:00
Jonas Platte 8e75a940f7 Use Instant from web-time in more places (via ruma re-export)
web-time's Instant type is already used elsewhere in the project. It is
an alias for std's Instant type on most targets, but tries to call into
JavaScript on wasm32-unknown-unknown (assuming that the wasm blob is
used in from a browser context). Its Duration type is a plain re-export
of std's Duration, even on wasm32-unknown-unknown.
2025-01-07 09:35:52 +01:00
Kévin Commaille 70fb7899e6 feat!(timeline): Allow to send attachments from bytes (#4451)
Sometimes we can get the bytes directly, e.g. in Fractal we can get an
image from the clipboard. It avoids to have to write the data to a
temporary file only to have the data loaded back in memory by the SDK
right after.

The first commit to accept any type that implements `Into<String>` for
the filename is grouped here because it simplifies slightly the second
commit.

Note that we could also use `AttachmentSource` in the other
`send_attachment` APIs, on `Room` and `RoomSendQueue`, for consistency.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-01-06 15:44:29 +01:00
Benjamin Bouvier 1480fada6e refactor(event cache): make it clearer that vecdiff updates must be handled with with_events_mut
Every caller to `with_events_mut` must propagate the vector diff
updates, otherwise updates would be missing to the room event cache's
observers. This slightly tweaks the signature to make this a bit
clearer, and adjusts the code comment as well.
2025-01-06 13:14:31 +01:00
Kévin Commaille c50358366f refactor!(sdk): Set thumbnail in AttachmentConfig with builder method instead of constructor
`AttachmentConfig::with_thumbnail()` is replaced by
`AttachmentConfig::new().thumbnail()`.

Simplifies the use of `AttachmentConfig`, by avoiding code like:

```rust
let config = if let Some(thumbnail) = thumbnail {
  AttachmentConfig::with_thumbnail(thumbnail)
} else {
  AttachmentConfig::new()
};
```

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-22 17:45:04 +00:00
Valere adb4428a69 test(crypto): Add some basic snapshot testing in crypto crate 2024-12-20 19:52:37 +01:00
Damir Jelić 667a8e684c chore: Fix a typo in the changelog 2024-12-20 13:59:54 +01:00
Ivan Enderlin f4b50db972 test: Increase timeout for codecoverage. 2024-12-20 13:57:45 +01:00
Ivan Enderlin 1abb2efc51 refactor(sdk): Rename two variables. 2024-12-20 13:57:45 +01:00
Ivan Enderlin c4132252d3 feat(ui): Enable TimelineSettings::vectordiffs_as_inputs if event cache storage is enabled.
This patch automatically enables
`TimelineSettings::vectordiffs_as_inputs` if and only if the event cache
storage is enabled.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 51c76a15ad chore(ui): Make Clippy happy. 2024-12-20 13:57:45 +01:00
Ivan Enderlin f1842ba5d0 refactor(ui): Timeline receives pagination events as VectorDiffs!
This patch allows the paginated events of a `Timeline` to be received
via `RoomEventCacheUpdate::UpdateTimelineEvents` as `VectorDiff`s.
2024-12-20 13:57:45 +01:00
Ivan Enderlin d8dd72fd9c refactor(ui): Deduplicate timeline items conditionnally.
A previous patch deduplicates the remote events conditionnally. This
patch does the same but for timeline items.

The `Timeline` has its own deduplication algorithm (for remote
events, and for timeline items). The `Timeline` is about to receive
its updates via the `EventCache` which has its own deduplication
mechanism (`matrix_sdk::event_cache::Deduplicator`). To avoid conflicts
between the two, we conditionnally deduplicate timeline items based on
`TimelineSettings::vectordiffs_as_inputs`.

This patch takes the liberty to refactor the deduplication mechanism of
the timeline items to make it explicit with its own methods, so
that it can be re-used for `TimelineItemPosition::At`. A specific
short-circuit was present before, which is no more possible with the
rewrite to a generic mechanism. Consequently, when a local timeline
item becomes a remote timeline item, it was previously updated (via
`ObservableItems::replace`), but now the local timeline item is removed
(via `ObservableItems::remove`), and then the remote timeline item is
inserted (via `ObservableItems::insert`). Depending of whether a virtual
timeline item like a date divider is around, the position of the removal
and the insertion might not be the same (!), which is perfectly fine as
the date divider will be re-computed anyway. The result is exactly the
same, but the `VectorDiff` updates emitted by the `Timeline` are a bit
different (different paths, same result).

This is why this patch needs to update a couple of tests.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 054f5e28f6 fix(common): Use a trick to avoid hitting the recursion_limit too quickly.
This patch adds a trick around `SyncTimelineEvent` to avoid reaching the
`recursion_limit` too quickly. Read the documentation in this patch to
learn more.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 38e35b99d0 test(ui): Increase the recursion_limit.
Since we have added a new variant to `RoomEventCacheUpdate`, a macro
hits the recursion limit. It needs to be updated in order for tests to
run again.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 39afb531ef task(ui): DayDivider has been renamed DateDivider.
This patch updates this branch to `main` where `DayDivider` has been
renamed `DateDivider`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin c1ff5ff49f refactor(ui): Deduplicate remote events conditionnally.
The `Timeline` has its own remote event deduplication mechanism. But
we are transitioning to receive updates from the `EventCache` via
`VectorDiff`, which are emitted via `RoomEvents`, which already runs its
own deduplication mechanism (`matrix_sdk::event_cache::Deduplicator`).
Deduplication from the `EventCache` will generate `VectorDiff::Remove`
for example. It can create a conflict with the `Timeline` deduplication
mechanism.

This patch updates the deduplication mechanism from the `Timeline`
when adding or updating remote events to be conditionnal: when
`TimelineSettings::vectordiffs_as_inputs` is enabled, the deduplication
mechanism of the `Timeline` is silent, it does nothing, otherwise it
runs.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 2358e4c32f task(ui): Support VectorDiff::Remove, in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::Remove,`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 409fccb709 task(ui): Support VectorDiff::Insert in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::Insert`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin b25fd830ec task(ui): Add AllRemoteEvents::range.
This patch adds the `AllRemoteEvents::range` method. This
is going to be useful to support `VectorDiff::Insert` inside
`TimelineStateTransaction::handle_remote_events_with_diffs`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 02ab57870a task(ui): Add ObservableItems::insert_remote_event.
This patch adds the `ObservavbleItems::insert_remote_event` method.
This is going to be useful to implement `VectorDiff::Insert` inside
`TimelineStateTransaction::handle_remote_events_with_diffs`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin eca3749b28 task(ui): Support VectorDiff::Clear in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::Clear`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 3f17325bac task(ui): Support VectorDiff::PushBack in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::PushBack`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin 23c09b2c9d task(ui): Support VectorDiff::PushFront in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::PushFront`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin c1f8232450 task(ui): Support VectorDiff::Append in TimelineStateTransaction::handle_remote_events_with_diffs.
This patch updates
`TimelineStateTransaction::handle_remote_events_with_diffs` to support
`VectorDiff::Append`.
2024-12-20 13:57:45 +01:00
Ivan Enderlin e28073361d feat(ui): Add blank handle_remote_events_with_diffs. 2024-12-20 13:57:45 +01:00
Ivan Enderlin 1c2fb1ab72 refactor(sdk): Add RoomEventCacheUpdate::UpdateTimelineEvents.
This patch adds a new variant to `RoomEventCacheUpdate`, namely
`UpdateTimelineEvents. It's going to replace `AddTimelineEvents` soon
once it's stable enough. This is a transition. They are read by the
`Timeline` if and only if `TimelineSettings::vectordiffs_as_inputs` is
turned on.
2024-12-20 13:57:45 +01:00
Ivan Enderlin be89e3aacb feat(ui): Add TimelineBuilder::with_vectordiffs_as_inputs.
This patch adds `with_vectordiffs_as_inputs` on `TimelineBuilder` and
`vectordiffs_as_inputs` on `TimelineSettings`. This new flag allows to
transition from one system to another for the `Timeline`, when enabled,
the `Timeline` will accept `VectorDiff<SyncTimelineEvent>` for the
inputs instead of `Vec<SyncTimelineEvent>`.
2024-12-20 13:57:45 +01:00
Damir Jelić 36427b0e12 fix(ui): Consider banned rooms as rooms we left in the non-left rooms matcher
Recently we started to differentiate between rooms we've been banned
from from rooms we have left on our own.

Sadly the non-left rooms matcher only checked if the room state is not
equal to the Left state. This then accidentally moved all the banned
rooms to be considered as non-left.

We replace the single if expression with a match and list all the
states, this way we're going to be notified by the compiler that we need
to consider any new states we add in the future.
2024-12-20 12:35:34 +01:00
Daniel Salinas f8a9d12c88 Use a type alias to allow bindings to take advantage of custom types 2024-12-20 10:46:13 +01:00
Benjamin Bouvier 5f5e979e16 refactor!: Put the RequestConfig argument of Client::send() into a builder method
Instead of `Client::send(request, request_config)`, consumers can now do
`Client::send(request).with_request_config(request_config)`.
2024-12-20 10:35:18 +01:00
Valere 519f281844 Merge pull request #4428 from matrix-org/valere/insta_rs_snapshot_testing
test(snapshot): Use snapshot testing in sdk-common
2024-12-19 18:30:49 +01:00
Valere 3b31bbec0c test(snapshot): Use snapshot testing in sdk-common 2024-12-19 18:11:55 +01:00
Benjamin Bouvier f2942db316 refactor: avoid use of async_trait for RoomIdentityProvider
This is an 8 seconds (out of 22) decrease of the matrix-sdk compile
times.
2024-12-19 17:38:59 +01:00
Andy Balaam e4712be946 task(crypto): Support receiving stable identifier for MSC4147 2024-12-19 15:27:34 +00:00
Benjamin Bouvier bc8c4f5e58 fix(event cache): don't touch the linked chunk if an operation wouldn't cause meaningful changes
See comment on top of `deduplicated_all_new_events`.
2024-12-19 14:19:55 +01:00
Benjamin Bouvier fe9354a886 test: make test_room_keys_received_on_notification_client_trigger_redecryption more stable
When starting to back-paginate, in this test, we:

- either have a previous-batch token, that points to the first event
*before* the message was sent,
- or have no previous-batch token, because we stopped sync before
receiving the first sync result.

Because of the behavior introduced in 944a9220, we don't restart
back-paginating from the end, if we've reached the start. Now, if we are
in the case described by the first bullet item, then we may backpaginate
until the start of the room, and stop then, because we've back-paginated
all events. And so we'll never see the message sent by Alice after we
stopped sync'ing.

One solution to get to the desired state is to clear the internal state
of the room event cache, thus deleting the previous-batch token, thus
causing the situation described in the second bullet item. This achieves
what we want, that is, back-paginating from the end of the timeline.
2024-12-19 14:19:55 +01:00
Benjamin Bouvier d00ff8fa1f refactor(event cache): remove duplicated method RoomEventCacheState::clear() 2024-12-19 14:19:55 +01:00
Benjamin Bouvier 60f521cc23 feat(event cache): don't add a previous gap if all events were deduplicated, after back-pagination 2024-12-19 14:19:55 +01:00
Benjamin Bouvier bcb9a86a00 feat(event cache): don't add a previous gap if all events were deduplicated, after sync 2024-12-19 14:19:55 +01:00
Benjamin Bouvier 3f0712010f refactor(event cache): add a way to know if we deduplicated all events (at least one) 2024-12-19 14:19:55 +01:00
Benjamin Bouvier d89194f071 refactor: display source pagination error in PaginatorError::SdkError 2024-12-19 14:19:55 +01:00
Benjamin Bouvier a20ad728b5 feat(event cache): don't restart back-pagination from the end if we had no prev-batch token 2024-12-19 14:19:55 +01:00
Benjamin Bouvier 0d546dce5f refactor(event cache): move PaginationToken from the pagination to room/mod file 2024-12-19 14:19:55 +01:00
Jorge Martín 38cc9fb7c8 test(room): Improve Room::room_member_updates_sender tests 2024-12-19 14:14:05 +01:00
Jorge Martín 616c193a30 feat(room): create a cleanup task in Room::subscribe_to_knock_requests
This cleanup task will run while the knock request subscription runs and will use the `Room::room_member_updates_sender` notification to call `Room::remove_outdated_seen_knock_requests_ids` and remove outdated seen knock request ids automatically.
2024-12-19 14:14:05 +01:00
Jorge Martín 4a88e7cfee feat(room): add BaseRoom::remove_outdated_seen_knock_requests_ids fn
This will check the current seen knock request ids against the room members related to them and will remove those seen ids for members which are no longer in knock state or come from an outdated knock member event.
2024-12-19 14:14:05 +01:00
Jorge Martín 5d0fed5e53 feat(room): add helper methods to BaseRoom to get and write the current seen knock request ids while keeping them thread-safe with a lock around them 2024-12-19 14:14:05 +01:00
Jorge Martín 9975365a1e feat(room): add Room::room_member_updates_sender
This sender will notify receivers when new room members are received: this can happen either when reloading the full room member list from the HS or when new member events arrive during a sync.

The sender will emit a `RoomMembersUpdate`, which can be either a full reload or a partial one, including the user ids of the members that changed.
2024-12-19 14:14:05 +01:00
Integral f18e0b18a1 Replace PathBuf/Utf8PathBuf with Path/Utf8Path when ownership not needed 2024-12-19 13:29:09 +01:00
Jorge Martín b18100228e test(room): add test to verify how Room::observe_events will behave when several events are received in a short while 2024-12-19 12:16:49 +01:00
Benjamin Bouvier de568837fb fix(linked chunk): fix order handling of initial chunks in UpdateToVectorDiff::new()
The code would use a chunk iterator that moves forward, but call
`push_front()` repetitively on each chunk, semantically storing the
lengths in *reverse* order.

This could result in subsequent panics, when a new chunk was added,
because the links would not match what's expected (e.g. the last chunk
must have no successor, etc.).
2024-12-18 19:50:25 +01:00
Ivan Enderlin bc582ae101 doc(common): Update documentation of AsVector.
This patch updates the documentation of `AsVector`.
2024-12-18 19:47:25 +01:00
Damir Jelić bb573117e1 chore: Release matrix-sdk version 0.9.0 2024-12-18 15:12:23 +01:00
Andy Balaam ff7077b742 doc(crypto): Add changelog entry for #4424 2024-12-18 13:51:00 +00:00
Ivan Enderlin bb70229dd8 chore: Make Clippy happy. 2024-12-18 13:24:55 +01:00
Andy Balaam 03947618ff Merge branch 'release-for-crypto-wasm-11'
This brings in the fix for #4424 that we did on a release branch to
allow a quick release of crypto-wasm
2024-12-18 11:25:53 +00:00
Andy Balaam b18e7d71ed fix(crypto): Fix error when reading VerifiedStateOrBool with old PreviouslyVerifiedButNoLonger value 2024-12-18 11:22:53 +00:00
Andy Balaam 612ba6fa29 task(crypto): Accept old PreviouslyVerified value of ShieldStateCode when deserializing 2024-12-18 11:22:53 +00:00
Andy Balaam db39c6bea6 task(crypto): Accept old PreviouslyVerified value of VerificationLevel when deserializing 2024-12-18 11:22:53 +00:00
Andy Balaam 5f3b56a987 task(crypto): Accept old PreviouslyVerified value of SenderData when deserializing 2024-12-18 11:22:53 +00:00
Benjamin Bouvier 373709fb38 feat(event cache): don't replace a gap chunk by an empty items chunks 2024-12-17 18:30:57 +01:00
Benjamin Bouvier 5d8ad3a4a9 fix(linked chunk): in LinkedChunk::ritems_from, skip as long as we're on the right chunk
The previous code would skip based on the position's index, but not the
position's chunk. It could be that the position's chunk is different
from the first items chunk, as shown in the example, where the linked
chunk ends with a gap; in this case, the position's index would be 0,
while the first chunk found while iterating backwards had 3 items. As a
result, items 'd' and 'e' would be skipped incorrectly.

The fix is to take into account the chunk id when skipping over items.
2024-12-17 17:48:10 +01:00
Damir Jelić 0ca35d6c4a test: Test that room keys received by notification clients trigger redecryptions 2024-12-17 16:07:21 +01:00
Damir Jelić daeffc07b3 feat: Derive PartialEq and Eq for RoomListLoadingState 2024-12-17 16:07:21 +01:00
Damir Jelić bd15f4ecbe feat(timeline): Listen to the room keys stream to retry decryptions 2024-12-17 16:07:21 +01:00
Damir Jelić f17f4e2bf6 feat: Add a stream to listen to room keys being inserted to the store 2024-12-17 16:07:21 +01:00
Damir Jelić 177ec1216f feat(crypto)!: Don't ignore the error if the room_keys_received stream lags 2024-12-17 16:07:21 +01:00
Valere 512a2d2662 Merge pull request #4399 from matrix-org/valere/ffi_save_store_dehydration_pickle_key
feat(crypto-ffi-bindings): Save/Load dehydrated pickle key
2024-12-17 10:07:54 +01:00
Valere 95582a6c3c feat(crypto-bindings): Save/Load dehydrated pickle key
review: better tests
2024-12-17 09:51:28 +01:00
Jorge Martín 866b5fea40 feat(room): Separate RoomState::Banned from RoomState::Left.
This is needed to tell apart rooms in left and banned state in places like `RoomInfo` or `RoomPreview`.

The banned rooms will still count as left rooms in the sync processes.
2024-12-16 19:19:56 +01:00
Benjamin Bouvier 34ea42aec0 feat(ffi): expose the linked chunk debug string function at the FFI layer 2024-12-16 16:41:03 +01:00
Benjamin Bouvier cae7e43b91 feat(multiverse): add linked chunk debug screen in multiverse 2024-12-16 16:41:03 +01:00
Benjamin Bouvier 34d15a4d37 feat(event cache): propose a debug representation for the linked chunk in RoomEvents too 2024-12-16 16:41:03 +01:00
Benjamin Bouvier f6cb8186c6 task(event cache): limit the display of event ids to 8 chars in the raw chunk debug string 2024-12-16 16:41:03 +01:00
dependabot[bot] 47044b1a23 chore(deps): bump crate-ci/typos from 1.28.2 to 1.28.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.2 to 1.28.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.28.2...v1.28.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 17:29:27 +02:00
Jorge Martín 05d46e6027 Rename JoinRequest in the SDK crates to KnockRequest, make Room::mark_knock_requests_as_seen thread safe and pass user_ids instead of event_ids: the user ids will be used to get the related member state events and they'll only be marked as seen if they're in a knock state.
Also, add extra checks to the integration tests.
2024-12-16 14:08:09 +01:00
Jorge Martín 338769508e feat(ffi): add bindings for subscribing to the join requests 2024-12-16 14:08:09 +01:00
Jorge Martín 93ebae6601 feat(room): allow subscribing to requests to join a room
This subscription will combine 3 streams: one notifying the members in the room have changed, another notifying the seen join requests have changed, and finally a third one notifying when the room members are no longer synced.

With this info we can track when we need to generate a new list of join requests to be emitted so the client can always have an up to date list.
2024-12-16 14:08:09 +01:00
Jorge Martín 780c264e59 feat(room): add JoinRequest abstraction
This struct is an abstraction over a room member or state event with knock membership.
2024-12-16 14:08:09 +01:00
Jorge Martín 9a899c1cb1 feat(room): add 'seen request to join ids' to the stores
This will allow us to keep track of which join room requests are marked as 'seen' by the current user and return them as such.

Also, add some methods to `Room` to mark new join requests as seen and to get the current ids for the seen join requests.
2024-12-16 14:08:09 +01:00
Richard van der Hoff 2703f7f7d4 crypto: extra logging in OtherUserIdentity
Add some extra logging in these two methids, to try to narrow down a bug report
we received.
2024-12-16 12:59:16 +00:00
Kévin Commaille 8d2e672996 feat!: Upgrade Ruma to 0.12.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-16 11:56:44 +01:00
Benjamin Bouvier 5a25e65da3 test(event cache): use the MatrixMockServer 2024-12-16 11:56:22 +01:00
Benjamin Bouvier c197808b42 task(event cache): get rid of one level of indent 2024-12-16 11:56:22 +01:00
Benjamin Bouvier ed34719295 task(event cache): simplify handling a back-pagination result 2024-12-16 11:56:22 +01:00
Benjamin Bouvier a052a79aaf fix(event cache): store the gap *before* events, after back-paginating
The conditions required to cause the bug might have been impossible to
reach in the real world, because it assumes a mix of:

- events present in the linked chunk
- no prev-batch token

However: now that we have storage, we could end up in this situation,
when reaching the start of the timeline (since there'll be no previous
gap in that case). We need to handle that better in the linked chunk
representation itself, but in the meanwhile, we should insert the gap
and the events in a relative correct order.
2024-12-16 11:56:22 +01:00
Benjamin Bouvier b6542477bb task(event cache): make the code more concise in back-pagination 2024-12-16 11:56:22 +01:00
Kévin Commaille a573b650c9 chore(sdk): Remove image-rayon cargo feature check from build.rs
The cargo feature was removed, but the build script was forgotten.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-15 18:07:42 +01:00
Valere 789bd317b3 Merge pull request #4383 from matrix-org/valere/cache_dehydration_pickle_key
feat(crypto): Support storing the dehydrated device pickle key
2024-12-13 14:42:44 +01:00
Valere 2b39476d9b feat(crypto): Support storing the dehydrated device pickle key 2024-12-13 13:05:19 +01:00
Andy Balaam 6dcefe49c2 feat(utds): Provide the reason why an event was an expected UTD 2024-12-13 10:46:10 +00:00
Benjamin Bouvier 150d9e4b05 fix(event cache store): always use immediate mode when handling linked chunk updates
If a linked chunk update starts with a RemoveChunk update, then the
transaction may start with a SELECT query and be considered a read
transaction. Soon enough, it will be upgraded into a write transaction,
because of the next UPDATE/DELETE operations that happen thereafter. If
there's another write transaction already happening, this may result in
a SQLITE_BUSY error, according to
https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions

One solution is to always start the transaction in immediate mode. This
may also fail with SQLITE_BUSY according to the documentation, but it's
unclear whether it will happen in general, since we're using WAL mode
too. Let's try it out.
2024-12-12 17:59:42 +01:00
Damir Jelić 54bd1d7931 refactor(base): Move the joined member count logic into its respective sub-functions 2024-12-12 14:44:21 +01:00
Damir Jelić 7ae31d0cb1 fix(base): Subtract the number of service members from the number joined members
This patch fixes an edge case where the member is alone in the room with
a service member. We already subtracted the number of service members
in the case we calculated the room summary ourselves, but we did not do
so when the server provided the room summary.

This lead to the room, instead of being called `Empty`, being called
`Foo and N others`.
2024-12-12 14:44:21 +01:00
Damir Jelić f7f58dfd71 feat(ui): Add the MemberHints state event type to the required state
This state event allows us to correctly calculate the room display name
according to MSC4171.

MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171
2024-12-12 14:44:21 +01:00
Richard van der Hoff 780a4630e4 chore(ffi): avoid hardcoding clang version
Update the workaround for https://github.com/rust-lang/rust/issues/109717 to
avoid hardcoding the clang version; instead, run `clang -dumpversion` to figure
it out.

While we're there, use the `CC_x86_64-linux-android` env var, which should
point to clang, rather than relying on `ANDROID_NDK_HOME` to be set.
2024-12-12 12:54:00 +00:00
Benjamin Bouvier 3356e0cc82 refactor(state store): use a single lock for all memory store accesses
The `MemoryStore` implementation of the `StateStore` has grown into a
monster, with one lock per field. It's probably overkill, as individual
fields don't need fine-grained locks like this; after all, accesses to
the store shouldn't be reentrant in general.

Fixes #3720.
2024-12-12 10:04:09 +01:00
Richard van der Hoff fda374ee81 feat(ffi): Add new properties to UnableToDecryptInfo
Followup to https://github.com/matrix-org/matrix-rust-sdk/pull/4360: expose
the new properties via FFI
2024-12-11 18:42:28 +00:00
Benjamin Bouvier 0264e49968 task(event cache): rename a few things
- rename RawLinkedChunk -> RawChunk
- rename RawChunk::id -> RawChunk::identifier
- precise that a `RawChunk` is mostly a `Chunk` with different
previous/next links.
2024-12-11 12:10:24 +01:00
Benjamin Bouvier d42c449612 refactor(event cache): only store a prev-batch token if the timeline was limited 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 925d10f2ff task(event cache store): include the number of added items in one log 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 4402f59e74 refactor(event cache): spawn a task to handle updates to the event cache store 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 20184552a8 feat(event cache): start with an empty linked chunk if reloading failed 2024-12-11 12:10:24 +01:00
Benjamin Bouvier 832fedb05e feat(event cache): display raw linked chunks from storage when they fail to be rebuilt 2024-12-11 12:10:24 +01:00
Benjamin Bouvier eeb14f6cbe refactor!(event cache store): have the event cache store return raw linked chunks, not the full linked chunk
And let the caller rebuild the linked chunk. This is slightly nicer in
that it allows us to display the raw representation of a reloaded linked
chunk, before checking its internal state is consistent; this will allow
for better debug of issues related to the linked chunk internal state.

No functional changes.
2024-12-11 12:10:24 +01:00
Kévin Commaille a562f73b1e doc(timeline): Document media caching of send_attachment
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Kévin Commaille 7295f29055 doc(send_queue): Document media caching of send_attachment
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Kévin Commaille 723d7973d5 fix(send_queue): Use MediaFormat::File when caching attachment thumbnail
The `MediaFormat` reflects only the request that would be made to the homeserver.
There is no link between the format of the files stored in the media cache and their purpose in an event.

`MediaFormat::Tumbnail` means that we request a server-generated thumbnail of a file in the media repository.

Since the thumbnail is its own file in the media repository, it makes more sense to use `MediaFormat::File`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-11 10:45:39 +01:00
Stefan Ceriu d5e7a9c949 chore(ui): rename date divider is_same_date_as to is_same_date_divider_group_as 2024-12-10 14:11:48 +02:00
Stefan Ceriu 8f064581d6 chore(ui): rename the day_dividers module to date_dividers 2024-12-10 14:11:48 +02:00
Stefan Ceriu 634edf2b65 chore(ui): rename all timeline "day dividers" to "date deviders" following the introduction of montly divider mode 2024-12-10 14:11:48 +02:00
Stefan Ceriu 935e4df927 feat(ui): make the timeline date separators configurable; have them appear either when the day changes or when the month changes. 2024-12-10 14:11:48 +02:00
Richard van der Hoff 1d72d2774f feat(ui): Add more properties to UnableToDecryptInfo 2024-12-10 11:36:04 +00:00
Richard van der Hoff 1e72131e7f feat(ui) Add UnableToDecryptInfo::user_trusts_own_identity 2024-12-10 11:36:04 +00:00
Richard van der Hoff e8b3949db3 feat(ui): Add UnableToDecryptInfo::event_local_age_millis 2024-12-10 11:36:04 +00:00
Richard van der Hoff c501a39ad4 refactor(sdk): Add Encryption::device_creation_timestamp
... so that we can use it in more places
2024-12-10 11:36:04 +00:00
Richard van der Hoff a04f9187f8 refactor(ui): store UTD info within PendingUtdReport
... making it easier to report late decryptions.
2024-12-10 11:36:04 +00:00
Benjamin Bouvier 32e2070f56 refactor(ffi): use a bool instead of an option to make the API less awkward
By default, the event cache store will be disabled. If disabled, it will
clean all the events in the cache store; most of the time this will do
nothing, since the store will not even be filled with any event data, so
it would be cheap to do. If some data was filled in the cache store
before, then it would be cleared after the cache store has been
disabled.

This makes a less awkward API than the previous one, where `None` and
`Some(false)` carried different semantics.
2024-12-10 12:05:29 +01:00
Benjamin Bouvier 4ee96aaffc feat(event cache): add a way to clear a single room's persistent storage 2024-12-10 12:05:29 +01:00
Benjamin Bouvier 0783cf89ba feat(ffi): add a feature flag to enable persistent storage for the event cache 2024-12-10 12:05:29 +01:00
Benjamin Bouvier cf02e694f2 feat(event cache store): add a method to clear all rooms' linked chunks 2024-12-10 12:05:29 +01:00
Ivan Enderlin cf178d603c doc(ui): Fix typos. 2024-12-10 11:36:05 +01:00
Ivan Enderlin ee94c86164 doc(ui): Fix a typo. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 3526761580 doc(ui): Unfold a Self in the doc. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 9a08975c8e doc(ui): Explain why ObservableItemsEntries does not implement Iterator. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 6b56c9efd8 doc(ui): Explain why Deref is fine, but DerefMut is not. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0f2ada0958 refactor(ui): Rename ObservableItems::clone to clone_items. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0d17ea353f refactor(ui): Replace a panic by a sensible value + error!. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 13e26b13e7 doc(ui): Rephrase the documentation of ObservableItems::all_remote_events. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 72f1bd6180 doc(ui): Document ObservableItems::items more and TimelineItemKind. 2024-12-10 11:36:05 +01:00
Ivan Enderlin e32ea1627e doc(ui): Fix typos. 2024-12-10 11:36:05 +01:00
Ivan Enderlin ed1f2e29ed refactor(ui): ObservableItemsTransactionEntry::remove is no longer unsafe 2024-12-10 11:36:05 +01:00
Ivan Enderlin 92cb18207e test(ui): Write test suite for ObservableItems. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 80f6b8d2cd test(ui): Write test suite for AllRemoteEvents.
This patch adds test suite for `AllRemoteEvents`.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 05969fefde chore: Make Clippy happy. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 81c962238a doc(ui): Add more documentation for AllRemoteEvents. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 56218ee5d7 refactor(ui): Create ObservableItemsTransactionEntry.
This patch creates `ObservableItemsTransactionEntry` that mimics
`ObservableVectorTransactionEntry`. The differences are `set` is
renamed `replace`, and `remove` is unsafe (because I failed to update
`AllRemoteEvents` in this method due to the borrow checker).
2024-12-10 11:36:05 +01:00
Ivan Enderlin aa9138b281 doc(ui): Add more documentation for ObservableItemsTransaction. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 6f231523b3 refactor(ui): Create ObservableItemsEntries and ObservableItemsEntry.
This patch creates `ObservableItemsEntries` and particularly
`ObservableItemsEntry` that wraps the equivalent
`ObservableVectorEntries` and `ObservableVectorEntry` with the
noticeable difference that `ObservableItemsEntry` does **not** expose
the `remove` method. It only exposes `replace` (which is a renaming
of `set`).
2024-12-10 11:36:05 +01:00
Ivan Enderlin 943b3fbd91 refactor(ui): Rename ObservableItems::set to replace.
This patch renames `ObservableItems(Transaction)::set` to `replace`, it
conveys the semantics a bit better for new comers.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 40ff880597 doc(ui): Add more documentation. 2024-12-10 11:36:05 +01:00
Ivan Enderlin 0647be1bc3 refactor(ui): Move AllRemoteEvents inside observable_items.
This patch moves `AllRemoteEvents` inside `observable_items` so that
more methods can be made private, which reduces the risk of misuses
of this API. In particular,  the following methods are now strictly
private:

- `clear`
- `push_front`
- `push_back`
- `remove`
- `timeline_item_has_been_inserted_at`
- `timeline_item_has_been_removed_at`

In fact, now, all `&mut self` method (except `get_by_event_id_mut`) are
now strictly private!
2024-12-10 11:36:05 +01:00
Ivan Enderlin b069b20e18 refactor(ui): Create ObservableItems(Transaction). 2024-12-10 11:36:05 +01:00
Ivan Enderlin 91b73a2b16 refactor(ui): Maintain timeline_item_index when timeline items are inserted or removed.
This patch maintains the `timeline_item_index` when timeline items are
inserted or removed.
2024-12-10 11:36:05 +01:00
Ivan Enderlin 14d0f6877a refactor(ui): Maintain timeline_item_index when remote events are manipulated.
This patch maintains the `timeline_item_index` when a new remote events
is added or removed.
2024-12-10 11:36:05 +01:00
Ivan Enderlin a2210bce48 refactor(ui): Add EventMeta::timeline_item_index.
This is the foundation for the mapping between remote events and
timeline items.
2024-12-10 11:36:05 +01:00
Benjamin Bouvier 68cb85a2b2 refactor(event cache store): use a single transaction to handle all linked chunk updates at once
Instead of one transaction per update. This ensures that if a single
update fails, then none is taken into account.
2024-12-10 11:32:30 +01:00
Jonas Richard Richter 72fcc50f80 feat(ffi): Expose the method to send custom events with JSON content (#4390)
This patch adds the Room::send_raw method to the bindings, making it usable from
e.g. Swift.

Signed-off-by: Jonas Richard Richter <jonas-richard.richter@telekom.de>
2024-12-10 11:03:31 +01:00
Andy Balaam 5721c3622d refactor(key_backups): Rename fast_exists_on_server to exists_on_server 2024-12-09 16:33:56 +00:00
Andy Balaam 50eb46dc82 refactor(key_backups): Rename exists_on_server to fetch_exists_on_server 2024-12-09 16:33:56 +00:00
Andy Balaam 8aae16ffd7 feat(crypto) Provide a method to check whether server backup exists without hitting the server every time 2024-12-09 16:33:56 +00:00
Benjamin Bouvier e402ed4ce8 refactor(event cache): get the *most recent* pagination token, not the *oldest* one
Whenever it needs to back-paginate, the event cache should start with
the *most recent* backpagination token, not the oldest one.

This isn't a functional change, until the persistent storage is enabled.
The reason is that, currently, there is one previous-batch token alive;
after it's used, it's replaced with another gap and the events it served
to request from the server.

When persistent storage will be enabled, we'll have situations like the
one shown in the test code, where we can have multiple previous-batch
token alive at the same time. In that case, we'll need to back-paginate
from the most recent events to the least recent events, and not the
other way around, or we'll have holes in the timeline that won't be
filled until we got to the start of the timeline.
2024-12-09 15:57:14 +01:00
Kévin Commaille a1a04ee513 chore: Remove MSRV from READMEs
It can be found in Cargo.toml.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-09 16:22:00 +02:00
Benjamin Bouvier affdc25256 refactor(linked chunk): rename len() to num_items()
This makes it clearer that it's only concerned about the number of
items, not the number of chunks.
2024-12-09 14:46:21 +01:00
Benjamin Bouvier 8db78efbbc fix(event cache): use a correcter heuristic to decide whether to add initial events or not
Thanks Hywan for spotting the issue.
2024-12-09 14:46:21 +01:00
Kévin Commaille d8184e72eb fix(media): Make sure that local MXC URIs only try to get media from the cache and ignore requested dimensions (#4387)
Extracted from #4329. This does not change the `MediaFormat` of the
request used in the media cache by the send queue.

---------

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-09 13:43:49 +01:00
torrybr 3bd57d4307 feat(sdk): support for observing m.beacon events 2024-12-09 10:33:37 +01:00
Kévin Commaille 42193f1b06 chore(xtask): Remove unnecessary lifetime
`const` variables are always `'static`. Detected by clippy.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-08 16:57:22 +01:00
Kévin Commaille a277e6d37f chore(xtask): Disable unexpected_cfgs lint
It is triggered by the `xshell::cmd!` macro, and is fixed in xshell 0.2.7, which we cannot upgrade to.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-12-08 16:57:22 +01:00
Benjamin Bouvier bf6fa4cd55 fix(event cache): don't fill initial items if the room already had events (#4381)
The test requires subtle conditions to trigger:

- initialize a timeline from a room-list-service's room
- start a backpagination with that timeline (so the room event cache's
paginator is busy)
- try to initialize another timeline with the same room-list-service's
room (e.g. because the first room has been closed, and the app using it
doesn't have a room cache)

This would fail, because initializing a timeline calls
`EventCache::add_initial_events()` all the time, which tries to reset
the paginator's state, which assumes the paginator's not paginating at
this point. In a soon future, we'll get rid of the
`add_initial_events()` function because the event cache will handle its
own persistent storage; in the meantime, a correct fix is to skip
`add_initial_events()` if there was already something in the linked
chunk. After all, we're likely to fill the initial events with the same
events all the time, or a subset of more recent events. By doing that,
we're likely keeping *more* events in the linked chunk, instead.

Thanks to @stefanceriu for reporting the issue and confirming the fix
works!
2024-12-06 12:37:34 +01:00
Damir Jelić 6501a44e6a feat: Add support for MSC4171
Introduce support for MSC4171, enabling the designation of certain users
as service members. These flagged users are excluded from the room
display name calculation.

MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4171
2024-12-05 14:23:36 +01:00
Mathieu Velten ee30008f38 feat: Accept any string as a key for m.direct account data (#4228)
This is the follow up of this [Ruma
PR](https://github.com/ruma/ruma/pull/1946) for the SDK.

---------

Signed-off-by: Mathieu Velten <mathieu@velten.xyz>
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-05 12:37:16 +00:00
Damir Jelić 22cb8a1878 chore: Bump the pprof version to fix a security issue 2024-12-05 12:08:25 +01:00
Timo 111f916a78 feat(WidgetDriver): Pass Matrix API errors to the widget (#4241)
Currently the WidgetDriver just returns unspecified error strings to the
widget that can be used to display an issue description to the user. It
is not helpful to run code like a retry or other error mitigation logic.

Here it is proposed to add standardized errors for issues that every
widget driver implementation can run into (all matrix cs api errors):
https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1838804895

This PR forwards the errors that occur during the widget processing to
the widget in the correct format.

NOTE:
It does not include request Url and http Headers. See also:
https://github.com/matrix-org/matrix-spec-proposals/pull/2762#discussion_r1839802292

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-04 16:52:02 +00:00
Benjamin Bouvier a6e1f05957 test(event cache): use the MatrixMockServer for integration testing 2024-12-04 17:18:45 +01:00
Benjamin Bouvier 0b64c68191 test(event cache): make use of macros to avoid manual timeouts 2024-12-04 17:18:45 +01:00
Benjamin Bouvier 713039279c Enable persistent storage in multiverse
And fix an issue that would cause a crash because a timeline wasn't
initialized and we tried to unwrap it later.
2024-12-04 17:18:45 +01:00
Benjamin Bouvier d317e5d73c feat(event cache): don't react specifically to limited timelines, when storage's enabled 2024-12-04 17:18:45 +01:00
Damir Jelić ee93c278df chore: Update the hashbrown version we're using 2024-12-04 16:31:15 +01:00
Valere 1009ea86ae Merge pull request #4305 from matrix-org/valere/support_for_withheld_reason
feat(crypto): Add optional withheld reason to `UnableToDecryptReason`
2024-12-04 15:57:22 +01:00
Damir Jelić 7d8e7af308 Revert "chore(ui): Unify the logic for timeline item insertions"
This reverts commit d2ecd745f6.
2024-12-04 15:55:49 +01:00
Damir Jelić 136522c694 Revert "doc(timeline): tweak comments when inserting a new item"
This reverts commit 197da2c585.
2024-12-04 15:55:49 +01:00
Valere 6801811226 feat(crypto): Supports new UtdCause variants for withheld keys
Adds new UtdCause variants for withheld keys, enabling applications to display customised messages when an Unable-To-Decrypt message is expected.

refactor(crypto): Move WithheldCode from crypto to common crate
2024-12-04 15:33:23 +01:00
Benjamin Bouvier a4434d79c9 feat(event cache): strip bundled relations before persisting events 2024-12-04 12:35:58 +01:00
Benjamin Bouvier e0b1b5dc05 test: don't use the event cache storage but regular syncs instead 2024-12-04 12:35:46 +01:00
Benjamin Bouvier 1a63d8f0b7 task(event cache): ignore add_initial_events() when the event cache storage has been enabled
Not worth testing IMO, since this is about the "temporary" API we're
going to remove in subsequent patches.
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5bf3b11edf test: rewrite test_send_edit_when_timeline_is_clear to not use add_initial_items
Moar MatrixMockServer \o/
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 8f1722f2a8 test: have the PendingEdit test helper use the matrix mock server and event cache storage 2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5d95387935 test: remove unused adding of initial events in sliding sync test helper 2024-12-04 12:35:46 +01:00
Benjamin Bouvier bd93a9a40e test: remvoe use of add_initial_events in test_add_initial_events
And make it a smoke test that the event cache correctly gets events it
retrieves from sync.
2024-12-04 12:35:46 +01:00
Benjamin Bouvier 5cde4a6630 test: remove use of add_initial_events in test_ignored_unignored 2024-12-04 12:35:46 +01:00
Benjamin Bouvier de5511f009 test: rewrite test_ignored_unignored so it makes use of the MatrixMockServer 2024-12-04 12:35:46 +01:00
Damir Jelić 9bdd9fa831 chore: Update the RELEASING file so it doesn't mention git-cliff anymore 2024-12-04 11:25:00 +01:00
Damir Jelić 48bb3dbbe7 chore: Update our contributing guide for the manual changelog entries 2024-12-04 11:25:00 +01:00
Damir Jelić b8bf847fc1 chore: Set up cargo-release to update the versions in the changelog 2024-12-04 11:25:00 +01:00
Damir Jelić 17812b6949 chore: Remove our cliff config and don't use cliff to generate changelogs 2024-12-04 11:25:00 +01:00
Damir Jelić bab979aaf4 chore: Update our changelogs 2024-12-04 11:25:00 +01:00
Damir Jelić 42778dc79d chore: Replace git-cliff in the weekly-report command 2024-12-04 11:25:00 +01:00
Damir Jelić a948be9c85 chore: Downgrade xshell due to broken stdin interactions
Since xshell 0.2.3 the behavior of the run() function has changed in a
incompatible manner. Namely the stdin for the run() function no longer
inherits stdin from the shell. This makes it impossible for commands
that are executed by the run() function to accept input from the shell.

We don't use this functionality in many places but the `xtask release
prepare` command is now broken.

Let's just pin xshell to a working version while we wait for this to be
resolved upstream.

Upstream-issue: https://github.com/matklad/xshell/issues/63
2024-12-04 11:22:43 +01:00
Stefan Ceriu 9c381c1022 feat(ffi): expose a generic message_filtered_timeline that can be configured to only include RoomMessage type events and filter those further based on their message type.
Virtual timeline items will still be provided and the `default_event_filter` will be applied before everything else.
Instances of these timelines will be used to power the 2 different tabs shown on the new media browser. The client will be responsible for interacting with it similar to a normal timeline and transforming its data into something renderable e.g. section by date separators (which will be made configurable in a follow up PR)
2024-12-04 11:41:25 +02:00
Andy Balaam 9002f82659 task(backup_tests): Move mock helpers into MatrixMockServer 2024-12-03 14:55:41 +00:00
Andy Balaam 5f7fb4699a task(backup_tests): Split exists_on_server test into separate tests 2024-12-03 14:55:41 +00:00
Andy Balaam 5907104e0e task(backup_tests): Use helper functions to shorten exists_on_server tests 2024-12-03 14:55:41 +00:00
Ivan Enderlin d7dff5b026 refactor(ui): Add AllRemoteEvents::get_by_event_id_mut.
Having a mutable iterator can be dangerous and is probably too generic
regarding the safety we want to add around the `AllRemoteEvents` type.

This patch removes `iter_mut` and replaces it by its only use case:
`get_by_event_id_mut`.
2024-12-03 13:52:42 +01:00
Ivan Enderlin cabde8ed11 refactor(ui): AllRemoteEvents::back becomes last to add semantics.
This patch renames `AllRemoteEvents::back` to `last` so that it now gets
a specific semantics instead of being generic.
2024-12-03 13:52:42 +01:00
Ivan Enderlin b02fd92ad0 refactor(ui): Introduce the AllRemoteEvents type.
This patch replaces `VecDeque<EventMeta>` by `AllRemoteEvents` which
is a wrapper type around `VecDeque<EventMeta>`, but this new type aims
at adding semantics API rather than a generic API. It also helps to
isolate the use of these values and to know precisely when and how they
are used.

As a first step, `AllRemoteEvents` implements a generic API to not break
the existing code. Next patches will revisit that a little bit step
by step.
2024-12-03 13:52:42 +01:00
Ivan Enderlin 9be8578aff doc(ui): Explain why all_remote_events is necessary. 2024-12-03 13:26:26 +01:00
Ivan Enderlin 4f28dd85bf refactor: TimelineStateTransaction::add_or_update_remote_event always return true.
The `add_or_update_remote_event` method always returns `true`. This
patch updates the method to return nothing, and cleans up the call sites
accordingly. This patch also adds comments to clarify the code flow.

The bool value returned by `add_or_update_remote_event` was supposed
to be `false` if the event was duplicated. First off, as soon as the
`Timeline` receives its events from the `EventCache` via `VectorDiff`,
the `event_cache::Deduplicator` will take care of deduplication,
so the `Timeline` won't have to handle that itself. Second,
`add_or_update_remote_event` was sometimes removing an event, but it
was re-inserting a new one immediately without returning `false`: it was
never returning `false` because a new event was always added.
2024-12-03 13:26:26 +01:00
Damir Jelić 74119e8861 Revert "chore: Remove Ruma from the cargo-deny git dep allow list"
This reverts commit f256fe4b24.

As discussed, we want to prioritize the testing of new Ruma features
over stability.
2024-12-03 12:58:37 +01:00
Timo e76b8f7e15 tests: Refactor the widget tests to use MockMatrixServer (#4236)
This is a follow up PR on https://github.com/matrix-org/matrix-rust-sdk/pull/3987.

And tries to use the `MockMatrixServer` wherever reasonable in the
widget integration tests.

---------

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-03 08:36:24 +00:00
Integral 31bd5c6790 refactor: replace static with const for global constants
Signed-off-by: Integral <integral@member.fsf.org>
2024-12-03 09:13:57 +01:00
Andy Balaam 50f036d283 task(crypto_tests): Fix test that will fail when we handle backups correctly
This test was checking that a new device which has access to backups
returned an unknown UTD when it was given empty JSON for the event. It
was only passing because we currently have incorrect behaviour when
backups are enabled.

The fix is to make the device old and without access to backups, so that
we still consider this UTD unexpected.
2024-12-02 17:34:44 +00:00
Andy Balaam 8c73f0c655 task(crypto_tests): Remove duplicate test 2024-12-02 17:34:44 +00:00
Andy Balaam 8de76deb1b task(crypto_tests): Re-arrange and reword utd_cause tests 2024-12-02 17:34:44 +00:00
Andy Balaam b65728d46f task(crypto_tests): Shorten utd_cause tests with helper functions for devices 2024-12-02 17:34:44 +00:00
Andy Balaam 0b4b4ea791 task(crypto_tests): Shorten utd_cause tests with utility functions for UnableToDecryptInfos 2024-12-02 17:34:44 +00:00
Andy Balaam 552ab81739 task(crypto_tests): Add comments and clarify tests for determining UTD causes 2024-12-02 17:34:44 +00:00
dependabot[bot] d49d12249a build(deps): bump crate-ci/typos from 1.27.3 to 1.28.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.27.3 to 1.28.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.27.3...v1.28.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 17:19:21 +01:00
Gabriel Féron ed1d406b72 fix(store): fix indexing issue in ObservableMap (#4346)
Any `.remove` operation called on a `ObservableMap` did not re-index
`values` _after_ the removed position, which means any later operation
on elements inserted after the previously removed key would either be
fetched wrongly, or panic when using `.get_or_create`.

This PR fixes these two related bugs and adds extra test cases.

---------

Signed-off-by: g@leirbag.net
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-12-02 16:00:19 +00:00
Ivan Enderlin 80a48f53ad refactor: Rename add_or_update_event to add_or_update_remote_event. 2024-12-02 16:45:35 +01:00
Ivan Enderlin 51cfaaacee refactor: Rename TimelineMetadata::all_events to all_remote_events.
This patch renames the `all_events` field to `all_remote_events` in
`TimelineMetada` for the sake of clarity.
2024-12-02 16:45:35 +01:00
Benjamin Bouvier 2f9866cf04 test(event cache): test that the event cache correctly reads updates 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 7de74e2c04 feat(event cache): reload the linked chunk from the store, if storage's enabled 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 019de4ffa0 test(event cache): add a test that the event cache correctly stores updates 2024-12-02 16:27:05 +01:00
Benjamin Bouvier 9f1e3c179b feat(event cache): propagate linked chunk in-memory updates to storage (conditionally) 2024-12-02 16:27:05 +01:00
Damir Jelić 17e17f0b9c ci: Build the Mac framework using the reldbg profile 2024-12-02 15:54:47 +01:00
Benjamin Bouvier 5da36d13c8 fix(event cache): consider empty items chunks in the memory store 2024-12-02 14:09:42 +01:00
Benjamin Bouvier cce322f9c8 test(event cache): add integration test for handling updates and reloading a linked chunk 2024-12-02 14:09:42 +01:00
Benjamin Bouvier ed3b03f454 feat(event cache): implement reloading a linked chunk from the memory store too 2024-12-02 14:09:42 +01:00
Benjamin Bouvier 27e1cded2e feat(event cache): reload a linked chunk from a sqlite store 2024-12-02 14:09:42 +01:00
Ivan Enderlin ad3d1fb6b3 refactor(ui): Use an iterator instead of Vec to represent events.
This patch changes `TimelineStateTransaction::add_remote_events_at`
to take an `IntoIterator<Item = Into<SyncTimelineEvent>>`
for `events`. In the current code, it saves one
`iter().map(Into::into).collect::<Vec<_>>()`, but it will save another
one when we will support `VectorDiff`s coming from the `EventCache`.

It also avoids to allocate a vector to pass new events (this mostly
happens in the test, but it can happen in real life).
2024-12-02 13:09:07 +01:00
Jorge Martín d2fecb6701 fix(room_preview): When requesting a room summary, use fallback server names
If no server names are provided for the room summary request and the
room's server name doesn't match the current user's server name, add the
room alias/id server name as a fallback value. This seems to fix room
preview through federation.

Also, when getting a summary for a room list item, if it's an invite
one, add the server name of the inviter's user id as another possible
fallback.

Changelog: Use the inviter's server name and the server name from the
room alias as fallback values for the via parameter when requesting the
room summary from the homeserver.
2024-12-02 12:11:47 +01:00
Kévin Commaille 685386df13 chore(xtask): Fix scope of push_dir
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 18:03:54 +01:00
Kévin Commaille f94b202341 chore(xtask): Upgrade xshell
Gets rid of an unexpected_cfgs warning.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 18:03:54 +01:00
Kévin Commaille d1a6956e77 chore(sdk): Disable unused_async clippy lint for unimplemented method
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-30 09:21:23 +01:00
Damir Jelić 2d2215edbe chore: Make use of the member event builder in the EventFactory 2024-11-30 09:20:49 +01:00
Damir Jelić bcd0d20e2f test: Add a method to build m.room.member events in the EventFactory 2024-11-30 09:20:49 +01:00
Kévin Commaille ba5881355d chore(test): Upgrade ctor
Fixes the `unexpected_cfgs` warning so it doesn't need to be disabled anymore.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-29 19:59:24 +01:00
Andy Balaam 1072d0a019 chore(js_tracing): Elide explicit lifetime to satisfy clippy 2024-11-29 18:45:45 +01:00
Damir Jelić 783c86aa78 ci: Build the Mac framework in release mode
The dev profile fails with a linker issue about not finding the
__chkstk_darwin symbol.
2024-11-29 18:45:45 +01:00
Damir Jelić 5564fe8852 ci: Bump the mac OS runner to 15 2024-11-29 18:45:45 +01:00
Damir Jelić e1f0037fd5 chore: Define the lifetime of some const strings explicitly 2024-11-28 11:53:35 +01:00
Benjamin Bouvier daa984f7de feat(event cache store): enable foreign keys pragma \o/ 2024-11-28 11:48:46 +01:00
Benjamin Bouvier aa0eb760de test(event cache): add a test for reading events from multiple rooms
This was to make sure that we can search by blob.
2024-11-28 11:48:46 +01:00
Benjamin Bouvier 9ed65bc321 task(event cache): address review points 2024-11-28 11:48:46 +01:00
Benjamin Bouvier ce95b6089f doc(event cache): add the copyright notice and basic module doc comment 2024-11-28 11:48:46 +01:00
Benjamin Bouvier c6ba71ae33 feat(event cache): allow reloading from the store, and test functionalities
This required adding support for *reading* out of the event cache, for
the sqlite backend. This paves the way for the next PR (reload from the
cache), and it should also help with testing at the `EventCacheStore`
trait layer some day.
2024-11-28 11:48:46 +01:00
Benjamin Bouvier e57d38cf57 doc(common): add a note that a decrypted raw event always has a room id 2024-11-28 11:48:46 +01:00
Benjamin Bouvier 9bea0cff24 feat(event cache): implement the sqlite backend for events 2024-11-28 11:48:46 +01:00
Benjamin Bouvier 197da2c585 doc(timeline): tweak comments when inserting a new item
A comment was duplicating (the first trace! that's removed here), and
the second block comment only applied to new items, and was not as
concise as it could be.
2024-11-28 10:09:50 +01:00
Ivan Enderlin d2ecd745f6 chore(ui): Unify the logic for timeline item insertions
This patch unifies the logic for inserting timeline items at `Start`
and `End` positions. Both `TimelineItemPositions` can share the same
implementation, making separate logic unnecessary. Previously, `End`
included a duplicated events check as well, while `Start` did not, leading
to inconsistency.

The changes strictly involve moving and refactoring, with no functional
modifications.
2024-11-28 08:25:16 +01:00
Damir Jelić e99939db85 refactor(crypto): Rename the IncomingResponse enum to AnyIncomingResponse 2024-11-27 19:55:27 +01:00
Damir Jelić 600a708e7b refactor!(crypto): Rename the OutgoingRequests enum to AnyOutgoingRequest 2024-11-27 19:55:27 +01:00
Damir Jelić a94a5f1716 chore(crypto): Split out the requests module 2024-11-27 19:55:27 +01:00
Damir Jelić 46064680ce refactor!(crypto): Don't re-export the request types from the request module 2024-11-27 19:55:27 +01:00
Damir Jelić 6fe5acfc97 refactor(crypto): Move the requests module under the types module 2024-11-27 19:55:27 +01:00
Valere 3369903766 Merge pull request #4275 from matrix-org/valere/utd_hook_historical_message
feat(utd_hook): Report device-historical expected UTD with new reason
2024-11-27 18:23:53 +01:00
Valere a0c86d9645 feat(utd_hook): Report historical expected UTD with new reason
This PR introduces a new variant to `UtdCause` specifically for device-historical messages (`HistoricalMessage`). These messages cannot be decrypted if key storage is inaccessible. Applications can leverage this new variant to provide more informative error messages to users.
2024-11-27 18:09:06 +01:00
Damir Jelić 7a454888a3 chore: Bump the deps and move some of them to the workspace 2024-11-27 17:03:50 +01:00
Ivan Enderlin 37f52e1c6c fix(common): LinkedChunk emits an Update::NewItemsChunk when constructed.
This patch updates `LinkedChunk::new_with_update_history` to emit an
`Update::NewItemsChunk` because the first chunk is created and it must
emit an update accordingly.
2024-11-27 14:40:26 +01:00
Ivan Enderlin 185423539e test(ui): Fix the test_echo test.
This patch fixes the `test_echo` test. It was doing the following:

* client sends an event to the server,
* servers acknowledges with the ID `$wWgymRfo7ri1uQx0NXO40vLJ`,
* client syncs and the server returns one event with ID `$7at8sd:localhost`,
* the test expects those 2 events to be the same!, which is incorrect.

The test was working because the transaction IDs are the same, but
that's an abuse of the existing code (the code will change soon, another
patch is coming). Whatever the code does: the connection must be based
on the event ID, not the transaction ID.
2024-11-27 14:29:24 +01:00
Stefan Ceriu 9e20659d5d chore: bring back MediaSource JSON serialization methods 2024-11-27 15:13:24 +02:00
Damir Jelić 7783188769 chore: Box the OidcSession so the AuthSession enum isn't unnecessarily big 2024-11-27 13:23:34 +01:00
Damir Jelić 514af54c4c chore: Fix some clippy warnings about our docs 2024-11-27 13:23:34 +01:00
Damir Jelić ad615b7612 chore: Fix some clippy lint warnings around the usage of map_or 2024-11-27 13:23:34 +01:00
Damir Jelić a1b7906a7d chore: Fix some clippy lints around lifetimes 2024-11-27 13:23:34 +01:00
Damir Jelić 79c8d2c345 chore: Don't build the docs for xtask
Building the docs for xtask spews a bunch of unexpected cfg warnings. As
these warnings come from a macro in a dependency and the docs for xtask
don't exist nor will, let's just not build them with the rest of the
docs.
2024-11-27 13:23:34 +01:00
Damir Jelić dcf6af405d chore: Silence unexpected cfg warnings
These are all coming from macro invocations of macros that are defined
in other crates. It's likely a clippy issue. We should try to revert
this the next time we bump the nightly version we're using.
2024-11-27 13:23:34 +01:00
Damir Jelić bb598b61a5 chore: Bump the nightly version we use for the CI 2024-11-27 13:23:34 +01:00
Ivan Enderlin 1c554c4912 chore(ui): Clarifies what TimelineItemPosition::UpdateDecrypted holds.
This patch tries to clear confusion around
`TimelineItemPosition::UpdateDecrypted(usize)`: it does contains
a timeline item index. This patch changes to
`TimelineItemPosition::UpdateDecrypted { timeline_item_index: usize }`
2024-11-27 12:04:59 +01:00
Benjamin Bouvier 21f8b7ed31 refactor(linked chunk): simplify further impl of the LinkedChunkRebuilder 2024-11-27 11:01:44 +01:00
Benjamin Bouvier 23ee8e25dd feat(linked chunk): add a way to reconstruct a linked chunk from its raw representation 2024-11-27 11:01:44 +01:00
Benjamin Bouvier 1098095846 refactor(linked chunk): replace LinkedChunk::len() with a simpler implementation
It's unused so it's mostly cosmetic, and it's trivial to reimplement
using `linked_chunk.items().count()`; let's do that instead of keeping
the perfect exact count synchronized with the chunks, which pollutes the
code in a few places.
2024-11-27 10:16:12 +01:00
Ivan Enderlin 3e7d7e8a31 chore(ui): Rename TimelineEnd to TimelineNewItemPosition.
This patch renames `TimelineEnd` into `TimelineNewItemPosition` for
2 reasons:

1. In the following patches, we will introduce a new variant to insert
   at a specific index, so the suffix `End` would no longer make sense.

2. It's exactly like `TimelineItemPosition` except that it's used
   only and strictly only to add **new** items, which is why we can't use
   `TimelineItemPosition` because it contains the `UpdateDecrypted`
   variant. This renaming reflects it's only about **new** items.

This patch takes the opportunity to move the `RemoteEventOrigin` inside
`TimelineNewItemPosition` to simplify method signatures. They always
go together.
2024-11-26 20:29:31 +01:00
Benjamin Bouvier 2c45316bcb fixup! fix(room): make Room::history_visibility() return an Option 2024-11-26 19:02:46 +01:00
Benjamin Bouvier 8dc7c1f876 fix(ui): have the room list service require the create and history visibility events
These two are required to properly compute the room preview of a joined
room:

- m.room.create ends up filling the `room_type` (space or not)
- m.room.history_visibility ends up filling the `is_world_readable`
  field.
2024-11-26 19:02:46 +01:00
Benjamin Bouvier db84936dcd fix(room): make Room::history_visibility() return an Option
And introduce `Room::history_visibility_or_default()` to return a better
sensible default, according to the spec.
2024-11-26 19:02:46 +01:00
Kévin Commaille 75d7d07013 chore(ffi): Fix thumbnail size info
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:45:54 +01:00
Kévin Commaille d4d5f45edc feat(media)!: Make all fields of Thumbnail required
It seems sensible to assume that if a client is able to generate a thumbnail,
it should be able to get all this information for it too.
A thumbnail with no information is not really useful, as we don't know when it could be used instead of the original image.

Removes `BaseThumbnailInfo`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Kévin Commaille d0257d1cb2 refactor(media): Add method to split Thumbnail into parts
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Kévin Commaille ecf44348cf fix(client): Do not use the encrypted original file's content type as the encrypted thumbnail's content type
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-26 15:20:07 +01:00
Ivan Enderlin cc8bc05537 refactor: RoomEvents::reset really clear the linked chunk.
This patch updates `RoomEvents::reset` to not drop the `LinkedChunk` to
clear it.
2024-11-26 15:19:24 +01:00
Ivan Enderlin 728d646ce2 fix(common): AsVector clears its internal state on Update::Clear.
This patch fixes a bug in `AsVector`: when an `Update::Clear` value
is received, `AsVector`'s internal state must be cleared too, i.e. the
`UpdateToVectorDiff::chunks` field should be reset to an initial value!

This patch adds a test to ensure this works as expected.
2024-11-26 15:19:24 +01:00
Stefan Ceriu ca397dca0f feat(ffi): wrap Ruma MediaSources and run validations before passing them over FFI
Ruma doesn't currently validate mxuri's and as such `MediaSource`s passed over FFI can contain invalid/empty URLs. This change introduces a wrapper type around Ruma's and failable transformations so that appropiate actions can be taken beforehand e.g. returning a `TimelineItemContent::FailedToParseMessageLike` or nil-ing out the thumbnail info.
2024-11-26 15:40:24 +02:00
Benjamin Bouvier 1fbe6815c3 task(event cache): log whenever we receive an ignore user list change 2024-11-26 12:29:34 +01:00
Ivan Enderlin c61f70727f fix: RelationalLinkedChunk handles Update::Clear.
What the title says.
2024-11-25 17:45:01 +01:00
Ivan Enderlin 2abbf58825 feat(common): Implement LinkedChunk::clear.
This patch implements `LinkedChunk::clear`. The code from `impl Drop
for LinkedChunk` has been moved inside `Ends::clear`, and replaced by
a simple `self.links.clear()`. In addition, `LinkedChunk::clear` indeed
calls `self.links.clear()` but also resets all fields.

This patch adds the `Clear` variant to `Update`.

This patch updates `AsVector` to emit a `VectorDiff::Clear` on
`Update::Clear`.

Finally, this patch adds the necessary tests.
2024-11-25 17:08:43 +01:00
Ivan Enderlin b979b2ea1e doc(common): Fix typos. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 24b968ad39 refactor: EventCacheStore::handle_linked_chunk_updates takes a Vec<Update>.
This patch updates `EventCacheStore::handle_linked_chunk_updates` to
take a `Vec<Update<Item, Gap>>` instead of `&[Update<Item, Gap>]`.
In fact, `linked_chunk::ObservableUpdates::take()` already returns a
`Vec<Update<Item, Gap>>`; we can simply forward this `Vec` up to here
without any further clones.
2024-11-25 17:08:27 +01:00
Ivan Enderlin faa8aa2b9c fix(base): Move all fields of MemoryStore inside a StdRwLock<_>.
This patch creates a new `MemoryStoreInner` and moves all fields from
`MemoryStore` into this new type. All locks are removed, but a new lock
is added around `MemoryStoreInner`. That way we have a single lock.
2024-11-25 17:08:27 +01:00
Ivan Enderlin db9ee9d87b refactor: Add constructors for Position and ChunkIdentifier.
This patch adds constructors for `Position` and `ChunkIdentifier` so
that we keep their inner values private.
2024-11-25 17:08:27 +01:00
Ivan Enderlin 1dbb494b94 feat(common): RelationalLinkedChunk stores the RoomId. 2024-11-25 17:08:27 +01:00
Ivan Enderlin fe52b4cb78 feat(common): EventCacheStore::handle_linked_chunk_updates takes a &RoomId. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 5519442ad8 doc(common): Fix a typo. 2024-11-25 17:08:27 +01:00
Ivan Enderlin 88363d8033 feat(base): MemoryStore uses RelationalLinkedChunk to store events.
That's it.
2024-11-25 17:08:27 +01:00
Ivan Enderlin fb5d8f29ac feat(common): Implement RelationalLinkedChunk.
A `RelationalLinkedChunk` is like a `LinkedChunk` but with a relational
layout, similar to what we would have in a database.

This is used by memory stores. The idea is to have a data layout that
is similar for memory stores and for relational database stores, to
represent a `LinkedChunk`.

This type is also designed to receive `Update`. Applying `Update`s
directly on a `LinkedChunk` is not ideal and particularly not trivial
as the `Update`s do _not_ match the internal data layout of the
`LinkedChunk`, they have been designed for storages, like a relational
database for example.

This type is not as performant as `LinkedChunk` (in terms of memory
layout, CPU caches etc.). It is only designed to be used in memory
stores, which are mostly used for test purposes or light usages of the
SDK.
2024-11-25 17:08:27 +01:00
Benjamin Bouvier 912b121d27 feat(timeline): make more errors transparent 2024-11-25 15:11:02 +01:00
Benjamin Bouvier 2e975d9b19 fix(base): all EventCacheStoreLock must refer to the same underlying cross-process lock
And not duplicate it once per `EventCacheStoreLock`.
2024-11-25 15:11:02 +01:00
Benjamin Bouvier edc93e62b4 task(sdk): expose the SqliteEventCacheStore from the SDK crate
And use it in multiverse.
2024-11-25 15:11:02 +01:00
Ivan Enderlin 9d6ffa951f doc(sdk): Specify how the Client::observe_events works. 2024-11-25 11:49:36 +01:00
Benjamin Bouvier 079ec023b7 task(oidc): add logs when refreshing an OIDC token 2024-11-25 10:58:50 +01:00
Damir Jelić e55a1c7e00 chore: Rework the crypto crate README 2024-11-22 18:20:38 +01:00
Damir Jelić ddd737e4d8 docs: Add a tutorial to the crypto crate
Changelog: Add a tutorial describing how to add end-to-end encryption
support to an existing library.
2024-11-22 18:20:38 +01:00
Mauro Romito 38a15afc9c build (apple): add dynamic type to debug package 2024-11-22 18:44:48 +02:00
Jorge Martín fa93daabd2 feat(ffi): Add RoomInfo::join_rule field to bindings
Breaking-Change: Add `RoomInfo::join_rule` field, remove `RoomInfo::is_public` in the FFI crate, as they contain the same info.
2024-11-22 16:09:55 +01:00
Jorge Martín 6b0987385e refactor(room_preview): make RoomPreview use the local known data only for joined rooms
When instantiating a room preview, previously it would try to just check if the room exists locally either as joined, invited, knocked, left, etc., and then retrieve the info we cached about it.

While this seems fine for most cases, it turns out for non-joined rooms, the info we have locally will **always** be the one we received when the invite/knock/leave action took place and it'll never be updated,
so we may have the case where we knock into a room, never receive a response, someone changes the join rule of the room to something else and we'll think about this room as a 'request to join' room until we clear the local cache.

To prevent that, we can only use the local data for joined rooms, which are constantly updated, and try to use the room summary API and other fallbacks for the rest, even if they're rooms known to us.
2024-11-22 14:20:50 +01:00
Benjamin Bouvier 48fbda844f fix(oidc): make sure we keep track of an ongoing OIDC refresh up to the end
There's a lock making sure we're not doing multiple refreshes of an OIDC
token at the same time. Unfortunately, this lock could be dropped, if
the task spawned by the inner function was detached.

The lock must be held throughout the entire detached task's lifetime,
which this refactoring ensures, by setting the lock's result after
calling the inner function.
2024-11-21 18:36:11 +01:00
Damir Jelić bc70f3c051 refactor: Clean up the Room::compute_display_name() method 2024-11-21 14:34:38 +01:00
Benjamin Bouvier d2f255d613 feat(ffi): add a new function helper to create a caption edit
It has the same semantics used when creating a caption (if no formatted
caption is provided, assume a provided caption is markdown and use that
as the formatted caption).
2024-11-21 10:40:39 +01:00
Doug bf86b168d7 feat(timeline): mark media events as editable in the timeline (#4303)
This PR makes audio, file, image and video messages be editable so that
the timeline signals when it is possible to use #4277/#4300 for editing
captions.
2024-11-21 10:25:32 +01:00
Ivan Enderlin e5ca44bb04 feat(base): Add EventCacheStore::handle_linked_chunk_updates.
This patch adds the `handle_linked_chunk_updates` method on the
`EventCacheStore` trait. Part of
https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
2024-11-20 16:39:49 +01:00
Benjamin Bouvier 1f563c964c task: add manual Sync impl for VerificationCache to avoid overflowing evaluation requirements 2024-11-20 16:33:39 +01:00
Benjamin Bouvier 9a9730d59e task: move the EventFactory to the matrix-sdk-test crate
This makes it available to the crypto crate, by lowering it into the
local dependency tree.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier af3ce4b32b task: remove the dependency from common to test
The (matrix-sdk-)common crate used the (matrix-sdk-)test crate only to
benefit from the `async_test` proc macro, which is conveniently defined
in another crate.

My goal is to make `EventFactory`, at this point in the commit history,
defined in the main SDK crate, available in the test crate.
`EventFactory` makes use of some types defined in common, so there's a
circular dependency at the moment.

To split this circular dependency, I've changed the common crate to
depend on the test-macro crate directly; now the test crate can depend
on the common crate, and everybody's happy.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier 03f0c3a001 task: move the MockClientBuilder to its own mock file 2024-11-20 16:33:39 +01:00
Benjamin Bouvier 639833acf1 task: move test_utils.rs to test_utils/mod.rs
This is more in line with what we're doing in the SDK in general.
2024-11-20 16:33:39 +01:00
Benjamin Bouvier 60893d2797 test(send_queue): add tests for editing a caption while media not sent yet
test(timeline): add an integration test for sending an attachment

test(timeline): add tests for multiple caption edits and local reaction to a media upload
2024-11-20 10:11:56 +01:00
Benjamin Bouvier 9e45111d8b feat(send queue): allow updating caption while the media is being sent 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 0080f17c1f feat(base): add a way to update a dependent send queue request 2024-11-20 10:11:56 +01:00
Benjamin Bouvier fa47af3dd6 refactor!(base): rename StateStore::update_dependent_queued_request to mark_dependent_queued_requests_as_ready 2024-11-20 10:11:56 +01:00
Benjamin Bouvier c4ff07124b feat(ffi): allow editing a media caption from the FFI layer 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 900cf5d071 room: create edits to add a caption to a media event 2024-11-20 10:11:56 +01:00
Benjamin Bouvier 8a6ced0e8f fix(send queue): when adding a local reaction, look for media events in dependent requests too 2024-11-19 17:22:06 +01:00
Benjamin Bouvier f20401c657 test(timeline): add an integration test for sending an attachment in the timeline
Also includes a caption for a file media event, which acts as a
regression test for the previous commit.
2024-11-19 16:59:31 +01:00
Benjamin Bouvier b987fc1de2 fix(media): include the formatted caption and filename for audio and file attachments too 2024-11-19 16:59:31 +01:00
Benjamin Bouvier efeac2ef39 fix(base): clear a room's send queue and dependent event queue after removing it from the state store 2024-11-19 16:50:35 +01:00
Valere 6b80055bd2 fix(utd_hook): Fix regression causing retry to report false late decrypt (#4252)
There has been a recent change on `Decryptor::decrypt_event_impl` causing
the function to return an TimelineEvent of kind unable to decrypt
instead of failing with an error.

The `late_decrypt` detection code was not changed, causing any retry to
mark UTDs as late decrypt.
2024-11-19 16:40:18 +01:00
Jorge Martín 0af53e99ee feat(room_preview): Compute display name for RoomPreview when possible 2024-11-19 16:11:09 +01:00
Jorge Martín bc0c2a6be2 feat(room_preview): Add RoomPreview::heroes field for known rooms 2024-11-19 16:11:09 +01:00
Damir Jelić 0b16d488ad chore: Release matrix-sdk version 0.8.0 (#4291)
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-11-19 14:11:19 +01:00
Damir Jelić d40aac89cb fix: Use the DisplayName struct to protect against homoglyph attacks 2024-11-19 11:54:01 +01:00
Damir Jelić e4ebeb8a42 feat(base): Introduce a DisplayName struct
This patch introduces a struct that normalizes and sanitizes display
names. Display names can be a source of abuse and can contain characters
which might make it hard to distinguish one display name from the other.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Moved the code to the SDK crate so it can be properly tested.
2024-11-08 11:55:26 +01:00
Benjamin Bouvier 965a59d5b8 task(tests): create the client with MatrixMockServer::make_client() instead of embedding one into the struct 2024-11-07 17:37:58 +01:00
Benjamin Bouvier f032d16d20 task(tests): mock upload too 2024-11-07 17:37:58 +01:00
Benjamin Bouvier 57137cdd5b task(tests): introduce prebuilt mocks and mocking helpers 2024-11-07 17:37:58 +01:00
Damir Jelić 5d83808143 feat(base): Consider knocked members to be part of the room for display name disambiguation 2024-11-07 16:41:16 +01:00
Damir Jelić df465a0420 chore(base): Improve the docs for the AmbiguityCache 2024-11-07 16:41:16 +01:00
Damir Jelić 4ca69da93c chore(base): Improve the docs for the DisplayNameUsers struct 2024-11-07 16:41:16 +01:00
Damir Jelić 4039359512 chore(base): Clean up the display name ambiguity calculation logic 2024-11-07 16:41:16 +01:00
Damir Jelić 1304902cb4 refactor(base): Rename AmbiguityMap to DisplayNameUsers
The ambiguity map tracks the users which are using a single display
name, so let's reflect that in the name.
2024-11-07 16:41:16 +01:00
Damir Jelić 219be9b731 refactor(base)!: Rename DisplayName to RoomDisplayName 2024-11-07 16:41:16 +01:00
Benjamin Bouvier 237419c740 feat(media): introduce a stripped down MediaThumbnailSettings::new only taking a width and height
This one is used when caching a thumbnail everywhere, and when
attempting to retrieve it; it gives us a single place where to
coordinate the default `MediaThumbnailSettings` parameters.
2024-11-07 13:04:10 +01:00
Benjamin Bouvier 1658397139 refactor!(media): rename MediaThumbnailsSetting::new to with_method() 2024-11-07 13:04:10 +01:00
Benjamin Bouvier bab6761388 fix(event cache): give looser parameters for the deduplicator's bloom filters
The previous values would lead to super large memory allocations, as
observed with `valgrind --tool=massive` on the tiny test added in this
commit:

- for 400 rooms each having 100 events, this led to 540MB of
allocations.
- for 1000 rooms each having 100 events, this led to 1.5GB of
allocations.

This is not acceptable for any kind of devices, especially for mobile
devices which may be more constrained on memory. The bloom filter is an
optimisation to avoid going through events in the room's event list, so
it shouldn't cause a big toll like that; instead, we can reduce the
parameters values given when creating the filters.

With the given parameters, 1000 rooms each having 100 events leads to
1.2MB of allocations.
2024-11-07 12:59:16 +01:00
Timo 5193c2033f feat(ffi): Auto approve the required widget capabilities for element call raise hand and reaction feature. 2024-11-07 12:42:17 +01:00
Jorge Martín 0f9bc20bb8 fix(ffi): use subscribe_reset for verification_state instead, add a regression test 2024-11-07 12:36:07 +01:00
Jorge Martín d54f2a8b04 fix(encryption): emit an updated current verification state before any network request happens
This way we don't get stuck with an outdated value if there is no network connection.
2024-11-07 12:36:07 +01:00
Jorge Martín 00c4071fe1 feat(ffi): allow VerificationStateListener to emit the current state
With this, we get notified of the current verification state almost immediately.

Without it, you may either call it too soon and receive an `Unknown` state or you might have to call `Encryption::wait_for_e2ee_initialization_tasks()` and wait until it's finished to request a valid state value.
2024-11-07 12:36:07 +01:00
Damir Jelić 65287178d1 chore: Bump futures-util in the lock file
We were locked onto a yanked version of futures-util.
2024-11-07 11:10:56 +01:00
Damir Jelić 90b8015d71 chore: Don't ignore the aquamarine RUST-SEC issue, we bumped aquamarine 2024-11-07 11:10:56 +01:00
Damir Jelić f256fe4b24 chore: Remove Ruma from the cargo-deny git dep allow list 2024-11-07 11:10:56 +01:00
Benjamin Bouvier 8d07f36247 chore(send queue): adapt to new locks around the event cache store 😎 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 77ee02f529 refactor!(media): rename MediaRequest to MediaRequestParameters
Because it's not a request we send to the server; it's some of the
request parameters.
2024-11-06 15:33:51 +01:00
Benjamin Bouvier 566a13b16e refactor!(media): inline MediaThumbnailSize into MediaThumbnailSettings
Changelog: all the fields of `MediaThumbnailSize` have been inlined into
 `MediaThumbnailSettings`, and the former type has been removed.
2024-11-06 15:33:51 +01:00
Benjamin Bouvier 4bbe620d0f feat(timeline): use the send queue for media uploads behind a feature toggle 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 9178e4ce33 chore(send queue): review 2024-11-06 15:33:51 +01:00
Benjamin Bouvier c04a73c28d chore(send queue): move code for media upload to its own file 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 13244d808b chore(send queue): move more code around to split work into smaller functions 2024-11-06 15:33:51 +01:00
Benjamin Bouvier e9d5aa1221 chore(send queue): move code around to avoid an enormous send_attachment method 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 57ad256fe1 doc(send queue): beef up the send queue module comment and describe uploads 2024-11-06 15:33:51 +01:00
Benjamin Bouvier c196a9754b feat(timeline): send medias via the send queue 2024-11-06 15:33:51 +01:00
Benjamin Bouvier a8992f37d7 test(send queue): add a smoke test for sending an attachment with the send queue 2024-11-06 15:33:51 +01:00
Benjamin Bouvier 9483703e35 feat(send queue): allow sending attachments with the send queue 2024-11-06 15:33:51 +01:00
Ivan Enderlin 0942dab2fd doc(base): Document Client::event_cache_store a bit more. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 94bd421a8d refactor: Use a common code for try_take_leased_lock.
This code is shared by all `MemoryStore` implementations.
2024-11-06 15:03:50 +01:00
Ivan Enderlin 7b3eb0b6f1 feat(base,sdk): Client now uses EventCacheStoreLock. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 8b85ff2434 feat(base): Create EventCacheStoreLock. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 94c507dd38 test: Testing the cross-process event cache store. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 37304c8cdc refactor: Implement try_take_leased_lock on SqliteEventCacheStore 2024-11-06 15:03:50 +01:00
Ivan Enderlin 16a86587ea refactor: Implement try_take_leased_lock on MemoryStore. 2024-11-06 15:03:50 +01:00
Ivan Enderlin e24d9b3ce3 feat(base): Create LockableEventCacheStore. 2024-11-06 15:03:50 +01:00
Ivan Enderlin 9f11bced10 chore: Rename BackingStore::Error to BackingStore::LockingError.
The idea is to avoid name conflicts when implementing other traits
that use the `Error` associated type.
2024-11-06 15:03:50 +01:00
Ivan Enderlin e5d4ea5964 chore(base): Simplify &* with .as_ref() or .deref().
This patch replaces a `&*` by a `.as_ref()` and a `.deref()`. The result
is the same but it's just simpler for newcomers to understand what
happens.
2024-11-06 15:03:50 +01:00
Jorge Martín fbc914f586 feat(ffi): add room display name to room alias transformation 2024-11-06 09:22:50 +01:00
Jorge Martín bb2d19a1d8 feat(ffi): add room alias format validation 2024-11-06 09:22:50 +01:00
Ivan Enderlin 933033cc25 fix(sdk): Do not always remove empty chunks from LinkedChunk.
This patch introduces `EmptyChunk`, a new enum used to represent whether
empty chunks must be removed/unlink or kept from the `LinkedChunk`. It
is used by `LinkedChunk::remove_item_at`.

Why is it important? For example, imagine the following situation:

- one inserts a single event in a new chunk (possible if a (sliding)
  sync is done with `timeline_limit=1`),
- one inserts many events at the position of the previous event,
  with one of the new events being a duplicate of the first event
  (possible if a (sliding) sync is done with `timeline_limit=10` this
  time),
- prior to this patch, the older event was removed, resulting in an
  empty chunk, which was removed from the `LinkedChunk`, invalidating
  the insertion position!

So, with this patch:

- `RoomEvents::remove_events` does remove empty chunks, but
- `RoomEvents::remove_events_and_update_insert_position` does NOT remove
  empty chunks, they are kept in case the position wants to insert in this
  same chunk.
2024-11-05 16:56:42 +01:00
Benjamin Bouvier b233aa64d2 chore(timeline): rename TimelineItemPosition::Update to UpdateDecrypted 2024-11-05 16:38:13 +01:00
Damir Jelić ace96e372f chore: Fix a warning from an invalid Cargo.toml config for the OIDC example 2024-11-05 16:30:25 +01:00
Mathieu Velten 8865e2ff74 RoomListLoadingState now yields immediately with current value
This fixes a problem when doing an incremental sync at launch,
where `NotLoaded` event	would not be dispatched	until data became
available or timeout is	reached, leading to app waiting for it.
2024-11-05 12:15:16 +01:00
Mathieu Velten 2fa54e5cfa Activate share_pos on the room-list sliding sync instance 2024-11-05 12:15:16 +01:00
Benjamin Bouvier 04275d7c27 refactor!(room list): remove unneeded argument from RoomList::entries_with_dynamic_adapters
Changelog: the parameter `room_info_notable_update_receiver` was removed
 from `RoomList::entries_with_dynamic_adapters`, since it could be
 inferred internally instead.
2024-11-05 10:02:49 +01:00
Benjamin Bouvier 90d6a37b31 refactor(timeline): factor out in-reply-to updates 2024-11-04 17:50:55 +01:00
Benjamin Bouvier 590c2dd9fd fix(timeline): update responses after a successful decryption
Fixes #4196.
2024-11-04 17:50:55 +01:00
Benjamin Bouvier 478dc0ea90 chore(base): refactor internal helpers related to media
Notably, make it super clear what parameters are required to create the
attachment type, since the function doesn't consume the whole
`AttachmentConfig` for realz.
2024-11-04 17:44:30 +01:00
Benjamin Bouvier 7a422fe126 chore(send queue): rename de to dependent_request 2024-11-04 17:42:47 +01:00
Benjamin Bouvier a739ddfc84 chore(event cache store): update test to reflect that previous events and dependent events are cleared
Because the latest migration would clear events to-be-sent from the send
queue, we need to reflect this in this test.
2024-11-04 17:42:47 +01:00
Benjamin Bouvier 1f2e8c5007 refactor!(event cache store): store the serialized QueuedRequestKind, not a raw event
Changelog: The send queue will now store a serialized
 `QueuedRequestKind` instead of a raw event, which breaks the format.
 As a result, all send queues have been emptied.
2024-11-04 17:42:47 +01:00
Benjamin Bouvier c2a921cb58 chore(send queue): move sending of an event to an helper function 2024-11-04 17:42:47 +01:00
Benjamin Bouvier 06e6cba156 chore(event cache store): Support multiple parent key types for dependent requests
This makes it possible to have different kinds of *parent key*, to
update a dependent request. A dependent request waits for the parent key
to be set, before it can be acted upon; before, it could only be an
event id, because a dependent request would only wait for an event to be
sent. In a soon future, we're going to support uploading medias as
requests, and some subsequent requests will depend on this, but won't be
able to rely on an event id (since an upload doesn't return an
event/event id).

Since this changes the format of `DependentQueuedRequest`, which is
directly serialized into the state stores, I've also cleared the table,
to not have to migrate the data in there. Dependent requests are
supposed to be transient anyways, so it would be a bug if they were many
of them in the queue.

Since a migration was needed anyways, I've also removed the `rename`
annotations (that supported a previous format) for the
`DependentQueuedRequestKind` enum.
2024-11-04 17:42:47 +01:00
Jorge Martín 6828f93720 feat(ffi): add Client::is_room_alias_available function 2024-11-04 16:38:44 +01:00
Jorge Martín 7f7b996d24 refactor(ffi): modify Client::resolve_room_alias function
Breaking: `ffi::Client::resolve_room_alias` now returns `Result<Option<ResolvedRoomAlias>, ClientError>` instead of `Result<ResolvedRoomAlias, ClientError>`. This allows the client to match the 3 possible cases:

- The room alias exists.
- The room alias does not exist.
- The function failed internally.
2024-11-04 16:38:44 +01:00
Jorge Martín ee252437d1 fix(pinned_events): get pinned event ids from the HS if the sync doesn't contain it
This should take care of a bug that caused pinned events to be incorrectly removed when the new pinned event ids list was based on an empty one if the required state of the room didn't contain any pinned events info
2024-11-04 16:36:31 +01:00
dependabot[bot] 494532d579 chore(deps): bump crate-ci/typos from 1.26.8 to 1.27.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.8 to 1.27.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.26.8...v1.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 15:56:26 +01:00
Ivan Enderlin 4002136cfb feat(ui): Remove RoomListService::new_with_encryption.
This patch removes `RoomListService::new_with_encryption`. This feature
is not used, not useful since it's best to use `EncryptionSyncService`,
and it can be racy depending on how it's used. To avoid potential errors
and bugs, it's preferable to remove this code.
2024-11-04 15:17:19 +01:00
Ivan Enderlin 5717eb1722 chore(ui): Display the real error of Error::EventCache (#4207)
This patch displays the wrapped error.
2024-11-04 14:12:18 +00:00
Benjamin Bouvier c08194aa44 chore(ffi): introduce AsyncRuntimeDropped helper
This avoids proliferation of `ManuallyDrop` in the code base, by having
a single type that's used for dropping under an async runtime.
2024-11-04 14:37:50 +01:00
Jorge Martín 5d141fce13 task(room_directory_search): add 'server' parameter to the room directory search
Changelog: a new optional `via_server` parameter was added to `sdk::RoomDirectorySearch::search`, to specify which homeserver to use for searching rooms. In the FFI layer, this parameter is called `via_server_name`.
2024-11-04 09:55:11 +01:00
Richard van der Hoff 70bcddfba5 fix(crypto): Fix spelling error in a warning message. 2024-11-01 12:17:13 +00:00
Kévin Commaille 3c48459768 fix: Upgrade Ruma to 0.11.1
Brings in a fix for KeyId::key_name.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-11-01 12:37:11 +01:00
Benjamin Bouvier 5107f5f23a chore(ffi): in Client::account_url return early when we're not an oidc session
This avoids one spammy log for sessions not using oidc.
2024-11-01 12:07:40 +01:00
Valere d4b9145bc2 Merge pull request #4105 from matrix-org/valere/crypto_ffi_expose_verification_violation
crypto-ffi: Expose `has_verification_violation` for `UserIdentity`
2024-10-31 11:32:46 +01:00
Valere 49f7fe90a9 crypto-ffi: Expose has_verification_violation for UserIdentity 2024-10-31 11:04:42 +01:00
Kévin Commaille 75683d268f refactor(crypto)!: Remove unused OneTimeKey::Key and SessionCreationError::OneTimeKeyUnknown variants
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-30 17:13:47 +01:00
Ivan Enderlin 71abbeb1f1 test(sdk): Use EventFactory to simplify the test cases. 2024-10-30 15:28:38 +01:00
Ivan Enderlin fe79826c7a feat(sdk): Find and remove duplicated events in RoomEvents.
This patch uses the new `Deduplicator` type, along with
`LinkeChunk::remove_item_at` to remove duplicated events. When a new
event is received, the older one is removed.
2024-10-30 15:28:38 +01:00
Ivan Enderlin 7d64ea1bbc feat(sdk): Introduce event_cache::Deduplicator.
This patch introduces `Deduplicator`, an efficient type to detect
duplicated events in the event cache. It uses a bloom filter, and
decorates a collection of events with `Decoration`, which an enum that
marks whether an event is unique, duplicated or invalid.
2024-10-30 15:28:38 +01:00
Kévin Commaille 5158b39277 refactor!: Upgrade Ruma to 0.11.0
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-30 09:42:19 +01:00
Benjamin Bouvier be88e0ad69 feat(event cache store): Implement renaming media keys 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 50473ba1a8 chore(ring buffer): prefix all tests with test_ in this file 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 5d828d234e feat(ring buffer): implement RingBuffer::iter_mut() 2024-10-29 18:15:28 +01:00
Benjamin Bouvier 9c858c1208 refactor(base): rename all send-queue related "events" to "requests"
Changelog: Renamed all the send-queue related "events" to "requests", so
  as to generalize usage of the send queue to not-events (e.g. medias,
  redactions, etc.).
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 58d46f015b refactor(base): add a QueuedRequestKind enum
In a next commit, the `QueuedEvent` will be renamed to `QueuedRequest`.
This specifies which kind of request we want to send with the send
queue; for now, it can only be an event.
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 4cbd18cb37 refactor(base): move all send-queue related types to a new store::send_queue module
No changes in functionality, only code motion.
2024-10-29 18:15:10 +01:00
Benjamin Bouvier 888f992df0 refactor(base): Renamed StateStore::list_dependend_send_queue_events to load_dependent_send_queue_events 2024-10-29 18:15:10 +01:00
Kévin Commaille ee80291c41 chore: Never skip breaking changes with git-cliff
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-29 17:33:01 +01:00
Ivan Enderlin de3a667eb9 chore: Add an empty line between struct fields. 2024-10-29 17:21:34 +01:00
Andy Balaam ce9dc73376 doc(crypto) Crypto changelog documenting VerificationRequestState::Transitioned interface change 2024-10-29 12:08:46 +00:00
Jorge Martín 03535832ec refactor(room): remove sdk::Room::room_power_levels function
This has been replaced by `sdk_base::Room::power_levels`, which can also be used from `sdk::Room`
2024-10-29 12:55:39 +01:00
Jorge Martín c143f981bd refactor(room_list): only display the knock state events if the current user can act on them
That is, if their power level allows them to either invite or kick users.
2024-10-29 12:55:39 +01:00
Jorge Martín f4a18989fb feat(room_list): allow knock state event as latest_event
This allows clients to display pending knocking requests in the room list items.
2024-10-29 12:55:39 +01:00
Ivan Enderlin 6752cf73df test(sdk): Move tests into their correct module. 2024-10-29 10:52:14 +01:00
Ivan Enderlin e87bed8ef4 chore(sdk): Move all RoomEventCache types from mod.rs to room/mod.rs. 2024-10-29 10:52:14 +01:00
Ivan Enderlin 2f19e2b762 chore(sdk): Rename event_cache/store.rs to event_cache/room/events.rs.
This patch renames the `store.rs` file to `room/events.rs`.
2024-10-29 10:52:14 +01:00
Ivan Enderlin b66024c386 test: Update Synapse from 1.115 to 1.117.
This patch updates Synapse in our CI infrastructure and in the
`matrix-sdk-integration-testing` crate.
2024-10-29 10:50:26 +01:00
Ivan Enderlin c48bb13159 doc: Deal with paragraphes in trailers (#4179)
Git trailers have a funny format.

---------

Signed-off-by: Ivan Enderlin <ivan@mnt.io>
Co-authored-by: Benjamin Bouvier <public@benj.me>
2024-10-29 09:39:42 +00:00
Andy Balaam 5f0ba1e7df refactor(crypto) Avoid msk and ssk abbreviations in test data 2024-10-28 16:35:39 +00:00
Andy Balaam 91fa1669be refactor(crypto) Rename device methods in IdentityChangeDataSet to match identity names 2024-10-28 16:35:39 +00:00
Andy Balaam a1a4ce0a95 refactor(crypto) Tidy IdentityChangeDataSet test data 2024-10-28 16:35:39 +00:00
Andy Balaam 131921c045 fix(tests) Increase a test timeout to fix occasional flakes I saw locally 2024-10-28 16:35:39 +00:00
Ivan Enderlin b62661bc70 feat(sdk): Map Update::RemoveItem into VectorDiff::Remove in UpdateToVectorDiff.
This patch implements the support of `Update::RemoveItem` inside
`UpdateToVectorDiff` to emit a `VectorDiff::Remove`.
2024-10-28 17:17:01 +01:00
Ivan Enderlin e0be1e8e32 fix(sdk): Fix a bug in an optimisation of UpdatetoVectorDiff.
This patch fixes a bug in an optimisation inside `UpdateToVectorDiff`
when an `Update::PushItems` is handled. It can sometimes create
`VectorDiff::Append` instead of a `VectorDiff::Insert`. The tests will
be part of the next patch.
2024-10-28 17:17:01 +01:00
Ivan Enderlin c23c3b9558 chore(sdk): Rename a couple of variables.
This is another clean up patch.
2024-10-28 17:17:01 +01:00
Ivan Enderlin ca3d5693b4 chore(sdk): Rename a couple of variables.
This is a clean up patch, nothing fancy.
2024-10-28 17:17:01 +01:00
Ivan Enderlin 135c448f2d chore(sdk): Extract code into a map_to_offset method.
This is only code move, nothing has changed.
2024-10-28 17:17:01 +01:00
Ivan Enderlin 01cbce907c feat(sdk): Add LinkedChunk::remove_item_at.
This patch adds the `LinkedChunk::remove_item_at` method, along with
`Update::RemoveItem` variant.
2024-10-28 17:17:01 +01:00
Stefan Ceriu df4a5c36fc Pass the DeviceData in between the Ready and Transitioned states instead of fetching it from the store. 2024-10-28 17:04:50 +02:00
Stefan Ceriu 8492968792 Pass a copy of the other DeviceData in between the (Requested, Ready) and (Created, Ready) states 2024-10-28 17:04:50 +02:00
Stefan Ceriu d31f5b2a72 chore(tests): fix verification integration tests following changes to the data associated with VerificationRequestState::Requested 2024-10-28 17:04:50 +02:00
Stefan Ceriu 8469cb1146 fix(crypto): fix incorrect VerificationMachine tests
- the tests used to incorrectly wrap the to-device content into an event as if it was sent by alice instead of bob
2024-10-28 17:04:50 +02:00
Stefan Ceriu bb8b0cf6b9 Expose requesting device details to the final client 2024-10-28 17:04:50 +02:00
Stefan Ceriu 35dabf7346 feat(crypto): store a copy of the requesting DeviceData within VerificationRequestStates 2024-10-28 17:04:50 +02:00
Stefan Ceriu f771eec3c5 Fix a clippy warning re single matching 2024-10-28 17:04:50 +02:00
Stefan Ceriu 6455585f1e Documentation + cleanup 2024-10-28 17:04:50 +02:00
Stefan Ceriu 3a34b03726 Expose mechanism for registering to verification updates before actually accepting one
- allows handling remote cancellations on verification requests that have not yet been accepted
2024-10-28 17:04:50 +02:00
Stefan Ceriu 8cf0716db2 refactor(ffi): switch to using VerificationRequest::changes instead of direct to_device events. 2024-10-28 17:04:50 +02:00
Stefan Ceriu 660a305cfa feat(ffi): add support for receiving and working with session verification requests
fixup! feat(ffi): add support for receiving and working with session verification requests
2024-10-28 17:04:50 +02:00
dependabot[bot] b2af1eeb20 chore(deps): bump crate-ci/typos from 1.26.0 to 1.26.8
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.26.0 to 1.26.8.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.26.0...v1.26.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 15:36:15 +01:00
Ivan Enderlin 77b3aa8124 test(sdk): Test the RoomEvents' methods.
This patch adds unit tests for the `RoomEvents`' methods.
2024-10-28 13:30:35 +01:00
Ivan Enderlin cf7cb5c350 doc(sdk): Add more documentation for RoomEvents. 2024-10-28 13:30:35 +01:00
Ivan Enderlin ac7bc6461f chore(sdk): Add an Event type alias for the sake of convenience.
This patch adds an `Event` type alias to `SyncTimelineEvent` to (i) make
the code shorter, (ii) remove some cognitive effort, (iii) make things
more convenient.
2024-10-28 13:30:35 +01:00
Doug 7c39fd6ae5 chore(ffi): Expose supported OIDC prompts in the login details. 2024-10-28 13:26:14 +01:00
Jorge Martin Espinosa 40f4fc138b chore(room_preview): add RoomListItem::preview_room (#4152)
This method will return a `RoomPreview` for the provided room id. 

Also added `fn RoomPreview::leave()` action to be able to decline
invites or cancel knocks, since there wasn't a
`Client::leave_room_by_id` counterpart as there is for join.

The PR also deprecates `RoomListItem::invited_room`, since we have a
better alternative now.

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-10-25 14:33:16 +02:00
Andy Balaam d3d7c03892 doc(crypto) Update a doc comment on update_user_state_to 2024-10-25 14:27:06 +02:00
Andy Balaam 3558886b98 feat(crypto) Support Verified and VerificationViolation updates in IdentityStatusChanges streams 2024-10-25 10:32:14 +01:00
Andy Balaam 47361b93e9 refactor(crypto) Test RoomIdentityState by hard-coding identity states 2024-10-25 10:32:14 +01:00
Andy Balaam f5cdbd8e41 refactor(crypto) Rename test functions to reflect wider name change
and simplify them slightly by combining the wrapper with the main
function. The separation used to be needed, but is not any more.
2024-10-25 10:32:14 +01:00
Benjamin Bouvier f8c23d8aa0 feat(media): add support for async uploads
Changelog: Support for preallocated media content URI has been added in
`Media::create_content_uri()`, and uploading the content for such a
preallocated URI is possible with `Media::upload_preallocated()`.
2024-10-24 16:49:05 +02:00
Benjamin Bouvier 1a3c5045dd chore(room): add copyright notice to sdk/room/mod 2024-10-24 16:49:05 +02:00
Stefan Ceriu ca1d829788 sliding_sync(state): use * for m.call.member when requesting state through sliding sync
- introduced in https://github.com/matrix-org/matrix-rust-sdk/pull/4159 with an empty string
- call members use custom `state_key`s and as such not specifying the sentinel won't match them and state won't be returned
2024-10-24 11:35:41 +03:00
Valere 31e9600078 feat(send_queue): Persist failed to send errors (#4137)
Modify the SendQueue in order to persist the error that cause the event
to fail to send as a `QueueWedgeError`. The `QueueWedgeError` is not a
1:1 mapping for all kinds of errors, but holds variant and information
that the client can react to in order to propose "quick fixes"/solution
before retrying to send.

Fixes https://github.com/matrix-org/matrix-rust-sdk/issues/3973 
Also fixes https://github.com/element-hq/element-x-ios/issues/3287
because when a timeline reset occurs the fail to send reason is also
lost.

This PR starts with a refactoring commit
https://github.com/matrix-org/matrix-rust-sdk/commit/e7696003e846b64c41761109f326fd37c4506040
to introduce the new `QueueWedgedError` and move the logic that was in
the ffi layer to convert api errors to SendState error variant. This
`QueueWedgedError` can be directly use in the `SendingFailed` variant
and expose to ffi.

Second commit
https://github.com/matrix-org/matrix-rust-sdk/commit/109c1337465ba7965825fec858fd2a4b8b954611
adds the persistence, `QueuedEvent` now have an optional error field
instead of a `is_weged` boolean. Same for LocalEchoContent::Event.
Adds also Migration for sqlite and indexeddb

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>

Changelog:  We now persist the error that caused an event to fail to send. The error `QueueWedgeError` contains info that client can use to try to resolve the problem when the error is not automatically retry-able. Some breaking changes occurred in the FFI layer for `timeline::EventSendState`, `SendingFailed` now directly contains the wedge reason enum; use it in place of the removed variant of `EventSendState`.
2024-10-23 12:48:55 +02:00
Richard van der Hoff 3291a426d8 test(crypto): rename UtdCause tests with test_ prefix 2024-10-23 09:44:07 +01:00
Richard van der Hoff 1368a8534c feat(crypto): Add more reason codes to UtdCause 2024-10-23 09:44:07 +01:00
Richard van der Hoff 7cfcc8ecc1 refactor(crypto): pass utd info into UtdCause::determine
We'll need this for future changes
2024-10-23 09:44:07 +01:00
Richard van der Hoff c4f9c20115 feat(crypto): rename UtdCause::Membership
Before we do any more work here, give this variant a better name

Breaking-Change: `matrix_sdk_crypto::type::events::UtdCause::Membership` has
been renamed to `...::SentBeforeWeJoined`.
2024-10-23 09:44:07 +01:00
Richard van der Hoff 0c81206653 refactor(timeline): retry_event_decryption: re-use utd cause
Rather than calling `UtdCause::determine` again when an event is successfully
decrypted on retry, re-use the cause we already determined.
2024-10-23 09:44:07 +01:00
Richard van der Hoff a61bc3cbbd refactor(ui): add UTD info to TimelineEventKind::UnableToDecrypt
Stash the reason for the decryption failure in
`matrix-sdk-ui::event_handler::TimelineEventKind::UnableToDecrypt`.

It's not yet used.
2024-10-23 09:44:07 +01:00
Richard van der Hoff 74de617d76 refactor(ui): add TimelineEventKind::UnableToDecrypt
Give `matrix-sdk-ui::event_handler::TimelineEventKind` a new variant which
specifically represents events that could not be decrypted.
2024-10-23 09:44:07 +01:00
Jorge Martín 3f5d54c494 chore(knocking): Add optional reason and server_names parameters to Client::knock 2024-10-22 18:33:18 +02:00
Stefan Ceriu f2f99fb207 chore(ffi): move the store_in_cache timeline media upload parameter before the progress_watcher closure for aesthetic reasons 2024-10-22 18:18:32 +03:00
Benjamin Bouvier 6196ebaba6 chore(media): use the same media method when caching a thumbnail as the default one used in the FFI
The FFI will request a scaled version of the thumbnail by default; let's
use the same cache key when caching the thumbnail after an upload.

Thanks @zecakeh for flagging the issue.
2024-10-22 15:47:58 +02:00
Ivan Enderlin 65bb373379 chore(ui): Add the DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE constant.
This patch refactors 2 `chain(once(…))` with a 1 `chain`. It
also clarifies the extra `required_state` that are added for room
subscriptions.
2024-10-22 14:55:10 +02:00
Ivan Enderlin e62c47132e feat(ui): RoomListService::subscribe_to_rooms no longer has a settings argument.
This patch removes the `settings` argument of
`RoomListService::subscribe_to_rooms`. The settings were mostly composed
of:

* `required_state`: now shared with `all_rooms`, so that we are
  sure they are synced; except that `m.room.create` is added for
  subscriptions.
* `timeline_limit`: now defaults to 20.

This patch thus creates the `DEFAULT_REQUIRED_STATE` and
`DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT` constants.

Finally, this patch updates the tests, and updates all usages of
`subscribe_to_rooms`.
2024-10-22 14:55:10 +02:00
Ivan Enderlin 996b391506 feat(ui): Add m.room.topic and m.room.pinned_events in all_rooms.
This patch adds the `m.room.topic` and `m.room.pinned_events` state
events in the `required_state` of the `all_rooms` sliding sync list of
`RoomListService`.
2024-10-22 14:55:10 +02:00
Ivan Enderlin 74722f48aa fix(ui): Add the m.call.member state event in the required state.
This patch adds the `m.call.member` state event in the `required_state`
for `all_rooms` of the `RoomListService`.
2024-10-22 14:55:10 +02:00
Andy Balaam e3180cdbc5 fix(crypto): Don't warn about verified users when subscribing to identity updates 2024-10-22 12:40:31 +01:00
Benjamin Bouvier 9c03c5dd7e feat(media): cache thumbnails too with a sensible media request key
We can't know which key is going to be used precisely for the thumbnail,
so assume non-animated cropped same-size thumbnail media request.

Changelog: when `SendAttachment::store_in_cache()` is set, the thumbnail
is also cached with a sensible default media request (not animated,
cropped, same dimensions as the uploaded thumbnail).
2024-10-22 12:06:15 +02:00
Benjamin Bouvier b46ebbf34e feat(media): don't clone the data when uploading an encrypted media 2024-10-22 12:06:15 +02:00
Benjamin Bouvier d3bfdb9563 feat(media)!: optionally cache a media after upload
Changelog: Uploaded medias can now be cached in multiple
attachment-related methods like `Room::send_attachment`.
2024-10-22 12:06:15 +02:00
Richard van der Hoff 3887c10444 test: Update tests to use new UTD TimelineEventKind variant
Make the tests behave the same way as the network code, by returning UTDs
as `TimelineEventKind::UnableToDecrypt` instead of `TimelineEventKind::PlainText`.
2024-10-21 17:26:34 +01:00
Richard van der Hoff b69575d5ff refactor(timeline): store UTDs in decrypt_room_event
When `decrypt_room_event` fails to decrypt an event, return the UTD as a
`TimelineEvent` instead of an Error.
2024-10-21 17:26:34 +01:00
Richard van der Hoff 543152d914 refactor(timeline): store UTDs in decrypt_sync_room_event
When `decrypt_sync_room_event` fails to decrypt an event, return the UTD as a
`SyncTimelineEvent` instead of an Error.
2024-10-21 17:26:34 +01:00
Richard van der Hoff c8b38257f1 refactor(common): add TimelineEventKind::UnableToDecrypt 2024-10-21 17:26:34 +01:00
Andy Balaam 2df359d316 fix(experimental-algorithms) Add missing argument to handle_supported_key_request 2024-10-21 16:56:31 +01:00
Benjamin Bouvier 951a4354c6 refactor(timeline): get rid of local_item_by_transaction_id
There's no need for this API anymore.

Changelog: `Timeline::get_event_timeline_item_by_transaction_id` has
been removed. There's no API that makes use of an `EventTimelineItem`
now, those APIs are using a `TimelineEventItemId` instead.
2024-10-21 17:25:23 +02:00
Doug befcd069c3 FFI: Expose UserIdentity::is_verified and add a new Encryption::user_identity method. (#4142) 2024-10-21 13:36:34 +00:00
Richard van der Hoff 0c26988cf5 refactor(base): Remove impl From for SyncTimelineEvent
I feel like the ability to convert straight from a `Raw<AnySyncTimelineEvent>>`
into a `SyncTimelineEvent` is somewhat over-simplified: the two are only
occasionally equivalent, and it's better to be explicit.

Changelog: `SyncTimelineEvent` no longer implements `From<Raw<AnySyncTimelineEvent>>`.
2024-10-21 12:48:14 +01:00
Ivan Enderlin a7f69973c2 feat(sdk): Dropping a UpdatesSubscriber release the reader token for the GC.
The event cache stores its events in a linked chunk. The linked chunk
supports updates (`ObservableUpdates`) via `LinkedChunk::updates()`.
This `ObservableUpdates` receives all updates that are happening inside
the `LinkedChunk`. An `ObservableUpdates` wraps `UpdatesInner`, which
is the real logic to handle multiple update readers. Each reader has a
unique `ReaderToken`. `UpdatesInner` has a garbage collector that drops
all updates that are read by all readers. And here comes the problem.

A category of readers are `UpdatesSubscriber`, returned by
`ObservableUpdates::subscribe()`. When an `UpdatesSubscriber` is
dropped, its reader token was still alive, thus preventing the garbage
collector to clear all its pending updates: they were kept in memory
for the eternity.

This patch implements `Drop` for `UpdatesSubscriber` to correctly remove
its `ReaderToken` from `UpdatesInner`. This patch also adds a test that
runs multiple subscribers, and when one is dropped, its pending updates
are collected by the garbage collector.
2024-10-21 11:17:09 +02:00
Ivan Enderlin 1750bf597f test(sdk): Fix a comment. 2024-10-21 11:17:09 +02:00
Jorge Martín ad677cb6f2 chore(ffi): Add optional canonical_alias field to CreateRoomParameters 2024-10-18 13:21:04 +02:00
Andy Balaam 350a26cee9 refactor(crypto): Extract a test helper function for simulating verification 2024-10-18 11:37:37 +01:00
Benjamin Bouvier 08152bd9fc refactor(sdk)!: rename PrepareEncryptedFile et al. to UploadEncryptedFile
Changelog: Renamed `PrepareEncryptedFile` and
`Client::prepare_encrypted_file` to `UploadEncryptedFile` and
`Client::upload_encrypted_file`.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 89183a3d4b doc(timeline): rejigger a doc comment around sending attachments 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 65ed4f3f22 refactor(ffi): push further and inline parse_mime into the same caller 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 41d392f899 refactor(ffi): commonize creation of the attachment with a thumbnail 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 1ce5160846 refactor(ffi): introduce a parse_mime function to avoid code repetition 2024-10-17 16:54:50 +02:00
Benjamin Bouvier dc4c6b4d73 refactor(media): inline update_audio_message_event into its unique caller
The name wasn't very descriptive, and it's tweaking the content, so
let's do that in place, instead of deferring to another method somewhere
else in the codebase.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 3b33f3779f chore(media): rename upload methods to make their intents clearer 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 7089ff51c4 refactor(room): take the transaction id by ownership in with_transaction_id
This allows letting the caller whether they need to clone it or not, and
avoids a spurious clone in one call site.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier d8de12561b refactor(media): regroup preparation of the media message after uploading the content
The tails of the prepare_attachment_message and
prepare_encrypted_attachment_message were almost the same, with the one
different that they were using different ctors for the `EventContent`
types. In fact, all these `EventContent` types also expose a plain `new`
function that can take in either an encrypted or a plain media source,
so we can commonize the code there.
2024-10-17 16:54:50 +02:00
Benjamin Bouvier 56edc9d00f chore(media): rename all event content values to content 2024-10-17 16:54:50 +02:00
Benjamin Bouvier 2ea114d988 chore(media): reduce indent level of upload_thumbnail by one with let-else 2024-10-17 16:54:50 +02:00
Jorge Martín c7708d6154 feature(ffi): Add optional CreateRoomParameters::join_rule_override
This allows clients to set custom join rules for a room, as would be needed for the knock-only rooms, or restricted rooms (those that can only be joined if the user is part of some other room or space).
2024-10-17 16:05:07 +02:00
Benjamin Bouvier bdfe64179b feat(ffi): support custom membership state value in MembershipState 2024-10-17 15:30:34 +02:00
Benjamin Bouvier 59c47fb22d fix(ffi): don't panic when running into an unknown membership state
Fixes #1254.
2024-10-17 15:30:34 +02:00
Benjamin Bouvier 821fa8fa99 refactor(timeline): don't return a bool in Timeline::edit
See previous commit for explanations. This makes for a simpler API
anyways.

Changelog: `Timeline::edit` doesn't return a bool anymore to indicate it
couldn't manage the edit in some cases, but will return errors
indicating what the cause of the error is.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier a5f1769e28 refactor(timeline): return an invalid local echo state error if a local echo disappeared
I think this can't happen, but the send queue can return an error if a
local echo identified by a transaction id doesn't exist anymore in the
database. The only reason the latter could happen is because the local
echo has been sent, in which case an update to the timeline would be
dispatched, and the timeline item would have morphed into a remote echo
in the meantime. So it's really rare that this would happen, and the
`Timeline::redact()` method doesn't have to return a boolean to indicate
success in general.

Changelog: `Timeline::redact()` doesn't return a boolean; previously, it
would only return false if the internal state was invalid, so a new
error `RedactError::InvalidLocalEchoState` has been introduced to
represent that.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier 59fce90943 chore(ffi): revert to using a room method to edit if a remote event couldn't be found in the timeline
This maintains functionality we had prior to the previous commit: if an
event's missing from the timeline (e.g. timeline's been cleared after a
gappy sync response), then still allow editing it.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier 81bebcf692 refactor(timeline): fuse edit_by_id() within edit()
In particular, this means that trying to edit an event that's not
present anymore in a timeline (e.g. after a timeline reset) will fail,
while it worked before.

Changelog: `Timeline::edit_by_id` has been fused into `Timeline::edit`,
which now takes a `TimelineEventItemId` as the identifier for the local
or remote item to edit. This also means that editing an event that's not
in the timeline anymore will now fail. Callers should manually create
the edit event's content, and then send it via the send queue; which the
FFI function `Room::edit` does.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier c4dd2d192e refactor(timeline): fuse redact_by_id() within redact()
Changelog: `Timeline::redact_by_id` has been fused into
`Timeline::redact`, which now takes a `TimelineEventItemId` as an
identifier of the item (local or remote) to redact.
2024-10-17 14:55:17 +02:00
Benjamin Bouvier a901506a53 fix(ffi): don't panic when joining after having cancelled a media upload
Fixes #3573.
2024-10-16 18:52:06 +02:00
Richard van der Hoff 87f89ec561 crypto: update changelog 2024-10-16 16:44:48 +01:00
Richard van der Hoff 2820f5f3b4 crypto: new method OlmMachine::try_decrypt_room_event 2024-10-16 16:44:48 +01:00
Richard van der Hoff 427c59e266 crypto: add UnableToDecryptReason to UnableToDecryptInfo
Add a field to store the reason that the decryption failed
2024-10-16 16:44:48 +01:00
Richard van der Hoff 4a7f924161 timeline: tests for deserializing SyncTimelineEvent with unsigned events 2024-10-16 16:44:48 +01:00
Richard van der Hoff 2829b07305 doc: fix typo in contributing guide
The convention is for changelog entries to be in the imperative, not the past
tense.
2024-10-16 17:06:10 +02:00
Benjamin Bouvier 4f49b23751 refactor(timeline): introduce TimelineUniqueId as an opaque type for the unique identifier
We can now use this type instead of passing a string, which means
there's no way to confuse oneself in methods like
`toggle_reaction_local`.

Changelog: Introduced `TimelineUniqueId`, returned by
`TimelineItem::unique_id()` and serving as an opaque identifier to use
in other methods modifying the timeline item (e.g. `toggle_reaction`).
2024-10-16 16:21:34 +02:00
Benjamin Bouvier 962a78ab13 chore(timeline): always increment the unique id to avoid issues with stall IDs across timeline clears 2024-10-16 16:21:34 +02:00
Jorge Martín 664f6d5f5a feat(knocking): add code to process knocked rooms separately during sync 2024-10-16 16:12:39 +02:00
Benjamin Bouvier 30f3a3c2e4 chore(timeline): fix instrumentation of update_event_send_state
This would not report the `txn_id` field because of the `skip_all`. It's
actually interesting to also get the error, so I'm only skipping self
from now on.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 8df5d655c0 feat(multiverse): add support to toggle a reaction on the last message of a room 2024-10-16 15:03:36 +02:00
Benjamin Bouvier 1552426961 timeline: get rid of conversions from string to TimelineEventItemId
I suppose these were useful at the FFI layer at some point, but they
aren't anymore, so they could be removed.

Changelog: Got rid of `From<String/&str>` for `TimelineEventItemId`.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 8b7494d17b timeline: use a TimelineItemId to react to a timeline item
Changelog: `Timeline::toggle_reaction` now identifies the item that's
reacted to with a `TimelineEventItemId`.
2024-10-16 15:03:36 +02:00
Benjamin Bouvier 77e5281781 chore(timeline): instrument timeline tasks with their focus and internal prefix
This would have avoided a few hours of debugging where we thought there
was an issue with multiple timelines spawned at the same time, and then
realized it was expected because of the existence of the pinned timeline
in EX apps.
2024-10-16 14:38:21 +02:00
Andy Balaam efc2e2c4c8 doc(contributing): Recommend --interactive for git rebase --autosquash
Older versions of Git require --interactive when we supply --autosquash,
and it's also probably a good idea generally.

See https://stackoverflow.com/a/77663575/22610 for more info.
2024-10-16 10:47:15 +01:00
Jorge Martín 79798a9de9 refactor(oidc): allow passing a Prompt to get an OIDC url
Changelog: `Client::url_for_oidc_login` is now `Client::url_for_oidc` with an additional `OidcPrompt` parameter. `abort_oidc_login` has been renamed to `abort_oidc_auth`.

This allows clients to directly open the web page they want: the login one, the registration one, consent, etc. It should improve the UX in the registration flow since we can now skip the login one.
2024-10-16 11:28:34 +02:00
Kévin Commaille 3d0423447c fix(qrcode): Do not enable default features of image crate
Gets rid of dependencies for the different image formats.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-16 10:55:11 +02:00
Kévin Commaille 9999d3ba96 chore(sdk)!: Remove image-proc feature and functions to generate a thumbnail
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-16 10:55:11 +02:00
Kévin Commaille ee4ef2eb53 sdk: Remove room from in-memory list when calling Room::forget
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-15 17:04:47 +02:00
Kévin Commaille 0b57ef4bf6 sdk: Remove room from m.direct account data in Room::forget
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-15 17:04:47 +02:00
Benjamin Bouvier 6dd2e3becf refactor(event cache): use a single mutex for the prev and next batch pagination tokens 2024-10-15 16:15:40 +02:00
Benjamin Bouvier 1018d71bb7 refactor(event cache): get rid of the RoomPaginationData data structure
It only contained two fields, and it avoids one extra level of cognitive
overhead and makes the type hierarchy flatter.
2024-10-15 16:15:40 +02:00
Benjamin Bouvier 87472e7679 refactor(event cache): introduce RoomEventCacheState for inner mutable state
This limits the possibility of race conditions in users of this API.
2024-10-15 16:15:40 +02:00
Benjamin Bouvier cdbfae2aee doc(event cache): simplify module comment, as a source file isn't a good todo list
All the items have their equivalent sub item in the issue anyways.
2024-10-15 16:15:40 +02:00
dependabot[bot] 2eca7271ea chore(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 09:27:23 +02:00
dependabot[bot] 92a02a51c4 chore(deps): bump crate-ci/typos from 1.25.0 to 1.26.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.25.0 to 1.26.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.25.0...v1.26.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-15 08:04:16 +02:00
Andy Balaam 019d198af8 crypto: Sort IdentityStatusChanges when providing them via subscribe_to_identity_status_changes
Fixes https://github.com/element-hq/element-meta/issues/2566
2024-10-14 10:46:58 +01:00
Damir Jelić e6db85b7d4 chore: Enable the default features for futures-util (#4120)
We depend on the `futures_util::steam_select` macro since 9b36a04b. This
macro requires the async-await-macros and std feature of futures-util.

These features are the default features so let's just stop disabling the
default features for futures-util.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Jonas Platte <jplatte@matrix.org>
2024-10-13 19:41:11 +00:00
Jonas Platte a4bda1ac66 chore: Move lint configuration out of .cargo/config.toml
This allows removing a lot of hacks to avoid spurious rebuilds.
2024-10-11 12:40:18 +02:00
Jonas Platte e46e63771b chore(ffi): Merge export and export_async attribute macros 2024-10-11 09:57:48 +02:00
Damir Jelić 41a2ad09cf chore: Move the ffi macros into the bindings folder 2024-10-10 19:58:50 +02:00
Benjamin Bouvier 32919405d6 refactor(ffi): use a single provider for lazily computed info 2024-10-10 15:39:55 +02:00
Benjamin Bouvier b002a8da52 refactor(ffi): Don't repeat information in EventTimelineItem about local vs remote echoes 2024-10-10 15:39:55 +02:00
Benjamin Bouvier 85682ac37f chore(timeline): add extra logs to investigate edit issues 2024-10-10 15:17:18 +02:00
Ivan Enderlin 22c765b9ab fix(ui): all_rooms in RoomListService requires the m.room.avatar state.
This patch updates the `required_state` of `all_rooms` inside the
`RoomListService` to add `m.room.name`. Apparently, Synapse doesn't
always update the `response.rooms.*.avatar` field when the avatar is
updated. It's being investigated, but it doesn't hurt to ensure we get
it from the state events.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 3ad8f1d607 test(integration): Fix one test by adding required_state.
To fix the `test_room_avatar_group_conversation`, we need to ask for the
`m.room.avatar` state event from `required_state`. The rest of the patch
rewrites the test a little bit to make it more Rust idiomatic.

The `response.rooms.*.avatar` field from sliding sync should contain the
new avatar, but for the moment, it doesn't. It seems to be a bug.
2024-10-10 15:07:57 +02:00
Ivan Enderlin a4782939b3 test(integration): Fix one test by adding required_state.
To fix the `test_left_room`, we need to ask for the `m.room.member`
state event from `required_state`. The rest of the patch rewrites the
test a little bit to make it more Rust idiomatic.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 72dc307400 fix(base): Add a way for handle_timeline to ignore state events.
Sliding sync expects all state events to be in `required_state`. State
events in `timeline` **must be ignored**. However, in sync v2, state
events in `timeline` **must be handled**.

In the sync response flow, both sliding sync and sync v2 uses the same
`handle_timeline` method. This patch adds an argument to ignore state
events. This is not ideal, but it's a temporary solution as a first
step. The next step is to refactor this code, but let's start easy.

The rest of the patch updates the tests accordingly.
2024-10-10 15:07:57 +02:00
Ivan Enderlin 248cf55272 fix(base): Don't use state events from timeline with sliding sync.
With sliding sync, we must handle state events from `required_state`
only, not from `timeline`, this is a mistake as they might be incomplete
or _staled_.
2024-10-10 15:07:57 +02:00
Damir Jelić 1260e740ba Update the contributing guide with our new git-cliff setup
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-10-10 14:32:46 +02:00
Damir Jelić 9a4a67d488 Document the new release process 2024-10-10 14:32:46 +02:00
Damir Jelić ab0871f299 Call git-cliff as a pre-release hook 2024-10-10 14:32:46 +02:00
Damir Jelić 86d9fe59d2 Create an xtask for the release handling 2024-10-10 14:32:46 +02:00
Damir Jelić 1945b508c3 Create some missing changelog files 2024-10-10 14:32:46 +02:00
Damir Jelić 4c7461357c Add a git-cliff configuration file 2024-10-10 14:32:46 +02:00
Damir Jelić ca7f2ad3d0 Add a cargo-release config 2024-10-10 14:32:46 +02:00
Benjamin Bouvier 711f4cb868 ci: detect unused dependencies with cargo-machete 2024-10-10 14:18:36 +02:00
Benjamin Bouvier cb51a3155a chore: get rid of unused dependencies 2024-10-10 14:18:36 +02:00
Damir Jelić 81119a66d8 ci: Install libsqlite, it does not seem to be part of the latest ubuntu image (#4108) 2024-10-10 13:43:58 +02:00
Richard van der Hoff 9c6413551c Inline SyncTimelineEvent::set_raw
This is only used in one place, and is much better inlined anyway.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 42f0d83b53 timeline: remove redundant Debug implementations
These are no longer required now that the event itself lives in an inner class.
2024-10-09 15:19:26 +01:00
Richard van der Hoff d9167f208a timeline: Extract inner parts of [Sync]TimelineEvent
Pull out the bits of these classes which are dependent on success or otherwise
of decrypting an event to a new enum.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 7f0a3f0e47 timeline: make TimelineEvent::into_raw return a Raw<AnySyncTimelineEvent>
Give `Timeline::into_raw()` the same treatmeant we just gave `Timeline::ra()`.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 8fe61e1fb3 timeline: make TimelineEvent::raw return a Raw<AnySyncTimelineEvent>
I'm going to be replacing the inner structure of `TimelineEvent` with an
implementation that holds a `Raw<AnySyncTimelineEvent>`, rather than a
`Raw<AnyTimelineEvent>`. Prepare for that by changing the accessors to return
`Raw<AnySyncTimelineEvent>`.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 07cfe3da94 timeline: make TimelineEvent fields private
... and add accessors instead.

Give `TimelineEvent` the same treatment we just gave `SyncTimelineEvent`: make
the fields private, and use accessors where we previously used direct access.
2024-10-09 15:19:26 +01:00
Richard van der Hoff 4d472f6aed timeline: make SyncTimelineEvent fields private
... and add accessors instead.

I'm going to change the inner structure of `SyncTimelineEvent`, meaning that
access will have to be via an accessor in future. Let's start by making the
fields private, and use accessors where we previously used direct access.
2024-10-09 15:19:26 +01:00
Richard van der Hoff ce231e6c2b timeline: test for SyncTimelineEvent serialization
I'm going to change the internal structure of `SyncTimelineEvent`, and since
it implements `Deserialize`, we need to not break it. Let's add a test for the
current format.
2024-10-09 15:19:26 +01:00
Richard van der Hoff b36a9ad781 timeline: Add documentation to [Sync]TimelineEvent
I found it hard to understand what these two structs were for, so let's start
by giving them some documentation.
2024-10-09 15:19:26 +01:00
Doug 95ae5d1938 ffi: Rename get_media_file body parameter to filename. 2024-10-09 10:52:35 +02:00
Doug 9d976d0bcf sdk: Update get_media_file to take a filename instead of the body. 2024-10-09 10:52:35 +02:00
Kévin Commaille 17370a5702 sdk: Upgrade aquamarine
Finally get rid of syn 1!

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-08 19:05:35 +02:00
Jorge Martín b793acd2b1 sdk-ui: allow already sent local events to be redacted using redact_by_id
Test this use case.
2024-10-08 17:50:05 +02:00
Mathieu Velten 752706c51d Get back to Recovering syncing when we haven't sync for a while 2024-10-08 17:17:42 +02:00
Benjamin Bouvier 736aa0351c ffi: add our own macro for processing exports
Including one that will always warn if used with async functions, and
the other one always setting the tokio runtime if used for async stuff.
2024-10-08 17:11:39 +02:00
boxdot 4bcb9b7d9f fix: Fix a deadlock between bootstrap_cross_signing and sync (#4060)
`bootstrap_cross_signing` holds a lock on the private identity. In case
a new identity is created, it will try to acquire a lock on `account`.
The latter is locked by `sync`, which tries to acquire a lock on the private identity.

Note that the `bootstrap_cross_signing` call is executed in a separate
task e.g. in `restore_session`. In particular, this task and `sync` both
race to acquire locks described above.

Signed-off-by: boxdot <d@zerovolt.org>
2024-10-08 15:12:40 +02:00
Jorge Martín 867d9c71fd sdk-base: add prev_room_state to RoomInfo
This is useful for the knocking feature since we'll be able to differentiate between rooms that you were just invited from rooms that you knocked and then were granted access, or rooms that you left and rooms where your knocking attempt was rejected.

The `mark_room_as_*` functions have been updated so they reuse the same `set_state` function underneath, which only updates the previous state if the new one doesn't match.
2024-10-08 13:16:40 +02:00
Kévin Commaille 2dcf06fad2 sdk: Add support for authenticated media stable feature
Was added post-merge to the MSC for servers that support
authenticated media but do not support all of Matrix 1.11 yet.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-08 12:21:32 +02:00
Jorge Martín 1fc3450eac ffi & sdk: add room knocking to Client 2024-10-08 12:07:01 +02:00
Benjamin Bouvier 19b9a73ecc ffi: add async_runtime annotation for impl block with async fun 2024-10-08 11:12:01 +02:00
Ivan Enderlin 4d45b02e91 fix(ui): Consider timeline_limit in sliding sync as non-sticky.
This patch changes the behaviour of `timeline_limit` in sliding sync
requests. It previously was sticky, but since it's now mandatory
with MSC4186, it's preferable it to be non-sticky, otherwise in
some scenarios it might default to 0 (its default value). How?
If the server doesn't reply with our `txn_id` (because it doesn't
support sticky parameters or because it misses a `txn_id`), the
next request will be built with a default `timeline_limit` value,
which is zero, and won't get updated to the `timeline_limit` value
from `SlidingSyncListStickyParameters`. This is not good. Instead,
we must consider `timeline_limit` as non-sticky, and moves it from
`SlidingSyncListStickyParameters` to `SlidingSyncListInner`. This is
what this patch does.
2024-10-07 16:38:07 +02:00
Ivan Enderlin a9cfba2c03 chore(cargo): Update ruma. 2024-10-07 16:38:07 +02:00
Benjamin Bouvier ff7e8c75ee ci: try using macos-14 runners for swift-related tasks 2024-10-07 16:14:18 +02:00
Stefan Ceriu 2967b73aff ci: speed up iOS bindings tests by building them on the dev profile
- speed regression introduced when switching the default bindings profile to `reldbg` in #4020
2024-10-07 16:07:38 +02:00
Ivan Enderlin 6f0fbf92e4 Revert "Revert "chore(ui,ffi): Remove the RoomList::entries method.""
This reverts commit af390328b5.
2024-10-07 15:58:38 +02:00
Benjamin Bouvier 4cbc162964 timeline: update replies when a message has been edited 2024-10-07 15:11:09 +02:00
Andy Balaam 6c7acf6faa ffi: Expose the master_key method on UserIdentity 2024-10-07 13:31:38 +01:00
Andy Balaam 181ee643b1 crypto: Expose a way to pin a user's identity 2024-10-07 13:31:38 +01:00
Doug a12a46b777 ffi: Add caption/formatted_caption to media timeline items.
Also includes the computed filename too.
2024-10-07 14:11:19 +02:00
Doug 93fce02606 chore: Update Ruma to add media caption methods.
fixup

fixup
2024-10-07 14:11:19 +02:00
Benjamin Bouvier 351fbf60c1 tests: serialize Unsigned with serde 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5c353923cd timeline: add test for poll edit in relations overriding pending poll edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier dc4cc02926 timeline: add helpers for Flow to avoid redundant code 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 6b543d105f timeline: avoid passing the raw event in two places 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5a1728a468 timeline: rename find_and_remove_pending to maybe_unstash_pending_edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 5c8d1d816e timeline: move adding a new msg to its own function 2024-10-07 07:47:05 +02:00
Benjamin Bouvier bd7f0d695b timeline: add TimelineItemContent::as_poll
That's more aligned with `as_message()`, and allows getting rid of one
custom test helper.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier ccf8bf8652 timeline(test): add test for latest poll with a bundled edit 2024-10-07 07:47:05 +02:00
Benjamin Bouvier f21de25da0 timeline: apply bundled edits for polls too 2024-10-07 07:47:05 +02:00
Benjamin Bouvier d789983eff timeline: rename extract_edit_content to extract_room_msg_edit_content 2024-10-07 07:47:05 +02:00
Benjamin Bouvier f8e65f53cd timeline: provide the edit JSON for edits either pending or bundled 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 05cbb9e290 timeline(refactor): get rid of the stored event id in the pending_edits array
Since it's implied from the `Replacement` data structure.

Also reuse `find_and_remove_pending` in more places.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier d403bf3431 timeline(code motion): move the poll code to other files
The content of a poll timeline item goes to the content directory. The
data structure handling pending poll events goes into state.

No functional changes, only code motion.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier 56ccda4ded timeline: apply Message edits in a single place 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 157499955a tests: allow passing an u64 to EventBuilder::server_ts 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 8a71ac622d tests: allow bundled relations in EventBuidler 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 45968b2a2b timeline: prefer a bundled edit to a pending edit when adding a new message 2024-10-07 07:47:05 +02:00
Benjamin Bouvier efbf9472f2 latest event: consider bundled edits when constructing an event item from a latest event 2024-10-07 07:47:05 +02:00
Benjamin Bouvier 1434285a1b timeline: extract edits from bundled relations and pass an optional edited content to Message::from_event
No changes in functionality.
2024-10-07 07:47:05 +02:00
Benjamin Bouvier 46856f54af timeline: get rid of one indent level thanks to a let else 2024-10-07 07:47:05 +02:00
Kévin Commaille 3ce2f16d55 base: Do not warn when room in m.direct account data is missing
It can occur that the data contains rooms that were forgotten.
Knowing that we now update that data after every sync, that creates
a lot of noise in the logs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-07 06:51:19 +02:00
Kévin Commaille abf3c6e7b7 ui: Do not warn when no reaction to redact was found
The `handle_reaction_redaction` method is called by `handle_redaction`
for every single redaction event that we receive as a first step to
check if the redaction matches a reaction.
It means that not finding a reaction to redact is perfectly fine and is not worthy of a warning.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 18:00:30 +03:00
Damir Jelić a3a0125421 chore: Set up cargo-deny 2024-10-04 16:53:17 +02:00
Damir Jelić 657c72904a chore: Define our license in every crate we have 2024-10-04 16:53:17 +02:00
Damir Jelić de752eb089 chore: Use a released version of the qrcode crate for the qr-login example 2024-10-04 16:53:17 +02:00
Damir Jelić a4415c9fa5 chore: Use a released version of vodozemac 2024-10-04 16:53:17 +02:00
Andy Balaam 5d46b35d95 crypto: Rename some straggling 'Identities' to 'Identity'
The main enum was renamed to `UserIdentity` and some aliases and
comments had not kept up.
2024-10-04 14:37:12 +01:00
Kévin Commaille 65b422312c chore: Enable the proper feature of tower
We only use `service_fn` which is behind the `util` feature.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 12:51:38 +02:00
Kévin Commaille 7bac0340d6 base: Apply RoomInfo migrations for notable_tags and pinned_events
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-10-04 11:58:10 +02:00
Richard van der Hoff 1d1863d323 crypto: Give decrypt_room_event a new return type
I want to do a bit of a refactoring on `TimelineEvent`, so let's start by
giving `decrypt_room_event` its own return type.
2024-10-03 16:23:45 +01:00
Andy Balaam a695e291fa crypto: Rename PreviouslyVerified to VerificationViolation
For consistency with other places, we have now settled on
`VerificationViolation` as the best way to express this situation.
2024-10-03 15:41:55 +01:00
Andy Balaam c5f5bc8496 crypto: FFI bindings for subscribe_to_identity_status_changes 2024-10-03 13:24:24 +01:00
Andy Balaam cd072e6dff crypto: Provide a way to subscribe to identity status changes 2024-10-03 13:24:24 +01:00
Andy Balaam 9b36a04bb9 crypto: Provide the core logic about how identities change in a room when changes occur 2024-10-03 13:24:24 +01:00
Andy Balaam 6b357de947 crypto: Allow accessing the underlying identity on a UserIdentity 2024-10-03 13:24:24 +01:00
Andy Balaam e85e50b185 crypto: Provide DerefMut on OwnUserIdentity and UserIdentity 2024-10-03 13:24:24 +01:00
Erik Johnston c9fd5a0787 Do not log full keys query response which can be very large (#4065)
Signed-off-by: Erik Johnston <erikj@jki.re>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2024-10-03 10:42:43 +00:00
Valere 6de676f491 Merge pull request #4046 from matrix-org/valere/trust_decoration_decryption_trust_req
crypto: Expose with_decryption_trust_requirement for ClientBuilder
2024-10-02 12:13:15 +02:00
Benjamin Bouvier 06f60e3b62 base: rename account_data to account_data_processor and other review comments 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 96765cad28 base: add helper to process data on a room info from state changes or store 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 4f265ccd22 base: move processing of the direct room inside the AccountDataProcessor 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 759a9b0e18 base: make the dependency to push rules explicit when processing a room's subpart of a response 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 0a854cdbf7 base: get rid of StateChanges::add_account_data 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 40e6e9f028 base: avoid double-contains check in apply_changes
Calling `contains_key` and then doing `if let Some() = .get` have the
same effect.
2024-10-02 11:58:09 +02:00
Benjamin Bouvier 8f4fcf6299 base: experiment with handling global account data as a separate processor 2024-10-02 11:58:09 +02:00
Benjamin Bouvier 2283c28503 base: tidy up sliding sync code around e2ee 2024-10-02 11:58:09 +02:00
dependabot[bot] 59d3608c32 chore(deps): bump actions/checkout from 2.0.0 to 4.2.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.0.0 to 4.2.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 14:37:36 +02:00
Benjamin Bouvier 06e9f01a4a chore: fix new typos 2024-10-01 14:07:14 +02:00
dependabot[bot] 0ff63d3008 chore(deps): bump crate-ci/typos from 1.20.10 to 1.25.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.25.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.10...v1.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-01 14:07:14 +02:00
Damir Jelić 5cc7730dd9 chore: Configure dependabot to notify us about outdated github actions 2024-10-01 13:23:08 +02:00
Damir Jelić e5bd7602e4 refactor: Use a match arm when evaluating session comparison results 2024-10-01 13:00:51 +02:00
Damir Jelić 2fc4aacdd0 feat: Prefer room keys with better SenderData when comparing duplicate room keys 2024-10-01 13:00:51 +02:00
Pratik Deshpande f7d99cc506 Added a binding for custom login using JWT 2024-10-01 12:33:06 +03:00
Valere 60319914e1 code review | quick doc and test cleaning 2024-10-01 10:19:25 +02:00
Valere 740356a350 test: ClientBuilder test for decryption trust requirement 2024-10-01 10:19:25 +02:00
Valere 806ee13aa0 ffi: Expose room_decryption_trust_requirement for ClientBuilder 2024-10-01 10:19:25 +02:00
Valere 3fd2f5794e crypto: Expose with_decryption_trust_requirement for ClientBuilder 2024-10-01 10:15:17 +02:00
Damir Jelić dc055c632c chore: Update the changelog to mention the UserIdentity renames 2024-09-30 18:04:04 +02:00
Damir Jelić c2ab795122 doc: Fix some doc links for the user identities 2024-09-30 18:04:04 +02:00
Damir Jelić e7bc510313 refactor: Rename the UserIdentities enum into UserIdentity 2024-09-30 18:04:04 +02:00
Damir Jelić 7efee5a5af refactor: Rename UserIdentity to OtherUserIdentity in the crypto crate 2024-09-30 18:04:04 +02:00
reivilibre 866b6e5f2d client builder: return a ClientBuildError when failing to build, instead of filtering out unexpected errors (#4016)
This old method was checking invariants that were
spooky-action-at-a-distance: these invariants have changed since then,
so this would panic instead of returning a proper error to the caller.

Signed-off-by: oliverw@element.io

---------

Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
2024-09-30 13:43:25 +02:00
Kévin Commaille 1e0e815fab base: Remove deprecated StateStore APIs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:18:10 +02:00
Kévin Commaille 99fe49bbac sdk: Remove deprecated Room APIs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:18:10 +02:00
Kévin Commaille 68bc14e567 base: Expose room avatar info from RoomInfo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-30 12:55:39 +03:00
Benjamin Bouvier fe648d9cb5 event cache(refactoring): don't have related event rely on ordering
This was because we used a `BTreeSet`, which doesn't make sense anymore
since the data part of the key got mangled with some value unrelated to
the key itself.
2024-09-30 11:50:43 +02:00
Jorge Martín 743799fbd2 ffi: move the dependency override from ffi/Cargo.toml to the root one
This seems to be the only way to make the log rotation fix work and avoid build warnings like:

```
warning: patch for the non root package will be ignored, specify patch at the workspace root:
package:   matrix-rust-sdk/bindings/matrix-sdk-ffi/Cargo.toml
workspace: = matrix-rust-sdk/Cargo.toml
    Finished `dev` profile [unoptimized] target(s) in 0.30s
```
2024-09-30 10:22:41 +02:00
Jorge Martín ac61fc8830 ffi: create EventShieldsProvider to load shields on demand in the clients 2024-09-27 17:22:55 +02:00
Jorge Martín 52898fa526 ffi: create EventOrTransactionId enum for functions that can receive both 2024-09-27 17:22:55 +02:00
Jorge Martín 263386ea53 ffi: use event_or_transaction_id parameter name for Timeline functions that can take both 2024-09-27 17:22:55 +02:00
Jorge Martín 0082fbc0b4 ffi: add EventTimelineItemDebugInfoProvider to lazily retrieve an event's debug info 2024-09-27 17:22:55 +02:00
Jorge Martín 281a79ffc6 ffi: make From implementations for some event types use values, not references 2024-09-27 17:22:55 +02:00
Jorge Martín bdf303aa57 ffi: remove unused unwrap_or_clone_arc_into_variant macro 2024-09-27 17:22:55 +02:00
Jorge Martín 7dcf45562c ffi: fix bindings not using Arc wrappers 2024-09-27 17:22:55 +02:00
Jorge Martín 548c66750f sdk-ui: Move the event-fetching logic for edit and redact functions to the sdk-ui crate where they can be tested, to the edit_by_id and redact_by_id functions.
Added some tests for those, based on the existing ones.
2024-09-27 17:22:55 +02:00
Jorge Martín 67df36f733 ffi: Turn EventTimelineItem into a record type
This improves parsing times in mobile Clients. On Android, this means a 5-10x faster parsing of timeline events.

To do that I had to:

- Make functions like `edit/redact/forward` take an identifier (EventId/TransactionId) instead of the actual event. This id will be used to look for the actual SDK timeline event in the timeline. This change will make these functions a bit less performant.
- Make `InReplyToDetails` an object instead since a record can't recursively contain itself.
- Turn `EventTimelineItem` into a record type. Do the same with `Message`, which is now `MessageContent`.
2024-09-27 17:22:55 +02:00
Damir Jelić e61fb45504 ffi: Allow recovery to be enabled using a passphrase 2024-09-27 17:04:00 +02:00
Jorge Martín 9b7f89c183 ffi: use fork of tracing crate with a fix for Android logs rotation 2024-09-27 10:37:21 +02:00
Damir Jelić 322c5b3f83 refactor: Fold the private UserIdentities struct into UserIdentity
It doesn't serve any purpose and only confuses people since we have many
similarly named types.
2024-09-26 12:20:36 +02:00
Valere 14ec35e67f Merge pull request #3985 from matrix-org/valere/invisible_crypto/identity_based_withheld_code
crypto: change withheld code for IdentityBased share strategy
2024-09-25 17:24:43 +02:00
Valere 2bb0c50266 crypto: change withheld code for IdentityBased share strategy 2024-09-25 16:57:52 +02:00
Ivan Enderlin d254217217 test(integration): Enable test_room_preview and test_room_avatar_group_conversation.
These tests were failing since the migration from the sliding sync proxy
to Synapse.

Since the previous fixes to re-enable other tests, these 2 passes for
free.
2024-09-25 16:40:49 +02:00
Ivan Enderlin 83ce4c7ca2 test(integration): Fix test_room_notification_count.
This test was failing since the migration from the sliding sync proxy
to Synapse.

This patch fixes the test. The failing parts were:

1. The `timeline_limit` wasn't set, so Synapse was returning an error,
2. The `unread_notifications` was set to 0 and could not be set to 1
   because that's an encrypted room.

The fact `timeline_limit` is now mandatory has been mentioned in the MSC:
https://github.com/matrix-org/matrix-spec-proposals/pull/4186/files#r1775138458
A patch in Ruma has been created. The previous patch in this repository
also contains the fix for the SDK side.

The assertions around `unread_notifications` have been removed. We no
longer use this API anymore (and it should be deprecated by the way).
2024-09-25 16:13:08 +02:00
Ivan Enderlin 2562aa3fee chore(test): Clean a test by rewriting the code a little bit. 2024-09-25 16:13:08 +02:00
Ivan Enderlin fcb1c96869 feat(sdk): Sliding Sync has a required timeline_limit now.
Since MSC4186, the `timeline_limit` value is required.

This patch uses 1 as the default value for `timeline_limit`, and forces
the `timeline_limit` to be defined everywhere.
2024-09-25 16:13:08 +02:00
Ivan Enderlin 88ceeb3513 testing again 2024-09-25 16:12:57 +02:00
Ivan Enderlin a5dc8ff871 test(integration): Remove what seems to be a bug but it's not.
When Bob receives the invite, the room has the correct name. Bob to sync
more to receive the new name. This is not a bug.

This patch updates the `CreateRoomRequest` to set the correct name
immediately.
2024-09-25 16:12:57 +02:00
Ivan Enderlin c3caf6cbca test(integration): Enable test_notification.
This test was failing since the migration from the sliding sync proxy
to Synapse.

This patch fixes the test. The failing part was:

```rust
assert_eq!(notification.joined_members_count, 1);
```

This patch changes the value from 1 to 0. Indeed, Synapse doesn't share
this data for the sake of privacy because the room is not joined.

A comment has been made on MSC4186 to precise this behaviour:
https://github.com/matrix-org/matrix-spec-proposals/pull/4186#discussion_r1774775560.

Moreover, this test was asserting a bug (which is alright), but now
a bug report has been made. The patch contains the link to this bug
report.

The code has been a bit rewritten to make it simpler, and more comments
have been added.
2024-09-25 16:12:57 +02:00
Ivan Enderlin 8469c6465e test(integration): Update Synapse to 1.115. 2024-09-25 14:55:16 +02:00
Jorge Martín 03f7806000 sdk-ui & sdk: add a relationship type filter to load_event_with_relations
We need this for pinned events, where we want reactions, edits and redactions, but we don't want replies or threaded replies.
2024-09-25 12:09:14 +02:00
Kévin Commaille 5ba90611b4 ui: Allow to subscribe to read receipt changes in timeline metadata
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-09-24 15:48:07 +02:00
Benjamin Bouvier bda2acf5f6 use precise Dockerfile version 2024-09-24 15:09:05 +02:00
Ivan Enderlin 40f1ce80ea test: Bye bye SS proxy, hello Synapse \o/.
This patch removes the sliding sync proxy, and makes the
`matrix-sdk-integration-testing` tests to run against Synapse with
MSC4186 enabled.
2024-09-24 15:09:05 +02:00
Andy Balaam 3492bd6929 doc: Fix a typo in an error message 2024-09-24 13:13:39 +01:00
Jorge Martín 5e9f629edb sdk-ui: make room encryption optional to create a timeline
Instead of forcing the room encryption to be known when the timeline is created and failing if it's not known, take the latest room encryption info as a base value and update it when processing timeline events.

At the time of writing this commit, the encryption info is only used to decide whether shields should be calculated for timeline items or not.
2024-09-24 08:22:51 +02:00
Richard van der Hoff 794dbb36dc crypto: minor fixes to documentation on UserIdentity 2024-09-23 11:25:26 +01:00
Johannes Marbach 2a03de3bd5 Reformat again... 2024-09-19 08:03:36 +02:00
Johannes Marbach 79d8738ff5 Reformat 2024-09-19 08:03:36 +02:00
Johannes Marbach e16ca9d8ba ffi: default to reldbg when building iOS bindings
Relates to: #4009
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-09-19 08:03:36 +02:00
Benjamin Bouvier 746d7e13ab tests: change strategy for test_ensure_max_concurrency_is_observed 2024-09-18 17:34:24 +02:00
Benjamin Bouvier 1b4f665d99 integration tests: update instructions (#4017)
- explain what these tests are
- mention that it's sometimes needed to rebuild the synapse image

---------

Signed-off-by: Benjamin Bouvier <benjamin@bouvier.cc>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2024-09-18 14:45:33 +00:00
527 changed files with 64417 additions and 21656 deletions
-48
View File
@@ -1,13 +1,3 @@
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
# when a `--target` is passed to Cargo. Historically this was not the case, and
# because of that, cross-compilation would not set the rustflags configured
# below in `target.'cfg(...)'` for them, resulting in cache invalidation.
#
# Since this is an unstable feature (enabled at the bottom of the file), this
# setting is unfortunately ignored on stable toolchains, but it's still better
# to have it apply on nightly than using the old behavior for all toolchains.
target-applies-to-host = false
[alias]
xtask = "run --package xtask --"
uniffi-bindgen = "run --package uniffi-bindgen --"
@@ -15,43 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
# Exclude tarpaulin, android and ios from extra lints since on stable, without
# the nightly-only target-applies-to-host setting at the top, cross compilation
# and otherwise changing cfg's can be very bad for caching. These should never
# be the default either and don't have much target-specific code that would
# benefit from the extra lints.
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
rustflags = [
"-Wrust_2018_idioms",
"-Wsemicolon_in_expressions_from_macros",
"-Wunused_extern_crates",
"-Wunused_import_braces",
"-Wunused_qualifications",
"-Wtrivial_casts",
"-Wtrivial_numeric_casts",
"-Wclippy::cloned_instead_of_copied",
"-Wclippy::dbg_macro",
"-Wclippy::inefficient_to_string",
"-Wclippy::macro_use_imports",
"-Wclippy::mut_mut",
"-Wclippy::needless_borrow",
"-Wclippy::nonstandard_macro_braces",
"-Wclippy::str_to_string",
"-Wclippy::todo",
"-Wclippy::unused_async",
"-Wclippy::redundant_clone",
]
[target.'cfg(target_arch = "wasm32")']
rustflags = [
# We have some types that are !Send and/or !Sync only on wasm, it would be
# slightly more efficient, but also pretty annoying, to wrap them in Rc
# where we would use Arc on other platforms.
"-Aclippy::arc_with_non_send_sync",
]
# activate the target-applies-to-host feature.
# Required for `target-applies-to-host` at the top to take effect.
[unstable]
rustdoc-map = true
target-applies-to-host = true
+67
View File
@@ -0,0 +1,67 @@
# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
[graph]
all-features = true
exclude = [
# dev only dependency
"criterion"
]
[advisories]
version = 2
ignore = [
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
]
[licenses]
version = 2
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"ISC",
"MIT",
"MPL-2.0",
"Unicode-3.0",
"Zlib",
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
{ allow = ["CDDL-1.0"], crate = "inferno" },
{ allow = ["LicenseRef-ring"], crate = "ring" },
]
[[licenses.clarify]]
name = "ring"
expression = "LicenseRef-ring"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[bans]
# We should disallow this, but it's currently a PITA.
multiple-versions = "allow"
wildcards = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
# A patch override for the bindings fixing a bug for Android before upstream
# releases a new version.
"https://github.com/element-hq/tracing.git",
# Sam as for the tracing dependency.
"https://github.com/element-hq/paranoid-android.git",
# Well, it's Ruma.
"https://github.com/ruma/ruma",
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
"https://github.com/jplatte/async-compat",
# We can release vodozemac whenever we need but let's not block development
# on releases.
"https://github.com/matrix-org/vodozemac",
]
+7
View File
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
-13
View File
@@ -1,13 +0,0 @@
name: Security audit
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/audit@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: rustfmt
- name: Run Benchmarks
+5 -5
View File
@@ -85,7 +85,7 @@ jobs:
java-version: '17'
- name: Install android sdk
uses: malinskiy/action-android/install-sdk@release/0.1.4
uses: malinskiy/action-android/install-sdk@release/0.1.7
- name: Install android ndk
uses: nttld/setup-ndk@v1
@@ -131,7 +131,7 @@ jobs:
test-apple:
name: matrix-rust-components-swift
needs: xtask
runs-on: macos-12
runs-on: macos-15
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
@@ -175,7 +175,7 @@ jobs:
run: swift test
- name: Build Framework
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
complement-crypto:
name: "Run Complement Crypto tests"
@@ -186,12 +186,12 @@ jobs:
test-crypto-apple-framework-generation:
name: Generate Crypto FFI Apple XCFramework
runs-on: macos-12
runs-on: macos-15
if: github.event_name == 'push' || !github.event.pull_request.draft
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
+44 -40
View File
@@ -18,6 +18,9 @@ concurrency:
env:
CARGO_TERM_COLOR: always
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
# Just want it to run the tests (option `no` instead of `auto`).
INSTA_UPDATE: no
jobs:
xtask:
@@ -40,7 +43,6 @@ jobs:
- markdown
- socks
- sso-login
- image-proc
steps:
- name: Checkout
@@ -49,6 +51,11 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@v2
with:
@@ -114,6 +121,11 @@ jobs:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -165,6 +177,12 @@ jobs:
with:
tool: protoc@3.20.3
- name: Install libsqlite
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
@@ -206,12 +224,16 @@ jobs:
- name: '[m]-common'
cmd: matrix-sdk-common
- name: '[m], no-default'
cmd: matrix-sdk-no-default
- name: '[m]-ui'
cmd: matrix-sdk-ui
check_only: true
- name: '[m]-indexeddb'
cmd: indexeddb
- name: '[m], no-default, wasm-flags'
cmd: matrix-sdk-no-default
- name: '[m], indexeddb stores'
cmd: matrix-sdk-indexeddb-stores
@@ -230,6 +252,7 @@ jobs:
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.0
if: '!matrix.check_only'
with:
version: v0.10.3
@@ -259,27 +282,10 @@ jobs:
target/debug/xtask ci wasm ${{ matrix.cmd }}
- name: Wasm-Pack test
if: '!matrix.check_only'
run: |
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
components: rustfmt
- name: Cargo fmt
run: |
cargo fmt -- --check
typos:
name: Spell Check with Typos
runs-on: ubuntu-latest
@@ -289,10 +295,10 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.20.10
uses: crate-ci/typos@v1.29.5
clippy:
name: Run clippy
lint:
name: Lint
needs: xtask
runs-on: ubuntu-latest
@@ -308,8 +314,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
components: clippy
toolchain: nightly-2024-11-26
components: clippy, rustfmt
- name: Load cache
uses: Swatinem/rust-cache@v2
@@ -323,6 +329,10 @@ jobs:
key: "${{ needs.xtask.outputs.cachekey-linux }}"
fail-on-cache-miss: true
- name: Check Formatting
run: |
target/debug/xtask ci style
- name: Clippy
run: |
target/debug/xtask ci clippy
@@ -335,7 +345,7 @@ jobs:
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
services:
# sliding sync needs a postgres container
# synapse needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -353,21 +363,10 @@ jobs:
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# run sliding sync and point it at the postgres container and synapse container.
# the postgres container needs to be above this to make sure it has started prior to this service.
slidingsync:
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./coverage.yml
env:
SYNCV3_SERVER: "http://synapse:8008"
SYNCV3_SECRET: "SUPER_CI_SECRET"
SYNCV3_BINDADDR: ":8118"
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
ports:
- 8118:8118
# 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:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -378,6 +377,11 @@ jobs:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
+7 -14
View File
@@ -29,7 +29,6 @@ jobs:
# run several docker containers with the same networking stack so the hostname 'postgres'
# maps to the postgres container, etc.
services:
# sliding sync needs a postgres container
postgres:
# Docker Hub image
image: postgres
@@ -47,21 +46,10 @@ jobs:
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# run sliding sync and point it at the postgres container and synapse container.
# the postgres container needs to be above this to make sure it has started prior to this service.
slidingsync:
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./ci.yml
env:
SYNCV3_SERVER: "http://synapse:8008"
SYNCV3_SECRET: "SUPER_CI_SECRET"
SYNCV3_BINDADDR: ":8118"
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
ports:
- 8118:8118
# 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:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
env:
SYNAPSE_COMPLEMENT_DATABASE: sqlite
SERVER_NAME: synapse
@@ -74,6 +62,11 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install libsqlite
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -101,7 +94,7 @@ jobs:
run: |
rustup run stable cargo tarpaulin \
--skip-clean --profile cov --out xml \
--features experimental-widgets,testing,image-proc
--features experimental-widgets,testing
env:
CARGO_PROFILE_COV_INHERITS: 'dev'
CARGO_PROFILE_COV_DEBUG: 1
+14
View File
@@ -0,0 +1,14 @@
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
on:
pull_request:
paths:
- '**/Cargo.toml'
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -0,0 +1,12 @@
name: Detects unused dependencies
on:
pull_request: { branches: "*" }
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Machete
uses: bnjbvr/cargo-machete@main
+2 -4
View File
@@ -36,7 +36,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
- name: Install Node.js
uses: actions/setup-node@v4
@@ -51,11 +51,9 @@ jobs:
# Keep in sync with xtask docs
- name: Build documentation
env:
# Work around https://github.com/rust-lang/cargo/issues/10744
CARGO_TARGET_APPLIES_TO_HOST: "true"
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
run:
cargo doc --no-deps --workspace --features docsrs
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+1 -1
View File
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.0.0
- uses: actions/checkout@v4
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+2 -2
View File
@@ -1,6 +1,6 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Codecov
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
@@ -64,7 +64,7 @@ jobs:
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
os-name: 🐧
cachekey-id: linux
- os: macos-12
- os: macos-15
os-name: 🍏
cachekey-id: macos
+119
View File
@@ -0,0 +1,119 @@
# Architecture
The SDK is split into multiple layers:
```
WASM (external crate matrix-rust-sdk-crypto-wasm)
/
/ uniffi
/ /
/ bindings (matrix-sdk-ffi)
crypto |
bindings |
| |
| UI (matrix-sdk-ui)
| \
| \
| main (matrix-sdk)
| / /
crypto /
\ /
store (matrix-sdk-base, + all the store impls)
|
common (matrix-sdk-common)
```
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
`MemoryStore` which is defined in `matrix-sdk-base`.
## `crates/matrix-sdk`
This is the main crate, and one that is expected to be used by most consumers. Notable data types
include:
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
sync, etc.
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
allows running queries that are room-specific, notably sending events.
## `crates/matrix-sdk-base`
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
storage backends, as well as dummy in-memory implementations of these traits.
## `crates/matrix-sdk-common`
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
crates (the only crate it's using is test helpers).
## `crates/matrix-sdk-crypto`
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
## `crates/matrix-sdk-indexeddb`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
indexeddb backend (for use in Web browsers, via WebAssembly).
## `crates/matrix-sdk-qrcode`
Implementation of QR codes for interactive verifications, used in the crypto crate.
## `crates/matrix-sdk-sqlite`
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
SQLite backend.
## `crates/matrix-sdk-store-encryption`
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
implement encryption at rest can use those primitives.
## `crates/matrix-sdk-ui`
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
everything related to crypto and E2EE for the current `Client`.
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
retrieving the list of current rooms, and exposing its entries.
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
down.
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
(aggregations) into single timeline items.
## `bindings/matrix-sdk-crypto-ffi/`
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
Element apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi/`
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
## `bindings/matrix-sdk-ffi-macros/`
Macros used in `bindings/matrix-sdk-ffi`.
## `testing/matrix-sdk-test/`
Common test helpers, used by all the other crates.
## `testing/matrix-sdk-test-macros/`
Implementation of the `#[async_test]` test macro.
## `testing/matrix-sdk-integration-testing/`
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
setup is provided to ease running the tests, and it is compatible for running with Podman too.
# Inspiration
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
+132 -27
View File
@@ -29,43 +29,148 @@ 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*.
### Snapshot Testing
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.
You can add/review snapshot tests using [insta.rs](https://insta.rs)
A *good commit message* should be composed of:
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
Any code change that breaks serialisation will then break a test, the author will then have to decide
how to handle migration and test it if needed.
- 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:
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
- “add new method“
- “enhance performance“
- “fix receipts“
Unix:
```
curl -LsSf https://insta.rs/install.sh | sh
```
Examples of good commit messages:
Windows:
```
powershell -c "irm https://insta.rs/install.ps1 | iex"
```
- “ffi: Add new method `frobnicate_the_foos`
- “indexeddb: Break up the request inside `get_inbound_group_sessions`
- “read_receipts: Store receipts locally, fixing #12345
Usual flow is to first run the test, then review them.
```
cargo insta test
cargo insta review
```
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.
## Pull requests
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
branch name.)
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
each commit should have a *good commit message*.
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.
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.)
# Writing changelog entries
Our goal is to maintain clear, concise, and informative changelogs that
accurately document changes in the project. Changelog entries should be written
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
Be sure to include a link to the pull request for additional context. A
well-written changelog entry should be understandable even to those who may not
be deeply familiar with the project. Provide enough context to ensure clarity
and ease of understanding.
A couple of examples of bad changelog entry would look like:
```markdown
- Fixed a panic.
```
```markdown
- Added the Bar function to Foo.
```
A good example of a changelog entry could look like the following:
```markdown
- Use the inviter's server name and the server name from the room alias as
fallback values for the via parameter when requesting the room summary from
the homeserver. This ensures requests succeed even when the room being
previewed is hosted on a federated server.
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
```
For security-related changelog entries, please include the following additional
details alongside the pull request number:
* Impact: Clearly describe the issue's potential impact on users or systems.
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
```markdown
- Use a constant-time Base64 encoder for secret key material to mitigate
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
```
## Commit message format
Commit messages should be formatted as Conventional Commits. In addition, some
git trailers are supported and have special meaning (see below).
### Conventional commits
Conventional Commits are structured as follows:
```
<type>(<scope>): <short summary>
```
The type of changes which will be included in changelogs is one of the following:
* `feat`: A new feature
* `fix`: A bug fix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
* `ci`: Changes to CI configuration files and scripts
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
### Security fixes
Commits addressing security vulnerabilities must include specific trailers for
vulnerability metadata, which should also be reflected in the corresponding
changelog entry.
The metadata must be included in the following git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
Please include all of the fields that are available.
Example:
```
fix(crypto): Use a constant-time Base64 encoder for secret key material
This patch fixes a security issue around a side-channel vulnerability[1]
when decoding secret key material using Base64.
In some circumstances an attacker can obtain information about secret
secret key material via a controlled-channel and side-channel attack.
This patch avoids the side-channel by switching to the base64ct crate
for the encoding, and more importantly, the decoding of secret key
material.
Security-Impact: Low
CVE: CVE-2024-40640
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
```
## Review process
@@ -126,7 +231,7 @@ requested.
commits, the [autosquash] option can help with this.
```bash
git rebase main --autosquash
git rebase main --interactive --autosquash
```
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
Generated
+1011 -997
View File
File diff suppressed because it is too large Load Diff
+97 -46
View File
@@ -18,36 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.76"
rust-version = "1.83"
[workspace.dependencies]
anyhow = "1.0.68"
assert-json-diff = "2"
anyhow = "1.0.95"
aquamarine = "0.6.0"
assert-json-diff = "2.0.2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.1"
assert_matches2 = "0.1.2"
async-rx = "0.1.3"
async-stream = "0.3.3"
async-trait = "0.1.60"
async-stream = "0.3.5"
async-trait = "0.1.85"
as_variant = "1.2.0"
base64 = "0.22.0"
byteorder = "1.4.3"
base64 = "0.22.1"
byteorder = "1.5.0"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.5.0", features = ["tracing"] }
eyeball-im-util = "0.6.0"
futures-core = "0.3.28"
futures-executor = "0.3.21"
futures-util = { version = "0.3.26", default-features = false, features = [
"alloc",
] }
growable-bloom-filter = "2.1.0"
http = "1.1.0"
imbl = "3.0.0"
itertools = "0.12.0"
once_cell = "1.16.0"
pin-project-lite = "0.2.9"
eyeball-im = { version = "0.6.0", features = ["tracing"] }
eyeball-im-util = "0.8.0"
futures-core = "0.3.31"
futures-executor = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2.15", default-features = false }
gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.2.0"
imbl = "4.0.1"
indexmap = "2.7.1"
insta = { version = "1.42.1", features = ["json"] }
itertools = "0.14.0"
js-sys = "0.3.69"
mime = "0.3.17"
once_cell = "1.20.2"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d", features = [
reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { version = "0.12.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -60,37 +73,45 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf
"unstable-msc3489",
"unstable-msc4075",
"unstable-msc4140",
"unstable-msc4171",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" }
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
ruma-common = { version = "0.15.1" }
serde = "1.0.217"
serde_html_form = "0.2.7"
serde_json = "1.0.138"
sha2 = "0.10.8"
similar-asserts = "1.5.0"
similar-asserts = "1.6.1"
stream_assert = "0.1.1"
thiserror = "1.0.38"
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tempfile = "3.16.0"
thiserror = "2.0.11"
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
tracing-subscriber = "0.3.18"
unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.0"
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "57cbf7e939d7b54d20207e8361b7135bd65c9cc2", features = ["insecure-pk-encryption"] }
wiremock = "0.6.0"
zeroize = "1.6.0"
url = "2.5.4"
uuid = "1.12.1"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.33"
web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.10.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
@@ -107,6 +128,9 @@ debug = 0
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
quote = { opt-level = 2 }
sha2 = { opt-level = 2 }
# faster runs for insta.rs snapshot testing
insta.opt-level = 3
similar.opt-level = 3
# Custom profile with full debugging info, use `--profile dbg` to select
[profile.dbg]
@@ -122,10 +146,37 @@ 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" }
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
rust_2018_idioms = "warn"
semicolon_in_expressions_from_macros = "warn"
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
] }
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
[workspace.lints.clippy]
assigning_clones = "allow"
box_default = "allow"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
inefficient_to_string = "warn"
macro_use_imports = "warn"
mut_mut = "warn"
needless_borrow = "warn"
nonstandard_macro_braces = "warn"
str_to_string = "warn"
todo = "warn"
unused_async = "warn"
redundant_clone = "warn"
+2 -8
View File
@@ -24,17 +24,11 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
used to add Matrix E2EE support to your client or client library.
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
## Status
The library is in an alpha state, things that are implemented generally work but
the API will change in breaking ways.
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
If you are interested in using the matrix-sdk now is the time to try it out and
provide feedback.
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
## Bindings
-21
View File
@@ -1,21 +0,0 @@
# 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
```
+47
View File
@@ -0,0 +1,47 @@
# Releasing and publishing the SDK
While the release process can be handled manually, `cargo-release` has been
configured to make it more convenient.
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
that no pull request is required to cut a release. However, since the SDK
repo is set up so that each push requires a pull request, we need to slightly
deviate from the default workflow. A `cargo-xtask` has been created to make the
process as smooth as possible.
The procedure is as follows:
1. Switch to a release branch:
```bash
git switch -c release-x.y.z
  ```
2. Prepare the release. This will update the `README.md`, set the versions in
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
```bash
cargo xtask release prepare --execute minor|patch|rc
```
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
satisfied, push the branch and open a PR.
```bash
git push --set-upstream origin/release-x.y.z
```
4. Pass the review and merge the branch as you would with any other branch.
5. Create tags for your new release, publish the release on crates.io and push
the tags:
```bash
# Switch to main first.
git switch main
# Pull in the now-merged release commit(s).
git pull
# Create tags, publish the release on crates.io, and push the tags.
cargo xtask release publish --execute
```
For more information on cargo-release: https://github.com/crate-ci/cargo-release
-121
View File
@@ -1,121 +0,0 @@
# Upgrades 0.5 ➜ 0.6
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
## Minimum Supported Rust Version Update: `1.60`
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
## Dependencies
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
## Repo Structure Updates
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
## Architecture Changes / API overall
### Builder Pattern
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
- the [login configuration][login builder] and [login with sso][ssologin builder],
- [`SledStore` configuratiion][sled-store builder]
- [`Indexeddb` configuration][indexeddb builder]
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
### Splitting of concerns: Media
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
### Event Handling & sync updaes
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
### Refresh Tokens
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
### Further changes
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`.
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
- Javascript specific features are now behind the `js` feature-gate
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
## Quick Troubleshooting
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
### expected slice `[u8]`, found struct ...
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
```
|
69 | device.verified()
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
```
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
You are seeing something along the lines of:
```
19 | if room_member.state_key != client.user_id().await.unwrap() {
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
help: remove the `.await`
|
19 - if room_member.state_key != client.user_id().await.unwrap() {
19 + if room_member.state_key != client.user_id().unwrap() {
```
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
+4 -1
View File
@@ -23,7 +23,7 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre
wiremock = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
[[bench]]
name = "crypto_bench"
@@ -36,3 +36,6 @@ harness = false
[[bench]]
name = "room_bench"
harness = false
[package.metadata.release]
release = false
+24 -32
View File
@@ -1,22 +1,20 @@
use std::{sync::Arc, time::Duration};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
utils::IntoRawStateEventContent,
};
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
use matrix_sdk_base::{
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
use matrix_sdk_test::{
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
api::client::membership::get_member_events,
device_id,
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
owned_room_id, owned_user_id,
events::room::member::{MembershipState, RoomMemberEvent},
mxc_uri, owned_room_id, owned_user_id,
serde::Raw,
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
};
@@ -34,28 +32,17 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
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 f = EventFactory::new().room(&room_id);
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();
let event = f
.member(&user_id)
.membership(MembershipState::Join)
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
.display_name("Alice Margatroid")
.reason("Looking for support")
.into_raw();
member_events.push(event);
}
@@ -74,7 +61,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
.block_on(sqlite_store.save_changes(&changes))
.expect("initial filling of sqlite failed");
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
let base_client = BaseClient::with_store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
);
runtime
.block_on(base_client.set_session_meta(
@@ -171,8 +161,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
);
let room = client.get_room(&room_id).expect("Room not found");
assert!(!room.pinned_event_ids().is_empty());
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
let count = PINNED_EVENTS_COUNT;
let name = format!("{count} pinned events");
@@ -191,8 +182,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
b.to_async(&runtime).iter(|| async {
assert!(!room.pinned_event_ids().is_empty());
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert!(!pinned_event_ids.is_empty());
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
// Reset cache so it always loads the events from the mocked endpoint
client.event_cache().empty_immutable_cache().await;
+9 -3
View File
@@ -2,8 +2,8 @@ use std::sync::Arc;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use matrix_sdk::{
authentication::matrix::{MatrixSession, MatrixSessionTokens},
config::StoreConfig,
matrix_auth::{MatrixSession, MatrixSessionTokens},
Client, RoomInfo, RoomState, StateChanges,
};
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
@@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
@@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) {
b.to_async(&runtime).iter(|| async {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(StoreConfig::new().state_store(store.clone()))
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
)
.build()
.await
.expect("Can't build client");
+1
View File
@@ -13,6 +13,7 @@ let package = Package(
],
products: [
.library(name: "MatrixRustSDK",
type: .dynamic,
targets: ["MatrixRustSDK"]),
],
targets: [
@@ -26,6 +26,7 @@ futures-util = { workspace = true }
hmac = "0.12.1"
http = { workspace = true }
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
matrix-sdk-ffi-macros = { workspace = true }
pbkdf2 = "0.12.2"
rand = { workspace = true }
ruma = { workspace = true }
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
[lints]
workspace = true
[package.metadata.release]
release = false
-4
View File
@@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
```
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
+41 -17
View File
@@ -1,10 +1,15 @@
use std::{env, error::Error};
use std::{
env,
error::Error,
path::{Path, PathBuf},
process::Command,
};
use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-ffi/build.rs] too!
@@ -12,26 +17,45 @@ fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
if target_arch == "x86_64" && target_os == "android" {
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
let build_os = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "windows",
_ => panic!(
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
// Configure rust to statically link against the `libclang_rt.builtins` supplied
// with clang.
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
// Android NDK.
let clang_path = PathBuf::from(
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
// clang_path should now look something like
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
// We strip `/bin/clang` from the end to get the toolchain path.
let toolchain_path = clang_path
.ancestors()
.nth(2)
.expect("could not find NDK toolchain path")
.to_str()
.expect("NDK toolchain path is not valid UTF-8");
let clang_version = get_clang_major_version(&clang_path);
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
}
}
/// Run the clang binary at `clang_path`, and return its major version number
fn get_clang_major_version(clang_path: &Path) -> String {
let clang_output =
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
if !clang_output.status.success() {
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
}
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
clang_version.split('.').next().expect("could not parse clang output").to_owned()
}
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
@@ -69,7 +69,7 @@ impl BackupRecoveryKey {
const PBKDF_ROUNDS: i32 = 500_000;
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl BackupRecoveryKey {
/// Create a new random [`BackupRecoveryKey`].
#[allow(clippy::new_without_default)]
@@ -1,19 +1,25 @@
use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_crypto::dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
use matrix_sdk_crypto::{
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use serde_json::json;
use tokio::runtime::Handle;
use zeroize::Zeroize;
use crate::{CryptoStoreError, DehydratedDeviceKey};
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum DehydrationError {
#[error(transparent)]
Pickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
#[error(transparent)]
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
#[error(transparent)]
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
#[error(transparent)]
@@ -22,6 +28,8 @@ pub enum DehydrationError {
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
#[error(transparent)]
Rand(#[from] rand::Error),
}
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
@@ -29,10 +37,16 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
match value {
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
Self::LegacyPickle(e)
}
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
Self::MissingSigningKey(e)
}
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
Self::PickleKeyLength(l)
}
}
}
}
@@ -53,7 +67,7 @@ impl Drop for DehydratedDevices {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl DehydratedDevices {
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
let inner = self.runtime.block_on(self.inner.create())?;
@@ -66,14 +80,14 @@ impl DehydratedDevices {
pub fn rehydrate(
&self,
pickle_key: Vec<u8>,
pickle_key: &DehydratedDeviceKey,
device_id: String,
device_data: String,
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
let device_id: OwnedDeviceId = device_id.into();
let mut key = get_pickle_key(&pickle_key)?;
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
let ret = RehydratedDevice {
runtime: self.runtime.to_owned(),
@@ -85,10 +99,41 @@ impl DehydratedDevices {
}
.into();
key.zeroize();
Ok(ret)
}
/// Get the cached dehydrated device pickle key if any.
///
/// None if the key was not previously cached (via
/// [`Self::save_dehydrated_device_pickle_key`]).
///
/// Should be used to periodically rotate the dehydrated device to avoid
/// OTK exhaustion and accumulation of to_device messages.
pub fn get_dehydrated_device_key(
&self,
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
Ok(self
.runtime
.block_on(self.inner.get_dehydrated_device_pickle_key())?
.map(crate::DehydratedDeviceKey::from))
}
/// Store the dehydrated device pickle key in the crypto store.
///
/// This is useful if the client wants to periodically rotate dehydrated
/// devices to avoid OTK exhaustion and accumulated to_device problems.
pub fn save_dehydrated_device_key(
&self,
pickle_key: &crate::DehydratedDeviceKey,
) -> Result<(), CryptoStoreError> {
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
}
/// Deletes the previously stored dehydrated device pickle key.
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
}
}
#[derive(uniffi::Object)]
@@ -107,7 +152,7 @@ impl Drop for RehydratedDevice {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RehydratedDevice {
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
@@ -133,20 +178,18 @@ impl Drop for DehydratedDevice {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl DehydratedDevice {
pub fn keys_for_upload(
&self,
device_display_name: String,
pickle_key: Vec<u8>,
pickle_key: &DehydratedDeviceKey,
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
let mut key = get_pickle_key(&pickle_key)?;
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
let request =
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
key.zeroize();
Ok(request.into())
}
}
@@ -177,15 +220,36 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
}
}
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
let pickle_key_length = pickle_key.len();
#[cfg(test)]
mod tests {
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
if pickle_key_length == 32 {
let mut key = Box::new([0u8; 32]);
key.copy_from_slice(pickle_key);
#[test]
fn test_creating_dehydrated_key() {
let result = DehydratedDeviceKey::new();
assert!(result.is_ok());
let dehydrated_device_key = result.unwrap();
let base_64 = dehydrated_device_key.to_base64();
let inner_bytes = dehydrated_device_key.inner;
Ok(key)
} else {
Err(DehydrationError::PickleKeyLength(pickle_key_length))
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
assert_eq!(base_64, copy.to_base64());
}
#[test]
fn test_creating_dehydrated_key_failure() {
let bytes = [0u8; 24];
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
assert!(pickle_key.is_err());
match pickle_key {
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
assert_eq!(bytes.len(), pickle_key_length);
}
_ => panic!("Should have failed!"),
}
}
}
+6 -3
View File
@@ -1,8 +1,9 @@
#![allow(missing_docs)]
use matrix_sdk_crypto::{
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
SignatureError as InnerSignatureError,
};
use matrix_sdk_sqlite::OpenStoreError;
use ruma::{IdParseError, OwnedUserId};
@@ -57,6 +58,8 @@ pub enum CryptoStoreError {
InvalidUserId(String, IdParseError),
#[error(transparent)]
Identifier(#[from] IdParseError),
#[error(transparent)]
DehydrationError(#[from] InnerDehydrationError),
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -112,7 +115,7 @@ mod tests {
#[test]
fn test_withheld_error_mapping() {
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
use matrix_sdk_common::deserialized_responses::WithheldCode;
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
+57 -14
View File
@@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
use matrix_sdk_crypto::{
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
store::{
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
@@ -62,6 +65,8 @@ pub use verification::{
};
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
use crate::dehydrated_devices::DehydrationError;
/// Struct collecting data that is important to migrate to the rust-sdk
#[derive(Deserialize, Serialize, uniffi::Record)]
pub struct MigrationData {
@@ -196,7 +201,7 @@ impl From<anyhow::Error> for MigrationError {
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate(
data: MigrationData,
path: String,
@@ -359,7 +364,7 @@ async fn save_changes(
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the migration.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_sessions(
data: SessionMigrationData,
path: String,
@@ -532,7 +537,7 @@ fn collect_sessions(
/// * `passphrase` - The passphrase that should be used to encrypt the data at
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
/// store and all its data will remain unencrypted.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn migrate_room_settings(
room_settings: HashMap<String, RoomSettings>,
path: String,
@@ -558,7 +563,7 @@ pub fn migrate_room_settings(
}
/// Callback that will be passed over the FFI to report progress
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressListener {
/// The callback that should be called on the Rust side
///
@@ -675,15 +680,20 @@ pub struct EncryptionSettings {
impl From<EncryptionSettings> for RustEncryptionSettings {
fn from(v: EncryptionSettings) -> Self {
let sharing_strategy = if v.only_allow_trusted_devices {
CollectStrategy::OnlyTrustedDevices
} else if v.error_on_verified_user_problem {
CollectStrategy::ErrorOnVerifiedUserProblem
} else {
CollectStrategy::AllDevices
};
RustEncryptionSettings {
algorithm: v.algorithm.into(),
rotation_period: Duration::from_secs(v.rotation_period),
rotation_period_msgs: v.rotation_period_msgs,
history_visibility: v.history_visibility.into(),
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
only_allow_trusted_devices: v.only_allow_trusted_devices,
error_on_verified_user_problem: v.error_on_verified_user_problem,
},
sharing_strategy,
}
}
}
@@ -794,7 +804,7 @@ pub struct BackupKeys {
backup_version: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl BackupKeys {
/// Get the recovery key that we're holding on to.
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
@@ -822,6 +832,39 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
}
}
/// Dehydrated device key
#[derive(uniffi::Record, Clone)]
pub struct DehydratedDeviceKey {
pub(crate) inner: Vec<u8>,
}
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
}
/// Creates a new dehydration pickle key from the given slice.
///
/// Fail if the slice length is not 32.
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
Ok(inner.into())
}
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
pub fn to_base64(&self) -> String {
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
inner.to_base64()
}
}
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
DehydratedDeviceKey { inner: pickle_key.into() }
}
}
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
@@ -891,7 +934,7 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn version_info() -> VersionInfo {
VersionInfo {
version: matrix_sdk_crypto::VERSION.to_owned(),
@@ -915,12 +958,12 @@ pub struct VersionInfo {
pub git_description: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn version() -> String {
matrix_sdk_crypto::VERSION.to_owned()
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn vodozemac_version() -> String {
vodozemac::VERSION.to_owned()
}
@@ -935,7 +978,7 @@ pub struct PkEncryption {
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl PkEncryption {
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
/// encoded as Base64.
+2 -2
View File
@@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
/// Trait that can be used to forward Rust logs over FFI to a language specific
/// logger.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait Logger: Send {
/// Called every time the Rust side wants to post a log line.
fn log(&self, log_line: String);
@@ -42,7 +42,7 @@ pub struct LoggerWrapper {
}
/// Set the logger that should be used to forward Rust logs over FFI.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn set_logger(logger: Box<dyn Logger>) {
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
+16 -16
View File
@@ -17,8 +17,8 @@ use matrix_sdk_crypto::{
decrypt_room_key_export, encrypt_room_key_export,
olm::ExportedRoomKey,
store::{BackupDecryptionKey, Changes},
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, TrustRequirement,
UserIdentities,
types::requests::ToDeviceRequest,
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
};
use ruma::{
api::{
@@ -38,11 +38,12 @@ use ruma::{
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
AnySyncMessageLikeEvent, MessageLikeEvent,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
@@ -178,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl OlmMachine {
/// Create a new `OlmMachine`
///
@@ -314,8 +315,8 @@ impl OlmMachine {
if let Some(user_identity) = user_identity {
Ok(match user_identity {
UserIdentities::Own(i) => self.runtime.block_on(i.verify())?,
UserIdentities::Other(i) => self.runtime.block_on(i.verify())?,
SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?,
SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?,
}
.into())
} else {
@@ -528,11 +529,11 @@ impl OlmMachine {
) -> Result<SyncChangesResult, CryptoStoreError> {
let to_device: ToDevice = serde_json::from_str(&events)?;
let device_changes: RumaDeviceLists = device_changes.into();
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
.into_iter()
.map(|(k, v)| {
(
DeviceKeyAlgorithm::from(k),
OneTimeKeyAlgorithm::from(k),
v.clamp(0, i32::MAX)
.try_into()
.expect("Couldn't convert key counts into an UInt"),
@@ -540,8 +541,8 @@ impl OlmMachine {
})
.collect();
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
let (to_device_events, room_key_infos) = self.runtime.block_on(
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
@@ -861,12 +862,14 @@ impl OlmMachine {
/// * `strict_shields` - If `true`, messages will be decorated with strict
/// warnings (use `false` to match legacy behaviour where unsafe keys have
/// lower severity warnings and unverified identities are not decorated).
/// * `decryption_settings` - The setting for decrypting messages.
pub fn decrypt_room_event(
&self,
event: String,
room_id: String,
handle_verification_events: bool,
strict_shields: bool,
decryption_settings: DecryptionSettings,
) -> Result<DecryptedEvent, DecryptionError> {
// Element Android wants only the content and the type and will create a
// decrypted event with those two itself, this struct makes sure we
@@ -882,8 +885,6 @@ impl OlmMachine {
let event: Raw<_> = serde_json::from_str(&event)?;
let room_id = RoomId::parse(room_id)?;
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(
&event,
&room_id,
@@ -891,7 +892,7 @@ impl OlmMachine {
))?;
if handle_verification_events {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
if let Ok(e) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
@@ -909,8 +910,7 @@ impl OlmMachine {
}
}
let encryption_info =
decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info");
let encryption_info = decrypted.encryption_info;
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
+15 -12
View File
@@ -4,9 +4,12 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
types::requests::{
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
},
CrossSigningBootstrapRequests,
};
use ruma::{
api::client::{
@@ -136,7 +139,7 @@ pub enum Request {
impl From<OutgoingRequest> for Request {
fn from(r: OutgoingRequest) -> Self {
use matrix_sdk_crypto::OutgoingRequests::*;
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
match r.request() {
KeysUpload(u) => {
@@ -338,16 +341,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
}
}
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
fn from(r: &'a OwnedResponse) -> Self {
match r {
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
}
}
}
+11 -5
View File
@@ -1,8 +1,8 @@
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
use crate::CryptoStoreError;
/// Enum representing cross signing identities of our own user or some other
/// Enum representing cross signing identity of our own user or some other
/// user.
#[derive(uniffi::Enum)]
pub enum UserIdentity {
@@ -18,6 +18,8 @@ pub enum UserIdentity {
user_signing_key: String,
/// The public self-signing key of our identity.
self_signing_key: String,
/// True if this identity was verified at some point but is not anymore.
has_verification_violation: bool,
},
/// The user identity of other users.
Other {
@@ -27,13 +29,15 @@ pub enum UserIdentity {
master_key: String,
/// The public self-signing key of our identity.
self_signing_key: String,
/// True if this identity was verified at some point but is not anymore.
has_verification_violation: bool,
},
}
impl UserIdentity {
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result<Self, CryptoStoreError> {
Ok(match i {
UserIdentities::Own(i) => {
SdkUserIdentity::Own(i) => {
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
@@ -44,9 +48,10 @@ impl UserIdentity {
master_key: serde_json::to_string(&master)?,
user_signing_key: serde_json::to_string(&user_signing)?,
self_signing_key: serde_json::to_string(&self_signing)?,
has_verification_violation: i.has_verification_violation(),
}
}
UserIdentities::Other(i) => {
SdkUserIdentity::Other(i) => {
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
@@ -54,6 +59,7 @@ impl UserIdentity {
user_id: i.user_id().to_string(),
master_key: serde_json::to_string(&master)?,
self_signing_key: serde_json::to_string(&self_signing)?,
has_verification_violation: i.has_verification_violation(),
}
}
})
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
/// Listener that will be passed over the FFI to report changes to a SAS
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SasListener: Send {
/// The callback that should be called on the Rust side
///
@@ -82,7 +82,7 @@ pub struct Verification {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Verification {
/// Try to represent the `Verification` as an `Sas` verification object,
/// returns `None` if the verification is not a `Sas` verification.
@@ -112,7 +112,7 @@ pub struct Sas {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Sas {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -276,7 +276,7 @@ impl Sas {
/// Listener that will be passed over the FFI to report changes to a QrCode
/// verification.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrCodeListener: Send {
/// The callback that should be called on the Rust side
///
@@ -328,7 +328,7 @@ pub struct QrCode {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCode {
/// Get the user id of the other side.
pub fn other_user_id(&self) -> String {
@@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult {
/// Listener that will be passed over the FFI to report changes to a
/// verification request.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationRequestListener: Send {
/// The callback that should be called on the Rust side
///
@@ -562,7 +562,7 @@ pub struct VerificationRequest {
pub(crate) runtime: Handle,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl VerificationRequest {
/// The id of the other user that is participating in this verification
/// request.
@@ -752,7 +752,7 @@ impl VerificationRequest {
RustVerificationRequestState::Ready {
their_methods,
our_methods,
other_device_id: _,
other_device_data: _,
} => VerificationRequestState::Ready {
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
@@ -791,8 +791,7 @@ impl VerificationRequest {
// task.
let should_break = matches!(
state,
RustVerificationRequestState::Done { .. }
| RustVerificationRequestState::Cancelled { .. }
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
);
let state = Self::convert_verification_request(&request, state);
+28
View File
@@ -0,0 +1,28 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
name = "matrix-sdk-ffi-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
publish = false
[lib]
proc-macro = true
test = false
doctest = false
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.18"
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
[lints]
workspace = true
[package.metadata.release]
release = false
+14
View File
@@ -0,0 +1,14 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/main.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
# matrix-sdk-ffi-macros
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
+65
View File
@@ -0,0 +1,65 @@
// 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 proc_macro::TokenStream;
use quote::quote;
use syn::{ImplItem, Item, TraitItem};
/// Attribute to specify the async runtime parameter for the `uniffi`
/// export macros if there any `async fn`s in the input.
#[proc_macro_attribute]
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
let has_async_fn = |item| {
if let Item::Fn(fun) = &item {
if fun.sig.asyncness.is_some() {
return true;
}
} else if let Item::Impl(blk) = &item {
for item in &blk.items {
if let ImplItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
}
}
}
false
};
let attr2 = proc_macro2::TokenStream::from(attr);
let item2 = proc_macro2::TokenStream::from(item.clone());
let res = match syn::parse(item) {
Ok(item) => match has_async_fn(item) {
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
false => quote! { #[uniffi::export(#attr2)] },
},
Err(e) => e.into_compile_error(),
};
quote! {
#res
#item2
}
.into()
}
+6
View File
@@ -2,6 +2,9 @@
Breaking changes:
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
original message and the associated error code and kind.
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
`SendingFromUnverifiedDevice`. These indicate that your own device is not
properly cross-signed, which is a requirement when using the identity-based
@@ -31,4 +34,7 @@ Breaking changes:
Additions:
- Add `Encryption::get_user_identity` which returns `UserIdentity`
- Add `ClientBuilder::room_key_recipient_strategy`
- Add `Room::send_raw`
- Expose `withdraw_verification` to `UserIdentity`
+4 -3
View File
@@ -28,11 +28,11 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-util = { workspace = true }
log-panics = { version = "2", features = ["with-backtrace"] }
matrix-sdk-ffi-macros = { workspace = true }
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.16"
once_cell = { workspace = true }
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 }
serde_json = { workspace = true }
thiserror = { workspace = true }
@@ -56,7 +56,6 @@ features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-sliding-sync",
"experimental-widgets",
"markdown",
"rustls-tls", # note: differ from block below
@@ -71,7 +70,6 @@ features = [
"anyhow",
"e2e-encryption",
"experimental-oidc",
"experimental-sliding-sync",
"experimental-widgets",
"markdown",
"native-tls", # note: differ from block above
@@ -82,3 +80,6 @@ features = [
[lints]
workspace = true
[package.metadata.release]
release = false
+41 -17
View File
@@ -1,10 +1,15 @@
use std::{env, error::Error};
use std::{
env,
error::Error,
path::{Path, PathBuf},
process::Command,
};
use vergen::EmitBuilder;
/// Adds a temporary workaround for an issue with the Rust compiler and Android
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
///
/// IMPORTANT: if you modify this, make sure to modify
/// [../matrix-sdk-crypto-ffi/build.rs] too!
@@ -12,26 +17,45 @@ fn setup_x86_64_android_workaround() {
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
if target_arch == "x86_64" && target_os == "android" {
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
let build_os = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "windows",
_ => panic!(
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
),
};
const DEFAULT_CLANG_VERSION: &str = "18";
let clang_version =
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
let linux_x86_64_lib_dir = format!(
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
// Configure rust to statically link against the `libclang_rt.builtins` supplied
// with clang.
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
// Android NDK.
let clang_path = PathBuf::from(
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
);
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
// clang_path should now look something like
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
// We strip `/bin/clang` from the end to get the toolchain path.
let toolchain_path = clang_path
.ancestors()
.nth(2)
.expect("could not find NDK toolchain path")
.to_str()
.expect("NDK toolchain path is not valid UTF-8");
let clang_version = get_clang_major_version(&clang_path);
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
}
}
/// Run the clang binary at `clang_path`, and return its major version number
fn get_clang_major_version(clang_path: &Path) -> String {
let clang_output =
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
if !clang_output.status.success() {
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
}
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
clang_version.split('.').next().expect("could not parse clang output").to_owned()
}
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
-12
View File
@@ -8,15 +8,3 @@ dictionary Mentions {
interface RoomMessageEventContentWithoutRelation {
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
};
[Error]
interface ClientError {
Generic(string msg);
};
interface MediaSource {
[Name=from_json, Throws=ClientError]
constructor(string json);
string to_json();
string url();
};
+11 -4
View File
@@ -5,7 +5,7 @@ use std::{
};
use matrix_sdk::{
oidc::{
authentication::oidc::{
registrations::OidcRegistrationsError,
types::{
iana::oauth::OAuthClientAuthenticationMethod,
@@ -19,17 +19,18 @@ use matrix_sdk::{
};
use url::Url;
use crate::client::{Client, SlidingSyncVersion};
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
#[derive(uniffi::Object)]
pub struct HomeserverLoginDetails {
pub(crate) url: String,
pub(crate) sliding_sync_version: SlidingSyncVersion,
pub(crate) supports_oidc_login: bool,
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
pub(crate) supports_password_login: bool,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl HomeserverLoginDetails {
/// The URL of the currently configured homeserver.
pub fn url(&self) -> String {
@@ -46,6 +47,12 @@ impl HomeserverLoginDetails {
self.supports_oidc_login
}
/// The prompts advertised by the authentication issuer for use in the login
/// URL.
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
self.supported_oidc_prompts.clone()
}
/// Whether the current homeserver supports the password login flow.
pub fn supports_password_login(&self) -> bool {
self.supports_password_login
@@ -62,7 +69,7 @@ pub struct SsoHandler {
pub(crate) url: String,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SsoHandler {
/// Returns the URL for starting SSO authentication. The URL should be
/// opened in a web view. Once the web view succeeds, call `finish` with
+412 -83
View File
@@ -1,17 +1,13 @@
use std::{
collections::HashMap,
fmt::Debug,
mem::ManuallyDrop,
path::Path,
sync::{Arc, RwLock},
};
use anyhow::{anyhow, Context as _};
use matrix_sdk::{
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSettings,
},
oidc::{
authentication::oidc::{
registrations::{ClientId, OidcRegistrations},
requests::account_management::AccountManagementActionFull,
types::{
@@ -19,28 +15,31 @@ use matrix_sdk::{
registration::{
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
},
requests::Prompt as SdkOidcPrompt,
},
OidcAuthorizationData, OidcSession,
},
media::{
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
MediaThumbnailSettings,
},
reqwest::StatusCode,
ruma::{
api::client::{
media::get_content_thumbnail::v3::Method,
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
session::get_login_types,
user_directory::search_users,
},
events::{
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
},
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
sliding_sync::Version as SdkSlidingSyncVersion,
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
};
use matrix_sdk_ui::notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -54,13 +53,19 @@ use ruma::{
},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
room::{
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{json, Value};
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
@@ -74,9 +79,10 @@ use crate::{
notification_settings::NotificationSettings,
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
ruma::AuthData,
ruma::{AuthData, MediaSource},
sync_service::{SyncService, SyncServiceBuilder},
task_handle::TaskHandle,
utils::AsyncRuntimeDropped,
ClientError,
};
@@ -114,7 +120,7 @@ impl TryFrom<PusherKind> for RumaPusherKind {
let mut ruma_data = RumaHttpPusherData::new(data.url);
if let Some(payload) = data.default_payload {
let json: Value = serde_json::from_str(&payload)?;
ruma_data.default_payload = json;
ruma_data.data.insert("default_payload".to_owned(), json);
}
ruma_data.format = data.format.map(Into::into);
Ok(Self::Http(ruma_data))
@@ -140,25 +146,25 @@ impl From<PushFormat> for RumaPushFormat {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientDelegate: Sync + Send {
fn did_receive_auth_error(&self, is_soft_logout: bool);
fn did_refresh_tokens(&self);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientSessionDelegate: Sync + Send {
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
fn save_session_in_keychain(&self, session: Session);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ProgressWatcher: Send + Sync {
fn transmission_progress(&self, progress: TransmissionProgress);
}
/// A listener to the global (client-wide) error reporter of the send queue.
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomErrorListener: Sync + Send {
/// Called every time the send queue has ran into an error for a given room,
/// which will disable the send queue for that particular room.
@@ -182,58 +188,52 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: ManuallyDrop<MatrixClient>,
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
}
impl Drop for Client {
fn drop(&mut self) {
// Dropping the inner OlmMachine must happen within a tokio context
// because deadpool drops sqlite connections in the DB pool on tokio's
// blocking threadpool to avoid blocking async worker threads.
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl Client {
pub async fn new(
sdk_client: MatrixClient,
cross_process_refresh_lock_id: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
) -> Result<Self, ClientError> {
let session_verification_controller: Arc<
tokio::sync::RwLock<Option<SessionVerificationController>>,
> = Default::default();
let ctrl = session_verification_controller.clone();
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller.process_to_device_message(ev).await;
} else {
debug!("received to-device message, but verification controller isn't ready");
}
});
let cross_process_store_locks_holder_name =
sdk_client.cross_process_store_locks_holder_name().to_owned();
let client = Client {
inner: ManuallyDrop::new(sdk_client),
inner: AsyncRuntimeDropped::new(sdk_client),
delegate: RwLock::new(None),
session_verification_controller,
};
if let Some(process_id) = cross_process_refresh_lock_id {
if enable_oidc_refresh_lock {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates when enabling the cross-process lock"
))?;
}
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
client
.inner
.oidc()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
}
if let Some(session_delegate) = session_delegate {
@@ -260,11 +260,35 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Information about login options for the client's homeserver.
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok();
let oidc = self.inner.oidc();
let (supports_oidc_login, supported_oidc_prompts) = match oidc
.fetch_authentication_issuer()
.await
{
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
Ok(metadata) => {
let prompts = metadata
.prompt_values_supported
.as_ref()
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
(true, prompts)
}
Err(error) => {
error!("Failed to fetch OIDC provider metadata: {error}");
(true, Default::default())
}
},
Err(error) => {
error!("Failed to fetch authentication issuer: {error}");
(false, Default::default())
}
};
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
let sliding_sync_version = self.sliding_sync_version();
@@ -272,6 +296,7 @@ impl Client {
url: self.homeserver(),
sliding_sync_version,
supports_oidc_login,
supported_oidc_prompts,
supports_password_login,
})
}
@@ -295,6 +320,31 @@ impl Client {
Ok(())
}
/// Login using JWT
/// This is an implementation of the custom_login https://docs.rs/matrix-sdk/latest/matrix_sdk/matrix_auth/struct.MatrixAuth.html#method.login_custom
/// For more information on logging in with JWT: https://element-hq.github.io/synapse/latest/jwt.html
pub async fn custom_login_with_jwt(
&self,
jwt: String,
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<(), ClientError> {
let data = json!({ "token": jwt }).as_object().unwrap().clone();
let mut builder = self.inner.matrix_auth().login_custom("org.matrix.login.jwt", data)?;
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(())
}
/// Login using an email and password.
pub async fn login_with_email(
&self,
@@ -335,13 +385,14 @@ impl Client {
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
}
/// Requests the URL needed for login in a web view using OIDC. Once the web
/// Requests the URL needed for opening a web view using OIDC. Once the web
/// view has succeeded, call `login_with_oidc_callback` with the callback it
/// returns. If a failure occurs and a callback isn't available, make sure
/// to call `abort_oidc_login` to inform the client of this.
pub async fn url_for_oidc_login(
/// to call `abort_oidc_auth` to inform the client of this.
pub async fn url_for_oidc(
&self,
oidc_configuration: &OidcConfiguration,
prompt: OidcPrompt,
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
@@ -362,14 +413,15 @@ impl Client {
static_registrations,
)?;
let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?;
let data =
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
Ok(Arc::new(data))
}
/// Aborts an existing OIDC login operation that might have been cancelled,
/// failed etc.
pub async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
self.inner.oidc().abort_authorization(&authorization_data.state).await;
}
@@ -389,7 +441,7 @@ impl Client {
pub async fn get_media_file(
&self,
media_source: Arc<MediaSource>,
body: Option<String>,
filename: Option<String>,
mime_type: String,
use_cache: bool,
temp_dir: Option<String>,
@@ -401,8 +453,8 @@ impl Client {
.inner
.media()
.get_media_file(
&MediaRequest { source, format: MediaFormat::File },
body,
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
@@ -449,7 +501,7 @@ impl Client {
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
// Respawn tasks for rooms that had unsent events. At this point we've just
// created the subscriber, so it'll be notified about errors.
q.respawn_tasks_for_rooms_with_unsent_events().await;
q.respawn_tasks_for_rooms_with_unsent_requests().await;
loop {
match subscriber.recv().await {
@@ -501,7 +553,7 @@ impl Client {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
@@ -552,6 +604,10 @@ impl Client {
&self,
action: Option<AccountManagementAction>,
) -> Result<Option<String>, ClientError> {
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
return Ok(None);
}
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
Ok(url) => Ok(url.map(|u| u.to_string())),
Err(e) => {
@@ -617,7 +673,7 @@ impl Client {
}
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
let response = self.inner.create_room(request.into()).await?;
let response = self.inner.create_room(request.try_into()?).await?;
Ok(String::from(response.room_id()))
}
@@ -650,7 +706,7 @@ impl Client {
progress_watcher: Option<Box<dyn ProgressWatcher>>,
) -> Result<String, ClientError> {
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
let request = self.inner.media().upload(&mime_type, data);
let request = self.inner.media().upload(&mime_type, data, None);
if let Some(progress_watcher) = progress_watcher {
let mut subscriber = request.subscribe_to_send_progress();
@@ -670,12 +726,13 @@ impl Client {
&self,
media_source: Arc<MediaSource>,
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
let source = (*media_source).clone().media_source;
debug!(?source, "requesting media file");
Ok(self
.inner
.media()
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
.get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
.await?)
}
@@ -685,16 +742,16 @@ impl Client {
width: u64,
height: u64,
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
let source = (*media_source).clone().media_source;
debug!(?source, width, height, "requesting media thumbnail");
Ok(self
.inner
.media()
.get_media_content(
&MediaRequest {
&MediaRequestParameters {
source,
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
Method::Scale,
UInt::new(width).unwrap(),
UInt::new(height).unwrap(),
)),
@@ -947,6 +1004,20 @@ impl Client {
Ok(Arc::new(Room::new(room)))
}
/// Knock on a room to join it using its ID or alias.
pub async fn knock(
&self,
room_id_or_alias: String,
reason: Option<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(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
let room = self.inner.knock(room_id, reason, server_names).await?;
Ok(Arc::new(Room::new(room)))
}
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
Ok(self
.inner
@@ -969,10 +1040,21 @@ impl Client {
pub async fn resolve_room_alias(
&self,
room_alias: String,
) -> Result<ResolvedRoomAlias, ClientError> {
) -> Result<Option<ResolvedRoomAlias>, ClientError> {
let room_alias = RoomAliasId::parse(&room_alias)?;
let response = self.inner.resolve_room_alias(&room_alias).await?;
Ok(response.into())
match self.inner.resolve_room_alias(&room_alias).await {
Ok(response) => Ok(Some(response.into())),
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
Some(StatusCode::NOT_FOUND) => Ok(None),
_ => Err(http_error.into()),
},
Err(error) => Err(error.into()),
}
}
/// Checks if a room alias exists in the current homeserver.
pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
}
/// Given a room id, get the preview of a room, to interact with it.
@@ -984,7 +1066,7 @@ impl Client {
&self,
room_id: String,
via_servers: Vec<String>,
) -> Result<RoomPreview, ClientError> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
let via_servers = via_servers
@@ -997,16 +1079,16 @@ impl Client {
// 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?;
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), 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> {
) -> Result<Arc<RoomPreview>, ClientError> {
let room_alias =
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
@@ -1014,9 +1096,9 @@ impl Client {
// 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?;
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
Ok(RoomPreview::from_sdk(sdk_room_preview))
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
}
/// Waits until an at least partially synced room is received, and returns
@@ -1058,9 +1140,21 @@ impl Client {
Ok(())
}
/// Checks if a room alias is not in use yet.
///
/// Returns:
/// - `Ok(true)` if the room alias is available.
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
/// status code).
/// - An `Err` otherwise.
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
let alias = RoomAliasId::parse(alias)?;
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IgnoredUsersListener: Sync + Send {
fn call(&self, ignored_user_ids: Vec<String>);
}
@@ -1281,16 +1375,23 @@ pub struct CreateRoomParameters {
pub avatar: Option<String>,
#[uniffi(default = None)]
pub power_level_content_override: Option<PowerLevels>,
#[uniffi(default = None)]
pub join_rule_override: Option<JoinRule>,
#[uniffi(default = None)]
pub canonical_alias: Option<String>,
}
impl From<CreateRoomParameters> for create_room::v3::Request {
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
type Error = ClientError;
fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
let mut request = create_room::v3::Request::new();
request.name = value.name;
request.topic = value.topic;
request.is_direct = value.is_direct;
request.visibility = value.visibility.into();
request.preset = Some(value.preset.into());
request.room_alias_name = value.canonical_alias;
request.invite = match value.invite {
Some(invite) => invite
.iter()
@@ -1318,6 +1419,12 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
if let Some(join_rule_override) = value.join_rule_override {
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
}
request.initial_state = initial_state;
if let Some(power_levels) = value.power_level_content_override {
@@ -1326,12 +1433,14 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
request.power_level_content_override = Some(power_levels);
}
Err(e) => {
error!("Failed to serialize power levels, error: {e}");
return Err(ClientError::Generic {
msg: format!("Failed to serialize power levels, error: {e}"),
})
}
}
}
request
Ok(request)
}
}
@@ -1342,6 +1451,9 @@ pub enum RoomVisibility {
/// Indicates that the room will not be shown in the published room list.
Private,
/// A custom value that's not present in the spec.
Custom { value: String },
}
impl From<RoomVisibility> for Visibility {
@@ -1349,6 +1461,17 @@ impl From<RoomVisibility> for Visibility {
match value {
RoomVisibility::Public => Self::Public,
RoomVisibility::Private => Self::Private,
RoomVisibility::Custom { value } => value.as_str().into(),
}
}
}
impl From<Visibility> for RoomVisibility {
fn from(value: Visibility) -> Self {
match value {
Visibility::Public => Self::Public,
Visibility::Private => Self::Private,
_ => Self::Custom { value: value.as_str().to_owned() },
}
}
}
@@ -1412,10 +1535,13 @@ impl Session {
match auth_api {
// Build the session from the regular Matrix Auth Session.
AuthApi::Matrix(a) => {
let matrix_sdk::matrix_auth::MatrixSession {
let matrix_sdk::authentication::matrix::MatrixSession {
meta: matrix_sdk::SessionMeta { user_id, device_id },
tokens:
matrix_sdk::matrix_auth::MatrixSessionTokens { access_token, refresh_token },
matrix_sdk::authentication::matrix::MatrixSessionTokens {
access_token,
refresh_token,
},
} = a.session().context("Missing session")?;
Ok(Session {
@@ -1430,10 +1556,10 @@ impl Session {
}
// Build the session from the OIDC UserSession.
AuthApi::Oidc(api) => {
let matrix_sdk::oidc::UserSession {
let matrix_sdk::authentication::oidc::UserSession {
meta: matrix_sdk::SessionMeta { user_id, device_id },
tokens:
matrix_sdk::oidc::OidcSessionTokens {
matrix_sdk::authentication::oidc::OidcSessionTokens {
access_token,
refresh_token,
latest_id_token,
@@ -1494,12 +1620,12 @@ impl TryFrom<Session> for AuthSession {
.transpose()
.context("OIDC latest_id_token is invalid.")?;
let user_session = matrix_sdk::oidc::UserSession {
let user_session = matrix_sdk::authentication::oidc::UserSession {
meta: matrix_sdk::SessionMeta {
user_id: user_id.try_into()?,
device_id: device_id.into(),
},
tokens: matrix_sdk::oidc::OidcSessionTokens {
tokens: matrix_sdk::authentication::oidc::OidcSessionTokens {
access_token,
refresh_token,
latest_id_token,
@@ -1513,15 +1639,15 @@ impl TryFrom<Session> for AuthSession {
user: user_session,
};
Ok(AuthSession::Oidc(session))
Ok(AuthSession::Oidc(session.into()))
} else {
// Create a regular Matrix Session.
let session = matrix_sdk::matrix_auth::MatrixSession {
let session = matrix_sdk::authentication::matrix::MatrixSession {
meta: matrix_sdk::SessionMeta {
user_id: user_id.try_into()?,
device_id: device_id.into(),
},
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
access_token,
refresh_token,
},
@@ -1617,7 +1743,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn gen_transaction_id() -> String {
TransactionId::new().to_string()
}
@@ -1635,7 +1761,7 @@ impl MediaFileHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> Result<String, ClientError> {
@@ -1699,3 +1825,206 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum OidcPrompt {
/// The Authorization Server must not display any authentication or consent
/// user interface pages.
None,
/// The Authorization Server should prompt the End-User for
/// reauthentication.
Login,
/// The Authorization Server should prompt the End-User for consent before
/// returning information to the Client.
Consent,
/// The Authorization Server should prompt the End-User to select a user
/// account.
///
/// This enables an End-User who has multiple accounts at the Authorization
/// Server to select amongst the multiple accounts that they might have
/// current sessions for.
SelectAccount,
/// The Authorization Server should prompt the End-User to create a user
/// account.
///
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
Create,
/// An unknown value.
Unknown { value: String },
}
impl From<&SdkOidcPrompt> for OidcPrompt {
fn from(value: &SdkOidcPrompt) -> Self {
match value {
SdkOidcPrompt::None => Self::None,
SdkOidcPrompt::Login => Self::Login,
SdkOidcPrompt::Consent => Self::Consent,
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
SdkOidcPrompt::Create => Self::Create,
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
_ => Self::Unknown { value: value.to_string() },
}
}
}
impl From<OidcPrompt> for SdkOidcPrompt {
fn from(value: OidcPrompt) -> Self {
match value {
OidcPrompt::None => Self::None,
OidcPrompt::Login => Self::Login,
OidcPrompt::Consent => Self::Consent,
OidcPrompt::SelectAccount => Self::SelectAccount,
OidcPrompt::Create => Self::Create,
OidcPrompt::Unknown { value } => Self::Unknown(value),
}
}
}
/// The rule used for users wishing to join this room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum JoinRule {
/// Anyone can join the room without any prior action.
Public,
/// A user who wishes to join the room must first receive an invite to the
/// room from someone already inside of the room.
Invite,
/// Users can join the room if they are invited, or they can request an
/// invite to the room.
///
/// They can be allowed (invited) or denied (kicked/banned) access.
Knock,
/// Reserved but not yet implemented by the Matrix specification.
Private,
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s.
Restricted { rules: Vec<AllowRule> },
/// Users can join the room if they are invited, or if they meet any of the
/// conditions described in a set of [`AllowRule`]s, or they can request
/// an invite to the room.
KnockRestricted { rules: Vec<AllowRule> },
/// A custom join rule, up for interpretation by the consumer.
Custom {
/// The string representation for this custom rule.
repr: String,
},
}
/// An allow rule which defines a condition that allows joining a room.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AllowRule {
/// Only a member of the `room_id` Room can join the one this rule is used
/// in.
RoomMembership { room_id: String },
/// A custom allow rule implementation, containing its JSON representation
/// as a `String`.
Custom { json: String },
}
impl TryFrom<JoinRule> for RumaJoinRule {
type Error = ClientError;
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
match value {
JoinRule::Public => Ok(Self::Public),
JoinRule::Invite => Ok(Self::Invite),
JoinRule::Knock => Ok(Self::Knock),
JoinRule::Private => Ok(Self::Private),
JoinRule::Restricted { rules } => {
let rules = ruma_allow_rules_from_ffi(rules)?;
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::KnockRestricted { rules } => {
let rules = ruma_allow_rules_from_ffi(rules)?;
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
}
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
}
}
}
fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, ClientError> {
let mut ret = Vec::with_capacity(value.len());
for rule in value {
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
match rule {
Ok(rule) => ret.push(rule),
Err(error) => return Err(error),
}
}
Ok(ret)
}
impl TryFrom<AllowRule> for RumaAllowRule {
type Error = ClientError;
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
match value {
AllowRule::RoomMembership { room_id } => {
let room_id = RoomId::parse(room_id)?;
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
room_id,
)))
}
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
}
}
}
impl TryFrom<RumaJoinRule> for JoinRule {
type Error = String;
fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
match value {
RumaJoinRule::Knock => Ok(JoinRule::Knock),
RumaJoinRule::Public => Ok(JoinRule::Public),
RumaJoinRule::Private => Ok(JoinRule::Private),
RumaJoinRule::KnockRestricted(restricted) => {
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
Vec<_>,
Self::Error,
>>(
)?;
Ok(JoinRule::KnockRestricted { rules })
}
RumaJoinRule::Restricted(restricted) => {
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
Vec<_>,
Self::Error,
>>(
)?;
Ok(JoinRule::Restricted { rules })
}
RumaJoinRule::Invite => Ok(JoinRule::Invite),
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
_ => Err(format!("Unknown JoinRule: {:?}", value)),
}
}
}
impl TryFrom<RumaAllowRule> for AllowRule {
type Error = String;
fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
match value {
RumaAllowRule::RoomMembership(membership) => {
Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
}
RumaAllowRule::_Custom(repr) => {
let json = serde_json::to_string(&repr)
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
Ok(Self::Custom { json })
}
_ => Err(format!("Invalid AllowRule: {:?}", value)),
}
}
}
+83 -25
View File
@@ -1,13 +1,14 @@
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
use futures_util::StreamExt;
use matrix_sdk::{
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::{
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
CollectStrategy,
CollectStrategy, TrustRequirement,
},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
reqwest::Certificate,
ruma::{ServerName, UserId},
sliding_sync::{
@@ -47,7 +48,7 @@ pub struct QrCodeData {
inner: qrcode::QrCodeData,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl QrCodeData {
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
///
@@ -159,7 +160,7 @@ pub enum QrLoginProgress {
Done,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait QrLoginProgressListener: Sync + Send {
fn on_update(&self, state: QrLoginProgress);
}
@@ -202,6 +203,8 @@ pub enum ClientBuildError {
SlidingSyncVersion(VersionBuilderError),
#[error(transparent)]
Sdk(MatrixClientBuildError),
#[error(transparent)]
EventCache(#[from] EventCacheError),
#[error("Failed to build the client: {message}")]
Generic { message: String },
}
@@ -260,16 +263,22 @@ pub struct ClientBuilder {
proxy: Option<String>,
disable_ssl_verification: bool,
disable_automatic_token_refresh: bool,
cross_process_refresh_lock_id: Option<String>,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
additional_root_certificates: Vec<Vec<u8>>,
disable_built_in_root_certificates: bool,
encryption_settings: EncryptionSettings,
room_key_recipient_strategy: CollectStrategy,
decryption_trust_requirement: TrustRequirement,
request_config: Option<RequestConfig>,
/// Whether to enable use of the event cache store, for reloading events
/// when building timelines et al.
use_event_cache_persistent_storage: bool,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
@@ -283,7 +292,8 @@ impl ClientBuilder {
proxy: None,
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_refresh_lock_id: None,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
session_delegate: None,
additional_root_certificates: Default::default(),
disable_built_in_root_certificates: false,
@@ -294,18 +304,41 @@ impl ClientBuilder {
auto_enable_backups: false,
},
room_key_recipient_strategy: Default::default(),
decryption_trust_requirement: TrustRequirement::Untrusted,
request_config: Default::default(),
use_event_cache_persistent_storage: false,
})
}
pub fn enable_cross_process_refresh_lock(
/// Whether to use the event cache persistent storage or not.
///
/// This is a temporary feature flag, for testing the event cache's
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
///
/// This is disabled by default. When disabled, a one-time cleanup is
/// performed when creating the client, and it will clear all the events
/// previously stored in the event cache.
///
/// When enabled, it will attempt to store events in the event cache as
/// they're received, and reuse them when reconstructing timelines.
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.use_event_cache_persistent_storage = value;
Arc::new(builder)
}
pub fn cross_process_store_locks_holder_name(
self: Arc<Self>,
process_id: String,
session_delegate: Box<dyn ClientSessionDelegate>,
holder_name: String,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_refresh_lock_id = Some(process_id);
builder.session_delegate = Some(session_delegate.into());
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
Arc::new(builder)
}
@@ -449,6 +482,16 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set the trust requirement to be used when decrypting events.
pub fn room_decryption_trust_requirement(
self: Arc<Self>,
trust_requirement: TrustRequirement,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.decryption_trust_requirement = trust_requirement;
Arc::new(builder)
}
/// Add a default request config to this client.
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
@@ -460,9 +503,14 @@ impl ClientBuilder {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
inner_builder =
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
if let Some(session_paths) = &builder.session_paths {
let data_path = PathBuf::from(&session_paths.data_path);
let cache_path = PathBuf::from(&session_paths.cache_path);
let data_path = Path::new(&session_paths.data_path);
let cache_path = Path::new(&session_paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
@@ -470,12 +518,12 @@ impl ClientBuilder {
"Creating directories for data and cache stores.",
);
fs::create_dir_all(&data_path)?;
fs::create_dir_all(&cache_path)?;
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
inner_builder = inner_builder.sqlite_store_with_cache_path(
&data_path,
&cache_path,
data_path,
cache_path,
builder.passphrase.as_deref(),
);
} else {
@@ -548,7 +596,8 @@ impl ClientBuilder {
inner_builder = inner_builder
.with_encryption_settings(builder.encryption_settings)
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy);
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
.with_decryption_trust_requirement(builder.decryption_trust_requirement);
match builder.sliding_sync_version_builder {
SlidingSyncVersionBuilder::None => {
@@ -600,13 +649,22 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
if builder.use_event_cache_persistent_storage {
// Enable the persistent storage \o/
sdk_client.event_cache().enable_storage()?;
} else {
// Get rid of all the previous events, if any.
let store = sdk_client
.event_cache_store()
.lock()
.await
.map_err(EventCacheError::LockingStorage)?;
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
}
Ok(Arc::new(
Client::new(
sdk_client,
builder.cross_process_refresh_lock_id,
builder.session_delegate,
)
.await?,
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
.await?,
))
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub struct ElementWellKnown {
}
/// Helper function to parse a string into a ElementWellKnown struct
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
serde_json::from_str(&string).map_err(ClientError::new)
}
+111 -9
View File
@@ -6,6 +6,7 @@ use matrix_sdk::{
encryption::{backups, recovery},
};
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
use super::RUNTIME;
@@ -23,22 +24,22 @@ pub struct Encryption {
pub(crate) _client: Arc<Client>,
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupStateListener: Sync + Send {
fn on_update(&self, status: BackupState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait BackupSteadyStateListener: Sync + Send {
fn on_update(&self, status: BackupUploadState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RecoveryStateListener: Sync + Send {
fn on_update(&self, status: RecoveryState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait VerificationStateListener: Sync + Send {
fn on_update(&self, status: VerificationState);
}
@@ -162,7 +163,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait EnableRecoveryProgressListener: Sync + Send {
fn on_update(&self, status: EnableRecoveryProgress);
}
@@ -212,7 +213,7 @@ impl From<encryption::VerificationState> for VerificationState {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
/// called the fingerprint of the device.
@@ -253,7 +254,7 @@ impl Encryption {
/// Therefore it is necessary to poll the server for an answer every time
/// you want to differentiate between those two states.
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
Ok(self.inner.backups().exists_on_server().await?)
Ok(self.inner.backups().fetch_exists_on_server().await?)
}
pub fn recovery_state(&self) -> RecoveryState {
@@ -280,7 +281,7 @@ impl Encryption {
}
pub async fn is_last_device(&self) -> Result<bool> {
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
Ok(self.inner.recovery().is_last_device().await?)
}
pub async fn wait_for_backup_upload_steady_state(
@@ -315,6 +316,7 @@ impl Encryption {
pub async fn enable_recovery(
&self,
wait_for_backups_to_upload: bool,
mut passphrase: Option<String>,
progress_listener: Box<dyn EnableRecoveryProgressListener>,
) -> Result<String> {
let recovery = self.inner.recovery();
@@ -325,6 +327,12 @@ impl Encryption {
recovery.enable()
};
let enable = if let Some(passphrase) = &passphrase {
enable.with_passphrase(passphrase)
} else {
enable
};
let mut progress_stream = enable.subscribe_to_progress();
let task = RUNTIME.spawn(async move {
@@ -337,6 +345,7 @@ impl Encryption {
let ret = enable.await?;
task.abort();
passphrase.zeroize();
Ok(ret)
}
@@ -390,6 +399,7 @@ impl Encryption {
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());
@@ -402,6 +412,98 @@ impl Encryption {
pub async fn wait_for_e2ee_initialization_tasks(&self) {
self.inner.wait_for_e2ee_initialization_tasks().await;
}
/// Get the E2EE identity of a user.
///
/// This method always tries to fetch the identity from the store, which we
/// only have if the user is tracked, meaning that we are both members
/// of the same encrypted room. If no user is found locally, a request will
/// be made to the homeserver.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that the identity belongs to.
///
/// Returns a `UserIdentity` if one is found. Returns an error if there
/// was an issue with the crypto store or with the request to the
/// homeserver.
///
/// This will always return `None` if the client hasn't been logged in.
pub async fn user_identity(
&self,
user_id: String,
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
Ok(Some(identity)) => {
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
}
Ok(None) => {
info!("No identity found in the store.");
}
Err(error) => {
error!("Failed fetching identity from the store: {}", error);
}
};
info!("Requesting identity from the server.");
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
}
}
/// The E2EE identity of a user.
#[derive(uniffi::Object)]
pub struct UserIdentity {
inner: matrix_sdk::encryption::identities::UserIdentity,
}
#[matrix_sdk_ffi_macros::export]
impl UserIdentity {
/// Remember this identity, ensuring it does not result in a pin violation.
///
/// When we first see a user, we assume their cryptographic identity has not
/// been tampered with by the homeserver or another entity with
/// man-in-the-middle capabilities. We remember this identity and call this
/// action "pinning".
///
/// If the identity presented for the user changes later on, the newly
/// presented identity is considered to be in "pin violation". This
/// method explicitly accepts the new identity, allowing it to replace
/// the previously pinned one and bringing it out of pin violation.
///
/// UIs should display a warning to the user when encountering an identity
/// which is not verified and is in pin violation.
pub(crate) async fn pin(&self) -> Result<(), ClientError> {
Ok(self.inner.pin().await?)
}
/// Remove the requirement for this identity to be verified.
///
/// If an identity was previously verified and is not anymore it will be
/// reported to the user. In order to remove this notice users have to
/// verify again or to withdraw the verification requirement.
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
Ok(self.inner.withdraw_verification().await?)
}
/// Get the public part of the Master key of this user identity.
///
/// The public part of the Master key is usually used to uniquely identify
/// the identity.
///
/// Returns None if the master key does not actually contain any keys.
pub(crate) fn master_key(&self) -> Option<String> {
self.inner.master_key().get_first_key().map(|k| k.to_base64())
}
/// Is the user identity considered to be verified.
///
/// If the identity belongs to another user, our own user identity needs to
/// be verified as well for the identity to be considered to be verified.
pub fn is_verified(&self) -> bool {
self.inner.is_verified()
}
}
#[derive(uniffi::Object)]
@@ -409,7 +511,7 @@ pub struct IdentityResetHandle {
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl IdentityResetHandle {
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
/// process is using.
+576 -6
View File
@@ -1,17 +1,23 @@
use std::fmt::Display;
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
use matrix_sdk::{
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
use uniffi::UnexpectedUniFFICallbackError;
#[derive(Debug, thiserror::Error)]
use crate::{room_list::RoomListError, timeline::FocusEventError};
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
Generic { msg: String },
#[error("api error {code}: {msg}")]
MatrixApi { kind: ErrorKind, code: String, msg: String },
}
impl ClientError {
@@ -40,7 +46,22 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
impl From<matrix_sdk::Error> for ClientError {
fn from(e: matrix_sdk::Error) -> Self {
Self::new(e)
match e {
matrix_sdk::Error::Http(http_error) => {
if let Some(api_error) = http_error.as_client_api_error() {
if let ErrorBody::Standard { kind, message } = &api_error.body {
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
return Self::Generic { msg: message.to_string() };
};
return Self::MatrixApi { kind, code, msg: message.to_owned() };
}
}
Self::Generic { msg: http_error.to_string() }
}
_ => Self::Generic { msg: e.to_string() },
}
}
}
@@ -128,6 +149,12 @@ impl From<RoomError> for ClientError {
}
}
impl From<RoomListError> for ClientError {
fn from(e: RoomListError) -> Self {
Self::new(e)
}
}
impl From<EventCacheError> for ClientError {
fn from(e: EventCacheError) -> Self {
Self::new(e)
@@ -146,6 +173,108 @@ impl From<RoomSendQueueError> for ClientError {
}
}
impl From<NotYetImplemented> for ClientError {
fn from(_: NotYetImplemented) -> Self {
Self::new("This functionality is not implemented yet.")
}
}
impl From<FocusEventError> for ClientError {
fn from(e: FocusEventError) -> Self {
Self::new(e)
}
}
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
/// String.
///
/// Represent a failed to send unrecoverable error of an event sent via the
/// send_queue. It is a serializable representation of a client error, see
/// `From` implementation for more details. These errors can not be
/// automatically retried, but yet some manual action can be taken before retry
/// sending. If not the only solution is to delete the local event.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum QueueWedgeError {
/// This error occurs when there are some insecure devices in the room, and
/// the current encryption setting prohibit sharing with them.
InsecureDevices {
/// The insecure devices as a Map of userID to deviceID.
user_device_map: HashMap<String, Vec<String>>,
},
/// This error occurs when a previously verified user is not anymore, and
/// the current encryption setting prohibit sharing when it happens.
IdentityViolations {
/// The users that are expected to be verified but are not.
users: Vec<String>,
},
/// It is required to set up cross-signing and properly erify the current
/// session before sending.
CrossVerificationRequired,
/// Some media content to be sent has disappeared from the cache.
MissingMediaContent,
/// Some mime type couldn't be parsed.
InvalidMimeType { mime_type: String },
/// Other errors.
GenericApiError { msg: String },
}
/// Simple display implementation that strips out userIds/DeviceIds to avoid
/// accidental logging.
impl Display for QueueWedgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QueueWedgeError::InsecureDevices { .. } => {
f.write_str("There are insecure devices in the room")
}
QueueWedgeError::IdentityViolations { .. } => {
f.write_str("Some users that were previously verified are not anymore")
}
QueueWedgeError::CrossVerificationRequired => {
f.write_str("Own verification is required")
}
QueueWedgeError::MissingMediaContent => {
f.write_str("Media to be sent disappeared from local storage")
}
QueueWedgeError::InvalidMimeType { mime_type } => {
write!(f, "Invalid mime type '{mime_type}' for media upload")
}
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
}
}
}
impl From<SdkQueueWedgeError> for QueueWedgeError {
fn from(value: SdkQueueWedgeError) -> Self {
match value {
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
user_device_map: user_device_map
.iter()
.map(|(user_id, devices)| {
(
user_id.to_string(),
devices.iter().map(|device_id| device_id.to_string()).collect(),
)
})
.collect(),
},
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
},
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
SdkQueueWedgeError::InvalidMimeType { mime_type } => {
Self::InvalidMimeType { mime_type }
}
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
@@ -217,3 +346,444 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
Self::Generic { msg: e.to_string() }
}
}
/// Something has not been implemented yet.
#[derive(thiserror::Error, Debug)]
#[error("not implemented yet")]
pub struct NotYetImplemented;
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
// Please keep the variants sorted alphabetically.
pub enum ErrorKind {
/// `M_BAD_ALIAS`
///
/// One or more [room aliases] within the `m.room.canonical_alias` event do
/// not point to the room ID for which the state event is to be sent to.
///
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
BadAlias,
/// `M_BAD_JSON`
///
/// The request contained valid JSON, but it was malformed in some way, e.g.
/// missing required keys, invalid values for keys.
BadJson,
/// `M_BAD_STATE`
///
/// The state change requested cannot be performed, such as attempting to
/// unban a user who is not banned.
BadState,
/// `M_BAD_STATUS`
///
/// The application service returned a bad status.
BadStatus {
/// The HTTP status code of the response.
status: Option<u16>,
/// The body of the response.
body: Option<String>,
},
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
///
/// The user is unable to reject an invite to join the [server notices]
/// room.
///
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
CannotLeaveServerNoticeRoom,
/// `M_CANNOT_OVERWRITE_MEDIA`
///
/// The [`create_content_async`] endpoint was called with a media ID that
/// already has content.
///
/// [`create_content_async`]: crate::media::create_content_async
CannotOverwriteMedia,
/// `M_CAPTCHA_INVALID`
///
/// The Captcha provided did not match what was expected.
CaptchaInvalid,
/// `M_CAPTCHA_NEEDED`
///
/// A Captcha is required to complete the request.
CaptchaNeeded,
/// `M_CONNECTION_FAILED`
///
/// The connection to the application service failed.
ConnectionFailed,
/// `M_CONNECTION_TIMEOUT`
///
/// The connection to the application service timed out.
ConnectionTimeout,
/// `M_DUPLICATE_ANNOTATION`
///
/// The request is an attempt to send a [duplicate annotation].
///
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
DuplicateAnnotation,
/// `M_EXCLUSIVE`
///
/// The resource being requested is reserved by an application service, or
/// the application service making the request has not created the
/// resource.
Exclusive,
/// `M_FORBIDDEN`
///
/// Forbidden access, e.g. joining a room without permission, failed login.
Forbidden,
/// `M_GUEST_ACCESS_FORBIDDEN`
///
/// The room or resource does not permit [guests] to access it.
///
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
GuestAccessForbidden,
/// `M_INCOMPATIBLE_ROOM_VERSION`
///
/// The client attempted to join a room that has a version the server does
/// not support.
IncompatibleRoomVersion {
/// The room's version.
room_version: String,
},
/// `M_INVALID_PARAM`
///
/// A parameter that was specified has the wrong value. For example, the
/// server expected an integer and instead received a string.
InvalidParam,
/// `M_INVALID_ROOM_STATE`
///
/// The initial state implied by the parameters to the [`create_room`]
/// request is invalid, e.g. the user's `power_level` is set below that
/// necessary to set the room name.
///
/// [`create_room`]: crate::room::create_room
InvalidRoomState,
/// `M_INVALID_USERNAME`
///
/// The desired user name is not valid.
InvalidUsername,
/// `M_LIMIT_EXCEEDED`
///
/// The request has been refused due to [rate limiting]: too many requests
/// have been sent in a short period of time.
///
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
LimitExceeded {
/// How long a client should wait before they can try again.
retry_after_ms: Option<u64>,
},
/// `M_MISSING_PARAM`
///
/// A required parameter was missing from the request.
MissingParam,
/// `M_MISSING_TOKEN`
///
/// No [access token] was specified for the request, but one is required.
///
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
MissingToken,
/// `M_NOT_FOUND`
///
/// No resource was found for this request.
NotFound,
/// `M_NOT_JSON`
///
/// The request did not contain valid JSON.
NotJson,
/// `M_NOT_YET_UPLOADED`
///
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
/// and the content is not yet available.
///
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
NotYetUploaded,
/// `M_RESOURCE_LIMIT_EXCEEDED`
///
/// The request cannot be completed because the homeserver has reached a
/// resource limit imposed on it. For example, a homeserver held in a
/// shared hosting environment may reach a resource limit if it starts
/// using too much memory or disk space.
ResourceLimitExceeded {
/// A URI giving a contact method for the server administrator.
admin_contact: String,
},
/// `M_ROOM_IN_USE`
///
/// The [room alias] specified in the [`create_room`] request is already
/// taken.
///
/// [`create_room`]: crate::room::create_room
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
RoomInUse,
/// `M_SERVER_NOT_TRUSTED`
///
/// The client's request used a third-party server, e.g. identity server,
/// that this server does not trust.
ServerNotTrusted,
/// `M_THREEPID_AUTH_FAILED`
///
/// Authentication could not be performed on the [third-party identifier].
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidAuthFailed,
/// `M_THREEPID_DENIED`
///
/// The server does not permit this [third-party identifier]. This may
/// happen if the server only permits, for example, email addresses from
/// a particular domain.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidDenied,
/// `M_THREEPID_IN_USE`
///
/// The [third-party identifier] is already in use by another user.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidInUse,
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
///
/// The homeserver does not support adding a [third-party identifier] of the
/// given medium.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidMediumNotSupported,
/// `M_THREEPID_NOT_FOUND`
///
/// No account matching the given [third-party identifier] could be found.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidNotFound,
/// `M_TOO_LARGE`
///
/// The request or entity was too large.
TooLarge,
/// `M_UNABLE_TO_AUTHORISE_JOIN`
///
/// The room is [restricted] and none of the conditions can be validated by
/// the homeserver. This can happen if the homeserver does not know
/// about any of the rooms listed as conditions, for example.
///
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
UnableToAuthorizeJoin,
/// `M_UNABLE_TO_GRANT_JOIN`
///
/// A different server should be attempted for the join. This is typically
/// because the resident server can see that the joining user satisfies
/// one or more conditions, such as in the case of [restricted rooms],
/// but the resident server would be unable to meet the authorization
/// rules.
///
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
UnableToGrantJoin,
/// `M_UNAUTHORIZED`
///
/// The request was not correctly authorized. Usually due to login failures.
Unauthorized,
/// `M_UNKNOWN`
///
/// An unknown error has occurred.
Unknown,
/// `M_UNKNOWN_TOKEN`
///
/// The [access or refresh token] specified was not recognized.
///
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
UnknownToken {
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
/// the server requires re-authentication but the session is not
/// invalidated. The client can acquire a new access token by
/// specifying the device ID it is already using to the login API.
///
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
soft_logout: bool,
},
/// `M_UNRECOGNIZED`
///
/// The server did not understand the request.
///
/// This is expected to be returned with a 404 HTTP status code if the
/// endpoint is not implemented or a 405 HTTP status code if the
/// endpoint is implemented, but the incorrect HTTP method is used.
Unrecognized,
/// `M_UNSUPPORTED_ROOM_VERSION`
///
/// The request to [`create_room`] used a room version that the server does
/// not support.
///
/// [`create_room`]: crate::room::create_room
UnsupportedRoomVersion,
/// `M_URL_NOT_SET`
///
/// The application service doesn't have a URL configured.
UrlNotSet,
/// `M_USER_DEACTIVATED`
///
/// The user ID associated with the request has been deactivated.
UserDeactivated,
/// `M_USER_IN_USE`
///
/// The desired user ID is already taken.
UserInUse,
/// `M_USER_LOCKED`
///
/// The account has been [locked] and cannot be used at this time.
///
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
UserLocked,
/// `M_USER_SUSPENDED`
///
/// The account has been [suspended] and can only be used for limited
/// actions at this time.
///
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
UserSuspended,
/// `M_WEAK_PASSWORD`
///
/// The password was [rejected] by the server for being too weak.
///
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
WeakPassword,
/// `M_WRONG_ROOM_KEYS_VERSION`
///
/// The version of the [room keys backup] provided in the request does not
/// match the current backup version.
///
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
WrongRoomKeysVersion {
/// The currently active backup version.
current_version: Option<String>,
},
/// A custom API error.
Custom { errcode: String },
}
impl TryFrom<RumaApiErrorKind> for ErrorKind {
type Error = NotYetImplemented;
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
match &value {
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
status: status.map(|code| code.clone().as_u16()),
body: body.clone(),
}),
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
}
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
}
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
RumaApiErrorKind::LimitExceeded { retry_after } => {
let retry_after_ms = match retry_after {
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
Some(RetryAfter::DateTime(system_time)) => {
let duration = system_time.duration_since(SystemTime::now()).ok();
duration.map(|duration| duration.as_millis() as u64)
}
None => None,
};
Ok(ErrorKind::LimitExceeded { retry_after_ms })
}
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
}
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
RumaApiErrorKind::ThreepidMediumNotSupported => {
Ok(ErrorKind::ThreepidMediumNotSupported)
}
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
RumaApiErrorKind::UnknownToken { soft_logout } => {
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
}
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
}
RumaApiErrorKind::_Custom { .. } => {
// There is no way to map the extra values since they're private, so we omit
// them
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
}
// In any other case, return it as the mapping not being yet implemented
_ => Err(NotYetImplemented),
}
}
}
+87 -10
View File
@@ -1,21 +1,30 @@
use anyhow::{bail, Context};
use ruma::events::{
room::{message::Relation, redaction::SyncRoomRedactionEvent},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
events::{
room::{
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
},
EventId,
};
use crate::{
room_member::MembershipState,
ruma::{MessageType, NotifyType},
utils::Timestamp,
ClientError,
};
#[derive(uniffi::Object)]
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineEvent {
pub fn event_id(&self) -> String {
self.0.event_id().to_string()
@@ -25,8 +34,8 @@ impl TimelineEvent {
self.0.sender().to_string()
}
pub fn timestamp(&self) -> u64 {
self.0.origin_server_ts().0.into()
pub fn timestamp(&self) -> Timestamp {
self.0.origin_server_ts().into()
}
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
@@ -100,7 +109,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
let original_content = get_state_event_original_content(content)?;
StateEventContent::RoomMemberContent {
user_id: state_key,
membership_state: original_content.membership.into(),
membership_state: original_content.membership.try_into()?,
}
}
AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName,
@@ -197,7 +206,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
_ => None,
});
MessageLikeEventContent::RoomMessage {
message_type: original_content.msgtype.into(),
message_type: original_content.msgtype.try_into()?,
in_reply_to_event_id,
}
}
@@ -350,3 +359,71 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
}
}
}
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
pub enum RoomMessageEventMessageType {
Audio,
Emote,
File,
Image,
Location,
Notice,
ServerNotice,
Text,
Video,
VerificationRequest,
Other,
}
impl From<RumaMessageType> for RoomMessageEventMessageType {
fn from(val: ruma::events::room::message::MessageType) -> Self {
match val {
RumaMessageType::Audio { .. } => Self::Audio,
RumaMessageType::Emote { .. } => Self::Emote,
RumaMessageType::File { .. } => Self::File,
RumaMessageType::Image { .. } => Self::Image,
RumaMessageType::Location { .. } => Self::Location,
RumaMessageType::Notice { .. } => Self::Notice,
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
RumaMessageType::Text { .. } => Self::Text,
RumaMessageType::Video { .. } => Self::Video,
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
_ => Self::Other,
}
}
}
/// Contains the 2 possible identifiers of an event, either it has a remote
/// event id or a local transaction id, never both or none.
#[derive(Clone, uniffi::Enum)]
pub enum EventOrTransactionId {
EventId { event_id: String },
TransactionId { transaction_id: String },
}
impl From<TimelineEventItemId> for EventOrTransactionId {
fn from(value: TimelineEventItemId) -> Self {
match value {
TimelineEventItemId::EventId(event_id) => {
EventOrTransactionId::EventId { event_id: event_id.to_string() }
}
TimelineEventItemId::TransactionId(transaction_id) => {
EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() }
}
}
}
}
impl TryFrom<EventOrTransactionId> for TimelineEventItemId {
type Error = IdParseError;
fn try_from(value: EventOrTransactionId) -> Result<Self, Self::Error> {
match value {
EventOrTransactionId::EventId { event_id } => {
Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?))
}
EventOrTransactionId::TransactionId { transaction_id } => {
Ok(TimelineEventItemId::TransactionId(transaction_id.into()))
}
}
}
}
@@ -0,0 +1,24 @@
// 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 matrix_sdk::crypto::IdentityState;
#[derive(uniffi::Record)]
pub struct IdentityStatusChange {
/// The user ID of the user whose identity status changed
pub user_id: String,
/// The new state of the identity of the user.
pub changed_to: IdentityState,
}
+8 -23
View File
@@ -1,24 +1,8 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
macro_rules! unwrap_or_clone_arc_into_variant {
(
$arc:ident $(, .$field:tt)?, $pat:pat => $body:expr
) => {
#[allow(unused_variables)]
match &(*$arc)$(.$field)? {
$pat => {
#[warn(unused_variables)]
match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? {
$pat => Some($body),
_ => unreachable!(),
}
},
_ => None,
}
};
}
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
// lines after docs.
mod authentication;
mod chunk_iterator;
@@ -29,10 +13,13 @@ mod encryption;
mod error;
mod event;
mod helpers;
mod identity_status_change;
mod live_location_share;
mod notification;
mod notification_settings;
mod platform;
mod room;
mod room_alias;
mod room_directory_search;
mod room_info;
mod room_list;
@@ -49,19 +36,17 @@ mod utils;
mod widget;
use async_compat::TOKIO1 as RUNTIME;
use matrix_sdk::ruma::events::room::{
message::RoomMessageEventContentWithoutRelation, MediaSource,
};
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
use self::{
error::ClientError,
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
task_handle::TaskHandle,
};
uniffi::include_scaffolding!("api");
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn sdk_git_sha() -> String {
env!("VERGEN_GIT_SHA").to_owned()
}
@@ -0,0 +1,32 @@
// 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 crate::ruma::LocationContent;
#[derive(uniffi::Record)]
pub struct LastLocation {
/// The most recent location content of the user.
pub location: LocationContent,
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
pub ts: u64,
}
/// Details of a users live location share.
#[derive(uniffi::Record)]
pub struct LiveLocationShare {
/// The user's last known location.
pub last_location: LastLocation,
/// The live status of the live location share.
pub(crate) is_live: bool,
/// The user ID of the person sharing their live location.
pub user_id: String,
}
+1 -1
View File
@@ -88,7 +88,7 @@ pub struct NotificationClient {
pub(crate) _client: Arc<Client>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl NotificationClient {
/// See also documentation of
/// `MatrixNotificationClient::get_notification`.
@@ -49,7 +49,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
}
/// Delegate to notify of changes in push rules
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait NotificationSettingsDelegate: Sync + Send {
fn settings_did_change(&self);
}
@@ -98,7 +98,7 @@ impl Drop for NotificationSettings {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl NotificationSettings {
pub fn set_delegate(&self, delegate: Option<Box<dyn NotificationSettingsDelegate>>) {
if let Some(delegate) = delegate {
+172 -6
View File
@@ -14,8 +14,11 @@ use tracing_subscriber::{
EnvFilter, Layer,
};
use crate::tracing::LogLevel;
pub fn log_panics() {
std::env::set_var("RUST_BACKTRACE", "1");
log_panics::init();
}
@@ -228,12 +231,76 @@ pub struct TracingFileConfiguration {
max_files: Option<u64>,
}
#[derive(PartialEq, PartialOrd)]
enum LogTarget {
Hyper,
MatrixSdkFfi,
MatrixSdk,
MatrixSdkClient,
MatrixSdkCrypto,
MatrixSdkCryptoAccount,
MatrixSdkOidc,
MatrixSdkHttpClient,
MatrixSdkSlidingSync,
MatrixSdkBaseSlidingSync,
MatrixSdkUiTimeline,
MatrixSdkEventCache,
MatrixSdkBaseEventCache,
MatrixSdkEventCacheStore,
}
impl LogTarget {
fn as_str(&self) -> &'static str {
match self {
LogTarget::Hyper => "hyper",
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
LogTarget::MatrixSdk => "matrix_sdk",
LogTarget::MatrixSdkClient => "matrix_sdk::client",
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
}
}
}
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::Hyper, LogLevel::Warn),
(LogTarget::MatrixSdkFfi, LogLevel::Info),
(LogTarget::MatrixSdk, LogLevel::Info),
(LogTarget::MatrixSdkClient, LogLevel::Trace),
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
];
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
];
#[derive(uniffi::Record)]
pub struct TracingConfiguration {
/// A filter line following the [RUST_LOG format].
///
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
filter: String,
/// The desired log level
log_level: LogLevel,
/// Additional targets that the FFI client would like to use e.g.
/// the target names for created [`crate::tracing::Span`]
extra_targets: Option<Vec<String>>,
/// Whether to log to stdout, or in the logcat on Android.
write_to_stdout_or_system: bool,
@@ -242,12 +309,111 @@ pub struct TracingConfiguration {
write_to_files: Option<TracingFileConfiguration>,
}
#[uniffi::export]
fn build_tracing_filter(config: &TracingConfiguration) -> String {
// We are intentionally not setting a global log level because we don't want to
// risk third party crates logging sensitive information.
// As such we need to make sure that panics will be properly logged.
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
let mut filters = vec!["panic=error".to_owned()];
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
// Use the default if the log level shouldn't be changed for this target or
// if it's already logging more than requested
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
level.as_str()
} else {
config.log_level.as_str()
};
filters.push(format!("{}={}", target.as_str(), level));
});
// Finally append the extra targets requested by the client
if let Some(extra_targets) = &config.extra_targets {
for target in extra_targets {
filters.push(format!("{}={}", target, config.log_level.as_str()));
}
}
filters.join(",")
}
#[matrix_sdk_ffi_macros::export]
pub fn setup_tracing(config: TracingConfiguration) {
log_panics();
tracing_subscriber::registry()
.with(EnvFilter::new(&config.filter))
.with(EnvFilter::new(build_tracing_filter(&config)))
.with(text_layers(config))
.init();
}
#[cfg(test)]
mod tests {
use super::build_tracing_filter;
#[test]
fn test_default_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Error,
extra_targets: Some(vec!["super_duper_app".to_owned()]),
write_to_stdout_or_system: true,
write_to_files: None,
};
let filter = build_tracing_filter(&config);
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=debug,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=debug,\
matrix_sdk::sliding_sync=info,\
matrix_sdk_base::sliding_sync=info,\
matrix_sdk_ui::timeline=info,\
matrix_sdk::event_cache=info,\
matrix_sdk_base::event_cache=info,\
matrix_sdk_sqlite::event_cache_store=info,\
super_duper_app=error"
);
}
#[test]
fn test_trace_tracing_filter() {
let config = super::TracingConfiguration {
log_level: super::LogLevel::Trace,
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
write_to_stdout_or_system: true,
write_to_files: None,
};
let filter = build_tracing_filter(&config);
assert_eq!(
filter,
"panic=error,\
hyper=warn,\
matrix_sdk_ffi=info,\
matrix_sdk=info,\
matrix_sdk::client=trace,\
matrix_sdk_crypto=trace,\
matrix_sdk_crypto::olm::account=trace,\
matrix_sdk::oidc=trace,\
matrix_sdk::http_client=trace,\
matrix_sdk::sliding_sync=trace,\
matrix_sdk_base::sliding_sync=trace,\
matrix_sdk_ui::timeline=trace,\
matrix_sdk::event_cache=trace,\
matrix_sdk_base::event_cache=trace,\
matrix_sdk_sqlite::event_cache_store=trace,\
super_duper_app=trace,\
some_other_span=trace"
);
}
}
+471 -107
View File
@@ -1,16 +1,16 @@
use std::{collections::HashMap, sync::Arc};
use std::{collections::HashMap, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
event_cache::paginator::PaginatorError,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
};
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
use mime::Mime;
use ruma::{
api::client::room::report_content,
@@ -19,35 +19,43 @@ use ruma::{
call::notify,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
message::RoomMessageEventContentWithoutRelation,
history_visibility::HistoryVisibility as RumaHistoryVisibility,
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
},
TimelineEventType,
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
},
EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId,
UserId,
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
};
use tokio::sync::RwLock;
use tracing::error;
use tracing::{error, warn};
use super::RUNTIME;
use crate::{
chunk_iterator::ChunkIterator,
error::{ClientError, MediaInfoError, RoomError},
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
event::{MessageLikeEventType, StateEventType},
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
room_info::RoomInfo,
room_member::RoomMember,
ruma::{ImageInfo, Mentions, NotifyType},
timeline::{FocusEventError, ReceiptType, Timeline},
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
timeline::{
configuration::{AllowedMessageTypes, TimelineConfiguration},
ReceiptType, SendHandle, Timeline,
},
utils::u64_to_uint,
TaskHandle,
};
#[derive(Debug, uniffi::Enum)]
#[derive(Debug, Clone, uniffi::Enum)]
pub enum Membership {
Invited,
Joined,
Left,
Knocked,
Banned,
}
impl From<RoomState> for Membership {
@@ -56,6 +64,8 @@ impl From<RoomState> for Membership {
RoomState::Invited => Membership::Invited,
RoomState::Joined => Membership::Joined,
RoomState::Left => Membership::Left,
RoomState::Knocked => Membership::Knocked,
RoomState::Banned => Membership::Banned,
}
}
}
@@ -78,12 +88,8 @@ impl Room {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl Room {
pub fn id(&self) -> String {
self.inner.room_id().to_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.
@@ -159,7 +165,12 @@ impl Room {
/// the user who invited the logged-in user to a room.
pub async fn inviter(&self) -> Option<RoomMember> {
if self.inner.state() == RoomState::Invited {
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
self.inner
.invite_details()
.await
.ok()
.and_then(|a| a.inviter)
.and_then(|m| m.try_into().ok())
} else {
None
}
@@ -188,68 +199,42 @@ impl Room {
}
}
/// 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(
/// Build a new timeline instance with the given configuration.
pub async fn timeline_with_configuration(
&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() }
})?;
configuration: TimelineConfiguration,
) -> Result<Arc<Timeline>, ClientError> {
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
let room = &self.inner;
builder = builder.with_focus(configuration.focus.try_into()?);
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
builder = builder.event_filter(move |event, room_version_id| {
default_event_filter(event, room_version_id)
&& match event {
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
types.contains(&content.msgtype.into())
}
_ => false,
},
_ => false,
}
});
}
if let Some(internal_id_prefix) = internal_id_prefix {
if let Some(internal_id_prefix) = configuration.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() });
}
};
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
let timeline = builder.build().await?;
Ok(Timeline::new(timeline))
}
pub async fn pinned_events_timeline(
&self,
internal_id_prefix: Option<String>,
max_events_to_load: u16,
max_concurrent_requests: u16,
) -> Result<Arc<Timeline>, ClientError> {
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 = builder
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
.build()
.await?;
Ok(Timeline::new(timeline))
pub fn id(&self) -> String {
self.inner.room_id().to_string()
}
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
@@ -269,7 +254,7 @@ impl Room {
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
Ok(member.into())
Ok(member.try_into().context("Unknown state membership")?)
}
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
@@ -290,7 +275,7 @@ impl Room {
}
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
Ok(RoomInfo::new(&self.inner).await?)
RoomInfo::new(&self.inner).await
}
pub fn subscribe_to_room_info_updates(
@@ -328,6 +313,22 @@ impl Room {
Ok(())
}
/// Send a raw event to the room.
///
/// # Arguments
///
/// * `event_type` - The type of the event to send.
///
/// * `content` - The content of the event to send encoded as JSON string.
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
let content_json: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
self.inner.send_raw(&event_type, content_json).await?;
Ok(())
}
/// Redacts an event from the room.
///
/// # Arguments
@@ -378,15 +379,12 @@ impl Room {
let int_score = score.map(|value| value.into());
self.inner
.client()
.send(
report_content::v3::Request::new(
self.inner.room_id().into(),
event_id,
int_score,
reason,
),
None,
)
.send(report_content::v3::Request::new(
self.inner.room_id().into(),
event_id,
int_score,
reason,
))
.await?;
Ok(())
}
@@ -582,6 +580,31 @@ impl Room {
})))
}
pub fn subscribe_to_identity_status_changes(
&self,
listener: Box<dyn IdentityStatusChangeListener>,
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let status_changes = room.subscribe_to_identity_status_changes().await;
if let Ok(status_changes) = status_changes {
// TODO: what to do with failures?
let mut status_changes = pin!(status_changes);
while let Some(identity_status_changes) = status_changes.next().await {
listener.call(
identity_status_changes
.into_iter()
.map(|change| {
let user_id = change.user_id.to_string();
IdentityStatusChange { user_id, changed_to: change.changed_to }
})
.collect(),
);
}
}
})))
}
/// 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> {
@@ -600,7 +623,7 @@ impl Room {
}
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
let power_levels = self.inner.room_power_levels().await?;
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
Ok(RoomPowerLevels::from(power_levels))
}
@@ -756,10 +779,8 @@ impl Room {
pub async fn withdraw_verification_and_resend(
&self,
user_ids: Vec<String>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let user_ids: Vec<OwnedUserId> =
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
@@ -771,7 +792,7 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
@@ -789,10 +810,8 @@ impl Room {
pub async fn ignore_device_trust_and_resend(
&self,
devices: HashMap<String, Vec<String>>,
transaction_id: String,
send_handle: Arc<SendHandle>,
) -> Result<(), ClientError> {
let transaction_id: OwnedTransactionId = transaction_id.into();
let encryption = self.inner.client().encryption();
for (user_id, device_ids) in devices.iter() {
@@ -807,32 +826,313 @@ impl Room {
}
}
self.inner.send_queue().unwedge(&transaction_id).await?;
send_handle.try_resend().await?;
Ok(())
}
/// Attempt to manually resend messages that failed to send due to issues
/// that should now have been fixed.
/// Clear the event cache storage for the current room.
///
/// This is useful for example, when there's a
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
/// the user may have re-verified on a different device and would now
/// like to send the failed message that's waiting on this device.
/// This will remove all the information related to the event cache, in
/// memory and in the persisted storage, if enabled.
pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> {
let (room_event_cache, _drop_handles) = self.inner.event_cache().await?;
room_event_cache.clear().await?;
Ok(())
}
/// Subscribes to requests to join this room (knock member events), using a
/// `listener` to be notified of the changes.
///
/// # Arguments
/// The current requests to join the room will be emitted immediately
/// when subscribing, along with a [`TaskHandle`] to cancel the
/// subscription.
pub async fn subscribe_to_knock_requests(
self: Arc<Self>,
listener: Box<dyn KnockRequestsListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
pin_mut!(stream);
while let Some(requests) = stream.next().await {
listener.call(requests.into_iter().map(Into::into).collect());
}
// Cancel the seen ids cleanup task
seen_ids_cleanup_handle.abort();
})));
Ok(handle)
}
/// Return a debug representation for the internal room events data
/// structure, one line per entry in the resulting vector.
pub async fn room_events_debug_string(&self) -> Result<Vec<String>, ClientError> {
let (cache, _drop_guards) = self.inner.event_cache().await?;
Ok(cache.debug_string().await)
}
/// Update the canonical alias of the room.
///
/// * `transaction_id` - The send queue transaction identifier of the local
/// echo that should be unwedged.
pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> {
let transaction_id: &TransactionId = transaction_id.as_str().into();
self.inner.send_queue().unwedge(transaction_id).await?;
/// Note that publishing the alias in the room directory is done separately.
pub async fn update_canonical_alias(
&self,
alias: Option<String>,
alt_aliases: Vec<String>,
) -> Result<(), ClientError> {
let new_alias = alias.map(TryInto::try_into).transpose()?;
let new_alt_aliases =
alt_aliases.into_iter().map(RoomAliasId::parse).collect::<Result<_, _>>()?;
self.inner
.privacy_settings()
.update_canonical_alias(new_alias, new_alt_aliases)
.await
.map_err(Into::into)
}
/// Publish a new room alias for this room in the room directory.
///
/// Returns:
/// - `true` if the room alias didn't exist and it's now published.
/// - `false` if the room alias was already present so it couldn't be
/// published.
pub async fn publish_room_alias_in_room_directory(
&self,
alias: String,
) -> Result<bool, ClientError> {
let new_alias = RoomAliasId::parse(alias)?;
self.inner
.privacy_settings()
.publish_room_alias_in_room_directory(&new_alias)
.await
.map_err(Into::into)
}
/// Remove an existing room alias for this room in the room directory.
///
/// Returns:
/// - `true` if the room alias was present and it's now removed from the
/// room directory.
/// - `false` if the room alias didn't exist so it couldn't be removed.
pub async fn remove_room_alias_from_room_directory(
&self,
alias: String,
) -> Result<bool, ClientError> {
let alias = RoomAliasId::parse(alias)?;
self.inner
.privacy_settings()
.remove_room_alias_from_room_directory(&alias)
.await
.map_err(Into::into)
}
/// Enable End-to-end encryption in this room.
pub async fn enable_encryption(&self) -> Result<(), ClientError> {
self.inner.enable_encryption().await.map_err(Into::into)
}
/// Update room history visibility for this room.
pub async fn update_history_visibility(
&self,
visibility: RoomHistoryVisibility,
) -> Result<(), ClientError> {
let visibility: RumaHistoryVisibility = visibility.try_into()?;
self.inner
.privacy_settings()
.update_room_history_visibility(visibility)
.await
.map_err(Into::into)
}
/// Update the join rule for this room.
pub async fn update_join_rules(&self, new_rule: JoinRule) -> Result<(), ClientError> {
let new_rule: RumaJoinRule = new_rule.try_into()?;
self.inner.privacy_settings().update_join_rule(new_rule).await.map_err(Into::into)
}
/// Update the room's visibility in the room directory.
pub async fn update_room_visibility(
&self,
visibility: RoomVisibility,
) -> Result<(), ClientError> {
self.inner
.privacy_settings()
.update_room_visibility(visibility.into())
.await
.map_err(Into::into)
}
/// Returns the visibility for this room in the room directory.
///
/// [Public](`RoomVisibility::Public`) rooms are listed in the room
/// directory and can be found using it.
pub async fn get_room_visibility(&self) -> Result<RoomVisibility, ClientError> {
let visibility = self.inner.privacy_settings().get_room_visibility().await?;
Ok(visibility.into())
}
/// Start the current users live location share in the room.
pub async fn start_live_location_share(&self, duration_millis: u64) -> Result<(), ClientError> {
self.inner.start_live_location_share(duration_millis, None).await?;
Ok(())
}
/// Stop the current users live location share in the room.
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
Ok(())
}
/// Send the current users live location beacon in the room.
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
self.inner
.send_location_beacon(geo_uri)
.await
.expect("Unable to send live location beacon");
Ok(())
}
/// Subscribes to live location shares in this room, using a `listener` to
/// be notified of the changes.
///
/// The current live location shares will be emitted immediately when
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
pub fn subscribe_to_live_location_shares(
self: Arc<Self>,
listener: Box<dyn LiveLocationShareListener>,
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let subscription = room.observe_live_location_shares();
let mut stream = subscription.subscribe();
let mut pinned_stream = pin!(stream);
while let Some(event) = pinned_stream.next().await {
let last_location = LocationContent {
body: "".to_owned(),
geo_uri: event.last_location.location.uri.clone().to_string(),
description: None,
zoom_level: None,
asset: None,
};
let Some(beacon_info) = event.beacon_info else {
warn!("Live location share is missing the associated beacon_info state, skipping event.");
continue;
};
listener.call(vec![LiveLocationShare {
last_location: LastLocation {
location: last_location,
ts: event.last_location.ts.0.into(),
},
is_live: beacon_info.is_live(),
user_id: event.user_id.to_string(),
}])
}
})))
}
/// Forget this room.
///
/// This communicates to the homeserver that it should forget the room.
///
/// Only left or banned-from rooms can be forgotten.
pub async fn forget(&self) -> Result<(), ClientError> {
self.inner.forget().await?;
Ok(())
}
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: Sync + Send {
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
}
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
Self {
event_id: request.event_id.to_string(),
user_id: request.member_info.user_id.to_string(),
room_id: request.room_id().to_string(),
display_name: request.member_info.display_name.clone(),
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
reason: request.member_info.reason.clone(),
timestamp: request.timestamp.map(|ts| ts.into()),
is_seen: request.is_seen,
actions: Arc::new(KnockRequestActions { inner: request }),
}
}
}
/// A listener for receiving new requests to a join a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait KnockRequestsListener: Send + Sync {
fn call(&self, join_requests: Vec<KnockRequest>);
}
/// An FFI representation of a request to join a room.
#[derive(Debug, Clone, uniffi::Record)]
pub struct KnockRequest {
/// The event id of the event that contains the `knock` membership change.
pub event_id: String,
/// The user id of the user who's requesting to join the room.
pub user_id: String,
/// The room id of the room whose access was requested.
pub room_id: String,
/// The optional display name of the user who's requesting to join the room.
pub display_name: Option<String>,
/// The optional avatar url of the user who's requesting to join the room.
pub avatar_url: Option<String>,
/// An optional reason why the user wants join the room.
pub reason: Option<String>,
/// The timestamp when this request was created.
pub timestamp: Option<u64>,
/// Whether the knock request has been marked as `seen` so it can be
/// filtered by the client.
pub is_seen: bool,
/// A set of actions to perform for this knock request.
pub actions: Arc<KnockRequestActions>,
}
/// A set of actions to perform for a knock request.
#[derive(Debug, Clone, uniffi::Object)]
pub struct KnockRequestActions {
inner: matrix_sdk::room::knock_requests::KnockRequest,
}
#[matrix_sdk_ffi_macros::export]
impl KnockRequestActions {
/// Accepts the knock request by inviting the user to the room.
pub async fn accept(&self) -> Result<(), ClientError> {
self.inner.accept().await.map_err(Into::into)
}
/// Declines the knock request by kicking the user from the room with an
/// optional reason.
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
}
/// Declines the knock request by banning the user from the room with an
/// optional reason.
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
}
/// Marks the knock request as 'seen'.
///
/// **IMPORTANT**: this won't update the current reference to this request,
/// a new one with the updated value should be emitted instead.
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
self.inner.mark_as_seen().await.map_err(Into::into)
}
}
/// Generates a `matrix.to` permalink to the given room alias.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn matrix_to_room_alias_permalink(
room_alias: String,
) -> std::result::Result<String, ClientError> {
@@ -888,16 +1188,21 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomInfoListener: Sync + Send {
fn call(&self, room_info: RoomInfo);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait TypingNotificationsListener: Sync + Send {
fn call(&self, typing_user_ids: Vec<String>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait IdentityStatusChangeListener: Sync + Send {
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
}
#[derive(uniffi::Object)]
pub struct RoomMembersIterator {
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
@@ -909,7 +1214,7 @@ impl RoomMembersIterator {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomMembersIterator {
fn len(&self) -> u32 {
self.chunk_iterator.len()
@@ -918,7 +1223,7 @@ impl RoomMembersIterator {
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
self.chunk_iterator
.next(chunk_size)
.map(|members| members.into_iter().map(|m| m.into()).collect())
.map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect())
}
}
@@ -957,7 +1262,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
match media_source.as_ref() {
match &media_source.as_ref().media_source {
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
}
@@ -1057,3 +1362,62 @@ impl TryFrom<ComposerDraftType> for SdkComposerDraftType {
Ok(draft_type)
}
}
#[derive(Debug, Clone, uniffi::Enum)]
pub enum RoomHistoryVisibility {
/// Previous events are accessible to newly joined members from the point
/// they were invited onwards.
///
/// Events stop being accessible when the member's state changes to
/// something other than *invite* or *join*.
Invited,
/// Previous events are accessible to newly joined members from the point
/// they joined the room onwards.
/// Events stop being accessible when the member's state changes to
/// something other than *join*.
Joined,
/// Previous events are always accessible to newly joined members.
///
/// All events in the room are accessible, even those sent when the member
/// was not a part of the room.
Shared,
/// All events while this is the `HistoryVisibility` value may be shared by
/// any participating homeserver with anyone, regardless of whether they
/// have ever joined the room.
WorldReadable,
/// A custom visibility value.
Custom { value: String },
}
impl TryFrom<RumaHistoryVisibility> for RoomHistoryVisibility {
type Error = NotYetImplemented;
fn try_from(value: RumaHistoryVisibility) -> Result<Self, Self::Error> {
match value {
RumaHistoryVisibility::Invited => Ok(RoomHistoryVisibility::Invited),
RumaHistoryVisibility::Shared => Ok(RoomHistoryVisibility::Shared),
RumaHistoryVisibility::WorldReadable => Ok(RoomHistoryVisibility::WorldReadable),
RumaHistoryVisibility::Joined => Ok(RoomHistoryVisibility::Joined),
RumaHistoryVisibility::_Custom(_) => {
Ok(RoomHistoryVisibility::Custom { value: value.to_string() })
}
_ => Err(NotYetImplemented),
}
}
}
impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
type Error = NotYetImplemented;
fn try_from(value: RoomHistoryVisibility) -> Result<Self, Self::Error> {
match value {
RoomHistoryVisibility::Invited => Ok(RumaHistoryVisibility::Invited),
RoomHistoryVisibility::Shared => Ok(RumaHistoryVisibility::Shared),
RoomHistoryVisibility::Joined => Ok(RumaHistoryVisibility::Joined),
RoomHistoryVisibility::WorldReadable => Ok(RumaHistoryVisibility::WorldReadable),
RoomHistoryVisibility::Custom { .. } => Err(NotYetImplemented),
}
}
}
+17
View File
@@ -0,0 +1,17 @@
use matrix_sdk::RoomDisplayName;
/// Verifies the passed `String` matches the expected room alias format:
///
/// This means it's lowercase, with no whitespace chars, has a single leading
/// `#` char and a single `:` separator between the local and domain parts, and
/// the local part only contains characters that can't be percent encoded.
#[matrix_sdk_ffi_macros::export]
fn is_room_alias_format_valid(alias: String) -> bool {
matrix_sdk::utils::is_room_alias_format_valid(alias)
}
/// Transforms a Room's display name into a valid room alias name.
#[matrix_sdk_ffi_macros::export]
fn room_alias_name_from_room_display_name(room_name: String) -> String {
RoomDisplayName::Named(room_name).to_room_alias_name()
}
@@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
use ruma::ServerName;
use tokio::sync::RwLock;
use super::RUNTIME;
@@ -68,6 +69,12 @@ impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescriptio
}
}
/// A helper for performing room searches in the room directory.
/// The way this is intended to be used is:
///
/// 1. Register a callback using [`RoomDirectorySearch::results`].
/// 2. Start the room search with [`RoomDirectorySearch::search`].
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
#[derive(uniffi::Object)]
pub struct RoomDirectorySearch {
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
@@ -79,30 +86,51 @@ impl RoomDirectorySearch {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomDirectorySearch {
/// Asks the server for the next page of the current search.
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> {
/// Starts a filtered search for the server.
///
/// If the `filter` is not provided it will search for all the rooms.
/// You can specify a `batch_size` to control the number of rooms to fetch
/// per request.
///
/// If the `via_server` is not provided it will search in the current
/// homeserver by default.
///
/// This method will clear the current search results and start a new one.
pub async fn search(
&self,
filter: Option<String>,
batch_size: u32,
via_server_name: Option<String>,
) -> Result<(), ClientError> {
let server = via_server_name.map(ServerName::parse).transpose()?;
let mut inner = self.inner.write().await;
inner.search(filter, batch_size).await?;
inner.search(filter, batch_size, server).await?;
Ok(())
}
/// Get the number of pages that have been loaded so far.
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
let inner = self.inner.read().await;
Ok(inner.loaded_pages() as u32)
}
/// Get whether the search is at the last page.
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
let inner = self.inner.read().await;
Ok(inner.is_at_last_page())
}
/// Registers a callback to receive new search results when starting a
/// search or getting new paginated results.
pub async fn results(
&self,
listener: Box<dyn RoomDirectorySearchEntriesListener>,
@@ -169,7 +197,7 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
}
+23 -5
View File
@@ -1,10 +1,13 @@
use std::collections::HashMap;
use matrix_sdk::RoomState;
use tracing::warn;
use crate::{
client::JoinRule,
error::ClientError,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero},
room::{Membership, RoomHero, RoomHistoryVisibility},
room_member::RoomMember,
};
@@ -54,12 +57,16 @@ pub struct RoomInfo {
/// Events causing mentions/highlights for the user, according to their
/// notification settings.
num_unread_mentions: u64,
/// The currently pinned event ids
/// The currently pinned event ids.
pinned_event_ids: Vec<String>,
/// The join rule for this room, if known.
join_rule: Option<JoinRule>,
/// The history visibility for this room, if known.
history_visibility: RoomHistoryVisibility,
}
impl RoomInfo {
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
let unread_notification_counts = room.unread_notification_counts();
let power_levels_map = room.users_with_power_levels().await;
@@ -67,7 +74,13 @@ impl RoomInfo {
for (id, level) in power_levels_map.iter() {
user_power_levels.insert(id.to_string(), *level);
}
let pinned_event_ids = room.pinned_event_ids().iter().map(|id| id.to_string()).collect();
let pinned_event_ids =
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
let join_rule = room.join_rule().try_into();
if let Err(e) = &join_rule {
warn!("Failed to parse join rule: {:?}", e);
}
Ok(Self {
id: room.room_id().to_string(),
@@ -90,7 +103,10 @@ impl RoomInfo {
.await
.ok()
.and_then(|details| details.inviter)
.map(Into::into),
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
_ => None,
},
heroes: room.heroes().into_iter().map(Into::into).collect(),
@@ -114,6 +130,8 @@ impl RoomInfo {
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
join_rule: join_rule.ok(),
history_visibility: room.history_visibility_or_default().try_into()?,
})
}
}
+79 -92
View File
@@ -1,13 +1,12 @@
#![allow(deprecated)]
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt, TryFutureExt};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
assign, RoomId,
},
sliding_sync::http,
use matrix_sdk::ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
};
use matrix_sdk_ui::{
room_list_service::filters::{
@@ -19,14 +18,17 @@ use matrix_sdk_ui::{
timeline::default_event_filter,
unable_to_decrypt_hook::UtdHookManager,
};
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
use tokio::sync::RwLock;
use crate::{
error::ClientError,
room::{Membership, Room},
room_info::RoomInfo,
room_preview::RoomPreview,
timeline::{EventTimelineItem, Timeline},
timeline_event_filter::TimelineEventTypeFilter,
utils::AsyncRuntimeDropped,
TaskHandle, RUNTIME,
};
@@ -51,7 +53,7 @@ pub enum RoomListError {
#[error("Event cache ran into an error: {error}")]
EventCache { error: String },
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
IncorrectRoomMembership { expected: Membership, actual: Membership },
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
}
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
@@ -85,7 +87,7 @@ pub struct RoomListService {
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomListService {
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
let state_stream = self.inner.state();
@@ -135,11 +137,7 @@ impl RoomListService {
})))
}
fn subscribe_to_rooms(
&self,
room_ids: Vec<String>,
settings: Option<RoomSubscription>,
) -> Result<(), RoomListError> {
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
let room_ids = room_ids
.into_iter()
.map(|room_id| {
@@ -147,10 +145,7 @@ impl RoomListService {
})
.collect::<Result<Vec<_>, _>>()?;
self.inner.subscribe_to_rooms(
&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
settings.map(Into::into),
);
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
Ok(())
}
@@ -162,7 +157,7 @@ pub struct RoomList {
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomList {
fn loading_state(
&self,
@@ -182,40 +177,12 @@ impl RoomList {
})
}
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> Arc<TaskHandle> {
let this = self.inner.clone();
let utd_hook = self.room_list_service.utd_hook.clone();
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
let (entries, entries_stream) = this.entries();
pin_mut!(entries_stream);
listener.on_update(vec![RoomListEntriesUpdate::Append {
values: entries
.into_iter()
.map(|room| Arc::new(RoomListItem::from(room, utd_hook.clone())))
.collect(),
}]);
while let Some(diffs) = entries_stream.next().await {
listener.on_update(
diffs
.into_iter()
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
.collect(),
);
}
})))
}
fn entries_with_dynamic_adapters(
self: Arc<Self>,
page_size: u32,
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self.clone();
let client = self.room_list_service.inner.client();
let utd_hook = self.room_list_service.utd_hook.clone();
// The following code deserves a bit of explanation.
@@ -263,10 +230,7 @@ impl RoomList {
// borrowing `this`, which is going to live long enough since it will live as
// long as `entries_stream` and `dynamic_entries_controller`.
let (entries_stream, dynamic_entries_controller) =
this.inner.entries_with_dynamic_adapters(
page_size.try_into().unwrap(),
client.room_info_notable_update_receiver(),
);
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
// FFI dance to make those values consumable by foreign language, nothing fancy
// here, that's the real code for this method.
@@ -319,7 +283,7 @@ pub struct RoomListEntriesWithDynamicAdaptersResult {
entries_stream: Arc<TaskHandle>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomListEntriesWithDynamicAdaptersResult {
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
self.controller.clone()
@@ -397,17 +361,17 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListServiceState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
fn on_update(&self, state: RoomListLoadingState);
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
}
@@ -470,7 +434,7 @@ impl RoomListEntriesUpdate {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomListEntriesListener: Send + Sync + Debug {
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
}
@@ -488,7 +452,7 @@ impl RoomListDynamicEntriesController {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl RoomListDynamicEntriesController {
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
self.inner.set_filter(kind.into())
@@ -576,7 +540,7 @@ impl RoomListItem {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl RoomListItem {
fn id(&self) -> String {
self.inner.id().to_string()
@@ -602,7 +566,7 @@ impl RoomListItem {
}
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
Ok(RoomInfo::new(self.inner.inner_room()).await?)
RoomInfo::new(self.inner.inner_room()).await
}
/// The room's current membership state.
@@ -611,23 +575,71 @@ impl RoomListItem {
}
/// Builds a `Room` FFI from an invited room without initializing its
/// internal timeline
/// internal timeline.
///
/// An error will be returned if the room is a state different than invited
/// An error will be returned if the room is a state different than invited.
///
/// ⚠️ Holding on to this room instance after it has been joined is not
/// safe. Use `full_room` instead
/// safe. Use `full_room` instead.
#[deprecated(note = "Please use `preview_room` instead.")]
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Invited) {
return Err(RoomListError::IncorrectRoomMembership {
expected: Membership::Invited,
expected: vec![Membership::Invited],
actual: self.membership(),
});
}
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
}
/// Builds a `RoomPreview` from a room list item. This is intended for
/// invited or knocked rooms.
///
/// An error will be returned if the room is in a state other than invited
/// or knocked.
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
// Validate parameters first.
let server_names: Vec<OwnedServerName> = via
.into_iter()
.map(|server| ServerName::parse(server).map_err(ClientError::from))
.collect::<Result<_, ClientError>>()?;
// Validate internal room state.
let membership = self.membership();
if !matches!(membership, Membership::Invited | Membership::Knocked) {
return Err(RoomListError::IncorrectRoomMembership {
expected: vec![Membership::Invited, Membership::Knocked],
actual: membership,
}
.into());
}
// Do the thing.
let client = self.inner.client();
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
{
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
(room_or_alias_id, Vec::new())
} else {
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
(room_or_alias_id, server_names)
};
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
}
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
}
/// Build a full `Room` FFI object, filling its associated timeline.
///
/// An error will be returned if the room is a state different than joined
@@ -635,7 +647,7 @@ impl RoomListItem {
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
if !matches!(self.membership(), Membership::Joined) {
return Err(RoomListError::IncorrectRoomMembership {
expected: Membership::Joined,
expected: vec![Membership::Joined],
actual: self.membership(),
});
}
@@ -702,33 +714,8 @@ impl RoomListItem {
self.inner.is_encrypted().await.unwrap_or(false)
}
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
}
}
#[derive(uniffi::Record)]
pub struct RequiredState {
pub key: String,
pub value: String,
}
#[derive(uniffi::Record)]
pub struct RoomSubscription {
pub required_state: Option<Vec<RequiredState>>,
pub timeline_limit: Option<u32>,
pub include_heroes: Option<bool>,
}
impl From<RoomSubscription> for http::request::RoomSubscription {
fn from(val: RoomSubscription) -> Self {
assign!(http::request::RoomSubscription::default(), {
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()),
include_heroes: val.include_heroes,
})
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event().await.map(Into::into)
}
}
@@ -738,7 +725,7 @@ pub struct UnreadNotificationsCount {
notification_count: u32,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl UnreadNotificationsCount {
fn highlight_count(&self) -> u32 {
self.highlight_count
+40 -21
View File
@@ -1,7 +1,7 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::UserId;
use crate::error::ClientError;
use crate::error::{ClientError, NotYetImplemented};
#[derive(Clone, uniffi::Enum)]
pub enum MembershipState {
@@ -19,49 +19,64 @@ pub enum MembershipState {
/// The user has left.
Leave,
/// A custom membership state value.
Custom { value: String },
}
impl From<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self {
impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
type Error = NotYetImplemented;
fn try_from(
m: matrix_sdk::ruma::events::room::member::MembershipState,
) -> Result<Self, Self::Error> {
match m {
matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban,
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
MembershipState::Invite
matrix_sdk::ruma::events::room::member::MembershipState::Ban => {
Ok(MembershipState::Ban)
}
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
Ok(MembershipState::Invite)
}
matrix_sdk::ruma::events::room::member::MembershipState::Join => {
Ok(MembershipState::Join)
}
matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join,
matrix_sdk::ruma::events::room::member::MembershipState::Knock => {
MembershipState::Knock
Ok(MembershipState::Knock)
}
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
MembershipState::Leave
Ok(MembershipState::Leave)
}
matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => {
Ok(MembershipState::Custom { value: m.to_string() })
}
_ => {
tracing::warn!("Other membership state change not yet implemented");
Err(NotYetImplemented)
}
_ => unimplemented!(
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
),
}
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::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]
#[matrix_sdk_ffi_macros::export]
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()
}
/// Generates a `matrix.to` permalink to the given userID.
#[uniffi::export]
#[matrix_sdk_ffi_macros::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(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct RoomMember {
pub user_id: String,
pub display_name: Option<String>,
@@ -72,20 +87,24 @@ pub struct RoomMember {
pub normalized_power_level: i64,
pub is_ignored: bool,
pub suggested_role_for_power_level: RoomMemberRole,
pub membership_change_reason: Option<String>,
}
impl From<SdkRoomMember> for RoomMember {
fn from(m: SdkRoomMember) -> Self {
RoomMember {
impl TryFrom<SdkRoomMember> for RoomMember {
type Error = NotYetImplemented;
fn try_from(m: SdkRoomMember) -> Result<Self, Self::Error> {
Ok(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(),
membership: m.membership().clone().try_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(),
}
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
})
}
}
+160 -31
View File
@@ -1,9 +1,114 @@
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
use ruma::space::SpaceRoomJoinRule;
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_member::RoomMember,
utils::AsyncRuntimeDropped,
};
/// A room preview for a room. It's intended to be used to represent rooms that
/// aren't joined yet.
#[derive(uniffi::Object)]
pub struct RoomPreview {
inner: SdkRoomPreview,
client: AsyncRuntimeDropped<Client>,
}
#[matrix_sdk_ffi_macros::export]
impl RoomPreview {
/// Returns the room info the preview contains.
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
let info = &self.inner;
Ok(RoomPreviewInfo {
room_id: info.room_id.to_string(),
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
name: info.name.clone(),
topic: info.topic.clone(),
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
num_joined_members: info.num_joined_members,
num_active_members: info.num_active_members,
room_type: info.room_type.as_ref().into(),
is_history_world_readable: info.is_world_readable,
membership: info.state.map(|state| state.into()),
join_rule: info
.join_rule
.clone()
.try_into()
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
is_direct: info.is_direct,
heroes: info
.heroes
.as_ref()
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
})
}
/// Leave the room if the room preview state is either joined, invited or
/// knocked.
///
/// Will return an error otherwise.
pub async fn leave(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.leave().await.map_err(Into::into)
}
/// Get the user who created the invite, if any.
pub async fn inviter(&self) -> Option<RoomMember> {
let room = self.client.get_room(&self.inner.room_id)?;
let invite_details = room.invite_details().await.ok()?;
invite_details.inviter.and_then(|m| m.try_into().ok())
}
/// Forget the room if we had access to it, and it was left or banned.
pub async fn forget(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.forget().await?;
Ok(())
}
/// Get the membership details for the current user.
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
let room = self.client.get_room(&self.inner.room_id)?;
let (own_member, sender_member) = match room.own_membership_details().await {
Ok(memberships) => memberships,
Err(error) => {
warn!("Couldn't get membership info: {error}");
return None;
}
};
Some(RoomMembershipDetails {
own_room_member: own_member.try_into().ok()?,
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
})
}
}
/// Contains the current user's room member info and the optional room member
/// info of the sender of the `m.room.member` event that this info represents.
#[derive(uniffi::Record)]
pub struct RoomMembershipDetails {
pub own_room_member: RoomMember,
pub sender_room_member: Option<RoomMember>,
}
impl RoomPreview {
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
Self { client, inner }
}
}
/// The preview of a room, be it invited/joined/left, or not.
#[derive(uniffi::Record)]
pub struct RoomPreview {
pub struct RoomPreviewInfo {
/// The room id for this room.
pub room_id: String,
/// The canonical alias for the room.
@@ -16,38 +121,62 @@ pub struct RoomPreview {
pub avatar_url: Option<String>,
/// The number of joined members.
pub num_joined_members: u64,
/// The number of active members, if known (joined + invited).
pub num_active_members: Option<u64>,
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: Option<String>,
pub room_type: RoomType,
/// Is the history world-readable for this room?
pub is_history_world_readable: bool,
/// 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,
pub is_history_world_readable: Option<bool>,
/// The membership state for the current user, if known.
pub membership: Option<Membership>,
/// The join rule for this room (private, public, knock, etc.).
pub join_rule: JoinRule,
/// Whether the room is direct or not, if known.
pub is_direct: Option<bool>,
/// Room heroes.
pub heroes: Option<Vec<RoomHero>>,
}
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
),
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
type Error = ();
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
Ok(match join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
SpaceRoomJoinRule::Private => JoinRule::Private,
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
SpaceRoomJoinRule::Public => JoinRule::Public,
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
_ => {
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
return Err(());
}
})
}
}
/// The type of room for a [`RoomPreviewInfo`].
#[derive(Debug, Clone, uniffi::Enum)]
pub enum RoomType {
/// It's a plain chat room.
Room,
/// It's a space that can group several rooms.
Space,
/// It's a custom implementation.
Custom { value: String },
}
impl From<Option<&RumaRoomType>> for RoomType {
fn from(value: Option<&RumaRoomType>) -> Self {
match value {
Some(RumaRoomType::Space) => RoomType::Space,
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
// SAFETY: this was checked in the match branch above
value: value.unwrap().to_string(),
},
_ => RoomType::Room,
}
}
}
+212 -105
View File
@@ -15,9 +15,7 @@
use std::{collections::BTreeSet, sync::Arc, time::Duration};
use extension_trait::extension_trait;
use matrix_sdk::attachment::{
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
};
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
use ruma::{
assign,
events::{
@@ -42,7 +40,8 @@ use ruma::{
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
},
},
matrix_uri::MatrixId as RumaMatrixId,
@@ -54,6 +53,7 @@ use tracing::info;
use crate::{
error::{ClientError, MediaInfoError},
helpers::unwrap_or_clone_arc,
timeline::MessageContent,
utils::u64_to_uint,
};
@@ -89,7 +89,7 @@ impl From<AuthData> for ruma::api::client::uiaa::AuthData {
/// Parse a matrix entity from a given URI, be it either
/// a `matrix.to` link or a `matrix:` URI
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
return Some(MatrixEntity {
@@ -153,33 +153,28 @@ impl From<&RumaMatrixId> for MatrixId {
}
}
#[uniffi::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_new(
msgtype: MessageType,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_markdown(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_markdown_as_emote(
md: String,
) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_html(
body: String,
html_body: String,
@@ -189,7 +184,7 @@ pub fn message_event_content_from_html(
)))
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_from_html_as_emote(
body: String,
html_body: String,
@@ -199,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
)))
}
#[extension_trait]
pub impl MediaSourceExt for MediaSource {
fn from_json(json: String) -> Result<MediaSource, ClientError> {
let res = serde_json::from_str(&json)?;
Ok(res)
#[derive(Clone, uniffi::Object)]
pub struct MediaSource {
pub(crate) media_source: RumaMediaSource,
}
#[matrix_sdk_ffi_macros::export]
impl MediaSource {
#[uniffi::constructor]
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
let media_source = RumaMediaSource::Plain(url.into());
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
fn to_json(&self) -> String {
serde_json::to_string(self).expect("Media source should always be serializable ")
pub fn url(&self) -> String {
self.media_source.url()
}
// Used on Element X Android
#[uniffi::constructor]
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
media_source.verify()?;
Ok(Arc::new(MediaSource { media_source }))
}
// Used on Element X Android
pub fn to_json(&self) -> String {
serde_json::to_string(&self.media_source)
.expect("Media source should always be serializable ")
}
}
impl TryFrom<RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value })
}
}
impl TryFrom<&RumaMediaSource> for MediaSource {
type Error = ClientError;
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
value.verify()?;
Ok(Self { media_source: value.clone() })
}
}
impl From<MediaSource> for RumaMediaSource {
fn from(value: MediaSource) -> Self {
value.media_source
}
}
#[extension_trait]
pub(crate) impl MediaSourceExt for RumaMediaSource {
fn verify(&self) -> Result<(), ClientError> {
match self {
RumaMediaSource::Plain(url) => {
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
RumaMediaSource::Encrypted(file) => {
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
}
}
Ok(())
}
fn url(&self) -> String {
match self {
MediaSource::Plain(url) => url.to_string(),
MediaSource::Encrypted(file) => file.url.to_string(),
RumaMediaSource::Plain(url) => url.to_string(),
RumaMediaSource::Encrypted(file) => file.url.to_string(),
}
}
}
@@ -227,6 +285,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
}
}
#[derive(Clone)]
pub struct Mentions {
pub user_ids: Vec<String>,
pub room: bool,
@@ -260,8 +319,25 @@ pub enum MessageType {
Other { msgtype: String, body: String },
}
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
/// If the filename field is present in a media message, clients should treat
/// body as a caption instead of a file name. Otherwise, the body is the
/// file name.
///
/// So:
/// - if a media has a filename and a caption, the body is the caption, filename
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
}
impl TryFrom<MessageType> for RumaMessageType {
type Error = serde_json::Error;
type Error = ClientError;
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
Ok(match value {
@@ -271,35 +347,39 @@ impl TryFrom<MessageType> for RumaMessageType {
}))
}
MessageType::Image { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Image(event_content)
}
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Audio(event_content)
}
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::Video(event_content)
}
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
.info(content.info.map(Into::into).map(Box::new));
event_content.formatted = content.formatted.map(Into::into);
event_content.filename = content.filename;
event_content.formatted = content.formatted_caption.map(Into::into);
event_content.filename = filename;
Self::File(event_content)
}
MessageType::Notice { content } => {
@@ -322,9 +402,11 @@ impl TryFrom<MessageType> for RumaMessageType {
}
}
impl From<RumaMessageType> for MessageType {
fn from(value: RumaMessageType) -> Self {
match value {
impl TryFrom<RumaMessageType> for MessageType {
type Error = ClientError;
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
Ok(match value {
RumaMessageType::Emote(c) => MessageType::Emote {
content: EmoteMessageContent {
body: c.body.clone(),
@@ -333,19 +415,20 @@ 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),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
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()),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(Into::into),
audio: c.audio.map(Into::into),
voice: c.voice.map(Into::into),
@@ -353,20 +436,20 @@ 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),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
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),
filename: c.filename().to_owned(),
caption: c.caption().map(ToString::to_string),
formatted_caption: c.formatted_caption().map(Into::into),
source: Arc::new(c.source.try_into()?),
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
},
},
RumaMessageType::Notice(c) => MessageType::Notice {
@@ -402,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
},
}
})
}
}
@@ -438,18 +521,20 @@ pub struct EmoteMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct ImageMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<ImageInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct AudioMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<AudioInfo>,
pub audio: Option<UnstableAudioDetailsContent>,
@@ -458,18 +543,20 @@ pub struct AudioMessageContent {
#[derive(Clone, uniffi::Record)]
pub struct VideoMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<VideoInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
pub body: String,
pub formatted: Option<FormattedBody>,
pub filename: Option<String>,
/// The computed filename, for use in a client.
pub filename: String,
pub caption: Option<String>,
pub formatted_caption: Option<FormattedBody>,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
}
@@ -483,6 +570,7 @@ pub struct ImageInfo {
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
pub blurhash: Option<String>,
pub is_animated: Option<bool>,
}
impl From<ImageInfo> for RumaImageInfo {
@@ -493,8 +581,9 @@ impl From<ImageInfo> for RumaImageInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
blurhash: value.blurhash,
is_animated: value.is_animated,
})
}
}
@@ -516,6 +605,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
is_animated: value.is_animated,
})
}
}
@@ -598,7 +688,7 @@ impl From<VideoInfo> for RumaVideoInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
blurhash: value.blurhash,
})
}
@@ -641,7 +731,7 @@ impl From<FileInfo> for RumaFileInfo {
mimetype: value.mimetype,
size: value.size.map(u64_to_uint),
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
})
}
}
@@ -676,21 +766,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
}
}
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
type Error = MediaInfoError;
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
}
}
#[derive(Clone, uniffi::Record)]
pub struct NoticeMessageContent {
pub body: String,
@@ -763,8 +838,10 @@ pub enum MessageFormat {
Unknown { format: String },
}
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
type Error = ClientError;
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -772,15 +849,21 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
size: info.size.map(Into::into),
});
Self {
Ok(Self {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
blurhash: info.blurhash.clone(),
}
is_animated: info.is_animated,
})
}
}
@@ -794,8 +877,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
}
}
impl From<&RumaVideoInfo> for VideoInfo {
fn from(info: &RumaVideoInfo) -> Self {
impl TryFrom<&RumaVideoInfo> for VideoInfo {
type Error = ClientError;
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -803,21 +888,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
size: info.size.map(Into::into),
});
Self {
Ok(Self {
duration: info.duration,
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
blurhash: info.blurhash.clone(),
}
})
}
}
impl From<&RumaFileInfo> for FileInfo {
fn from(info: &RumaFileInfo) -> Self {
impl TryFrom<&RumaFileInfo> for FileInfo {
type Error = ClientError;
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
@@ -825,12 +917,17 @@ impl From<&RumaFileInfo> for FileInfo {
size: info.size.map(Into::into),
});
Self {
Ok(Self {
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
}
thumbnail_source: info
.thumbnail_source
.as_ref()
.map(TryInto::try_into)
.transpose()?
.map(Arc::new),
})
}
}
@@ -861,3 +958,13 @@ impl From<RumaPollKind> for PollKind {
}
}
}
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
/// [`MessageContent`] value.
#[matrix_sdk_ffi_macros::export]
pub fn content_without_relation_from_message(
message: MessageContent,
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
let msg_type = message.msg_type.try_into()?;
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type)))
}
@@ -1,18 +1,19 @@
use std::sync::{Arc, RwLock};
use anyhow::Context as _;
use futures_util::StreamExt;
use matrix_sdk::{
encryption::{
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest},
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
};
use ruma::UserId;
use tracing::{error, info};
use super::RUNTIME;
use crate::error::ClientError;
use crate::{error::ClientError, utils::Timestamp};
#[derive(uniffi::Object)]
pub struct SessionVerificationEmoji {
@@ -20,7 +21,7 @@ pub struct SessionVerificationEmoji {
description: String,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationEmoji {
pub fn symbol(&self) -> String {
self.symbol.clone()
@@ -37,8 +38,20 @@ pub enum SessionVerificationData {
Decimals { values: Vec<u16> },
}
#[uniffi::export(callback_interface)]
/// Details about the incoming verification request
#[derive(Debug, uniffi::Record)]
pub struct SessionVerificationRequestDetails {
sender_id: String,
flow_id: String,
device_id: String,
display_name: Option<String>,
/// First time this device was seen in milliseconds since epoch.
first_seen_timestamp: Timestamp,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SessionVerificationControllerDelegate: Sync + Send {
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
fn did_accept_verification_request(&self);
fn did_start_sas_verification(&self);
fn did_receive_verification_data(&self, data: SessionVerificationData);
@@ -58,19 +71,53 @@ pub struct SessionVerificationController {
sas_verification: Arc<RwLock<Option<SasVerification>>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SessionVerificationController {
pub async fn is_verified(&self) -> Result<bool, ClientError> {
let device =
self.encryption.get_own_device().await?.context("Our own device is missing")?;
Ok(device.is_cross_signed_by_owner())
}
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
*self.delegate.write().unwrap() = delegate;
}
/// Set this particular request as the currently active one and register for
/// events pertaining it.
/// * `sender_id` - The user requesting verification.
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
pub async fn acknowledge_verification_request(
&self,
sender_id: String,
flow_id: String,
) -> Result<(), ClientError> {
let sender_id = UserId::parse(sender_id.clone())?;
let verification_request = self
.encryption
.get_verification_request(&sender_id, flow_id)
.await
.ok_or(ClientError::new("Unknown session verification request"))?;
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Accept the previously acknowledged verification request
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification_request) = verification_request {
let methods = vec![VerificationMethod::SasV1];
verification_request.accept_with_methods(methods).await?;
}
Ok(())
}
/// Request verification for the current device
pub async fn request_verification(&self) -> Result<(), ClientError> {
let methods = vec![VerificationMethod::SasV1];
let verification_request = self
@@ -78,30 +125,41 @@ impl SessionVerificationController {
.request_verification_with_methods(methods)
.await
.map_err(anyhow::Error::from)?;
*self.verification_request.write().unwrap() = Some(verification_request);
*self.verification_request.write().unwrap() = Some(verification_request.clone());
RUNTIME.spawn(Self::listen_to_verification_request_changes(
verification_request,
self.sas_verification.clone(),
self.delegate.clone(),
));
Ok(())
}
/// Transition the current verification request into a SAS verification
/// flow.
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
match verification.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
match verification_request.start_sas().await {
Ok(Some(verification)) => {
*self.sas_verification.write().unwrap() = Some(verification.clone());
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
}
_ => {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
}
}
}
@@ -109,31 +167,37 @@ impl SessionVerificationController {
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn approve_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.confirm().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.confirm().await?)
}
/// Reject the short auth string
pub async fn decline_verification(&self) -> Result<(), ClientError> {
let sas_verification = self.sas_verification.read().unwrap().clone();
if let Some(sas_verification) = sas_verification {
sas_verification.mismatch().await?;
}
Ok(())
let Some(sas_verification) = sas_verification else {
return Err(ClientError::new("SAS verification missing"));
};
Ok(sas_verification.mismatch().await?)
}
/// Cancel the current verification request
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
let verification_request = self.verification_request.read().unwrap().clone();
if let Some(verification) = verification_request {
verification.cancel().await?;
}
Ok(())
let Some(verification_request) = verification_request else {
return Err(ClientError::new("Verification request missing."));
};
Ok(verification_request.cancel().await?)
}
}
@@ -149,58 +213,88 @@ impl SessionVerificationController {
}
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
match event {
// TODO: Use the changes stream for this as well once we expose
// VerificationRequest::changes() in the main crate.
AnyToDeviceEvent::KeyVerificationStart(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
}
if let Some(verification) = self
.encryption
.get_verification(
self.user_identity.user_id(),
event.content.transaction_id.as_str(),
)
.await
{
if let Some(sas_verification) = verification.sas() {
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
info!("Received verification request: {:}", event.sender);
if sas_verification.accept().await.is_ok() {
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let Some(request) = self
.encryption
.get_verification_request(&event.sender, &event.content.transaction_id)
.await
else {
error!("Failed retrieving verification request");
return;
};
let delegate = self.delegate.clone();
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_fail()
if !request.is_self_verification() {
info!("Received non-self verification request. Ignoring.");
return;
}
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
else {
error!("Received key verification event but the request is in the wrong state.");
return;
};
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
sender_id: request.other_user_id().into(),
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
display_name: other_device_data.display_name().map(str::to_string),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
}
}
async fn listen_to_verification_request_changes(
verification_request: VerificationRequest,
sas_verification: Arc<RwLock<Option<SasVerification>>>,
delegate: Delegate,
) {
let mut stream = verification_request.changes();
while let Some(state) = stream.next().await {
match state {
VerificationRequestState::Transitioned { verification } => {
let Some(verification) = verification.sas() else {
error!("Invalid, non-sas verification flow. Returning.");
return;
};
*sas_verification.write().unwrap() = Some(verification.clone());
if verification.accept().await.is_ok() {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_start_sas_verification()
}
let delegate = delegate.clone();
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
verification,
delegate,
));
} else if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_fail()
}
}
}
AnyToDeviceEvent::KeyVerificationReady(event) => {
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
return;
VerificationRequestState::Ready { .. } => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_accept_verification_request()
}
}
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_accept_verification_request()
VerificationRequestState::Cancelled(..) => {
if let Some(delegate) = &*delegate.read().unwrap() {
delegate.did_cancel();
}
}
_ => {}
}
_ => (),
}
}
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
match &*self.verification_request.read().unwrap() {
Some(verification) => verification.flow_id() == transaction_id,
None => false,
}
}
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
let mut stream = sas.changes();
while let Some(state) = stream.next().await {
+38 -9
View File
@@ -38,6 +38,7 @@ pub enum SyncServiceState {
Running,
Terminated,
Error,
Offline,
}
impl From<MatrixSyncServiceState> for SyncServiceState {
@@ -47,11 +48,12 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
MatrixSyncServiceState::Running => Self::Running,
MatrixSyncServiceState::Terminated => Self::Terminated,
MatrixSyncServiceState::Error => Self::Error,
MatrixSyncServiceState::Offline => Self::Offline,
}
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncServiceStateObserver: Send + Sync + Debug {
fn on_update(&self, state: SyncServiceState);
}
@@ -62,7 +64,7 @@ pub struct SyncService {
utd_hook: Option<Arc<UtdHookManager>>,
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncService {
pub fn room_list_service(&self) -> Arc<RoomListService> {
Arc::new(RoomListService {
@@ -72,11 +74,11 @@ impl SyncService {
}
pub async fn start(&self) {
self.inner.start().await;
self.inner.start().await
}
pub async fn stop(&self) -> Result<(), ClientError> {
Ok(self.inner.stop().await?)
pub async fn stop(&self) {
self.inner.stop().await
}
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
@@ -110,11 +112,18 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock(app_identifier);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
/// Enable the "offline" mode for the [`SyncService`].
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_offline_mode();
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
}
@@ -153,7 +162,7 @@ impl SyncServiceBuilder {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait UnableToDecryptDelegate: Sync + Send {
fn on_utd(&self, info: UnableToDecryptInfo);
}
@@ -201,6 +210,22 @@ pub struct UnableToDecryptInfo {
/// 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,
/// The difference between the event creation time (`origin_server_ts`) and
/// the time our device was created. If negative, this event was sent
/// *before* our device was created.
pub event_local_age_millis: i64,
/// Whether the user had verified their own identity at the point they
/// received the UTD event.
pub user_trusts_own_identity: bool,
/// The homeserver of the user that sent the undecryptable event.
pub sender_homeserver: String,
/// Our local user's own homeserver, or `None` if the client is not logged
/// in.
pub own_homeserver: Option<String>,
}
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
@@ -209,6 +234,10 @@ impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
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,
event_local_age_millis: value.event_local_age_millis,
user_trusts_own_identity: value.user_trusts_own_identity,
sender_homeserver: value.sender_homeserver.to_string(),
own_homeserver: value.own_homeserver.map(String::from),
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ impl TaskHandle {
}
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TaskHandle {
// Cancel a task handle.
pub fn cancel(&self) {
@@ -0,0 +1,85 @@
use ruma::EventId;
use super::FocusEventError;
use crate::{error::ClientError, event::RoomMessageEventMessageType};
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
Live,
Event { event_id: String, num_context_events: u16 },
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
}
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
type Error = ClientError;
fn try_from(
value: TimelineFocus,
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
match value {
TimelineFocus::Live => Ok(Self::Live),
TimelineFocus::Event { event_id, num_context_events } => {
let parsed_event_id =
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
event_id: event_id.clone(),
err: err.to_string(),
})?;
Ok(Self::Event { target: parsed_event_id, num_context_events })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
}
}
}
}
/// Changes how date dividers get inserted, either in between each day or in
/// between each month
#[derive(uniffi::Enum)]
pub enum DateDividerMode {
Daily,
Monthly,
}
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
fn from(value: DateDividerMode) -> Self {
match value {
DateDividerMode::Daily => Self::Daily,
DateDividerMode::Monthly => Self::Monthly,
}
}
}
#[derive(uniffi::Enum)]
pub enum AllowedMessageTypes {
All,
Only { types: Vec<RoomMessageEventMessageType> },
}
/// Various options used to configure the timeline's behavior.
///
/// # Arguments
///
/// * `internal_id_prefix` -
///
/// * `allowed_message_types` -
///
/// * `date_divider_mode` -
#[derive(uniffi::Record)]
pub struct TimelineConfiguration {
/// What should the timeline focus on?
pub focus: TimelineFocus,
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
/// in the timeline
pub allowed_message_types: AllowedMessageTypes,
/// 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.
pub internal_id_prefix: Option<String>,
/// How often to insert date dividers
pub date_divider_mode: DateDividerMode,
}
+126 -81
View File
@@ -16,49 +16,81 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource};
use tracing::warn;
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, MessageType, PollKind};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
utils::Timestamp,
};
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
match value {
Content::Message(message) => {
let msgtype = message.msgtype().msgtype().to_owned();
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContentKind::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
source: Arc::new(MediaSource::from(content.source.clone())),
match TryInto::<MessageContent>::try_into(message) {
Ok(message) => TimelineItemContent::Message { content: message },
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: msgtype,
error: error.to_string(),
},
}
}
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::CallInvite => TimelineItemContentKind::CallInvite,
Content::Sticker(sticker) => {
let content = sticker.content();
Content::CallNotify => TimelineItemContentKind::CallNotify,
let media_source = RumaMediaSource::from(content.source.clone());
Content::UnableToDecrypt(msg) => {
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
if let Err(error) = media_source.verify() {
return TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
};
}
match TryInto::<ImageInfo>::try_into(&content.info) {
Ok(info) => TimelineItemContent::Sticker {
body: content.body.clone(),
info,
source: Arc::new(MediaSource { media_source }),
},
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
},
}
}
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
},
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::UnableToDecrypt(msg) => {
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
}
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
user_id: membership.user_id().to_string(),
user_display_name: membership.display_name(),
change: membership.change().map(Into::into),
reason,
}
}
Content::ProfileChange(profile) => {
let (display_name, prev_display_name) = profile
@@ -74,7 +106,7 @@ impl TimelineItemContent {
)
})
.unzip();
TimelineItemContentKind::ProfileChange {
TimelineItemContent::ProfileChange {
display_name: display_name.flatten(),
prev_display_name: prev_display_name.flatten(),
avatar_url: avatar_url.flatten(),
@@ -82,37 +114,68 @@ impl TimelineItemContent {
}
}
Content::OtherState(state) => TimelineItemContentKind::State {
Content::OtherState(state) => TimelineItemContent::State {
state_key: state.state_key().to_owned(),
content: state.content().into(),
},
Content::FailedToParseMessageLike { event_type, error } => {
TimelineItemContentKind::FailedToParseMessageLike {
TimelineItemContent::FailedToParseMessageLike {
event_type: event_type.to_string(),
error: error.to_string(),
}
}
Content::FailedToParseState { event_type, state_key, error } => {
TimelineItemContentKind::FailedToParseState {
TimelineItemContent::FailedToParseState {
event_type: event_type.to_string(),
state_key: state_key.to_string(),
state_key,
error: error.to_string(),
}
}
}
}
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
#[derive(Clone, uniffi::Record)]
pub struct MessageContent {
pub msg_type: MessageType,
pub body: String,
pub in_reply_to: Option<Arc<InReplyToDetails>>,
pub thread_root: Option<String>,
pub is_edited: bool,
pub mentions: Option<Mentions>,
}
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
type Error = ClientError;
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
Ok(Self {
msg_type: value.msgtype().clone().try_into()?,
body: value.body().to_owned(),
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
is_edited: value.is_edited(),
thread_root: value.thread_root().map(|id| id.to_string()),
mentions: value.mentions().cloned().map(|m| m.into()),
})
}
}
#[derive(uniffi::Enum)]
pub enum TimelineItemContentKind {
Message,
impl From<ruma::events::Mentions> for Mentions {
fn from(value: ruma::events::Mentions) -> Self {
Self {
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
room: value.room,
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum TimelineItemContent {
Message {
content: MessageContent,
},
RedactedMessage,
Sticker {
body: String,
@@ -125,7 +188,7 @@ pub enum TimelineItemContentKind {
max_selections: u64,
answers: Vec<PollAnswer>,
votes: HashMap<String, Vec<String>>,
end_time: Option<u64>,
end_time: Option<Timestamp>,
has_been_edited: bool,
},
CallInvite,
@@ -137,6 +200,7 @@ pub enum TimelineItemContentKind {
user_id: String,
user_display_name: Option<String>,
change: Option<MembershipChange>,
reason: Option<String>,
},
ProfileChange {
display_name: Option<String>,
@@ -160,36 +224,6 @@ pub enum TimelineItemContentKind {
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
pub fn msgtype(&self) -> MessageType {
self.0.msgtype().clone().into()
}
pub fn body(&self) -> String {
self.0.msgtype().body().to_owned()
}
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
self.0.in_reply_to().map(InReplyToDetails::from)
}
pub fn is_threaded(&self) -> bool {
self.0.is_threaded()
}
pub fn is_edited(&self) -> bool {
self.0.is_edited()
}
pub fn content(&self) -> Arc<RoomMessageEventContentWithoutRelation> {
Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone()))
}
}
#[derive(uniffi::Record)]
pub struct InReplyToDetails {
event_id: String,
event: RepliedToEventDetails,
@@ -201,14 +235,25 @@ impl InReplyToDetails {
}
}
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
#[matrix_sdk_ffi_macros::export]
impl InReplyToDetails {
pub fn event_id(&self) -> String {
self.event_id.clone()
}
pub fn event(&self) -> RepliedToEventDetails {
self.event.clone()
}
}
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
TimelineDetails::Pending => RepliedToEventDetails::Pending,
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
content: Arc::new(TimelineItemContent(event.content().to_owned())),
content: event.content().clone().into(),
sender: event.sender().to_string(),
sender_profile: event.sender_profile().into(),
},
@@ -221,11 +266,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
}
}
#[derive(uniffi::Enum)]
#[derive(Clone, uniffi::Enum)]
pub enum RepliedToEventDetails {
Unavailable,
Pending,
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
Error { message: String },
}
@@ -275,7 +320,7 @@ pub struct Reaction {
#[derive(Clone, uniffi::Record)]
pub struct ReactionSenderData {
pub sender_id: String,
pub timestamp: u64,
pub timestamp: Timestamp,
}
#[derive(Clone, uniffi::Enum)]
@@ -419,15 +464,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
}
}
#[derive(uniffi::Record)]
#[derive(Clone, uniffi::Record)]
pub struct PollAnswer {
pub id: String,
pub text: String,
}
impl From<PollResult> for TimelineItemContentKind {
impl From<PollResult> for TimelineItemContent {
fn from(value: PollResult) -> Self {
TimelineItemContentKind::Poll {
TimelineItemContent::Poll {
question: value.question,
kind: PollKind::from(value.kind),
max_selections: value.max_selections,
@@ -437,7 +482,7 @@ impl From<PollResult> for TimelineItemContentKind {
.map(|i| PollAnswer { id: i.id, text: i.text })
.collect(),
votes: value.votes,
end_time: value.end_time,
end_time: value.end_time.map(|t| t.into()),
has_been_edited: value.has_been_edited,
}
}
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
+12 -2
View File
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// level + target) it is called with. Please make sure that the number of
/// different combinations of those parameters this can be called with is
/// constant in the final executable.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
Mutex::new(BTreeMap::new());
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
#[derive(uniffi::Object)]
pub struct Span(tracing::Span);
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
impl Span {
/// Create a span originating at the given callsite (file, line and column).
///
@@ -185,6 +185,16 @@ impl LogLevel {
LogLevel::Trace => tracing::Level::TRACE,
}
}
pub(crate) fn as_str(&self) -> &'static str {
match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
+57 -1
View File
@@ -12,12 +12,68 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::UInt;
use std::{mem::ManuallyDrop, ops::Deref};
use async_compat::TOKIO1 as RUNTIME;
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
use tracing::warn;
#[derive(Debug, Clone)]
pub struct Timestamp(u64);
impl From<MilliSecondsSinceUnixEpoch> for Timestamp {
fn from(date: MilliSecondsSinceUnixEpoch) -> Self {
Self(date.0.into())
}
}
uniffi::custom_newtype!(Timestamp, u64);
pub(crate) fn u64_to_uint(u: u64) -> UInt {
UInt::new(u).unwrap_or_else(|| {
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
UInt::MAX
})
}
/// Tiny wrappers for data types that must be dropped in the context of an async
/// runtime.
///
/// This is useful whenever such a data type may transitively call some
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
/// at the moment), like done in some `deadpool` drop impls.
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
impl<T> AsyncRuntimeDropped<T> {
/// Create a new wrapper for this type that will be dropped under an async
/// runtime.
pub fn new(val: T) -> Self {
Self(ManuallyDrop::new(val))
}
}
impl<T> Drop for AsyncRuntimeDropped<T> {
fn drop(&mut self) {
let _guard = RUNTIME.enter();
// SAFETY: self.inner is never used again, which is the only requirement
// for ManuallyDrop::drop to be used safely.
unsafe {
ManuallyDrop::drop(&mut self.0);
}
}
}
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
impl<T> Deref for AsyncRuntimeDropped<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
+42 -27
View File
@@ -5,6 +5,7 @@ use matrix_sdk::{
async_trait,
widget::{MessageLikeEventFilter, StateEventFilter},
};
use ruma::events::MessageLikeEventType;
use tracing::error;
use crate::{room::Room, RUNTIME};
@@ -15,7 +16,7 @@ pub struct WidgetDriverAndHandle {
pub handle: Arc<WidgetDriverHandle>,
}
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
Ok(WidgetDriverAndHandle {
@@ -29,7 +30,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
#[derive(uniffi::Object)]
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl WidgetDriver {
pub async fn run(
&self,
@@ -96,7 +97,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
/// * `room` - A matrix room which is used to query the logged in username
/// * `props` - Properties from the client that can be used by a widget to adapt
/// to the client. e.g. language, font-scale...
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
pub async fn generate_webview_url(
widget_settings: WidgetSettings,
room: Arc<Room>,
@@ -241,7 +242,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
///
/// * `props` - A struct containing the configuration parameters for a element
/// call widget.
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn new_virtual_element_call_widget(
props: VirtualElementCallWidgetOptions,
) -> Result<WidgetSettings, ParseError> {
@@ -261,13 +262,38 @@ pub fn new_virtual_element_call_widget(
/// Editing and extending the capabilities from this function is also possible,
/// but should only be done as temporal workarounds until this function is
/// adjusted
#[uniffi::export]
#[matrix_sdk_ffi_macros::export]
pub fn get_element_call_required_permissions(
own_user_id: String,
own_device_id: String,
) -> WidgetCapabilities {
use ruma::events::StateEventType;
let read_send = vec![
// To read and send rageshake requests from other room members
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read and send encryption keys
// TODO change this to the appropriate to-device version once ready
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
// To read and send custom EC reactions. They are different to normal `m.reaction`
// because they can be send multiple times to the same event.
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.reaction".to_owned(),
},
// This allows send raise hand reactions.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::Reaction.to_string(),
},
// This allows to detect if someone does not raise their hand anymore.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::RoomRedaction.to_string(),
},
];
WidgetCapabilities {
read: vec![
// To compute the current state of the matrixRTC session.
@@ -278,19 +304,13 @@ pub fn get_element_call_required_permissions(
WidgetEventFilter::StateWithType {
event_type: StateEventType::RoomEncryption.to_string(),
},
// To read rageshake requests from other room members
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To read encryption keys
// TODO change this to the appropriate to-device version once ready
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
// This allows the widget to check the room version, so it can know about
// version-specific auth rules (namely MSC3779).
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
],
]
.into_iter()
.chain(read_send.clone())
.collect(),
send: vec![
// To send the call participation state event (main MatrixRTC event).
// This is required for legacy state events (using only one event for all devices with
@@ -313,15 +333,10 @@ pub fn get_element_call_required_permissions(
event_type: StateEventType::CallMember.to_string(),
state_key: format!("_{own_user_id}_{own_device_id}"),
},
// To request other room members to send rageshakes
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.rageshake_request".to_owned(),
},
// To send this user's encryption keys
WidgetEventFilter::MessageLikeWithType {
event_type: "io.element.call.encryption_keys".to_owned(),
},
],
]
.into_iter()
.chain(read_send)
.collect(),
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
@@ -354,7 +369,7 @@ impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
#[derive(uniffi::Object)]
pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
#[uniffi::export(async_runtime = "tokio")]
#[matrix_sdk_ffi_macros::export]
impl WidgetDriverHandle {
/// Receive a message from the widget driver.
///
@@ -417,7 +432,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
}
/// Different kinds of filters that could be applied to the timeline events.
#[derive(uniffi::Enum)]
#[derive(uniffi::Enum, Clone)]
pub enum WidgetEventFilter {
/// Matches message-like events with the given `type`.
MessageLikeWithType { event_type: String },
@@ -469,7 +484,7 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
}
}
#[uniffi::export(callback_interface)]
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait WidgetCapabilitiesProvider: Send + Sync {
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
}
+124 -11
View File
@@ -1,21 +1,134 @@
# unreleased
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- [**breaking**] `EventCacheStore` allows to control which media content is
allowed in the media cache, and how long it should be kept, with a
`MediaRetentionPolicy`:
- `EventCacheStore::add_media_content()` has an extra argument,
`ignore_policy`, which decides whether a media content should ignore the
`MediaRetentionPolicy`. It should be stored alongside the media content.
- `EventCacheStore` has four new methods: `media_retention_policy()`,
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
`clean_up_media_cache()`.
- `EventCacheStore` implementations should delegate media cache methods to the
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
They need to implement the `EventCacheStoreMedia` trait that can be tested
with the `event_cache_store_media_integration_tests!` macro.
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
### Refactor
- [**breaking**] Replaced `Room::compute_display_name` with the reintroduced
`Room::display_name()`. The new method computes a display name, or return a
cached value from the previous successful computation. If you need a sync
variant, consider using `Room::cached_display_name()`.
([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470))
- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent`
have been fused into a single type `TimelineEvent`, and its field
`push_actions` has been made `Option`al (it is set to `None` when we couldn't
compute the push actions, because we lacked some information).
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
## [0.9.0] - 2024-12-18
### Features
- Introduced support for
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
the designation of certain users as service members. These flagged users are
excluded from the room display name calculation.
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
### Bug Fixes
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
called. Previously, items following the removed item were not shifted left by
one position, leaving them at incorrect indices.
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
## [0.8.0] - 2024-11-19
### Bug Fixes
- Add more invalid characters for room aliases.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
- `AmbiguityCache` contains the room member's user ID
- `AmbiguityCache` contains the room member's user ID.
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
request an animated thumbnail They both take a `MediaThumbnailSettings`
instead of `MediaThumbnailSize`.
- Consider knocked members to be part of the room for display name
disambiguation.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
- Allow aborting media uploads.
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
### Refactor
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
- Rename `AmbiguityMap` to `DisplayNameUsers`.
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
don't acquire the lock for every room they read.
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
`Client::rooms` and `Client::rooms_filtered`.
- `Client::get_stripped_rooms` has finally been removed.
- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail
- They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`.
- The `StateStore` methods to access data in the media cache where moved to a separate
`EventCacheStore` trait.
- The `instant` module was removed, use the `ruma::time` module instead.
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
- [**breaking**] The `StateStore` methods to access data in the media cache
where moved to a separate `EventCacheStore` trait.
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
# 0.7.0
+20 -10
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.10.0"
[package.metadata.docs.rs]
all-features = true
@@ -21,16 +21,15 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
experimental-sliding-sync = [
"ruma/unstable-msc3575",
"ruma/unstable-msc4186",
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []
test-send-sync = [
"matrix-sdk-common/test-send-sync",
"matrix-sdk-crypto?/test-send-sync",
]
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
@@ -49,8 +48,9 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.4.0", features = ["serde"] }
eyeball = { workspace = true }
bitflags = { version = "2.8.0", features = ["serde"] }
decancer = "3.2.8"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im = { workspace = true }
futures-util = { workspace = true }
growable-bloom-filter = { workspace = true }
@@ -60,7 +60,16 @@ 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", "unstable-msc2867", "rand"] }
regex = "1.11.1"
ruma = { workspace = true, features = [
"canonical-json",
"unstable-msc2867",
"unstable-msc3381",
"unstable-msc3575",
"unstable-msc4186",
"rand",
] }
unicode-normalization = { workspace = true }
serde = { workspace = true, features = ["rc"] }
serde_json = { workspace = true }
tokio = { workspace = true }
@@ -76,12 +85,13 @@ futures-executor = { workspace = true }
http = { workspace = true }
matrix-sdk-test = { workspace = true }
stream_assert = { workspace = true }
similar-asserts = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.33"
wasm-bindgen-test = { workspace = true }
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+21 -4
View File
@@ -17,14 +17,17 @@
use std::fmt;
pub use matrix_sdk_common::debug::*;
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
use ruma::{
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
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.
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(DebugRawEventNoId));
@@ -38,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
impl fmt::Debug for DebugInvitedRoom<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvitedRoom")
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
@@ -46,10 +49,24 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
}
}
/// A wrapper around a knocked on 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 `knock_state`.
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for DebugKnockedRoom<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KnockedRoom")
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
.finish()
}
}
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
#[cfg(not(tarpaulin_include))]
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
list.entries(self.0.iter().map(DebugRawEvent));
@@ -14,9 +14,11 @@
//! SDK-specific variations of response types from Ruma.
use std::{collections::BTreeMap, fmt, iter};
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
pub use matrix_sdk_common::deserialized_responses::*;
use once_cell::sync::Lazy;
use regex::Regex;
use ruma::{
events::{
room::{
@@ -28,9 +30,10 @@ use ruma::{
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
};
use serde::Serialize;
use unicode_normalization::UnicodeNormalization;
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
@@ -67,6 +70,178 @@ pub struct AmbiguityChanges {
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::MXID_PATTERN)
.expect("We should be able to create a regex from our static MXID pattern")
});
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
.expect("We should be able to create a regex from our static left-to-right pattern")
});
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
.expect("We should be able to create a regex from our static hidden characters pattern")
});
/// Regex to match `i` characters.
///
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
});
/// Regex to match `0` characters.
///
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
});
/// Regex to match a couple of dot-like characters, also matches an actual dot.
///
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
/// ambiguous.
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
});
/// A high-level wrapper for strings representing display names.
///
/// This wrapper provides attempts to determine whether a display name
/// contains characters that could make it ambiguous or easily confused
/// with similar names.
///
///
/// # Examples
///
/// ```
/// use matrix_sdk_base::deserialized_responses::DisplayName;
///
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
///
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
/// ```
///
/// ```
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
/// let display_name = DisplayName::new("@alice:localhost");
///
/// // The display name looks like an MXID, which makes it ambiguous.
/// assert!(display_name.is_inherently_ambiguous());
/// ```
#[derive(Debug, Clone, Eq)]
pub struct DisplayName {
raw: String,
decancered: Option<String>,
}
impl Hash for DisplayName {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if let Some(decancered) = &self.decancered {
decancered.hash(state);
} else {
self.raw.hash(state);
}
}
}
impl PartialEq for DisplayName {
fn eq(&self, other: &Self) -> bool {
match (self.decancered.as_deref(), other.decancered.as_deref()) {
(None, None) => self.raw == other.raw,
(None, Some(_)) | (Some(_), None) => false,
(Some(this), Some(other)) => this == other,
}
}
}
impl DisplayName {
/// Regex pattern matching an MXID.
const MXID_PATTERN: &'static str = "@.+[:.].+";
/// Regex pattern matching some left-to-right formatting marks:
/// * LTR and RTL marks U+200E and U+200F
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
/// Regex pattern matching bunch of unicode control characters and otherwise
/// misleading/invisible characters.
///
/// This includes:
/// * various width spaces U+2000 - U+200D
/// * Combining characters U+0300 - U+036F
/// * Blank/invisible characters (U2800, U2062-U2063)
/// * Arabic Letter RTL mark U+061C
/// * Zero width no-break space (BOM) U+FEFF
const HIDDEN_CHARACTERS_PATTERN: &'static str =
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
/// Creates a new [`DisplayName`] from the given raw string.
///
/// The raw display name is transformed into a Unicode-normalized form, with
/// common confusable characters removed to reduce ambiguity.
///
/// **Note**: If removing confusable characters fails,
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
/// [`DisplayName::as_normalized_str()`] will return `None.
pub fn new(raw: &str) -> Self {
let normalized = raw.nfd().collect::<String>();
let replaced = DOT_REGEX.replace_all(&normalized, ":");
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
// We re-run the dot replacement because decancer normalized a lot of weird
// characets into a `.`, it just doesn't do that for /u{1d16d}.
let replaced = DOT_REGEX.replace_all(&replaced, ":");
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
replaced.to_string()
});
Self { raw: raw.to_owned(), decancered }
}
/// Is this display name considered to be ambiguous?
///
/// If the display name has cancer (i.e. fails normalisation or has a
/// different normalised form) or looks like an MXID, then it's ambiguous.
pub fn is_inherently_ambiguous(&self) -> bool {
// If we look like an MXID or have hidden characters then we're ambiguous.
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
}
/// Returns the underlying raw and and unsanitized string of this
/// [`DisplayName`].
pub fn as_raw_str(&self) -> &str {
&self.raw
}
/// Returns the underlying normalized and and sanitized string of this
/// [`DisplayName`].
///
/// Returns `None` if normalization failed during construction of this
/// [`DisplayName`].
pub fn as_normalized_str(&self) -> Option<&str> {
self.decancered.as_deref()
}
fn has_hidden_characters(&self) -> bool {
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
}
fn looks_like_an_mxid(&self) -> bool {
self.decancered
.as_deref()
.map(|d| MXID_REGEX.is_match(d))
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
}
}
/// A deserialized response for the rooms members API call.
///
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
@@ -294,10 +469,29 @@ impl MemberEvent {
///
/// It there is no `displayname` in the event's content, the localpart or
/// the user ID is returned.
pub fn display_name(&self) -> &str {
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart())
pub fn display_name(&self) -> DisplayName {
DisplayName::new(
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart()),
)
}
/// The optional reason why the membership changed.
pub fn reason(&self) -> Option<&str> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
_ => None,
}
}
/// The optional timestamp for this member event.
pub fn timestamp(&self) -> Option<UInt> {
match self {
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
_ => None,
}
}
}
@@ -310,3 +504,240 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
}
}
}
#[cfg(test)]
mod test {
macro_rules! assert_display_name_eq {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
similar_asserts::assert_eq!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_display_name_ne {
($left:expr, $right:expr $(, $desc:expr)?) => {{
let left = crate::deserialized_responses::DisplayName::new($left);
let right = crate::deserialized_responses::DisplayName::new($right);
assert_ne!(
left,
right
$(, $desc)?
);
}};
}
macro_rules! assert_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
name.is_inherently_ambiguous(),
"The display {:?} should be considered amgibuous",
name
);
};
}
macro_rules! assert_not_ambiguous {
($name:expr) => {
let name = crate::deserialized_responses::DisplayName::new($name);
assert!(
!name.is_inherently_ambiguous(),
"The display {:?} should not be considered amgibuous",
name
);
};
}
#[test]
fn test_display_name_inherently_ambiguous() {
// These should not be inherently ambiguous, only if another similarly looking
// display name appears should they be considered to be ambiguous.
assert_not_ambiguous!("Alice");
assert_not_ambiguous!("Carol");
assert_not_ambiguous!("Car0l");
assert_not_ambiguous!("Ivan");
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
assert_not_ambiguous!("Sahasrahla");
// Left to right is fine, if it's the only one in the room.
assert_not_ambiguous!("\u{202e}alharsahas");
// These on the other hand contain invisible chars.
assert_ambiguous!("Sa̴hasrahla");
assert_ambiguous!("Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_capitalization() {
// Display name with different capitalization
assert_display_name_eq!("Alice", "alice");
}
#[test]
fn test_display_name_equality_different_names() {
// Different display names
assert_display_name_ne!("Alice", "Carol");
}
#[test]
fn test_display_name_equality_capital_l() {
// Different display names
assert_display_name_eq!("Hello", "HeIlo");
}
#[test]
fn test_display_name_equality_confusable_zero() {
// Different display names
assert_display_name_eq!("Carol", "Car0l");
}
#[test]
fn test_display_name_equality_cyrillic() {
// Display name with scritpure symbols
assert_display_name_eq!("alice", "аlice");
}
#[test]
fn test_display_name_equality_scriptures() {
// Display name with scritpure symbols
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
}
#[test]
fn test_display_name_equality_frakturs() {
// Display name with fraktur symbols
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
}
#[test]
fn test_display_name_equality_circled() {
// Display name with circled symbols
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
}
#[test]
fn test_display_name_equality_squared() {
// Display name with squared symbols
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
}
#[test]
fn test_display_name_equality_big_unicode() {
// Display name with big unicode letters
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
}
#[test]
fn test_display_name_equality_left_to_right() {
// Display name with a left-to-right character
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
}
#[test]
fn test_display_name_equality_diacritical() {
// Display name with a diacritical mark.
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
}
#[test]
fn test_display_name_equality_zero_width_joiner() {
// Display name with a zero-width joiner
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
}
#[test]
fn test_display_name_equality_zero_width_space() {
// Display name with zero-width space.
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
}
#[test]
fn test_display_name_equality_ligatures() {
// Display name with a ligature.
assert_display_name_eq!("ff", "\u{FB00}");
}
#[test]
fn test_display_name_confusable_mxid_colon() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid\u{0589}domain.tld");
assert_ambiguous!("@mxid\u{05c3}domain.tld");
assert_ambiguous!("@mxid\u{0703}domain.tld");
assert_ambiguous!("@mxid\u{0a83}domain.tld");
assert_ambiguous!("@mxid\u{16ec}domain.tld");
assert_ambiguous!("@mxid\u{205a}domain.tld");
assert_ambiguous!("@mxid\u{2236}domain.tld");
assert_ambiguous!("@mxid\u{fe13}domain.tld");
assert_ambiguous!("@mxid\u{fe52}domain.tld");
assert_ambiguous!("@mxid\u{fe30}domain.tld");
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
}
#[test]
fn test_display_name_confusable_mxid_dot() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain\u{0701}tld");
assert_ambiguous!("@mxid:domain\u{0702}tld");
assert_ambiguous!("@mxid:domain\u{2024}tld");
assert_ambiguous!("@mxid:domain\u{fe52}tld");
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_a() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
}
#[test]
fn test_display_name_confusable_mxid_replacing_l() {
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
// Additionally these should be considered to be ambiguous on their own.
assert_ambiguous!("@mxid:domain.tId");
assert_ambiguous!("@mxid:domain.t\u{217c}d");
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
assert_ambiguous!("@mxid:domain.t\u{2223}d");
}
}
+19
View File
@@ -15,10 +15,13 @@
//! Error conditions.
use matrix_sdk_common::store_locks::LockStoreError;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
use thiserror::Error;
use crate::event_cache::store::EventCacheStoreError;
/// Result type of the rust-sdk.
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -42,6 +45,14 @@ pub enum Error {
#[error(transparent)]
StateStore(#[from] crate::store::StoreError),
/// An error happened while manipulating the event cache store.
#[error(transparent)]
EventCacheStore(#[from] EventCacheStoreError),
/// An error happened while attempting to lock the event cache store.
#[error(transparent)]
EventCacheLock(#[from] LockStoreError),
/// An error occurred in the crypto store.
#[cfg(feature = "e2e-encryption")]
#[error(transparent)]
@@ -61,4 +72,12 @@ pub enum Error {
/// function with invalid parameters
#[error("receive_all_members function was called with invalid parameters")]
InvalidReceiveMembersParameters,
/// This request failed because the local data wasn't sufficient.
#[error("Local cache doesn't contain all necessary data to perform the action.")]
InsufficientData,
/// There was a [`serde_json`] deserialization error.
#[error(transparent)]
DeserializationError(#[from] serde_json::error::Error),
}
@@ -0,0 +1,30 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
pub mod store;
/// The kind of event the event storage holds.
pub type Event = TimelineEvent;
/// The kind of gap the event storage holds.
#[derive(Clone, Debug)]
pub struct Gap {
/// The token to use in the query, extracted from a previous "from" /
/// "end" field of a `/messages` response.
pub prev_token: String,
}
@@ -0,0 +1,665 @@
// 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.
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use assert_matches::assert_matches;
use async_trait::async_trait;
use matrix_sdk_common::{
deserialized_responses::{
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
VerificationState,
},
linked_chunk::{
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
Update,
},
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
push::Action, room_id, uint, RoomId,
};
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
use crate::{
event_cache::{Event, Gap},
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
};
/// Create a test event with all data filled, for testing that linked chunk
/// correctly stores event data.
///
/// Keep in sync with [`check_test_event`].
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
let encryption_info = EncryptionInfo {
sender: (*ALICE).into(),
sender_device: None,
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
curve25519_key: "1337".to_owned(),
sender_claimed_keys: Default::default(),
},
verification_state: VerificationState::Verified,
};
let event = EventFactory::new()
.text_msg(content)
.room(room_id)
.sender(*ALICE)
.into_raw_timeline()
.cast();
TimelineEvent {
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
event,
encryption_info,
unsigned_encryption_info: None,
}),
push_actions: Some(vec![Action::Notify]),
}
}
/// Check that an event created with [`make_test_event`] contains the expected
/// data.
///
/// Keep in sync with [`make_test_event`].
#[track_caller]
pub fn check_test_event(event: &TimelineEvent, text: &str) {
// Check push actions.
let actions = event.push_actions.as_ref().unwrap();
assert_eq!(actions.len(), 1);
assert_matches!(&actions[0], Action::Notify);
// Check content.
assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => {
// Check encryption fields.
assert_eq!(d.encryption_info.sender, *ALICE);
assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => {
assert_eq!(curve25519_key, "1337");
});
// Check event.
let deserialized = d.event.deserialize().unwrap();
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
assert_eq!(msg.as_original().unwrap().content.body(), text);
});
});
}
/// `EventCacheStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`event_cache_store_integration_tests!`] macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreIntegrationTests {
/// Test media content storage.
async fn test_media_content(&self);
/// Test replacing a MXID.
async fn test_replace_media_key(&self);
/// Test handling updates to a linked chunk and reloading these updates from
/// the store.
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
/// Test that rebuilding a linked chunk from an empty store doesn't return
/// anything.
async fn test_rebuild_empty_linked_chunk(&self);
/// Test that clear all the rooms' linked chunks works.
async fn test_clear_all_rooms_chunks(&self);
/// Test that removing a room from storage empties all associated data.
async fn test_remove_room(&self);
}
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let request_thumbnail = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
Method::Crop,
uint!(100),
uint!(100),
)),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequestParameters {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media failed");
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
assert_eq!(
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
Some(&content),
"media not found by URI though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media still found by URI after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media again failed");
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
// Let's add the thumbnail media.
self.add_media_content(
&request_thumbnail,
thumbnail_content.clone(),
IgnoreMediaRetentionPolicy::No,
)
.await
.expect("adding thumbnail failed");
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// We get a file with the URI, we don't know which one.
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
"media not found by URI though two where added"
);
// Let's add another media with a different URI.
self.add_media_content(
&request_other_file,
other_content.clone(),
IgnoreMediaRetentionPolicy::No,
)
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
assert_eq!(
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
Some(&other_content),
"other file not found by URI"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media found by URI wasn't removed"
);
assert!(
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
"other media found by URI was removed"
);
}
async fn test_replace_media_key(&self) {
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
let req = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let content = "hello".as_bytes().to_owned();
// Media isn't present in the cache.
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
// Add the media.
self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media failed");
// Sanity-check: media is found after adding it.
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
// Replacing a media request works.
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
let new_req = MediaRequestParameters {
source: MediaSource::Plain(new_uri.to_owned()),
format: MediaFormat::File,
};
self.replace_media_key(&req, &new_req)
.await
.expect("replacing the media request key failed");
// Finding with the previous request doesn't work anymore.
assert!(
self.get_media_content(&req).await.unwrap().is_none(),
"unexpected media found with the old key"
);
// Finding with the new request does work.
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
}
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
let room_id = room_id!("!r0:matrix.org");
self.handle_linked_chunk_updates(
room_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![
make_test_event(room_id, "hello"),
make_test_event(room_id, "world"),
],
},
// a gap chunk
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "parmesan".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(room_id, "sup")],
},
],
)
.await
.unwrap();
// The linked chunk is correctly reloaded.
let raws = self.reload_linked_chunk(room_id).await.unwrap();
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
let mut chunks = lc.chunks();
{
let first = chunks.next().unwrap();
// Note: we can't assert the previous/next chunks, as these fields and their
// getters are private.
assert_eq!(first.identifier(), CId::new(0));
assert_matches!(first.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 2);
check_test_event(&events[0], "hello");
check_test_event(&events[1], "world");
});
}
{
let second = chunks.next().unwrap();
assert_eq!(second.identifier(), CId::new(1));
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "parmesan");
});
}
{
let third = chunks.next().unwrap();
assert_eq!(third.identifier(), CId::new(2));
assert_matches!(third.content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
check_test_event(&events[0], "sup");
});
}
assert!(chunks.next().is_none());
}
async fn test_rebuild_empty_linked_chunk(&self) {
// When I rebuild a linked chunk from an empty store, it's empty.
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
assert!(rebuild_linked_chunk(raw_parts).is_none());
}
async fn test_clear_all_rooms_chunks(&self) {
let r0 = room_id!("!r0:matrix.org");
let r1 = room_id!("!r1:matrix.org");
// Add updates for the first room.
self.handle_linked_chunk_updates(
r0,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
},
],
)
.await
.unwrap();
// Add updates for the second room.
self.handle_linked_chunk_updates(
r1,
vec![
// Empty items chunk.
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// a gap chunk
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(2), 0),
items: vec![make_test_event(r0, "yummy")],
},
],
)
.await
.unwrap();
// Sanity check: both linked chunks can be reloaded.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
// Clear the chunks.
self.clear_all_rooms_chunks().await.unwrap();
// Both rooms now have no linked chunk.
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
}
async fn test_remove_room(&self) {
let r0 = room_id!("!r0:matrix.org");
let r1 = room_id!("!r1:matrix.org");
// Add updates to the first room.
self.handle_linked_chunk_updates(
r0,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
},
],
)
.await
.unwrap();
// Add updates to the second room.
self.handle_linked_chunk_updates(
r1,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec![make_test_event(r0, "yummy")],
},
],
)
.await
.unwrap();
// Try to remove content from r0.
self.remove_room(r0).await.unwrap();
// Check that r0 doesn't have a linked chunk anymore.
let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
assert!(r0_linked_chunk.is_empty());
// Check that r1 is unaffected.
let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
assert!(!r1_linked_chunk.is_empty());
}
}
/// Macro building to allow your `EventCacheStore` implementation to run the
/// entire tests suite locally.
///
/// You need to provide a `async fn get_event_cache_store() ->
/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
/// store on the same level you invoke the macro.
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache::store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
/// # };
///
/// #[cfg(test)]
/// mod tests {
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
///
/// async fn get_event_cache_store(
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
/// Ok(MyStore::new())
/// }
///
/// event_cache_store_integration_tests!();
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! event_cache_store_integration_tests {
() => {
mod event_cache_store_integration_tests {
use matrix_sdk_test::async_test;
use $crate::event_cache::store::{
EventCacheStoreIntegrationTests, IntoEventCacheStore,
};
use super::get_event_cache_store;
#[async_test]
async fn test_media_content() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_media_content().await;
}
#[async_test]
async fn test_replace_media_key() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_replace_media_key().await;
}
#[async_test]
async fn test_handle_updates_and_rebuild_linked_chunk() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
}
#[async_test]
async fn test_rebuild_empty_linked_chunk() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_rebuild_empty_linked_chunk().await;
}
#[async_test]
async fn test_clear_all_rooms_chunks() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_clear_all_rooms_chunks().await;
}
#[async_test]
async fn test_remove_room() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_remove_room().await;
}
}
};
}
/// Macro generating tests for the event cache store, related to time (mostly
/// for the cross-process lock).
#[allow(unused_macros)]
#[macro_export]
macro_rules! event_cache_store_integration_tests_time {
() => {
#[cfg(not(target_arch = "wasm32"))]
mod event_cache_store_integration_tests_time {
use std::time::Duration;
use matrix_sdk_test::async_test;
use $crate::event_cache::store::IntoEventCacheStore;
use super::get_event_cache_store;
#[async_test]
async fn test_lease_locks() {
let store = get_event_cache_store().await.unwrap().into_event_cache_store();
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert!(acquired0);
// Should extend the lease automatically (same holder).
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(acquired2);
// Should extend the lease automatically (same holder + time is ok).
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(acquired3);
// Another attempt at taking the lock should fail, because it's taken.
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired4);
// Even if we insist.
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired5);
// That's a nice test we got here, go take a little nap.
tokio::time::sleep(Duration::from_millis(50)).await;
// Still too early.
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired55);
// Ok you can take another nap then.
tokio::time::sleep(Duration::from_millis(250)).await;
// At some point, we do get the lock.
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
assert!(acquired6);
tokio::time::sleep(Duration::from_millis(1)).await;
// The other gets it almost immediately too.
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert!(acquired7);
tokio::time::sleep(Duration::from_millis(1)).await;
// But when we take a longer lease...
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired8);
// It blocks the other user.
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(!acquired9);
// We can hold onto our lease.
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired10);
}
}
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,330 @@
// Copyright 2025 Kévin Commaille
//
// 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.
//! Configuration to decide whether or not to keep media in the cache, allowing
//! to do periodic cleanups to avoid to have the size of the media cache grow
//! indefinitely.
//!
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
//! [`EventCacheStore::clean_up_media_cache()`].
//!
//! In the future, other settings will allow to run automatic periodic cleanup
//! jobs.
//!
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
use ruma::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
/// The retention policy for media content used by the [`EventCacheStore`].
///
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MediaRetentionPolicy {
/// The maximum authorized size of the overall media cache, in bytes.
///
/// The cache size is defined as the sum of the sizes of all the (possibly
/// encrypted) media contents in the cache, excluding any metadata
/// associated with them.
///
/// If this is set and the cache size is bigger than this value, the oldest
/// media contents in the cache will be removed during a cleanup until the
/// cache size is below this threshold.
///
/// Note that it is possible for the cache size to temporarily exceed this
/// value between two cleanups.
///
/// Defaults to 400 MiB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cache_size: Option<usize>,
/// The maximum authorized size of a single media content, in bytes.
///
/// The size of a media content is the size taken by the content in the
/// database, after it was possibly encrypted, so it might differ from the
/// initial size of the content.
///
/// The maximum authorized size of a single media content is actually the
/// lowest value between `max_cache_size` and `max_file_size`.
///
/// If it is set, media content bigger than the maximum size will not be
/// cached. If the maximum size changed after media content that exceeds the
/// new value was cached, the corresponding content will be removed
/// during a cleanup.
///
/// Defaults to 20 MiB.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_file_size: Option<usize>,
/// The duration after which unaccessed media content is considered
/// expired.
///
/// If this is set, media content whose last access is older than this
/// duration will be removed from the media cache during a cleanup.
///
/// Defaults to 60 days.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_access_expiry: Option<Duration>,
}
impl MediaRetentionPolicy {
/// Create a [`MediaRetentionPolicy`] with the default values.
pub fn new() -> Self {
Self::default()
}
/// Create an empty [`MediaRetentionPolicy`].
///
/// This means that all media will be cached and cleanups have no effect.
pub fn empty() -> Self {
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
}
/// Set the maximum authorized size of the overall media cache, in bytes.
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
self.max_cache_size = size;
self
}
/// Set the maximum authorized size of a single media content, in bytes.
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
self.max_file_size = size;
self
}
/// Set the duration before which unaccessed media content is considered
/// expired.
pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
self.last_access_expiry = duration;
self
}
/// Whether this policy has limitations.
///
/// If this policy has no limitations, a cleanup job would have no effect.
///
/// Returns `true` if at least one limitation is set.
pub fn has_limitations(&self) -> bool {
self.max_cache_size.is_some()
|| self.max_file_size.is_some()
|| self.last_access_expiry.is_some()
}
/// Whether the given size exceeds the maximum authorized size of the media
/// cache.
///
/// # Arguments
///
/// * `size` - The overall size of the media cache to check, in bytes.
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
self.max_cache_size.is_some_and(|max_size| size > max_size)
}
/// The computed maximum authorized size of a single media content, in
/// bytes.
///
/// This is the lowest value between `max_cache_size` and `max_file_size`.
pub fn computed_max_file_size(&self) -> Option<usize> {
match (self.max_cache_size, self.max_file_size) {
(None, None) => None,
(None, Some(size)) => Some(size),
(Some(size), None) => Some(size),
(Some(max_cache_size), Some(max_file_size)) => Some(max_cache_size.min(max_file_size)),
}
}
/// Whether the given size, in bytes, exceeds the computed maximum
/// authorized size of a single media content.
///
/// # Arguments
///
/// * `size` - The size of the media content to check, in bytes.
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
}
/// Whether a content whose last access was at the given time has expired.
///
/// # Arguments
///
/// * `current_time` - The current time.
///
/// * `last_access_time` - The time when the media content to check was last
/// accessed.
pub fn has_content_expired(
&self,
current_time: SystemTime,
last_access_time: SystemTime,
) -> bool {
self.last_access_expiry.is_some_and(|max_duration| {
current_time
.duration_since(last_access_time)
// If this returns an error, the last access time is newer than the current time.
// This shouldn't happen but in this case the content cannot be expired.
.is_ok_and(|elapsed| elapsed >= max_duration)
})
}
}
impl Default for MediaRetentionPolicy {
fn default() -> Self {
Self {
// 400 MiB.
max_cache_size: Some(400 * 1024 * 1024),
// 20 MiB.
max_file_size: Some(20 * 1024 * 1024),
// 60 days.
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
}
}
}
#[cfg(test)]
mod tests {
use ruma::time::{Duration, SystemTime};
use super::MediaRetentionPolicy;
#[test]
fn test_media_retention_policy_has_limitations() {
let mut policy = MediaRetentionPolicy::empty();
assert!(!policy.has_limitations());
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
assert!(policy.has_limitations());
policy = policy.with_last_access_expiry(None);
assert!(!policy.has_limitations());
policy = policy.with_max_cache_size(Some(1_024));
assert!(policy.has_limitations());
policy = policy.with_max_cache_size(None);
assert!(!policy.has_limitations());
policy = policy.with_max_file_size(Some(1_024));
assert!(policy.has_limitations());
policy = policy.with_max_file_size(None);
assert!(!policy.has_limitations());
// With default values.
assert!(MediaRetentionPolicy::new().has_limitations());
}
#[test]
fn test_media_retention_policy_max_cache_size() {
let file_size = 2_048;
let mut policy = MediaRetentionPolicy::empty();
assert!(!policy.exceeds_max_cache_size(file_size));
assert_eq!(policy.computed_max_file_size(), None);
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_cache_size(Some(4_096));
assert!(!policy.exceeds_max_cache_size(file_size));
assert_eq!(policy.computed_max_file_size(), Some(4_096));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_cache_size(Some(2_048));
assert!(!policy.exceeds_max_cache_size(file_size));
assert_eq!(policy.computed_max_file_size(), Some(2_048));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_cache_size(Some(1_024));
assert!(policy.exceeds_max_cache_size(file_size));
assert_eq!(policy.computed_max_file_size(), Some(1_024));
assert!(policy.exceeds_max_file_size(file_size));
}
#[test]
fn test_media_retention_policy_max_file_size() {
let file_size = 2_048;
let mut policy = MediaRetentionPolicy::empty();
assert_eq!(policy.computed_max_file_size(), None);
assert!(!policy.exceeds_max_file_size(file_size));
// With max_file_size only.
policy = policy.with_max_file_size(Some(4_096));
assert_eq!(policy.computed_max_file_size(), Some(4_096));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_file_size(Some(2_048));
assert_eq!(policy.computed_max_file_size(), Some(2_048));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_file_size(Some(1_024));
assert_eq!(policy.computed_max_file_size(), Some(1_024));
assert!(policy.exceeds_max_file_size(file_size));
// With max_cache_size as well.
policy = policy.with_max_cache_size(Some(2_048));
assert_eq!(policy.computed_max_file_size(), Some(1_024));
assert!(policy.exceeds_max_file_size(file_size));
policy = policy.with_max_file_size(Some(2_048));
assert_eq!(policy.computed_max_file_size(), Some(2_048));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_file_size(Some(4_096));
assert_eq!(policy.computed_max_file_size(), Some(2_048));
assert!(!policy.exceeds_max_file_size(file_size));
policy = policy.with_max_cache_size(Some(1_024));
assert_eq!(policy.computed_max_file_size(), Some(1_024));
assert!(policy.exceeds_max_file_size(file_size));
}
#[test]
fn test_media_retention_policy_has_content_expired() {
let epoch = SystemTime::UNIX_EPOCH;
let last_access_time = epoch + Duration::from_secs(30);
let epoch_plus_60 = epoch + Duration::from_secs(60);
let epoch_plus_120 = epoch + Duration::from_secs(120);
let mut policy = MediaRetentionPolicy::empty();
assert!(!policy.has_content_expired(epoch, last_access_time));
assert!(!policy.has_content_expired(last_access_time, last_access_time));
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
assert!(!policy.has_content_expired(epoch, last_access_time));
assert!(!policy.has_content_expired(last_access_time, last_access_time));
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
assert!(!policy.has_content_expired(epoch, last_access_time));
assert!(!policy.has_content_expired(last_access_time, last_access_time));
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
assert!(!policy.has_content_expired(epoch, last_access_time));
assert!(!policy.has_content_expired(last_access_time, last_access_time));
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
assert!(!policy.has_content_expired(epoch, last_access_time));
assert!(policy.has_content_expired(last_access_time, last_access_time));
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
}
}
@@ -0,0 +1,884 @@
// Copyright 2025 Kévin Commaille
//
// 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;
use async_trait::async_trait;
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
use ruma::{time::SystemTime, MxcUri};
use tokio::sync::Mutex as AsyncMutex;
use super::MediaRetentionPolicy;
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
/// API for implementors of [`EventCacheStore`] to manage their media through
/// their implementation of [`EventCacheStoreMedia`].
///
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
#[derive(Debug)]
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
/// The time provider.
time_provider: Time,
/// The current [`MediaRetentionPolicy`].
policy: Mutex<MediaRetentionPolicy>,
/// A mutex to ensure a single cleanup is running at a time.
cleanup_guard: AsyncMutex<()>,
}
impl MediaService {
/// Construct a new default `MediaService`.
///
/// [`MediaService::restore()`] should be called after constructing the
/// `MediaService` to restore its previous state.
pub fn new() -> Self {
Self::default()
}
}
impl Default for MediaService {
fn default() -> Self {
Self::with_time_provider(DefaultTimeProvider)
}
}
impl<Time> MediaService<Time>
where
Time: TimeProvider,
{
/// Construct a new `MediaService` with the given `TimeProvider` and an
/// empty `MediaRetentionPolicy`.
fn with_time_provider(time_provider: Time) -> Self {
Self {
time_provider,
policy: Mutex::new(MediaRetentionPolicy::empty()),
cleanup_guard: AsyncMutex::new(()),
}
}
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
/// that was persisted in the store.
///
/// This should be called immediately after constructing the `MediaService`.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
if let Some(policy) = policy {
*self.policy.lock() = policy;
}
}
/// Set the `MediaRetentionPolicy` of this service.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `policy` - The `MediaRetentionPolicy` to use.
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
&self,
store: &Store,
policy: MediaRetentionPolicy,
) -> Result<(), Store::Error> {
store.set_media_retention_policy_inner(policy).await?;
*self.policy.lock() = policy;
Ok(())
}
/// Get the `MediaRetentionPolicy` of this service.
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
*self.policy.lock()
}
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `content` - The content of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
pub async fn add_media_content<Store: EventCacheStoreMedia>(
&self,
store: &Store,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Store::Error> {
let policy = self.media_retention_policy();
if ignore_policy == IgnoreMediaRetentionPolicy::No
&& policy.exceeds_max_file_size(content.len())
{
// We do not cache the content.
return Ok(());
}
store
.add_media_content_inner(
request,
content,
self.time_provider.now(),
policy,
ignore_policy,
)
.await
}
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
pub async fn set_ignore_media_retention_policy<Store: EventCacheStoreMedia>(
&self,
store: &Store,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Store::Error> {
store.set_ignore_media_retention_policy_inner(request, ignore_policy).await
}
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `request` - The `MediaRequestParameters` of the file.
pub async fn get_media_content<Store: EventCacheStoreMedia>(
&self,
store: &Store,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Store::Error> {
store.get_media_content_inner(request, self.time_provider.now()).await
}
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
///
/// * `uri` - The `MxcUri` of the media file.
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
&self,
store: &Store,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Store::Error> {
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
}
/// Clean up the media cache with the current `MediaRetentionPolicy`.
///
/// If there is already an ongoing cleanup, this is a noop.
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
&self,
store: &Store,
) -> Result<(), Store::Error> {
let Ok(_guard) = self.cleanup_guard.try_lock() else {
// There is another ongoing cleanup.
return Ok(());
};
let policy = self.media_retention_policy();
if !policy.has_limitations() {
// No need to call the backend.
return Ok(());
}
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
}
}
/// An abstract trait that can be used to implement different store backends
/// for the media cache of the SDK.
///
/// The main purposes of this trait are to be able to centralize where we handle
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
/// simplify the implementation of tests by being able to have complete control
/// over the `SystemTime`s provided to the store.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreMedia: AsyncTraitDeps {
/// The error type used by this media cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
/// The persisted media retention policy in the media cache.
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
/// Persist the media retention policy in the media cache.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to persist.
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Add a media file's content in the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `content` - The content of the file.
///
/// * `current_time` - The current time, to set the last access time of the
/// media.
///
/// * `policy` - The media retention policy, to check whether the media is
/// too big to be cached.
///
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
/// for this media. This setting should be persisted alongside the media
/// and taken into account whenever the policy is used.
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
current_time: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// If the media of the given request is not found, this should be a noop.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_for_uri_inner(
&self,
uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Clean up the media cache with the given policy.
///
/// For the integration tests, it is expected that content that does not
/// pass the last access expiry and max file size criteria will be
/// removed first. After that, the remaining cache size should be
/// computed to compare against the max cache size criteria.
///
/// # Arguments
///
/// * `policy` - The media retention policy to use for the cleanup. The
/// `cleanup_frequency` will be ignored.
///
/// * `current_time` - The current time, to be used to check for expired
/// content.
async fn clean_up_media_cache_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error>;
}
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
/// content.
///
/// Some media cache actions are noops when the media content that is processed
/// is filtered out by the policy. This can break some features of the SDK, like
/// the send queue, that expects to be able to persist all media files in the
/// store to restore them when the client is restored.
///
/// This can be converted to a boolean with
/// [`IgnoreMediaRetentionPolicy::is_yes()`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IgnoreMediaRetentionPolicy {
/// The media retention policy will be ignored and the current action will
/// not be a noop.
///
/// Any media content in this state must NOT be used when applying a
/// `MediaRetentionPolicy`. This applies to ANY criteria, like the maximum
/// file size, the maximum cache size or the last access expiry.
///
/// This state is supposed to be transient, and to only be used internally
/// by the SDK.
Yes,
/// The media retention policy will be respected and the current action
/// might be a noop.
No,
}
impl IgnoreMediaRetentionPolicy {
/// Whether this is an [`IgnoreMediaRetentionPolicy::Yes`] variant.
pub fn is_yes(self) -> bool {
matches!(self, Self::Yes)
}
}
/// An abstract trait to provide the current `SystemTime` for the
/// [`MediaService`].
pub trait TimeProvider {
/// The current time.
fn now(&self) -> SystemTime;
}
/// The default time provider, that calls `ruma::time::SystemTime::now()`.
#[derive(Debug)]
pub struct DefaultTimeProvider;
impl TimeProvider for DefaultTimeProvider {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
#[cfg(test)]
mod tests {
use std::{fmt, sync::MutexGuard};
use async_trait::async_trait;
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_test::async_test;
use ruma::{
events::room::MediaSource,
mxc_uri,
time::{Duration, SystemTime},
MxcUri, OwnedMxcUri,
};
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
use crate::{
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
media::{MediaFormat, MediaRequestParameters, UniqueKey},
};
#[derive(Debug, Default)]
struct MockEventCacheStoreMedia {
inner: Mutex<MockEventCacheStoreMediaInner>,
}
impl MockEventCacheStoreMedia {
/// Whether the store was accessed.
fn accessed(&self) -> bool {
self.inner.lock().accessed
}
/// Reset the `accessed` boolean.
fn reset_accessed(&self) {
self.inner.lock().accessed = false;
}
/// Access the inner store.
///
/// Should be called for every access to the inner store as it also sets
/// the `accessed` boolean.
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
let mut inner = self.inner.lock();
inner.accessed = true;
inner
}
}
#[derive(Debug, Default)]
struct MockEventCacheStoreMediaInner {
/// Whether this store was accessed.
///
/// Must be set to `true` for any operation that unlocks the store.
accessed: bool,
/// The persisted media retention policy.
media_retention_policy: Option<MediaRetentionPolicy>,
/// The list of media content.
media_list: Vec<MediaContent>,
/// The time of the last cleanup.
cleanup_time: Option<SystemTime>,
}
#[derive(Debug, Clone)]
struct MediaContent {
/// The unique key for the media content.
key: String,
/// The original URI of the media content.
uri: OwnedMxcUri,
/// The media content.
content: Vec<u8>,
/// Whether the `MediaRetentionPolicy` should be ignored for this media
/// content;
ignore_policy: bool,
/// The time of the last access of the media content.
last_access: SystemTime,
}
#[derive(Debug)]
struct MockEventCacheStoreMediaError;
impl fmt::Display for MockEventCacheStoreMediaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MockEventCacheStoreMediaError")
}
}
impl std::error::Error for MockEventCacheStoreMediaError {}
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
fn from(value: MockEventCacheStoreMediaError) -> Self {
Self::backend(value)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
type Error = MockEventCacheStoreMediaError;
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
Ok(self.inner().media_retention_policy)
}
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.inner().media_retention_policy = Some(policy);
Ok(())
}
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
current_time: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
return Ok(());
}
let mut inner = self.inner();
let key = request.unique_key();
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
let media_content = &mut inner.media_list[pos];
media_content.content = content;
media_content.last_access = current_time;
media_content.ignore_policy = ignore_policy;
} else {
inner.media_list.push(MediaContent {
key,
uri: request.uri().to_owned(),
content,
ignore_policy,
last_access: current_time,
});
}
Ok(())
}
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
let key = request.unique_key();
let mut inner = self.inner();
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
inner.media_list[pos].ignore_policy = ignore_policy.is_yes();
}
Ok(())
}
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let key = request.unique_key();
let mut inner = self.inner();
let Some(media_content) =
inner.media_list.iter_mut().find(|content| content.key == key)
else {
return Ok(None);
};
media_content.last_access = current_time;
Ok(Some(media_content.content.clone()))
}
async fn get_media_content_for_uri_inner(
&self,
uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner();
let Some(media_content) =
inner.media_list.iter_mut().find(|content| content.uri == uri)
else {
return Ok(None);
};
media_content.last_access = current_time;
Ok(Some(media_content.content.clone()))
}
async fn clean_up_media_cache_inner(
&self,
_policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error> {
// This is mostly a noop. We don't care about this test implementation, only
// whether this method was called with the right time.
self.inner().cleanup_time = Some(current_time);
Ok(())
}
}
#[derive(Debug)]
struct MockTimeProvider {
now: Mutex<SystemTime>,
}
impl MockTimeProvider {
/// Construct a `MockTimeProvider` with the given current time.
fn new(now: SystemTime) -> Self {
Self { now: Mutex::new(now) }
}
/// Set the current time.
fn set_now(&self, now: SystemTime) {
*self.now.lock() = now;
}
}
impl TimeProvider for MockTimeProvider {
fn now(&self) -> SystemTime {
*self.now.lock()
}
}
#[async_test]
async fn test_media_service_empty_policy() {
let content = b"some text content";
let uri = mxc_uri!("mxc://server.local/AbcDe1234");
let request = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// By default an empty policy is used.
assert!(!service.media_retention_policy().has_limitations());
service.restore(None);
assert!(!service.media_retention_policy().has_limitations());
assert!(!store.accessed());
// Add media.
service
.add_media_content(&store, &request, content.to_vec(), IgnoreMediaRetentionPolicy::No)
.await
.unwrap();
assert!(store.accessed());
let media_content = store.inner().media_list[0].clone();
assert_eq!(media_content.uri, uri);
assert_eq!(media_content.content, content);
assert!(!media_content.ignore_policy);
assert_eq!(media_content.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Get media from request.
let loaded_content = service.get_media_content(&store, &request).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[0].clone();
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
let loaded_content = service.get_media_content_for_uri(&store, uri).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[0].clone();
assert_eq!(media.last_access, now);
// Update ignore_policy.
service
.set_ignore_media_retention_policy(&store, &request, IgnoreMediaRetentionPolicy::Yes)
.await
.unwrap();
assert!(store.accessed());
let media_content = store.inner().media_list[0].clone();
assert!(media_content.ignore_policy);
// Try a cleanup. With the empty policy the store should not be accessed.
assert_eq!(store.inner().cleanup_time, None);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
assert!(!store.accessed());
assert_eq!(store.inner().cleanup_time, None);
}
#[async_test]
async fn test_media_service_non_empty_policy() {
// Content of less than 32 bytes.
let small_content = b"some text content";
let small_uri = mxc_uri!("mxc://server.local/small");
let small_request = MediaRequestParameters {
source: MediaSource::Plain(small_uri.to_owned()),
format: MediaFormat::File,
};
// Content of more than 32 bytes.
let big_content = b"some much much larger text content";
let big_uri = mxc_uri!("mxc://server.local/big");
let big_request = MediaRequestParameters {
source: MediaSource::Plain(big_uri.to_owned()),
format: MediaFormat::File,
};
// Limit the file size to 32 bytes in the retention policy.
let policy = MediaRetentionPolicy { max_file_size: Some(32), ..Default::default() };
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// Check that restoring the policy works.
service.restore(Some(MediaRetentionPolicy::default()));
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
assert!(!store.accessed());
// Set the media retention policy.
service.set_media_retention_policy(&store, policy).await.unwrap();
assert!(store.accessed());
assert_eq!(service.media_retention_policy(), policy);
assert_eq!(store.inner().media_retention_policy, Some(policy));
store.reset_accessed();
// Add small media, it should work because its size is lower than the max file
// size.
service
.add_media_content(
&store,
&small_request,
small_content.to_vec(),
IgnoreMediaRetentionPolicy::No,
)
.await
.unwrap();
assert!(store.accessed());
let media_content = store.inner().media_list[0].clone();
assert_eq!(media_content.uri, small_uri);
assert_eq!(media_content.content, small_content);
assert!(!media_content.ignore_policy);
assert_eq!(media_content.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Get media from request.
let loaded_content = service.get_media_content(&store, &small_request).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[0].clone();
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
let loaded_content = service.get_media_content_for_uri(&store, small_uri).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[0].clone();
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Add big media, it will not work because it is bigger than the max file size.
service
.add_media_content(
&store,
&big_request,
big_content.to_vec(),
IgnoreMediaRetentionPolicy::No,
)
.await
.unwrap();
assert!(!store.accessed());
assert_eq!(store.inner().media_list.len(), 1);
store.reset_accessed();
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content, None);
store.reset_accessed();
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content, None);
// Add big media, but this time ignore the policy.
service
.add_media_content(
&store,
&big_request,
big_content.to_vec(),
IgnoreMediaRetentionPolicy::Yes,
)
.await
.unwrap();
assert!(store.accessed());
assert_eq!(store.inner().media_list.len(), 2);
store.reset_accessed();
// Get media from request.
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[1].clone();
assert_eq!(media.last_access, now);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
// Get media from URI.
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
assert!(store.accessed());
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
// The last access time was updated.
let media = store.inner().media_list[1].clone();
assert_eq!(media.last_access, now);
// Try a cleanup, the store should be accessed.
assert_eq!(store.inner().cleanup_time, None);
let now = now + Duration::from_secs(60);
service.time_provider.set_now(now);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
assert!(store.accessed());
assert_eq!(store.inner().cleanup_time, Some(now));
}
}
@@ -0,0 +1,28 @@
// Copyright 2025 Kévin Commaille
//
// 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.
//! Types and traits regarding media caching of the event cache store.
mod media_retention_policy;
mod media_service;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::EventCacheStoreMediaIntegrationTests;
pub use self::{
media_retention_policy::MediaRetentionPolicy,
media_service::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService},
};
@@ -0,0 +1,450 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{
time::{Instant, SystemTime},
MxcUri, OwnedMxcUri, RoomId,
};
use super::{
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
EventCacheStore, EventCacheStoreError, Result,
};
use crate::{
event_cache::{Event, Gap},
media::{MediaRequestParameters, UniqueKey as _},
};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct MemoryStore {
inner: StdRwLock<MemoryStoreInner>,
media_service: MediaService,
}
#[derive(Debug)]
struct MemoryStoreInner {
media: RingBuffer<MediaContent>,
leases: HashMap<String, (String, Instant)>,
events: RelationalLinkedChunk<Event, Gap>,
media_retention_policy: Option<MediaRetentionPolicy>,
}
/// A media content in the `MemoryStore`.
#[derive(Debug)]
struct MediaContent {
/// The URI of the content.
uri: OwnedMxcUri,
/// The unique key of the content.
key: String,
/// The bytes of the content.
data: Vec<u8>,
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
ignore_policy: bool,
/// The time of the last access of the content.
last_access: SystemTime,
}
// 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 {
inner: StdRwLock::new(MemoryStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
events: RelationalLinkedChunk::new(),
media_retention_policy: None,
}),
// No need to call `restore()` since nothing is persisted.
media_service: MediaService::new(),
}
}
}
impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Self::default()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStore for MemoryStore {
type Error = EventCacheStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
let mut inner = self.inner.write().unwrap();
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
}
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
inner.events.apply_updates(room_id, updates);
Ok(())
}
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
let inner = self.inner.read().unwrap();
inner
.events
.reload_chunks(room_id)
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.inner.write().unwrap().events.clear();
Ok(())
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<()> {
self.media_service.add_media_content(self, request, data, ignore_policy).await
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut inner = self.inner.write().unwrap();
if let Some(media_content) =
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
{
media_content.uri = to.uri().to_owned();
media_content.key = to.unique_key();
}
Ok(())
}
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
self.media_service.get_media_content(self, request).await
}
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
let expected_key = request.unique_key();
let mut inner = self.inner.write().unwrap();
let Some(index) =
inner.media.iter().position(|media_content| media_content.key == expected_key)
else {
return Ok(());
};
inner.media.remove(index);
Ok(())
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.media_service.get_media_content_for_uri(self, uri).await
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut inner = self.inner.write().unwrap();
let positions = inner
.media
.iter()
.enumerate()
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
inner.media.remove(position);
}
Ok(())
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_media_retention_policy(self, policy).await
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.media_service.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
}
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
self.media_service.clean_up_media_cache(self).await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreMedia for MemoryStore {
type Error = EventCacheStoreError;
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
Ok(self.inner.read().unwrap().media_retention_policy)
}
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.inner.write().unwrap().media_retention_policy = Some(policy);
Ok(())
}
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
last_access: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
// Do not store it.
return Ok(());
};
// Now, let's add it.
let mut inner = self.inner.write().unwrap();
inner.media.push(MediaContent {
uri: request.uri().to_owned(),
key: request.unique_key(),
data,
ignore_policy,
last_access,
});
Ok(())
}
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
{
media_content.ignore_policy = ignore_policy.is_yes();
}
Ok(())
}
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn get_media_content_for_uri_inner(
&self,
expected_uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn clean_up_media_cache_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error> {
if !policy.has_limitations() {
// We can safely skip all the checks.
return Ok(());
}
let mut inner = self.inner.write().unwrap();
// First, check media content that exceed the max filesize.
if policy.computed_max_file_size().is_some() {
inner.media.retain(|content| {
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len())
});
}
// Then, clean up expired media content.
if policy.last_access_expiry.is_some() {
inner.media.retain(|content| {
content.ignore_policy
|| !policy.has_content_expired(current_time, content.last_access)
});
}
// Finally, if the cache size is too big, remove old items until it fits.
if let Some(max_cache_size) = policy.max_cache_size {
// Reverse the iterator because in case the cache size is overflowing, we want
// to count the number of old items to remove. Items are sorted by last access
// and old items are at the start.
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|(mut cache_size, mut items_to_remove), (index, content)| {
if content.ignore_policy {
// Do not count it.
return (cache_size, items_to_remove);
}
let remove_item = if items_to_remove.is_empty() {
// We have not reached the max cache size yet.
if let Some(sum) = cache_size.checked_add(content.data.len()) {
cache_size = sum;
// Start removing items if we have exceeded the max cache size.
cache_size > max_cache_size
} else {
// The cache size is overflowing, remove the remaining items, since the
// max cache size cannot be bigger than
// usize::MAX.
true
}
} else {
// We have reached the max cache size already, just remove it.
true
};
if remove_item {
items_to_remove.push(index);
}
(cache_size, items_to_remove)
},
);
// The indexes are already in reverse order so we can just iterate in that order
// to remove them starting by the end.
for index in items_to_remove {
inner.media.remove(index);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{MemoryStore, Result};
use crate::event_cache_store_media_integration_tests;
async fn get_event_cache_store() -> Result<MemoryStore> {
Ok(MemoryStore::new())
}
event_cache_store_integration_tests!();
event_cache_store_integration_tests_time!();
event_cache_store_media_integration_tests!(with_media_size_tests);
}
@@ -0,0 +1,195 @@
// 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.
//! The event cache stores holds events and downloaded media when the cache was
//! activated to save bandwidth at the cost of increased storage space usage.
//!
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
//! into the event cache for the actual storage. By default this brings an
//! in-memory store.
use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
pub mod media;
mod memory_store;
mod traits;
use matrix_sdk_common::store_locks::{
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::EventCacheStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
};
/// The high-level public type to represent an `EventCacheStore` lock.
#[derive(Clone)]
pub struct EventCacheStoreLock {
/// The inner cross process lock that is used to lock the `EventCacheStore`.
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
/// The store itself.
///
/// That's the only place where the store exists.
store: Arc<DynEventCacheStore>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for EventCacheStoreLock {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive()
}
}
impl EventCacheStoreLock {
/// Create a new lock around the [`EventCacheStore`].
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessStoreLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
where
S: IntoEventCacheStore,
{
let store = store.into_event_cache_store();
Self {
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
)),
store,
}
}
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
}
}
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
/// When this structure is dropped (falls out of scope), the lock will be
/// unlocked.
pub struct EventCacheStoreLockGuard<'a> {
/// The cross process lock guard.
#[allow(unused)]
cross_process_lock_guard: CrossProcessStoreLockGuard,
/// A reference to the store.
store: &'a DynEventCacheStore,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
}
}
impl Deref for EventCacheStoreLockGuard<'_> {
type Target = DynEventCacheStore;
fn deref(&self) -> &Self::Target {
self.store
}
}
/// Event cache store specific error type.
#[derive(Debug, thiserror::Error)]
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
#[error("The event cache store failed to be unlocked")]
Locked,
/// An unencrypted store was tried to be unlocked with a passphrase.
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
Unencrypted,
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
Encryption(#[from] StoreEncryptionError),
/// The store failed to encode or decode some data.
#[error("Error encoding or decoding data from the event cache store: {0}")]
Codec(#[from] Utf8Error),
/// The store failed to serialize or deserialize some data.
#[error("Error serializing or deserializing data from the event cache store: {0}")]
Serialization(#[from] serde_json::Error),
/// The database format has changed in a backwards incompatible way.
#[error(
"The database format of the event cache store changed in an incompatible way, \
current version: {0}, latest version: {1}"
)]
UnsupportedDatabaseVersion(usize, usize),
/// The store contains invalid data.
#[error("The store contains invalid data: {details}")]
InvalidData {
/// Details why the data contained in the store was invalid.
details: String,
},
}
impl EventCacheStoreError {
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
}
/// An `EventCacheStore` specific result type.
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
/// make it usable inside the cross process lock.
#[derive(Clone, Debug)]
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BackingStore for LockableEventCacheStore {
type LockError = EventCacheStoreError;
async fn try_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> std::result::Result<bool, Self::LockError> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
@@ -0,0 +1,351 @@
// 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, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
linked_chunk::{RawChunk, Update},
AsyncTraitDeps,
};
use ruma::{MxcUri, RoomId};
use super::{
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
EventCacheStoreError,
};
use crate::{
event_cache::{Event, Gap},
media::MediaRequestParameters,
};
/// A default capacity for linked chunks, when manipulating in conjunction with
/// an `EventCacheStore` implementation.
// TODO: move back?
pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStore: AsyncTraitDeps {
/// The error type used by this event cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
/// Try to take a lock using the given store.
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error>;
/// An [`Update`] reflects an operation that has happened inside a linked
/// chunk. The linked chunk is used by the event cache to store the events
/// in-memory. This method aims at forwarding this update inside this store.
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error>;
/// Remove all data tied to a given room from the cache.
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
// Right now, this means removing all the linked chunk. If implementations
// override this behavior, they should *also* include this code.
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
}
/// Return all the raw components of a linked chunk, so the caller may
/// reconstruct the linked chunk later.
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
/// Clear persisted events for all the rooms.
///
/// This will empty and remove all the linked chunks stored previously,
/// using the above [`Self::handle_linked_chunk_updates`] methods.
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Replaces the given media's content key with another one.
///
/// This should be used whenever a temporary (local) MXID has been used, and
/// it must now be replaced with its actual remote counterpart (after
/// uploading some content, or creating an empty MXC URI).
///
/// ⚠ No check is performed to ensure that the media formats are consistent,
/// i.e. it's possible to update with a thumbnail key a media that was
/// keyed as a file before. The caller is responsible of ensuring that
/// the replacement makes sense, according to their use case.
///
/// This should not raise an error when the `from` parameter points to an
/// unknown media, and it should silently continue in this case.
///
/// # Arguments
///
/// * `from` - The previous `MediaRequest` of the file.
///
/// * `to` - The new `MediaRequest` of the file.
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// In theory, there could be several files stored using the same URI and a
/// different `MediaFormat`. This API is meant to be used with a media file
/// that has only been stored with a single format.
///
/// If there are several media files for a given URI in different formats,
/// this API will only return one of them. Which one is left as an
/// implementation detail.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
async fn get_media_content_for_uri(&self, uri: &MxcUri)
-> Result<Option<Vec<u8>>, Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// This should not raise an error when the `uri` parameter points to an
/// unknown media, and it should return an Ok result in this case.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
/// keep media content.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to use.
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get the current `MediaRetentionPolicy`.
fn media_retention_policy(&self) -> MediaRetentionPolicy;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Clean up the media cache with the current `MediaRetentionPolicy`.
///
/// If there is already an ongoing cleanup, this is a noop.
async fn clean_up_media_cache(&self) -> Result<(), Self::Error>;
}
#[repr(transparent)]
struct EraseEventCacheStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
type Error = EventCacheStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
}
async fn handle_linked_chunk_updates(
&self,
room_id: &RoomId,
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
}
async fn reload_linked_chunk(
&self,
room_id: &RoomId,
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
}
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.replace_media_key(from, to).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.0.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
}
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
self.0.clean_up_media_cache().await.map_err(Into::into)
}
}
/// A type-erased [`EventCacheStore`].
pub type DynEventCacheStore = dyn EventCacheStore<Error = EventCacheStoreError>;
/// A type that can be type-erased into `Arc<dyn EventCacheStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-base`, but it is automatically implemented for everything that
/// implements `EventCacheStore`.
pub trait IntoEventCacheStore {
#[doc(hidden)]
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
}
impl<T> IntoEventCacheStore for T
where
T: EventCacheStore + Sized + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
Arc::new(EraseEventCacheStoreError(self))
}
}
// Turns a given `Arc<T>` into `Arc<DynEventCacheStore>` by attaching the
// `EventCacheStore` impl vtable of `EraseEventCacheStoreError<T>`.
impl<T> IntoEventCacheStore for Arc<T>
where
T: EventCacheStore + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
let ptr: *const T = Arc::into_raw(self);
let ptr_erased = ptr as *const EraseEventCacheStoreError<T>;
// SAFETY: EraseEventCacheStoreError is repr(transparent) so T and
// EraseEventCacheStoreError<T> have the same layout and ABI
unsafe { Arc::from_raw(ptr_erased) }
}
}
@@ -1,189 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Trait and macro of integration tests for `EventCacheStore` implementations.
use async_trait::async_trait;
use ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
};
use super::DynEventCacheStore;
use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSettings};
/// `EventCacheStore` integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// [`event_cache_store_integration_tests!`] macro.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStoreIntegrationTests {
/// Test media content storage.
async fn test_media_content(&self);
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file =
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
let request_thumbnail = MediaRequest {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
Method::Crop,
uint!(100),
uint!(100),
)),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequest {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone())
.await
.expect("adding media again failed");
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
// Let's add the thumbnail media.
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
.await
.expect("adding thumbnail failed");
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// Let's add another media with a different URI.
self.add_media_content(&request_other_file, other_content.clone())
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
}
}
/// Macro building to allow your `EventCacheStore` implementation to run the
/// entire tests suite locally.
///
/// You need to provide a `async fn get_event_cache_store() ->
/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
/// store on the same level you invoke the macro.
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache_store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
/// # };
///
/// #[cfg(test)]
/// mod tests {
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
///
/// async fn get_event_cache_store(
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
/// Ok(MyStore::new())
/// }
///
/// event_cache_store_integration_tests!();
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! event_cache_store_integration_tests {
() => {
mod event_cache_store_integration_tests {
use matrix_sdk_test::async_test;
use $crate::event_cache_store::{EventCacheStoreIntegrationTests, IntoEventCacheStore};
use super::get_event_cache_store;
#[async_test]
async fn test_media_content() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_media_content().await;
}
}
};
}
@@ -1,116 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{num::NonZeroUsize, sync::RwLock as StdRwLock};
use async_trait::async_trait;
use matrix_sdk_common::ring_buffer::RingBuffer;
use ruma::{MxcUri, OwnedMxcUri};
use super::{EventCacheStore, EventCacheStoreError, Result};
use crate::media::{MediaRequest, UniqueKey as _};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
/// Default if no other is configured at startup.
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct MemoryStore {
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, 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 { media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)) }
}
}
impl MemoryStore {
/// Create a new empty MemoryStore
pub fn new() -> Self {
Self::default()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventCacheStore for MemoryStore {
type Error = EventCacheStoreError;
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
// Now, let's add it.
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
let media = self.media.read().unwrap();
let expected_key = request.unique_key();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = request.unique_key();
let Some(index) = media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
(media_uri == &expected_key).then_some(position)
})
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{EventCacheStore, MemoryStore, Result};
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
Ok(MemoryStore::new())
}
event_cache_store_integration_tests!();
}
@@ -1,85 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The event cache stores holds events and downloaded media when the cache was
//! activated to save bandwidth at the cost of increased storage space usage.
//!
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
//! into the event cache for the actual storage. By default this brings an
//! in-memory store.
use std::str::Utf8Error;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
mod memory_store;
mod traits;
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::EventCacheStoreIntegrationTests;
pub use self::{
memory_store::MemoryStore,
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
};
/// Event cache store specific error type.
#[derive(Debug, thiserror::Error)]
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
#[error("The event cache store failed to be unlocked")]
Locked,
/// An unencrypted store was tried to be unlocked with a passphrase.
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
Unencrypted,
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
Encryption(#[from] StoreEncryptionError),
/// The store failed to encode or decode some data.
#[error("Error encoding or decoding data from the event cache store: {0}")]
Codec(#[from] Utf8Error),
/// The database format has changed in a backwards incompatible way.
#[error(
"The database format of the event cache store changed in an incompatible way, \
current version: {0}, latest version: {1}"
)]
UnsupportedDatabaseVersion(usize, usize),
}
impl EventCacheStoreError {
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
}
/// An `EventCacheStore` specific result type.
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
@@ -1,145 +0,0 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::MxcUri;
use super::EventCacheStoreError;
use crate::media::MediaRequest;
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventCacheStore: AsyncTraitDeps {
/// The error type used by this event cache store.
type Error: fmt::Debug + Into<EventCacheStoreError>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequest,
content: Vec<u8>,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequest,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
}
#[repr(transparent)]
struct EraseEventCacheStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
type Error = EventCacheStoreError;
async fn add_media_content(
&self,
request: &MediaRequest,
content: Vec<u8>,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequest,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
}
/// A type-erased [`EventCacheStore`].
pub type DynEventCacheStore = dyn EventCacheStore<Error = EventCacheStoreError>;
/// A type that can be type-erased into `Arc<dyn EventCacheStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-base`, but it is automatically implemented for everything that
/// implements `EventCacheStore`.
pub trait IntoEventCacheStore {
#[doc(hidden)]
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
}
impl<T> IntoEventCacheStore for T
where
T: EventCacheStore + Sized + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
Arc::new(EraseEventCacheStoreError(self))
}
}
// Turns a given `Arc<T>` into `Arc<DynEventCacheStore>` by attaching the
// `EventCacheStore` impl vtable of `EraseEventCacheStoreError<T>`.
impl<T> IntoEventCacheStore for Arc<T>
where
T: EventCacheStore + 'static,
{
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
let ptr: *const T = Arc::into_raw(self);
let ptr_erased = ptr as *const EraseEventCacheStoreError<T>;
// SAFETY: EraseEventCacheStoreError is repr(transparent) so T and
// EraseEventCacheStoreError<T> have the same layout and ABI
unsafe { Arc::from_raw(ptr_erased) }
}
}
+118 -45
View File
@@ -1,18 +1,24 @@
//! Utilities for working with events to decide whether they are suitable for
//! use as a [crate::Room::latest_event].
#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use ruma::events::{
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::message::SyncRoomMessageEvent,
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
use ruma::{
events::{
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::SyncRoomMessageEvent,
power_levels::RoomPowerLevels,
},
sticker::SyncStickerEvent,
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
},
UserId,
};
use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId};
use ruma::{MxcUri, OwnedEventId};
use serde::{Deserialize, Serialize};
use crate::MinimalRoomMemberEvent;
@@ -37,6 +43,10 @@ pub enum PossibleLatestEvent<'a> {
/// This message is suitable - it's a call notification
YesCallNotify(&'a SyncCallNotifyEvent),
/// This state event is suitable - it's a knock membership change
/// that can be handled by the current user.
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
// Later: YesState(),
// Later: YesReaction(),
/// Not suitable - it's a state event
@@ -50,14 +60,17 @@ pub enum PossibleLatestEvent<'a> {
/// Decide whether an event could be stored as the latest event in a room.
/// Returns a LatestEvent representing our decision.
#[cfg(feature = "e2e-encryption")]
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
pub fn is_suitable_for_latest_event<'a>(
event: &'a AnySyncTimelineEvent,
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
) -> PossibleLatestEvent<'a> {
match event {
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
// Check if this is a replacement for another message. If it is, ignore it
if let Some(original_message) = message.as_original() {
let is_replacement =
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
relation_type == RelationType::Replacement
} else {
@@ -66,13 +79,13 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
});
if is_replacement {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
PossibleLatestEvent::NoUnsupportedMessageLikeType
} else {
return PossibleLatestEvent::YesRoomMessage(message);
PossibleLatestEvent::YesRoomMessage(message)
}
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
return PossibleLatestEvent::YesRoomMessage(message);
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
@@ -103,8 +116,29 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
// suitable
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
// We don't currently support state events
AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType,
// We don't currently support most state events
AnySyncTimelineEvent::State(state) => {
// But we make an exception for knocked state events *if* the current user
// can either accept or decline them
if let AnySyncStateEvent::RoomMember(member) = state {
if matches!(member.membership(), MembershipState::Knock) {
let can_accept_or_decline_knocks = match power_levels_info {
Some((own_user_id, room_power_levels)) => {
room_power_levels.user_can_invite(own_user_id)
|| room_power_levels.user_can_kick(own_user_id)
}
_ => false,
};
// The current user can act on the knock changes, so they should be
// displayed
if can_accept_or_decline_knocks {
return PossibleLatestEvent::YesKnockedStateEvent(member);
}
}
}
PossibleLatestEvent::NoUnsupportedEventType
}
}
}
@@ -130,7 +164,7 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
#[derive(Clone, Debug, Serialize)]
pub struct LatestEvent {
/// The actual event.
event: SyncTimelineEvent,
event: TimelineEvent,
/// The member profile of the event' sender.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -144,7 +178,7 @@ pub struct LatestEvent {
#[derive(Deserialize)]
struct SerializedLatestEvent {
/// The actual event.
event: SyncTimelineEvent,
event: TimelineEvent,
/// The member profile of the event' sender.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -177,7 +211,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
Err(err) => variant_errors.push(err),
}
match serde_json::from_str::<SyncTimelineEvent>(raw.get()) {
match serde_json::from_str::<TimelineEvent>(raw.get()) {
Ok(value) => {
return Ok(LatestEvent {
event: value,
@@ -196,13 +230,13 @@ impl<'de> Deserialize<'de> for LatestEvent {
impl LatestEvent {
/// Create a new [`LatestEvent`] without the sender's profile.
pub fn new(event: SyncTimelineEvent) -> Self {
pub fn new(event: TimelineEvent) -> Self {
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
}
/// Create a new [`LatestEvent`] with maybe the sender's profile.
pub fn new_with_sender_details(
event: SyncTimelineEvent,
event: TimelineEvent,
sender_profile: Option<MinimalRoomMemberEvent>,
sender_name_is_ambiguous: Option<bool>,
) -> Self {
@@ -210,17 +244,17 @@ impl LatestEvent {
}
/// Transform [`Self`] into an event.
pub fn into_event(self) -> SyncTimelineEvent {
pub fn into_event(self) -> TimelineEvent {
self.event
}
/// Get a reference to the event.
pub fn event(&self) -> &SyncTimelineEvent {
pub fn event(&self) -> &TimelineEvent {
&self.event
}
/// Get a mutable reference to the event.
pub fn event_mut(&mut self) -> &mut SyncTimelineEvent {
pub fn event_mut(&mut self) -> &mut TimelineEvent {
&mut self.event
}
@@ -260,11 +294,16 @@ impl LatestEvent {
#[cfg(test)]
mod tests {
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use assert_matches::assert_matches;
#[cfg(feature = "e2e-encryption")]
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use ruma::serde::Raw;
#[cfg(feature = "e2e-encryption")]
use ruma::{
events::{
call::{
@@ -302,14 +341,16 @@ mod tests {
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
UnsignedRoomRedactionEvent,
},
owned_event_id, owned_mxc_uri, owned_user_id,
serde::Raw,
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
VoipVersionId,
};
use serde_json::json;
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
use super::LatestEvent;
#[cfg(feature = "e2e-encryption")]
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_room_messages_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
@@ -328,12 +369,13 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.msgtype.msgtype(), "m.image");
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_polls_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
@@ -351,12 +393,13 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_invites_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
@@ -375,10 +418,11 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_notifications_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
@@ -397,10 +441,11 @@ mod tests {
));
assert_let!(
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event)
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_stickers_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
@@ -418,11 +463,12 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
let event =
@@ -440,11 +486,12 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_redacted_messages_are_suitable() {
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
@@ -468,11 +515,12 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_encrypted_messages_are_unsuitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
@@ -490,9 +538,13 @@ mod tests {
}),
));
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoEncrypted
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_state_events_are_unsuitable() {
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
@@ -507,11 +559,12 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_replacement_events_are_unsuitable() {
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
@@ -531,7 +584,7 @@ mod tests {
));
assert_matches!(
is_suitable_for_latest_event(&event),
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
@@ -543,7 +596,7 @@ mod tests {
latest_event: LatestEvent,
}
let event = SyncTimelineEvent::new(
let event = TimelineEvent::new(
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
);
@@ -562,9 +615,12 @@ mod tests {
json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
}
},
}
@@ -578,6 +634,23 @@ mod tests {
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The previous format can also be deserialized.
let serialized = json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
}
},
}
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The even older format can also be deserialized.
let serialized = json!({
"latest_event": event
});
+7 -6
View File
@@ -15,6 +15,7 @@
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![warn(missing_docs, missing_debug_implementations)]
pub use matrix_sdk_common::*;
@@ -27,15 +28,15 @@ mod client;
pub mod debug;
pub mod deserialized_responses;
mod error;
pub mod event_cache_store;
pub mod event_cache;
pub mod latest_event;
pub mod media;
pub mod notification_settings;
mod response_processors;
mod rooms;
pub mod read_receipts;
pub use read_receipts::PreviousEventsProvider;
#[cfg(feature = "experimental-sliding-sync")]
pub mod sliding_sync;
pub mod store;
@@ -54,12 +55,12 @@ pub use http;
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use rooms::{
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
RoomStateFilter,
apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate,
RoomMemberships, RoomState, RoomStateFilter,
};
pub use store::{
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
};
pub use utils::{
+27 -27
View File
@@ -14,6 +14,7 @@ use ruma::{
},
MxcUri, UInt,
};
use serde::{Deserialize, Serialize};
const UNIQUE_SEPARATOR: &str = "_";
@@ -25,7 +26,7 @@ pub trait UniqueKey {
}
/// The requested format of a media file.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MediaFormat {
/// The file that was uploaded.
File,
@@ -43,9 +44,9 @@ impl UniqueKey for MediaFormat {
}
}
/// The requested size of a media thumbnail.
#[derive(Clone, Debug)]
pub struct MediaThumbnailSize {
/// The desired settings of a media thumbnail.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaThumbnailSettings {
/// The desired resizing method.
pub method: Method,
@@ -56,19 +57,6 @@ pub struct MediaThumbnailSize {
/// The desired height of the thumbnail. The actual thumbnail may not match
/// the size specified.
pub height: UInt,
}
impl UniqueKey for MediaThumbnailSize {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
}
}
/// The desired settings of a media thumbnail.
#[derive(Clone, Debug)]
pub struct MediaThumbnailSettings {
/// The desired size of the thumbnail.
pub size: MediaThumbnailSize,
/// If we want to request an animated thumbnail from the homeserver.
///
@@ -82,14 +70,24 @@ pub struct MediaThumbnailSettings {
impl MediaThumbnailSettings {
/// Constructs a new `MediaThumbnailSettings` with the given method, width
/// and height.
pub fn new(method: Method, width: UInt, height: UInt) -> Self {
Self { size: MediaThumbnailSize { method, width, height }, animated: false }
///
/// Requests a non-animated thumbnail by default.
pub fn with_method(method: Method, width: UInt, height: UInt) -> Self {
Self { method, width, height, animated: false }
}
/// Constructs a new `MediaThumbnailSettings` with the given width and
/// height.
///
/// Requests scaling, and a non-animated thumbnail.
pub fn new(width: UInt, height: UInt) -> Self {
Self { method: Method::Scale, width, height, animated: false }
}
}
impl UniqueKey for MediaThumbnailSettings {
fn unique_key(&self) -> String {
let mut key = self.size.unique_key();
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
if self.animated {
key.push_str(UNIQUE_SEPARATOR);
@@ -109,9 +107,11 @@ impl UniqueKey for MediaSource {
}
}
/// A request for media data.
#[derive(Clone, Debug)]
pub struct MediaRequest {
/// Parameters for a request for retrieve media data.
///
/// This is used as a key in the media cache too.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MediaRequestParameters {
/// The source of the media file.
pub source: MediaSource,
@@ -119,7 +119,7 @@ pub struct MediaRequest {
pub format: MediaFormat,
}
impl MediaRequest {
impl MediaRequestParameters {
/// Get the [`MxcUri`] from `Self`.
pub fn uri(&self) -> &MxcUri {
match &self.source {
@@ -129,7 +129,7 @@ impl MediaRequest {
}
}
impl UniqueKey for MediaRequest {
impl UniqueKey for MediaRequestParameters {
fn unique_key(&self) -> String {
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
}
@@ -225,14 +225,14 @@ mod tests {
fn test_media_request_url() {
let mxc_uri = mxc_uri!("mxc://homeserver/media");
let plain = MediaRequest {
let plain = MediaRequestParameters {
source: MediaSource::Plain(mxc_uri.to_owned()),
format: MediaFormat::File,
};
assert_eq!(plain.uri(), mxc_uri);
let file = MediaRequest {
let file = MediaRequestParameters {
source: MediaSource::Encrypted(Box::new(
serde_json::from_value(json!({
"url": mxc_uri,
+192 -245
View File
@@ -123,7 +123,7 @@ use std::{
};
use eyeball_im::Vector;
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use ruma::{
events::{
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
@@ -202,15 +202,19 @@ impl RoomReadReceipts {
///
/// Returns whether a new event triggered a new unread/notification/mention.
#[inline(always)]
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
if marks_as_unread(&event.event, user_id) {
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
if marks_as_unread(event.raw(), user_id) {
self.num_unread += 1;
}
let mut has_notify = false;
let mut has_mention = false;
for action in &event.push_actions {
let Some(actions) = event.push_actions.as_ref() else {
return;
};
for action in actions.iter() {
if !has_notify && action.should_notify() {
self.num_notifications += 1;
has_notify = true;
@@ -236,7 +240,7 @@ impl RoomReadReceipts {
&mut self,
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
events: impl IntoIterator<Item = &'a TimelineEvent>,
) -> bool {
let mut counting_receipts = false;
@@ -269,11 +273,11 @@ impl RoomReadReceipts {
pub trait PreviousEventsProvider: Send + Sync {
/// Returns the list of known timeline events, in sync order, for the given
/// room.
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
}
impl PreviousEventsProvider for () {
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
Vector::new()
}
}
@@ -292,7 +296,7 @@ struct ReceiptSelector {
impl ReceiptSelector {
fn new(
all_events: &Vector<SyncTimelineEvent>,
all_events: &Vector<TimelineEvent>,
latest_active_receipt_event: Option<&EventId>,
) -> Self {
let event_id_to_pos = Self::create_sync_index(all_events.iter());
@@ -310,7 +314,7 @@ impl ReceiptSelector {
/// Create a mapping of `event_id` -> sync order for all events that have an
/// `event_id`.
fn create_sync_index<'a>(
events: impl Iterator<Item = &'a SyncTimelineEvent> + 'a,
events: impl Iterator<Item = &'a TimelineEvent> + 'a,
) -> BTreeMap<OwnedEventId, usize> {
// TODO: this should be cached and incrementally updated.
BTreeMap::from_iter(
@@ -405,10 +409,10 @@ impl ReceiptSelector {
/// Try to match an implicit receipt, that is, the one we get for events we
/// sent ourselves.
#[instrument(skip_all)]
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) {
for ev in new_events {
// Get the `sender` field, if any, or skip this event.
let Ok(Some(sender)) = ev.event.get_field::<OwnedUserId>("sender") else { continue };
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
if sender == user_id {
// Get the event id, if any, or skip this event.
let Some(event_id) = ev.event_id() else { continue };
@@ -432,13 +436,13 @@ impl ReceiptSelector {
/// Returns true if there's an event common to both groups of events, based on
/// their event id.
fn events_intersects<'a>(
previous_events: impl Iterator<Item = &'a SyncTimelineEvent>,
new_events: &[SyncTimelineEvent],
previous_events: impl Iterator<Item = &'a TimelineEvent>,
new_events: &[TimelineEvent],
) -> bool {
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
new_events
.iter()
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
}
/// Given a set of events coming from sync, for a room, update the
@@ -454,8 +458,8 @@ pub(crate) fn compute_unread_counts(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
previous_events: Vector<SyncTimelineEvent>,
new_events: &[SyncTimelineEvent],
previous_events: Vector<TimelineEvent>,
new_events: &[TimelineEvent],
read_receipts: &mut RoomReadReceipts,
) {
debug!(?read_receipts, "Starting.");
@@ -620,11 +624,14 @@ mod tests {
use std::{num::NonZeroUsize, ops::Not as _};
use eyeball_im::Vector;
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::{sync_timeline_event, EventBuilder};
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::event_factory::EventFactory;
use ruma::{
event_id,
events::receipt::{ReceiptThread, ReceiptType},
events::{
receipt::{ReceiptThread, ReceiptType},
room::{member::MembershipState, message::MessageType},
},
owned_event_id, owned_user_id,
push::Action,
room_id, user_id, EventId, UserId,
@@ -638,24 +645,14 @@ mod tests {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let f = EventFactory::new();
// A message from somebody else marks the room as unread...
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
assert!(marks_as_unread(&ev, user_id));
// ... but a message from ourselves doesn't.
let ev = sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
});
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
@@ -665,24 +662,16 @@ mod tests {
let other_user_id = user_id!("@bob:example.org");
// An edit to a message from somebody else doesn't mark the room as unread.
let ev = sync_timeline_event!({
"sender": other_user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": {
"body": " * edited message",
"m.new_content": {
"body": "edited message",
"msgtype": "m.text"
},
"m.relates_to": {
"event_id": "$someeventid:localhost",
"rel_type": "m.replace"
},
"msgtype": "m.text"
},
});
let ev = EventFactory::new()
.text_msg("* edited message")
.edit(
event_id!("$someeventid:localhost"),
MessageType::text_plain("edited message").into(),
)
.event_id(event_id!("$ida"))
.sender(other_user_id)
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
@@ -692,19 +681,11 @@ mod tests {
let other_user_id = user_id!("@bob:example.org");
// A redact of a message from somebody else doesn't mark the room as unread.
let ev = sync_timeline_event!({
"content": {
"reason": "🛑"
},
"event_id": "$151957878228ssqrJ:localhost",
"origin_server_ts": 151957878000000_u64,
"sender": other_user_id,
"type": "m.room.redaction",
"redacts": "$151957878228ssqrj:localhost",
"unsigned": {
"age": 85
}
});
let ev = EventFactory::new()
.redaction(event_id!("$151957878228ssqrj:localhost"))
.sender(other_user_id)
.event_id(event_id!("$151957878228ssqrJ:localhost"))
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
@@ -715,22 +696,11 @@ mod tests {
let other_user_id = user_id!("@bob:example.org");
// A reaction from somebody else to a message doesn't mark the room as unread.
let ev = sync_timeline_event!({
"content": {
"m.relates_to": {
"event_id": "$15275047031IXQRi:localhost",
"key": "👍",
"rel_type": "m.annotation"
}
},
"event_id": "$15275047031IXQRi:localhost",
"origin_server_ts": 159027581000000_u64,
"sender": other_user_id,
"type": "m.reaction",
"unsigned": {
"age": 85
}
});
let ev = EventFactory::new()
.reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
.sender(other_user_id)
.event_id(event_id!("$15275047031IXQRi:localhost"))
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
@@ -739,18 +709,13 @@ mod tests {
fn test_state_event_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let event_id = event_id!("$1");
let ev = sync_timeline_event!({
"content": {
"displayname": "Alice",
"membership": "join",
},
"event_id": event_id,
"origin_server_ts": 1432135524678u64,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
});
let ev = EventFactory::new()
.member(user_id)
.membership(MembershipState::Join)
.display_name("Alice")
.event_id(event_id)
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
let other_user_id = user_id!("@bob:example.org");
@@ -759,17 +724,14 @@ mod tests {
#[test]
fn test_count_unread_and_mentions() {
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
SyncTimelineEvent::new_with_push_actions(
sync_timeline_event!({
"sender": user_id,
"type": "m.room.message",
"event_id": "$ida",
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}),
push_actions,
)
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
let mut ev = EventFactory::new()
.text_msg("A")
.sender(user_id)
.event_id(event_id!("$ida"))
.into_event();
ev.push_actions = Some(push_actions);
ev
}
let user_id = user_id!("@alice:example.org");
@@ -843,14 +805,12 @@ mod tests {
// When provided with one event, that's not the receipt event, we don't count
// it.
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
SyncTimelineEvent::new(sync_timeline_event!({
"sender": "@bob:example.org",
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 12344446,
"content": { "body":"A", "msgtype": "m.text" },
}))
fn make_event(event_id: &EventId) -> TimelineEvent {
EventFactory::new()
.text_msg("A")
.sender(user_id!("@bob:example.org"))
.event_id(event_id)
.into()
}
let mut receipts = RoomReadReceipts {
@@ -948,20 +908,6 @@ mod tests {
assert_eq!(receipts.num_mentions, 0);
}
fn sync_timeline_message(
sender: &UserId,
event_id: impl serde::Serialize,
body: impl serde::Serialize,
) -> SyncTimelineEvent {
SyncTimelineEvent::new(sync_timeline_event!({
"sender": sender,
"type": "m.room.message",
"event_id": event_id,
"origin_server_ts": 42,
"content": { "body": body, "msgtype": "m.text" },
}))
}
/// Smoke test for `compute_unread_counts`.
#[test]
fn test_basic_compute_unread_counts() {
@@ -972,15 +918,14 @@ mod tests {
let mut previous_events = Vector::new();
let ev1 = sync_timeline_message(other_user_id, receipt_event_id, "A");
let ev2 = sync_timeline_message(other_user_id, "$2", "A");
let f = EventFactory::new();
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event();
let receipt_event = EventBuilder::new().make_receipt_event_content([(
receipt_event_id.to_owned(),
ReceiptType::Read,
user_id.to_owned(),
ReceiptThread::Unthreaded,
)]);
let receipt_event = f
.read_receipts()
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
let mut read_receipts = Default::default();
compute_unread_counts(
@@ -999,7 +944,8 @@ mod tests {
previous_events.push_back(ev1);
previous_events.push_back(ev2);
let new_event = sync_timeline_message(other_user_id, "$3", "A");
let new_event =
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
compute_unread_counts(
user_id,
room_id,
@@ -1013,13 +959,14 @@ mod tests {
assert_eq!(read_receipts.num_unread, 2);
}
fn make_test_events(user_id: &UserId) -> Vector<SyncTimelineEvent> {
let ev1 = sync_timeline_message(user_id, "$1", "With the lights out, it's less dangerous");
let ev2 = sync_timeline_message(user_id, "$2", "Here we are now, entertain us");
let ev3 = sync_timeline_message(user_id, "$3", "I feel stupid and contagious");
let ev4 = sync_timeline_message(user_id, "$4", "Here we are now, entertain us");
let ev5 = sync_timeline_message(user_id, "$5", "Hello, hello, hello, how low?");
vec![ev1, ev2, ev3, ev4, ev5].into()
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
let f = EventFactory::new().sender(user_id);
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
let ev3 = f.text_msg("I feel stupid and contagious").event_id(event_id!("$3"));
let ev4 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$4"));
let ev5 = f.text_msg("Hello, hello, hello, how low?").event_id(event_id!("$5"));
[ev1, ev2, ev3, ev4, ev5].into_iter().map(Into::into).collect()
}
/// Test that when multiple receipts come in a single event, we can still
@@ -1035,30 +982,32 @@ mod tests {
// Given a receipt event marking events 1-3 as read using a combination of
// different thread and privacy types,
let f = EventFactory::new();
for receipt_type_1 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
for receipt_thread_1 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
for receipt_type_2 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
for receipt_thread_2 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
let receipt_event = EventBuilder::new().make_receipt_event_content([
(
owned_event_id!("$2"),
let receipt_event = f
.read_receipts()
.add(
event_id!("$2"),
user_id,
receipt_type_1.clone(),
user_id.to_owned(),
receipt_thread_1.clone(),
),
(
owned_event_id!("$3"),
)
.add(
event_id!("$3"),
user_id,
receipt_type_2.clone(),
user_id.to_owned(),
receipt_thread_2.clone(),
),
(
owned_event_id!("$1"),
)
.add(
event_id!("$1"),
user_id,
receipt_type_1.clone(),
user_id.to_owned(),
receipt_thread_2.clone(),
),
]);
)
.build();
// When I compute the notifications for this room (with no new events),
let mut read_receipts = RoomReadReceipts::default();
@@ -1118,12 +1067,10 @@ mod tests {
let events = make_test_events(user_id!("@bob:example.org"));
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$6"),
ReceiptType::Read,
user_id.clone(),
ReceiptThread::Unthreaded,
)]);
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
let mut read_receipts = RoomReadReceipts::default();
assert!(read_receipts.pending.is_empty());
@@ -1154,12 +1101,10 @@ mod tests {
let events = make_test_events(user_id!("@bob:example.org"));
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$1"),
ReceiptType::Read,
user_id.clone(),
ReceiptThread::Unthreaded,
)]);
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
// Sync with a read receipt *and* a single event that was already known: in that
// case, only consider the new events in isolation, and compute the
@@ -1190,12 +1135,7 @@ mod tests {
let events = make_test_events(uid);
// An event with no id.
let ev6 = SyncTimelineEvent::new(sync_timeline_event!({
"sender": uid,
"type": "m.room.message",
"origin_server_ts": 42,
"content": { "body": "yolo", "msgtype": "m.text" },
}));
let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event();
let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6]));
@@ -1261,8 +1201,9 @@ mod tests {
#[test]
fn test_receipt_selector_handle_pending_receipts_noop() {
let sender = user_id!("@bob:example.org");
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
{
@@ -1296,8 +1237,9 @@ mod tests {
#[test]
fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() {
let sender = user_id!("@bob:example.org");
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
{
@@ -1332,8 +1274,9 @@ mod tests {
#[test]
fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() {
let sender = user_id!("@bob:example.org");
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
{
@@ -1373,8 +1316,9 @@ mod tests {
#[test]
fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() {
let sender = user_id!("@bob:example.org");
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events: Vector<_> = vec![ev1, ev2].into();
{
@@ -1412,21 +1356,25 @@ mod tests {
#[test]
fn test_receipt_selector_handle_new_receipt() {
let myself = owned_user_id!("@alice:example.org");
let myself = user_id!("@alice:example.org");
let events = make_test_events(user_id!("@bob:example.org"));
let f = EventFactory::new();
{
// Thread receipts are ignored.
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$5"),
ReceiptType::Read,
myself.clone(),
ReceiptThread::Thread(owned_event_id!("$2")),
)]);
let receipt_event = f
.read_receipts()
.add(
event_id!("$5"),
myself,
ReceiptType::Read,
ReceiptThread::Thread(owned_event_id!("$2")),
)
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
@@ -1440,14 +1388,12 @@ mod tests {
// receipt.
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$6"),
receipt_type.clone(),
myself.clone(),
receipt_thread.clone(),
)]);
let receipt_event = f
.read_receipts()
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending[0], event_id!("$6"));
assert_eq!(pending.len(), 1);
@@ -1460,14 +1406,12 @@ mod tests {
// receipt.
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$3"),
receipt_type.clone(),
myself.clone(),
receipt_thread.clone(),
)]);
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
@@ -1479,14 +1423,12 @@ mod tests {
// better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$4")));
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$3"),
receipt_type.clone(),
myself.clone(),
receipt_thread.clone(),
)]);
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
@@ -1498,14 +1440,12 @@ mod tests {
// new better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$3"),
receipt_type.clone(),
myself.clone(),
receipt_thread.clone(),
)]);
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
@@ -1519,23 +1459,14 @@ mod tests {
// new better receipt.
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
let receipt_event = EventBuilder::new().make_receipt_event_content([
(
owned_event_id!("$4"),
ReceiptType::ReadPrivate,
myself.clone(),
ReceiptThread::Unthreaded,
),
(
owned_event_id!("$6"),
ReceiptType::ReadPrivate,
myself.clone(),
ReceiptThread::Main,
),
(owned_event_id!("$3"), ReceiptType::Read, myself.clone(), ReceiptThread::Main),
]);
let receipt_event = f
.read_receipts()
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
.build();
let pending = selector.handle_new_receipt(&myself, &receipt_event);
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending.len(), 1);
assert_eq!(pending[0], event_id!("$6"));
@@ -1560,8 +1491,16 @@ mod tests {
assert!(best_receipt.is_none());
// Now, if there are events I've written too...
events.push_back(sync_timeline_message(&myself, "$6", "A mulatto, an albino"));
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
let f = EventFactory::new();
events.push_back(
f.text_msg("A mulatto, an albino")
.sender(&myself)
.event_id(event_id!("$6"))
.into_event(),
);
events.push_back(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
let mut selector = ReceiptSelector::new(&events, None);
// And I search for my implicit read receipt,
@@ -1573,7 +1512,7 @@ mod tests {
#[test]
fn test_compute_unread_counts_with_implicit_receipt() {
let user_id = owned_user_id!("@alice:example.org");
let user_id = user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
let room_id = room_id!("!room:example.org");
@@ -1581,28 +1520,36 @@ mod tests {
let mut events = make_test_events(bob);
// One by me,
events.push_back(sync_timeline_message(&user_id, "$6", "A mulatto, an albino"));
let f = EventFactory::new();
events.push_back(
f.text_msg("A mulatto, an albino")
.sender(user_id)
.event_id(event_id!("$6"))
.into_event(),
);
// And others by Bob,
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
events.push_back(sync_timeline_message(bob, "$8", "A denial, a denial"));
events.push_back(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
events.push_back(
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
);
let events: Vec<_> = events.into_iter().collect();
// I have a read receipt attached to one of Bob's event sent before my message,
let receipt_event = EventBuilder::new().make_receipt_event_content([(
owned_event_id!("$3"),
ReceiptType::Read,
user_id.clone(),
ReceiptThread::Unthreaded,
)]);
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.build();
let mut read_receipts = RoomReadReceipts::default();
// And I compute the unread counts for all those new events (no previous events
// in that room),
compute_unread_counts(
&user_id,
user_id,
room_id,
Some(&receipt_event),
Vector::new(),
@@ -0,0 +1,162 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashMap, HashSet},
mem,
};
use ruma::{
events::{
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
},
serde::Raw,
RoomId,
};
use tracing::{debug, instrument, trace, warn};
use crate::{store::Store, RoomInfo, StateChanges};
/// Applies a function to an existing `RoomInfo` if present in changes, or one
/// loaded from the database.
fn map_info<F: FnOnce(&mut RoomInfo)>(
room_id: &RoomId,
changes: &mut StateChanges,
store: &Store,
f: F,
) {
if let Some(info) = changes.room_infos.get_mut(room_id) {
f(info);
} else if let Some(room) = store.room(room_id) {
let mut info = room.clone_info();
f(&mut info);
changes.add_room(info);
} else {
debug!(room = %room_id, "couldn't find room in state changes or store");
}
}
#[must_use]
pub(crate) struct AccountDataProcessor {
parsed_events: Vec<AnyGlobalAccountDataEvent>,
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
}
impl AccountDataProcessor {
/// Creates a new processor for global account data.
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
let mut raw_by_type = BTreeMap::new();
let mut parsed_events = Vec::new();
for raw_event in events {
let event = match raw_event.deserialize() {
Ok(e) => e,
Err(e) => {
let event_type: Option<String> = raw_event.get_field("type").ok().flatten();
warn!(event_type, "Failed to deserialize a global account data event: {e}");
continue;
}
};
raw_by_type.insert(event.event_type(), raw_event.clone());
parsed_events.push(event);
}
Self { raw_by_type, parsed_events }
}
/// Returns the push rules found by this processor.
pub fn push_rules(&self) -> Option<&Raw<AnyGlobalAccountDataEvent>> {
self.raw_by_type.get(&GlobalAccountDataEventType::PushRules)
}
/// Processes the direct rooms in a sync response:
///
/// Given a [`StateChanges`] instance, processes any direct room info
/// from the global account data and adds it to the room infos to
/// save.
#[instrument(skip_all)]
pub(crate) fn process_direct_rooms(
&self,
events: &[AnyGlobalAccountDataEvent],
store: &Store,
changes: &mut StateChanges,
) {
for event in events {
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
for (user_identifier, rooms) in direct_event.content.iter() {
for room_id in rooms {
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
}
}
let rooms = store.rooms();
let mut old_dms = rooms
.iter()
.filter_map(|r| {
let direct_targets = r.direct_targets();
(!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets))
})
.collect::<HashMap<_, _>>();
// Update the direct targets of rooms if they changed.
for (room_id, new_direct_targets) in new_dms {
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
if old_direct_targets == new_direct_targets {
continue;
}
}
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
map_info(room_id, changes, store, |info| {
info.base_info.dm_targets = new_direct_targets;
});
}
// Remove the targets of old direct chats.
for room_id in old_dms.keys() {
trace!(?room_id, "Unmarking room as direct room");
map_info(room_id, changes, store, |info| {
info.base_info.dm_targets.clear();
});
}
}
}
/// Applies the processed data to the state changes.
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
// Fill in the content of `changes.account_data`.
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
// Process direct rooms.
let has_new_direct_room_data = self
.parsed_events
.iter()
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
if has_new_direct_room_data {
self.process_direct_rooms(&self.parsed_events, store, changes);
} else if let Ok(Some(direct_account_data)) =
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
{
debug!("Found direct room data in the Store, applying it");
if let Ok(direct_account_data) = direct_account_data.deserialize() {
self.process_direct_rooms(&[direct_account_data], store, changes);
} else {
warn!("Failed to deserialize direct room account data");
}
}
}
}

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