Compare commits

...

34 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The problem is the following: all items are traversed.

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

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

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

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

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

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

This patch updates OpenSSL to 0.10.70.
2025-02-03 09:42:58 +01:00
Damir Jelić 57919f5480 chore: Bump most of our deps 2025-01-31 17:14:37 +01:00
Damir Jelić b8949cfe26 chore: Bump vodozemac 2025-01-31 17:14:37 +01:00
Damir Jelić 8d27b0c811 test: Simplify some tests using the assert_next_with_timeout macro 2025-01-31 14:15:18 +01:00
Damir Jelić 3707d2fb81 test: Make the timeout parameter in the assert_next_with_timeout macro optional 2025-01-31 14:15:18 +01:00
Damir Jelić eaaa5e17a0 chore: Fix a doc example in the MatrixMockServer 2025-01-31 14:15:18 +01:00
Ivan Enderlin e3958b754c chore(crypto-ffi): Done is a unit type, no need for { .. }. 2025-01-31 14:07:43 +01:00
Ivan Enderlin 78d9e1292f chore(sdk): Do not iterate over the entire iterator when we can reach back.
This patch uses `next_back()` instead of `last()`, which is equivalent
but `last()` requires to iterate over the entire iterator, while
`next_back()` is a single operation.
2025-01-31 14:07:43 +01:00
Ivan Enderlin d594b4dad7 chore(sdk): Remove a useless type conversion.
This patch removes a useless type conversion. The `Room::event()` method
returns a `TimelineEvent`, so calling `Into::into` is useless: we map
`TimelineEvent` to `TimelineEvent`.
2025-01-31 14:07:43 +01:00
Ivan Enderlin 3f40ad83a5 chore(sdk): Remove a useless type conversion.
This patch removes a useless type conversion. The iterator produces
`TimelineEvent`, so mapping to `TimelineEvent::from` is useless: we map
`TimelineEvent` to `TimelineEvent`.
2025-01-31 14:07:43 +01:00
Ivan Enderlin 5049d1a3b6 chore(sqlite): Use repeat_n(…, n) instead of repeat(…).take(n).
Thanks Clippy!
2025-01-31 14:07:43 +01:00
59 changed files with 1772 additions and 441 deletions
+2 -1
View File
@@ -61,6 +61,7 @@ allow-git = [
"https://github.com/jplatte/const_panic",
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
"https://github.com/jplatte/async-compat",
# A newer version of vodozemac to support new dehydrated devices format.
# We can release vodozemac whenever we need but let's not block development
# on releases.
"https://github.com/matrix-org/vodozemac",
]
+1 -1
View File
@@ -295,7 +295,7 @@ jobs:
uses: actions/checkout@v4
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.29.4
uses: crate-ci/typos@v1.29.5
lint:
name: Lint
Generated
+341 -175
View File
File diff suppressed because it is too large Load Diff
+35 -34
View File
@@ -18,48 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
[workspace.package]
rust-version = "1.82"
rust-version = "1.83"
[workspace.dependencies]
anyhow = "1.0.93"
anyhow = "1.0.95"
aquamarine = "0.6.0"
assert-json-diff = "2.0.2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.2"
async-rx = "0.1.3"
async-stream = "0.3.5"
async-trait = "0.1.83"
async-trait = "0.1.85"
as_variant = "1.2.0"
base64 = "0.22.1"
byteorder = "1.5.0"
chrono = "0.4.38"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.6.0", features = ["tracing"] }
eyeball-im-util = "0.8.0"
futures-core = "0.3.31"
futures-executor = "0.3.21"
futures-executor = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2.15", default-features = false }
gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.1.0"
http = "1.2.0"
imbl = "4.0.1"
indexmap = "2.6.0"
insta = { version = "1.41.1", features = ["json"] }
itertools = "0.13.0"
indexmap = "2.7.1"
insta = { version = "1.42.1", features = ["json"] }
itertools = "0.14.0"
js-sys = "0.3.69"
mime = "0.3.17"
once_cell = "1.20.2"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.15"
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
rand = "0.8.5"
reqwest = { version = "0.12.4", default-features = false }
reqwest = { version = "0.12.12", default-features = false }
rmp-serde = "1.3.0"
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
# branch until a proper release with breaking changes happens.
ruma = { git = "https://github.com/ruma/ruma", rev = "b868438f5d91918e97d2c3f64d7c82a0d86d29d4", features = [
ruma = { version = "0.12.1", features = [
"client-api-c",
"compat-upload-signatures",
"compat-user-id",
@@ -74,17 +75,17 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "b868438f5d91918e97d2c3f64d
"unstable-msc4140",
"unstable-msc4171",
] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "b868438f5d91918e97d2c3f64d7c82a0d86d29d4" }
serde = "1.0.151"
serde_html_form = "0.2.0"
serde_json = "1.0.91"
ruma-common = { version = "0.15.1" }
serde = "1.0.217"
serde_html_form = "0.2.7"
serde_json = "1.0.138"
sha2 = "0.10.8"
similar-asserts = "1.6.0"
similar-asserts = "1.6.1"
stream_assert = "0.1.1"
tempfile = "3.9.0"
thiserror = "2.0.3"
tokio = { version = "1.41.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.14"
tempfile = "3.16.0"
thiserror = "2.0.11"
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
tracing-core = "0.1.32"
tracing-subscriber = "0.3.18"
@@ -92,25 +93,25 @@ unicode-normalization = "0.1.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.4"
uuid = "1.11.0"
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "3d655add09992d17fdb2b55c60f532658090487d", features = ["insecure-pk-encryption"] }
uuid = "1.12.1"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.33"
web-sys = "0.3.69"
wiremock = "0.6.2"
zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.0" }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.9.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.0", default-features = false }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.10.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
# Default release profile, select with `--release`
[profile.release]
@@ -791,8 +791,7 @@ impl VerificationRequest {
// task.
let should_break = matches!(
state,
RustVerificationRequestState::Done { .. }
| RustVerificationRequestState::Cancelled { .. }
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
);
let state = Self::convert_verification_request(&request, state);
@@ -9,6 +9,7 @@ readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
publish = false
[lib]
proc-macro = true
@@ -22,3 +23,6 @@ syn = { version = "2.0.43", features = ["full", "extra-traits"] }
[lints]
workspace = true
[package.metadata.release]
release = false
+3
View File
@@ -2,6 +2,9 @@
Breaking changes:
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
original message and the associated error code and kind.
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
`SendingFromUnverifiedDevice`. These indicate that your own device is not
properly cross-signed, which is a requirement when using the identity-based
-5
View File
@@ -8,8 +8,3 @@ dictionary Mentions {
interface RoomMessageEventContentWithoutRelation {
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
};
[Error]
interface ClientError {
Generic(string msg);
};
+457 -3
View File
@@ -1,4 +1,4 @@
use std::{collections::HashMap, fmt, fmt::Display};
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
use matrix_sdk::{
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
@@ -7,14 +7,17 @@ use matrix_sdk::{
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
use uniffi::UnexpectedUniFFICallbackError;
use crate::{room_list::RoomListError, timeline::FocusEventError};
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ClientError {
#[error("client error: {msg}")]
Generic { msg: String },
#[error("api error {code}: {msg}")]
MatrixApi { kind: ErrorKind, code: String, msg: String },
}
impl ClientError {
@@ -43,7 +46,22 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
impl From<matrix_sdk::Error> for ClientError {
fn from(e: matrix_sdk::Error) -> Self {
Self::new(e)
match e {
matrix_sdk::Error::Http(http_error) => {
if let Some(api_error) = http_error.as_client_api_error() {
if let ErrorBody::Standard { kind, message } = &api_error.body {
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
return Self::Generic { msg: message.to_string() };
};
return Self::MatrixApi { kind, code, msg: message.to_owned() };
}
}
Self::Generic { msg: http_error.to_string() }
}
_ => Self::Generic { msg: e.to_string() },
}
}
}
@@ -333,3 +351,439 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
#[derive(thiserror::Error, Debug)]
#[error("not implemented yet")]
pub struct NotYetImplemented;
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
// Please keep the variants sorted alphabetically.
pub enum ErrorKind {
/// `M_BAD_ALIAS`
///
/// One or more [room aliases] within the `m.room.canonical_alias` event do
/// not point to the room ID for which the state event is to be sent to.
///
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
BadAlias,
/// `M_BAD_JSON`
///
/// The request contained valid JSON, but it was malformed in some way, e.g.
/// missing required keys, invalid values for keys.
BadJson,
/// `M_BAD_STATE`
///
/// The state change requested cannot be performed, such as attempting to
/// unban a user who is not banned.
BadState,
/// `M_BAD_STATUS`
///
/// The application service returned a bad status.
BadStatus {
/// The HTTP status code of the response.
status: Option<u16>,
/// The body of the response.
body: Option<String>,
},
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
///
/// The user is unable to reject an invite to join the [server notices]
/// room.
///
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
CannotLeaveServerNoticeRoom,
/// `M_CANNOT_OVERWRITE_MEDIA`
///
/// The [`create_content_async`] endpoint was called with a media ID that
/// already has content.
///
/// [`create_content_async`]: crate::media::create_content_async
CannotOverwriteMedia,
/// `M_CAPTCHA_INVALID`
///
/// The Captcha provided did not match what was expected.
CaptchaInvalid,
/// `M_CAPTCHA_NEEDED`
///
/// A Captcha is required to complete the request.
CaptchaNeeded,
/// `M_CONNECTION_FAILED`
///
/// The connection to the application service failed.
ConnectionFailed,
/// `M_CONNECTION_TIMEOUT`
///
/// The connection to the application service timed out.
ConnectionTimeout,
/// `M_DUPLICATE_ANNOTATION`
///
/// The request is an attempt to send a [duplicate annotation].
///
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
DuplicateAnnotation,
/// `M_EXCLUSIVE`
///
/// The resource being requested is reserved by an application service, or
/// the application service making the request has not created the
/// resource.
Exclusive,
/// `M_FORBIDDEN`
///
/// Forbidden access, e.g. joining a room without permission, failed login.
Forbidden,
/// `M_GUEST_ACCESS_FORBIDDEN`
///
/// The room or resource does not permit [guests] to access it.
///
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
GuestAccessForbidden,
/// `M_INCOMPATIBLE_ROOM_VERSION`
///
/// The client attempted to join a room that has a version the server does
/// not support.
IncompatibleRoomVersion {
/// The room's version.
room_version: String,
},
/// `M_INVALID_PARAM`
///
/// A parameter that was specified has the wrong value. For example, the
/// server expected an integer and instead received a string.
InvalidParam,
/// `M_INVALID_ROOM_STATE`
///
/// The initial state implied by the parameters to the [`create_room`]
/// request is invalid, e.g. the user's `power_level` is set below that
/// necessary to set the room name.
///
/// [`create_room`]: crate::room::create_room
InvalidRoomState,
/// `M_INVALID_USERNAME`
///
/// The desired user name is not valid.
InvalidUsername,
/// `M_LIMIT_EXCEEDED`
///
/// The request has been refused due to [rate limiting]: too many requests
/// have been sent in a short period of time.
///
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
LimitExceeded {
/// How long a client should wait before they can try again.
retry_after_ms: Option<u64>,
},
/// `M_MISSING_PARAM`
///
/// A required parameter was missing from the request.
MissingParam,
/// `M_MISSING_TOKEN`
///
/// No [access token] was specified for the request, but one is required.
///
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
MissingToken,
/// `M_NOT_FOUND`
///
/// No resource was found for this request.
NotFound,
/// `M_NOT_JSON`
///
/// The request did not contain valid JSON.
NotJson,
/// `M_NOT_YET_UPLOADED`
///
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
/// and the content is not yet available.
///
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
NotYetUploaded,
/// `M_RESOURCE_LIMIT_EXCEEDED`
///
/// The request cannot be completed because the homeserver has reached a
/// resource limit imposed on it. For example, a homeserver held in a
/// shared hosting environment may reach a resource limit if it starts
/// using too much memory or disk space.
ResourceLimitExceeded {
/// A URI giving a contact method for the server administrator.
admin_contact: String,
},
/// `M_ROOM_IN_USE`
///
/// The [room alias] specified in the [`create_room`] request is already
/// taken.
///
/// [`create_room`]: crate::room::create_room
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
RoomInUse,
/// `M_SERVER_NOT_TRUSTED`
///
/// The client's request used a third-party server, e.g. identity server,
/// that this server does not trust.
ServerNotTrusted,
/// `M_THREEPID_AUTH_FAILED`
///
/// Authentication could not be performed on the [third-party identifier].
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidAuthFailed,
/// `M_THREEPID_DENIED`
///
/// The server does not permit this [third-party identifier]. This may
/// happen if the server only permits, for example, email addresses from
/// a particular domain.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidDenied,
/// `M_THREEPID_IN_USE`
///
/// The [third-party identifier] is already in use by another user.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidInUse,
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
///
/// The homeserver does not support adding a [third-party identifier] of the
/// given medium.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidMediumNotSupported,
/// `M_THREEPID_NOT_FOUND`
///
/// No account matching the given [third-party identifier] could be found.
///
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
ThreepidNotFound,
/// `M_TOO_LARGE`
///
/// The request or entity was too large.
TooLarge,
/// `M_UNABLE_TO_AUTHORISE_JOIN`
///
/// The room is [restricted] and none of the conditions can be validated by
/// the homeserver. This can happen if the homeserver does not know
/// about any of the rooms listed as conditions, for example.
///
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
UnableToAuthorizeJoin,
/// `M_UNABLE_TO_GRANT_JOIN`
///
/// A different server should be attempted for the join. This is typically
/// because the resident server can see that the joining user satisfies
/// one or more conditions, such as in the case of [restricted rooms],
/// but the resident server would be unable to meet the authorization
/// rules.
///
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
UnableToGrantJoin,
/// `M_UNAUTHORIZED`
///
/// The request was not correctly authorized. Usually due to login failures.
Unauthorized,
/// `M_UNKNOWN`
///
/// An unknown error has occurred.
Unknown,
/// `M_UNKNOWN_TOKEN`
///
/// The [access or refresh token] specified was not recognized.
///
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
UnknownToken {
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
/// the server requires re-authentication but the session is not
/// invalidated. The client can acquire a new access token by
/// specifying the device ID it is already using to the login API.
///
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
soft_logout: bool,
},
/// `M_UNRECOGNIZED`
///
/// The server did not understand the request.
///
/// This is expected to be returned with a 404 HTTP status code if the
/// endpoint is not implemented or a 405 HTTP status code if the
/// endpoint is implemented, but the incorrect HTTP method is used.
Unrecognized,
/// `M_UNSUPPORTED_ROOM_VERSION`
///
/// The request to [`create_room`] used a room version that the server does
/// not support.
///
/// [`create_room`]: crate::room::create_room
UnsupportedRoomVersion,
/// `M_URL_NOT_SET`
///
/// The application service doesn't have a URL configured.
UrlNotSet,
/// `M_USER_DEACTIVATED`
///
/// The user ID associated with the request has been deactivated.
UserDeactivated,
/// `M_USER_IN_USE`
///
/// The desired user ID is already taken.
UserInUse,
/// `M_USER_LOCKED`
///
/// The account has been [locked] and cannot be used at this time.
///
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
UserLocked,
/// `M_USER_SUSPENDED`
///
/// The account has been [suspended] and can only be used for limited
/// actions at this time.
///
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
UserSuspended,
/// `M_WEAK_PASSWORD`
///
/// The password was [rejected] by the server for being too weak.
///
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
WeakPassword,
/// `M_WRONG_ROOM_KEYS_VERSION`
///
/// The version of the [room keys backup] provided in the request does not
/// match the current backup version.
///
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
WrongRoomKeysVersion {
/// The currently active backup version.
current_version: Option<String>,
},
/// A custom API error.
Custom { errcode: String },
}
impl TryFrom<RumaApiErrorKind> for ErrorKind {
type Error = NotYetImplemented;
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
match &value {
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
status: status.map(|code| code.clone().as_u16()),
body: body.clone(),
}),
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
}
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
}
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
RumaApiErrorKind::LimitExceeded { retry_after } => {
let retry_after_ms = match retry_after {
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
Some(RetryAfter::DateTime(system_time)) => {
let duration = system_time.duration_since(SystemTime::now()).ok();
duration.map(|duration| duration.as_millis() as u64)
}
None => None,
};
Ok(ErrorKind::LimitExceeded { retry_after_ms })
}
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
}
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
RumaApiErrorKind::ThreepidMediumNotSupported => {
Ok(ErrorKind::ThreepidMediumNotSupported)
}
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
RumaApiErrorKind::UnknownToken { soft_logout } => {
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
}
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
}
RumaApiErrorKind::_Custom { .. } => {
// There is no way to map the extra values since they're private, so we omit
// them
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
}
// In any other case, return it as the mapping not being yet implemented
_ => Err(NotYetImplemented),
}
}
}
+10
View File
@@ -1033,6 +1033,16 @@ impl Room {
}
})))
}
/// Forget this room.
///
/// This communicates to the homeserver that it should forget the room.
///
/// Only left or banned-from rooms can be forgotten.
pub async fn forget(&self) -> Result<(), ClientError> {
self.inner.forget().await?;
Ok(())
}
}
/// A listener for receiving new live location shares in a room.
@@ -65,6 +65,14 @@ impl RoomPreview {
invite_details.inviter.and_then(|m| m.try_into().ok())
}
/// Forget the room if we had access to it, and it was left or banned.
pub async fn forget(&self) -> Result<(), ClientError> {
let room =
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
room.forget().await?;
Ok(())
}
/// Get the membership details for the current user.
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
let room = self.client.get_room(&self.inner.room_id)?;
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- [**breaking**] `EventCacheStore` allows to control which media content is
+2 -2
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.9.0"
version = "0.10.0"
[package.metadata.docs.rs]
all-features = true
@@ -48,7 +48,7 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.6.0", features = ["serde"] }
bitflags = { version = "2.8.0", features = ["serde"] }
decancer = "3.2.8"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im = { workspace = true }
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type
`TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when
we couldn't compute the push actions, because we lacked some information).
+2 -2
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.9.0"
version = "0.10.0"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -58,7 +58,7 @@ 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"] }
getrandom = { workspace = true, default-features = false, features = ["js"] }
js-sys = { workspace = true }
[lints]
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- [**breaking**] `CollectStrategy::DeviceBasedStrategy` is now split into three
+4 -4
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.9.0"
version = "0.10.0"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -41,7 +41,7 @@ as_variant = { workspace = true }
async-trait = { workspace = true }
bs58 = { version = "0.5.1" }
byteorder = { workspace = true }
cfg-if = "1.0"
cfg-if = "1.0.0"
ctr = "0.9.2"
eyeball = { workspace = true }
futures-core = { workspace = true }
@@ -61,13 +61,13 @@ serde = { workspace = true, features = ["derive", "rc"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
subtle = "2.6.1"
time = { version = "0.3.36", features = ["formatting"] }
time = { version = "0.3.37", features = ["formatting"] }
tokio-stream = { workspace = true, features = ["sync"] }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
url = { workspace = true }
ulid = { version = "1.1.3" }
ulid = { version = "1.1.4" }
uniffi = { workspace = true, optional = true }
vodozemac = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
## [0.9.0] - 2024-12-18
No notable changes in this release.
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-indexeddb"
version = "0.9.0"
version = "0.10.0"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Web's IndexedDB Storage backend for matrix-sdk"
license = "Apache-2.0"
@@ -45,7 +45,7 @@ sha2 = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# for wasm32 we need to activate this
getrandom = { version = "0.2.6", features = ["js"] }
getrandom = { workspace = true, features = ["js"] }
[dev-dependencies]
assert_matches = { workspace = true }
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
## [0.9.0] - 2024-12-18
No notable changes in this release.
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-qrcode"
description = "Library to encode and decode QR codes for interactive verifications in Matrix land"
version = "0.9.0"
version = "0.10.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
+8
View File
@@ -6,12 +6,20 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- [**breaking**] `SqliteEventCacheStore` implements the new APIs of
`EventCacheStore` for `MediaRetentionPolicy`. See the changelog of
`matrix-sdk-base` for more details.
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
- The SQLite databases are optimized during the construction of the stores. It
should improve the performance of the queries.
([#4602](https://github.com/matrix-org/matrix-rust-sdk/pull/4602))
- The size of the WAL files is now limited to 10MB. This avoids cases where the
WAL file takes as much space as the database.
([#4602](https://github.com/matrix-org/matrix-rust-sdk/pull/4602))
## [0.9.0] - 2024-12-18
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-sqlite"
version = "0.9.0"
version = "0.10.0"
edition = "2021"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Sqlite storage backend for matrix-sdk"
@@ -35,7 +35,7 @@ vodozemac = { workspace = true }
[dev-dependencies]
assert_matches = { workspace = true }
glob = "0.3.1"
glob = "0.3.2"
matrix-sdk-base = { workspace = true, features = ["testing"] }
matrix-sdk-common = { workspace = true }
matrix-sdk-crypto = { workspace = true, features = ["testing"] }
@@ -94,8 +94,12 @@ impl SqliteCryptoStore {
passphrase: Option<&str>,
) -> Result<Self, OpenStoreError> {
let conn = pool.get().await?;
conn.set_journal_size_limit().await?;
let version = conn.db_version().await?;
run_migrations(&conn, version).await?;
conn.optimize().await?;
let store_cipher = match passphrase {
Some(p) => Some(Arc::new(conn.get_or_create_store_cipher(p).await?)),
None => None,
@@ -106,8 +106,11 @@ impl SqliteEventCacheStore {
passphrase: Option<&str>,
) -> Result<Self, OpenStoreError> {
let conn = pool.get().await?;
conn.set_journal_size_limit().await?;
let version = conn.db_version().await?;
run_migrations(&conn, version).await?;
conn.optimize().await?;
let store_cipher = match passphrase {
Some(p) => Some(Arc::new(conn.get_or_create_store_cipher(p).await?)),
+12 -1
View File
@@ -69,7 +69,7 @@ mod keys {
/// This is used to figure whether the sqlite database requires a migration.
/// Every new SQL migration should imply a bump of this number, and changes in
/// the [`SqliteStateStore::run_migrations`] function..
const DATABASE_VERSION: u8 = 11;
const DATABASE_VERSION: u8 = 12;
/// A sqlite based cryptostore.
#[derive(Clone)]
@@ -104,6 +104,8 @@ impl SqliteStateStore {
passphrase: Option<&str>,
) -> Result<Self, OpenStoreError> {
let conn = pool.get().await?;
conn.set_journal_size_limit().await?;
let mut version = conn.db_version().await?;
if version == 0 {
@@ -117,6 +119,7 @@ impl SqliteStateStore {
};
let this = Self { store_cipher, pool };
this.run_migrations(&conn, version, None).await?;
conn.optimize().await?;
Ok(this)
}
@@ -329,6 +332,14 @@ impl SqliteStateStore {
.await?;
}
if from < 12 && to >= 12 {
// Defragment the DB and optimize its size on the filesystem.
// This should have been run in the migration for version 7, to reduce the size
// of the DB as we removed the media cache.
conn.execute_batch("VACUUM").await?;
conn.set_kv("version", vec![12]).await?;
}
Ok(())
}
+33 -1
View File
@@ -103,6 +103,38 @@ pub(crate) trait SqliteAsyncConnExt {
where
Res: Send + 'static,
Query: Fn(&Transaction<'_>, Vec<Key>) -> Result<Vec<Res>> + Send + 'static;
/// Optimize the database.
///
/// [The SQLite docs] recommend to run this regularly and after any schema
/// change. The easiest is to do it consistently when the state store is
/// constructed, after eventual migrations.
///
/// [The SQLite docs]: https://www.sqlite.org/pragma.html#pragma_optimize
async fn optimize(&self) -> Result<()> {
self.execute_batch("PRAGMA optimize=0x10002;").await?;
Ok(())
}
/// Limit the size of the WAL file.
///
/// By default, while the DB connections of the databases are open, [the
/// size of the WAL file can keep increasing] depending on the size
/// needed for the transactions. A critical case is VACUUM which
/// basically writes the content of the DB file to the WAL file before
/// writing it back to the DB file, so we end up taking twice the size
/// of the database.
///
/// By setting this limit, the WAL file is truncated after its content is
/// written to the database, if it is bigger than the limit.
///
/// The limit is set to 10MB.
///
/// [the size of the WAL file can keep increasing]: https://www.sqlite.org/wal.html#avoiding_excessively_large_wal_files
async fn set_journal_size_limit(&self) -> Result<()> {
self.execute_batch("PRAGMA journal_size_limit = 10000000;").await.map_err(Error::from)?;
Ok(())
}
}
#[async_trait]
@@ -378,7 +410,7 @@ impl SqliteKeyValueStoreAsyncConnExt for SqliteAsyncConn {
pub(crate) fn repeat_vars(count: usize) -> impl fmt::Display {
assert_ne!(count, 0, "Can't generate zero repeated vars");
iter::repeat("?").take(count).format(",")
iter::repeat_n("?", count).format(",")
}
/// Convert the given `SystemTime` to a timestamp, as the number of seconds
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Bug Fixes
- Remove the usage of an unwrap in the `StoreCipher::import_with_key` method.
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-store-encryption"
version = "0.9.0"
version = "0.10.0"
edition = "2021"
description = "Helpers for encrypted storage keys for the Matrix SDK"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -17,7 +17,7 @@ js = ["dep:getrandom", "getrandom?/js"]
base64 = { workspace = true }
blake3 = "1.5.5"
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
getrandom = { version = "0.2.15", optional = true }
getrandom = { workspace = true, optional = true }
hmac = { workspace = true }
pbkdf2 = { workspace = true }
rand = { workspace = true }
+5
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Bug Fixes
- Don't consider rooms in the banned state to be non-left rooms. This bug was
@@ -14,6 +16,9 @@ All notable changes to this project will be documented in this file.
([#4448](https://github.com/matrix-org/matrix-rust-sdk/pull/4448))
- Fix `EventTimelineItem::latest_edit_json()` when it is populated by a live
edit. ([#4552](https://github.com/matrix-org/matrix-rust-sdk/pull/4552))
- Fix our own explicit read receipt being ignored when loading it from the
state store, which resulted in our own read receipt being wrong sometimes.
([#4600](https://github.com/matrix-org/matrix-rust-sdk/pull/4600))
### Features
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-ui"
description = "GUI-centric utilities on top of matrix-rust-sdk (experimental)."
version = "0.9.0"
version = "0.10.0"
edition = "2021"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"
@@ -990,7 +990,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
let new_item = TimelineItem::new(
prev_item
.with_kind(ti_kind)
.with_content(TimelineItemContent::message(content, None, &txn.items), None),
.with_content(TimelineItemContent::message(content, None, &txn.items)),
prev_item.internal_id.to_owned(),
);
@@ -1605,7 +1605,7 @@ async fn fetch_replied_to_event(
event_id: in_reply_to.to_owned(),
event: TimelineDetails::Pending,
});
let event_item = item.with_content(TimelineItemContent::Message(reply), None);
let event_item = item.with_content(TimelineItemContent::Message(reply));
let new_timeline_item = TimelineItem::new(event_item, internal_id);
state.items.replace(index, new_timeline_item);
@@ -1254,10 +1254,20 @@ impl AllRemoteEvents {
/// Shift to the right all timeline item indexes that are equal to or
/// greater than `new_timeline_item_index`.
fn increment_all_timeline_item_index_after(&mut self, new_timeline_item_index: usize) {
for event_meta in self.0.iter_mut() {
// Traverse items from back to front because:
// - if `new_timeline_item_index` is 0, we need to shift all items anyways, so
// all items must be traversed,
// - otherwise, it's unlikely we want to traverse all items: the item has been
// either inserted or pushed back, so there is no need to traverse the first
// items; we can also break the iteration as soon as all timeline item index
// after `new_timeline_item_index` has been updated.
for event_meta in self.0.iter_mut().rev() {
if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() {
if *timeline_item_index >= new_timeline_item_index {
*timeline_item_index += 1;
} else {
// Items are ordered.
break;
}
}
}
@@ -1266,10 +1276,20 @@ impl AllRemoteEvents {
/// Shift to the left all timeline item indexes that are greater than
/// `removed_wtimeline_item_index`.
fn decrement_all_timeline_item_index_after(&mut self, removed_timeline_item_index: usize) {
for event_meta in self.0.iter_mut() {
// Traverse items from back to front because:
// - if `new_timeline_item_index` is 0, we need to shift all items anyways, so
// all items must be traversed,
// - otherwise, it's unlikely we want to traverse all items: the item has been
// either inserted or pushed back, so there is no need to traverse the first
// items; we can also break the iteration as soon as all timeline item index
// after `new_timeline_item_index` has been updated.
for event_meta in self.0.iter_mut().rev() {
if let Some(timeline_item_index) = event_meta.timeline_item_index.as_mut() {
if *timeline_item_index > removed_timeline_item_index {
*timeline_item_index -= 1;
} else {
// Items are ordered.
break;
}
}
}
@@ -101,6 +101,7 @@ impl ReadReceipts {
// Get old receipt.
let old_receipt = self.get_latest(new_receipt.user_id, &new_receipt.receipt_type);
if old_receipt
.as_ref()
.is_some_and(|(old_receipt_event_id, _)| old_receipt_event_id == new_receipt.event_id)
@@ -108,27 +109,35 @@ impl ReadReceipts {
// The receipt has not changed so there is nothing to do.
return;
}
let old_event_id = old_receipt.map(|(event_id, _)| event_id);
// Find receipts positions.
let mut old_receipt_pos = None;
let mut old_item_pos = None;
let mut old_item_event_id = None;
let mut new_receipt_pos = None;
let mut new_item_pos = None;
let mut new_item_event_id = None;
for (pos, event) in all_events.iter().rev().enumerate() {
if old_event_id == Some(&event.event_id) {
if old_receipt_pos.is_none() && old_event_id == Some(&event.event_id) {
old_receipt_pos = Some(pos);
}
// The receipt should appear on the first event that is visible.
if old_receipt_pos.is_some() && old_item_event_id.is_none() && event.visible {
old_item_pos = event.timeline_item_index;
old_item_event_id = Some(event.event_id.clone());
}
if new_receipt.event_id == event.event_id {
if new_receipt_pos.is_none() && new_receipt.event_id == event.event_id {
new_receipt_pos = Some(pos);
}
// The receipt should appear on the first event that is visible.
if new_receipt_pos.is_some() && new_item_event_id.is_none() && event.visible {
new_item_pos = event.timeline_item_index;
new_item_event_id = Some(event.event_id.clone());
}
@@ -166,6 +175,7 @@ impl ReadReceipts {
if let Some(old_event_id) = old_event_id.cloned() {
self.remove_event_receipt_for_user(&old_event_id, new_receipt.user_id);
}
// Add the new receipt to the new event.
self.add_event_receipt_for_user(
new_receipt.event_id.to_owned(),
@@ -193,9 +203,12 @@ impl ReadReceipts {
}
let timeline_update = ReadReceiptTimelineUpdate {
old_item_pos,
old_event_id: old_item_event_id,
new_item_pos,
new_event_id: new_item_event_id,
};
timeline_update.apply(
timeline_items,
new_receipt.user_id.to_owned(),
@@ -273,27 +286,51 @@ struct FullReceipt<'a> {
/// A read receipt update in the timeline.
#[derive(Clone, Debug, Default)]
struct ReadReceiptTimelineUpdate {
/// The position of the timeline item that had the old receipt of the user,
/// if any.
old_item_pos: Option<usize>,
/// The old event that had the receipt of the user, if any.
old_event_id: Option<OwnedEventId>,
/// The position of the timeline item that has the new receipt of the user,
/// if any.
new_item_pos: Option<usize>,
/// The new event that has the receipt of the user, if any.
new_event_id: Option<OwnedEventId>,
}
impl ReadReceiptTimelineUpdate {
/// Remove the old receipt from the corresponding timeline item.
fn remove_old_receipt(&self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) {
fn remove_old_receipt(&mut self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) {
let Some(event_id) = &self.old_event_id else {
// Nothing to do.
return;
};
let Some((receipt_pos, event_item)) = rfind_event_by_id(items, event_id) else {
let item_pos = self.old_item_pos.or_else(|| {
items
.iter()
.enumerate()
.rev()
.filter_map(|(nth, item)| Some((nth, item.as_event()?)))
.find_map(|(nth, event_item)| {
(event_item.event_id() == Some(event_id)).then_some(nth)
})
});
let Some(item_pos) = item_pos else {
debug!(%event_id, %user_id, "inconsistent state: old event item for read receipt was not found");
return;
};
let event_item_id = event_item.internal_id.to_owned();
let mut event_item = event_item.clone();
self.old_item_pos = Some(item_pos);
let event_item = &items[item_pos];
let event_item_id = event_item.unique_id().to_owned();
let Some(mut event_item) = event_item.as_event().cloned() else {
warn!("received a read receipt for a virtual item, this should not be possible");
return;
};
if let Some(remote_event_item) = event_item.as_remote_mut() {
if remote_event_item.read_receipts.swap_remove(user_id).is_none() {
@@ -303,7 +340,7 @@ impl ReadReceiptTimelineUpdate {
receipt doesn't have a receipt for the user"
);
}
items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id));
items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
} else {
warn!("received a read receipt for a local item, this should not be possible");
}
@@ -321,18 +358,40 @@ impl ReadReceiptTimelineUpdate {
return;
};
let Some((receipt_pos, event_item)) = rfind_event_by_id(items, &event_id) else {
// This can happen for new timeline items, the receipts will be loaded directly
// during construction of the item.
let item_pos = self.new_item_pos.or_else(|| {
items
.iter()
.enumerate()
// Don't iterate over all items if the `old_item_pos` is known: the `item_pos`
// for the new item is necessarily _after_ the old item.
.skip(self.old_item_pos.unwrap_or(0))
.rev()
.filter_map(|(nth, item)| Some((nth, item.as_event()?)))
.find_map(|(nth, event_item)| {
(event_item.event_id() == Some(&event_id)).then_some(nth)
})
});
let Some(item_pos) = item_pos else {
debug!(%event_id, %user_id, "inconsistent state: new event item for read receipt was not found");
return;
};
let event_item_id = event_item.internal_id.to_owned();
let mut event_item = event_item.clone();
debug_assert!(
item_pos >= self.old_item_pos.unwrap_or(0),
"The new receipt must be added on a timeline item that is _after_ the timeline item that was holding the old receipt");
let event_item = &items[item_pos];
let event_item_id = event_item.unique_id().to_owned();
let Some(mut event_item) = event_item.as_event().cloned() else {
warn!("received a read receipt for a virtual item, this should not be possible");
return;
};
if let Some(remote_event_item) = event_item.as_remote_mut() {
remote_event_item.read_receipts.insert(user_id, receipt);
items.replace(receipt_pos, TimelineItem::new(event_item, event_item_id));
items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
} else {
warn!("received a read receipt for a local item, this should not be possible");
}
@@ -340,7 +399,7 @@ impl ReadReceiptTimelineUpdate {
/// Apply this update to the timeline.
fn apply(
self,
mut self,
items: &mut ObservableItemsTransaction<'_>,
user_id: OwnedUserId,
receipt: Receipt,
@@ -395,10 +454,7 @@ impl TimelineStateTransaction<'_> {
room_data_provider: &P,
) {
let read_receipts = room_data_provider.load_event_receipts(event_id).await;
// Filter out receipts for our own user.
let own_user_id = room_data_provider.own_user_id();
let read_receipts = read_receipts.into_iter().filter(|(user_id, _)| user_id != own_user_id);
// Since they are explicit read receipts, we need to check if they are
// superseded by implicit read receipts.
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use std::{ops::ControlFlow, sync::Arc};
use as_variant::as_variant;
use indexmap::IndexMap;
@@ -51,7 +51,7 @@ use ruma::{
use tracing::{debug, error, field::debug, info, instrument, trace, warn};
use super::{
algorithms::{rfind_event_by_id, rfind_event_item},
algorithms::rfind_event_by_id,
controller::{
ObservableItemsTransaction, ObservableItemsTransactionEntry, PendingEdit, PendingEditKind,
TimelineMetadata, TimelineStateTransaction,
@@ -685,7 +685,8 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
let mut new_msg = msg.clone();
new_msg.apply_edit(new_content);
let mut new_item = item.with_content(TimelineItemContent::Message(new_msg), edit_json);
let mut new_item =
item.with_content_and_latest_edit(TimelineItemContent::Message(new_msg), edit_json);
if let Flow::Remote { encryption_info, .. } = &self.ctx.flow {
new_item = new_item.with_encryption_info(encryption_info.clone());
@@ -830,7 +831,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
}
};
Some(item.with_content(new_content, edit_json))
Some(item.with_content_and_latest_edit(new_content, edit_json))
}
/// Adds a new poll to the timeline.
@@ -893,14 +894,11 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
return;
};
let new_item = item.with_content(
TimelineItemContent::Poll(poll_state.add_response(
&self.ctx.sender,
self.ctx.timestamp,
&c,
)),
None,
);
let new_item = item.with_content(TimelineItemContent::Poll(poll_state.add_response(
&self.ctx.sender,
self.ctx.timestamp,
&c,
)));
trace!("Adding poll response.");
self.items.replace(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned()));
@@ -919,7 +917,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
match poll_state.end(self.ctx.timestamp) {
Ok(poll_state) => {
let new_item = item.with_content(TimelineItemContent::Poll(poll_state), None);
let new_item = item.with_content(TimelineItemContent::Poll(poll_state));
trace!("Ending poll.");
self.items
@@ -1261,18 +1259,48 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
event_id: Option<&EventId>,
transaction_id: Option<&TransactionId>,
) -> Option<Arc<TimelineItem>> {
// Start with the canonical case: detect a local timeline item that matches
// `event_id` or `transaction_id`.
if let Some((local_timeline_item_index, local_timeline_item)) =
rfind_event_item(items, |event_timeline_item| {
if event_timeline_item.is_local_echo() {
event_id == event_timeline_item.event_id()
|| (transaction_id.is_some()
&& transaction_id == event_timeline_item.transaction_id())
// Detect a local timeline item that matches `event_id` or `transaction_id`.
if let Some((local_timeline_item_index, local_timeline_item)) = items
.iter()
// Get the index of each item.
.enumerate()
// Iterate from the end to the start.
.rev()
// Use a `Iterator::try_fold` to produce a single value, and to stop the iterator
// when a non local event timeline item is met. We want to stop iterating when:
//
// - a duplicate local event timeline item has been found,
// - a non local event timeline item is met,
// - a non event timeline is met.
//
// Indeed, it is a waste of time to iterate over all items in `items`. Local event
// timeline items are necessarily at the end of `items`: as soon as they have been
// iterated, we can stop the entire iteration.
.try_fold((), |(), (nth, timeline_item)| {
let Some(event_timeline_item) = timeline_item.as_event() else {
// Not an event timeline item? Stop iterating here.
return ControlFlow::Break(None);
};
// Not a local event timeline item? Stop iterating here.
if !event_timeline_item.is_local_echo() {
return ControlFlow::Break(None);
}
if event_id == event_timeline_item.event_id()
|| (transaction_id.is_some()
&& transaction_id == event_timeline_item.transaction_id())
{
// A duplicate local event timeline item has been found!
ControlFlow::Break(Some((nth, event_timeline_item)))
} else {
false
// This local event timeline is not the one we are looking for. Continue our
// search.
ControlFlow::Continue(())
}
})
.break_value()
.flatten()
{
trace!(
?event_id,
@@ -1281,7 +1309,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
"Removing local timeline item"
);
transfer_details(new_event_timeline_item, &local_timeline_item);
transfer_details(new_event_timeline_item, local_timeline_item);
// Remove the local timeline item.
return Some(items.remove(local_timeline_item_index));
@@ -1312,7 +1340,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
let new_reply_content =
TimelineItemContent::Message(message.with_in_reply_to(in_reply_to));
let new_reply_item =
entry.with_kind(event_item.with_content(new_reply_content, None));
entry.with_kind(event_item.with_content(new_reply_content));
ObservableItemsTransactionEntry::replace(&mut entry, new_reply_item);
}
});
@@ -509,11 +509,18 @@ impl EventTimelineItem {
Self { reactions, ..self.clone() }
}
/// Clone the current event item, and update its content.
pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
let mut new = self.clone();
new.content = new_content;
new
}
/// Clone the current event item, and update its content.
///
/// Optionally update `latest_edit_json` if the update is an edit received
/// from the server.
pub(super) fn with_content(
pub(super) fn with_content_and_latest_edit(
&self,
new_content: TimelineItemContent,
edit_json: Option<Raw<AnySyncTimelineEvent>>,
@@ -523,7 +530,6 @@ impl EventTimelineItem {
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
r.latest_edit_json = edit_json;
}
new
}
@@ -20,7 +20,7 @@ use matrix_sdk::deserialized_responses::{
AlgorithmInfo, EncryptionInfo, VerificationLevel, VerificationState,
};
use matrix_sdk_base::deserialized_responses::{DecryptedRoomEvent, TimelineEvent};
use matrix_sdk_test::{async_test, ALICE};
use matrix_sdk_test::{async_test, ALICE, BOB};
use ruma::{
event_id,
events::{
@@ -369,3 +369,75 @@ async fn test_relations_edit_overrides_pending_edit_poll() {
assert_pending!(stream);
}
#[async_test]
async fn test_updated_reply_doesnt_lose_latest_edit() {
let timeline = TestTimeline::new();
let mut stream = timeline.subscribe_events().await;
let f = &timeline.factory;
// Start with a message event.
let target = event_id!("$1");
timeline.handle_live_event(f.text_msg("hey").sender(&ALICE).event_id(target)).await;
{
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
assert!(item.latest_edit_json().is_none());
assert_eq!(item.content().as_message().unwrap().body(), "hey");
assert_pending!(stream);
}
// Have someone send a reply.
let reply = event_id!("$2");
timeline
.handle_live_event(f.text_msg("hallo").sender(&BOB).reply_to(target).event_id(reply))
.await;
{
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
assert!(item.latest_edit_json().is_none());
assert_eq!(item.content().as_message().unwrap().body(), "hallo");
assert_pending!(stream);
}
// Edit the reply.
timeline
.handle_live_event(
f.text_msg("* guten tag")
.sender(&BOB)
.edit(reply, MessageType::text_plain("guten tag").into()),
)
.await;
{
let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value);
assert!(item.latest_edit_json().is_some());
assert_eq!(item.content().as_message().unwrap().body(), "guten tag");
assert_pending!(stream);
}
// Edit the original.
timeline
.handle_live_event(
f.text_msg("* hello")
.sender(&ALICE)
.edit(target, MessageType::text_plain("hello").into()),
)
.await;
{
// The reply is updated.
let item = assert_next_matches!(stream, VectorDiff::Set { index: 1, value } => value);
// And still has the latest edit JSON.
assert!(item.latest_edit_json().is_some());
assert_eq!(item.content().as_message().unwrap().body(), "guten tag");
// The original is updated.
let item = assert_next_matches!(stream, VectorDiff::Set { index: 0, value } => value);
// And now has a latest edit JSON.
assert!(item.latest_edit_json().is_some());
assert_pending!(stream);
}
}
+11 -6
View File
@@ -37,9 +37,8 @@ use matrix_sdk::{
use matrix_sdk_base::{
crypto::types::events::CryptoContextInfo, latest_event::LatestEvent, RoomInfo, RoomState,
};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, BOB, DEFAULT_TEST_ROOM_ID};
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
use ruma::{
event_id,
events::{
reaction::ReactionEventContent,
receipt::{Receipt, ReceiptThread, ReceiptType},
@@ -353,11 +352,17 @@ impl RoomDataProvider for TestRoomDataProvider {
&'a self,
event_id: &'a EventId,
) -> IndexMap<OwnedUserId, Receipt> {
if event_id == event_id!("$event_with_bob_receipt") {
[(BOB.to_owned(), Receipt::new(MilliSecondsSinceUnixEpoch(uint!(10))))].into()
} else {
IndexMap::new()
let mut map = IndexMap::new();
for (user_id, (receipt_event_id, receipt)) in
self.initial_user_receipts.values().flat_map(|m| m.values()).flatten()
{
if receipt_event_id == event_id {
map.insert(user_id.clone(), receipt.clone());
}
}
map
}
async fn push_rules_and_context(&self) -> Option<(Ruleset, PushConditionRoomCtx)> {
@@ -187,6 +187,49 @@ async fn test_events_received_before_start_are_not_lost() {
assert_eq!(results.votes["1"], vec![ALICE.to_string()]);
}
#[async_test]
async fn test_adding_response_doesnt_clear_latest_json_edit() {
let timeline = TestTimeline::new();
// Alice sends the poll.
timeline.send_poll_start(&ALICE, fakes::poll_a()).await;
// Alice edits the poll.
let poll_item = timeline.poll_event().await;
let poll_id = poll_item.event_id().unwrap();
timeline.send_poll_edit(&ALICE, poll_id, fakes::poll_b()).await;
// Sanity check: the poll has a latest edit JSON.
assert!(timeline.event_items().await[0].latest_edit_json().is_some());
// Now Bob also votes
timeline.send_poll_response(&BOB, vec!["0"], poll_id).await;
// The poll still has a latest edit JSON.
assert!(timeline.event_items().await[0].latest_edit_json().is_some());
}
#[async_test]
async fn test_ending_poll_doesnt_clear_latest_json_edit() {
let timeline = TestTimeline::new();
// Alice sends the poll.
timeline.send_poll_start(&ALICE, fakes::poll_a()).await;
let poll_item = timeline.poll_event().await;
let poll_id = poll_item.event_id().unwrap();
// Alice edits the poll.
timeline.send_poll_edit(&ALICE, poll_id, fakes::poll_b()).await;
// Sanity check: the poll has a latest edit JSON.
assert!(timeline.event_items().await[0].latest_edit_json().is_some());
// Now the poll is ended.
timeline.send_poll_end(&ALICE, "ended", poll_id).await;
// The poll still has a latest edit JSON.
assert!(timeline.event_items().await[0].latest_edit_json().is_some());
}
impl TestTimeline {
async fn event_items(&self) -> Vec<EventTimelineItem> {
self.controller.items().await.iter().filter_map(|item| item.as_event().cloned()).collect()
@@ -220,7 +220,27 @@ async fn test_read_receipts_updates_on_filtered_events() {
#[async_test]
async fn test_read_receipts_updates_on_filtered_events_with_stored() {
let timeline = TestTimeline::new().with_settings(TimelineSettings {
let event_with_bob_receipt_id = event_id!("$event_with_bob_receipt");
// Add initial unthreaded private receipt.
let mut initial_user_receipts = ReadReceiptMap::new();
initial_user_receipts
.entry(ReceiptType::Read)
.or_default()
.entry(ReceiptThread::Unthreaded)
.or_default()
.insert(
BOB.to_owned(),
(
event_with_bob_receipt_id.to_owned(),
Receipt::new(ruma::MilliSecondsSinceUnixEpoch(uint!(5))),
),
);
let timeline = TestTimeline::with_room_data_provider(
TestRoomDataProvider::default().with_initial_user_receipts(initial_user_receipts),
)
.with_settings(TimelineSettings {
track_read_receipts: true,
event_filter: Arc::new(filter_notice),
..Default::default()
@@ -230,9 +250,7 @@ async fn test_read_receipts_updates_on_filtered_events_with_stored() {
timeline.handle_live_event(f.text_msg("A").sender(*ALICE)).await;
timeline
.handle_live_event(
f.notice("B").sender(*CAROL).event_id(event_id!("$event_with_bob_receipt")),
)
.handle_live_event(f.notice("B").sender(*CAROL).event_id(event_with_bob_receipt_id))
.await;
// No read receipt for our own user.
@@ -272,7 +290,27 @@ async fn test_read_receipts_updates_on_filtered_events_with_stored() {
#[async_test]
async fn test_read_receipts_updates_on_back_paginated_filtered_events() {
let timeline = TestTimeline::new().with_settings(TimelineSettings {
let event_with_bob_receipt_id = event_id!("$event_with_bob_receipt");
// Add initial unthreaded private receipt.
let mut initial_user_receipts = ReadReceiptMap::new();
initial_user_receipts
.entry(ReceiptType::Read)
.or_default()
.entry(ReceiptThread::Unthreaded)
.or_default()
.insert(
BOB.to_owned(),
(
event_with_bob_receipt_id.to_owned(),
Receipt::new(ruma::MilliSecondsSinceUnixEpoch(uint!(5))),
),
);
let timeline = TestTimeline::with_room_data_provider(
TestRoomDataProvider::default().with_initial_user_receipts(initial_user_receipts),
)
.with_settings(TimelineSettings {
track_read_receipts: true,
event_filter: Arc::new(filter_notice),
..Default::default()
@@ -289,10 +327,7 @@ async fn test_read_receipts_updates_on_back_paginated_filtered_events() {
.await;
timeline
.handle_back_paginated_event(
f.notice("B")
.sender(*CAROL)
.event_id(event_id!("$event_with_bob_receipt"))
.into_raw_timeline(),
f.notice("B").sender(*CAROL).event_id(event_with_bob_receipt_id).into_raw_timeline(),
)
.await;
@@ -612,3 +647,91 @@ async fn test_clear_read_receipts() {
assert_eq!(event_b.read_receipts().len(), 1);
assert!(event_b.read_receipts().get(*BOB).is_some());
}
#[async_test]
async fn test_implicit_read_receipt_before_explicit_read_receipt() {
// Test a timeline in this order:
// 1. $alice_event: sent by alice, has no explicit read receipts.
// 2. $bob_event: sent by bob, has no explicit read receipts.
// 3. $carol_event: sent by carol, has the explicit read receipts of all users.
let room_id = room_id!("!room:localhost");
let alice_event_id = owned_event_id!("$alice_event");
let bob_event_id = owned_event_id!("$bob_event");
let carol_event_id = owned_event_id!("$carol_event");
// Add initial unthreaded private receipt.
let mut initial_user_receipts = ReadReceiptMap::new();
let unthreaded_read_receipts = initial_user_receipts
.entry(ReceiptType::Read)
.or_default()
.entry(ReceiptThread::Unthreaded)
.or_default();
unthreaded_read_receipts.insert(
ALICE.to_owned(),
(carol_event_id.clone(), Receipt::new(ruma::MilliSecondsSinceUnixEpoch(uint!(10)))),
);
unthreaded_read_receipts.insert(
BOB.to_owned(),
(carol_event_id.clone(), Receipt::new(ruma::MilliSecondsSinceUnixEpoch(uint!(5)))),
);
unthreaded_read_receipts.insert(
CAROL.to_owned(),
(carol_event_id.clone(), Receipt::new(ruma::MilliSecondsSinceUnixEpoch(uint!(1)))),
);
let timeline = TestTimeline::with_room_data_provider(
TestRoomDataProvider::default().with_initial_user_receipts(initial_user_receipts),
)
.with_settings(TimelineSettings { track_read_receipts: true, ..Default::default() });
// Check that the receipts are at the correct place.
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*ALICE).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*BOB).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*CAROL).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
// Add the events.
timeline
.handle_back_paginated_event(
timeline
.factory
.text_msg("I am Carol!")
.sender(*CAROL)
.room(room_id)
.event_id(&carol_event_id)
.into_raw_timeline(),
)
.await;
timeline
.handle_back_paginated_event(
timeline
.factory
.text_msg("I am Bob!")
.sender(*BOB)
.room(room_id)
.event_id(&bob_event_id)
.into_raw_timeline(),
)
.await;
timeline
.handle_back_paginated_event(
timeline
.factory
.text_msg("I am Alice!")
.sender(*ALICE)
.room(room_id)
.event_id(&alice_event_id)
.into_raw_timeline(),
)
.await;
// The receipts shouldn't have moved.
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*ALICE).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*BOB).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
let (receipt_event_id, _) = timeline.controller.latest_user_read_receipt(*CAROL).await.unwrap();
assert_eq!(receipt_event_id, carol_event_id);
}
+2
View File
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
### Features
- Allow to set and check whether an image is animated via its `ImageInfo`.
+9 -9
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.9.0"
version = "0.10.0"
[package.metadata.docs.rs]
features = ["docsrs"]
@@ -68,11 +68,11 @@ as_variant = { workspace = true }
async-channel = "2.3.1"
async-stream = { workspace = true }
async-trait = { workspace = true }
axum = { version = "0.7.9", optional = true }
bytes = "1.8.0"
bytesize = "1.3"
axum = { version = "0.8.1", optional = true }
bytes = "1.9.0"
bytesize = "1.3.0"
chrono = { workspace = true, optional = true }
event-listener = "5.3.1"
event-listener = "5.4.0"
eyeball = { workspace = true }
eyeball-im = { workspace = true }
eyre = { version = "0.6.12", optional = true }
@@ -113,7 +113,7 @@ sha2 = { workspace = true, optional = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio-stream = { workspace = true, features = ["sync"] }
tower = { version = "0.5.1", features = ["util"], optional = true }
tower = { version = "0.5.2", features = ["util"], optional = true }
tracing = { workspace = true, features = ["attributes"] }
uniffi = { workspace = true, optional = true }
url = { workspace = true, features = ["serde"] }
@@ -129,12 +129,12 @@ tokio = { workspace = true, features = ["macros"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
backoff = { version = "0.4.0", features = ["tokio"] }
openidconnect = { version = "4.0.0-rc.1", optional = true }
openidconnect = { version = "4.0.0", optional = true }
# only activate reqwest's stream feature on non-wasm, the wasm part seems to not
# support *sending* streams, which makes it useless for us.
reqwest = { workspace = true, features = ["stream", "gzip", "http2"] }
tokio = { workspace = true, features = ["fs", "rt", "macros"] }
tokio-util = "0.7.12"
tokio-util = "0.7.13"
wiremock = { workspace = true, optional = true }
[dev-dependencies]
@@ -142,7 +142,7 @@ anyhow = { workspace = true }
assert-json-diff = { workspace = true }
assert_matches = { workspace = true }
assert_matches2 = { workspace = true }
dirs = "5.0.1"
dirs = "6.0.0"
futures-executor = { workspace = true }
matrix-sdk-base = { workspace = true, features = ["testing"] }
matrix-sdk-test = { workspace = true }
@@ -371,9 +371,11 @@ impl Oidc {
/// use anyhow::bail;
/// use futures_util::StreamExt;
/// use matrix_sdk::{
/// authentication::qrcode::{LoginProgress, QrCodeData, QrCodeModeData},
/// authentication::{
/// oidc::types::registration::VerifiedClientMetadata,
/// qrcode::{LoginProgress, QrCodeData, QrCodeModeData},
/// },
/// Client,
/// oidc::types::registration::VerifiedClientMetadata,
/// };
/// # fn client_metadata() -> VerifiedClientMetadata { unimplemented!() }
/// # _ = async {
@@ -717,7 +719,7 @@ impl Oidc {
/// ```no_run
/// use futures_util::StreamExt;
/// use matrix_sdk::Client;
/// # fn persist_session(_: &matrix_sdk::oidc::OidcSession) {}
/// # fn persist_session(_: &matrix_sdk::authentication::oidc::OidcSession) {}
/// # _ = async {
/// let homeserver = "http://example.com";
/// let client = Client::builder()
@@ -831,9 +833,9 @@ impl Oidc {
///
/// ```no_run
/// use matrix_sdk::{Client, ServerName};
/// use matrix_sdk::oidc::types::client_credentials::ClientCredentials;
/// use matrix_sdk::oidc::types::registration::ClientMetadata;
/// # use matrix_sdk::oidc::types::registration::VerifiedClientMetadata;
/// use matrix_sdk::authentication::oidc::types::client_credentials::ClientCredentials;
/// use matrix_sdk::authentication::oidc::types::registration::ClientMetadata;
/// # use matrix_sdk::authentication::oidc::types::registration::VerifiedClientMetadata;
/// # let client_metadata = ClientMetadata::default().validate().unwrap();
/// # fn persist_client_registration (_: &str, _: &ClientMetadata, _: &ClientCredentials) {}
/// # _ = async {
@@ -1065,7 +1067,7 @@ impl Oidc {
/// ```no_run
/// # use anyhow::anyhow;
/// use matrix_sdk::{Client};
/// # use matrix_sdk::oidc::AuthorizationResponse;
/// # use matrix_sdk::authentication::oidc::AuthorizationResponse;
/// # use url::Url;
/// # let homeserver = Url::parse("https://example.com").unwrap();
/// # let redirect_uri = Url::parse("http://127.0.0.1/oidc").unwrap();
+124 -10
View File
@@ -81,7 +81,7 @@ use crate::{
},
config::RequestConfig,
deduplicating_handler::DeduplicatingHandler,
error::{HttpError, HttpResult},
error::HttpResult,
event_cache::EventCache,
event_handler::{
EventHandler, EventHandlerContext, EventHandlerDropGuard, EventHandlerHandle,
@@ -93,8 +93,8 @@ use crate::{
send_queue::SendQueueData,
sliding_sync::Version as SlidingSyncVersion,
sync::{RoomUpdate, SyncResponse},
Account, AuthApi, AuthSession, Error, Media, Pusher, RefreshTokenError, Result, Room,
TransmissionProgress,
Account, AuthApi, AuthSession, Error, HttpError, Media, Pusher, RefreshTokenError, Result,
Room, TransmissionProgress,
};
#[cfg(feature = "e2e-encryption")]
use crate::{
@@ -1155,16 +1155,20 @@ impl Client {
};
if let Some(room) = self.get_room(&room_id) {
// The cached data can only be trusted if the room is joined: for invite and
// knock rooms, no updates will be received for the rooms after the invite/knock
// action took place so we may have very out to date data for important fields
// such as `join_rule`
if room.state() == RoomState::Joined {
return Ok(RoomPreview::from_joined(&room).await);
// The cached data can only be trusted if the room state is joined or
// banned: for invite and knock rooms, no updates will be received
// for the rooms after the invite/knock action took place so we may
// have very out to date data for important fields such as
// `join_rule`. For left rooms, the homeserver should return the latest info.
match room.state() {
RoomState::Joined | RoomState::Banned => {
return Ok(RoomPreview::from_known_room(&room).await);
}
RoomState::Left | RoomState::Invited | RoomState::Knocked => {}
}
}
RoomPreview::from_not_joined(self, room_id, room_or_alias_id, via).await
RoomPreview::from_remote_room(self, room_id, room_or_alias_id, via).await
}
/// Resolve a room alias to a room id and a list of servers which know
@@ -3182,4 +3186,114 @@ pub(crate) mod tests {
.await;
assert_matches!(ret, Ok(()));
}
#[async_test]
async fn test_room_preview_for_invited_room_hits_summary_endpoint() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room_id = room_id!("!a-room:matrix.org");
// Make sure the summary endpoint is called once
server.mock_room_summary().ok(room_id).mock_once().mount().await;
// We create a locally cached invited room
let invited_room = client.inner.base_client.get_or_create_room(room_id, RoomState::Invited);
// And we get a preview, the server endpoint was reached
let preview = client
.get_room_preview(room_id.into(), Vec::new())
.await
.expect("Room preview should be retrieved");
assert_eq!(invited_room.room_id().to_owned(), preview.room_id);
}
#[async_test]
async fn test_room_preview_for_left_room_hits_summary_endpoint() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room_id = room_id!("!a-room:matrix.org");
// Make sure the summary endpoint is called once
server.mock_room_summary().ok(room_id).mock_once().mount().await;
// We create a locally cached left room
let left_room = client.inner.base_client.get_or_create_room(room_id, RoomState::Left);
// And we get a preview, the server endpoint was reached
let preview = client
.get_room_preview(room_id.into(), Vec::new())
.await
.expect("Room preview should be retrieved");
assert_eq!(left_room.room_id().to_owned(), preview.room_id);
}
#[async_test]
async fn test_room_preview_for_knocked_room_hits_summary_endpoint() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room_id = room_id!("!a-room:matrix.org");
// Make sure the summary endpoint is called once
server.mock_room_summary().ok(room_id).mock_once().mount().await;
// We create a locally cached knocked room
let knocked_room = client.inner.base_client.get_or_create_room(room_id, RoomState::Knocked);
// And we get a preview, the server endpoint was reached
let preview = client
.get_room_preview(room_id.into(), Vec::new())
.await
.expect("Room preview should be retrieved");
assert_eq!(knocked_room.room_id().to_owned(), preview.room_id);
}
#[async_test]
async fn test_room_preview_for_joined_room_retrieves_local_room_info() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room_id = room_id!("!a-room:matrix.org");
// Make sure the summary endpoint is not called
server.mock_room_summary().ok(room_id).never().mount().await;
// We create a locally cached joined room
let joined_room = client.inner.base_client.get_or_create_room(room_id, RoomState::Joined);
// And we get a preview, no server endpoint was reached
let preview = client
.get_room_preview(room_id.into(), Vec::new())
.await
.expect("Room preview should be retrieved");
assert_eq!(joined_room.room_id().to_owned(), preview.room_id);
}
#[async_test]
async fn test_room_preview_for_banned_room_retrieves_local_room_info() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room_id = room_id!("!a-room:matrix.org");
// Make sure the summary endpoint is not called
server.mock_room_summary().ok(room_id).never().mount().await;
// We create a locally cached banned room
let banned_room = client.inner.base_client.get_or_create_room(room_id, RoomState::Banned);
// And we get a preview, no server endpoint was reached
let preview = client
.get_room_preview(room_id.into(), Vec::new())
.await
.expect("Room preview should be retrieved");
assert_eq!(banned_room.room_id().to_owned(), preview.room_id);
}
}
@@ -17,7 +17,7 @@
use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration};
use eyeball::Subscriber;
use matrix_sdk_base::{deserialized_responses::TimelineEvent, timeout::timeout};
use matrix_sdk_base::timeout::timeout;
use matrix_sdk_common::linked_chunk::ChunkContent;
use tracing::{debug, instrument, trace};
@@ -180,7 +180,6 @@ impl RoomPagination {
// (backward). The `RoomEvents` API expects the first event to be the oldest.
.rev()
.cloned()
.map(TimelineEvent::from)
.collect::<Vec<_>>();
let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos);
@@ -22,7 +22,7 @@ use matrix_sdk_common::linked_chunk::{
ObservableUpdates, Position,
};
use ruma::{
events::{room::redaction::SyncRoomRedactionEvent, AnySyncTimelineEvent},
events::{room::redaction::SyncRoomRedactionEvent, AnySyncTimelineEvent, MessageLikeEventType},
OwnedEventId, RoomVersionId,
};
use tracing::{debug, error, instrument, trace, warn};
@@ -97,6 +97,18 @@ impl RoomEvents {
/// event in the chunk, and replace it by the redacted form.
#[instrument(skip_all)]
fn maybe_apply_new_redaction(&mut self, room_version: &RoomVersionId, event: &Event) {
let raw_event = event.raw();
// Do not deserialise the entire event if we aren't certain it's a
// `m.room.redaction`. It saves a non-negligible amount of computations.
let Ok(Some(MessageLikeEventType::RoomRedaction)) =
raw_event.get_field::<MessageLikeEventType>("type")
else {
return;
};
// It is a `m.room.redaction`! We can deserialize it entirely.
let Ok(AnySyncTimelineEvent::MessageLike(
ruma::events::AnySyncMessageLikeEvent::RoomRedaction(redaction),
)) = event.raw().deserialize()
+1 -4
View File
@@ -148,10 +148,7 @@ impl EventSource for &Room {
}
trace!("trying with /event now");
self.event(event_id, None)
.await
.map(Into::into)
.map_err(|err| EditError::Fetch(Box::new(err)))
self.event(event_id, None).await.map_err(|err| EditError::Fetch(Box::new(err)))
}
}
@@ -194,17 +194,14 @@ fn wrap_room_member_events(
#[cfg(test)]
mod tests {
use std::{
pin::{pin, Pin},
time::Duration,
};
use std::time::Duration;
use futures_core::Stream;
use futures_util::FutureExt;
use matrix_sdk_base::crypto::{IdentityState, IdentityStatusChange};
use futures_util::{pin_mut, FutureExt as _, StreamExt as _};
use matrix_sdk_base::crypto::IdentityState;
use matrix_sdk_test::{async_test, test_json::keys_query_sets::IdentityChangeDataSet};
use test_setup::TestSetup;
use tokio_stream::{StreamExt, Timeout};
use crate::assert_next_with_timeout;
#[async_test]
async fn test_when_user_becomes_unpinned_we_report_it() {
@@ -215,13 +212,14 @@ mod tests {
t.pin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob becomes unpinned
t.unpin_bob().await;
// Then we were notified about it
let change = next_change(&mut pin!(changes)).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::PinViolation);
assert_eq!(change.len(), 1);
@@ -236,13 +234,14 @@ mod tests {
t.verify_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob's identity changes
t.unpin_bob().await;
// Then we were notified about a verification violation
let change = next_change(&mut pin!(changes)).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change.len(), 1);
@@ -257,20 +256,20 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob becomes pinned
t.pin_bob().await;
// Then we were notified about the initial state of the room
let change1 = next_change(&mut changes).await;
let change1 = assert_next_with_timeout!(stream);
assert_eq!(change1[0].user_id, t.bob_user_id());
assert_eq!(change1[0].changed_to, IdentityState::PinViolation);
assert_eq!(change1.len(), 1);
// And the change when Bob became pinned
let change2 = next_change(&mut changes).await;
let change2 = assert_next_with_timeout!(stream);
assert_eq!(change2[0].user_id, t.bob_user_id());
assert_eq!(change2[0].changed_to, IdentityState::Pinned);
assert_eq!(change2.len(), 1);
@@ -282,8 +281,8 @@ mod tests {
let t = TestSetup::new_room_with_other_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob becomes verified
t.verify_bob().await;
@@ -292,10 +291,10 @@ mod tests {
t.unpin_bob().await;
// Then we are only notified about the unpinning part
let change2 = next_change(&mut changes).await;
assert_eq!(change2[0].user_id, t.bob_user_id());
assert_eq!(change2[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change2.len(), 1);
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change.len(), 1);
}
#[async_test]
@@ -307,20 +306,20 @@ mod tests {
t.unpin_bob_with(IdentityChangeDataSet::key_query_with_identity_a()).await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob becomes verified
t.verify_bob().await;
// Then we were notified about the initial state of the room
let change1 = next_change(&mut changes).await;
let change1 = assert_next_with_timeout!(stream);
assert_eq!(change1[0].user_id, t.bob_user_id());
assert_eq!(change1[0].changed_to, IdentityState::PinViolation);
assert_eq!(change1.len(), 1);
// And the change when Bob became verified
let change2 = next_change(&mut changes).await;
let change2 = assert_next_with_timeout!(stream);
assert_eq!(change2[0].user_id, t.bob_user_id());
assert_eq!(change2[0].changed_to, IdentityState::Verified);
assert_eq!(change2.len(), 1);
@@ -341,20 +340,20 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob becomes verified
t.verify_bob().await;
// Then we were notified about the initial state of the room
let change1 = next_change(&mut changes).await;
let change1 = assert_next_with_timeout!(stream);
assert_eq!(change1[0].user_id, t.bob_user_id());
assert_eq!(change1[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change1.len(), 1);
// And the change when Bob became verified
let change2 = next_change(&mut changes).await;
let change2 = assert_next_with_timeout!(stream);
assert_eq!(change2[0].user_id, t.bob_user_id());
assert_eq!(change2[0].changed_to, IdentityState::Verified);
assert_eq!(change2.len(), 1);
@@ -369,13 +368,14 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob joins the room
t.bob_joins().await;
// Then we were notified about it
let change = next_change(&mut pin!(changes)).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::PinViolation);
assert_eq!(change.len(), 1);
@@ -391,13 +391,14 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob joins the room
t.bob_joins().await;
// Then we were notified about it
let change = next_change(&mut pin!(changes)).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change.len(), 1);
@@ -412,7 +413,8 @@ mod tests {
t.verify_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob joins the room
t.bob_joins().await;
@@ -421,8 +423,7 @@ mod tests {
t.unpin_bob().await;
//// Then we were only notified about the unpin
let mut changes = pin!(changes);
let change = next_change(&mut changes).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change.len(), 1);
@@ -437,15 +438,15 @@ mod tests {
t.pin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob joins the room
t.bob_joins().await;
// Then there is no notification
tokio::time::sleep(Duration::from_millis(200)).await;
let change = changes.next().now_or_never();
let change = stream.next().now_or_never();
assert!(change.is_none());
}
@@ -458,20 +459,20 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// When Bob leaves the room
t.bob_leaves().await;
// Then we were notified about the initial state of the room
let change1 = next_change(&mut changes).await;
let change1 = assert_next_with_timeout!(stream);
assert_eq!(change1[0].user_id, t.bob_user_id());
assert_eq!(change1[0].changed_to, IdentityState::PinViolation);
assert_eq!(change1.len(), 1);
// And we were notified about the change when the user left
let change2 = next_change(&mut changes).await;
let change2 = assert_next_with_timeout!(stream);
// Note: the user left the room, but we see that as them "becoming pinned" i.e.
// "you no longer need to notify about this user".
assert_eq!(change2[0].user_id, t.bob_user_id());
@@ -488,24 +489,23 @@ mod tests {
t.unpin_bob().await;
// And we are listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let mut changes = pin!(changes);
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// NOTE: below we pull the changes out of the subscription after each action.
// This makes sure that the identity changes and membership changes are
// properly ordered. If we pull them out later, the identity changes get
// shifted forward because they rely on less-complex async stuff under
// the hood. Calling next_change ends up winding the async
// machinery sufficiently that the membership change and any subsequent events
// have fully completed.
// This makes sure that the identity changes and membership changes are properly
// ordered. If we pull them out later, the identity changes get shifted forward
// because they rely on less-complex async stuff under the hood. Calling
// next_change ends up winding the async machinery sufficiently that the
// membership change and any subsequent events have fully completed.
// When Bob joins the room ...
t.bob_joins().await;
let change1 = next_change(&mut changes).await;
let change1 = assert_next_with_timeout!(stream);
// ... becomes pinned ...
t.pin_bob().await;
let change2 = next_change(&mut changes).await;
let change2 = assert_next_with_timeout!(stream);
// ... leaves and joins again (ignored since they stay pinned) ...
t.bob_leaves().await;
@@ -513,11 +513,11 @@ mod tests {
// ... becomes unpinned ...
t.unpin_bob().await;
let change3 = next_change(&mut changes).await;
let change3 = assert_next_with_timeout!(stream);
// ... and leaves.
t.bob_leaves().await;
let change4 = next_change(&mut changes).await;
let change4 = assert_next_with_timeout!(stream);
assert_eq!(change1[0].user_id, t.bob_user_id());
assert_eq!(change2[0].user_id, t.bob_user_id());
@@ -542,10 +542,11 @@ mod tests {
t.unpin_bob().await;
// When we start listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// Then we were immediately notified about Bob being unpinned
let change = next_change(&mut pin!(changes)).await;
let change = assert_next_with_timeout!(stream);
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::PinViolation);
assert_eq!(change.len(), 1);
@@ -558,34 +559,26 @@ mod tests {
t.verify_bob().await;
// When we start listening for identity changes
let changes = t.subscribe_to_identity_status_changes().await;
let stream = t.subscribe_to_identity_status_changes().await;
pin_mut!(stream);
// (And we unpin so that something is available in the changes stream)
t.unpin_bob().await;
// Then we were only notified about the unpin, not being verified
let change = next_change(&mut pin!(changes)).await;
assert_eq!(change[0].user_id, t.bob_user_id());
assert_eq!(change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(change.len(), 1);
let next_change = assert_next_with_timeout!(stream);
assert_eq!(next_change[0].user_id, t.bob_user_id());
assert_eq!(next_change[0].changed_to, IdentityState::VerificationViolation);
assert_eq!(next_change.len(), 1);
}
// TODO: I (andyb) haven't figured out how to test room membership changes that
// affect our own user (they should not be shown). Specifically, I haven't
// figure out how to get out own user into a non-pinned state.
async fn next_change(
changes: &mut Pin<&mut Timeout<impl Stream<Item = Vec<IdentityStatusChange>>>>,
) -> Vec<IdentityStatusChange> {
changes
.next()
.await
.expect("Should not reach end of changes stream")
.expect("Should not time out waiting for a change")
}
mod test_setup {
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::time::{SystemTime, UNIX_EPOCH};
use futures_core::Stream;
use matrix_sdk_base::{
@@ -605,7 +598,6 @@ mod tests {
owned_user_id, OwnedUserId, TransactionId, UserId,
};
use serde_json::json;
use tokio_stream::{StreamExt as _, Timeout};
use wiremock::{
matchers::{header, method, path_regex},
Mock, MockServer, ResponseTemplate,
@@ -786,12 +778,11 @@ mod tests {
pub(super) async fn subscribe_to_identity_status_changes(
&self,
) -> Timeout<impl Stream<Item = Vec<IdentityStatusChange>>> {
) -> impl Stream<Item = Vec<IdentityStatusChange>> {
self.room
.subscribe_to_identity_status_changes()
.await
.expect("Should be able to subscribe")
.timeout(Duration::from_secs(5))
}
async fn init() -> (Client, OwnedUserId, SyncResponseBuilder) {
+6 -3
View File
@@ -2896,11 +2896,14 @@ impl Room {
///
/// This communicates to the homeserver that it should forget the room.
///
/// Only left rooms can be forgotten.
/// Only left or banned-from rooms can be forgotten.
pub async fn forget(&self) -> Result<()> {
let state = self.state();
if state != RoomState::Left {
return Err(Error::WrongRoomState(WrongRoomState::new("Left", state)));
match state {
RoomState::Joined | RoomState::Invited | RoomState::Knocked => {
return Err(Error::WrongRoomState(WrongRoomState::new("Left / Banned", state)));
}
RoomState::Left | RoomState::Banned => {}
}
let request = forget_room::v3::Request::new(self.inner.room_id().to_owned());
+7 -3
View File
@@ -126,8 +126,12 @@ impl RoomPreview {
}
}
/// Create a room preview from a known room we've joined.
pub(crate) async fn from_joined(room: &Room) -> Self {
/// Create a room preview from a known room.
///
/// Note this shouldn't be used with invited or knocked rooms, since the
/// local info may be out of date and no longer represent the latest room
/// state.
pub(crate) async fn from_known_room(room: &Room) -> Self {
let is_direct = room.is_direct().await.ok();
let display_name = room.display_name().await.ok().map(|name| name.to_string());
@@ -143,7 +147,7 @@ impl RoomPreview {
}
#[instrument(skip(client))]
pub(crate) async fn from_not_joined(
pub(crate) async fn from_remote_room(
client: &Client,
room_id: OwnedRoomId,
room_or_alias_id: &RoomOrAliasId,
+39 -8
View File
@@ -785,17 +785,18 @@ impl MatrixMockServer {
///
/// # Examples
///
/// ``` #
/// tokio_test::block_on(async {
/// use matrix_sdk_base::RoomMemberships;
/// use ruma::events::room::member::MembershipState;
/// use ruma::events::room::member::RoomMemberEventContent;
/// use ruma::user_id;
/// use matrix_sdk_test::event_factory::EventFactory;
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{event_id, room_id},
/// test_utils::mocks::MatrixMockServer,
/// };
/// use matrix_sdk_base::RoomMemberships;
/// use matrix_sdk_test::event_factory::EventFactory;
/// use ruma::{
/// events::room::member::{MembershipState, RoomMemberEventContent},
/// user_id,
/// };
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
/// let event_id = event_id!("$id");
@@ -811,7 +812,12 @@ impl MatrixMockServer {
/// .into_raw_timeline()
/// .cast();
///
/// mock_server.mock_get_members().ok(vec![alice_knock_event]).mock_once().mount().await;
/// mock_server
/// .mock_get_members()
/// .ok(vec![alice_knock_event])
/// .mock_once()
/// .mount()
/// .await;
/// let room = mock_server.sync_joined_room(&client, room_id).await;
///
/// let members = room.members(RoomMemberships::all()).await.unwrap();
@@ -918,6 +924,13 @@ impl MatrixMockServer {
let mock = Mock::given(method("GET")).and(path_regex(r"^/_matrix/client/versions"));
MockEndpoint { mock, server: &self.server, endpoint: VersionsEndpoint }
}
/// Creates a prebuilt mock for the room summary endpoint [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266).
pub fn mock_room_summary(&self) -> MockEndpoint<'_, RoomSummaryEndpoint> {
let mock = Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/unstable/im.nheko.summary/rooms/.*/summary"));
MockEndpoint { mock, server: &self.server, endpoint: RoomSummaryEndpoint }
}
}
/// Parameter to [`MatrixMockServer::sync_room`].
@@ -2280,3 +2293,21 @@ impl<'a> MockEndpoint<'a, VersionsEndpoint> {
MatrixMock { server: self.server, mock }
}
}
/// A prebuilt mock for the room summary endpoint.
pub struct RoomSummaryEndpoint;
impl<'a> MockEndpoint<'a, RoomSummaryEndpoint> {
/// Returns a successful response with some default data for the given room
/// id.
pub fn ok(self, room_id: &RoomId) -> MatrixMock<'a> {
let mock = self.mock.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"room_id": room_id,
"guest_can_join": true,
"num_joined_members": 1,
"world_readable": true,
"join_rule": "public",
})));
MatrixMock { server: self.server, mock }
}
}
+30 -2
View File
@@ -102,10 +102,38 @@ pub async fn logged_in_client_with_server() -> (Client, wiremock::MockServer) {
(client, server)
}
/// Asserts the next item in a `Stream` or `Subscriber` can be loaded in the
/// given timeout in the given timeout in milliseconds.
/// Asserts that the next item in a `Stream` is received within a given timeout.
///
/// This macro waits for the next item from an asynchronous `Stream` or, if no
/// item is received within the specified timeout, the macro panics.
///
/// # Parameters
///
/// - `$stream`: The `Stream` or `Subscriber` to poll for the next item.
/// - `$timeout_ms` (optional): The timeout in milliseconds to wait for the next
/// item. Defaults to 500ms if not provided.
///
/// # Example
///
/// ```rust
/// use futures_util::{stream, StreamExt};
/// use matrix_sdk::assert_next_with_timeout;
///
/// # async {
/// let mut stream = stream::iter(vec![1, 2, 3]);
/// let next_item = assert_next_with_timeout!(stream, 1000); // Waits up to 1000ms
/// assert_eq!(next_item, 1);
///
/// // The timeout can be omitted, in which case it defaults to 500 ms.
/// let next_item = assert_next_with_timeout!(stream); // Waits up to 500ms
/// assert_eq!(next_item, 2);
/// # };
/// ```
#[macro_export]
macro_rules! assert_next_with_timeout {
($stream:expr) => {
$crate::assert_next_with_timeout!($stream, 500)
};
($stream:expr, $timeout_ms:expr) => {{
// Needed for subscribers, as they won't use the StreamExt features
#[allow(unused_imports)]
@@ -76,11 +76,11 @@ async fn test_secret_store_create_default_key() {
let key_id = key_id.to_owned();
move |request: &wiremock::Request| {
let path_segments =
let mut path_segments =
request.url.path_segments().expect("The URL should be able to be a base");
let key_id_segment = path_segments
.last()
.next_back()
.expect("The path should have a key ID as the last segment")
.to_owned();
@@ -2,7 +2,7 @@ use std::time::Duration;
use assert_matches2::assert_matches;
use matrix_sdk::config::SyncSettings;
use matrix_sdk_base::RoomState;
use matrix_sdk_base::{RoomInfoNotableUpdateReasons, RoomState};
use matrix_sdk_test::{
async_test, test_json, GlobalAccountDataTestEvent, LeftRoomBuilder, SyncResponseBuilder,
DEFAULT_TEST_ROOM_ID,
@@ -77,6 +77,67 @@ async fn test_forget_non_direct_room() {
}
}
#[async_test]
async fn test_forget_banned_room() {
let (client, server) = logged_in_client_with_server().await;
let user_id = client.user_id().unwrap();
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
event_cache.enable_storage().unwrap();
Mock::given(method("POST"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/forget$"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY))
.named("forget")
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path(format!("/_matrix/client/r0/user/{user_id}/account_data/m.direct")))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EMPTY))
.named("set_mdirect")
.expect(0)
.mount(&server)
.await;
mock_sync(&server, &*test_json::LEAVE_SYNC, None).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let _response = client.sync_once(sync_settings).await.unwrap();
// Let the event cache process updates.
yield_now().await;
{
// There is some data in the cache store.
let event_cache_store = client.event_cache_store().lock().await.unwrap();
let room_data = event_cache_store.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
assert!(!room_data.is_empty());
}
// Make the room banned
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
let mut room_info = room.clone_info();
room_info.mark_as_banned();
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::MEMBERSHIP);
assert_eq!(room.state(), RoomState::Banned);
room.forget().await.unwrap();
assert!(client.get_room(&DEFAULT_TEST_ROOM_ID).is_none());
{
// Data in the event cache store has been removed.
let event_cache_store = client.event_cache_store().lock().await.unwrap();
let room_data = event_cache_store.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
assert!(room_data.is_empty());
}
}
#[async_test]
async fn test_forget_direct_room() {
let (client, server) = logged_in_client_with_server().await;
@@ -0,0 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
+2 -2
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-test-macros"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.10.0"
[lib]
proc-macro = true
@@ -24,4 +24,4 @@ syn = { version = "2.0.43", features = ["full", "extra-traits"] }
workspace = true
[package.metadata.release]
release = false
release = true
+9
View File
@@ -0,0 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.
<!-- next-header -->
## [Unreleased] - ReleaseDate
## [0.10.0] - 2025-02-04
+4 -4
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-test"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.7.0"
version = "0.10.0"
[lib]
test = false
@@ -19,8 +19,8 @@ doctest = false
as_variant = { workspace = true }
http = { workspace = true }
insta = { workspace = true }
matrix-sdk-common = { path = "../../crates/matrix-sdk-common" }
matrix-sdk-test-macros = { version = "0.7.0", path = "../matrix-sdk-test-macros" }
matrix-sdk-common = { version = "0.10.0", path = "../../crates/matrix-sdk-common" }
matrix-sdk-test-macros = { version = "0.10.0", path = "../matrix-sdk-test-macros" }
once_cell = { workspace = true }
# Enable the unstable feature for polls support.
# "client-api-s" enables need the "server" feature of ruma-client-api, which is needed to serialize Response objects to JSON.
@@ -43,4 +43,4 @@ wasm-bindgen-test = "0.3.33"
workspace = true
[package.metadata.release]
release = false
release = true
+4 -4
View File
@@ -90,7 +90,7 @@ fn check_prerequisites() {
fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
let sh = sh();
let cmd = cmd!(sh, "cargo release --no-publish --no-tag --no-push");
let cmd = cmd!(sh, "cargo release --workspace --no-publish --no-tag --no-push");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
let cmd = cmd.arg(version.as_str());
@@ -111,15 +111,15 @@ fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
fn publish(execute: bool) -> Result<()> {
let sh = sh();
let cmd = cmd!(sh, "cargo release tag");
let cmd = cmd!(sh, "cargo release tag --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
cmd.run()?;
let cmd = cmd!(sh, "cargo release publish");
let cmd = cmd!(sh, "cargo release publish --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
cmd.run()?;
let cmd = cmd!(sh, "cargo release push");
let cmd = cmd!(sh, "cargo release push --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
cmd.run()?;