Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c46e42201 | |||
| 0d4bc65e28 | |||
| 5e1bae02fe | |||
| 77a67de7df | |||
| f27eb4d1c8 | |||
| 05814c5559 | |||
| d5d9898fb4 | |||
| 3f71d9a379 | |||
| b077f45e78 | |||
| 648d527f2f | |||
| 8513547e92 | |||
| d18669e8d9 | |||
| a0426251a3 | |||
| 2739c5bf27 | |||
| 381f4d419f | |||
| 9ab5547065 | |||
| df3cb002a5 | |||
| 6ebd4295b9 | |||
| c5104d68fd | |||
| 0064839283 | |||
| 2727d72916 | |||
| 33a2cc3031 | |||
| 38097f90b2 | |||
| 47d08683a2 | |||
| 57919f5480 | |||
| b8949cfe26 | |||
| 8d27b0c811 | |||
| 3707d2fb81 | |||
| eaaa5e17a0 | |||
| e3958b754c | |||
| 78d9e1292f | |||
| d594b4dad7 | |||
| 3f40ad83a5 | |||
| 5049d1a3b6 |
+2
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+35
-34
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,8 +8,3 @@ dictionary Mentions {
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,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 }
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?)),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user