Compare commits

...

68 Commits

Author SHA1 Message Date
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
146 changed files with 5194 additions and 1143 deletions
+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
+2 -2
View File
@@ -288,7 +288,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: rustfmt
- name: Cargo fmt
@@ -323,7 +323,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2024-06-25
toolchain: nightly-2024-11-26
components: clippy
- name: Load cache
+2 -2
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
@@ -53,7 +53,7 @@ jobs:
env:
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'
Generated
+5 -1
View File
@@ -3001,10 +3001,11 @@ dependencies = [
"eyeball-im",
"futures-core",
"futures-util",
"getrandom",
"gloo-timers",
"imbl",
"js-sys",
"matrix-sdk-test",
"matrix-sdk-test-macros",
"proptest",
"ruma",
"serde",
@@ -3026,6 +3027,7 @@ version = "0.8.0"
dependencies = [
"aes",
"anyhow",
"aquamarine",
"as_variant",
"assert_matches",
"assert_matches2",
@@ -3274,9 +3276,11 @@ dependencies = [
name = "matrix-sdk-test"
version = "0.7.0"
dependencies = [
"as_variant",
"ctor",
"getrandom",
"http",
"matrix-sdk-common",
"matrix-sdk-test-macros",
"once_cell",
"ruma",
+1
View File
@@ -22,6 +22,7 @@ rust-version = "1.76"
[workspace.dependencies]
anyhow = "1.0.68"
aquamarine = "0.6.0"
assert-json-diff = "2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.1"
+5 -4
View File
@@ -2,15 +2,16 @@ 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,
config::SyncSettings, test_utils::logged_in_client_with_server, utils::IntoRawStateEventContent,
};
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, EventBuilder, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder,
};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{
api::client::membership::get_member_events,
+1
View File
@@ -13,6 +13,7 @@ let package = Package(
],
products: [
.library(name: "MatrixRustSDK",
type: .dynamic,
targets: ["MatrixRustSDK"]),
],
targets: [
-7
View File
@@ -13,10 +13,3 @@ interface RoomMessageEventContentWithoutRelation {
interface ClientError {
Generic(string msg);
};
interface MediaSource {
[Name=from_json, Throws=ClientError]
constructor(string json);
string to_json();
string url();
};
+71 -18
View File
@@ -32,9 +32,7 @@ use matrix_sdk::{
user_directory::search_users,
},
events::{
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
},
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
},
serde::Raw,
@@ -55,7 +53,12 @@ use ruma::{
},
events::{
ignored_user_list::IgnoredUserListEventContent,
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
room::{
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
@@ -76,7 +79,7 @@ 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,
@@ -450,7 +453,7 @@ impl Client {
.inner
.media()
.get_media_file(
&MediaRequestParameters { source, format: MediaFormat::File },
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
@@ -723,7 +726,7 @@ 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
@@ -739,9 +742,9 @@ impl Client {
width: u64,
height: u64,
) -> Result<Vec<u8>, ClientError> {
let source = (*media_source).clone();
let source = (*media_source).clone().media_source;
debug!(source = ?media_source, width, height, "requesting media thumbnail");
debug!(?source, width, height, "requesting media thumbnail");
Ok(self
.inner
.media()
@@ -1630,7 +1633,7 @@ 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 {
@@ -1917,9 +1920,13 @@ 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 ruma::events::room::join_rules::JoinRule {
impl TryFrom<JoinRule> for RumaJoinRule {
type Error = ClientError;
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
@@ -1929,11 +1936,11 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
JoinRule::Knock => Ok(Self::Knock),
JoinRule::Private => Ok(Self::Private),
JoinRule::Restricted { rules } => {
let rules = allow_rules_from(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 = allow_rules_from(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)?),
@@ -1941,12 +1948,10 @@ impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
}
}
fn allow_rules_from(
value: Vec<AllowRule>,
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
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<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
match rule {
Ok(rule) => ret.push(rule),
Err(error) => return Err(error),
@@ -1955,7 +1960,7 @@ fn allow_rules_from(
Ok(ret)
}
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
impl TryFrom<AllowRule> for RumaAllowRule {
type Error = ClientError;
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
@@ -1966,6 +1971,54 @@ impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
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)),
}
}
}
+1 -1
View File
@@ -202,7 +202,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,
}
}
+4 -4
View File
@@ -1,6 +1,8 @@
// TODO: target-os conditional would be good.
#![allow(unused_qualifications, clippy::new_without_default)]
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
// lines after docs.
mod authentication;
mod chunk_iterator;
@@ -33,13 +35,11 @@ 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,
};
+1 -1
View File
@@ -973,7 +973,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),
}
+11 -1
View File
@@ -1,8 +1,10 @@
use std::collections::HashMap;
use matrix_sdk::RoomState;
use tracing::warn;
use crate::{
client::JoinRule,
notification_settings::RoomNotificationMode,
room::{Membership, RoomHero},
room_member::RoomMember,
@@ -54,8 +56,10 @@ 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>,
}
impl RoomInfo {
@@ -70,6 +74,11 @@ impl RoomInfo {
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(),
creator: room.creator().as_ref().map(ToString::to_string),
@@ -118,6 +127,7 @@ impl RoomInfo {
num_unread_notifications: room.num_unread_notifications(),
num_unread_mentions: room.num_unread_mentions(),
pinned_event_ids,
join_rule: join_rule.ok(),
})
}
}
+11 -2
View File
@@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
use tracing::warn;
use crate::{
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_member::RoomMember,
utils::AsyncRuntimeDropped,
};
@@ -38,6 +41,10 @@ impl RoomPreview {
.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()),
})
}
@@ -85,13 +92,15 @@ pub struct RoomPreviewInfo {
/// The room type (space, custom) or nothing, if it's a regular room.
pub room_type: RoomType,
/// Is the history world-readable for this room?
pub is_history_world_readable: 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 TryFrom<SpaceRoomJoinRule> for JoinRule {
+133 -67
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,
@@ -154,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId {
}
}
#[matrix_sdk_ffi_macros::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Arc::new(MediaSource::Plain(url.into()))
}
#[matrix_sdk_ffi_macros::export]
pub fn message_event_content_new(
msgtype: MessageType,
@@ -200,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(),
}
}
}
@@ -280,7 +337,7 @@ fn get_body_and_filename(filename: String, caption: Option<String>) -> (String,
}
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 {
@@ -292,7 +349,7 @@ 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(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_caption.map(Into::into);
event_content.filename = filename;
@@ -301,7 +358,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Audio { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaAudioMessageEventContent::new(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_caption.map(Into::into);
event_content.filename = filename;
@@ -310,7 +367,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::Video { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaVideoMessageEventContent::new(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_caption.map(Into::into);
event_content.filename = filename;
@@ -319,7 +376,7 @@ impl TryFrom<MessageType> for RumaMessageType {
MessageType::File { content } => {
let (body, filename) = get_body_and_filename(content.filename, content.caption);
let mut event_content =
RumaFileMessageEventContent::new(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_caption.map(Into::into);
event_content.filename = filename;
@@ -345,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(),
@@ -359,16 +418,17 @@ impl From<RumaMessageType> for MessageType {
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.clone()),
info: c.info.as_deref().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 {
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.clone()),
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),
@@ -379,8 +439,8 @@ impl From<RumaMessageType> for MessageType {
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.clone()),
info: c.info.as_deref().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 {
@@ -388,8 +448,8 @@ impl From<RumaMessageType> for MessageType {
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.clone()),
info: c.info.as_deref().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 {
@@ -425,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
msgtype: value.msgtype().to_owned(),
body: value.body().to_owned(),
},
}
})
}
}
@@ -520,7 +580,7 @@ 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,
})
}
@@ -625,7 +685,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,
})
}
@@ -668,7 +728,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()),
})
}
}
@@ -703,21 +763,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,
@@ -790,8 +835,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),
@@ -799,15 +846,20 @@ 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(),
}
})
}
}
@@ -821,8 +873,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),
@@ -830,21 +884,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),
@@ -852,12 +913,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),
})
}
}
+43 -12
View File
@@ -16,26 +16,55 @@ use std::{collections::HashMap, sync::Arc};
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
use ruma::events::{room::MediaSource, FullStateEventContent};
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
use super::ProfileDetails;
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
use crate::{
error::ClientError,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match value {
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
Content::Message(message) => {
let msgtype = message.msgtype().msgtype().to_owned();
match TryInto::<MessageContent>::try_into(message) {
Ok(message) => TimelineItemContent::Message { content: message },
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: msgtype,
error: error.to_string(),
},
}
}
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
Content::Sticker(sticker) => {
let content = sticker.content();
TimelineItemContent::Sticker {
body: content.body.clone(),
info: (&content.info).into(),
source: Arc::new(MediaSource::from(content.source.clone())),
let media_source = RumaMediaSource::from(content.source.clone());
if let Err(error) = media_source.verify() {
return TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
};
}
match TryInto::<ImageInfo>::try_into(&content.info) {
Ok(info) => TimelineItemContent::Sticker {
body: content.body.clone(),
info,
source: Arc::new(MediaSource { media_source }),
},
Err(error) => TimelineItemContent::FailedToParseMessageLike {
event_type: sticker.content().event_type().to_string(),
error: error.to_string(),
},
}
}
@@ -117,16 +146,18 @@ pub struct MessageContent {
pub mentions: Option<Mentions>,
}
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
Self {
msg_type: value.msgtype().clone().into(),
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()),
}
})
}
}
+43 -10
View File
@@ -24,7 +24,7 @@ use matrix_sdk::crypto::CollectStrategy;
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
room::edit::EditedContent as SdkEditedContent,
@@ -53,7 +53,7 @@ use ruma::{
},
AnyMessageLikeEventContent,
},
EventId,
EventId, UInt,
};
use tokio::{
sync::Mutex,
@@ -144,19 +144,26 @@ fn build_thumbnail_info(
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;
let height = thumbnail_info
.height
.and_then(|u| UInt::try_from(u).ok())
.ok_or(RoomError::InvalidAttachmentData)?;
let width = thumbnail_info
.width
.and_then(|u| UInt::try_from(u).ok())
.ok_or(RoomError::InvalidAttachmentData)?;
let size = thumbnail_info
.size
.and_then(|u| UInt::try_from(u).ok())
.ok_or(RoomError::InvalidAttachmentData)?;
let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let thumbnail = Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
};
let thumbnail =
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
Ok(AttachmentConfig::with_thumbnail(thumbnail))
}
@@ -545,6 +552,7 @@ impl Timeline {
.await
{
Ok(()) => Ok(()),
Err(timeline::Error::EventNotInTimeline(_)) => {
// If we couldn't edit, assume it was an (remote) event that wasn't in the
// timeline, and try to edit it via the room itself.
@@ -560,7 +568,8 @@ impl Timeline {
room.send_queue().send(edit_event).await?;
Ok(())
}
Err(err) => Err(err)?,
Err(err) => Err(err.into()),
}
}
@@ -1278,6 +1287,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
#[derive(Clone, uniffi::Enum)]
pub enum EditedContent {
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
PollStart { poll_data: PollData },
}
@@ -1288,6 +1298,12 @@ impl TryFrom<EditedContent> for SdkEditedContent {
EditedContent::RoomMessage { content } => {
Ok(SdkEditedContent::RoomMessage((*content).clone()))
}
EditedContent::MediaCaption { caption, formatted_caption } => {
Ok(SdkEditedContent::MediaCaption {
caption,
formatted_caption: formatted_caption.map(Into::into),
})
}
EditedContent::PollStart { poll_data } => {
let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
Ok(SdkEditedContent::PollStart {
@@ -1299,6 +1315,23 @@ impl TryFrom<EditedContent> for SdkEditedContent {
}
}
/// Create a caption edit.
///
/// If no `formatted_caption` is provided, then it's assumed the `caption`
/// represents valid Markdown that can be used as the formatted caption.
#[matrix_sdk_ffi_macros::export]
fn create_caption_edit(
caption: Option<String>,
formatted_caption: Option<FormattedBody>,
) -> EditedContent {
let formatted_caption =
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
EditedContent::MediaCaption {
caption,
formatted_caption: formatted_caption.as_ref().map(Into::into),
}
}
/// Wrapper to retrieve some timeline item info lazily.
#[derive(Clone, uniffi::Object)]
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
+1 -1
View File
@@ -30,7 +30,7 @@ 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-crypto?/test-send-sync"]
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
+9 -6
View File
@@ -1459,10 +1459,14 @@ impl BaseClient {
pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
match self.olm_machine().await.as_ref() {
Some(o) => {
let (history_visibility, settings) = self
.get_room(room_id)
.map(|r| (r.history_visibility(), r.encryption_settings()))
.unwrap_or((HistoryVisibility::Joined, None));
let Some(room) = self.get_room(room_id) else {
return Err(Error::InsufficientData);
};
let history_visibility = room.history_visibility_or_default();
let Some(room_encryption_event) = room.encryption_settings() else {
return Err(Error::EncryptionNotEnabled);
};
// Don't share the group session with members that are invited
// if the history visibility is set to `Joined`
@@ -1474,9 +1478,8 @@ impl BaseClient {
let members = self.store.get_user_ids(room_id, filter).await?;
let settings = settings.ok_or(Error::EncryptionNotEnabled)?;
let settings = EncryptionSettings::new(
settings,
room_encryption_event,
history_visibility,
self.room_key_recipient_strategy.clone(),
);
+4 -4
View File
@@ -27,7 +27,7 @@ use ruma::{
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));
@@ -41,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))
@@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
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))
@@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
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));
@@ -16,12 +16,17 @@ use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, ti
use async_trait::async_trait;
use matrix_sdk_common::{
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
linked_chunk::{relational::RelationalLinkedChunk, Update},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{MxcUri, OwnedMxcUri};
use ruma::{MxcUri, OwnedMxcUri, RoomId};
use super::{EventCacheStore, EventCacheStoreError, Result};
use crate::media::{MediaRequestParameters, UniqueKey as _};
use crate::{
event_cache::{Event, Gap},
media::{MediaRequestParameters, UniqueKey as _},
};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
@@ -29,8 +34,14 @@ use crate::media::{MediaRequestParameters, UniqueKey as _};
#[allow(clippy::type_complexity)]
#[derive(Debug)]
pub struct MemoryStore {
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
leases: StdRwLock<HashMap<String, (String, Instant)>>,
inner: StdRwLock<MemoryStoreInner>,
}
#[derive(Debug)]
struct MemoryStoreInner {
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
leases: HashMap<String, (String, Instant)>,
events: RelationalLinkedChunk<Event, Gap>,
}
// SAFETY: `new_unchecked` is safe because 20 is not zero.
@@ -39,8 +50,11 @@ 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)),
leases: Default::default(),
inner: StdRwLock::new(MemoryStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
events: RelationalLinkedChunk::new(),
}),
}
}
}
@@ -63,7 +77,20 @@ impl EventCacheStore for MemoryStore {
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
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 add_media_content(
@@ -73,8 +100,10 @@ impl EventCacheStore for MemoryStore {
) -> 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));
let mut inner = self.inner.write().unwrap();
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
Ok(())
}
@@ -86,8 +115,10 @@ impl EventCacheStore for MemoryStore {
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut medias = self.media.write().unwrap();
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
let mut inner = self.inner.write().unwrap();
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
{
*mxc = to.uri().to_owned();
*key = to.unique_key();
}
@@ -98,8 +129,9 @@ impl EventCacheStore for MemoryStore {
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
let expected_key = request.unique_key();
let media = self.media.read().unwrap();
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
let inner = self.inner.read().unwrap();
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
(media_key == &expected_key).then(|| media_content.to_owned())
}))
}
@@ -107,23 +139,27 @@ impl EventCacheStore for MemoryStore {
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
let expected_key = request.unique_key();
let mut media = self.media.write().unwrap();
let Some(index) = media
let mut inner = self.inner.write().unwrap();
let Some(index) = inner
.media
.iter()
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
else {
return Ok(());
};
media.remove(index);
inner.media.remove(index);
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media = self.media.write().unwrap();
let mut inner = self.inner.write().unwrap();
let expected_key = uri.to_owned();
let positions = media
let positions = inner
.media
.iter()
.enumerate()
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
@@ -133,7 +169,7 @@ impl EventCacheStore for MemoryStore {
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
media.remove(position);
inner.media.remove(position);
}
Ok(())
@@ -43,7 +43,7 @@ pub use self::{
#[derive(Clone)]
pub struct EventCacheStoreLock {
/// The inner cross process lock that is used to lock the `EventCacheStore`.
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
/// The store itself.
///
@@ -70,11 +70,11 @@ impl EventCacheStoreLock {
let store = store.into_event_cache_store();
Self {
cross_process_lock: CrossProcessStoreLock::new(
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
),
)),
store,
}
}
@@ -100,13 +100,13 @@ pub struct EventCacheStoreLockGuard<'a> {
}
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
}
}
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
impl Deref for EventCacheStoreLockGuard<'_> {
type Target = DynEventCacheStore;
fn deref(&self) -> &Self::Target {
@@ -15,11 +15,14 @@
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::AsyncTraitDeps;
use ruma::MxcUri;
use matrix_sdk_common::{linked_chunk::Update, AsyncTraitDeps};
use ruma::{MxcUri, RoomId};
use super::EventCacheStoreError;
use crate::media::MediaRequestParameters;
use crate::{
event_cache::{Event, Gap},
media::MediaRequestParameters,
};
/// An abstract trait that can be used to implement different store backends
/// for the event cache of the SDK.
@@ -37,6 +40,15 @@ pub trait EventCacheStore: AsyncTraitDeps {
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>;
/// Add a media file's content in the media store.
///
/// # Arguments
@@ -131,6 +143,14 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
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 add_media_content(
&self,
request: &MediaRequestParameters,
+6 -5
View File
@@ -74,7 +74,7 @@ pub fn is_suitable_for_latest_event<'a>(
// 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 {
@@ -83,12 +83,13 @@ pub fn is_suitable_for_latest_event<'a>(
});
if is_replacement {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
PossibleLatestEvent::NoUnsupportedMessageLikeType
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
return PossibleLatestEvent::YesRoomMessage(message);
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
return PossibleLatestEvent::YesRoomMessage(message);
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
+1
View File
@@ -16,6 +16,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))]
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
#![warn(missing_docs, missing_debug_implementations)]
pub use matrix_sdk_common::*;
+1 -1
View File
@@ -438,7 +438,7 @@ fn events_intersects<'a>(
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
+2 -1
View File
@@ -1,4 +1,5 @@
#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
#![allow(unexpected_cfgs)] // Triggered by the `EventContent` macro usage
mod members;
pub(crate) mod normal;
+125 -75
View File
@@ -487,8 +487,14 @@ impl Room {
}
/// Get the history visibility policy of this room.
pub fn history_visibility(&self) -> HistoryVisibility {
self.inner.read().history_visibility().clone()
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
self.inner.read().history_visibility().cloned()
}
/// Get the history visibility policy of this room, or a sensible default if
/// the event is missing.
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
self.inner.read().history_visibility_or_default().clone()
}
/// Is the room considered to be public.
@@ -573,49 +579,113 @@ impl Room {
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
let update_cache = |new_val: RoomDisplayName| {
self.inner.update_if(|info| {
if info.cached_display_name.as_ref() != Some(&new_val) {
info.cached_display_name = Some(new_val.clone());
true
} else {
false
}
});
new_val
};
enum DisplayNameOrSummary {
Summary(RoomSummary),
DisplayName(RoomDisplayName),
}
let summary = {
let display_name_or_summary = {
let inner = self.inner.read();
if let Some(name) = inner.name() {
let name = name.trim().to_owned();
drop(inner); // drop the lock on `self.inner` to avoid deadlocking in `update_cache`.
return Ok(update_cache(RoomDisplayName::Named(name)));
match (inner.name(), inner.canonical_alias()) {
(Some(name), _) => {
let name = RoomDisplayName::Named(name.trim().to_owned());
DisplayNameOrSummary::DisplayName(name)
}
(None, Some(alias)) => {
let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
DisplayNameOrSummary::DisplayName(name)
}
// We can't directly compute the display name from the summary here because Rust
// thinks that the `inner` lock is still held even if we explicitly call `drop()`
// on it. So we introduced the DisplayNameOrSummary type and do the computation in
// two steps.
(None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
}
if let Some(alias) = inner.canonical_alias() {
let alias = alias.alias().trim().to_owned();
drop(inner); // See above comment.
return Ok(update_cache(RoomDisplayName::Aliased(alias)));
}
inner.summary.clone()
};
// From here, use some heroes to compute the room's name.
let own_user_id = self.own_user_id().as_str();
let display_name = match display_name_or_summary {
DisplayNameOrSummary::Summary(summary) => {
self.compute_display_name_from_summary(summary).await?
}
DisplayNameOrSummary::DisplayName(display_name) => display_name,
};
// Update the cached display name before we return the newly computed value.
self.inner.update_if(|info| {
if info.cached_display_name.as_ref() != Some(&display_name) {
info.cached_display_name = Some(display_name.clone());
true
} else {
false
}
});
Ok(display_name)
}
/// Compute a [`RoomDisplayName`] from the given [`RoomSummary`].
async fn compute_display_name_from_summary(
&self,
summary: RoomSummary,
) -> StoreResult<RoomDisplayName> {
let summary_member_count = summary.joined_member_count + summary.invited_member_count;
let (heroes, num_joined_invited_guess) = if !summary.room_heroes.is_empty() {
let mut names = Vec::with_capacity(summary.room_heroes.len());
for hero in &summary.room_heroes {
if hero.user_id == own_user_id {
continue;
}
if let Some(display_name) = &hero.display_name {
names.push(display_name.clone());
continue;
}
let heroes = self.extract_heroes(&summary.room_heroes).await?;
(heroes, None)
} else {
let (heroes, num_joined_invited) = self.compute_summary().await?;
(heroes, Some(num_joined_invited))
};
let num_joined_invited = if self.state() == RoomState::Invited {
// when we were invited we don't have a proper summary, we have to do best
// guessing
heroes.len() as u64 + 1
} else if summary_member_count == 0 {
if let Some(num_joined_invited) = num_joined_invited_guess {
num_joined_invited
} else {
self.store
.get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
.await?
.len() as u64
}
} else {
summary_member_count
};
debug!(
room_id = ?self.room_id(),
own_user = ?self.own_user_id,
num_joined_invited,
heroes = ?heroes,
"Calculating name for a room based on heroes",
);
let display_name = compute_display_name_from_heroes(
num_joined_invited,
heroes.iter().map(|hero| hero.as_str()).collect(),
);
Ok(display_name)
}
/// Extract and collect the display names of the room heroes from a
/// [`RoomSummary`].
///
/// Returns the display names as a list of strings.
async fn extract_heroes(&self, heroes: &[RoomHero]) -> StoreResult<Vec<String>> {
let own_user_id = self.own_user_id().as_str();
let mut names = Vec::with_capacity(heroes.len());
let heroes = heroes.iter().filter(|hero| hero.user_id != own_user_id);
for hero in heroes {
if let Some(display_name) = &hero.display_name {
names.push(display_name.clone());
} else {
match self.get_member(&hero.user_id).await {
Ok(Some(member)) => {
names.push(member.name().to_owned());
@@ -628,42 +698,9 @@ impl Room {
}
}
}
}
(names, None)
} else {
let (heroes, num_joined_invited) = self.compute_summary().await?;
(heroes, Some(num_joined_invited))
};
let num_joined_invited = if self.state() == RoomState::Invited {
// when we were invited we don't have a proper summary, we have to do best
// guessing
heroes.len() as u64 + 1
} else if summary.joined_member_count == 0 && summary.invited_member_count == 0 {
if let Some(num_joined_invited) = num_joined_invited_guess {
num_joined_invited
} else {
self.store
.get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
.await?
.len() as u64
}
} else {
summary.joined_member_count + summary.invited_member_count
};
debug!(
room_id = ?self.room_id(),
own_user = ?self.own_user_id,
num_joined_invited,
heroes = ?heroes,
"Calculating name for a room based on heroes",
);
Ok(update_cache(compute_display_name_from_heroes(
num_joined_invited,
heroes.iter().map(|hero| hero.as_str()).collect(),
)))
Ok(names)
}
/// Compute the room summary with the data present in the store.
@@ -1491,11 +1528,24 @@ impl RoomInfo {
/// Returns the history visibility for this room.
///
/// Defaults to `WorldReadable`, if missing.
pub fn history_visibility(&self) -> &HistoryVisibility {
/// Returns None if the event was never seen during sync.
pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
match &self.base_info.history_visibility {
Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
_ => None,
}
}
/// Returns the history visibility for this room, or a sensible default.
///
/// Returns `Shared`, the default specified by the [spec], when the event is
/// missing.
///
/// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7
pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
match &self.base_info.history_visibility {
Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
_ => &HistoryVisibility::WorldReadable,
_ => &HistoryVisibility::Shared,
}
}
@@ -90,6 +90,8 @@ pub trait StateStoreIntegrationTests {
async fn test_send_queue_priority(&self);
/// Test operations related to send queue dependents.
async fn test_send_queue_dependents(&self);
/// Test an update to a send queue dependent request.
async fn test_update_send_queue_dependent(&self);
/// Test saving/restoring server capabilities.
async fn test_server_capabilities_saving(&self);
}
@@ -972,6 +974,24 @@ impl StateStoreIntegrationTests for DynStateStore {
self.populate().await?;
{
// Add a send queue request in that room.
let txn = TransactionId::new();
let ev =
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
.unwrap();
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
// Add a single dependent queue request.
self.save_dependent_queued_request(
room_id,
&txn,
ChildTransactionId::new(),
DependentQueuedRequestKind::RedactEvent,
)
.await?;
}
self.remove_room(room_id).await?;
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
@@ -1023,6 +1043,8 @@ impl StateStoreIntegrationTests for DynStateStore {
.is_empty(),
"still event recepts in the store"
);
assert!(self.load_send_queue_requests(room_id).await?.is_empty());
assert!(self.load_dependent_queued_requests(room_id).await?.is_empty());
self.remove_room(stripped_room_id).await?;
@@ -1458,7 +1480,7 @@ impl StateStoreIntegrationTests for DynStateStore {
// Update the event id.
let event_id = owned_event_id!("$1");
let num_updated = self
.update_dependent_queued_request(
.mark_dependent_queued_requests_as_ready(
room_id,
&txn0,
SentRequestKey::Event(event_id.clone()),
@@ -1528,6 +1550,54 @@ impl StateStoreIntegrationTests for DynStateStore {
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 2);
}
async fn test_update_send_queue_dependent(&self) {
let room_id = room_id!("!test_send_queue_dependents:localhost");
let txn = TransactionId::new();
// Save a dependent redaction for an event.
let child_txn = ChildTransactionId::new();
self.save_dependent_queued_request(
room_id,
&txn,
child_txn.clone(),
DependentQueuedRequestKind::RedactEvent,
)
.await
.unwrap();
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].parent_key.is_none());
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
// Make it a reaction, instead of a redaction.
self.update_dependent_queued_request(
room_id,
&child_txn,
DependentQueuedRequestKind::ReactEvent { key: "👍".to_owned() },
)
.await
.unwrap();
// It worked.
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].parent_transaction_id, txn);
assert_eq!(dependents[0].own_transaction_id, child_txn);
assert!(dependents[0].parent_key.is_none());
assert_matches!(
&dependents[0].kind,
DependentQueuedRequestKind::ReactEvent { key } => {
assert_eq!(key, "👍");
}
);
}
}
/// Macro building to allow your StateStore implementation to run the entire
@@ -1686,6 +1756,12 @@ macro_rules! statestore_integration_tests {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_send_queue_dependents().await;
}
#[async_test]
async fn test_update_send_queue_dependent() {
let store = get_store().await.expect("creating store failed").into_state_store();
store.test_update_send_queue_dependent().await;
}
}
};
}
@@ -796,6 +796,8 @@ impl StateStore for MemoryStore {
self.stripped_members.write().unwrap().remove(room_id);
self.room_user_receipts.write().unwrap().remove(room_id);
self.room_event_receipts.write().unwrap().remove(room_id);
self.send_queue_events.write().unwrap().remove(room_id);
self.dependent_send_queue_events.write().unwrap().remove(room_id);
Ok(())
}
@@ -915,7 +917,7 @@ impl StateStore for MemoryStore {
Ok(())
}
async fn update_dependent_queued_request(
async fn mark_dependent_queued_requests_as_ready(
&self,
room: &RoomId,
parent_txn_id: &TransactionId,
@@ -931,6 +933,23 @@ impl StateStore for MemoryStore {
Ok(num_updated)
}
async fn update_dependent_queued_request(
&self,
room: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool, Self::Error> {
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
for d in dependents.iter_mut() {
if d.own_transaction_id == *own_transaction_id {
d.kind = new_content;
return Ok(true);
}
}
Ok(false)
}
async fn remove_dependent_queued_request(
&self,
room: &RoomId,
@@ -367,6 +367,27 @@ pub struct DependentQueuedRequest {
pub parent_key: Option<SentRequestKey>,
}
impl DependentQueuedRequest {
/// Does the dependent request represent a new event that is *not*
/// aggregated, aka it is going to be its own item in a timeline?
pub fn is_own_event(&self) -> bool {
match self.kind {
DependentQueuedRequestKind::EditEvent { .. }
| DependentQueuedRequestKind::RedactEvent
| DependentQueuedRequestKind::ReactEvent { .. }
| DependentQueuedRequestKind::UploadFileWithThumbnail { .. } => {
// These are all aggregated events, or non-visible items (file upload producing
// a new MXC ID).
false
}
DependentQueuedRequestKind::FinishUpload { .. } => {
// This one graduates into a new media event.
true
}
}
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for QueuedRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+27 -5
View File
@@ -424,21 +424,31 @@ pub trait StateStore: AsyncTraitDeps {
content: DependentQueuedRequestKind,
) -> Result<(), Self::Error>;
/// Update a set of dependent send queue requests with a key identifying the
/// homeserver's response, effectively marking them as ready.
/// Mark a set of dependent send queue requests as ready, using a key
/// identifying the homeserver's response.
///
/// ⚠ Beware! There's no verification applied that the parent key type is
/// compatible with the dependent event type. The invalid state may be
/// lazily filtered out in `load_dependent_queued_requests`.
///
/// Returns the number of updated requests.
async fn update_dependent_queued_request(
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error>;
/// Update a dependent send queue request with the new content.
///
/// Returns true if the request was found and could be updated.
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool, Self::Error>;
/// Remove a specific dependent send queue request by id.
///
/// Returns true if the dependent send queue request has been indeed
@@ -709,14 +719,14 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
.map_err(Into::into)
}
async fn update_dependent_queued_request(
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
sent_parent_key: SentRequestKey,
) -> Result<usize, Self::Error> {
self.0
.update_dependent_queued_request(room_id, parent_txn_id, sent_parent_key)
.mark_dependent_queued_requests_as_ready(room_id, parent_txn_id, sent_parent_key)
.await
.map_err(Into::into)
}
@@ -735,6 +745,18 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
self.0.load_dependent_queued_requests(room_id).await.map_err(Into::into)
}
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool, Self::Error> {
self.0
.update_dependent_queued_request(room_id, own_transaction_id, new_content)
.await
.map_err(Into::into)
}
}
/// Convenience functionality for state stores.
+2 -2
View File
@@ -248,7 +248,7 @@ impl Timeline {
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
impl fmt::Debug for DebugInvitedRoomUpdates<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
}
@@ -257,7 +257,7 @@ impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
struct DebugKnockedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, KnockedRoomUpdate>);
#[cfg(not(tarpaulin_include))]
impl<'a> fmt::Debug for DebugKnockedRoomUpdates<'a> {
impl fmt::Debug for DebugKnockedRoomUpdates<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugKnockedRoom(v)))).finish()
}
+7 -1
View File
@@ -44,10 +44,16 @@ wasm-bindgen = "0.2.84"
[dev-dependencies]
assert_matches = { workspace = true }
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
matrix-sdk-test = { workspace = true }
matrix-sdk-test-macros = { path = "../../testing/matrix-sdk-test-macros" }
wasm-bindgen-test = "0.3.33"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Enable the test macro.
tokio = { workspace = true, features = ["rt", "macros"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
# Enable the JS feature for getrandom.
getrandom = { version = "0.2.6", default-features = false, features = ["js"] }
js-sys = "0.3.64"
[lints]
+1 -1
View File
@@ -81,7 +81,7 @@ impl<T: 'static> Future for JoinHandle<T> {
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use super::spawn;
+1 -1
View File
@@ -351,7 +351,7 @@ pub fn make_tracing_subscriber(logger: Option<JsLogger>) -> JsLoggingSubscriber
#[cfg(test)]
pub(crate) mod tests {
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use tracing::{debug, subscriber::with_default};
use wasm_bindgen::{JsCast, JsValue};
@@ -302,6 +302,12 @@ impl UpdateToVectorDiff {
self.chunks.insert(next_chunk_index, (*new, 0));
}
// First chunk!
(None, None) if self.chunks.is_empty() => {
self.chunks.push_back((*new, 0));
}
// Impossible state.
(None, None) => {
unreachable!(
"Inserting new chunk with no previous nor next chunk identifiers \
@@ -405,6 +411,14 @@ impl UpdateToVectorDiff {
// Exiting the _detaching_ mode.
detaching = false;
}
Update::Clear => {
// Clean `self.chunks`.
self.chunks.clear();
// Let's straightforwardly emit a `VectorDiff::Clear`.
diffs.push(VectorDiff::Clear);
}
}
}
@@ -450,10 +464,11 @@ impl UpdateToVectorDiff {
mod tests {
use std::fmt::Debug;
use assert_matches::assert_matches;
use imbl::{vector, Vector};
use super::{
super::{EmptyChunk, LinkedChunk},
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
VectorDiff,
};
@@ -473,6 +488,7 @@ mod tests {
VectorDiff::Remove { index } => {
accumulator.remove(index);
}
VectorDiff::Clear => accumulator.clear(),
diff => unimplemented!("{diff:?}"),
}
}
@@ -686,14 +702,72 @@ mod tests {
&[VectorDiff::Insert { index: 14, value: 'z' }],
);
drop(linked_chunk);
assert!(as_vector.take().is_empty());
// Finally, ensure the “reconstitued” vector is the one expected.
// Ensure the “reconstitued” vector is the one expected.
assert_eq!(
accumulator,
vector!['m', 'a', 'w', 'x', 'y', 'b', 'd', 'i', 'j', 'k', 'l', 'e', 'f', 'g', 'z', 'h']
);
// Let's try to clear the linked chunk now.
linked_chunk.clear();
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Clear]);
assert!(accumulator.is_empty());
drop(linked_chunk);
assert!(as_vector.take().is_empty());
}
#[test]
fn test_as_vector_with_update_clear() {
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
let mut as_vector = linked_chunk.as_vector().unwrap();
{
// 1 initial chunk in the `UpdateToVectorDiff` mapper.
let chunks = &as_vector.mapper.chunks;
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
assert_eq!(chunks[0].1, 0);
assert!(as_vector.take().is_empty());
}
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Append { .. });
assert_matches!(&diffs[1], VectorDiff::Append { .. });
// 2 chunks in the `UpdateToVectorDiff` mapper.
assert_eq!(as_vector.mapper.chunks.len(), 2);
}
linked_chunk.clear();
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 1);
assert_matches!(&diffs[0], VectorDiff::Clear);
// 1 chunk in the `UpdateToVectorDiff` mapper.
let chunks = &as_vector.mapper.chunks;
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].0, ChunkIdentifierGenerator::FIRST_IDENTIFIER);
assert_eq!(chunks[0].1, 0);
}
// And we can push again.
linked_chunk.push_items_back(['a', 'b', 'c', 'd']);
{
let diffs = as_vector.take();
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Append { .. });
assert_matches!(&diffs[1], VectorDiff::Append { .. });
}
}
#[test]
@@ -0,0 +1,465 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::{BTreeMap, HashSet},
marker::PhantomData,
};
use tracing::error;
use super::{
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
ObservableUpdates,
};
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
///
/// Instead of using linking the chunks with pointers, this uses
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
/// which will get resolved later when re-building the full data structure. This
/// allows using chunks that references other chunks that aren't known yet.
struct TemporaryChunk<Item, Gap> {
id: ChunkIdentifier,
previous: Option<ChunkIdentifier>,
next: Option<ChunkIdentifier>,
content: ChunkContent<Item, Gap>,
}
/// A data structure to rebuild a linked chunk from its raw representation.
///
/// A linked chunk can be rebuilt incrementally from its internal
/// representation, with the chunks being added *in any order*, as long as they
/// form a single connected component eventually (viz., there's no
/// subgraphs/sublists isolated from the one final linked list). If they don't,
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
/// error).
#[allow(missing_debug_implementations)]
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
/// Work-in-progress chunks.
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
/// Is the final `LinkedChunk` expected to include an update history, as if
/// it were created with [`LinkedChunk::new_with_update_history`]?
build_with_update_history: bool,
}
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
fn default() -> Self {
Self::new()
}
}
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
/// Create an empty [`LinkedChunkBuilder`] with no update history.
pub fn new() -> Self {
Self { chunks: Default::default(), build_with_update_history: false }
}
/// Stash a gap chunk with its content.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_gap(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
content: Gap,
) {
let chunk = TemporaryChunk { id, previous, next, content: ChunkContent::Gap(content) };
self.chunks.insert(id, chunk);
}
/// Stash an item chunk with its contents.
///
/// This can be called even if the previous and next chunks have not been
/// added yet. Resolving these chunks will happen at the time of calling
/// [`LinkedChunkBuilder::build()`].
pub fn push_items(
&mut self,
previous: Option<ChunkIdentifier>,
id: ChunkIdentifier,
next: Option<ChunkIdentifier>,
items: impl IntoIterator<Item = Item>,
) {
let chunk = TemporaryChunk {
id,
previous,
next,
content: ChunkContent::Items(items.into_iter().collect()),
};
self.chunks.insert(id, chunk);
}
/// Request that the resulting linked chunk will have an update history, as
/// if it were created with [`LinkedChunk::new_with_update_history`].
pub fn with_update_history(&mut self) {
self.build_with_update_history = true;
}
/// Run all error checks before reconstructing the full linked chunk.
///
/// Must be called after checking `self.chunks` isn't empty in
/// [`Self::build`].
///
/// Returns the identifier of the first chunk.
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
// Look for the first id.
let first_id =
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
// it's a malformed list.
let Some(first_id) = first_id else {
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
// We're going to iterate from the first to the last chunk.
// Keep track of chunks we've already visited.
let mut visited = HashSet::new();
// Start from the first chunk.
let mut maybe_cur = Some(first_id);
while let Some(cur) = maybe_cur {
// The chunk must be referenced in `self.chunks`.
let Some(chunk) = self.chunks.get(&cur) else {
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
};
if let ChunkContent::Items(items) = &chunk.content {
if items.len() > CAP {
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
}
}
// If it's not the first chunk,
if cur != first_id {
// It must have a previous link.
let Some(prev) = chunk.previous else {
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
first_candidate: first_id,
second_candidate: cur,
});
};
// And we must have visited its predecessor at this point, since we've
// iterated from the first chunk.
if !visited.contains(&prev) {
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
}
}
// Add the current chunk to the list of seen chunks.
if !visited.insert(cur) {
// If we didn't insert, then it was already visited: there's a cycle!
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
}
// Move on to the next chunk. If it's none, we'll quit the loop.
maybe_cur = chunk.next;
}
// If there are more chunks than those we've visited: some of them were not
// linked to the "main" branch of the linked list, so we had multiple connected
// components.
if visited.len() != self.chunks.len() {
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
}
Ok(first_id)
}
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
if self.chunks.is_empty() {
return Ok(None);
}
// Run checks.
let first_id = self.check_consistency()?;
// We're now going to iterate from the first to the last chunk. As we're doing
// this, we're also doing a few other things:
//
// - rebuilding the final `Chunk`s one by one, that will be linked using
// pointers,
// - counting items from the item chunks we'll encounter,
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
let mut max_chunk_id = first_id.index();
// Small helper to graduate a temporary chunk into a final one. As we're doing
// this, we're also updating the maximum chunk id (that will be used to
// set up the id generator), and the number of items in this chunk.
let mut graduate_chunk = |id: ChunkIdentifier| {
let temp = self.chunks.remove(&id)?;
// Update the maximum chunk identifier, while we're around.
max_chunk_id = max_chunk_id.max(id.index());
// Graduate the current temporary chunk into a final chunk.
let chunk_ptr = Chunk::new_leaked(id, temp.content);
Some((temp.next, chunk_ptr))
};
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingFirstChunk);
};
let mut prev_chunk_ptr = first_chunk_ptr;
while let Some(id) = next_chunk_id {
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
// Can't really happen, but oh well.
return Err(LinkedChunkBuilderError::MissingChunk { id });
};
let chunk = unsafe { chunk_ptr.as_mut() };
// Link the current chunk to its previous one.
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
prev_chunk.next = Some(chunk_ptr);
chunk.previous = Some(prev_chunk_ptr);
// Prepare for the next iteration.
prev_chunk_ptr = chunk_ptr;
next_chunk_id = new_next;
}
debug_assert!(self.chunks.is_empty());
// Maintain the convention that `Ends::last` may be unset.
let last_chunk_ptr = prev_chunk_ptr;
let last_chunk_ptr =
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
let chunk_identifier_generator =
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
max_chunk_id,
));
let updates =
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
}
}
#[derive(thiserror::Error, Debug)]
pub enum LinkedChunkBuilderError {
#[error("chunk with id {} is too large", id.index())]
ChunkTooLarge { id: ChunkIdentifier },
#[error("there's no first chunk")]
MissingFirstChunk,
#[error("there are multiple first chunks")]
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
#[error("unable to resolve chunk with id {}", id.index())]
MissingChunk { id: ChunkIdentifier },
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
Cycle { repeated: ChunkIdentifier },
#[error("multiple connected components")]
MultipleConnectedComponents,
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::LinkedChunkBuilder;
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
#[test]
fn test_empty() {
let lcb = LinkedChunkBuilder::<3, char, char>::new();
// Building an empty linked chunk works, and returns `None`.
let lc = lcb.build().unwrap();
assert!(lc.is_none());
}
#[test]
fn test_success() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
// the chunk id space.
let cid3 = ChunkIdentifier::new(3);
// Check that we can successfully create a linked chunk, independently of the
// order in which chunks are added.
//
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
// Adding chunk cid0.
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
// Adding chunk cid3.
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
// Adding chunk cid1.
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
let mut lc =
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
// Check the entire content first.
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
// Run checks on the first chunk.
let mut chunks = lc.chunks();
let first_chunk = chunks.next().unwrap();
{
assert!(first_chunk.previous().is_none());
assert_eq!(first_chunk.identifier(), cid0);
}
// Run checks on the second chunk.
let second_chunk = chunks.next().unwrap();
{
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
assert_eq!(second_chunk.identifier(), cid1);
}
// Run checks on the third chunk.
let third_chunk = chunks.next().unwrap();
{
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
assert!(third_chunk.next().is_none());
assert_eq!(third_chunk.identifier(), cid3);
}
// There's no more chunk.
assert!(chunks.next().is_none());
// The linked chunk had 5 items.
assert_eq!(lc.len(), 5);
// Now, if we add a new chunk, its identifier should be the previous one we used
// + 1.
lc.push_gap_back('h');
let last_chunk = lc.chunks().last().unwrap();
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
}
#[test]
fn test_chunk_too_large() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
// Adding a chunk with 4 items will fail, because the max capacity specified in
// the builder generics is 3.
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
assert_eq!(id, cid0);
});
}
#[test]
fn test_missing_first_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
}
#[test]
fn test_multiple_first_chunks() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
// Second chunk lies and pretends to be the first too.
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
assert_eq!(first_candidate, cid0);
assert_eq!(second_candidate, cid1);
});
}
#[test]
fn test_missing_chunk() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
assert_eq!(id, cid1);
});
}
#[test]
fn test_cycle() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
assert_eq!(repeated, cid0);
});
}
#[test]
fn test_multiple_connected_components() {
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
let cid0 = ChunkIdentifier::new(0);
let cid1 = ChunkIdentifier::new(1);
let cid2 = ChunkIdentifier::new(2);
// cid0 and cid1 are linked to each other.
lcb.push_gap(None, cid0, Some(cid1), 'g');
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
// cid2 stands on its own.
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
let res = lcb.build();
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
}
}
+196 -89
View File
@@ -93,6 +93,8 @@ macro_rules! assert_items_eq {
}
mod as_vector;
mod builder;
pub mod relational;
mod updates;
use std::{
@@ -103,8 +105,9 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
};
use as_vector::*;
use updates::*;
pub use as_vector::*;
pub use builder::*;
pub use updates::*;
/// Errors of [`LinkedChunk`].
#[derive(thiserror::Error, Debug)]
@@ -190,6 +193,36 @@ impl<const CAP: usize, Item, Gap> Ends<CAP, Item, Gap> {
chunk = chunk.previous_mut()?;
}
}
/// Drop all chunks, and re-create the first one.
fn clear(&mut self) {
// Loop over all chunks, from the last to the first chunk, and drop them.
{
// Take the latest chunk.
let mut current_chunk_ptr = self.last.or(Some(self.first));
// As long as we have another chunk…
while let Some(chunk_ptr) = current_chunk_ptr {
// Fetch the previous chunk pointer.
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
// Re-box the chunk, and let Rust does its job.
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
// Update the `current_chunk_ptr`.
current_chunk_ptr = previous_ptr;
}
// At this step, all chunks have been dropped, including
// `self.first`.
}
// Recreate the first chunk.
self.first = Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER);
// Reset the last chunk.
self.last = None;
}
}
/// The [`LinkedChunk`] structure.
@@ -202,9 +235,6 @@ pub struct LinkedChunk<const CHUNK_CAPACITY: usize, Item, Gap> {
/// The links to the chunks, i.e. the first and the last chunk.
links: Ends<CHUNK_CAPACITY, Item, Gap>,
/// The number of items hold by this linked chunk.
length: usize,
/// The generator of chunk identifiers.
chunk_identifier_generator: ChunkIdentifierGenerator,
@@ -232,7 +262,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER),
last: None,
},
length: 0,
chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(),
updates: None,
marker: PhantomData,
@@ -251,17 +280,30 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
first: Chunk::new_items_leaked(ChunkIdentifierGenerator::FIRST_IDENTIFIER),
last: None,
},
length: 0,
chunk_identifier_generator: ChunkIdentifierGenerator::new_from_scratch(),
updates: Some(ObservableUpdates::new()),
marker: PhantomData,
}
}
/// Get the number of items in this linked chunk.
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.length
/// Clear all the chunks.
pub fn clear(&mut self) {
// Clear `self.links`.
self.links.clear();
// Clear `self.chunk_identifier_generator`.
self.chunk_identifier_generator = ChunkIdentifierGenerator::new_from_scratch();
// “Clear” `self.updates`.
if let Some(updates) = self.updates.as_mut() {
// TODO: Optimisation: Do we want to clear all pending `Update`s in `updates`?
updates.push(Update::Clear);
updates.push(Update::NewItemsChunk {
previous: None,
new: ChunkIdentifierGenerator::FIRST_IDENTIFIER,
next: None,
})
}
}
/// Push items at the end of the [`LinkedChunk`], i.e. on the last
@@ -277,7 +319,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
I::IntoIter: ExactSizeIterator,
{
let items = items.into_iter();
let number_of_items = items.len();
let last_chunk = self.links.latest_chunk_mut();
@@ -294,8 +335,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
// OK.
self.links.last = Some(last_chunk.as_ptr());
}
self.length += number_of_items;
}
/// Push a gap at the end of the [`LinkedChunk`], i.e. after the last
@@ -333,7 +372,7 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
.chunk_mut(chunk_identifier)
.ok_or(Error::InvalidChunkIdentifier { identifier: chunk_identifier })?;
let (chunk, number_of_items) = match &mut chunk.content {
let chunk = match &mut chunk.content {
ChunkContent::Gap(..) => {
return Err(Error::ChunkIsAGap { identifier: chunk_identifier })
}
@@ -347,50 +386,46 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
// Prepare the items to be pushed.
let items = items.into_iter();
let number_of_items = items.len();
(
// Push at the end of the current items.
if item_index == current_items_length {
chunk
// Push the new items.
.push_items(items, &self.chunk_identifier_generator, &mut self.updates)
// Push at the end of the current items.
if item_index == current_items_length {
chunk
// Push the new items.
.push_items(items, &self.chunk_identifier_generator, &mut self.updates)
}
// Insert inside the current items.
else {
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::DetachLastItems {
at: Position(chunk_identifier, item_index),
});
}
// Insert inside the current items.
else {
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::DetachLastItems {
at: Position(chunk_identifier, item_index),
});
}
// Split the items.
let detached_items = current_items.split_off(item_index);
// Split the items.
let detached_items = current_items.split_off(item_index);
let chunk = chunk
// Push the new items.
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
let chunk = chunk
// Push the new items.
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::StartReattachItems);
}
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::StartReattachItems);
}
let chunk = chunk
// Finally, push the items that have been detached.
.push_items(
detached_items.into_iter(),
&self.chunk_identifier_generator,
&mut self.updates,
);
let chunk = chunk
// Finally, push the items that have been detached.
.push_items(
detached_items.into_iter(),
&self.chunk_identifier_generator,
&mut self.updates,
);
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::EndReattachItems);
}
if let Some(updates) = self.updates.as_mut() {
updates.push(Update::EndReattachItems);
}
chunk
},
number_of_items,
)
chunk
}
}
};
@@ -402,8 +437,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
self.links.last = Some(chunk.as_ptr());
}
self.length += number_of_items;
Ok(())
}
@@ -471,8 +504,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
}
}
self.length -= 1;
// Stop borrowing `chunk`.
}
@@ -623,10 +654,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
debug_assert!(chunk.is_first_chunk().not(), "A gap cannot be the first chunk");
let (maybe_last_chunk_ptr, number_of_items) = match &mut chunk.content {
let maybe_last_chunk_ptr = match &mut chunk.content {
ChunkContent::Gap(..) => {
let items = items.into_iter();
let number_of_items = items.len();
let last_inserted_chunk = chunk
// Insert a new items chunk…
@@ -637,11 +667,9 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
// … and insert the items.
.push_items(items, &self.chunk_identifier_generator, &mut self.updates);
(
last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr()),
number_of_items,
)
last_inserted_chunk.is_last_chunk().then(|| last_inserted_chunk.as_ptr())
}
ChunkContent::Items(..) => {
return Err(Error::ChunkIsItems { identifier: chunk_identifier })
}
@@ -663,8 +691,6 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
self.links.last = Some(last_chunk_ptr);
}
self.length += number_of_items;
// Stop borrowing `chunk`.
}
@@ -842,41 +868,30 @@ impl<const CAP: usize, Item, Gap> LinkedChunk<CAP, Item, Gap> {
Some(AsVector::new(updates, token, chunk_iterator))
}
/// Returns the number of items of the linked chunk.
fn len(&self) -> usize {
self.items().count()
}
}
impl<const CAP: usize, Item, Gap> Drop for LinkedChunk<CAP, Item, Gap> {
fn drop(&mut self) {
// Take the latest chunk.
let mut current_chunk_ptr = self.links.last.or(Some(self.links.first));
// As long as we have another chunk…
while let Some(chunk_ptr) = current_chunk_ptr {
// Disconnect the chunk by updating `previous_chunk.next` pointer.
let previous_ptr = unsafe { chunk_ptr.as_ref() }.previous;
if let Some(mut previous_ptr) = previous_ptr {
unsafe { previous_ptr.as_mut() }.next = None;
}
// Re-box the chunk, and let Rust does its job.
let _chunk_boxed = unsafe { Box::from_raw(chunk_ptr.as_ptr()) };
// Update the `current_chunk_ptr`.
current_chunk_ptr = previous_ptr;
}
// At this step, all chunks have been dropped, including
// `self.first`.
// Only clear the links. Calling `Self::clear` would be an error as we don't
// want to emit an `Update::Clear` when `self` is dropped. Instead, we only care
// about freeing memory correctly. Rust can take care of everything except the
// pointers in `self.links`, hence the specific call to `self.links.clear()`.
self.links.clear();
}
}
/// A [`LinkedChunk`] can be safely sent over thread boundaries if `Item: Send`
/// and `Gap: Send`. The only unsafe part if around the `NonNull`, but the API
/// and `Gap: Send`. The only unsafe part is around the `NonNull`, but the API
/// and the lifetimes to deref them are designed safely.
unsafe impl<const CAP: usize, Item: Send, Gap: Send> Send for LinkedChunk<CAP, Item, Gap> {}
/// A [`LinkedChunk`] can be safely share between threads if `Item: Sync` and
/// `Gap: Sync`. The only unsafe part if around the `NonNull`, but the API and
/// `Gap: Sync`. The only unsafe part is around the `NonNull`, but the API and
/// the lifetimes to deref them are designed safely.
unsafe impl<const CAP: usize, Item: Sync, Gap: Sync> Sync for LinkedChunk<CAP, Item, Gap> {}
@@ -931,10 +946,22 @@ impl ChunkIdentifierGenerator {
/// It is not the position of the chunk, just its unique identifier.
///
/// Learn more with [`ChunkIdentifierGenerator`].
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
pub struct ChunkIdentifier(u64);
impl ChunkIdentifier {
/// Create a new [`ChunkIdentifier`].
pub(super) fn new(identifier: u64) -> Self {
Self(identifier)
}
/// Get the underlying identifier.
fn index(&self) -> u64 {
self.0
}
}
impl PartialEq<u64> for ChunkIdentifier {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
@@ -948,6 +975,11 @@ impl PartialEq<u64> for ChunkIdentifier {
pub struct Position(ChunkIdentifier, usize);
impl Position {
/// Create a new [`Position`].
pub(super) fn new(chunk_identifier: ChunkIdentifier, index: usize) -> Self {
Self(chunk_identifier, index)
}
/// Get the chunk identifier of the item.
pub fn chunk_identifier(&self) -> ChunkIdentifier {
self.0
@@ -966,6 +998,16 @@ impl Position {
pub fn decrement_index(&mut self) {
self.1 = self.1.checked_sub(1).expect("Cannot decrement the index because it's already 0");
}
/// Increment the index part (see [`Self::index`]), i.e. add 1.
///
/// # Panic
///
/// This method will panic if it will overflow, i.e. if the index is larger
/// than `usize::MAX`.
pub fn increment_index(&mut self) {
self.1 = self.1.checked_add(1).expect("Cannot increment the index because it's too large");
}
}
/// An iterator over a [`LinkedChunk`] that traverses the chunk in backward
@@ -1053,6 +1095,14 @@ impl<const CAPACITY: usize, Item, Gap> Chunk<CAPACITY, Item, Gap> {
Self { previous: None, next: None, identifier, content }
}
/// Create a new chunk given some content, but box it and leak it.
fn new_leaked(identifier: ChunkIdentifier, content: ChunkContent<Item, Gap>) -> NonNull<Self> {
let chunk = Self::new(identifier, content);
let chunk_box = Box::new(chunk);
NonNull::from(Box::leak(chunk_box))
}
/// Create a new gap chunk, but box it and leak it.
fn new_gap_leaked(identifier: ChunkIdentifier, content: Gap) -> NonNull<Self> {
let chunk = Self::new_gap(identifier, content);
@@ -1322,7 +1372,6 @@ where
.debug_struct("LinkedChunk")
.field("first (deref)", unsafe { self.links.first.as_ref() })
.field("last", &self.links.last)
.field("length", &self.length)
.finish_non_exhaustive()
}
}
@@ -1363,7 +1412,10 @@ impl EmptyChunk {
#[cfg(test)]
mod tests {
use std::ops::Not;
use std::{
ops::Not,
sync::{atomic::Ordering, Arc},
};
use assert_matches::assert_matches;
@@ -2607,4 +2659,59 @@ mod tests {
assert!(chunks.next().unwrap().is_last_chunk());
assert!(chunks.next().is_none());
}
// Test `LinkedChunk::clear`. This test creates a `LinkedChunk` with `new` to
// avoid creating too much confusion with `Update`s. The next test
// `test_clear_emit_an_update_clear` uses `new_with_update_history` and only
// test `Update::Clear`.
#[test]
fn test_clear() {
let mut linked_chunk = LinkedChunk::<3, Arc<char>, Arc<()>>::new();
let item = Arc::new('a');
let gap = Arc::new(());
linked_chunk.push_items_back([
item.clone(),
item.clone(),
item.clone(),
item.clone(),
item.clone(),
]);
linked_chunk.push_gap_back(gap.clone());
linked_chunk.push_items_back([item.clone()]);
assert_eq!(Arc::strong_count(&item), 7);
assert_eq!(Arc::strong_count(&gap), 2);
assert_eq!(linked_chunk.len(), 6);
assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 3);
// Now, we can clear the linked chunk and see what happens.
linked_chunk.clear();
assert_eq!(Arc::strong_count(&item), 1);
assert_eq!(Arc::strong_count(&gap), 1);
assert_eq!(linked_chunk.len(), 0);
assert_eq!(linked_chunk.chunk_identifier_generator.next.load(Ordering::SeqCst), 0);
}
#[test]
fn test_clear_emit_an_update_clear() {
use super::Update::*;
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
linked_chunk.clear();
assert_eq!(
linked_chunk.updates().unwrap().take(),
&[
Clear,
NewItemsChunk {
previous: None,
new: ChunkIdentifierGenerator::FIRST_IDENTIFIER,
next: None
}
]
);
}
}
@@ -0,0 +1,731 @@
// 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.
//! Implementation for a _relational linked chunk_, see
//! [`RelationalLinkedChunk`].
use ruma::{OwnedRoomId, RoomId};
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
/// A row of the [`RelationalLinkedChunk::chunks`].
#[derive(Debug, PartialEq)]
struct ChunkRow {
room_id: OwnedRoomId,
previous_chunk: Option<ChunkIdentifier>,
chunk: ChunkIdentifier,
next_chunk: Option<ChunkIdentifier>,
}
/// A row of the [`RelationalLinkedChunk::items`].
#[derive(Debug, PartialEq)]
struct ItemRow<Item, Gap> {
room_id: OwnedRoomId,
position: Position,
item: Either<Item, Gap>,
}
/// Kind of item.
#[derive(Debug, PartialEq)]
enum Either<Item, Gap> {
/// The content is an item.
Item(Item),
/// The content is a gap.
Gap(Gap),
}
/// 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 usage of the SDK.
///
/// [`LinkedChunk`]: super::LinkedChunk
#[derive(Debug)]
pub struct RelationalLinkedChunk<Item, Gap> {
/// Chunks.
chunks: Vec<ChunkRow>,
/// Items.
items: Vec<ItemRow<Item, Gap>>,
}
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
/// Create a new relational linked chunk.
pub fn new() -> Self {
Self { chunks: Vec::new(), items: Vec::new() }
}
/// Apply [`Update`]s. That's the only way to write data inside this
/// relational linked chunk.
pub fn apply_updates(&mut self, room_id: &RoomId, updates: Vec<Update<Item, Gap>>) {
for update in updates {
match update {
Update::NewItemsChunk { previous, new, next } => {
insert_chunk(&mut self.chunks, room_id, previous, new, next);
}
Update::NewGapChunk { previous, new, next, gap } => {
insert_chunk(&mut self.chunks, room_id, previous, new, next);
self.items.push(ItemRow {
room_id: room_id.to_owned(),
position: Position::new(new, 0),
item: Either::Gap(gap),
});
}
Update::RemoveChunk(chunk_identifier) => {
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
let indices_to_remove = self
.items
.iter()
.enumerate()
.filter_map(
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
(room_id == room_id_candidate
&& position.chunk_identifier() == chunk_identifier)
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
}
}
Update::PushItems { mut at, items } => {
for item in items {
self.items.push(ItemRow {
room_id: room_id.to_owned(),
position: at,
item: Either::Item(item),
});
at.increment_index();
}
}
Update::RemoveItem { at } => {
let mut entry_to_remove = None;
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
self.items.iter_mut().enumerate()
{
// Filter by room ID.
if room_id != room_id_candidate {
continue;
}
// Find the item to remove.
if *position == at {
debug_assert!(entry_to_remove.is_none(), "Found the same entry twice");
entry_to_remove = Some(nth);
}
// Update all items that come _after_ `at` to shift their index.
if position.chunk_identifier() == at.chunk_identifier()
&& position.index() > at.index()
{
position.decrement_index();
}
}
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
}
Update::DetachLastItems { at } => {
let indices_to_remove = self
.items
.iter()
.enumerate()
.filter_map(
|(nth, ItemRow { room_id: room_id_candidate, position, .. })| {
(room_id == room_id_candidate
&& position.chunk_identifier() == at.chunk_identifier()
&& position.index() >= at.index())
.then_some(nth)
},
)
.collect::<Vec<_>>();
for index_to_remove in indices_to_remove.into_iter().rev() {
self.items.remove(index_to_remove);
}
}
Update::StartReattachItems | Update::EndReattachItems => { /* nothing */ }
Update::Clear => {
self.chunks.clear();
self.items.clear();
}
}
}
fn insert_chunk(
chunks: &mut Vec<ChunkRow>,
room_id: &RoomId,
previous: Option<ChunkIdentifier>,
new: ChunkIdentifier,
next: Option<ChunkIdentifier>,
) {
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
room_id == room_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
// Link the chunk.
entry_for_previous_chunk.next_chunk = Some(new);
}
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
room_id == room_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
// Link the chunk.
entry_for_next_chunk.previous_chunk = Some(new);
}
// Insert the chunk.
chunks.push(ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: previous,
chunk: new,
next_chunk: next,
});
}
fn remove_chunk(
chunks: &mut Vec<ChunkRow>,
room_id: &RoomId,
chunk_to_remove: ChunkIdentifier,
) {
let entry_nth_to_remove = chunks
.iter()
.enumerate()
.find_map(|(nth, ChunkRow { room_id: room_id_candidate, chunk, .. })| {
(room_id == room_id_candidate && *chunk == chunk_to_remove).then_some(nth)
})
.expect("Remove an unknown chunk");
let ChunkRow { room_id, previous_chunk: previous, next_chunk: next, .. } =
chunks.remove(entry_nth_to_remove);
// Find the previous chunk, and update its next chunk.
if let Some(previous) = previous {
let entry_for_previous_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
&room_id == room_id_candidate && *chunk == previous
})
.expect("Previous chunk should be present");
// Insert the chunk.
entry_for_previous_chunk.next_chunk = next;
}
// Find the next chunk, and update its previous chunk.
if let Some(next) = next {
let entry_for_next_chunk = chunks
.iter_mut()
.find(|ChunkRow { room_id: room_id_candidate, chunk, .. }| {
&room_id == room_id_candidate && *chunk == next
})
.expect("Next chunk should be present");
// Insert the chunk.
entry_for_next_chunk.previous_chunk = previous;
}
}
}
}
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use ruma::room_id;
use super::{ChunkIdentifier as CId, *};
#[test]
fn test_new_items_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// 2 before 0
Update::NewItemsChunk { previous: None, new: CId::new(2), next: Some(CId::new(0)) },
// 3 between 2 and 0
Update::NewItemsChunk {
previous: Some(CId::new(2)),
new: CId::new(3),
next: Some(CId::new(0)),
},
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(3)),
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(2),
next_chunk: Some(CId::new(3))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(2)),
chunk: CId::new(3),
next_chunk: Some(CId::new(0))
},
],
);
// Items have not been modified.
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_new_gap_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
// 2 after 1
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: Some(CId::new(2))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(1)),
chunk: CId::new(2),
next_chunk: None
},
],
);
// Items contains the gap.
assert_eq!(
relational_linked_chunk.items,
&[ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Gap(())
}],
);
}
#[test]
fn test_remove_chunk() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// 0
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// 1 after 0
Update::NewGapChunk {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: (),
},
// 2 after 1
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
// remove 1
Update::RemoveChunk(CId::new(1)),
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(2))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(2),
next_chunk: None
},
],
);
// Items no longer contains the gap.
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_push_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
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!['a', 'b', 'c'] },
// new chunk (to test new items are pushed in the correct chunk)
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// new items on 1
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
// new items on 0 again
Update::PushItems { at: Position::new(CId::new(0), 3), items: vec!['d', 'e'] },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 3),
item: Either::Item('d')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 4),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_remove_item() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
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!['a', 'b', 'c', 'd', 'e'],
},
// remove an item: 'a'
Update::RemoveItem { at: Position::new(CId::new(0), 0) },
// remove an item: 'd'
Update::RemoveItem { at: Position::new(CId::new(0), 2) },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None
}],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('c')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('e')
},
],
);
}
#[test]
fn test_detach_last_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
// new chunk
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
// new items on 0
Update::PushItems {
at: Position::new(CId::new(0), 0),
items: vec!['a', 'b', 'c', 'd', 'e'],
},
// new items on 1
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['x', 'y', 'z'] },
// detach last items on 0
Update::DetachLastItems { at: Position::new(CId::new(0), 2) },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: Some(CId::new(1))
},
ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: Some(CId::new(0)),
chunk: CId::new(1),
next_chunk: None
},
],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 0),
item: Either::Item('x')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 1),
item: Either::Item('y')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(1), 2),
item: Either::Item('z')
},
],
);
}
#[test]
fn test_start_and_end_reattach_items() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
// Nothing happened.
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items.is_empty());
}
#[test]
fn test_clear() {
let room_id = room_id!("!r0:matrix.org");
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
relational_linked_chunk.apply_updates(
room_id,
vec![
// new chunk (this is not mandatory for this test, but let's try to be realistic)
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!['a', 'b', 'c'] },
],
);
// Chunks are correctly linked.
assert_eq!(
relational_linked_chunk.chunks,
&[ChunkRow {
room_id: room_id.to_owned(),
previous_chunk: None,
chunk: CId::new(0),
next_chunk: None,
}],
);
// Items contains the pushed items.
assert_eq!(
relational_linked_chunk.items,
&[
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 0),
item: Either::Item('a')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 1),
item: Either::Item('b')
},
ItemRow {
room_id: room_id.to_owned(),
position: Position::new(CId::new(0), 2),
item: Either::Item('c')
},
],
);
// Now, time for a clean up.
relational_linked_chunk.apply_updates(room_id, vec![Update::Clear]);
assert!(relational_linked_chunk.chunks.is_empty());
assert!(relational_linked_chunk.items.is_empty());
}
}
@@ -29,6 +29,9 @@ use super::{ChunkIdentifier, Position};
///
/// These updates are useful to store a `LinkedChunk` in another form of
/// storage, like a database or something similar.
///
/// [`LinkedChunk`]: super::LinkedChunk
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
#[derive(Debug, Clone, PartialEq)]
pub enum Update<Item, Gap> {
/// A new chunk of kind Items has been created.
@@ -96,11 +99,17 @@ pub enum Update<Item, Gap> {
/// Reattaching items (see [`Self::StartReattachItems`]) is finished.
EndReattachItems,
/// All chunks have been cleared, i.e. all items and all gaps have been
/// dropped.
Clear,
}
/// A collection of [`Update`]s that can be observed.
///
/// Get a value for this type with [`LinkedChunk::updates`].
///
/// [`LinkedChunk::updates`]: super::LinkedChunk::updates
#[derive(Debug)]
pub struct ObservableUpdates<Item, Gap> {
pub(super) inner: Arc<RwLock<UpdatesInner<Item, Gap>>>,
+4 -5
View File
@@ -343,7 +343,7 @@ mod tests {
};
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use tokio::{
spawn,
time::{sleep, Duration},
@@ -361,7 +361,7 @@ mod tests {
impl TestStore {
fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool {
try_take_leased_lock(&self.leases, lease_duration_ms, key, holder)
try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
}
}
@@ -502,12 +502,11 @@ mod tests {
pub mod memory_store_helper {
use std::{
collections::{hash_map::Entry, HashMap},
sync::RwLock,
time::{Duration, Instant},
};
pub fn try_take_leased_lock(
leases: &RwLock<HashMap<String, (String, Instant)>>,
leases: &mut HashMap<String, (String, Instant)>,
lease_duration_ms: u32,
key: &str,
holder: &str,
@@ -515,7 +514,7 @@ pub mod memory_store_helper {
let now = Instant::now();
let expiration = now + Duration::from_millis(lease_duration_ms.into());
match leases.write().unwrap().entry(key.to_owned()) {
match leases.entry(key.to_owned()) {
// There is an existing holder.
Entry::Occupied(mut entry) => {
let (current_holder, current_expiration) = entry.get_mut();
+1 -1
View File
@@ -62,7 +62,7 @@ where
pub(crate) mod tests {
use std::{future, time::Duration};
use matrix_sdk_test::async_test;
use matrix_sdk_test_macros::async_test;
use super::timeout;
@@ -105,7 +105,7 @@ macro_rules! timer {
#[cfg(test)]
mod tests {
#[cfg(not(target_arch = "wasm32"))]
#[matrix_sdk_test::async_test]
#[matrix_sdk_test_macros::async_test]
async fn test_timer_name() {
use tracing::{span, Level};
+6
View File
@@ -23,6 +23,11 @@ experimental-algorithms = []
uniffi = ["dep:uniffi"]
_disable-minimum-rotation-period-ms = []
# Private feature, see
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
# details.
test-send-sync = []
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
@@ -31,6 +36,7 @@ testing = ["matrix-sdk-test"]
[dependencies]
aes = "0.8.1"
aquamarine = { workspace = true }
as_variant = { workspace = true }
async-trait = { workspace = true }
bs58 = { version = "0.5.0" }
+31 -19
View File
@@ -1,13 +1,11 @@
A no-network-IO implementation of a state machine that handles E2EE for
[Matrix] clients.
# Usage
A no-network-IO implementation of a state machine that handles end-to-end
encryption for [Matrix] clients.
If you're just trying to write a Matrix client or bot in Rust, you're probably
looking for [matrix-sdk] instead.
However, if you're looking to add E2EE to an existing Matrix client or library,
read on.
However, if you're looking to add end-to-end encryption to an existing Matrix
client or library, read on.
The state machine works in a push/pull manner:
@@ -52,28 +50,42 @@ async fn main() -> Result<(), OlmError> {
Ok(())
}
```
It is recommended to use the [tutorial] to understand how end-to-end encryption
works in Matrix and how to add end-to-end encryption support in your Matrix
client library.
[Matrix]: https://matrix.org/
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
# Room key forwarding algorithm
The decision tree below visualizes the way this crate decides whether a message
key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a
key request, provided the `automatic-room-key-forwarding` feature is enabled.
Key forwarding is sometimes also referred to as key *gossiping*.
[forwarded_room_key]: <https://spec.matrix.org/v1.10/client-server-api/#mforwarded_room_key>
![](https://raw.githubusercontent.com/matrix-org/matrix-rust-sdk/main/contrib/key-sharing-algorithm/model.png)
# Crate Feature Flags
The following crate feature flags are available:
* `qrcode`: Enbles QRcode generation and reading code
| Feature | Default | Description |
| ------------------- | :-----: | -------------------------------------------------------------------------------------------------------------------------- |
| `qrcode` | No | Enables QR code based interactive verification |
| `js` | No | Enables JavaScript API usage for things like the current system time on WASM (does nothing on other targets) |
| `testing` | No | Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider. |
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
# Enabling logging
Users of the `matrix-sdk-crypto` crate can enable log output by depending on the
`tracing-subscriber` crate and including the following line in their
application (e.g. at the start of `main`):
```no_compile
tracing_subscriber::fmt::init();
```
The log output is controlled via the `RUST_LOG` environment variable by
setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
The output is printed to stdout.
The `RUST_LOG` variable also supports a more advanced syntax for filtering
log output more precisely, for instance with crate-level granularity. For
more information on this, check out the [tracing-subscriber documentation].
[tracing-subscriber documentation]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/
@@ -56,7 +56,7 @@ impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor
}
}
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
impl<R: Read> Read for AttachmentDecryptor<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner.read(buf)?;
@@ -616,7 +616,6 @@ impl GossipMachine {
/// i.
/// - `Err(x)`: Should *refuse* to share the session. `x` is the reason for
/// the refusal.
#[cfg(feature = "automatic-room-key-forwarding")]
async fn should_share_key(
&self,
@@ -548,7 +548,7 @@ impl IdentityManager {
// First time seen, create the identity. The current MSK will be pinned.
let identity = OtherUserIdentityData::new(master_key, self_signing)?;
let is_verified = maybe_verified_own_identity
.map_or(false, |own_user_identity| own_user_identity.is_identity_signed(&identity));
.is_some_and(|own_user_identity| own_user_identity.is_identity_signed(&identity));
if is_verified {
identity.mark_as_previously_verified();
}
@@ -435,16 +435,20 @@ impl OtherUserIdentity {
Ok(())
}
// Test helper
/// Test helper that marks that an identity has been previously verified and
/// persist the change in the store.
#[cfg(test)]
pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> {
self.inner.mark_as_previously_verified();
let to_save = UserIdentityData::Other(self.inner.clone());
let changes = Changes {
identities: IdentityChanges { changed: vec![to_save], ..Default::default() },
..Default::default()
};
self.verification_machine.store.inner().save_changes(changes).await?;
Ok(())
}
@@ -854,8 +858,8 @@ impl OtherUserIdentityData {
// Check if the new master_key is signed by our own **verified**
// user_signing_key. If the identity was verified we remember it.
let updated_is_verified = maybe_verified_own_user_signing_key
.map_or(false, |own_user_signing_key| {
let updated_is_verified =
maybe_verified_own_user_signing_key.is_some_and(|own_user_signing_key| {
own_user_signing_key.verify_master_key(&master_key).is_ok()
});
+943
View File
@@ -1,4 +1,5 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
// Copyright 2024 Damir Jelić
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,6 +17,7 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs, missing_debug_implementations)]
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
pub mod backups;
mod ciphers;
@@ -156,3 +158,944 @@ pub enum RoomEventDecryptionResult {
/// We were unable to decrypt the event
UnableToDecrypt(UnableToDecryptInfo),
}
#[cfg_attr(doc, aquamarine::aquamarine)]
/// A step by step guide that explains how to include [end-to-end-encryption]
/// support in a [Matrix] client library.
///
/// This crate implements a [sans-network-io](https://sans-io.readthedocs.io/)
/// state machine that allows you to add [end-to-end-encryption] support to a
/// [Matrix] client library.
///
/// This guide aims to provide a comprehensive understanding of end-to-end
/// encryption in Matrix without any prior knowledge requirements. However, it
/// is recommended that the reader has a basic understanding of Matrix and its
/// [client-server specification] for a more informed and efficient learning
/// experience.
///
/// The [introductory](#introduction) section provides a simplified explanation
/// of end-to-end encryption and its implementation in Matrix for those who may
/// not have prior knowledge. If you already have a solid understanding of
/// end-to-end encryption, including the [Olm] and [Megolm] protocols, you may
/// choose to skip directly to the [Getting Started](#getting-started) section.
///
/// # Table of Contents
/// 1. [Introduction](#introduction)
/// 2. [Getting started](#getting-started)
/// 3. [Decrypting room events](#decryption)
/// 4. [Encrypting room events](#encryption)
/// 5. [Interactively verifying devices and user identities](#verification)
///
/// # Introduction
///
/// Welcome to the first part of this guide, where we will introduce the
/// fundamental concepts of end-to-end encryption and its implementation in
/// Matrix.
///
/// This section will provide a clear and concise overview of what
/// end-to-end encryption is and why it is important for secure communication.
/// You will also learn about how Matrix uses end-to-end encryption to protect
/// the privacy and security of its users' communications. Whether you are new
/// to the topic or simply want to improve your understanding, this section will
/// serve as a solid foundation for the rest of the guide.
///
/// Let's dive in!
///
/// ## Notation
///
/// ## End-to-end-encryption
///
/// End-to-end encryption (E2EE) is a method of secure communication where only
/// the communicating devices, also known as "the ends," can read the data being
/// transmitted. This means that the data is encrypted on one device, and can
/// only be decrypted on the other device. The server is used only as a
/// transport mechanism to deliver messages between devices.
///
/// The following chart displays how communication between two clients using a
/// server in the middle usually works.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// bob[Bob]
/// subgraph Homeserver
/// direction LR
/// outbox[Alice outbox]
/// inbox[Bob inbox]
/// outbox -. unencrypted .-> inbox
/// end
///
/// alice -- encrypted --> outbox
/// inbox -- encrypted --> bob
/// ```
///
/// The next chart, instead, displays how the same flow is happening in a
/// end-to-end-encrypted world.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// bob[Bob]
/// subgraph Homeserver
/// direction LR
/// outbox[Alice outbox]
/// inbox[Bob inbox]
/// outbox == encrypted ==> inbox
/// end
///
/// alice == encrypted ==> outbox
/// inbox == encrypted ==> bob
/// ```
///
/// Note that the path from the outbox to the inbox is now encrypted as well.
///
/// Alice and Bob have created a secure communication channel
/// through which they can exchange messages confidentially, without the risk of
/// the server accessing the contents of their messages.
///
/// ## Publishing cryptographic identities of devices
///
/// If Alice and Bob want to establish a secure channel over which they can
/// exchange messages, they first need learn about each others cryptographic
/// identities. This is achieved by using the homeserver as a public key
/// directory.
///
/// A public key directory is used to store and distribute public keys of users
/// in an end-to-end encrypted system. The basic idea behind a public key
/// directory is that it allows users to easily discover and download the public
/// keys of other users with whom they wish to establish an end-to-end encrypted
/// communication.
///
/// Each user generates a pair of public and private keys. The user then uploads
/// their public key to the public key directory. Other users can then search
/// the directory to find the public key of the user they wish to communicate
/// with, and download it to their own device.
///
/// ```mermaid
/// flowchart LR
/// alice[Alice]
/// subgraph homeserver[Homeserver]
/// direction LR
/// directory[(Public key directory)]
/// end
/// bob[Bob]
///
/// alice -- upload keys --> directory
/// directory -- download keys --> bob
/// ```
///
/// Once a user has the other user's public key, they can use it to establish an
/// end-to-end encrypted channel using a [key-agreement protocol].
///
/// ## Using the Triple Diffie-Hellman key-agreement protocol
///
/// In the triple Diffie-Hellman key agreement protocol (3DH in short), each
/// user generates a long-term identity key pair and a set of one-time prekeys.
/// When two users want to establish a shared secret key, they exchange their
/// public identity keys and one of their prekeys. These public keys are then
/// used in a [Diffie-Hellman] key exchange to compute a shared secret key.
///
/// The use of one-time prekeys ensures that the shared secret key is different
/// for each session, even if the same identity keys are used.
///
/// ```mermaid
/// flowchart LR
/// subgraph alice_keys[Alice Keys]
/// direction TB
/// alice_key[Alice's identity key]
/// alice_base_key[Alice's one-time key]
/// end
///
/// subgraph bob_keys[Bob Keys]
/// direction TB
/// bob_key[Bob's identity key]
/// bob_one_time[Bob's one-time key]
/// end
///
/// alice_key <--> bob_one_time
/// alice_base_key <--> bob_one_time
/// alice_base_key <--> bob_key
/// ```
///
/// Similar to [X3DH] (Extended Triple Diffie-Hellman) key agreement protocol
///
/// ## Speeding up encryption for large groups
///
/// In the previous section we learned how to utilize a key agreement protocol
/// to establish secure 1-to-1 encrypted communication channels. These channels
/// allow us to encrypt a message for each device separately.
///
/// One critical property of these channels is that, if you want to send a
/// message to a group of devices, we'll need to encrypt the message for each
/// device individually.
///
/// TODO Explain how megolm fits into this
///
/// # Getting started
///
/// Before we start writing any code, let us get familiar with the basic
/// principle upon which this library is built.
///
/// The central piece of the library is the [`OlmMachine`] which acts as a state
/// machine which consumes data that gets received from the homeserver and
/// outputs data which should be sent to the homeserver.
///
/// ## Push/pull mechanism
///
/// The [`OlmMachine`] at the heart of it acts as a state machine that operates
/// in a push/pull manner. HTTP responses which were received from the
/// homeserver get forwarded into the [`OlmMachine`] and in turn the internal
/// state gets updated which produces HTTP requests that need to be sent to the
/// homeserver.
///
/// In a manner, we're pulling data from the server, we update our internal
/// state based on the data and in turn push data back to the server.
///
/// ```mermaid
/// flowchart LR
/// homeserver[Homeserver]
/// client[OlmMachine]
///
/// homeserver -- pull --> client
/// client -- push --> homeserver
/// ```
///
/// ## Initializing the state machine
///
/// ```
/// use anyhow::Result;
/// use matrix_sdk_crypto::OlmMachine;
/// use ruma::user_id;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let user_id = user_id!("@alice:localhost");
/// let device_id = "DEVICEID".into();
///
/// let machine = OlmMachine::new(user_id, device_id).await;
/// # Ok(())
/// # }
/// ```
///
/// This will create a [`OlmMachine`] that does not persist any data TODO
/// ```ignore
/// use anyhow::Result;
/// use matrix_sdk_crypto::OlmMachine;
/// use matrix_sdk_sqlite::SqliteCryptoStore;
/// use ruma::user_id;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let user_id = user_id!("@alice:localhost");
/// let device_id = "DEVICEID".into();
///
/// let store = SqliteCryptoStore::open("/home/example/matrix-client/", None).await?;
///
/// let machine = OlmMachine::with_store(user_id, device_id, store).await;
/// # Ok(())
/// # }
/// ```
///
/// # Decryption
///
/// In the world of encrypted communication, it is common to start with the
/// encryption step when implementing a protocol. However, in the case of adding
/// end-to-end encryption support to a Matrix client library, a simpler approach
/// is to first focus on the decryption process. This is because there are
/// already Matrix clients in existence that support encryption, which means
/// that our client library can simply receive encrypted messages and then
/// decrypt them.
///
/// In this section, we will guide you through the minimal steps
/// necessary to get the decryption process up and running using the
/// matrix-sdk-crypto Rust crate. By the end of this section you should have a
/// Matrix client that is able to decrypt room events that other clients have
/// sent.
///
/// To enable decryption the following three steps are needed:
///
/// 1. [The cryptographic identity of your device needs to be published to the
/// homeserver](#uploading-identity-and-one-time-keys).
/// 2. [Decryption keys coming in from other devices need to be processed and
/// stored](#receiving-room-keys-and-related-changes).
/// 3. [Individual messages need to be decrypted](#decrypting-room-events).
///
/// The simplified flowchart
/// ```mermaid
/// graph TD
/// sync[Sync with the homeserver]
/// receive_changes[Push E2EE related changes into the state machine]
/// send_outgoing_requests[Send all outgoing requests to the homeserver]
/// decrypt[Process the rest of the sync]
///
/// sync --> receive_changes;
/// receive_changes --> send_outgoing_requests;
/// send_outgoing_requests --> decrypt;
/// decrypt -- repeat --> sync;
/// ```
///
/// ## Uploading identity and one-time keys.
///
/// To enable end-to-end encryption in a Matrix client, the first step is to
/// announce the support for it to other users in the network. This is done by
/// publishing the client's long-term device keys and a set of one-time prekeys
/// to the Matrix homeserver. The homeserver then makes this information
/// available to other devices in the network.
///
/// The long-term device keys and one-time prekeys allow other devices to
/// encrypt messages specifically for your device.
///
/// To achieve this, you will need to extract any requests that need to be sent
/// to the homeserver from the [`OlmMachine`] and send them to the homeserver.
/// The following snippet showcases how to achieve this using the
/// [`OlmMachine::outgoing_requests()`] method:
///
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use ruma::api::client::keys::upload_keys::v3::Response;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest};
/// # async fn send_request(request: OutgoingRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let machine: OlmMachine = unimplemented!();
/// // Get all the outgoing requests.
/// let outgoing_requests = machine.outgoing_requests().await?;
///
/// // Send each request to the server and push the response into the state machine.
/// // You can safely send these requests out in parallel.
/// for request in outgoing_requests {
/// let request_id = request.request_id();
/// // Send the request to the server and await a response.
/// let response = send_request(request).await?;
/// // Push the response into the state machine.
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// #### 🔒 Locking rule
///
/// It's important to note that the outgoing requests method in the
/// [`OlmMachine`], while thread-safe, may return the same request multiple
/// times if it is called multiple times before the request has been marked as
/// sent. To prevent this issue, it is advisable to encapsulate the outgoing
/// request handling logic into a separate helper method and protect it from
/// being called multiple times concurrently using a lock.
///
/// This helps to ensure that the request is only handled once and prevents
/// multiple identical requests from being sent.
///
/// Additionally, if an error occurs while sending a request using the
/// [`OlmMachine::outgoing_requests()`] method, the request will be
/// naturally retried the next time the method is called.
///
/// A more complete example, which uses a helper method, might look like this:
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use ruma::api::client::keys::upload_keys::v3::Response;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, OutgoingRequest};
/// # async fn send_request(request: &OutgoingRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// struct Client {
/// outgoing_requests_lock: tokio::sync::Mutex<()>,
/// olm_machine: OlmMachine,
/// }
///
/// async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// // Let's acquire a lock so we know that we don't send out the same request out multiple
/// // times.
/// let guard = client.outgoing_requests_lock.lock().await;
///
/// for request in client.olm_machine.outgoing_requests().await? {
/// let request_id = request.request_id();
///
/// match send_request(&request).await {
/// Ok(response) => {
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// Err(error) => {
/// // It's OK to ignore transient HTTP errors since requests will be retried.
/// eprintln!(
/// "Error while sending out a end-to-end encryption \
/// related request: {error:?}"
/// );
/// }
/// }
/// }
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// Once we have the helper method that processes our outgoing requests we can
/// structure our sync method as follows:
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::OlmMachine;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// // This is happening at the top of the method so we advertise our
/// // end-to-end encryption capabilities as soon as possible.
/// process_outgoing_requests(client).await?;
///
/// // We can sync with the homeserver now.
/// let response = send_out_sync_request(client).await?;
///
/// // Process the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// ## Receiving room keys and related changes
///
/// The next step in our implementation is to forward messages that were sent
/// directly to the client's device, and state updates about the one-time
/// prekeys, to the [`OlmMachine`]. This is achieved using
/// the [`OlmMachine::receive_sync_changes()`] method.
///
/// The method performs two tasks:
///
/// 1. It processes and, if necessary, decrypts each [to-device] event that was
/// pushed into it, and returns the decrypted events. The original events are
/// replaced with their decrypted versions.
///
/// 2. It produces internal state changes that may trigger the creation of new
/// outgoing requests. For example, if the server informs the client that its
/// one-time prekeys have been depleted, the OlmMachine will create an
/// outgoing request to replenish them.
///
/// Our updated sync method now looks like this:
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
/// # use ruma::api::client::sync::sync_events::v3::Response;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
/// # unimplemented!();
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// process_outgoing_requests(client).await?;
///
/// let response = send_out_sync_request(client).await?;
///
/// let sync_changes = EncryptionSyncChanges {
/// to_device_events: response.to_device.events,
/// changed_devices: &response.device_lists,
/// one_time_keys_counts: &response.device_one_time_keys_count,
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
/// next_batch_token: Some(response.next_batch),
/// };
///
/// // Push the sync changes into the OlmMachine, make sure that this is
/// // happening before the `next_batch` token of the sync is persisted.
/// let to_device_events = client
/// .olm_machine
/// .receive_sync_changes(sync_changes)
/// .await?;
///
/// // Send the outgoing requests out that the sync changes produced.
/// process_outgoing_requests(client).await?;
///
/// // Process the rest of the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// It is important to note that the names of the fields in the response shown
/// in the example match the names of the fields specified in the [sync]
/// response specification.
///
/// It is critical to note that due to the ephemeral nature of to-device
/// events[[1]], it is important to process these events before persisting the
/// `next_batch` sync token. This is because if the `next_batch` sync token is
/// persisted before processing the to-device events, some messages might be
/// lost, leading to decryption failures.
///
/// ## Decrypting room events
///
/// The final step in the decryption process is to decrypt the room events that
/// are received from the server. To do this, the encrypted events must be
/// passed to the [`OlmMachine`], which will use the keys that were previously
/// exchanged between devices to decrypt the events. The decrypted events can
/// then be processed and displayed to the user in the Matrix client.
///
/// Room message [events] can be decrypted using the
/// [`OlmMachine::decrypt_room_event()`] method:
///
/// ```no_run
/// # use std::collections::BTreeMap;
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{OlmMachine, DecryptionSettings, TrustRequirement};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let encrypted = unimplemented!();
/// # let room_id = unimplemented!();
/// # let machine: OlmMachine = unimplemented!();
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
/// // Decrypt your room events now.
/// let decrypted = machine
/// .decrypt_room_event(encrypted, room_id, &settings)
/// .await?;
/// # Ok(())
/// # }
/// ```
/// It's worth mentioning that the [`OlmMachine::decrypt_room_event()`] method
/// is designed to be thread-safe and can be safely called concurrently. This
/// means that room message [events] can be processed in parallel, improving the
/// overall efficiency of the end-to-end encryption implementation.
///
/// By allowing room message [events] to be processed concurrently, the client's
/// implementation can take full advantage of the capabilities of modern
/// hardware and achieve better performance, especially when dealing with a
/// large number of messages at once.
///
/// # Encryption
///
/// In this section of the guide, we will focus on enabling the encryption of
/// messages in our Matrix client library. Up until this point, we have been
/// discussing the process of decrypting messages that have been encrypted by
/// other devices. Now, we will shift our focus to the process of encrypting
/// messages on the client side, so that they can be securely transmitted over
/// the Matrix network to other devices.
///
/// This section will guide you through the steps required to set up the
/// encryption process, including establishing the necessary sessions and
/// encrypting messages using the Megolm group session. The specific steps are
/// outlined bellow:
///
/// 1. [Cryptographic devices of other users need to be
/// discovered](#tracking-users)
///
/// 2. [Secure channels between the devices need to be
/// established](#establishing-end-to-end-encrypted-channels)
///
/// 3. [A room key needs to be exchanged with the group](#exchanging-room-keys)
///
/// 4. [Individual messages need to be encrypted using the room
/// key](#encrypting-room-events)
///
/// The process for enabling encryption in a two-device scenario is also
/// depicted in the following sequence diagram:
///
/// ```mermaid
/// sequenceDiagram
/// actor Alice
/// participant Homeserver
/// actor Bob
///
/// Alice->>Homeserver: Download Bob's one-time prekey
/// Homeserver->>Alice: Bob's one-time prekey
/// Alice->>Alice: Encrypt the room key
/// Alice->>Homeserver: Send the room key to each of Bob's devices
/// Homeserver->>Bob: Deliver the room key
/// Alice->>Alice: Encrypt the message
/// Alice->>Homeserver: Send the encrypted message
/// Homeserver->>Bob: Deliver the encrypted message
/// ```
///
/// In the following subsections, we will provide a step-by-step guide on how to
/// enable the encryption of messages using the OlmMachine. We will outline the
/// specific method calls and usage patterns that are required to establish the
/// necessary sessions, encrypt messages, and send them over the Matrix network.
///
/// ## Tracking users
///
/// The first step in the process of encrypting a message and sending it to a
/// device is to discover the devices that the recipient user has. This can be
/// achieved by sending a request to the homeserver to retrieve a list of the
/// recipient's device keys. The response to this request will include the
/// device keys for all of the devices that belong to the recipient, as well as
/// information about their current status and whether or not they support
/// end-to-end encryption.
///
/// The process for discovering and keeping track of devices for a user is
/// outlined in the Matrix specification in the "[Tracking the device list for a
/// user]" section.
///
/// A simplified sequence diagram of the process can also be found bellow.
///
/// ```mermaid
/// sequenceDiagram
/// actor Alice
/// participant Homeserver
///
/// Alice->>Homeserver: Sync with the homeserver
/// Homeserver->>Alice: Users whose device list has changed
/// Alice->>Alice: Mark user's devicel list as outdated
/// Alice->>Homeserver: Ask the server for the new device list of all the outdated users
/// Alice->>Alice: Update the local device list and mark the users as up-to-date
/// ```
///
/// The OlmMachine refers to users whose devices we are tracking as "tracked
/// users" and utilizes the [`OlmMachine::update_tracked_users()`] method to
/// start considering users to be tracked. Keeping the above diagram in mind, we
/// can now update our sync method as follows:
///
/// ```no_run
/// # use anyhow::Result;
/// # use std::ops::Deref;
/// # use matrix_sdk_crypto::{EncryptionSyncChanges, OlmMachine};
/// # use ruma::api::client::sync::sync_events::v3::{Response, JoinedRoom};
/// # use ruma::{OwnedUserId, serde::Raw, events::AnySyncStateEvent};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # struct Client {
/// # outgoing_requests_lock: tokio::sync::Mutex<()>,
/// # olm_machine: OlmMachine,
/// # }
/// # async fn process_outgoing_requests(client: &Client) -> Result<()> {
/// # unimplemented!();
/// # }
/// # async fn send_out_sync_request(client: &Client) -> Result<Response> {
/// # unimplemented!();
/// # }
/// # fn is_member_event_of_a_joined_user(event: &Raw<AnySyncStateEvent>) -> bool {
/// # true
/// # }
/// # fn get_user_id(event: &Raw<AnySyncStateEvent>) -> OwnedUserId {
/// # unimplemented!();
/// # }
/// # fn is_room_encrypted(room: &JoinedRoom) -> bool {
/// # true
/// # }
/// async fn sync(client: &Client) -> Result<()> {
/// process_outgoing_requests(client).await?;
///
/// let response = send_out_sync_request(client).await?;
///
/// let sync_changes = EncryptionSyncChanges {
/// to_device_events: response.to_device.events,
/// changed_devices: &response.device_lists,
/// one_time_keys_counts: &response.device_one_time_keys_count,
/// unused_fallback_keys: response.device_unused_fallback_key_types.as_deref(),
/// next_batch_token: Some(response.next_batch),
/// };
///
/// // Push the sync changes into the OlmMachine, make sure that this is
/// // happening before the `next_batch` token of the sync is persisted.
/// let to_device_events = client
/// .olm_machine
/// .receive_sync_changes(sync_changes)
/// .await?;
///
/// // Send the outgoing requests out that the sync changes produced.
/// process_outgoing_requests(client).await?;
///
/// // Collect all the joined and invited users of our end-to-end encrypted rooms here.
/// let mut users = Vec::new();
///
/// for (_, room) in &response.rooms.join {
/// // For simplicity reasons we're only looking at the state field of a joined room, but
/// // the events in the timeline are important as well.
/// for event in &room.state.events {
/// if is_member_event_of_a_joined_user(event) && is_room_encrypted(room) {
/// let user_id = get_user_id(event);
/// users.push(user_id);
/// }
/// }
/// }
///
/// // Mark all the users that we consider to be in a end-to-end encrypted room with us to be
/// // tracked. We need to know about all the devices each user has so we can later encrypt
/// // messages for each of their devices.
/// client.olm_machine.update_tracked_users(users.iter().map(Deref::deref)).await?;
///
/// // Process the rest of the sync response here.
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// Now that we have discovered the devices of the users we'd like to
/// communicate with in an end-to-end encrypted manner, we can start considering
/// encrypting messages for those devices. This concludes the sync processing
/// method, we are now ready to move on to the next section, which will explain
/// how to begin the encryption process.
///
/// ## Establishing end-to-end encrypted channels
///
/// In the [Triple
/// Diffie-Hellman](#using-the-triple-diffie-hellman-key-agreement-protocol)
/// section, we described the need for two Curve25519 keys from the recipient
/// device to establish a 1-to-1 secure channel: the long-term identity key of a
/// device and a one-time prekey. In the previous section, we started tracking
/// the device keys, including the long-term identity key that we need. The next
/// step is to download the one-time prekey on an on-demand basis and establish
/// the 1-to-1 secure channel.
///
/// To accomplish this, we can use the [`OlmMachine::get_missing_sessions()`]
/// method in bulk, which will claim the one-time prekey for all the devices of
/// a user that we're not already sharing a 1-to-1 encrypted channel with.
///
/// #### 🔒 Locking rule
///
/// As with the [`OlmMachine::outgoing_requests()`] method, it is necessary to
/// protect this method with a lock, otherwise we will be creating more 1-to-1
/// encrypted channels than necessary.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use ruma::UserId;
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::OlmMachine;
/// # async fn send_request(request: &Request) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let machine: OlmMachine = unimplemented!();
/// // Mark all the users that are part of an encrypted room as tracked
/// if let Some((request_id, request)) =
/// machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
/// {
/// let response = send_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// With the ability to exchange messages directly with devices, we can now
/// start sharing room keys over the 1-to-1 encrypted channel.
///
/// ## Exchanging room keys
///
/// To exchange a room key with our group, we will once again take a bulk
/// approach. The [`OlmMachine::share_room_key()`] method is used to accomplish
/// this step. This method will create a new room key, if necessary, and encrypt
/// it for each device belonging to the users provided as an argument. It will
/// then output an array of sendToDevice requests that we must send to the
/// server, and mark the requests as sent.
///
/// #### 🔒 Locking rule
///
/// Like some of the previous methods, OlmMachine::share_room_key() needs to be
/// protected by a lock to prevent the possibility of creating and sending
/// multiple room keys simultaneously for the same group. The lock can be
/// implemented on a per-room basis, which allows for parallel room key
/// exchanges across different rooms.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use ruma::UserId;
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::{OlmMachine, requests::ToDeviceRequest, EncryptionSettings};
/// # async fn send_request(request: &ToDeviceRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let room_id = unimplemented!();
/// # let settings = EncryptionSettings::default();
/// # let machine: OlmMachine = unimplemented!();
/// // Let's share a room key with our group.
/// let requests = machine.share_room_key(
/// room_id,
/// users.iter().map(Deref::deref),
/// EncryptionSettings::default(),
/// ).await?;
///
/// // Make sure each request is sent out
/// for request in requests {
/// let request_id = &request.txn_id;
/// let response = send_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// In order to ensure that room keys are rotated and exchanged when needed, the
/// [`OlmMachine::share_room_key()`] method should be called before sending
/// each room message in an end-to-end encrypted room. If a room key has
/// already been exchanged, the method becomes a no-op.
///
/// ## Encrypting room events
///
/// After the room key has been successfully shared, a plaintext can be
/// encrypted.
///
/// ```no_run
/// # use anyhow::Result;
/// # use matrix_sdk_crypto::{DecryptionSettings, OlmMachine, TrustRequirement};
/// # use ruma::events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let room_id = unimplemented!();
/// # let event = unimplemented!();
/// # let machine: OlmMachine = unimplemented!();
/// # let settings = DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
/// let content = AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain("It's a secret to everybody."));
/// let encrypted_content = machine.encrypt_room_event(room_id, content).await?;
/// # Ok(())
/// # }
/// ```
///
/// ## Appendix: Combining the session creation and room key exchange
///
/// The steps from the previous three sections should combined into a single
/// method that is used to send messages.
///
/// ```no_run
/// # use std::collections::{BTreeMap, HashSet};
/// # use std::ops::Deref;
/// # use anyhow::Result;
/// # use serde_json::json;
/// # use ruma::{UserId, RoomId, serde::Raw};
/// # use ruma::api::client::keys::claim_keys::v3::{Response, Request};
/// # use matrix_sdk_crypto::{EncryptionSettings, OlmMachine, ToDeviceRequest};
/// # use tokio::sync::MutexGuard;
/// # async fn send_request(request: &Request) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # async fn send_to_device_request(request: &ToDeviceRequest) -> Result<Response> {
/// # let response = unimplemented!();
/// # Ok(response)
/// # }
/// # async fn acquire_per_room_lock(room_id: &RoomId) -> MutexGuard<()> {
/// # unimplemented!();
/// # }
/// # async fn get_joined_members(room_id: &RoomId) -> Vec<&UserId> {
/// # unimplemented!();
/// # }
/// # fn is_room_encrypted(room_id: &RoomId) -> bool {
/// # true
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # let users: HashSet<&UserId> = HashSet::new();
/// # let machine: OlmMachine = unimplemented!();
/// struct Client {
/// session_establishment_lock: tokio::sync::Mutex<()>,
/// olm_machine: OlmMachine,
/// }
///
/// async fn establish_sessions(client: &Client, users: &[&UserId]) -> Result<()> {
/// if let Some((request_id, request)) =
/// client.olm_machine.get_missing_sessions(users.iter().map(Deref::deref)).await?
/// {
/// let response = send_request(&request).await?;
/// client.olm_machine.mark_request_as_sent(&request_id, &response).await?;
/// }
///
/// Ok(())
/// }
///
/// async fn share_room_key(machine: &OlmMachine, room_id: &RoomId, users: &[&UserId]) -> Result<()> {
/// let _lock = acquire_per_room_lock(room_id).await;
///
/// let requests = machine.share_room_key(
/// room_id,
/// users.iter().map(Deref::deref),
/// EncryptionSettings::default(),
/// ).await?;
///
/// // Make sure each request is sent out
/// for request in requests {
/// let request_id = &request.txn_id;
/// let response = send_to_device_request(&request).await?;
/// machine.mark_request_as_sent(&request_id, &response).await?;
/// }
///
/// Ok(())
/// }
///
/// async fn send_message(client: &Client, room_id: &RoomId, message: &str) -> Result<()> {
/// let mut content = json!({
/// "body": message,
/// "msgtype": "m.text",
/// });
///
/// if is_room_encrypted(room_id) {
/// let content = Raw::new(&json!({
/// "body": message,
/// "msgtype": "m.text",
/// }))?.cast();
///
/// let users = get_joined_members(room_id).await;
///
/// establish_sessions(client, &users).await?;
/// share_room_key(&client.olm_machine, room_id, &users).await?;
///
/// let encrypted = client
/// .olm_machine
/// .encrypt_room_event_raw(room_id, "m.room.message", &content)
/// .await?;
/// }
///
/// Ok(())
/// }
/// # Ok(())
/// # }
/// ```
///
/// TODO
///
/// [Matrix]: https://matrix.org/
/// [Olm]: https://gitlab.matrix.org/matrix-org/olm/-/blob/master/docs/olm.md
/// [Diffie-Hellman]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
/// [Megolm]: https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md
/// [end-to-end-encryption]: https://en.wikipedia.org/wiki/End-to-end_encryption
/// [homeserver]: https://spec.matrix.org/unstable/#architecture
/// [key-agreement protocol]: https://en.wikipedia.org/wiki/Key-agreement_protocol
/// [client-server specification]: https://matrix.org/docs/spec/client_server/
/// [forward secrecy]: https://en.wikipedia.org/wiki/Forward_secrecy
/// [replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
/// [Tracking the device list for a user]: https://spec.matrix.org/unstable/client-server-api/#tracking-the-device-list-for-a-user
/// [X3DH]: https://signal.org/docs/specifications/x3dh/
/// [to-device]: https://spec.matrix.org/unstable/client-server-api/#send-to-device-messaging
/// [sync]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3sync
/// [events]: https://spec.matrix.org/unstable/client-server-api/#events
///
/// [1]: https://spec.matrix.org/unstable/client-server-api/#server-behaviour-4
pub mod tutorial {}
+2 -2
View File
@@ -2311,8 +2311,8 @@ impl OlmMachine {
/// incremented and updated it in the database. Otherwise, `false`.
///
/// * The (possibly updated) generation counter.
pub async fn maintain_crypto_store_generation<'a>(
&'a self,
pub async fn maintain_crypto_store_generation(
&'_ self,
generation: &Mutex<Option<u64>>,
) -> StoreResult<(bool, u64)> {
let mut gen_guard = generation.lock().await;
+2
View File
@@ -222,6 +222,8 @@ pub enum OutgoingRequests {
#[cfg(test)]
impl OutgoingRequests {
/// Test helper to destructure the [`OutgoingRequests`] as a
/// [`ToDeviceRequest`].
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
as_variant!(self, Self::ToDeviceRequest)
}
@@ -102,7 +102,7 @@ impl CryptoStoreWrapper {
.await?
.as_ref()
.and_then(|i| i.own())
.map_or(false, |own| own.is_verified());
.is_some_and(|own| own.is_verified());
let secrets = changes.secrets.to_owned();
let devices = changes.devices.to_owned();
@@ -28,7 +28,6 @@
/// cryptostore_integration_tests!();
/// }
/// ```
#[allow(unused_macros)]
#[macro_export]
macro_rules! cryptostore_integration_tests {
@@ -632,7 +632,7 @@ impl CryptoStore for MemoryStore {
key: &str,
holder: &str,
) -> Result<bool> {
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
Ok(try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder))
}
}
+1 -1
View File
@@ -231,7 +231,7 @@ pub(crate) struct SyncedKeyQueryManager<'a> {
manager: &'a KeyQueryManager,
}
impl<'a> SyncedKeyQueryManager<'a> {
impl SyncedKeyQueryManager<'_> {
/// Add entries to the list of users being tracked for device changes
///
/// Any users not already on the list are flagged as awaiting a key query.
@@ -138,7 +138,7 @@ pub(crate) enum CrossSigningSubKeys<'a> {
UserSigning(&'a UserSigningPubkey),
}
impl<'a> CrossSigningSubKeys<'a> {
impl CrossSigningSubKeys<'_> {
/// Get the id of the user that owns this cross signing subkey.
pub fn user_id(&self) -> &UserId {
match self {
@@ -184,6 +184,11 @@ where
impl<C: EventType + Debug + Sized + Serialize> DecryptedOlmV1Event<C> {
#[cfg(test)]
/// Test helper to create a new [`DecryptedOlmV1Event`] with the given
/// content.
///
/// This should never be done in real code as we need to deserialize
/// decrypted events.
pub fn new(
sender: &UserId,
recipient: &UserId,
@@ -33,6 +33,19 @@ pub struct VerificationCache {
inner: Arc<VerificationCacheInner>,
}
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
#[cfg(not(feature = "test-send-sync"))]
unsafe impl Sync for VerificationCache {}
#[cfg(feature = "test-send-sync")]
#[test]
// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
fn test_send_sync_for_room() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<VerificationCache>();
}
#[derive(Debug, Default)]
struct VerificationCacheInner {
verification: StdRwLock<BTreeMap<OwnedUserId, BTreeMap<String, Verification>>>,
@@ -398,7 +398,7 @@ pub enum StartContent<'a> {
Room(&'a KeyVerificationStartEventContent),
}
impl<'a> StartContent<'a> {
impl StartContent<'_> {
#[allow(clippy::wrong_self_convention)]
pub fn from_device(&self) -> &DeviceId {
match self {
@@ -458,7 +458,7 @@ impl<'a> From<&'a ToDeviceKeyVerificationDoneEventContent> for DoneContent<'a> {
}
}
impl<'a> DoneContent<'a> {
impl DoneContent<'_> {
pub fn flow_id(&self) -> &str {
match self {
Self::ToDevice(c) => c.transaction_id.as_str(),
@@ -195,7 +195,7 @@ struct PendingStoreChanges<'a> {
operations: &'a mut Vec<PendingOperation>,
}
impl<'a> PendingStoreChanges<'a> {
impl PendingStoreChanges<'_> {
fn put(&mut self, key: JsValue, value: JsValue) {
self.operations.push(PendingOperation::Put { key, value });
}
@@ -1,3 +1,5 @@
//! Helpers for wasm32/browser environments
#![allow(dead_code)]
use base64::{
alphabet,
@@ -15,8 +17,6 @@ use ruma::{
use wasm_bindgen::JsValue;
use web_sys::IdbKeyRange;
/// Helpers for wasm32/browser environments
/// ASCII Group Separator, for elements in the keys
pub const KEY_SEPARATOR: &str = "\u{001D}";
/// ASCII Record Separator is sure smaller than the Key Separator but smaller
@@ -1595,6 +1595,48 @@ impl_state_store!({
}
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool> {
let encoded_key = self.encode_key(keys::DEPENDENT_SEND_QUEUE, room_id);
let tx = self.inner.transaction_on_one_with_mode(
keys::DEPENDENT_SEND_QUEUE,
IdbTransactionMode::Readwrite,
)?;
let obj = tx.object_store(keys::DEPENDENT_SEND_QUEUE)?;
// We store an encoded vector of the dependent requests.
// Reload the previous vector for this room, or create an empty one.
let prev = obj.get(&encoded_key)?.await?;
let mut prev = prev.map_or_else(
|| Ok(Vec::new()),
|val| self.deserialize_value::<Vec<DependentQueuedRequest>>(&val),
)?;
// Modify the dependent request, if found.
let mut found = false;
for entry in prev.iter_mut() {
if entry.own_transaction_id == *own_transaction_id {
found = true;
entry.kind = new_content;
break;
}
}
if found {
obj.put_key_val(&encoded_key, &self.serialize_value(&prev)?)?;
tx.await.into_result()?;
}
Ok(found)
}
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
+3
View File
@@ -101,6 +101,9 @@ pub enum Error {
#[error("Redaction failed: {0}")]
Redaction(#[source] ruma::canonical_json::RedactionError),
#[error("An update keyed by unique ID touched more than one entry")]
InconsistentUpdate,
}
macro_rules! impl_from {
@@ -3,11 +3,12 @@ use std::{borrow::Cow, fmt, path::Path, sync::Arc};
use async_trait::async_trait;
use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime};
use matrix_sdk_base::{
event_cache::store::EventCacheStore,
event_cache::{store::EventCacheStore, Event, Gap},
linked_chunk::Update,
media::{MediaRequestParameters, UniqueKey},
};
use matrix_sdk_store_encryption::StoreCipher;
use ruma::MilliSecondsSinceUnixEpoch;
use ruma::{MilliSecondsSinceUnixEpoch, RoomId};
use rusqlite::OptionalExtension;
use tokio::fs;
use tracing::debug;
@@ -182,6 +183,14 @@ impl EventCacheStore for SqliteEventCacheStore {
Ok(num_touched == 1)
}
async fn handle_linked_chunk_updates(
&self,
_room_id: &RoomId,
_updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
todo!()
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
+1
View File
@@ -15,6 +15,7 @@
not(any(feature = "state-store", feature = "crypto-store", feature = "event-cache")),
allow(dead_code, unused_imports)
)]
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
#[cfg(feature = "crypto-store")]
mod crypto_store;
@@ -509,6 +509,7 @@ trait SqliteConnectionStateStoreExt {
fn remove_display_name(&self, room_id: &[u8], name: &[u8]) -> rusqlite::Result<()>;
fn remove_room_display_names(&self, room_id: &[u8]) -> rusqlite::Result<()>;
fn remove_room_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>;
fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()>;
}
impl SqliteConnectionStateStoreExt for rusqlite::Connection {
@@ -720,6 +721,12 @@ impl SqliteConnectionStateStoreExt for rusqlite::Connection {
self.prepare("DELETE FROM send_queue_events WHERE room_id = ?")?.execute((room_id,))?;
Ok(())
}
fn remove_room_dependent_send_queue(&self, room_id: &[u8]) -> rusqlite::Result<()> {
self.prepare("DELETE FROM dependent_send_queue_events WHERE room_id = ?")?
.execute((room_id,))?;
Ok(())
}
}
#[async_trait]
@@ -1726,6 +1733,10 @@ impl StateStore for SqliteStateStore {
let send_queue_room_id = this.encode_key(keys::SEND_QUEUE, &room_id);
txn.remove_room_send_queue(&send_queue_room_id)?;
let dependent_send_queue_room_id =
this.encode_key(keys::DEPENDENTS_SEND_QUEUE, &room_id);
txn.remove_room_dependent_send_queue(&dependent_send_queue_room_id)?;
Ok(())
})
.await
@@ -1914,6 +1925,39 @@ impl StateStore for SqliteStateStore {
}
async fn update_dependent_queued_request(
&self,
room_id: &RoomId,
own_transaction_id: &ChildTransactionId,
new_content: DependentQueuedRequestKind,
) -> Result<bool> {
let room_id = self.encode_key(keys::DEPENDENTS_SEND_QUEUE, room_id);
let content = self.serialize_json(&new_content)?;
// See comment in `save_send_queue_event`.
let own_txn_id = own_transaction_id.to_string();
let num_updated = self
.acquire()
.await?
.with_transaction(move |txn| {
txn.prepare_cached(
r#"UPDATE dependent_send_queue_events
SET content = ?
WHERE own_transaction_id = ?
AND room_id = ?"#,
)?
.execute((content, own_txn_id, room_id))
})
.await?;
if num_updated > 1 {
return Err(Error::InconsistentUpdate);
}
Ok(num_updated == 1)
}
async fn mark_dependent_queued_requests_as_ready(
&self,
room_id: &RoomId,
parent_txn_id: &TransactionId,
+1 -1
View File
@@ -198,7 +198,7 @@ pub(crate) trait SqliteTransactionExt {
Query: Fn(&Transaction<'_>, Vec<Key>) -> Result<Vec<Res>> + Send + 'static;
}
impl<'a> SqliteTransactionExt for Transaction<'a> {
impl SqliteTransactionExt for Transaction<'_> {
fn chunk_large_query_over<Query, Res>(
&self,
mut keys_to_chunk: Vec<Key>,
+2
View File
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![cfg_attr(test, allow(unexpected_cfgs))] // Triggered by the init_tracing_for_tests!() invocation.
use ruma::html::HtmlSanitizerMode;
mod events;
@@ -89,12 +89,15 @@ const DEFAULT_REQUIRED_STATE: &[(StateEventType, &str)] = &[
(StateEventType::RoomPowerLevels, ""),
(StateEventType::CallMember, "*"),
(StateEventType::RoomJoinRules, ""),
// Those two events are required to properly compute room previews.
(StateEventType::RoomCreate, ""),
(StateEventType::RoomHistoryVisibility, ""),
];
/// The default `required_state` constant value for sliding sync room
/// subscriptions that must be added to `DEFAULT_REQUIRED_STATE`.
const DEFAULT_ROOM_SUBSCRIPTION_EXTRA_REQUIRED_STATE: &[(StateEventType, &str)] =
&[(StateEventType::RoomCreate, ""), (StateEventType::RoomPinnedEvents, "")];
&[(StateEventType::RoomPinnedEvents, "")];
/// The default `timeline_limit` value when used with room subscriptions.
const DEFAULT_ROOM_SUBSCRIPTION_TIMELINE_LIMIT: u32 = 20;
+4 -4
View File
@@ -31,7 +31,7 @@ use super::{
Error, Timeline, TimelineDropHandle, TimelineFocus,
};
use crate::{
timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin},
timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin},
unable_to_decrypt_hook::UtdHookManager,
};
@@ -273,9 +273,9 @@ impl TimelineBuilder {
inner.add_events_at(
events,
TimelineEnd::Back,
match origin {
EventsOrigin::Sync => RemoteEventOrigin::Sync,
TimelineNewItemPosition::End { origin: match origin {
EventsOrigin::Sync => RemoteEventOrigin::Sync,
}
}
).await;
}
@@ -22,7 +22,7 @@ use imbl::Vector;
#[cfg(test)]
use matrix_sdk::crypto::OlmMachine;
use matrix_sdk::{
deserialized_responses::SyncTimelineEvent,
deserialized_responses::{SyncTimelineEvent, TimelineEventKind as SdkTimelineEventKind},
event_cache::{paginator::Paginator, RoomEventCache},
send_queue::{
LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle,
@@ -52,8 +52,8 @@ use tracing::{
};
pub(super) use self::state::{
EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineEnd, TimelineMetadata,
TimelineState, TimelineStateTransaction,
EventMeta, FullEventMeta, PendingEdit, PendingEditKind, TimelineMetadata,
TimelineNewItemPosition, TimelineState, TimelineStateTransaction,
};
use super::{
event_handler::TimelineEventKind,
@@ -404,8 +404,11 @@ impl<P: RoomDataProvider> TimelineController<P> {
.map_err(PaginationError::Paginator)?,
};
self.add_events_at(pagination.events, TimelineEnd::Front, RemoteEventOrigin::Pagination)
.await;
self.add_events_at(
pagination.events,
TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination },
)
.await;
Ok(pagination.hit_end_of_timeline)
}
@@ -428,8 +431,11 @@ impl<P: RoomDataProvider> TimelineController<P> {
.map_err(PaginationError::Paginator)?,
};
self.add_events_at(pagination.events, TimelineEnd::Back, RemoteEventOrigin::Pagination)
.await;
self.add_events_at(
pagination.events,
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Pagination },
)
.await;
Ok(pagination.hit_end_of_timeline)
}
@@ -505,7 +511,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
let Some(prev_status) = prev_status else {
match &item.kind {
EventTimelineItemKind::Local(local) => {
if let Some(send_handle) = local.send_handle.clone() {
if let Some(send_handle) = &local.send_handle {
if send_handle
.react(key.to_owned())
.await
@@ -629,23 +635,14 @@ impl<P: RoomDataProvider> TimelineController<P> {
pub(super) async fn add_events_at(
&self,
events: Vec<impl Into<SyncTimelineEvent>>,
position: TimelineEnd,
origin: RemoteEventOrigin,
position: TimelineNewItemPosition,
) -> HandleManyEventsResult {
if events.is_empty() {
return Default::default();
}
let mut state = self.state.write().await;
state
.add_remote_events_at(
events,
position,
origin,
&self.room_data_provider,
&self.settings,
)
.await
state.add_remote_events_at(events, position, &self.room_data_provider, &self.settings).await
}
pub(super) async fn clear(&self) {
@@ -683,8 +680,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
state
.replace_with_remote_events(
events,
TimelineEnd::Back,
origin,
TimelineNewItemPosition::End { origin },
&self.room_data_provider,
&self.settings,
)
@@ -1064,16 +1060,22 @@ impl<P: RoomDataProvider> TimelineController<P> {
match decryptor.decrypt_event_impl(original_json).await {
Ok(event) => {
trace!(
"Successfully decrypted event that previously failed to decrypt"
);
if let SdkTimelineEventKind::UnableToDecrypt { utd_info, .. } =
event.kind
{
info!(
"Failed to decrypt event after receiving room key: {:?}",
utd_info.reason
);
None
} else {
// Notify observers that we managed to eventually decrypt an event.
if let Some(hook) = unable_to_decrypt_hook {
hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await;
}
// Notify observers that we managed to eventually decrypt an event.
if let Some(hook) = unable_to_decrypt_hook {
hook.on_late_decrypt(&remote_event.event_id, *utd_cause).await;
Some(event)
}
Some(event)
}
Err(e) => {
info!("Failed to decrypt event after receiving room key: {e}");
@@ -64,16 +64,27 @@ use crate::{
unable_to_decrypt_hook::UtdHookManager,
};
/// Which end of the timeline should an event be added to?
///
/// This is a simplification of `TimelineItemPosition` which doesn't contain the
/// `Update` variant, when adding a bunch of events at the same time.
/// This is a simplification of [`TimelineItemPosition`] which doesn't contain
/// the [`TimelineItemPosition::UpdateDecrypted`] variant, because it is used
/// only for **new** items.
#[derive(Debug)]
pub(crate) enum TimelineEnd {
/// Event should be prepended to the front of the timeline.
Front,
/// Event should be appended to the back of the timeline.
Back,
pub(crate) enum TimelineNewItemPosition {
/// One or more items are prepended to the timeline (i.e. they're the
/// oldest).
Start { origin: RemoteEventOrigin },
/// One or more items are appended to the timeline (i.e. they're the most
/// recent).
End { origin: RemoteEventOrigin },
}
impl From<TimelineNewItemPosition> for TimelineItemPosition {
fn from(value: TimelineNewItemPosition) -> Self {
match value {
TimelineNewItemPosition::Start { origin } => Self::Start { origin },
TimelineNewItemPosition::End { origin } => Self::End { origin },
}
}
}
#[derive(Debug)]
@@ -119,8 +130,7 @@ impl TimelineState {
pub(super) async fn add_remote_events_at<P: RoomDataProvider>(
&mut self,
events: Vec<impl Into<SyncTimelineEvent>>,
position: TimelineEnd,
origin: RemoteEventOrigin,
position: TimelineNewItemPosition,
room_data_provider: &P,
settings: &TimelineSettings,
) -> HandleManyEventsResult {
@@ -130,7 +140,7 @@ impl TimelineState {
let mut txn = self.transaction();
let handle_many_res =
txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await;
txn.add_remote_events_at(events, position, room_data_provider, settings).await;
txn.commit();
handle_many_res
@@ -241,7 +251,7 @@ impl TimelineState {
let handle_one_res = txn
.handle_remote_event(
event.into(),
TimelineItemPosition::UpdateDecrypted(idx),
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
room_data_provider,
settings,
&mut day_divider_adjuster,
@@ -285,15 +295,13 @@ impl TimelineState {
pub(super) async fn replace_with_remote_events<P: RoomDataProvider>(
&mut self,
events: Vec<SyncTimelineEvent>,
position: TimelineEnd,
origin: RemoteEventOrigin,
position: TimelineNewItemPosition,
room_data_provider: &P,
settings: &TimelineSettings,
) -> HandleManyEventsResult {
let mut txn = self.transaction();
txn.clear();
let result =
txn.add_remote_events_at(events, position, origin, room_data_provider, settings).await;
let result = txn.add_remote_events_at(events, position, room_data_provider, settings).await;
txn.commit();
result
}
@@ -347,17 +355,13 @@ impl TimelineStateTransaction<'_> {
pub(super) async fn add_remote_events_at<P: RoomDataProvider>(
&mut self,
events: Vec<impl Into<SyncTimelineEvent>>,
position: TimelineEnd,
origin: RemoteEventOrigin,
position: TimelineNewItemPosition,
room_data_provider: &P,
settings: &TimelineSettings,
) -> HandleManyEventsResult {
let mut total = HandleManyEventsResult::default();
let position = match position {
TimelineEnd::Front => TimelineItemPosition::Start { origin },
TimelineEnd::Back => TimelineItemPosition::End { origin },
};
let position = position.into();
let mut day_divider_adjuster = DayDividerAdjuster::default();
@@ -447,7 +451,7 @@ impl TimelineStateTransaction<'_> {
TimelineItemPosition::End { origin }
| TimelineItemPosition::Start { origin } => origin,
TimelineItemPosition::UpdateDecrypted(idx) => self
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self
.items
.get(idx)
.and_then(|item| item.as_event())
@@ -703,7 +707,7 @@ impl TimelineStateTransaction<'_> {
self.meta.all_events.push_back(event_meta.base_meta());
}
TimelineItemPosition::UpdateDecrypted(_) => {
TimelineItemPosition::UpdateDecrypted { .. } => {
if let Some(event) =
self.meta.all_events.iter_mut().find(|e| e.event_id == event_meta.event_id)
{
@@ -1027,7 +1031,7 @@ impl TimelineMetadata {
.skip(*i + 1)
// …that's not virtual and not sent by us…
.find(|(_, item)| {
item.as_event().map_or(false, |event| event.sender() != self.own_user_id)
item.as_event().is_some_and(|event| event.sender() != self.own_user_id)
})
.map(|(i, _)| i);
@@ -1113,7 +1117,7 @@ pub(crate) struct FullEventMeta<'a> {
pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
}
impl<'a> FullEventMeta<'a> {
impl FullEventMeta<'_> {
fn base_meta(&self) -> EventMeta {
EventMeta { event_id: self.event_id.to_owned(), visible: self.visible }
}
@@ -517,7 +517,7 @@ struct DayDividerInvariantsReport<'a, 'o> {
errors: Vec<DayDividerInsertError>,
}
impl<'a, 'o> Display for DayDividerInvariantsReport<'a, 'o> {
impl Display for DayDividerInvariantsReport<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Write all the items of a slice of timeline items.
fn write_items(
+3 -3
View File
@@ -54,15 +54,15 @@ pub enum Error {
UnknownEncryptionState,
/// Something went wrong with the room event cache.
#[error("Something went wrong with the room event cache.")]
#[error(transparent)]
EventCacheError(#[from] EventCacheError),
/// An error happened during pagination.
#[error("An error happened during pagination.")]
#[error(transparent)]
PaginationError(#[from] PaginationError),
/// An error happened during pagination.
#[error("An error happened when loading pinned events.")]
#[error(transparent)]
PinnedEventsError(#[from] PinnedEventsLoaderError),
/// An error happened while operating the room's send queue.
@@ -269,17 +269,26 @@ impl TimelineEventKind {
pub(super) enum TimelineItemPosition {
/// One or more items are prepended to the timeline (i.e. they're the
/// oldest).
Start { origin: RemoteEventOrigin },
Start {
/// The origin of the new item(s).
origin: RemoteEventOrigin,
},
/// One or more items are appended to the timeline (i.e. they're the most
/// recent).
End { origin: RemoteEventOrigin },
End {
/// The origin of the new item(s).
origin: RemoteEventOrigin,
},
/// A single item is updated, after it's been successfully decrypted.
///
/// This happens when an item that was a UTD must be replaced with the
/// decrypted event.
UpdateDecrypted(usize),
UpdateDecrypted {
/// The index of the **timeline item**.
timeline_item_index: usize,
},
}
/// The outcome of handling a single event with
@@ -481,8 +490,10 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
if !self.result.item_added {
trace!("No new item added");
if let Flow::Remote { position: TimelineItemPosition::UpdateDecrypted(idx), .. } =
self.ctx.flow
if let Flow::Remote {
position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
..
} = self.ctx.flow
{
// If add was not called, that means the UTD event is one that
// wouldn't normally be visible. Remove it.
@@ -576,7 +587,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
replacement: PendingEdit,
) {
match position {
TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted(_) => {
TimelineItemPosition::Start { .. } | TimelineItemPosition::UpdateDecrypted { .. } => {
// Only insert the edit if there wasn't any other edit
// before.
//
@@ -1012,7 +1023,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
| TimelineItemPosition::End { origin } => origin,
// For updates, reuse the origin of the encrypted event.
TimelineItemPosition::UpdateDecrypted(idx) => self.items[idx]
TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx } => self
.items[idx]
.as_event()
.and_then(|ev| Some(ev.as_remote()?.origin))
.unwrap_or_else(|| {
@@ -1162,7 +1174,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
Flow::Remote {
event_id: decrypted_event_id,
position: TimelineItemPosition::UpdateDecrypted(idx),
position: TimelineItemPosition::UpdateDecrypted { timeline_item_index: idx },
..
} => {
trace!("Updating timeline item at position {idx}");
@@ -368,7 +368,15 @@ impl EventTimelineItem {
match self.content() {
TimelineItemContent::Message(message) => {
matches!(message.msgtype(), MessageType::Text(_) | MessageType::Emote(_))
matches!(
message.msgtype(),
MessageType::Text(_)
| MessageType::Emote(_)
| MessageType::Audio(_)
| MessageType::File(_)
| MessageType::Image(_)
| MessageType::Video(_)
)
}
TimelineItemContent::Poll(poll) => {
poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
@@ -726,12 +734,14 @@ impl ReactionsByKeyBySender {
mod tests {
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use matrix_sdk::test_utils::{events::EventFactory, logged_in_client};
use matrix_sdk::test_utils::logged_in_client;
use matrix_sdk_base::{
deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http,
MinimalStateEvent, OriginalMinimalStateEvent,
};
use matrix_sdk_test::{async_test, sync_state_event, sync_timeline_event};
use matrix_sdk_test::{
async_test, event_factory::EventFactory, sync_state_event, sync_timeline_event,
};
use ruma::{
event_id,
events::{
+12
View File
@@ -461,6 +461,7 @@ impl Timeline {
.into());
}
}
EditedContent::PollStart { new_content, .. } => {
if matches!(item.content, TimelineItemContent::Poll(_)) {
AnyMessageLikeEventContent::UnstablePollStart(
@@ -476,6 +477,17 @@ impl Timeline {
.into());
}
}
EditedContent::MediaCaption { caption, formatted_caption } => {
if handle
.edit_media_caption(caption, formatted_caption)
.await
.map_err(RoomSendQueueError::StorageError)?
{
return Ok(());
}
return Err(EditError::InvalidLocalEchoState.into());
}
};
if !handle.edit(new_content).await.map_err(RoomSendQueueError::StorageError)? {
@@ -26,7 +26,7 @@ use matrix_sdk::event_cache::{
use tracing::{instrument, trace, warn};
use super::Error;
use crate::timeline::{controller::TimelineEnd, event_item::RemoteEventOrigin};
use crate::timeline::{controller::TimelineNewItemPosition, event_item::RemoteEventOrigin};
impl super::Timeline {
/// Add more events to the start of the timeline.
@@ -81,7 +81,7 @@ impl super::Timeline {
// `matrix_sdk::event_cache::RoomEventCacheUpdate` from
// `matrix_sdk::event_cache::RoomPagination::run_backwards`.
self.controller
.add_events_at(events, TimelineEnd::Front, RemoteEventOrigin::Pagination)
.add_events_at(events, TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination })
.await;
if num_events == 0 && !reached_start {
@@ -35,7 +35,7 @@ use stream_assert::assert_next_matches;
use super::TestTimeline;
use crate::timeline::{
controller::{TimelineEnd, TimelineSettings},
controller::{TimelineNewItemPosition, TimelineSettings},
event_item::{AnyOtherFullStateEventContent, RemoteEventOrigin},
tests::{ReadReceiptMap, TestRoomDataProvider},
MembershipChange, TimelineDetails, TimelineItemContent, TimelineItemKind, VirtualTimelineItem,
@@ -51,8 +51,7 @@ async fn test_initial_events() {
.controller
.add_events_at(
vec![f.text_msg("A").sender(*ALICE), f.text_msg("B").sender(*BOB)],
TimelineEnd::Back,
RemoteEventOrigin::Sync,
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
@@ -91,7 +90,10 @@ async fn test_replace_with_initial_events_and_read_marker() {
let f = &timeline.factory;
let ev = f.text_msg("hey").sender(*ALICE).into_sync();
timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await;
timeline
.controller
.add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync })
.await;
let items = timeline.controller.items().await;
assert_eq!(items.len(), 2);
@@ -317,8 +319,7 @@ async fn test_dedup_initial() {
// … and a new event also came in
event_c,
],
TimelineEnd::Back,
RemoteEventOrigin::Sync,
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
@@ -354,7 +355,10 @@ async fn test_internal_id_prefix() {
timeline
.controller
.add_events_at(vec![ev_a, ev_b, ev_c], TimelineEnd::Back, RemoteEventOrigin::Sync)
.add_events_at(
vec![ev_a, ev_b, ev_c],
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
let timeline_items = timeline.controller.items().await;
@@ -516,7 +520,10 @@ async fn test_replace_with_initial_events_when_batched() {
let f = &timeline.factory;
let ev = f.text_msg("hey").sender(*ALICE).into_sync();
timeline.controller.add_events_at(vec![ev], TimelineEnd::Back, RemoteEventOrigin::Sync).await;
timeline
.controller
.add_events_at(vec![ev], TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync })
.await;
let (items, mut stream) = timeline.controller.subscribe_batched().await;
assert_eq!(items.len(), 2);
@@ -16,12 +16,9 @@ use std::sync::Arc;
use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use matrix_sdk::{
assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate,
test_utils::events::EventFactory,
};
use matrix_sdk::{assert_next_matches_with_timeout, send_queue::RoomSendQueueUpdate};
use matrix_sdk_base::store::QueueWedgeError;
use matrix_sdk_test::{async_test, ALICE, BOB};
use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB};
use ruma::{
event_id,
events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent},
@@ -18,6 +18,7 @@ use std::{
io::Cursor,
iter,
sync::{Arc, Mutex},
time::Duration,
};
use as_variant::as_variant;
@@ -43,6 +44,7 @@ use ruma::{
};
use serde_json::{json, value::to_raw_value};
use stream_assert::assert_next_matches;
use tokio::time::sleep;
use super::TestTimeline;
use crate::{
@@ -50,6 +52,17 @@ use crate::{
unable_to_decrypt_hook::{UnableToDecryptHook, UnableToDecryptInfo, UtdHookManager},
};
#[derive(Debug, Default)]
struct DummyUtdHook {
utds: Mutex<Vec<UnableToDecryptInfo>>,
}
impl UnableToDecryptHook for DummyUtdHook {
fn on_utd(&self, info: UnableToDecryptInfo) {
self.utds.lock().unwrap().push(info);
}
}
#[async_test]
async fn test_retry_message_decryption() {
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
@@ -67,17 +80,6 @@ async fn test_retry_message_decryption() {
HztoSJUr/2Y\n\
-----END MEGOLM SESSION DATA-----";
#[derive(Debug, Default)]
struct DummyUtdHook {
utds: Mutex<Vec<UnableToDecryptInfo>>,
}
impl UnableToDecryptHook for DummyUtdHook {
fn on_utd(&self, info: UnableToDecryptInfo) {
self.utds.lock().unwrap().push(info);
}
}
let hook = Arc::new(DummyUtdHook::default());
let client = test_client_builder(None).build().await.unwrap();
let utd_hook = Arc::new(UtdHookManager::new(hook.clone(), client));
@@ -170,6 +172,73 @@ async fn test_retry_message_decryption() {
}
}
// There has been a regression when the `retry_event_decryption` function
// changed from failing with an Error to instead return a new type of timeline
// event in UTD. The regression caused the timeline to consider any
// re-decryption attempt as successful.
#[async_test]
async fn test_false_positive_late_decryption_regression() {
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
let hook = Arc::new(DummyUtdHook::default());
let client = test_client_builder(None).build().await.unwrap();
let utd_hook =
Arc::new(UtdHookManager::new(hook.clone(), client).with_max_delay(Duration::from_secs(1)));
let timeline = TestTimeline::with_unable_to_decrypt_hook(utd_hook.clone());
let f = &timeline.factory;
timeline
.handle_live_event(
f.event(RoomEncryptedEventContent::new(
EncryptedEventScheme::MegolmV1AesSha2(
MegolmV1AesSha2ContentInit {
ciphertext: "\
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
.to_owned(),
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA".to_owned(),
device_id: "NLAZCWIOCO".into(),
session_id: SESSION_ID.into(),
}
.into(),
),
None,
))
.sender(&BOB)
.into_utd_sync_timeline_event(),
)
.await;
let own_user_id = user_id!("@example:morheus.localhost");
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
timeline
.controller
.retry_event_decryption_test(
room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"),
olm_machine,
Some(iter::once(SESSION_ID.to_owned()).collect()),
)
.await;
assert_eq!(timeline.controller.items().await.len(), 2);
// Wait past the max delay for utd late decryption detection
sleep(Duration::from_secs(2)).await;
{
let utds = hook.utds.lock().unwrap();
assert_eq!(utds.len(), 1);
// This is the main thing we're testing: if this wasn't identified as a definite
// UTD, this would be `Some(..)`.
assert!(utds[0].time_to_decrypt.is_none());
}
}
#[async_test]
async fn test_retry_edit_decryption() {
const SESSION1_KEY: &[u8] = b"\
+12 -5
View File
@@ -31,11 +31,12 @@ use matrix_sdk::{
event_cache::paginator::{PaginableRoom, PaginatorError},
room::{EventWithContextResponse, Messages, MessagesOptions},
send_queue::RoomSendQueueUpdate,
test_utils::events::EventFactory,
BoxFuture,
};
use matrix_sdk_base::{latest_event::LatestEvent, RoomInfo, RoomState};
use matrix_sdk_test::{EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID};
use matrix_sdk_test::{
event_factory::EventFactory, EventBuilder, ALICE, BOB, DEFAULT_TEST_ROOM_ID,
};
use ruma::{
event_id,
events::{
@@ -56,7 +57,7 @@ use ruma::{
use tokio::sync::RwLock;
use super::{
controller::{TimelineEnd, TimelineSettings},
controller::{TimelineNewItemPosition, TimelineSettings},
event_handler::TimelineEventKind,
event_item::RemoteEventOrigin,
traits::RoomDataProvider,
@@ -236,7 +237,10 @@ impl TestTimeline {
async fn handle_live_event(&self, event: impl Into<SyncTimelineEvent>) {
let event = event.into();
self.controller
.add_events_at(vec![event], TimelineEnd::Back, RemoteEventOrigin::Sync)
.add_events_at(
vec![event],
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
}
@@ -255,7 +259,10 @@ impl TestTimeline {
async fn handle_back_paginated_event(&self, event: Raw<AnyTimelineEvent>) {
let timeline_event = TimelineEvent::new(event.cast());
self.controller
.add_events_at(vec![timeline_event], TimelineEnd::Front, RemoteEventOrigin::Pagination)
.add_events_at(
vec![timeline_event],
TimelineNewItemPosition::Start { origin: RemoteEventOrigin::Pagination },
)
.await;
}
@@ -18,8 +18,8 @@ use assert_matches2::{assert_let, assert_matches};
use eyeball_im::VectorDiff;
use futures_core::Stream;
use futures_util::{FutureExt as _, StreamExt as _};
use matrix_sdk::{deserialized_responses::SyncTimelineEvent, test_utils::events::EventFactory};
use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB};
use matrix_sdk::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_timeline_event, ALICE, BOB};
use ruma::{
event_id, events::AnyMessageLikeEventContent, server_name, uint, EventId,
MilliSecondsSinceUnixEpoch, OwnedEventId,
@@ -28,8 +28,8 @@ use stream_assert::assert_next_matches;
use tokio::time::timeout;
use crate::timeline::{
controller::TimelineEnd, event_item::RemoteEventOrigin, tests::TestTimeline, ReactionStatus,
TimelineEventItemId, TimelineItem,
controller::TimelineNewItemPosition, event_item::RemoteEventOrigin, tests::TestTimeline,
ReactionStatus, TimelineEventItemId, TimelineItem,
};
const REACTION_KEY: &str = "👍";
@@ -204,8 +204,7 @@ async fn test_initial_reaction_timestamp_is_stored() {
// Event comes next.
f.text_msg("A").event_id(&message_event_id).into_sync(),
],
TimelineEnd::Back,
RemoteEventOrigin::Sync,
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
@@ -15,8 +15,7 @@
use std::sync::Arc;
use eyeball_im::VectorDiff;
use matrix_sdk::test_utils::events::EventFactory;
use matrix_sdk_test::{async_test, ALICE, BOB, CAROL};
use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB, CAROL};
use ruma::{
event_id,
events::{
@@ -29,8 +29,8 @@ use stream_assert::assert_next_matches;
use super::TestTimeline;
use crate::timeline::{
controller::TimelineEnd, event_item::RemoteEventOrigin, AnyOtherFullStateEventContent,
TimelineDetails, TimelineItemContent,
controller::TimelineNewItemPosition, event_item::RemoteEventOrigin,
AnyOtherFullStateEventContent, TimelineDetails, TimelineItemContent,
};
#[async_test]
@@ -146,8 +146,7 @@ async fn test_reaction_redaction_timeline_filter() {
.event_builder
.make_sync_redacted_message_event(*ALICE, RedactedReactionEventContent::new()),
)],
TimelineEnd::Back,
RemoteEventOrigin::Sync,
TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync },
)
.await;
// Timeline items are actually empty.
+10 -4
View File
@@ -18,7 +18,7 @@ use eyeball::Subscriber;
use futures_util::FutureExt as _;
use indexmap::IndexMap;
#[cfg(test)]
use matrix_sdk::crypto::{DecryptionSettings, TrustRequirement};
use matrix_sdk::crypto::{DecryptionSettings, RoomEventDecryptionResult, TrustRequirement};
use matrix_sdk::{
deserialized_responses::TimelineEvent, event_cache::paginator::PaginableRoom, BoxFuture,
Result, Room,
@@ -302,8 +302,14 @@ impl Decryptor for (matrix_sdk_base::crypto::OlmMachine, ruma::OwnedRoomId) {
let (olm_machine, room_id) = self;
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
let event =
olm_machine.decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings).await?;
Ok(event.into())
match olm_machine
.try_decrypt_room_event(raw.cast_ref(), room_id, &decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => Ok(decrypted.into()),
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
Ok(TimelineEvent::new_utd_event(raw.clone(), utd_info))
}
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ pub(super) struct EventTimelineItemWithId<'a> {
pub internal_id: &'a TimelineUniqueId,
}
impl<'a> EventTimelineItemWithId<'a> {
impl EventTimelineItemWithId<'_> {
/// Create a clone of the underlying [`TimelineItem`] with the given kind.
pub fn with_inner_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Arc<TimelineItem> {
TimelineItem::new(self.inner.with_kind(kind), self.internal_id.clone())
@@ -290,11 +290,11 @@ impl UtdHookManager {
///
/// Must be called with the lock held on [`UtdHookManager::reported_utds`],
/// and takes a `MutexGuard` to enforce that.
async fn report_utd<'a>(
async fn report_utd(
info: UnableToDecryptInfo,
parent_hook: &Arc<dyn UnableToDecryptHook>,
client: &Client,
reported_utds_lock: &mut MutexGuard<'a, GrowableBloom>,
reported_utds_lock: &mut MutexGuard<'_, GrowableBloom>,
) {
let event_id = info.event_id.clone();
parent_hook.on_utd(info);
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(unexpected_cfgs)] // Triggered by the init_tracing_for_tests!() invocation.
use itertools::Itertools as _;
use matrix_sdk::deserialized_responses::TimelineEvent;
use ruma::{events::AnyStateEvent, serde::Raw, EventId, RoomId};
@@ -358,6 +358,8 @@ async fn test_sync_all_states() -> Result<(), Error> {
["m.room.power_levels", ""],
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
["m.room.create", ""],
["m.room.history_visibility", ""],
],
"include_heroes": true,
"filters": {
@@ -2224,6 +2226,7 @@ async fn test_room_subscription() -> Result<(), Error> {
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
["m.room.create", ""],
["m.room.history_visibility", ""],
["m.room.pinned_events", ""],
],
"timeline_limit": 20,
@@ -2263,6 +2266,7 @@ async fn test_room_subscription() -> Result<(), Error> {
["org.matrix.msc3401.call.member", "*"],
["m.room.join_rules", ""],
["m.room.create", ""],
["m.room.history_visibility", ""],
["m.room.pinned_events", ""],
],
"timeline_limit": 20,
@@ -19,14 +19,12 @@ use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::{
assert_next_matches_with_timeout,
config::SyncSettings,
executor::spawn,
ruma::MilliSecondsSinceUnixEpoch,
test_utils::{events::EventFactory, logged_in_client_with_server},
assert_next_matches_with_timeout, config::SyncSettings, executor::spawn,
ruma::MilliSecondsSinceUnixEpoch, test_utils::logged_in_client_with_server,
};
use matrix_sdk_test::{
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder,
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
SyncResponseBuilder,
};
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent};
use ruma::{
@@ -20,13 +20,12 @@ use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::{FutureExt, StreamExt};
use matrix_sdk::{
config::SyncSettings,
room::edit::EditedContent,
test_utils::{events::EventFactory, logged_in_client_with_server},
config::SyncSettings, room::edit::EditedContent, test_utils::logged_in_client_with_server,
Client,
};
use matrix_sdk_test::{
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
SyncResponseBuilder, ALICE, BOB,
};
use matrix_sdk_ui::{
timeline::{
@@ -20,12 +20,12 @@ use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::{
assert_next_matches_with_timeout,
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
assert_next_matches_with_timeout, config::SyncSettings,
test_utils::logged_in_client_with_server,
};
use matrix_sdk_test::{
async_test, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder,
SyncResponseBuilder, ALICE, BOB,
};
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id};
@@ -0,0 +1,203 @@
// 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::{fs::File, io::Write as _, path::PathBuf, time::Duration};
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::{FutureExt, StreamExt};
use matrix_sdk::{
assert_let_timeout, attachment::AttachmentConfig, test_utils::mocks::MatrixMockServer,
};
use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE};
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent};
use ruma::{
event_id,
events::room::{message::MessageType, MediaSource},
room_id,
};
use serde_json::json;
use tempfile::TempDir;
use tokio::time::sleep;
use wiremock::ResponseTemplate;
fn create_temporary_file(filename: &str) -> (TempDir, PathBuf) {
let tmp_dir = TempDir::new().unwrap();
let file_path = tmp_dir.path().join(filename);
let mut file = File::create(&file_path).unwrap();
file.write_all(b"hello world").unwrap();
(tmp_dir, file_path)
}
fn get_filename_and_caption(msg: &MessageType) -> (&str, Option<&str>) {
match msg {
MessageType::File(event) => (event.filename(), event.caption()),
MessageType::Image(event) => (event.filename(), event.caption()),
MessageType::Video(event) => (event.filename(), event.caption()),
MessageType::Audio(event) => (event.filename(), event.caption()),
_ => panic!("unexpected message type"),
}
}
#[async_test]
async fn test_send_attachment() {
let mock = MatrixMockServer::new().await;
let client = mock.client_builder().build().await;
mock.mock_room_state_encryption().plain().mount().await;
let room_id = room_id!("!a98sd12bjh:example.org");
let room = mock.sync_joined_room(&client, room_id).await;
let timeline = room.timeline().await.unwrap();
let (items, mut timeline_stream) =
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
assert!(items.is_empty());
let f = EventFactory::new();
mock.sync_room(
&client,
JoinedRoomBuilder::new(room_id).add_timeline_event(f.text_msg("hello").sender(&ALICE)),
)
.await;
// Sanity check.
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(msg.body(), "hello");
// No other updates.
assert!(timeline_stream.next().now_or_never().is_none());
// Store a file in a temporary directory.
let (_tmp_dir, file_path) = create_temporary_file("test.bin");
// Set up mocks for the file upload.
mock.mock_upload()
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2)).set_body_json(
json!({
"content_uri": "mxc://sdk.rs/media"
}),
))
.mock_once()
.mount()
.await;
mock.mock_room_send().ok(event_id!("$media")).mock_once().mount().await;
// Queue sending of an attachment.
let config = AttachmentConfig::new().caption(Some("caption".to_owned()));
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();
{
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
assert_let!(TimelineItemContent::Message(msg) = item.content());
// Body is the caption, because there's both a caption and filename.
assert_eq!(msg.body(), "caption");
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption")));
// The URI refers to the local cache.
assert_let!(MessageType::File(file) = msg.msgtype());
assert_let!(MediaSource::Plain(uri) = &file.source);
assert!(uri.to_string().contains("localhost"));
}
// Eventually, the media is updated with the final MXC IDs…
sleep(Duration::from_secs(2)).await;
{
assert_let_timeout!(
Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next()
);
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_matches!(item.send_state(), Some(EventSendState::NotSentYet));
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", Some("caption")));
// The URI now refers to the final MXC URI.
assert_let!(MessageType::File(file) = msg.msgtype());
assert_let!(MediaSource::Plain(uri) = &file.source);
assert_eq!(uri.to_string(), "mxc://sdk.rs/media");
}
// And eventually the event itself is sent.
{
assert_let_timeout!(
Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next()
);
assert_matches!(item.send_state(), Some(EventSendState::Sent{ event_id }) => {
assert_eq!(event_id, event_id!("$media"));
});
}
// That's all, folks!
assert!(timeline_stream.next().now_or_never().is_none());
}
#[async_test]
async fn test_react_to_local_media() {
let mock = MatrixMockServer::new().await;
let client = mock.client_builder().build().await;
// Disable the sending queue, to simulate offline mode.
client.send_queue().set_enabled(false).await;
mock.mock_room_state_encryption().plain().mount().await;
let room_id = room_id!("!a98sd12bjh:example.org");
let room = mock.sync_joined_room(&client, room_id).await;
let timeline = room.timeline().await.unwrap();
let (items, mut timeline_stream) =
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
assert!(items.is_empty());
assert!(timeline_stream.next().now_or_never().is_none());
// Store a file in a temporary directory.
let (_tmp_dir, file_path) = create_temporary_file("test.bin");
// Queue sending of an attachment (no captions).
let config = AttachmentConfig::new();
timeline.send_attachment(&file_path, mime::TEXT_PLAIN, config).use_send_queue().await.unwrap();
let item_id = {
assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));
// The item starts with no reactions.
assert!(item.reactions().is_empty());
item.identifier()
};
// Add a reaction to the file media event.
timeline.toggle_reaction(&item_id, "🤪").await.unwrap();
assert_let_timeout!(Some(VectorDiff::Set { index: 0, value: item }) = timeline_stream.next());
assert_let!(TimelineItemContent::Message(msg) = item.content());
assert_eq!(get_filename_and_caption(msg.msgtype()), ("test.bin", None));
// There's a reaction for the current user for the given emoji.
let reactions = item.reactions();
let own_user_id = client.user_id().unwrap();
reactions.get("🤪").unwrap().get(own_user_id).unwrap();
// That's all, folks!
assert!(timeline_stream.next().now_or_never().is_none());
}
@@ -19,12 +19,11 @@ use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::{
assert_let_timeout,
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
assert_let_timeout, config::SyncSettings, test_utils::logged_in_client_with_server,
};
use matrix_sdk_test::{
async_test,
event_factory::EventFactory,
mocks::{mock_encryption_state, mock_redaction},
sync_timeline_event, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent,
SyncResponseBuilder, BOB,
@@ -53,6 +52,7 @@ use crate::mock_sync;
mod echo;
mod edit;
mod focus_event;
mod media;
mod pagination;
mod pinned_event;
mod profiles;
@@ -3,14 +3,14 @@ use std::time::Duration;
use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use matrix_sdk::{
assert_next_matches_with_timeout,
config::SyncSettings,
sync::SyncResponse,
test_utils::{events::EventFactory, logged_in_client_with_server},
Client,
assert_next_matches_with_timeout, config::SyncSettings, sync::SyncResponse,
test_utils::logged_in_client_with_server, Client,
};
use matrix_sdk_base::deserialized_responses::TimelineEvent;
use matrix_sdk_test::{async_test, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, BOB};
use matrix_sdk_test::{
async_test, event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent,
SyncResponseBuilder, BOB,
};
use matrix_sdk_ui::{
timeline::{RoomExt, TimelineFocus, TimelineItemContent},
Timeline,
@@ -17,12 +17,10 @@ use std::{sync::Mutex, time::Duration};
use assert_matches2::{assert_let, assert_matches};
use eyeball_im::VectorDiff;
use futures_util::{FutureExt as _, StreamExt as _};
use matrix_sdk::{
assert_next_matches_with_timeout,
test_utils::{events::EventFactory, logged_in_client_with_server},
};
use matrix_sdk::{assert_next_matches_with_timeout, test_utils::logged_in_client_with_server};
use matrix_sdk_test::{
async_test,
event_factory::EventFactory,
mocks::{mock_encryption_state, mock_redaction},
JoinedRoomBuilder, SyncResponseBuilder, ALICE,
};
@@ -4,14 +4,11 @@ use assert_matches::assert_matches;
use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::{
config::SyncSettings,
test_utils::{events::EventFactory, logged_in_client_with_server},
};
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
use matrix_sdk_base::timeout::timeout;
use matrix_sdk_test::{
async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder,
ALICE, BOB, CAROL,
async_test, event_factory::EventFactory, mocks::mock_encryption_state, EventBuilder,
JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, CAROL,
};
use matrix_sdk_ui::timeline::{
Error as TimelineError, EventSendState, RoomExt, TimelineDetails, TimelineItemContent,

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