Merge branch 'main' into ganfra/kotlin_bindings

This commit is contained in:
ganfra
2022-11-25 10:19:19 +01:00
122 changed files with 4479 additions and 2847 deletions

View File

@@ -61,7 +61,7 @@ jobs:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
toolchain: stable
profile: minimal
override: true
@@ -243,6 +243,9 @@ jobs:
profile: minimal
override: true
- name: Install aarch64-apple-ios target
run: rustup target install aarch64-apple-ios
- name: Load cache
uses: Swatinem/rust-cache@v1
@@ -252,15 +255,12 @@ jobs:
path: target/debug/xtask
key: xtask-macos-${{ hashFiles('Cargo.toml', 'xtask/**') }}
- name: Install Uniffi
uses: actions-rs/cargo@v1
with:
command: install
args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev ${{ env.UNIFFI_REV }}
- name: Build library & bindings
run: target/debug/xtask swift build-library
- name: Run XCTests
working-directory: bindings/apple
run: swift test
- name: Build Framework
run: cargo xtask swift build-framework --only-target=aarch64-apple-ios

View File

@@ -370,7 +370,7 @@ jobs:
uses: actions/checkout@v3
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.12.8
uses: crate-ci/typos@v1.13.0
clippy:
name: Run clippy

1036
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,12 @@ members = [
default-members = ["benchmarks", "crates/*"]
resolver = "2"
[workspace.package]
rust-version = "1.65"
[workspace.dependencies]
ruma = { version = "0.7.4", features = ["client-api-c"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "ed100afddb5fb30f1ccf368d7e712a3a483e63bf", features = ["client-api-c"] }
ruma-common = { git = "https://github.com/ruma/ruma", rev = "ed100afddb5fb30f1ccf368d7e712a3a483e63bf" }
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "779e955f21a70e4aba43a7408f1841dcdf728b32" }
uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "779e955f21a70e4aba43a7408f1841dcdf728b32" }
@@ -25,13 +29,14 @@ uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "779e955f21
vodozemac = "0.3.0"
zeroize = "1.3.0"
# Default release profile, select with `--release`
[profile.release]
lto = true
# Default development profile; default for most Cargo commands, otherwise
# selected with `--debug`
[profile.dev]
# Copied from rust-analyzer. Saves a lot of disk space and hopefully
# compilation time / mem usage too, at the expense of potentially having to
# change this setting here when you want to use a debugger.
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
debug = 0
[profile.dev.package]
@@ -39,3 +44,18 @@ debug = 0
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
quote = { opt-level = 2 }
sha2 = { opt-level = 2 }
# Custom profile with full debugging info, use `--profile debug` to select
[profile.dbg]
inherits = "dev"
debug = 2
# Custom profile for use in (debug) builds of the binding crates, use
# `--profile release_dbg` to select
[profile.reldbg]
inherits = "dev"
incremental = false
# Compile all non-workspace crate in the dependency tree with optimizations
[profile.reldbg.package."*"]
opt-level = 3

View File

@@ -26,7 +26,7 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
## Minimum Supported Rust Version (MSRV)
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.64`
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.65`.
## Status

View File

@@ -3,7 +3,7 @@ name = "benchmarks"
description = "Matrix SDK benchmarks"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.56"
rust-version = { workspace = true }
version = "1.0.0"
publish = false

View File

@@ -2,89 +2,6 @@
set -eEu
cd "$(dirname "$0")"
cd ../..
# Path to the repo root
SRC_ROOT=../..
TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/generated"
mkdir -p ${GENERATED_DIR}
REL_FLAG="--release"
REL_TYPE_DIR="release"
# Build static libs for all the different architectures
# iOS
echo -e "Building for iOS [1/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios"
# MacOS
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-darwin"
echo -e "\nBuilding for macOS (Intel) [3/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-darwin"
# iOS Simulator
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-sim"
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios"
echo -e "\nCreating XCFramework"
# Lipo together the libraries for the same platform
# MacOS
lipo -create \
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"
# iOS Simulator
lipo -create \
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
# Generate uniffi files
# Architecture for the .a file argument doesn't matter, since the API is the same on all
uniffi-bindgen generate \
--language swift \
--lib-file "${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
--out-dir ${GENERATED_DIR} \
"${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl"
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
SWIFT_DIR="${GENERATED_DIR}/swift"
mkdir -p ${SWIFT_DIR}
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" \
-headers ${HEADERS_DIR} \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
-headers ${HEADERS_DIR} \
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
# Cleanup
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"; fi
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
cargo xtask swift build-framework --release $*

View File

@@ -19,19 +19,6 @@ else
echo "Running debug build"
fi
echo "Active architecture ${ACTIVE_ARCH}"
# Path to the repo root
SRC_ROOT=../..
TARGET_DIR="${SRC_ROOT}/target"
GENERATED_DIR="${SRC_ROOT}/bindings/apple/generated"
mkdir -p ${GENERATED_DIR}
REL_FLAG=""
REL_TYPE_DIR="debug"
# iOS Simulator arm64
if [ "$ACTIVE_ARCH" = "arm64" ]; then
TARGET="aarch64-apple-ios-sim"
@@ -39,52 +26,6 @@ if [ "$ACTIVE_ARCH" = "arm64" ]; then
else
TARGET="x86_64-apple-ios"
fi
echo "Active architecture ${ACTIVE_ARCH}"
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "$TARGET"
lipo -create \
"${TARGET_DIR}/$TARGET/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
# Generate uniffi files
uniffi-bindgen generate \
--language swift \
--lib-file "${TARGET_DIR}/$TARGET/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
--out-dir ${GENERATED_DIR} \
"${SRC_ROOT}/bindings/matrix-sdk-ffi/src/api.udl"
# Move them to the right place
HEADERS_DIR=${GENERATED_DIR}/headers
mkdir -p ${HEADERS_DIR}
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
# Rename and move modulemap to the right place
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
SWIFT_DIR="${GENERATED_DIR}/swift"
mkdir -p ${SWIFT_DIR}
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
# Build the xcframework
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
xcodebuild -create-xcframework \
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
-headers ${HEADERS_DIR} \
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
# Cleanup
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
if [ "$IS_CI" = false ] ; then
echo "Preparing matrix-rust-components-swift"
# Debug -> Copy generated files over to ../../../matrix-rust-components-swift
rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/"
rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK"
fi
cargo xtask swift build-framework --profile=reldbg --sim-only-target=${TARGET} $*

View File

@@ -3,7 +3,7 @@ name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
rust-version = "1.62"
rust-version = { workspace = true }
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"

View File

@@ -27,9 +27,10 @@ pub use error::{
};
use js_int::UInt;
pub use logger::{set_logger, Logger};
pub use machine::{KeyRequestPair, OlmMachine};
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::VerificationState;
use matrix_sdk_crypto::{
backups::SignatureState,
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
EncryptionSettings as RustEncryptionSettings, LocalTrust,
};

View File

@@ -10,9 +10,15 @@ use base64::{decode_config, encode, STANDARD_NO_PAD};
use js_int::UInt;
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
use matrix_sdk_crypto::{
backups::MegolmV1BackupKey as RustBackupKey, decrypt_room_key_export, encrypt_room_key_export,
matrix_sdk_qrcode::QrVerificationData, olm::ExportedRoomKey, store::RecoveryKey, LocalTrust,
OlmMachine as InnerMachine, UserIdentities, Verification as RustVerification,
backups::{
MegolmV1BackupKey as RustBackupKey, SignatureState,
SignatureVerification as RustSignatureCheckResult,
},
decrypt_room_key_export, encrypt_room_key_export,
matrix_sdk_qrcode::QrVerificationData,
olm::ExportedRoomKey,
store::RecoveryKey,
LocalTrust, OlmMachine as InnerMachine, UserIdentities, Verification as RustVerification,
};
use ruma::{
api::{
@@ -25,7 +31,7 @@ use ruma::{
upload_signatures::v3::Response as SignatureUploadResponse,
},
message::send_message_event::v3::Response as RoomMessageResponse,
sync::sync_events::v3::{DeviceLists as RumaDeviceLists, ToDevice},
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,
@@ -66,6 +72,46 @@ pub struct KeyRequestPair {
pub key_request: Request,
}
/// The result of a signature verification of a signed JSON object.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureVerification {
/// The result of the signature verification using the public key of our own
/// device.
pub device_signature: SignatureState,
/// The result of the signature verification using the public key of our own
/// user identity.
pub user_identity_signature: SignatureState,
/// The result of the signature verification using public keys of other
/// devices we own.
pub other_devices_signatures: HashMap<String, SignatureState>,
/// Is the signed JSON object trusted.
///
/// This flag tells us if the result has a valid signature from any of the
/// following:
///
/// * Our own device
/// * Our own user identity, provided the identity is trusted as well
/// * Any of our own devices, provided the device is trusted as well
pub trusted: bool,
}
impl From<RustSignatureCheckResult> for SignatureVerification {
fn from(r: RustSignatureCheckResult) -> Self {
let trusted = r.trusted();
Self {
device_signature: r.device_signature,
user_identity_signature: r.user_identity_signature,
other_devices_signatures: r
.other_signatures
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
trusted,
}
}
}
#[uniffi::export]
impl OlmMachine {
/// Get the user ID of the owner of this `OlmMachine`.
@@ -797,9 +843,7 @@ impl OlmMachine {
/// * `user_id` - The ID of the user for which we would like to fetch the
/// verification requests.
pub fn get_verification_requests(&self, user_id: &str) -> Vec<VerificationRequest> {
let user_id = if let Ok(user_id) = UserId::parse(user_id) {
user_id
} else {
let Ok(user_id) = UserId::parse(user_id) else {
return vec![];
};
@@ -1463,12 +1507,15 @@ impl OlmMachine {
/// }
/// }
/// ```
pub fn verify_backup(&self, backup_info: &str) -> Result<bool, CryptoStoreError> {
pub fn verify_backup(
&self,
backup_info: &str,
) -> Result<SignatureVerification, CryptoStoreError> {
let backup_info = serde_json::from_str(backup_info)?;
Ok(self
.runtime
.block_on(self.inner.backup_machine().verify_backup(backup_info, false))?
.trusted())
.into())
}
}

View File

@@ -431,7 +431,7 @@ interface OlmMachine {
BackupKeys? get_backup_keys();
boolean backup_enabled();
[Throws=CryptoStoreError]
boolean verify_backup([ByRef] string auth_data);
SignatureVerification verify_backup([ByRef] string auth_data);
};
dictionary PassphraseInfo {
@@ -439,6 +439,20 @@ dictionary PassphraseInfo {
i32 private_key_iterations;
};
dictionary SignatureVerification {
SignatureState device_signature;
SignatureState user_identity_signature;
record<DOMString, SignatureState> other_devices_signatures;
boolean trusted;
};
enum SignatureState {
"Missing",
"Invalid",
"ValidButNotTrusted",
"ValidAndTrusted",
};
dictionary MegolmV1BackupKey {
string public_key;
record<DOMString, record<DOMString, string>> signatures;

View File

@@ -8,7 +8,7 @@ keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.1.0-alpha.0"
publish = false
@@ -39,7 +39,7 @@ matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common"
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb", features = ["experimental-nodejs"] }
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2676", "unstable-msc2677"] }
ruma = { workspace = true, features = ["js", "rand", "unstable-msc2677"] }
vodozemac = { workspace = true, features = ["js"] }
wasm-bindgen = "0.2.83"
wasm-bindgen-futures = "0.4.33"

View File

@@ -41,13 +41,10 @@ impl Attachment {
/// destroyed. It is still possible to get a JSON-encoded backup
/// by calling `EncryptedAttachment.mediaEncryptionInfo`.
pub fn decrypt(attachment: &mut EncryptedAttachment) -> Result<Vec<u8>, JsError> {
let media_encryption_info = match attachment.media_encryption_info.take() {
Some(media_encryption_info) => media_encryption_info,
None => {
return Err(JsError::new(
"The media encryption info are absent from the given encrypted attachment",
))
}
let Some(media_encryption_info) = attachment.media_encryption_info.take() else {
return Err(JsError::new(
"The media encryption info are absent from the given encrypted attachment",
));
};
let encrypted_data: &[u8] = attachment.encrypted_data.as_slice();

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk-crypto-nodejs"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.6.0"
[package.metadata.docs.rs]
@@ -26,7 +26,7 @@ tracing = ["dep:tracing-subscriber"]
matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto", features = ["js"] }
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
matrix-sdk-sled = { version = "0.2.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] }
ruma = { workspace = true, features = ["rand", "unstable-msc2676", "unstable-msc2677"] }
ruma = { workspace = true, features = ["rand", "unstable-msc2677"] }
napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] }
napi-derive = "2.9.1"
serde_json = "1.0.79"

View File

@@ -50,14 +50,11 @@ impl Attachment {
/// by calling `EncryptedAttachment.mediaEncryptionInfo`.
#[napi]
pub fn decrypt(attachment: &mut EncryptedAttachment) -> napi::Result<Uint8Array> {
let media_encryption_info = match attachment.media_encryption_info.take() {
Some(media_encryption_info) => media_encryption_info,
None => {
return Err(napi::Error::from_reason(
"The media encryption info are absent from the given encrypted attachment"
.to_owned(),
))
}
let Some(media_encryption_info) = attachment.media_encryption_info.take() else {
return Err(napi::Error::from_reason(
"The media encryption info are absent from the given encrypted attachment"
.to_owned(),
));
};
let encrypted_data: &[u8] = attachment.encrypted_data.deref();

View File

@@ -6,16 +6,12 @@ homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
license = "Apache-2.0"
readme = "README.md"
rust-version = "1.56"
rust-version = { workspace = true }
repository = "https://github.com/matrix-org/matrix-rust-sdk"
[lib]
crate-type = ["cdylib", "staticlib"]
[features]
default = ["experimental-room-preview"] # the whole crate is still very experimental, so this is fine
experimental-room-preview = ["matrix-sdk/experimental-room-preview"]
[build-dependencies]
uniffi_build = { workspace = true, features = ["builtin-bindgen"] }
@@ -27,15 +23,23 @@ futures-signals = { version = "0.3.30", default-features = false }
futures-util = { version = "0.3.17", default-features = false }
# FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce sliding-sync being exposed here..
# see https://github.com/matrix-org/matrix-rust-sdk/issues/1014
#matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "experimental-timeline", "markdown", "sliding-sync", "socks"], version = "0.6.0" }
matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["anyhow", "sled", "e2e-encryption", "experimental-timeline", "markdown", "sliding-sync", "socks", "rustls-tls"], version = "0.6.0" }
once_cell = "1.10.0"
sanitize-filename-reader-friendly = "2.2.1"
serde_json = { version = "1" }
thiserror = "1.0.30"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-stream = "0.1.8"
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uniffi = { workspace = true }
uniffi_macros = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies]
tracing = { version = "0.1.29", default-features = false, features = ["log"] }
android_logger = "0.11"
log-panics = { version = "2", features = ["with-backtrace"]}
matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["anyhow", "experimental-timeline", "e2e-encryption", "sled", "markdown", "sliding-sync", "socks", "rustls-tls"], version = "0.6.0" }
[target.'cfg(not(target_os = "android"))'.dependencies]
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "experimental-timeline", "markdown", "sliding-sync", "socks"], version = "0.6.0" }

View File

@@ -203,6 +203,9 @@ dictionary Session {
interface Room {
[Throws=ClientError]
string display_name();
[Throws=ClientError]
boolean is_encrypted();
[Throws=ClientError]
string? member_avatar_url(string user_id);

View File

@@ -104,9 +104,8 @@ impl AuthenticationService {
initial_device_name: Option<String>,
device_id: Option<String>,
) -> Result<Arc<Client>, AuthenticationError> {
let client = match &*self.client.read().unwrap() {
Some(client) => client.clone(),
None => return Err(AuthenticationError::ClientMissing),
let Some(client) = self.client.read().unwrap().clone() else {
return Err(AuthenticationError::ClientMissing);
};
// Login and ask the server for the full user ID as this could be different from
@@ -143,9 +142,8 @@ impl AuthenticationService {
token: String,
device_id: String,
) -> Result<Arc<Client>, AuthenticationError> {
let client = match &*self.client.read().unwrap() {
Some(client) => client.clone(),
None => return Err(AuthenticationError::ClientMissing),
let Some(client) = self.client.read().unwrap().clone() else {
return Err(AuthenticationError::ClientMissing);
};
// Restore the client and ask the server for the full user ID as this

View File

@@ -17,7 +17,7 @@ use matrix_sdk::{
serde::Raw,
TransactionId, UInt,
},
Client as MatrixClient, Error, LoopCtrl, RumaApiError,
Client as MatrixClient, Error, LoopCtrl,
};
use super::{
@@ -297,15 +297,13 @@ impl Client {
/// Process a sync error and return loop control accordingly
pub(crate) fn process_sync_error(&self, sync_error: Error) -> LoopCtrl {
if let Some(RumaApiError::ClientApi(error)) = sync_error.as_ruma_api_error() {
if let ErrorKind::UnknownToken { soft_logout } = error.kind {
self.state.write().unwrap().is_soft_logout = soft_logout;
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_update_restore_token();
delegate.did_receive_auth_error(soft_logout);
}
return LoopCtrl::Break;
if let Some(ErrorKind::UnknownToken { soft_logout }) = sync_error.client_api_error_kind() {
self.state.write().unwrap().is_soft_logout = *soft_logout;
if let Some(delegate) = &*self.delegate.read().unwrap() {
delegate.did_update_restore_token();
delegate.did_receive_auth_error(*soft_logout);
}
return LoopCtrl::Break;
}
tracing::warn!("Ignoring sync error: {:?}", sync_error);

View File

@@ -20,6 +20,8 @@ macro_rules! unwrap_or_clone_arc_into_variant {
};
}
mod platform;
pub mod authentication_service;
pub mod client;
pub mod client_builder;
@@ -30,13 +32,10 @@ pub mod sliding_sync;
pub mod timeline;
mod uniffi_api;
use std::io;
use client::Client;
use client_builder::ClientBuilder;
use once_cell::sync::Lazy;
use tokio::runtime::Runtime;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
pub use uniffi_api::*;
pub static RUNTIME: Lazy<Runtime> =
@@ -72,13 +71,7 @@ impl From<anyhow::Error> for ClientError {
}
}
#[uniffi::export]
fn setup_tracing(configuration: String) {
tracing_subscriber::registry()
.with(EnvFilter::new(configuration))
.with(fmt::layer().with_ansi(false).with_writer(io::stderr))
.init();
}
pub use platform::*;
mod uniffi_types {
pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource};
@@ -94,11 +87,11 @@ mod uniffi_types {
SlidingSyncView, SlidingSyncViewBuilder, StoppableSpawn, UnreadNotificationsCount,
},
timeline::{
EmoteMessageContent, EncryptedMessage, EventTimelineItem, FormattedBody, ImageInfo,
ImageMessageContent, InsertAtData, Message, MessageFormat, MessageType,
NoticeMessageContent, Reaction, TextMessageContent, ThumbnailInfo, TimelineChange,
TimelineDiff, TimelineItem, TimelineItemContent, TimelineKey, UpdateAtData, VideoInfo,
VideoMessageContent, VirtualTimelineItem,
EmoteMessageContent, EncryptedMessage, EventTimelineItem, FileInfo, FileMessageContent,
FormattedBody, ImageInfo, ImageMessageContent, InsertAtData, Message, MessageFormat,
MessageType, NoticeMessageContent, Reaction, TextMessageContent, ThumbnailInfo,
TimelineChange, TimelineDiff, TimelineItem, TimelineItemContent, TimelineKey,
UpdateAtData, VideoInfo, VideoMessageContent, VirtualTimelineItem,
},
};
}

View File

@@ -0,0 +1,57 @@
#[cfg(target_os = "android")]
use android as platform_impl;
#[cfg(target_os = "ios")]
use ios as platform_impl;
#[cfg(not(any(target_os = "ios", target_os = "android")))]
use other as platform_impl;
#[cfg(target_os = "android")]
mod android {
use android_logger::{Config, FilterBuilder};
use tracing::log::Level;
pub fn setup_tracing(filter: String) {
std::env::set_var("RUST_BACKTRACE", "1");
log_panics::init();
let log_config = Config::default()
.with_min_level(Level::Trace)
.with_tag("matrix-rust-sdk")
.with_filter(FilterBuilder::new().parse(&filter).build());
android_logger::init_once(log_config);
}
}
#[cfg(target_os = "ios")]
mod ios {
use std::io;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
pub fn setup_tracing(configuration: String) {
tracing_subscriber::registry()
.with(EnvFilter::new(configuration))
.with(fmt::layer().with_ansi(false).with_writer(io::stderr))
.init();
}
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
mod other {
use std::io;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
pub fn setup_tracing(configuration: String) {
tracing_subscriber::registry()
.with(EnvFilter::new(configuration))
.with(fmt::layer().with_ansi(true).with_writer(io::stderr))
.init();
}
}
#[uniffi::export]
pub fn setup_tracing(filter: String) {
platform_impl::setup_tracing(filter)
}

View File

@@ -11,7 +11,10 @@ use matrix_sdk::{
Room as SdkRoom,
},
ruma::{
events::room::message::{Relation, Replacement, RoomMessageEvent, RoomMessageEventContent},
events::room::message::{
ForwardThread, MessageType, Relation, Replacement, RoomMessageEvent,
RoomMessageEventContent,
},
EventId, UserId,
},
};
@@ -58,10 +61,6 @@ impl Room {
self.room.is_public()
}
pub fn is_encrypted(&self) -> bool {
self.room.is_encrypted()
}
pub fn is_space(&self) -> bool {
self.room.is_space()
}
@@ -97,6 +96,14 @@ impl Room {
RUNTIME.block_on(async move { Ok(r.display_name().await?.to_string()) })
}
pub fn is_encrypted(&self) -> Result<bool> {
let room = self.room.clone();
RUNTIME.block_on(async move {
let is_encrypted = room.is_encrypted().await?;
Ok(is_encrypted)
})
}
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>> {
let room = self.room.clone();
let user_id = user_id;
@@ -191,8 +198,8 @@ impl Room {
let original_message =
event_content.as_original().context("Couldn't retrieve original message.")?;
let reply_content =
RoomMessageEventContent::text_markdown(msg).make_reply_to(original_message);
let reply_content = RoomMessageEventContent::text_markdown(msg)
.make_reply_to(original_message, ForwardThread::Yes);
timeline.send(reply_content.into(), txn_id.as_deref().map(Into::into)).await?;
@@ -233,7 +240,7 @@ impl Room {
let replacement = Replacement::new(
event_id.to_owned(),
Box::new(RoomMessageEventContent::text_markdown(new_msg.to_owned())),
MessageType::text_markdown(new_msg.to_owned()),
);
let mut edited_content = RoomMessageEventContent::text_markdown(new_msg);

View File

@@ -173,11 +173,10 @@ impl SessionVerificationController {
}
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
if let Some(verification) = &*self.verification_request.read().unwrap() {
return verification.flow_id() == transaction_id;
match &*self.verification_request.read().unwrap() {
Some(verification) => verification.flow_id() == transaction_id,
None => false,
}
false
}
async fn start_sas_verification(&self) {

View File

@@ -5,10 +5,6 @@ use futures_signals::{
signal_vec::{SignalVecExt, VecDiff},
};
use futures_util::{pin_mut, StreamExt};
#[cfg(feature = "experimental-room-preview")]
use matrix_sdk::ruma::events::{
room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
};
use matrix_sdk::ruma::{
api::client::sync::sync_events::{
v4::RoomSubscription as RumaRoomSubscription,
@@ -23,9 +19,7 @@ pub use matrix_sdk::{
use tokio::task::JoinHandle;
use super::{Client, Room, RUNTIME};
use crate::helpers::unwrap_or_clone_arc;
#[cfg(feature = "experimental-room-preview")]
use crate::EventTimelineItem;
use crate::{helpers::unwrap_or_clone_arc, EventTimelineItem};
pub struct StoppableSpawn {
handle: Arc<RwLock<Option<JoinHandle<()>>>>,
@@ -126,25 +120,14 @@ impl SlidingSyncRoom {
}
}
#[cfg(feature = "experimental-room-preview")]
#[uniffi::export]
impl SlidingSyncRoom {
#[allow(clippy::significant_drop_in_scrutinee)]
pub fn latest_room_message(&self) -> Option<Arc<EventTimelineItem>> {
let messages = self.inner.timeline();
// room is having the latest events at the end,
let lock = messages.lock_ref();
for ev in lock.iter().rev() {
if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(o),
))) = ev.event.deserialize()
{
let inner =
matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.event.clone());
return Some(Arc::new(EventTimelineItem(inner)));
}
}
None
RUNTIME.block_on(async {
let item = self.inner.timeline().await.latest()?.as_event()?.to_owned();
Some(Arc::new(EventTimelineItem(item)))
})
}
}

View File

@@ -277,6 +277,13 @@ impl Message {
info: c.info.as_deref().map(Into::into),
},
}),
MTy::File(c) => Some(MessageType::File {
content: FileMessageContent {
body: c.body.clone(),
source: Arc::new(c.source.clone()),
info: c.info.as_deref().map(Into::into),
},
}),
MTy::Notice(c) => Some(MessageType::Notice {
content: NoticeMessageContent {
body: c.body.clone(),
@@ -312,6 +319,7 @@ pub enum MessageType {
Emote { content: EmoteMessageContent },
Image { content: ImageMessageContent },
Video { content: VideoMessageContent },
File { content: FileMessageContent },
Notice { content: NoticeMessageContent },
Text { content: TextMessageContent },
}
@@ -336,6 +344,13 @@ pub struct VideoMessageContent {
pub info: Option<VideoInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileMessageContent {
pub body: String,
pub source: Arc<MediaSource>,
pub info: Option<FileInfo>,
}
#[derive(Clone, uniffi::Record)]
pub struct ImageInfo {
pub height: Option<u64>,
@@ -359,6 +374,14 @@ pub struct VideoInfo {
pub blurhash: Option<String>,
}
#[derive(Clone, uniffi::Record)]
pub struct FileInfo {
pub mimetype: Option<String>,
pub size: Option<u64>,
pub thumbnail_info: Option<ThumbnailInfo>,
pub thumbnail_source: Option<Arc<MediaSource>>,
}
#[derive(Clone, uniffi::Record)]
pub struct ThumbnailInfo {
pub height: Option<u64>,
@@ -446,6 +469,24 @@ impl From<&matrix_sdk::ruma::events::room::message::VideoInfo> for VideoInfo {
}
}
impl From<&matrix_sdk::ruma::events::room::message::FileInfo> for FileInfo {
fn from(info: &matrix_sdk::ruma::events::room::message::FileInfo) -> Self {
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
height: info.height.map(Into::into),
width: info.width.map(Into::into),
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
});
Self {
mimetype: info.mimetype.clone(),
size: info.size.map(Into::into),
thumbnail_info,
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
}
}
}
#[derive(Clone, uniffi::Enum)]
pub enum EncryptedMessage {
OlmV1Curve25519AesSha2 {

View File

@@ -30,13 +30,13 @@ def timeout(flow):
# hold a tuple containing a function that may or may not create a failure and
# the probability weight at which rate this failure should be triggered.
#
# The method should return an http.HTTPResponse if it should modify the
# The method should return an http.Response if it should modify the
# response or None if the response should be passed as is.
FAILURES = {
"Success": (lambda x: None, 50),
"Gateway error":
(lambda _: http.HTTPResponse.make(500, b"Gateway error"), 20),
"Limit exeeded": (lambda _: http.HTTPResponse.make(
(lambda _: http.Response.make(500, b"Gateway error"), 20),
"Limit exeeded": (lambda _: http.Response.make(
429,
json.dumps({
"errcode": "M_LIMIT_EXCEEDED",

View File

@@ -8,7 +8,7 @@ keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"]
license = "Apache-2.0"
name = "matrix-sdk-appservice"
version = "0.1.0"
rust-version = "1.62"
rust-version = { workspace = true }
publish = false
[features]

View File

@@ -20,8 +20,7 @@
//! - [x] ship with functionality to configure your webserver crate or simply
//! run the webserver for you
//! - [x] receive and validate requests from the homeserver correctly
//! - [x] allow calling the homeserver with proper virtual user identity
//! assertion
//! - [x] allow calling the homeserver with proper user identity assertion
//! - [x] have consistent room state by leveraging matrix-sdk's state store
//! - [ ] provide E2EE support by leveraging matrix-sdk's crypto store
//!
@@ -34,7 +33,7 @@
//!
//! The crate relies on the appservice registration being always in sync with
//! the actual registration used by the homeserver. That's because it's required
//! for the access tokens and because membership states for virtual users are
//! for the access tokens and because membership states for appservice users are
//! determined based on the registered namespaces.
//!
//! # Quickstart
@@ -60,7 +59,7 @@
//! )
//! .build()
//! .await?;
//! appservice.virtual_user(None).await?.add_event_handler(
//! appservice.user(None).await?.add_event_handler(
//! |_ev: SyncRoomMemberEvent| async {
//! // do stuff
//! },
@@ -109,12 +108,12 @@ use tracing::{debug, info, warn};
mod error;
pub mod event_handler;
pub mod registration;
pub mod virtual_user;
pub mod user;
mod webserver;
pub use registration::AppServiceRegistration;
use registration::NamespaceCache;
pub use virtual_user::VirtualUserBuilder;
pub use user::UserBuilder;
pub use webserver::AppServiceRouter;
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -172,13 +171,13 @@ impl AppServiceBuilder {
}
}
/// Set the client builder to use for the virtual user.
/// Set the client builder to use for the appservice user.
pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self {
self.client_builder = Some(client_builder);
self
}
/// Set the default `[RequestConfig]` to use for virtual users.
/// Set the default `[RequestConfig]` to use for appservice users.
pub fn default_request_config(mut self, default_request_config: RequestConfig) -> Self {
self.default_request_config = Some(default_request_config);
self
@@ -186,10 +185,10 @@ impl AppServiceBuilder {
/// Build the AppService.
///
/// This will also construct a [`virtual_user()`][AppService::virtual_user]
/// for the `sender_localpart` of the given registration. This virtual
/// This will also construct an appservice [`user()`][AppService::user]
/// for the `sender_localpart` of the given registration. This
/// user can be used to register an event handler for all incoming
/// events. Other virtual users only receive events if they're known to
/// events. Other appservice users only receive events if they're known to
/// be a member of a room.
pub async fn build(self) -> Result<AppService> {
let homeserver_url = self.homeserver_url;
@@ -212,12 +211,12 @@ impl AppServiceBuilder {
};
if let Some(client_builder) = self.client_builder {
appservice
.virtual_user_builder(&sender_localpart)
.user_builder(&sender_localpart)
.client_builder(client_builder)
.build()
.await?;
} else {
appservice.virtual_user_builder(&sender_localpart).build().await?;
appservice.user_builder(&sender_localpart).build().await?;
}
Ok(appservice)
}
@@ -233,7 +232,7 @@ impl AppService {
AppServiceBuilder::new(homeserver_url, server_name, registration)
}
/// Create a virtual user client.
/// Create an appservice user client.
///
/// Will create and return a client that's configured to [assert the
/// identity] on outgoing homeserver requests that need authentication.
@@ -242,50 +241,50 @@ impl AppService {
/// based on the `localpart`. The cached client can be retrieved by calling
/// this method again.
///
/// Note that if you want to do actions like joining rooms with a virtual
/// Note that if you want to do actions like joining rooms with a
/// user it needs to be registered first.
/// [`register_virtual_user()`][Self::register_virtual_user] can be used
/// [`register_user()`][Self::register_user] can be used
/// for that purpose.
///
/// # Arguments
///
/// * `localpart` - Used for constructing the virtual user accordingly. If
/// `None` is given it uses the `sender_localpart` from the registration.
/// * `localpart` - Used for constructing the user accordingly. If `None` is
/// given it uses the `sender_localpart` from the registration.
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
/// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn virtual_user(&self, localpart: Option<&str>) -> Result<Client> {
pub async fn user(&self, localpart: Option<&str>) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
let builder = match self.default_request_config {
Some(config) => self
.virtual_user_builder(localpart)
.user_builder(localpart)
.client_builder(Client::builder().request_config(config)),
None => self.virtual_user_builder(localpart),
None => self.user_builder(localpart),
};
builder.build().await
}
/// Same as [`virtual_user()`][Self::virtual_user] but with
/// Same as [`user()`][Self::user] but with
/// the ability to pass in a [`ClientBuilder`].
///
/// Since this method is a singleton follow-up calls with different
/// [`ClientBuilder`]s will be ignored.
pub async fn virtual_user_with_client_builder(
pub async fn user_with_client_builder(
&self,
localpart: Option<&str>,
builder: ClientBuilder,
) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
self.virtual_user_builder(localpart).client_builder(builder).build().await
self.user_builder(localpart).client_builder(builder).build().await
}
/// Create a new virtual user builder for the given `localpart`.
pub fn virtual_user_builder<'a>(&'a self, localpart: &'a str) -> VirtualUserBuilder<'a> {
VirtualUserBuilder::new(self, localpart)
/// Create a new appservice user builder for the given `localpart`.
pub fn user_builder<'a>(&'a self, localpart: &'a str) -> UserBuilder<'a> {
UserBuilder::new(self, localpart)
}
/// Get the map containing all constructed virtual user clients.
pub fn virtual_users(&self) -> Arc<DashMap<Localpart, Client>> {
/// Get the map containing all constructed appservice user clients.
pub fn users(&self) -> Arc<DashMap<Localpart, Client>> {
self.clients.clone()
}
@@ -337,8 +336,8 @@ impl AppService {
*self.event_handler.rooms.lock().await = Some(handler);
}
/// Register a virtual user by sending a [`register::v3::Request`] to the
/// homeserver.
/// Register an appservice user by sending a [`register::v3::Request`] to
/// the homeserver.
///
/// # Arguments
///
@@ -348,7 +347,7 @@ impl AppService {
/// # Returns
/// This function may return a UIAA response, which should be checked for
/// with [`Error::as_uiaa_response()`].
pub async fn register_virtual_user<'a>(
pub async fn register_user<'a>(
&self,
localpart: &'a str,
device_id: Option<&'a DeviceId>,
@@ -362,7 +361,7 @@ impl AppService {
device_id,
});
let client = self.virtual_user(None).await?;
let client = self.user(None).await?;
client.register(request).await?;
self.set_user_registered(localpart).await?;
@@ -371,7 +370,7 @@ impl AppService {
/// Add the given localpart to the database of registered localparts.
async fn set_user_registered(&self, localpart: impl AsRef<str>) -> Result<()> {
let client = self.virtual_user(None).await?;
let client = self.user(None).await?;
client
.store()
.set_custom_value(
@@ -384,7 +383,7 @@ impl AppService {
/// Get whether a localpart is listed in the database as registered.
async fn is_user_registered(&self, localpart: impl AsRef<str>) -> Result<bool> {
let client = self.virtual_user(None).await?;
let client = self.user(None).await?;
let key = [USER_KEY, localpart.as_ref().as_bytes()].concat();
let store = client.store().get_custom_value(&key).await?;
let registered =
@@ -423,21 +422,21 @@ impl AppService {
}
/// Receive an incoming [transaction], pushing the contained events to
/// active virtual clients.
/// active clients.
///
/// [transaction]: https://spec.matrix.org/v1.2/application-service-api/#put_matrixappv1transactionstxnid
async fn receive_transaction(
&self,
transaction: push_events::v1::IncomingRequest,
) -> Result<()> {
let sender_localpart_client = self.virtual_user(None).await?;
let sender_localpart_client = self.user(None).await?;
// Find membership events affecting members in our namespace, and update
// membership accordingly
for event in transaction.events.iter() {
let event = match event.deserialize() {
Ok(AnyTimelineEvent::State(AnyStateEvent::RoomMember(event))) => event,
_ => continue,
for raw_event in transaction.events.iter() {
let res = raw_event.deserialize();
let Ok(AnyTimelineEvent::State(AnyStateEvent::RoomMember(event))) = res else {
continue;
};
if !self.user_id_is_in_namespace(event.state_key()) {
continue;
@@ -461,18 +460,18 @@ impl AppService {
// Spawn a task for each client that constructs and pushes a sync event
let mut tasks: Vec<JoinHandle<_>> = Vec::new();
let transaction = Arc::new(transaction);
for virtual_user_client in self.clients.iter() {
for user_client in self.clients.iter() {
let client = sender_localpart_client.clone();
let virtual_user_client = virtual_user_client.clone();
let user_client = user_client.clone();
let transaction = transaction.clone();
let sender_localpart = self.registration.sender_localpart.clone();
let task = tokio::spawn(async move {
let virtual_user_localpart = match virtual_user_client.user_id() {
Some(user_id) => user_id.localpart(),
let Some(user_id) = user_client.user_id() else {
// The client is not logged in, skipping
None => return Ok(()),
return Ok(());
};
let user_localpart = user_id.localpart();
let mut response = sync_events::v3::Response::new(transaction.txn_id.to_string());
// Clients expect events to be grouped per room, where the
@@ -483,22 +482,16 @@ impl AppService {
// We special-case the `sender_localpart` user which receives all events and
// by falling back to a membership of "join" if it's unknown.
for raw_event in &transaction.events {
let room_id = match raw_event.deserialize_as::<EventRoomId>()?.room_id {
Some(room_id) => room_id,
None => {
warn!("Transaction contained event with no ID");
continue;
}
let Some(room_id) = raw_event.deserialize_as::<EventRoomId>()?.room_id else {
warn!("Transaction contained event with no ID");
continue;
};
let key =
&[USER_MEMBER, room_id.as_bytes(), b".", virtual_user_localpart.as_bytes()]
.concat();
let key = &[USER_MEMBER, room_id.as_bytes(), b".", user_localpart.as_bytes()]
.concat();
let membership = match client.store().get_custom_value(key).await? {
Some(value) => String::from_utf8(value).ok().map(MembershipState::from),
// Assume the `sender_localpart` user is in every known room
None if virtual_user_localpart == sender_localpart => {
Some(MembershipState::Join)
}
None if user_localpart == sender_localpart => Some(MembershipState::Join),
None => None,
};
@@ -518,10 +511,10 @@ impl AppService {
response.rooms.invite.entry(room_id).or_default();
}
Some(unknown) => debug!("Unknown membership type: {unknown}"),
None => debug!("Assuming {virtual_user_localpart} is not in {room_id}"),
None => debug!("Assuming {user_localpart} is not in {room_id}"),
}
}
virtual_user_client.receive_transaction(&transaction.txn_id, response).await?;
user_client.receive_transaction(&transaction.txn_id, response).await?;
Ok::<_, Error>(())
});
@@ -617,7 +610,7 @@ mod tests {
}
#[async_test]
async fn test_register_virtual_user() -> Result<()> {
async fn test_register_user() -> Result<()> {
let server = MockServer::start().await;
let appservice = appservice(Some(server.uri()), None).await?;
@@ -640,7 +633,7 @@ mod tests {
.mount(&server)
.await;
appservice.register_virtual_user(localpart, None).await?;
appservice.register_user(localpart, None).await?;
Ok(())
}
@@ -683,7 +676,7 @@ mod tests {
#[allow(clippy::mutex_atomic)]
let on_state_member = Arc::new(Mutex::new(false));
appservice.virtual_user(None).await?.add_event_handler({
appservice.user(None).await?.add_event_handler({
let on_state_member = on_state_member.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
*on_state_member.lock().unwrap() = true;
@@ -835,7 +828,7 @@ mod tests {
#[allow(clippy::mutex_atomic)]
let on_state_member = Arc::new(Mutex::new(false));
appservice.virtual_user(None).await?.add_event_handler({
appservice.user(None).await?.add_event_handler({
let on_state_member = on_state_member.clone();
move |_ev: OriginalSyncRoomMemberEvent| {
*on_state_member.lock().unwrap() = true;
@@ -904,7 +897,7 @@ mod tests {
.unwrap();
let members = appservice
.virtual_user(None)
.user(None)
.await?
.get_room(room_id)
.expect("Expected room to be available")
@@ -995,8 +988,8 @@ mod tests {
];
let appservice = appservice(None, None).await?;
let alice = appservice.virtual_user(Some("_appservice_alice")).await?;
let bob = appservice.virtual_user(Some("_appservice_bob")).await?;
let alice = appservice.user(Some("_appservice_alice")).await?;
let bob = appservice.user(Some("_appservice_bob")).await?;
appservice
.receive_transaction(push_events::v1::IncomingRequest::new("dontcare".into(), json))
.await?;

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Virtual users.
//! AppService users.
use matrix_sdk::{config::RequestConfig, Client, ClientBuildError, ClientBuilder, Session};
use ruma::{
@@ -23,9 +23,9 @@ use tracing::warn;
use crate::{AppService, Result};
/// Builder for a virtual user
/// Builder for an appservice user
#[derive(Debug)]
pub struct VirtualUserBuilder<'a> {
pub struct UserBuilder<'a> {
appservice: &'a AppService,
localpart: &'a str,
device_id: Option<OwnedDeviceId>,
@@ -34,11 +34,11 @@ pub struct VirtualUserBuilder<'a> {
restored_session: Option<Session>,
}
impl<'a> VirtualUserBuilder<'a> {
/// Create a new virtual user builder
impl<'a> UserBuilder<'a> {
/// Create a new appservice user builder
/// # Arguments
///
/// * `localpart` - The localpart of the virtual user
/// * `localpart` - The localpart of the appservice user
pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self {
Self {
appservice,
@@ -50,21 +50,21 @@ impl<'a> VirtualUserBuilder<'a> {
}
}
/// Set the device ID of the virtual user
/// Set the device ID of the appservice user
pub fn device_id(mut self, device_id: Option<OwnedDeviceId>) -> Self {
self.device_id = device_id;
self
}
/// Sets the client builder to use for the virtual user
/// Sets the client builder to use for the appservice user
pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self {
self.client_builder = client_builder;
self
}
/// Log in as the virtual user
/// Log in as the appservice user
///
/// In some cases it is necessary to log in as the virtual user, such as to
/// In some cases it is necessary to log in as the user, such as to
/// upload device keys
pub fn login(mut self) -> Self {
self.log_in = true;
@@ -74,14 +74,14 @@ impl<'a> VirtualUserBuilder<'a> {
/// Restore a persisted session
///
/// This is primarily useful if you enable
/// [`VirtualUserBuilder::login()`] and want to restore a session
/// [`UserBuilder::login()`] and want to restore a session
/// from a previous run.
pub fn restored_session(mut self, session: Session) -> Self {
self.restored_session = Some(session);
self
}
/// Build the virtual user
/// Build the appservice user
///
/// # Errors
/// This function returns an error if an invalid localpart is provided.
@@ -94,7 +94,7 @@ impl<'a> VirtualUserBuilder<'a> {
if !(self.appservice.user_id_is_in_namespace(&user_id)
|| self.localpart == self.appservice.registration.sender_localpart)
{
warn!("Virtual client id '{user_id}' is not in the namespace")
warn!("Client id '{user_id}' is not in the namespace")
}
let mut builder = self.client_builder;

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.6.1"
[package.metadata.docs.rs]

View File

@@ -22,17 +22,10 @@ use std::{
use std::{ops::Deref, sync::Arc};
use futures_signals::signal::ReadOnlyMutable;
use matrix_sdk_common::{
deserialized_responses::{
AmbiguityChanges, JoinedRoom, LeftRoom, MembersResponse, Rooms, SyncResponse,
SyncTimelineEvent, Timeline,
},
instant::Instant,
};
use matrix_sdk_common::{instant::Instant, locks::RwLock};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
store::{CryptoStore, MemoryStore as MemoryCryptoStore},
EncryptionSettings, OlmError, OlmMachine, ToDeviceRequest,
store::CryptoStore, EncryptionSettings, OlmError, OlmMachine, ToDeviceRequest,
};
#[cfg(feature = "e2e-encryption")]
use once_cell::sync::OnceCell;
@@ -62,12 +55,14 @@ use tracing::{debug, info, trace, warn};
#[cfg(feature = "e2e-encryption")]
use crate::error::Error;
use crate::{
deserialized_responses::{AmbiguityChanges, MembersResponse, SyncTimelineEvent},
error::Result,
rooms::{Room, RoomInfo, RoomType},
store::{
ambiguity_map::AmbiguityCache, Result as StoreResult, StateChanges, StateStoreExt, Store,
StoreConfig,
},
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse, Timeline},
Session, SessionMeta, SessionTokens, StateStore,
};
@@ -112,15 +107,10 @@ impl BaseClient {
/// * `config` - An optional session if the user already has one from a
/// previous login call.
pub fn with_store_config(config: StoreConfig) -> Self {
let store = config.state_store.map(Store::new).unwrap_or_else(Store::open_memory_store);
#[cfg(feature = "e2e-encryption")]
let crypto_store =
config.crypto_store.unwrap_or_else(|| Arc::new(MemoryCryptoStore::default()));
BaseClient {
store,
store: Store::new(config.state_store),
#[cfg(feature = "e2e-encryption")]
crypto_store,
crypto_store: config.crypto_store,
#[cfg(feature = "e2e-encryption")]
olm_machine: Default::default(),
}
@@ -166,6 +156,12 @@ impl BaseClient {
self.store.get_rooms()
}
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
/// yet in the store
pub async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room {
self.store.get_or_create_room(room_id, room_type).await
}
/// Get all the rooms this client knows about.
pub fn get_stripped_rooms(&self) -> Vec<Room> {
self.store.get_stripped_rooms()
@@ -579,6 +575,53 @@ impl BaseClient {
}
}
/// User has joined a room.
///
/// Update the internal and cached state accordingly. Return the final Room.
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
let room = self.store.get_or_create_room(room_id, RoomType::Joined).await;
if room.room_type() != RoomType::Joined {
let _sync_lock = self.sync_lock().read().await;
let mut room_info = room.clone_info();
room_info.mark_as_joined();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.store.save_changes(&changes).await?; // Update the store
room.update_summary(room_info); // Update the cached room handle
}
Ok(room)
}
/// User has left a room.
///
/// Update the internal and cached state accordingly. Return the final Room.
pub async fn room_left(&self, room_id: &RoomId) -> Result<Room> {
let room = self.store.get_or_create_room(room_id, RoomType::Left).await;
if room.room_type() != RoomType::Left {
let _sync_lock = self.sync_lock().read().await;
let mut room_info = room.clone_info();
room_info.mark_as_left();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.store.save_changes(&changes).await?; // Update the store
room.update_summary(room_info); // Update the cached room handle
}
Ok(room)
}
/// Get access to the store's sync lock.
pub fn sync_lock(&self) -> &RwLock<()> {
self.store.sync_lock()
}
/// Receive a response from a sync call.
///
/// # Arguments
@@ -605,7 +648,7 @@ impl BaseClient {
// that case we already received this response and there's nothing to
// do.
if self.store.sync_token.read().await.as_ref() == Some(&next_batch) {
return Ok(SyncResponse::new(next_batch));
return Ok(SyncResponse::default());
}
let now = Instant::now();
@@ -637,6 +680,7 @@ impl BaseClient {
room_info.update_summary(&new_info.summary);
room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref());
room_info.mark_state_fully_synced();
let mut user_ids = self
.handle_state(
@@ -717,6 +761,7 @@ impl BaseClient {
let room = self.store.get_or_create_room(&room_id, RoomType::Left).await;
let mut room_info = room.clone_info();
room_info.mark_as_left();
room_info.mark_state_partially_synced();
let mut user_ids = self
.handle_state(
@@ -757,6 +802,7 @@ impl BaseClient {
if let Some(r) = self.store.get_room(&room_id) {
let mut room_info = r.clone_info();
room_info.mark_as_invited();
room_info.mark_state_fully_synced();
changes.add_room(room_info);
}
@@ -784,14 +830,15 @@ impl BaseClient {
changes.ambiguity_maps = ambiguity_cache.cache;
let sync_lock = self.sync_lock().write().await;
self.store.save_changes(&changes).await?;
*self.store.sync_token.write().await = Some(next_batch.clone());
self.apply_changes(&changes).await;
drop(sync_lock);
info!("Processed a sync response in {:?}", now.elapsed());
let response = SyncResponse {
next_batch,
rooms: new_rooms,
presence,
account_data: account_data.events,
@@ -862,31 +909,38 @@ impl BaseClient {
for member in &members {
let member: SyncRoomMemberEvent = member.clone().into();
if self.store.get_member_event(room_id, member.state_key()).await?.is_none() {
#[cfg(feature = "e2e-encryption")]
match member.membership() {
MembershipState::Join | MembershipState::Invite => {
user_ids.insert(member.state_key().to_owned());
}
_ => (),
// TODO: All the actions in this loop used to be done only when the membership
// event was not in the store before. This was changed with the new room API,
// because e.g. leaving a room makes members events outdated and they need to be
// fetched by `get_members`. Therefore, they need to be overwritten here, even
// if they exist.
// However, this makes a new problem occur where setting the member events here
// potentially races with the sync.
// See <https://github.com/matrix-org/matrix-rust-sdk/issues/1205>.
#[cfg(feature = "e2e-encryption")]
match member.membership() {
MembershipState::Join | MembershipState::Invite => {
user_ids.insert(member.state_key().to_owned());
}
_ => (),
}
ambiguity_cache.handle_event(&changes, room_id, &member).await?;
if member.state_key() == member.sender() {
changes
.profiles
.entry(room_id.to_owned())
.or_default()
.insert(member.sender().to_owned(), member.borrow().into());
}
ambiguity_cache.handle_event(&changes, room_id, &member).await?;
if member.state_key() == member.sender() {
changes
.members
.profiles
.entry(room_id.to_owned())
.or_default()
.insert(member.state_key().to_owned(), member);
.insert(member.sender().to_owned(), member.borrow().into());
}
changes
.members
.entry(room_id.to_owned())
.or_default()
.insert(member.state_key().to_owned(), member);
}
#[cfg(feature = "e2e-encryption")]

View File

@@ -0,0 +1,119 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! SDK-specific variations of response types from Ruma.
use std::collections::BTreeMap;
pub use matrix_sdk_common::deserialized_responses::*;
use ruma::{
events::room::member::{
MembershipState, RoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent,
SyncRoomMemberEvent,
},
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
};
use serde::{Deserialize, Serialize};
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChange {
/// Is the member that is contained in the state key of the `m.room.member`
/// event itself ambiguous because of the event.
pub member_ambiguous: bool,
/// Has another user been disambiguated because of this event.
pub disambiguated_member: Option<OwnedUserId>,
/// Has another user become ambiguous because of this event.
pub ambiguated_member: Option<OwnedUserId>,
}
/// Collection of ambiguioty changes that room member events trigger.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChanges {
/// A map from room id to a map of an event id to the `AmbiguityChange` that
/// the event with the given id caused.
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
/// A deserialized response for the rooms members API call.
///
/// [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MembersResponse {
/// The list of members events.
pub chunk: Vec<RoomMemberEvent>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
}
/// Wrapper around both MemberEvent-Types
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum MemberEvent {
/// A member event from a room in joined or left state.
Sync(SyncRoomMemberEvent),
/// A member event from a room in invited state.
Stripped(StrippedRoomMemberEvent),
}
impl MemberEvent {
/// The inner Content of the wrapped Event
pub fn original_content(&self) -> Option<&RoomMemberEventContent> {
match self {
MemberEvent::Sync(e) => e.as_original().map(|e| &e.content),
MemberEvent::Stripped(e) => Some(&e.content),
}
}
/// The sender of this event.
pub fn sender(&self) -> &UserId {
match self {
MemberEvent::Sync(e) => e.sender(),
MemberEvent::Stripped(e) => &e.sender,
}
}
/// The ID of this event.
pub fn event_id(&self) -> Option<&EventId> {
match self {
MemberEvent::Sync(e) => Some(e.event_id()),
MemberEvent::Stripped(_) => None,
}
}
/// The Server Timestamp of this event.
pub fn origin_server_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
match self {
MemberEvent::Sync(e) => Some(e.origin_server_ts()),
MemberEvent::Stripped(_) => None,
}
}
/// The membership state of the user
pub fn membership(&self) -> &MembershipState {
match self {
MemberEvent::Sync(e) => e.membership(),
MemberEvent::Stripped(e) => &e.content.membership,
}
}
/// The user id associated to this member event
pub fn user_id(&self) -> &UserId {
match self {
MemberEvent::Sync(e) => e.state_key(),
MemberEvent::Stripped(e) => &e.state_key,
}
}
}

View File

@@ -25,6 +25,7 @@ pub use crate::{
};
mod client;
pub mod deserialized_responses;
mod error;
pub mod media;
mod rooms;
@@ -32,6 +33,7 @@ mod session;
#[cfg(feature = "sliding-sync")]
mod sliding_sync;
pub mod store;
pub mod sync;
mod utils;
pub use client::BaseClient;

View File

@@ -40,8 +40,8 @@ use tracing::debug;
use super::{BaseRoomInfo, DisplayName, RoomMember};
use crate::{
deserialized_responses::UnreadNotificationsCount,
store::{Result as StoreResult, StateStore, StateStoreExt},
sync::UnreadNotificationsCount,
MinimalStateEvent,
};
@@ -129,7 +129,7 @@ impl Room {
self.inner.read().unwrap().notification_counts
}
/// Check if the room has it's members fully synced.
/// Check if the room has its members fully synced.
///
/// Members might be missing if lazy member loading was enabled for the
/// sync.
@@ -139,6 +139,27 @@ impl Room {
self.inner.read().unwrap().members_synced
}
/// Check if the room states have been synced
///
/// States might be missing if we have only seen the room_id of this Room
/// so far, for example as the response for a `create_room` request without
/// being synced yet.
///
/// Returns true if the state is fully synced, false otherwise.
pub fn is_state_fully_synced(&self) -> bool {
self.inner.read().unwrap().sync_info == SyncInfo::FullySynced
}
/// Check if the room has its encryption event synced.
///
/// The encryption event can be missing when the room hasn't appeared in
/// sync yet.
///
/// Returns true if the encryption state is synced, false otherwise.
pub fn is_encryption_state_synced(&self) -> bool {
self.inner.read().unwrap().encryption_state_synced
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
@@ -405,12 +426,9 @@ impl Room {
/// return a `RoomMember` that can be in a joined, invited, left, banned
/// state.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
let member_event =
if let Some(m) = self.store.get_member_event(self.room_id(), user_id).await? {
m
} else {
return Ok(None);
};
let Some(member_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
return Ok(None);
};
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
@@ -499,11 +517,52 @@ pub struct RoomInfo {
pub(crate) members_synced: bool,
/// The prev batch of this room we received during the last sync.
pub(crate) last_prev_batch: Option<String>,
/// How much we know about this room.
#[serde(default = "SyncInfo::complete")] // see fn docs for why we use this default
pub(crate) sync_info: SyncInfo,
/// Whether or not the encryption info was been synced.
#[serde(default = "encryption_state_default")] // see fn docs for why we use this default
pub(crate) encryption_state_synced: bool,
/// Base room info which holds some basic event contents important for the
/// room state.
pub(crate) base_info: BaseRoomInfo,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) enum SyncInfo {
/// We only know the room exists and whether it is in invite / joined / left
/// state.
///
/// This is the case when we have a limited sync or only seen the room
/// because of a request we've done, like a room creation event.
NoState,
/// Some states have been synced, but they might have been filtered or is
/// stale, as it is from a room we've left.
PartiallySynced,
/// We have all the latest state events.
FullySynced,
}
impl SyncInfo {
// The sync_info field introduced a new field in the database schema, but to
// avoid a database migration, we let serde assume that if the room is in
// the database, yet the field isn't, we have synced it before this field
// was introduced - which was a a full sync.
fn complete() -> Self {
SyncInfo::FullySynced
}
}
// The encryption_state_synced field introduced a new field in the database
// schema, but to avoid a database migration, we let serde assume that if
// the room is in the database, yet the field isn't, we have synced it
// before this field was introduced - which was a a full sync.
fn encryption_state_default() -> bool {
true
}
impl RoomInfo {
#[doc(hidden)] // used by store tests, otherwise it would be pub(crate)
pub fn new(room_id: &RoomId, room_type: RoomType) -> Self {
@@ -514,35 +573,62 @@ impl RoomInfo {
summary: Default::default(),
members_synced: false,
last_prev_batch: None,
sync_info: SyncInfo::NoState,
encryption_state_synced: false,
base_info: BaseRoomInfo::new(),
}
}
/// Mark this Room as joined
/// Mark this Room as joined.
pub fn mark_as_joined(&mut self) {
self.room_type = RoomType::Joined;
}
/// Mark this Room as left
/// Mark this Room as left.
pub fn mark_as_left(&mut self) {
self.room_type = RoomType::Left;
}
/// Mark this Room as invited
/// Mark this Room as invited.
pub fn mark_as_invited(&mut self) {
self.room_type = RoomType::Invited;
}
/// Mark this Room as having all the members synced
/// Mark this Room as having all the members synced.
pub fn mark_members_synced(&mut self) {
self.members_synced = true;
}
/// Mark this Room still missing member information
/// Mark this Room still missing member information.
pub fn mark_members_missing(&mut self) {
self.members_synced = false;
}
/// Mark this Room still missing some state information.
pub fn mark_state_partially_synced(&mut self) {
self.sync_info = SyncInfo::PartiallySynced;
}
/// Mark this Room still having all state synced.
pub fn mark_state_fully_synced(&mut self) {
self.sync_info = SyncInfo::FullySynced;
}
/// Mark this Room still having no state synced.
pub fn mark_state_not_synced(&mut self) {
self.sync_info = SyncInfo::NoState;
}
/// Mark this Room as having the encryption state synced.
pub fn mark_encryption_state_synced(&mut self) {
self.encryption_state_synced = true;
}
/// Mark this Room still missing encryption state information.
pub fn mark_encryption_state_missing(&mut self) {
self.encryption_state_synced = false;
}
/// Set the `prev_batch`-token.
/// Returns whether the token has differed and thus has been upgraded:
/// `false` means no update was applied as the were the same
@@ -555,11 +641,16 @@ impl RoomInfo {
}
}
/// Whether this is an encrypted Room
/// Returns whether this is an encrypted Room.
pub fn is_encrypted(&self) -> bool {
self.base_info.encryption.is_some()
}
/// Set the encryption event content in this room.
pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
self.base_info.encryption = event;
}
/// Handle the given state event.
///
/// Returns true if the event modified the info, false otherwise.

View File

@@ -1,18 +1,17 @@
#[cfg(feature = "e2e-encryption")]
use std::ops::Deref;
use matrix_sdk_common::deserialized_responses::{
AmbiguityChanges, JoinedRoom, Rooms, SyncResponse,
};
use ruma::api::client::sync::sync_events::{v3, v4};
#[cfg(feature = "e2e-encryption")]
use ruma::UserId;
use super::BaseClient;
use crate::{
deserialized_responses::AmbiguityChanges,
error::Result,
rooms::RoomType,
store::{ambiguity_map::AmbiguityCache, StateChanges},
sync::{JoinedRoom, Rooms, SyncResponse},
};
impl BaseClient {
@@ -90,10 +89,12 @@ impl BaseClient {
let invite_states = &room_data.invite_state;
let room = store.get_or_create_stripped_room(&room_id).await;
let mut room_info = room.clone_info();
room_info.mark_state_partially_synced();
if let Some(r) = store.get_room(&room_id) {
let mut room_info = r.clone_info();
room_info.mark_as_invited(); // FIXME: this might not be accurate
room_info.mark_state_partially_synced();
changes.add_room(room_info);
}
@@ -107,6 +108,7 @@ impl BaseClient {
let room = store.get_or_create_room(&room_id, RoomType::Joined).await;
let mut room_info = room.clone_info();
room_info.mark_as_joined(); // FIXME: this might not be accurate
room_info.mark_state_partially_synced();
// FIXME not yet supported by sliding sync.
// room_info.update_summary(&room_data.summary);
@@ -230,7 +232,6 @@ impl BaseClient {
tracing::debug!("applied changes");
Ok(SyncResponse {
next_batch: "test".into(),
rooms: new_rooms,
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
notifications: changes.notifications,

View File

@@ -17,7 +17,6 @@ use std::{
sync::Arc,
};
use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent};
use ruma::{
events::room::member::{MembershipState, SyncRoomMemberEvent},
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
@@ -25,7 +24,10 @@ use ruma::{
use tracing::trace;
use super::{Result, StateChanges};
use crate::StateStore;
use crate::{
deserialized_responses::{AmbiguityChange, MemberEvent},
StateStore,
};
#[derive(Debug)]
pub(crate) struct AmbiguityCache {

View File

@@ -703,7 +703,7 @@ macro_rules! statestore_integration_tests {
assert!(matches!(
store.get_member_event(room_id, user_id).await.unwrap(),
Some(matrix_sdk_common::deserialized_responses::MemberEvent::Sync(_))
Some($crate::deserialized_responses::MemberEvent::Sync(_))
));
assert_eq!(store.get_room_infos().await.unwrap().len(), 1);
assert_eq!(store.get_stripped_room_infos().await.unwrap().len(), 0);
@@ -718,7 +718,7 @@ macro_rules! statestore_integration_tests {
assert!(matches!(
store.get_member_event(room_id, user_id).await.unwrap(),
Some(matrix_sdk_common::deserialized_responses::MemberEvent::Stripped(_))
Some($crate::deserialized_responses::MemberEvent::Stripped(_))
));
assert_eq!(store.get_room_infos().await.unwrap().len(), 0);
assert_eq!(store.get_stripped_room_infos().await.unwrap().len(), 1);

View File

@@ -505,16 +505,15 @@ pub(crate) struct Store {
pub(super) sync_token: Arc<RwLock<Option<String>>>,
rooms: Arc<DashMap<OwnedRoomId, Room>>,
stripped_rooms: Arc<DashMap<OwnedRoomId, Room>>,
/// A lock to synchronize access to the store, such that data by the sync is
/// never overwritten. The sync processing is supposed to use write access,
/// such that only it is currently accessing the store overall. Other things
/// might acquire read access, such that access to different rooms can be
/// parallelized.
sync_lock: Arc<RwLock<()>>,
}
impl Store {
/// Create a new Store with the default `MemoryStore`
pub fn open_memory_store() -> Self {
let inner = Arc::new(MemoryStore::new());
Self::new(inner)
}
/// Create a new store, wrapping the given `StateStore`
pub fn new(inner: Arc<dyn StateStore>) -> Self {
Self {
@@ -524,9 +523,15 @@ impl Store {
sync_token: Default::default(),
rooms: Default::default(),
stripped_rooms: Default::default(),
sync_lock: Default::default(),
}
}
/// Get access to the syncing lock.
pub fn sync_lock(&self) -> &RwLock<()> {
&self.sync_lock
}
/// Restore the access to the Store from the given `Session`, overwrites any
/// previously existing access to the Store.
pub async fn restore_session(&self, session: Session) -> Result<()> {
@@ -810,11 +815,11 @@ impl StateChanges {
///
/// let store_config = StoreConfig::new();
/// ```
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct StoreConfig {
#[cfg(feature = "e2e-encryption")]
pub(crate) crypto_store: Option<Arc<dyn CryptoStore>>,
pub(crate) state_store: Option<Arc<dyn StateStore>>,
pub(crate) crypto_store: Arc<dyn CryptoStore>,
pub(crate) state_store: Arc<dyn StateStore>,
}
#[cfg(not(tarpaulin_include))]
@@ -828,7 +833,11 @@ impl StoreConfig {
/// Create a new default `StoreConfig`.
#[must_use]
pub fn new() -> Self {
Default::default()
Self {
#[cfg(feature = "e2e-encryption")]
crypto_store: Arc::new(matrix_sdk_crypto::store::MemoryStore::new()),
state_store: Arc::new(MemoryStore::new()),
}
}
/// Set a custom implementation of a `CryptoStore`.
@@ -836,13 +845,19 @@ impl StoreConfig {
/// The crypto store must be opened before being set.
#[cfg(feature = "e2e-encryption")]
pub fn crypto_store(mut self, store: impl IntoCryptoStore) -> Self {
self.crypto_store = Some(store.into_crypto_store());
self.crypto_store = store.into_crypto_store();
self
}
/// Set a custom implementation of a `StateStore`.
pub fn state_store(mut self, store: impl IntoStateStore) -> Self {
self.state_store = Some(store.into_state_store());
self.state_store = store.into_state_store();
self
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,164 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The SDK's representation of the result of a `/sync` request.
use std::collections::BTreeMap;
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
use ruma::{
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{
v3::{Ephemeral, InvitedRoom, Presence, RoomAccountData, State},
DeviceLists, UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
},
events::{AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyToDeviceEvent},
serde::Raw,
DeviceKeyAlgorithm, OwnedRoomId,
};
use serde::{Deserialize, Serialize};
use crate::deserialized_responses::AmbiguityChanges;
/// Internal representation of a `/sync` response.
///
/// This type is intended to be applicable regardless of the endpoint used for
/// syncing.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SyncResponse {
/// Updates to rooms.
pub rooms: Rooms,
/// Updates to the presence status of other users.
pub presence: Presence,
/// The global private data created by this user.
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
/// Messages sent directly between devices.
pub to_device_events: Vec<Raw<AnyToDeviceEvent>>,
/// Information on E2E device updates.
///
/// Only present on an incremental sync.
pub device_lists: DeviceLists,
/// For each key algorithm, the number of unclaimed one-time keys
/// currently held on the server for a device.
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, u64>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
/// New notifications per room.
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
/// Updates to rooms in a [`SyncResponse`].
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Rooms {
/// The rooms that the user has left or been banned from.
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
/// The rooms that the user has joined.
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
/// The rooms that the user has been invited to.
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
}
/// Updates to joined rooms.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct JoinedRoom {
/// Counts of unread notifications for this room.
pub unread_notifications: UnreadNotificationsCount,
/// The timeline of messages and state changes in the room.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
/// The ephemeral events in the room that aren't recorded in the timeline or
/// state of the room. e.g. typing.
pub ephemeral: Ephemeral,
}
impl JoinedRoom {
pub(crate) fn new(
timeline: Timeline,
state: State,
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
ephemeral: Ephemeral,
unread_notifications: UnreadNotificationsCount,
) -> Self {
Self { unread_notifications, timeline, state, account_data, ephemeral }
}
}
/// Counts of unread notifications for a room.
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
pub struct UnreadNotificationsCount {
/// The number of unread notifications for this room with the highlight flag
/// set.
pub highlight_count: u64,
/// The total number of unread notifications for this room.
pub notification_count: u64,
}
impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
fn from(notifications: RumaUnreadNotificationsCount) -> Self {
Self {
highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
}
}
}
/// Updates to left rooms.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LeftRoom {
/// The timeline of messages and state changes in the room up to the point
/// when the user left.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: RoomAccountData,
}
impl LeftRoom {
pub(crate) fn new(timeline: Timeline, state: State, account_data: RoomAccountData) -> Self {
Self { timeline, state, account_data }
}
}
/// Events in the room.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Timeline {
/// True if the number of events returned was limited by the `limit` on the
/// filter.
pub limited: bool,
/// A token that can be supplied to to the `from` parameter of the
/// `/rooms/{roomId}/messages` endpoint.
pub prev_batch: Option<String>,
/// A list of events.
pub events: Vec<SyncTimelineEvent>,
}
impl Timeline {
pub(crate) fn new(limited: bool, prev_batch: Option<String>) -> Self {
Self { limited, prev_batch, ..Default::default() }
}
}

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.6.0"
[package.metadata.docs.rs]

View File

@@ -1,48 +1,12 @@
use std::{borrow::Borrow, collections::BTreeMap};
use std::collections::BTreeMap;
use ruma::{
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{
v3::{Ephemeral, InvitedRoom, Presence, RoomAccountData, State},
DeviceLists, UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
},
events::{
room::member::{
MembershipState, RoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent,
SyncRoomMemberEvent,
},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncTimelineEvent, AnyTimelineEvent,
AnyToDeviceEvent,
},
events::{AnySyncTimelineEvent, AnyTimelineEvent},
serde::Raw,
DeviceKeyAlgorithm, EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId,
OwnedRoomId, OwnedUserId, UserId,
DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedUserId,
};
use serde::{Deserialize, Serialize};
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChange {
/// Is the member that is contained in the state key of the `m.room.member`
/// event itself ambiguous because of the event.
pub member_ambiguous: bool,
/// Has another user been disambiguated because of this event.
pub disambiguated_member: Option<OwnedUserId>,
/// Has another user become ambiguous because of this event.
pub ambiguated_member: Option<OwnedUserId>,
}
/// Collection of ambiguioty changes that room member events trigger.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChanges {
/// A map from room id to a map of an event id to the `AmbiguityChange` that
/// the event with the given id caused.
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
/// The verification state of the device that sent an event to us.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum VerificationState {
@@ -121,38 +85,6 @@ impl From<TimelineEvent> for SyncTimelineEvent {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SyncResponse {
/// The batch token to supply in the `since` param of the next `/sync`
/// request.
pub next_batch: String,
/// Updates to rooms.
pub rooms: Rooms,
/// Updates to the presence status of other users.
pub presence: Presence,
/// The global private data created by this user.
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
/// Messages sent directly between devices.
pub to_device_events: Vec<Raw<AnyToDeviceEvent>>,
/// Information on E2E device updates.
///
/// Only present on an incremental sync.
pub device_lists: DeviceLists,
/// For each key algorithm, the number of unclaimed one-time keys
/// currently held on the server for a device.
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, u64>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
/// New notifications per room.
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
impl SyncResponse {
pub fn new(next_batch: String) -> Self {
Self { next_batch, ..Default::default() }
}
}
#[derive(Clone, Debug)]
pub struct TimelineEvent {
/// The actual event.
@@ -162,206 +94,6 @@ pub struct TimelineEvent {
pub encryption_info: Option<EncryptionInfo>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Rooms {
/// The rooms that the user has left or been banned from.
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
/// The rooms that the user has joined.
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
/// The rooms that the user has been invited to.
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
}
/// Updates to joined rooms.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct JoinedRoom {
/// Counts of unread notifications for this room.
pub unread_notifications: UnreadNotificationsCount,
/// The timeline of messages and state changes in the room.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
/// The ephemeral events in the room that aren't recorded in the timeline or
/// state of the room. e.g. typing.
pub ephemeral: Ephemeral,
}
impl JoinedRoom {
pub fn new(
timeline: Timeline,
state: State,
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
ephemeral: Ephemeral,
unread_notifications: UnreadNotificationsCount,
) -> Self {
Self { unread_notifications, timeline, state, account_data, ephemeral }
}
}
/// Counts of unread notifications for a room.
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
pub struct UnreadNotificationsCount {
/// The number of unread notifications for this room with the highlight flag
/// set.
pub highlight_count: u64,
/// The total number of unread notifications for this room.
pub notification_count: u64,
}
impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
fn from(notifications: RumaUnreadNotificationsCount) -> Self {
Self {
highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LeftRoom {
/// The timeline of messages and state changes in the room up to the point
/// when the user left.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: RoomAccountData,
}
impl LeftRoom {
pub fn new(timeline: Timeline, state: State, account_data: RoomAccountData) -> Self {
Self { timeline, state, account_data }
}
}
/// Events in the room.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Timeline {
/// True if the number of events returned was limited by the `limit` on the
/// filter.
pub limited: bool,
/// A token that can be supplied to to the `from` parameter of the
/// `/rooms/{roomId}/messages` endpoint.
pub prev_batch: Option<String>,
/// A list of events.
pub events: Vec<SyncTimelineEvent>,
}
impl Timeline {
pub fn new(limited: bool, prev_batch: Option<String>) -> Self {
Self { limited, prev_batch, ..Default::default() }
}
}
/// A slice of the timeline in the room.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct TimelineSlice {
/// The `next_batch` or `from` token used to obtain this slice
pub start: String,
/// The `prev_batch` or `to` token used to obtain this slice
/// If `None` this `TimelineSlice` is the beginning of the room
pub end: Option<String>,
/// Whether the number of events returned for this slice was limited
/// by a `limit`-filter when requesting
pub limited: bool,
/// A list of events.
pub events: Vec<SyncTimelineEvent>,
/// Whether this is a timeline slice obtained from a `SyncResponse`
pub sync: bool,
}
impl TimelineSlice {
pub fn new(
events: Vec<SyncTimelineEvent>,
start: String,
end: Option<String>,
limited: bool,
sync: bool,
) -> Self {
Self { start, end, events, limited, sync }
}
}
/// Wrapper around both MemberEvent-Types
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum MemberEvent {
Sync(SyncRoomMemberEvent),
Stripped(StrippedRoomMemberEvent),
}
impl MemberEvent {
/// The inner Content of the wrapped Event
pub fn original_content(&self) -> Option<&RoomMemberEventContent> {
match self {
MemberEvent::Sync(e) => e.as_original().map(|e| &e.content),
MemberEvent::Stripped(e) => Some(&e.content),
}
}
/// The sender of this event.
pub fn sender(&self) -> &UserId {
match self {
MemberEvent::Sync(e) => e.sender(),
MemberEvent::Stripped(e) => e.sender.borrow(),
}
}
/// The ID of this event.
pub fn event_id(&self) -> Option<&EventId> {
match self {
MemberEvent::Sync(e) => Some(e.event_id()),
MemberEvent::Stripped(_) => None,
}
}
/// The Server Timestamp of this event.
pub fn origin_server_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
match self {
MemberEvent::Sync(e) => Some(e.origin_server_ts()),
MemberEvent::Stripped(_) => None,
}
}
/// The membership state of the user
pub fn membership(&self) -> &MembershipState {
match self {
MemberEvent::Sync(e) => e.membership(),
MemberEvent::Stripped(e) => &e.content.membership,
}
}
/// The user id associated to this member event
pub fn user_id(&self) -> &UserId {
match self {
MemberEvent::Sync(e) => e.state_key(),
MemberEvent::Stripped(e) => &e.state_key,
}
}
}
/// A deserialized response for the rooms members API call.
///
/// [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MembersResponse {
/// The list of members events.
pub chunk: Vec<RoomMemberEvent>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
}
#[cfg(test)]
mod tests {
use ruma::{

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.6.0"
[package.metadata.docs.rs]
@@ -44,7 +44,7 @@ matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
olm-rs = { version = "2.2.0", features = ["serde"], optional = true }
pbkdf2 = { version = "0.11.0", default-features = false }
rand = "0.8.5"
ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] }
ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc2677"] }
serde = { version = "1.0.136", features = ["derive", "rc"] }
serde_json = "1.0.79"
sha2 = "0.10.2"

View File

@@ -21,7 +21,7 @@ use std::collections::BTreeMap;
use matrix_sdk_crypto::{OlmMachine, OlmError};
use ruma::{
api::client::sync::sync_events::v3::{ToDevice, DeviceLists},
api::client::sync::sync_events::{v3::ToDevice, DeviceLists},
device_id, user_id,
};

View File

@@ -30,8 +30,8 @@ use std::{
use matrix_sdk_common::locks::RwLock;
use ruma::{
api::client::backup::RoomKeyBackup, serde::Raw, DeviceKeyAlgorithm, OwnedDeviceId, OwnedRoomId,
OwnedTransactionId, TransactionId,
api::client::backup::RoomKeyBackup, serde::Raw, DeviceId, DeviceKeyAlgorithm, OwnedDeviceId,
OwnedRoomId, OwnedTransactionId, TransactionId,
};
use tracing::{debug, info, instrument, trace, warn};
@@ -85,21 +85,21 @@ impl From<PendingBackup> for OutgoingRequest {
}
}
/// The result of a signature check of a signed JSON object.
/// The result of a signature verification of a signed JSON object.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SignatureCheckResult {
/// The result of the signature check using the public key of our own
pub struct SignatureVerification {
/// The result of the signature verification using the public key of our own
/// device.
pub device_signature: SignatureState,
/// The result of the signature check using the public key of our own
/// The result of the signature verification using the public key of our own
/// user identity.
pub user_identity_signature: SignatureState,
/// The result of signature checks using public keys of other devices we
/// own.
/// The result of the signature verification using public keys of other
/// devices we own.
pub other_signatures: BTreeMap<OwnedDeviceId, SignatureState>,
}
impl SignatureCheckResult {
impl SignatureVerification {
/// Is the result considered to be trusted?
///
/// This tells us if the result has a valid signature from any of the
@@ -242,41 +242,26 @@ impl BackupMachine {
if let Some(user_signatures) = signatures.get(self.account.user_id()) {
for device_key_id in user_signatures.keys() {
if device_key_id.algorithm() == DeviceKeyAlgorithm::Ed25519 {
// No need to check our own device here, we're doing that
// using the check_own_device_signature().
// No need to check our own device here, we're doing that using
// the check_own_device_signature().
if device_key_id.device_id() == self.account.device_id() {
continue;
} else {
// We might iterate over some non-device signatures as well, but in this
// case there's no corresponding device and we get `Ok(None)` here, so
// things still work out.
let device = self
.store
.get_device(self.store.user_id(), device_key_id.device_id())
.await?;
}
trace!(
device_id = %device_key_id.device_id(),
"Checking backup auth data for device"
);
let state = self
.test_ed25519_device_signature(
device_key_id.device_id(),
signatures,
auth_data,
)
.await?;
let state = if let Some(device) = device {
self.backup_signed_by_device(device, signatures, auth_data)
} else {
trace!(
device_id = %device_key_id.device_id(),
"Device not found, can't check signature"
);
SignatureState::Missing
};
result.insert(device_key_id.device_id().to_owned(), state);
result.insert(device_key_id.device_id().to_owned(), state);
// Abort the loop if we found a trusted and valid
// signature, unless we should check all of them.
if state.trusted() && !compute_all_signatures {
break;
}
// Abort the loop if we found a trusted and valid signature,
// unless we should check all of them.
if state.trusted() && !compute_all_signatures {
break;
}
}
}
@@ -285,11 +270,31 @@ impl BackupMachine {
Ok(result)
}
async fn test_ed25519_device_signature(
&self,
device_id: &DeviceId,
signatures: &Signatures,
auth_data: &str,
) -> Result<SignatureState, CryptoStoreError> {
// We might iterate over some non-device signatures as well, but in this
// case there's no corresponding device and we get `Ok(None)` here, so
// things still work out.
let device = self.store.get_device(self.store.user_id(), device_id).await?;
trace!(%device_id, "Checking backup auth data for device");
if let Some(device) = device {
Ok(self.backup_signed_by_device(device, signatures, auth_data))
} else {
trace!(%device_id, "Device not found, can't check signature");
Ok(SignatureState::Missing)
}
}
async fn verify_auth_data_v1(
&self,
auth_data: MegolmV1AuthData,
compute_all_signatures: bool,
) -> Result<SignatureCheckResult, CryptoStoreError> {
) -> Result<SignatureVerification, CryptoStoreError> {
trace!(?auth_data, "Verifying backup auth data");
let serialized_auth_data = match auth_data.to_canonical_json() {
@@ -322,7 +327,7 @@ impl BackupMachine {
Default::default()
};
Ok(SignatureCheckResult { device_signature, user_identity_signature, other_signatures })
Ok(SignatureVerification { device_signature, user_identity_signature, other_signatures })
}
/// Verify some backup info that we downloaded from the server.
@@ -343,7 +348,7 @@ impl BackupMachine {
&self,
backup_info: RoomKeyBackupInfo,
compute_all_signatures: bool,
) -> Result<SignatureCheckResult, CryptoStoreError> {
) -> Result<SignatureVerification, CryptoStoreError> {
trace!(?backup_info, "Verifying backup auth data");
if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
@@ -449,7 +454,7 @@ impl BackupMachine {
.collect();
for session in &sessions {
session.mark_as_backed_up()
session.mark_as_backed_up();
}
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
@@ -470,54 +475,54 @@ impl BackupMachine {
expected = r.request_id.to_string().as_str(),
got = request_id.to_string().as_str(),
"Tried to mark a pending backup as sent but the request id didn't match"
)
);
}
} else {
warn!(
request_id = request_id.to_string().as_str(),
"Tried to mark a pending backup as sent but there isn't a backup pending"
);
}
};
Ok(())
}
async fn backup_helper(&self) -> Result<Option<PendingBackup>, CryptoStoreError> {
if let Some(backup_key) = &*self.backup_key.read().await {
if let Some(version) = backup_key.backup_version() {
let sessions =
self.store.inbound_group_sessions_for_backup(Self::BACKUP_BATCH_SIZE).await?;
if !sessions.is_empty() {
let key_count = sessions.len();
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
info!(
key_count = key_count,
keys = ?session_record,
?backup_key,
"Successfully created a room keys backup request"
);
let request = PendingBackup {
request_id: TransactionId::new(),
request: KeysBackupRequest { version, rooms: backup },
sessions: session_record,
};
Ok(Some(request))
} else {
trace!(?backup_key, "No room keys need to be backed up");
Ok(None)
}
} else {
warn!("Trying to backup room keys but the backup key wasn't uploaded");
Ok(None)
}
} else {
let Some(backup_key) = &*self.backup_key.read().await else {
warn!("Trying to backup room keys but no backup key was found");
Ok(None)
return Ok(None);
};
let Some(version) = backup_key.backup_version() else {
warn!("Trying to backup room keys but the backup key wasn't uploaded");
return Ok(None);
};
let sessions =
self.store.inbound_group_sessions_for_backup(Self::BACKUP_BATCH_SIZE).await?;
if sessions.is_empty() {
trace!(?backup_key, "No room keys need to be backed up");
return Ok(None);
}
let key_count = sessions.len();
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
info!(
key_count = key_count,
keys = ?session_record,
?backup_key,
"Successfully created a room keys backup request"
);
let request = PendingBackup {
request_id: TransactionId::new(),
request: KeysBackupRequest { version, rooms: backup },
sessions: session_record,
};
Ok(Some(request))
}
/// Backup all the non-backed up room keys we know about

View File

@@ -18,7 +18,7 @@ use thiserror::Error;
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
use super::store::CryptoStoreError;
use crate::olm::SessionExportError;
use crate::{olm::SessionExportError, types::SignedKey};
pub type OlmResult<T> = Result<T, OlmError>;
pub type MegolmResult<T> = Result<T, MegolmError>;
@@ -229,8 +229,18 @@ pub enum SessionCreationError {
OneTimeKeyUnknown(OwnedUserId, OwnedDeviceId),
/// Failed to verify the one-time key signatures.
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
InvalidSignature(OwnedUserId, OwnedDeviceId, SignatureError),
#[error(
"Failed to verify the signature of a one-time key, key: {one_time_key:?}, \
signing_key: {signing_key:?}: {error:?}"
)]
InvalidSignature {
/// The one-time key that failed the signature verification.
one_time_key: SignedKey,
/// The key that was used to verify the signature.
signing_key: Option<Ed25519PublicKey>,
/// The exact error describing why the signature verification failed.
error: SignatureError,
},
/// The user's device is missing a curve25519 key.
#[error(

View File

@@ -305,6 +305,57 @@ impl GossipMachine {
})
}
/// Try to encrypt the given `InboundGroupSession` for the given `Device` as
/// a forwarded room key.
///
/// This method might fail if we do not share an 1-to-1 Olm session with the
/// given `Device`, in that case we're going to queue up an
/// `/keys/claim` request to be sent out and retry once the 1-to-1 Olm
/// session has been established.
async fn try_to_forward_room_key(
&self,
event: &RoomKeyRequestEvent,
device: Device,
session: InboundGroupSession,
message_index: Option<u32>,
) -> OlmResult<Option<Session>> {
info!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
room_id = %session.room_id,
?message_index,
"Serving a room key request",
);
match self.forward_room_key(&session, &device, message_index).await {
Ok(s) => Ok(Some(s)),
Err(OlmError::MissingSession) => {
info!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
"Key request is missing an Olm session, \
putting the request in the wait queue",
);
self.handle_key_share_without_session(device, event.to_owned().into());
Ok(None)
}
Err(OlmError::SessionExport(e)) => {
warn!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
"Can't serve a room key request, the session \
can't be exported into a forwarded room key: {e:?}",
);
Ok(None)
}
Err(e) => Err(e),
}
}
/// Answer a room key request after we found the matching
/// `InboundGroupSession`.
async fn answer_room_key_request(
@@ -315,68 +366,7 @@ impl GossipMachine {
let device =
self.store.get_device(&event.sender, &event.content.requesting_device_id).await?;
if let Some(device) = device {
match self.should_share_key(&device, &session).await {
Err(e) => {
if let KeyForwardDecision::ChangedSenderKey = e {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"Received a key request from a device that changed \
their Curve25519 sender key"
);
} else {
debug!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
reason = ?e,
"Received a key request that we won't serve",
);
}
Ok(None)
}
Ok(message_index) => {
info!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
room_id = %session.room_id,
?message_index,
"Serving a room key request",
);
match self.forward_room_key(&session, &device, message_index).await {
Ok(s) => Ok(Some(s)),
Err(OlmError::MissingSession) => {
info!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
"Key request is missing an Olm session, \
putting the request in the wait queue",
);
self.handle_key_share_without_session(device, event.to_owned().into());
Ok(None)
}
Err(OlmError::SessionExport(e)) => {
warn!(
user_id = %device.user_id(),
device_id = %device.device_id(),
session_id = session.session_id(),
"Can't serve a room key request, the session \
can't be exported into a forwarded room key: \
{:?}",
e
);
Ok(None)
}
Err(e) => Err(e),
}
}
}
} else {
let Some(device) = device else {
warn!(
user_id = %event.sender,
device_id = %event.content.requesting_device_id,
@@ -384,7 +374,32 @@ impl GossipMachine {
);
self.store.update_tracked_user(&event.sender, true).await?;
Ok(None)
return Ok(None);
};
match self.should_share_key(&device, &session).await {
Ok(message_index) => {
self.try_to_forward_room_key(event, device, session, message_index).await
}
Err(e) => {
if let KeyForwardDecision::ChangedSenderKey = e {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"Received a key request from a device that changed \
their Curve25519 sender key"
);
} else {
debug!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
reason = ?e,
"Received a key request that we won't serve",
);
}
Ok(None)
}
}
}
@@ -751,29 +766,23 @@ impl GossipMachine {
if secret_name != &SecretName::RecoveryKey {
match self.store.import_secret(secret_name, &event.content.secret).await {
Ok(_) => self.mark_as_done(request).await?,
// If this is a store error propagate it up the call stack.
Err(SecretImportError::Store(e)) => return Err(e),
// Otherwise warn that there was something wrong with the
// secret.
Err(e) => {
// If this is a store error propagate it up
// the call stack.
if let SecretImportError::Store(e) = e {
return Err(e);
} else {
// Otherwise warn that there was
// something wrong with the secret.
warn!(
secret_name = secret_name.as_ref(),
error = ?e,
"Error while importing a secret"
)
}
warn!(
secret_name = secret_name.as_ref(),
error = ?e,
"Error while importing a secret"
);
}
}
} else {
// Skip importing the recovery key here since
// we'll want to check if the public key matches
// to the latest version on the server. The key
// will not be zeroized and
// instead leave the key in the event and let
// the user import it later.
// Skip importing the recovery key here since we'll want to check
// if the public key matches to the latest version on the server.
// The key will not be zeroized and instead leave the key in the
// event and let the user import it later.
}
Ok(())
@@ -925,25 +934,18 @@ impl GossipMachine {
sender_key: Curve25519PublicKey,
event: &DecryptedForwardedRoomKeyEvent,
) -> Result<Option<InboundGroupSession>, CryptoStoreError> {
if let Some(info) = event.room_key_info() {
if let Some(request) =
self.store.get_secret_request_by_info(&info.clone().into()).await?
{
if self.should_accept_forward(&request, sender_key).await? {
self.accept_forwarded_room_key(&request, sender_key, event).await
} else {
warn!(
sender = %event.sender,
%sender_key,
room_id = %info.room_id(),
session_id = info.session_id(),
"Received a forwarded room key from an unknown device, or \
from a device that the key request recipient doesn't own",
);
let Some(info) = event.room_key_info() else {
warn!(
sender = event.sender.as_str(),
sender_key = sender_key.to_base64(),
algorithm = %event.content.algorithm(),
"Received a forwarded room key with an unsupported algorithm",
);
return Ok(None);
};
Ok(None)
}
} else {
let Some(request) =
self.store.get_secret_request_by_info(&info.clone().into()).await? else {
warn!(
sender = %event.sender,
sender_key = %sender_key,
@@ -953,15 +955,19 @@ impl GossipMachine {
algorithm = %info.algorithm(),
"Received a forwarded room key that we didn't request",
);
return Ok(None);
};
Ok(None)
}
if self.should_accept_forward(&request, sender_key).await? {
self.accept_forwarded_room_key(&request, sender_key, event).await
} else {
warn!(
sender = event.sender.as_str(),
sender_key = sender_key.to_base64(),
algorithm = %event.content.algorithm(),
"Received a forwarded room key with an unsupported algorithm",
sender = %event.sender,
%sender_key,
room_id = %info.room_id(),
session_id = info.session_id(),
"Received a forwarded room key from an unknown device, or \
from a device that the key request recipient doesn't own",
);
Ok(None)
@@ -1545,15 +1551,15 @@ mod tests {
let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap();
if let AnyDecryptedOlmEvent::ForwardedRoomKey(e) = decrypted.result.event {
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &e)
.await
.unwrap();
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
} else {
let AnyDecryptedOlmEvent::ForwardedRoomKey(ev) = decrypted.result.event else {
panic!("Invalid decrypted event type");
}
};
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &ev)
.await
.unwrap();
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
// Check that alice now does have the session.
let session = alice_machine
@@ -1603,16 +1609,16 @@ mod tests {
.is_none());
let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap();
if let AnyDecryptedOlmEvent::ForwardedRoomKey(e) = decrypted.result.event {
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &e)
.await
.unwrap();
assert!(session.is_none(), "We should not receive a room key from another user");
} else {
let AnyDecryptedOlmEvent::ForwardedRoomKey(ev) = decrypted.result.event else {
panic!("Invalid decrypted event type");
}
};
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &ev)
.await
.unwrap();
assert!(session.is_none(), "We should not receive a room key from another user");
}
#[async_test]
@@ -1756,15 +1762,15 @@ mod tests {
let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap();
if let AnyDecryptedOlmEvent::ForwardedRoomKey(e) = decrypted.result.event {
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &e)
.await
.unwrap();
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
} else {
let AnyDecryptedOlmEvent::ForwardedRoomKey(ev) = decrypted.result.event else {
panic!("Invalid decrypted event type");
}
};
let session = alice_machine
.receive_forwarded_room_key(decrypted.result.sender_key, &ev)
.await
.unwrap();
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
// Check that alice now does have the session.
let session = alice_machine

View File

@@ -208,11 +208,8 @@ impl Device {
/// Get the Olm sessions that belong to this device.
pub(crate) async fn get_sessions(&self) -> StoreResult<Option<Arc<Mutex<Vec<Session>>>>> {
if let Some(k) = self.curve25519_key() {
self.verification_machine.store.get_sessions(&k.to_base64()).await
} else {
Ok(None)
}
let Some(k) = self.curve25519_key() else { return Ok(None) };
self.verification_machine.store.get_sessions(&k.to_base64()).await
}
/// Is this device considered to be verified.
@@ -572,9 +569,7 @@ impl ReadOnlyDevice {
event_type: &str,
content: Value,
) -> OlmResult<(Session, Raw<ToDeviceEncryptedEventContent>)> {
let sender_key = if let Some(k) = self.curve25519_key() {
k
} else {
let Some(sender_key) = self.curve25519_key() else {
warn!(
user_id = %self.user_id(),
device_id = %self.device_id(),
@@ -592,9 +587,7 @@ impl ReadOnlyDevice {
None
};
let mut session = if let Some(s) = session {
s
} else {
let Some(mut session) = session else {
warn!(
"Trying to encrypt a Megolm session for user {} on device {}, \
but no Olm session is found",

View File

@@ -432,7 +432,7 @@ impl IdentityManager {
let result = if let Some(mut i) = self.store.get_user_identity(user_id).await? {
match &mut i {
ReadOnlyUserIdentities::Own(ref mut identity) => {
ReadOnlyUserIdentities::Own(identity) => {
let user_signing = if let Some(s) = response
.user_signing_keys
.get(user_id)
@@ -452,7 +452,7 @@ impl IdentityManager {
.update(master_key, self_signing, user_signing)
.map(|_| (i, false))
}
ReadOnlyUserIdentities::Other(ref mut identity) => {
ReadOnlyUserIdentities::Other(identity) => {
identity.update(master_key, self_signing).map(|_| (i, false))
}
}

View File

@@ -1584,7 +1584,7 @@ pub(crate) mod tests {
api::{
client::{
keys::{claim_keys, get_keys, upload_keys},
sync::sync_events::v3::DeviceLists,
sync::sync_events::DeviceLists,
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,

View File

@@ -276,10 +276,8 @@ impl Account {
) -> OlmResult<Option<(Session, String)>> {
let s = self.store.get_sessions(&sender_key.to_base64()).await?;
// We don't have any existing sessions, return early.
let sessions = if let Some(s) = s {
s
} else {
let Some(sessions) = s else {
// We don't have any existing sessions, return early.
return Ok(None);
};
@@ -442,22 +440,21 @@ impl Account {
// ensures that we receive the room key even if we don't have access
// to the device.
if !matches!(event, AnyDecryptedOlmEvent::RoomKey(_)) {
if let Some(device) =
self.store.get_device_from_curve_key(event.sender(), sender_key).await?
{
if let Some(key) = device.ed25519_key() {
if key != event.keys().ed25519 {
return Err(EventError::MismatchedKeys(
key.into(),
event.keys().ed25519.into(),
)
.into());
}
} else {
let Some(device) =
self.store.get_device_from_curve_key(event.sender(), sender_key).await? else {
return Err(EventError::MissingSigningKey.into());
}
} else {
};
let Some(key) = device.ed25519_key() else {
return Err(EventError::MissingSigningKey.into());
};
if key != event.keys().ed25519 {
return Err(EventError::MismatchedKeys(
key.into(),
event.keys().ed25519.into(),
)
.into());
}
}
@@ -1036,12 +1033,12 @@ impl ReadOnlyAccount {
Err(e) => return Err(SessionCreationError::InvalidJson(e)),
};
device.verify_one_time_key(&one_time_key).map_err(|e| {
SessionCreationError::InvalidSignature(
device.user_id().to_owned(),
device.device_id().into(),
e,
)
device.verify_one_time_key(&one_time_key).map_err(|error| {
SessionCreationError::InvalidSignature {
signing_key: device.ed25519_key(),
one_time_key: one_time_key.clone(),
error,
}
})?;
let identity_key = device.curve25519_key().ok_or_else(|| {

View File

@@ -228,13 +228,13 @@ impl PrivateCrossSigningIdentity {
let master = MasterSigning::from_base64(self.user_id().to_owned(), master_key)?;
if public_identity.master_key() == &master.public_key {
Ok(Some(master))
Some(master)
} else {
Err(SecretImportError::MismatchedPublicKeys)
return Err(SecretImportError::MismatchedPublicKeys);
}
} else {
Ok(None)
}?;
None
};
let user_signing = if let Some(user_signing_key) = user_signing_key {
let subkey = UserSigning::from_base64(self.user_id().to_owned(), user_signing_key)?;

View File

@@ -147,14 +147,15 @@ fn verify_signature(
signatures: &Signatures,
canonical_json: &str,
) -> Result<(), SignatureError> {
if let Some(s) = signatures.get(user_id).and_then(|m| m.get(key_id)) {
match s {
Ok(Signature::Ed25519(s)) => Ok(public_key.verify(canonical_json.as_bytes(), s)?),
Ok(Signature::Other(_)) => Err(SignatureError::UnsupportedAlgorithm),
Err(_) => Err(SignatureError::InvalidSignature),
}
} else {
Err(SignatureError::NoSignatureFound)
let s = signatures
.get(user_id)
.and_then(|m| m.get(key_id))
.ok_or(SignatureError::NoSignatureFound)?;
match s {
Ok(Signature::Ed25519(s)) => Ok(public_key.verify(canonical_json.as_bytes(), s)?),
Ok(Signature::Other(_)) => Err(SignatureError::UnsupportedAlgorithm),
Err(_) => Err(SignatureError::InvalidSignature),
}
}

View File

@@ -317,10 +317,7 @@ impl VerificationMachine {
) -> Result<(), CryptoStoreError> {
let event = event.into();
#[allow(clippy::question_mark)]
let flow_id = if let Ok(flow_id) = FlowId::try_from(&event) {
flow_id
} else {
let Ok(flow_id) = FlowId::try_from(&event) else {
// This isn't a verification event, return early.
return Ok(());
};
@@ -344,180 +341,186 @@ impl VerificationMachine {
}
};
if let Some(content) = event.verification_content() {
match &content {
AnyVerificationContent::Request(r) => {
info!(
let Some(content) = event.verification_content() else { return Ok(()) };
match &content {
AnyVerificationContent::Request(r) => {
info!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
"Received a new verification request",
);
let Some(timestamp) = event.timestamp() else {
warn!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
"Received a new verification request",
"The key verification request didn't contain a valid timestamp"
);
return Ok(());
};
if let Some(timestamp) = event.timestamp() {
if Self::is_timestamp_valid(timestamp) {
if !event_sent_from_us(&event, r.from_device()) {
let request = VerificationRequest::from_request(
self.verifications.clone(),
self.store.clone(),
event.sender(),
flow_id,
r,
);
self.insert_request(request);
} else {
trace!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
"The received verification request was sent by us, ignoring it",
);
}
} else {
trace!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
?timestamp,
"The received verification request was too old or too far into the future",
);
}
} else {
warn!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
"The key verification request didn't contain a valid timestamp"
);
}
if !Self::is_timestamp_valid(timestamp) {
trace!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
?timestamp,
"The received verification request was too old or too far into the future",
);
return Ok(());
}
AnyVerificationContent::Cancel(c) => {
if let Some(verification) = self.get_request(event.sender(), flow_id.as_str()) {
verification.receive_cancel(event.sender(), c);
}
if let Some(verification) =
self.get_verification(event.sender(), flow_id.as_str())
{
match verification {
Verification::SasV1(sas) => {
// This won't produce an outgoing content
let _ = sas.receive_any_event(event.sender(), &content);
}
#[cfg(feature = "qrcode")]
Verification::QrV1(qr) => qr.receive_cancel(event.sender(), c),
}
}
if event_sent_from_us(&event, r.from_device()) {
trace!(
sender = event.sender().as_str(),
from_device = r.from_device().as_str(),
"The received verification request was sent by us, ignoring it",
);
return Ok(());
}
AnyVerificationContent::Ready(c) => {
if let Some(request) = self.get_request(event.sender(), flow_id.as_str()) {
if request.flow_id() == &flow_id {
request.receive_ready(event.sender(), c);
} else {
flow_id_mismatch();
}
}
}
AnyVerificationContent::Start(c) => {
if let Some(request) = self.get_request(event.sender(), flow_id.as_str()) {
if request.flow_id() == &flow_id {
request.receive_start(event.sender(), c).await?
} else {
flow_id_mismatch();
}
} else if let FlowId::ToDevice(_) = flow_id {
// TODO remove this soon, this has been deprecated by
// MSC3122 https://github.com/matrix-org/matrix-doc/pull/3122
if let Some(device) =
self.store.get_device(event.sender(), c.from_device()).await?
{
let identities = self.store.get_identities(device).await?;
match Sas::from_start_event(flow_id, c, identities, None, false) {
Ok(sas) => {
self.verifications.insert_sas(sas);
}
Err(cancellation) => self.queue_up_content(
event.sender(),
c.from_device(),
cancellation,
None,
),
}
}
}
}
AnyVerificationContent::Accept(_) | AnyVerificationContent::Key(_) => {
if let Some(sas) = self.get_sas(event.sender(), flow_id.as_str()) {
if sas.flow_id() == &flow_id {
if let Some((content, request_info)) =
sas.receive_any_event(event.sender(), &content)
{
self.queue_up_content(
sas.other_user_id(),
sas.other_device_id(),
content,
request_info,
);
}
} else {
flow_id_mismatch();
}
}
}
AnyVerificationContent::Mac(_) => {
if let Some(s) = self.get_sas(event.sender(), flow_id.as_str()) {
if s.flow_id() == &flow_id {
let content = s.receive_any_event(event.sender(), &content);
let request = VerificationRequest::from_request(
self.verifications.clone(),
self.store.clone(),
event.sender(),
flow_id,
r,
);
if s.is_done() {
self.mark_sas_as_done(s, content.map(|(c, _)| c)).await?;
} else {
// Even if we are not done (yet), there might be content to send
// out, e.g. in the case where we are done with our side of the
// verification process, but the other side has not yet sent their
// "done".
if let Some((content, request_id)) = content {
self.queue_up_content(
s.other_user_id(),
s.other_device_id(),
content,
request_id,
);
}
}
} else {
flow_id_mismatch();
}
}
self.insert_request(request);
}
AnyVerificationContent::Cancel(c) => {
if let Some(verification) = self.get_request(event.sender(), flow_id.as_str()) {
verification.receive_cancel(event.sender(), c);
}
AnyVerificationContent::Done(c) => {
if let Some(verification) = self.get_request(event.sender(), flow_id.as_str()) {
verification.receive_done(event.sender(), c);
}
#[allow(clippy::single_match)]
match self.get_verification(event.sender(), flow_id.as_str()) {
Some(Verification::SasV1(sas)) => {
let content = sas.receive_any_event(event.sender(), &content);
if sas.is_done() {
self.mark_sas_as_done(sas, content.map(|(c, _)| c)).await?;
}
if let Some(verification) = self.get_verification(event.sender(), flow_id.as_str())
{
match verification {
Verification::SasV1(sas) => {
// This won't produce an outgoing content
let _ = sas.receive_any_event(event.sender(), &content);
}
#[cfg(feature = "qrcode")]
Some(Verification::QrV1(qr)) => {
let (cancellation, request) = qr.receive_done(c).await?;
if let Some(c) = cancellation {
self.verifications.add_request(c.into())
}
if let Some(s) = request {
self.verifications.add_request(s.into())
}
}
None => (),
Verification::QrV1(qr) => qr.receive_cancel(event.sender(), c),
}
}
}
AnyVerificationContent::Ready(c) => {
let Some(request) = self.get_request(event.sender(), flow_id.as_str()) else {
return Ok(());
};
if request.flow_id() == &flow_id {
request.receive_ready(event.sender(), c);
} else {
flow_id_mismatch();
}
}
AnyVerificationContent::Start(c) => {
if let Some(request) = self.get_request(event.sender(), flow_id.as_str()) {
if request.flow_id() == &flow_id {
request.receive_start(event.sender(), c).await?
} else {
flow_id_mismatch();
}
} else if let FlowId::ToDevice(_) = flow_id {
// TODO remove this soon, this has been deprecated by
// MSC3122 https://github.com/matrix-org/matrix-doc/pull/3122
if let Some(device) =
self.store.get_device(event.sender(), c.from_device()).await?
{
let identities = self.store.get_identities(device).await?;
match Sas::from_start_event(flow_id, c, identities, None, false) {
Ok(sas) => {
self.verifications.insert_sas(sas);
}
Err(cancellation) => self.queue_up_content(
event.sender(),
c.from_device(),
cancellation,
None,
),
}
}
}
}
AnyVerificationContent::Accept(_) | AnyVerificationContent::Key(_) => {
let Some(sas) = self.get_sas(event.sender(), flow_id.as_str()) else {
return Ok(());
};
if sas.flow_id() != &flow_id {
flow_id_mismatch();
return Ok(());
}
let Some((content, request_info)) =
sas.receive_any_event(event.sender(), &content) else { return Ok(()) };
self.queue_up_content(
sas.other_user_id(),
sas.other_device_id(),
content,
request_info,
);
}
AnyVerificationContent::Mac(_) => {
let Some(s) = self.get_sas(event.sender(), flow_id.as_str()) else { return Ok(()) };
if s.flow_id() != &flow_id {
flow_id_mismatch();
return Ok(());
}
let content = s.receive_any_event(event.sender(), &content);
if s.is_done() {
self.mark_sas_as_done(s, content.map(|(c, _)| c)).await?;
} else {
// Even if we are not done (yet), there might be content to
// send out, e.g. in the case where we are done with our
// side of the verification process, but the other side has
// not yet sent their "done".
let Some((content, request_id)) = content else { return Ok(()) };
self.queue_up_content(
s.other_user_id(),
s.other_device_id(),
content,
request_id,
);
}
}
AnyVerificationContent::Done(c) => {
if let Some(verification) = self.get_request(event.sender(), flow_id.as_str()) {
verification.receive_done(event.sender(), c);
}
#[allow(clippy::single_match)]
match self.get_verification(event.sender(), flow_id.as_str()) {
Some(Verification::SasV1(sas)) => {
let content = sas.receive_any_event(event.sender(), &content);
if sas.is_done() {
self.mark_sas_as_done(sas, content.map(|(c, _)| c)).await?;
}
}
#[cfg(feature = "qrcode")]
Some(Verification::QrV1(qr)) => {
let (cancellation, request) = qr.receive_done(c).await?;
if let Some(c) = cancellation {
self.verifications.add_request(c.into())
}
if let Some(s) = request {
self.verifications.add_request(s.into())
}
}
None => {}
}
}
}
Ok(())
@@ -638,6 +641,7 @@ mod tests {
}
#[cfg(not(target_os = "macos"))]
#[allow(unknown_lints, clippy::unchecked_duration_subtraction)]
#[async_test]
async fn timing_out() {
let (alice_machine, bob) = setup_verification_machine().await;

View File

@@ -701,45 +701,41 @@ impl IdentitiesBeingVerified {
) -> Result<Option<ReadOnlyDevice>, CryptoStoreError> {
let device = self.store.get_device(self.other_user_id(), self.other_device_id()).await?;
if let Some(device) = device {
if device.keys() == self.device_being_verified.keys() {
if verified_devices.map_or(false, |v| v.contains(&device)) {
trace!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"Marking device as verified.",
);
device.set_trust_state(LocalTrust::Verified);
Ok(Some(device))
} else {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"The interactive verification process didn't verify \
the device",
);
Ok(None)
}
} else {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"The device keys have changed while an interactive \
verification was going on, not marking the device as verified.",
);
Ok(None)
}
} else {
let Some(device) = device else {
let device = &self.device_being_verified;
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"The device was deleted while an interactive verification was \
going on.",
"The device was deleted while an interactive verification was going on.",
);
return Ok(None);
};
if device.keys() != self.device_being_verified.keys() {
warn!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"The device keys have changed while an interactive verification \
was going on, not marking the device as verified.",
);
return Ok(None);
}
if verified_devices.map_or(false, |v| v.contains(&device)) {
trace!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"Marking device as verified.",
);
device.set_trust_state(LocalTrust::Verified);
Ok(Some(device))
} else {
info!(
user_id = device.user_id().as_str(),
device_id = device.device_id().as_str(),
"The interactive verification process didn't verify the device",
);
Ok(None)

View File

@@ -322,8 +322,8 @@ impl VerificationRequest {
&self,
data: QrVerificationData,
) -> Result<Option<QrVerification>, ScanError> {
let fut = if let InnerRequest::Ready(r) = &*self.inner.lock().unwrap() {
Some(QrVerification::from_scan(
let future = if let InnerRequest::Ready(r) = &*self.inner.lock().unwrap() {
QrVerification::from_scan(
r.store.clone(),
r.other_user_id.clone(),
r.state.other_device_id.clone(),
@@ -331,41 +331,37 @@ impl VerificationRequest {
data,
self.we_started,
Some(self.inner.clone().into()),
))
)
} else {
None
return Ok(None);
};
if let Some(future) = fut {
let qr_verification = future.await?;
let qr_verification = future.await?;
// We may have previously started our own QR verification (e.g. two devices
// displaying QR code at the same time), so we need to replace it with the newly
// scanned code.
if self
.verification_cache
.get_qr(qr_verification.other_user_id(), qr_verification.flow_id().as_str())
.is_some()
{
debug!(
user_id = %self.other_user(),
flow_id = self.flow_id().as_str(),
"Replacing existing QR verification"
);
self.verification_cache.replace_qr(qr_verification.clone());
} else {
debug!(
user_id = %self.other_user(),
flow_id = self.flow_id().as_str(),
"Inserting new QR verification"
);
self.verification_cache.insert_qr(qr_verification.clone());
}
Ok(Some(qr_verification))
// We may have previously started our own QR verification (e.g. two devices
// displaying QR code at the same time), so we need to replace it with the newly
// scanned code.
if self
.verification_cache
.get_qr(qr_verification.other_user_id(), qr_verification.flow_id().as_str())
.is_some()
{
debug!(
user_id = %self.other_user(),
flow_id = self.flow_id().as_str(),
"Replacing existing QR verification"
);
self.verification_cache.replace_qr(qr_verification.clone());
} else {
Ok(None)
debug!(
user_id = %self.other_user(),
flow_id = self.flow_id().as_str(),
"Inserting new QR verification"
);
self.verification_cache.insert_qr(qr_verification.clone());
}
Ok(Some(qr_verification))
}
pub(crate) fn from_request(
@@ -541,31 +537,24 @@ impl VerificationRequest {
let cancelled = Cancelled::new(true, code);
let cancel_content = cancelled.as_content(self.flow_id());
if let OutgoingContent::ToDevice(c) = cancel_content {
let recipients: Vec<OwnedDeviceId> = self
.recipient_devices
.iter()
.filter(|&d| filter_device.map_or(true, |device| **d != *device))
.cloned()
.collect();
let OutgoingContent::ToDevice(c) = cancel_content else { return None };
let recip_devices: Vec<OwnedDeviceId> = self
.recipient_devices
.iter()
.filter(|&d| filter_device.map_or(true, |device| **d != *device))
.cloned()
.collect();
// We don't need to notify anyone if no recipients were present
// but we did have a filter device, since this means that only a
// single device received the `m.key.verification.request` and that
// device accepted the request.
if recipients.is_empty() && filter_device.is_some() {
None
} else {
Some(ToDeviceRequest::for_recipients(
self.other_user(),
recipients,
c,
TransactionId::new(),
))
}
} else {
None
if recip_devices.is_empty() && filter_device.is_some() {
// We don't need to notify anyone if no recipients were present but
// we did have a filter device, since this means that only a single
// device received the `m.key.verification.request` and that device
// accepted the request.
return None;
}
let recipient = self.other_user();
Some(ToDeviceRequest::for_recipients(recipient, recip_devices, c, TransactionId::new()))
}
pub(crate) fn receive_ready(&self, sender: &UserId, content: &ReadyContent<'_>) {
@@ -601,17 +590,16 @@ impl VerificationRequest {
) -> Result<(), CryptoStoreError> {
let inner = self.inner.lock().unwrap().clone();
if let InnerRequest::Ready(s) = inner {
s.receive_start(sender, content, self.we_started, self.inner.clone().into()).await?;
} else {
let InnerRequest::Ready(s) = inner else {
warn!(
sender = sender.as_str(),
device_id = content.from_device().as_str(),
"Received a key verification start event but we're not yet in the ready state"
);
}
return Ok(());
};
Ok(())
s.receive_start(sender, content, self.we_started, self.inner.clone().into()).await
}
pub(crate) fn receive_done(&self, sender: &UserId, content: &DoneContent<'_>) {
@@ -628,21 +616,23 @@ impl VerificationRequest {
}
pub(crate) fn receive_cancel(&self, sender: &UserId, content: &CancelContent<'_>) {
if sender == self.other_user() {
trace!(
sender = sender.as_str(),
code = content.cancel_code().as_str(),
"Cancelling a verification request, other user has cancelled"
);
let mut inner = self.inner.lock().unwrap();
inner.cancel(false, content.cancel_code());
if sender != self.other_user() {
return;
}
if self.we_started() {
if let Some(request) =
self.cancel_for_other_devices(content.cancel_code().to_owned(), None)
{
self.verification_cache.add_verification_request(request.into());
}
trace!(
sender = sender.as_str(),
code = content.cancel_code().as_str(),
"Cancelling a verification request, other user has cancelled"
);
let mut inner = self.inner.lock().unwrap();
inner.cancel(false, content.cancel_code());
if self.we_started() {
if let Some(request) =
self.cancel_for_other_devices(content.cancel_code().to_owned(), None)
{
self.verification_cache.add_verification_request(request.into());
}
}
}
@@ -726,14 +716,11 @@ impl InnerRequest {
}
fn accept(&mut self, methods: Vec<VerificationMethod>) -> Option<OutgoingContent> {
if let InnerRequest::Requested(s) = self {
let (state, content) = s.clone().accept(methods);
*self = InnerRequest::Ready(state);
let InnerRequest::Requested(s) = self else { return None };
let (state, content) = s.clone().accept(methods);
*self = InnerRequest::Ready(state);
Some(content)
} else {
None
}
Some(content)
}
fn receive_done(&mut self, content: &DoneContent<'_>) {
@@ -962,9 +949,9 @@ struct Ready {
}
impl RequestState<Ready> {
fn to_started_sas<'a>(
fn to_started_sas(
&self,
content: &StartContent<'a>,
content: &StartContent<'_>,
identities: IdentitiesBeingVerified,
we_started: bool,
request_handle: RequestHandle,
@@ -994,19 +981,16 @@ impl RequestState<Ready> {
return Ok(None);
}
let device = if let Some(device) =
self.store.get_device(&self.other_user_id, &self.state.other_device_id).await?
{
device
} else {
warn!(
user_id = self.other_user_id.as_str(),
device_id = self.state.other_device_id.as_str(),
"Can't create a QR code, the device that accepted the \
verification doesn't exist"
);
return Ok(None);
};
let Some(device) =
self.store.get_device(&self.other_user_id, &self.state.other_device_id).await? else {
warn!(
user_id = self.other_user_id.as_str(),
device_id = self.state.other_device_id.as_str(),
"Can't create a QR code, the device that accepted the \
verification doesn't exist"
);
return Ok(None);
};
let identities = self.store.get_identities(device).await?;
@@ -1123,9 +1107,7 @@ impl RequestState<Ready> {
"Received a new verification start event",
);
let device = if let Some(d) = self.store.get_device(sender, content.from_device()).await? {
d
} else {
let Some(device) = self.store.get_device(sender, content.from_device()).await? else {
warn!(
sender = sender.as_str(),
device = content.from_device().as_str(),
@@ -1213,19 +1195,16 @@ impl RequestState<Ready> {
}
// TODO signal why starting the sas flow doesn't work?
let device = if let Some(device) =
self.store.get_device(&self.other_user_id, &self.state.other_device_id).await?
{
device
} else {
warn!(
user_id = self.other_user_id.as_str(),
device_id = self.state.other_device_id.as_str(),
"Can't start the SAS verification flow, the device that \
accepted the verification doesn't exist"
);
return Ok(None);
};
let Some(device) =
self.store.get_device(&self.other_user_id, &self.state.other_device_id).await? else {
warn!(
user_id = self.other_user_id.as_str(),
device_id = self.state.other_device_id.as_str(),
"Can't start the SAS verification flow, the device that \
accepted the verification doesn't exist"
);
return Ok(None);
};
let identities = self.store.get_identities(device).await?;

View File

@@ -180,20 +180,17 @@ impl InnerSas {
self,
methods: Vec<ShortAuthenticationString>,
) -> Option<(InnerSas, OwnedAcceptContent)> {
if let InnerSas::Started(s) = self {
let sas = s.into_we_accepted(methods);
let content = sas.as_content();
let InnerSas::Started(s) = self else { return None };
let sas = s.into_we_accepted(methods);
let content = sas.as_content();
trace!(
flow_id = sas.verification_flow_id.as_str(),
accepted_protocols = ?sas.state.accepted_protocols,
"Accepted a SAS verification"
);
trace!(
flow_id = sas.verification_flow_id.as_str(),
accepted_protocols = ?sas.state.accepted_protocols,
"Accepted a SAS verification"
);
Some((InnerSas::WeAccepted(sas), content))
} else {
None
}
Some((InnerSas::WeAccepted(sas), content))
}
#[cfg(test)]

View File

@@ -16,10 +16,11 @@ mod helpers;
mod inner_sas;
mod sas_state;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use futures_core::Stream;
use futures_signals::signal::{Mutable, SignalExt};
use futures_util::StreamExt;
use inner_sas::InnerSas;
use ruma::{
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
@@ -48,8 +49,7 @@ use crate::{
/// Short authentication string object.
#[derive(Clone, Debug)]
pub struct Sas {
inner: Arc<Mutex<InnerSas>>,
state: Arc<Mutable<SasState>>,
inner: Arc<Mutable<InnerSas>>,
account: ReadOnlyAccount,
identities_being_verified: IdentitiesBeingVerified,
flow_id: Arc<FlowId>,
@@ -268,12 +268,12 @@ impl Sas {
/// Does this verification flow support displaying emoji for the short
/// authentication string.
pub fn supports_emoji(&self) -> bool {
self.inner.lock().unwrap().supports_emoji()
self.inner.lock_ref().supports_emoji()
}
/// Did this verification flow start from a verification request.
pub fn started_from_request(&self) -> bool {
self.inner.lock().unwrap().started_from_request()
self.inner.lock_ref().started_from_request()
}
/// Is this a verification that is veryfying one of our own devices.
@@ -283,18 +283,18 @@ impl Sas {
/// Have we confirmed that the short auth string matches.
pub fn have_we_confirmed(&self) -> bool {
self.inner.lock().unwrap().have_we_confirmed()
self.inner.lock_ref().have_we_confirmed()
}
/// Has the verification been accepted by both parties.
pub fn has_been_accepted(&self) -> bool {
self.inner.lock().unwrap().has_been_accepted()
self.inner.lock_ref().has_been_accepted()
}
/// Get info about the cancellation if the verification flow has been
/// cancelled.
pub fn cancel_info(&self) -> Option<CancelInfo> {
if let InnerSas::Cancelled(c) = &*self.inner.lock().unwrap() {
if let InnerSas::Cancelled(c) = &*self.inner.lock_ref() {
Some(c.state.as_ref().clone().into())
} else {
None
@@ -309,7 +309,7 @@ impl Sas {
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) {
self.inner.lock().unwrap().set_creation_time(time)
self.inner.lock_mut().set_creation_time(time)
}
fn start_helper(
@@ -327,13 +327,11 @@ impl Sas {
request_handle.is_some(),
);
let state = (&inner).into();
let account = identities.store.account.clone();
(
Sas {
inner: Arc::new(Mutex::new(inner)),
state: Mutable::new(state).into(),
inner: Arc::new(Mutable::new(inner)),
account,
identities_being_verified: identities,
flow_id: flow_id.into(),
@@ -414,12 +412,10 @@ impl Sas {
request_handle.is_some(),
)?;
let state = (&inner).into();
let account = identities.store.account.clone();
Ok(Sas {
inner: Arc::new(Mutex::new(inner)),
state: Mutable::new(state).into(),
inner: Arc::new(Mutable::new(inner)),
account,
identities_being_verified: identities,
flow_id: flow_id.into(),
@@ -448,40 +444,31 @@ impl Sas {
) -> Option<OutgoingVerificationRequest> {
let old_state = self.state_debug();
let (request, state) = {
let mut guard = self.inner.lock().unwrap();
let request = {
let mut guard = self.inner.lock_mut();
let sas: InnerSas = (*guard).clone();
let methods = settings.allowed_methods;
if let Some((sas, content)) = sas.accept(methods) {
let state: SasState = (&sas).into();
*guard = sas;
(
Some(match content {
OwnedAcceptContent::ToDevice(c) => {
let content = AnyToDeviceEventContent::KeyVerificationAccept(c);
self.content_to_request(content).into()
}
OwnedAcceptContent::Room(room_id, content) => RoomMessageRequest {
room_id,
txn_id: TransactionId::new(),
content: AnyMessageLikeEventContent::KeyVerificationAccept(content),
}
.into(),
}),
Some(state),
)
Some(match content {
OwnedAcceptContent::ToDevice(c) => {
let content = AnyToDeviceEventContent::KeyVerificationAccept(c);
self.content_to_request(content).into()
}
OwnedAcceptContent::Room(room_id, content) => RoomMessageRequest {
room_id,
txn_id: TransactionId::new(),
content: AnyMessageLikeEventContent::KeyVerificationAccept(content),
}
.into(),
})
} else {
(None, None)
None
}
};
if let Some(new_state) = state {
self.update_state(new_state);
}
let new_state = self.state_debug();
trace!(
@@ -505,15 +492,14 @@ impl Sas {
&self,
) -> Result<(Vec<OutgoingVerificationRequest>, Option<SignatureUploadRequest>), CryptoStoreError>
{
let (contents, done, state) = {
let mut guard = self.inner.lock().unwrap();
let (contents, done) = {
let mut guard = self.inner.lock_mut();
let sas: InnerSas = (*guard).clone();
let (sas, contents) = sas.confirm();
let state: SasState = (&sas).into();
*guard = sas;
(contents, guard.is_done(), state)
(contents, guard.is_done())
};
let mac_requests = contents
@@ -540,17 +526,10 @@ impl Sas {
VerificationResult::Cancel(c) => {
Ok((self.cancel_with_code(c).into_iter().collect(), None))
}
VerificationResult::Ok => {
self.update_state(state);
Ok((mac_requests, None))
}
VerificationResult::SignatureUpload(r) => {
self.update_state(state);
Ok((mac_requests, Some(r)))
}
VerificationResult::Ok => Ok((mac_requests, None)),
VerificationResult::SignatureUpload(r) => Ok((mac_requests, Some(r))),
}
} else {
self.update_state(state);
Ok((mac_requests, None))
}
}
@@ -585,8 +564,8 @@ impl Sas {
///
/// [`cancel()`]: #method.cancel
pub fn cancel_with_code(&self, code: CancelCode) -> Option<OutgoingVerificationRequest> {
let (content, state) = {
let mut guard = self.inner.lock().unwrap();
let content = {
let mut guard = self.inner.lock_mut();
if let Some(request) = &self.request_handle {
request.cancel_with_code(&code);
@@ -594,22 +573,16 @@ impl Sas {
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.cancel(true, code);
let state: SasState = (&sas).into();
*guard = sas;
(
content.map(|c| match c {
OutgoingContent::Room(room_id, content) => {
RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into()
}
OutgoingContent::ToDevice(c) => self.content_to_request(c).into(),
}),
state,
)
content.map(|c| match c {
OutgoingContent::Room(room_id, content) => {
RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into()
}
OutgoingContent::ToDevice(c) => self.content_to_request(c).into(),
})
};
self.update_state(state);
content
}
@@ -625,22 +598,22 @@ impl Sas {
/// Has the SAS verification flow timed out.
pub fn timed_out(&self) -> bool {
self.inner.lock().unwrap().timed_out()
self.inner.lock_ref().timed_out()
}
/// Are we in a state where we can show the short auth string.
pub fn can_be_presented(&self) -> bool {
self.inner.lock().unwrap().can_be_presented()
self.inner.lock_ref().can_be_presented()
}
/// Is the SAS flow done.
pub fn is_done(&self) -> bool {
self.inner.lock().unwrap().is_done()
self.inner.lock_ref().is_done()
}
/// Is the SAS flow canceled.
pub fn is_cancelled(&self) -> bool {
self.inner.lock().unwrap().is_cancelled()
self.inner.lock_ref().is_cancelled()
}
/// Get the emoji version of the short auth string.
@@ -648,7 +621,7 @@ impl Sas {
/// Returns None if we can't yet present the short auth string, otherwise
/// seven tuples containing the emoji and description.
pub fn emoji(&self) -> Option<[Emoji; 7]> {
self.inner.lock().unwrap().emoji()
self.inner.lock_ref().emoji()
}
/// Get the index of the emoji representing the short auth string
@@ -658,7 +631,7 @@ impl Sas {
/// converted to an emoji using the
/// [relevant spec entry](https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji).
pub fn emoji_index(&self) -> Option<[u8; 7]> {
self.inner.lock().unwrap().emoji_index()
self.inner.lock_ref().emoji_index()
}
/// Get the decimal version of the short auth string.
@@ -667,7 +640,7 @@ impl Sas {
/// tuple containing three 4-digit integers that represent the short auth
/// string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.lock().unwrap().decimals()
self.inner.lock_ref().decimals()
}
/// Listen for changes in the SAS verification process.
@@ -759,29 +732,16 @@ impl Sas {
/// # anyhow::Ok(()) });
/// ```
pub fn changes(&self) -> impl Stream<Item = SasState> {
self.state.signal_cloned().to_stream()
self.inner.signal_cloned().to_stream().map(|s| (&s).into())
}
/// Get the current state of the verification process.
pub fn state(&self) -> SasState {
self.state.lock_ref().to_owned()
}
fn update_state(&self, new_state: SasState) {
let mut lock = self.state.lock_mut();
// Only update the state if it differs, this is important so clients don't end
// up printing the emoji twice. For example, the internal state might
// change into a MacReceived, because the other side already confirmed,
// but our side still needs to just show the emoji and wait for
// confirmation.
if *lock != new_state {
*lock = new_state;
}
(&*self.inner.lock_ref()).into()
}
fn state_debug(&self) -> State {
(&*self.inner.lock().unwrap()).into()
(&*self.inner.lock_ref()).into()
}
pub(crate) fn receive_any_event(
@@ -791,16 +751,14 @@ impl Sas {
) -> Option<(OutgoingContent, Option<RequestInfo>)> {
let old_state = self.state_debug();
let (content, state) = {
let mut guard = self.inner.lock().unwrap();
let content = {
let mut guard = self.inner.lock_mut();
let sas: InnerSas = (*guard).clone();
let (sas, content) = sas.receive_any_event(sender, content);
let state: SasState = (&sas).into();
*guard = sas;
(content, state)
content
};
let new_state = self.state_debug();
@@ -811,30 +769,25 @@ impl Sas {
"SAS received an event and changed its state"
);
self.update_state(state);
content
}
pub(crate) fn mark_request_as_sent(&self, request_id: &TransactionId) {
let old_state = self.state_debug();
let state = {
let mut guard = self.inner.lock().unwrap();
{
let mut guard = self.inner.lock_mut();
let sas: InnerSas = (*guard).clone();
if let Some(sas) = sas.mark_request_as_sent(request_id) {
let state: SasState = (&sas).into();
*guard = sas;
Some(state)
} else {
error!(
flow_id = self.flow_id().as_str(),
%request_id,
"Tried to mark a request as sent, but the request ID didn't match"
);
None
}
};
@@ -847,18 +800,14 @@ impl Sas {
%request_id,
"Marked a SAS verification HTTP request as sent"
);
if let Some(state) = state {
self.update_state(state);
}
}
pub(crate) fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
self.inner.lock().unwrap().verified_devices()
self.inner.lock_ref().verified_devices()
}
pub(crate) fn verified_identities(&self) -> Option<Arc<[ReadOnlyUserIdentities]>> {
self.inner.lock().unwrap().verified_identities()
self.inner.lock_ref().verified_identities()
}
pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest {

View File

@@ -615,30 +615,30 @@ impl SasState<Created> {
) -> Result<SasState<Accepted>, SasState<Cancelled>> {
self.check_event(sender, content.flow_id()).map_err(|c| self.clone().cancel(true, c))?;
if let AcceptMethod::SasV1(content) = content.method() {
let accepted_protocols = AcceptedProtocols::try_from(content.clone())
.map_err(|c| self.clone().cancel(true, c))?;
let AcceptMethod::SasV1(content) = content.method() else {
return Err(self.cancel(true, CancelCode::UnknownMethod));
};
let start_content = self.as_content().into();
let accepted_protocols = AcceptedProtocols::try_from(content.clone())
.map_err(|c| self.clone().cancel(true, c))?;
Ok(SasState {
inner: self.inner,
our_public_key: self.our_public_key,
ids: self.ids,
verification_flow_id: self.verification_flow_id,
creation_time: self.creation_time,
last_event_time: Instant::now().into(),
started_from_request: self.started_from_request,
state: Arc::new(Accepted {
start_content,
commitment: content.commitment.clone(),
request_id: TransactionId::new(),
accepted_protocols,
}),
})
} else {
Err(self.cancel(true, CancelCode::UnknownMethod))
}
let start_content = self.as_content().into();
Ok(SasState {
inner: self.inner,
our_public_key: self.our_public_key,
ids: self.ids,
verification_flow_id: self.verification_flow_id,
creation_time: self.creation_time,
last_event_time: Instant::now().into(),
started_from_request: self.started_from_request,
state: Arc::new(Accepted {
start_content,
commitment: content.commitment.clone(),
request_id: TransactionId::new(),
accepted_protocols,
}),
})
}
}
@@ -689,41 +689,44 @@ impl SasState<Started> {
state: Arc::new(Cancelled::new(true, CancelCode::UnknownMethod)),
};
if let StartMethod::SasV1(method_content) = content.method() {
let commitment = calculate_commitment(our_public_key, content);
let state = match content.method() {
StartMethod::SasV1(method_content) => {
let commitment = calculate_commitment(our_public_key, content);
info!(
public_key = our_public_key.to_base64(),
%commitment,
?content,
"Calculated SAS commitment",
);
info!(
public_key = our_public_key.to_base64(),
%commitment,
?content,
"Calculated SAS commitment",
);
if let Ok(accepted_protocols) = AcceptedProtocols::try_from(method_content) {
Ok(SasState {
inner: Arc::new(Mutex::new(Some(sas))),
our_public_key,
let Ok(accepted_protocols) = AcceptedProtocols::try_from(method_content) else {
return Err(canceled());
};
ids: SasIds { account, other_device, other_identity, own_identity },
creation_time: Arc::new(Instant::now()),
last_event_time: Arc::new(Instant::now()),
started_from_request,
verification_flow_id: flow_id,
state: Arc::new(Started {
protocol_definitions: method_content.to_owned(),
accepted_protocols,
commitment,
}),
})
} else {
Err(canceled())
Started {
protocol_definitions: method_content.to_owned(),
accepted_protocols,
commitment,
}
}
} else {
Err(canceled())
}
_ => return Err(canceled()),
};
Ok(SasState {
inner: Arc::new(Mutex::new(Some(sas))),
our_public_key,
ids: SasIds { account, other_device, other_identity, own_identity },
creation_time: Arc::new(Instant::now()),
last_event_time: Arc::new(Instant::now()),
started_from_request,
verification_flow_id: flow_id,
state: Arc::new(state),
})
}
#[cfg(test)]
@@ -813,30 +816,30 @@ impl SasState<Started> {
) -> Result<SasState<Accepted>, SasState<Cancelled>> {
self.check_event(sender, content.flow_id()).map_err(|c| self.clone().cancel(true, c))?;
if let AcceptMethod::SasV1(content) = content.method() {
let accepted_protocols = AcceptedProtocols::try_from(content.clone())
.map_err(|c| self.clone().cancel(true, c))?;
let AcceptMethod::SasV1(content) = content.method() else {
return Err(self.cancel(true, CancelCode::UnknownMethod));
};
let start_content = self.as_content().into();
let accepted_protocols = AcceptedProtocols::try_from(content.clone())
.map_err(|c| self.clone().cancel(true, c))?;
Ok(SasState {
inner: self.inner,
our_public_key: self.our_public_key,
ids: self.ids,
verification_flow_id: self.verification_flow_id,
creation_time: self.creation_time,
last_event_time: Instant::now().into(),
started_from_request: self.started_from_request,
state: Arc::new(Accepted {
start_content,
commitment: content.commitment.clone(),
request_id: TransactionId::new(),
accepted_protocols,
}),
})
} else {
Err(self.cancel(true, CancelCode::UnknownMethod))
}
let start_content = self.as_content().into();
Ok(SasState {
inner: self.inner,
our_public_key: self.our_public_key,
ids: self.ids,
verification_flow_id: self.verification_flow_id,
creation_time: self.creation_time,
last_event_time: Instant::now().into(),
started_from_request: self.started_from_request,
state: Arc::new(Accepted {
start_content,
commitment: content.commitment.clone(),
request_id: TransactionId::new(),
accepted_protocols,
}),
})
}
}

View File

@@ -5,7 +5,7 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Web's IndexedDB Storage backend for matrix-sdk"
license = "Apache-2.0"
edition = "2021"
rust-version = "1.62"
rust-version = { workspace = true }
readme = "README.md"
[package.metadata.docs.rs]

View File

@@ -530,10 +530,7 @@ impl IndexeddbCryptoStore {
for user_id in user_ids.iter() {
let dirty: bool =
!matches!(os.get(&user_id)?.await?.map(|v| v.into_serde()), Some(Ok(false)));
let user = match user_id.as_string().map(UserId::parse) {
Some(Ok(user)) => user,
_ => continue,
};
let Some(Ok(user)) = user_id.as_string().map(UserId::parse) else { continue };
self.tracked_users_cache.insert(user.clone());
if dirty {

View File

@@ -793,10 +793,7 @@ impl IndexeddbStateStore {
for (room_id, redactions) in &changes.redactions {
let range = self.encode_to_range(KEYS::ROOM_STATE, room_id)?;
let cursor = match state.open_cursor_with_range(&range)?.await? {
Some(c) => c,
_ => continue,
};
let Some(cursor) = state.open_cursor_with_range(&range)?.await? else { continue };
let mut room_version = None;

View File

@@ -8,7 +8,7 @@ homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
readme = "README.md"
rust-version = "1.62"
rust-version = { workspace = true }
repository = "https://github.com/matrix-org/matrix-rust-sdk"
[package.metadata.docs.rs]
@@ -19,7 +19,7 @@ rustdoc-args = ["--cfg", "docsrs"]
base64 = "0.13.0"
byteorder = "1.4.3"
qrcode = { version = "0.12.0", default-features = false }
ruma-common = "0.10.0"
ruma-common = { workspace = true }
thiserror = "1.0.30"
vodozemac = { workspace = true }

View File

@@ -6,7 +6,7 @@ authors = ["Damir Jelić <poljar@termina.org.uk>"]
repository = "https://github.com/matrix-org/matrix-rust-sdk"
description = "Sled Storage backend for matrix-sdk for native environments"
license = "Apache-2.0"
rust-version = "1.62"
rust-version = { workspace = true }
readme = "README.md"
[package.metadata.docs.rs]

View File

@@ -170,11 +170,18 @@ const ALL_GLOBAL_KEYS: &[&str] = &[VERSION_KEY];
type Result<A, E = SledStoreError> = std::result::Result<A, E>;
#[derive(Builder, Debug, PartialEq, Eq)]
#[derive(Debug, Clone)]
enum DbOrPath {
Db(Db),
Path(PathBuf),
}
#[derive(Builder, Debug)]
#[builder(name = "SledStateStoreBuilder", build_fn(skip))]
#[allow(dead_code)]
pub struct SledStateStoreBuilderConfig {
/// Path to the sled store files, created if not yet existing
path: PathBuf,
#[builder(setter(custom))]
db_or_path: DbOrPath,
/// Set the password the sled store is encrypted with (if any)
passphrase: String,
/// The strategy to use when a merge conflict is found, see
@@ -184,22 +191,47 @@ pub struct SledStateStoreBuilderConfig {
}
impl SledStateStoreBuilder {
/// Path to the sled store files, created if not it doesn't exist yet.
///
/// Mutually exclusive with [`db`][Self::db], whichever is called last wins.
pub fn path(&mut self, path: PathBuf) -> &mut SledStateStoreBuilder {
self.db_or_path = Some(DbOrPath::Path(path));
self
}
/// Use the given [`sled::Db`].
///
/// Mutually exclusive with [`path`][Self::path], whichever is called last
/// wins.
pub fn db(&mut self, db: Db) -> &mut SledStateStoreBuilder {
self.db_or_path = Some(DbOrPath::Db(db));
self
}
/// Create a [`SledStateStore`] with the options set on this builder.
///
/// # Errors
///
/// This method can fail for two general reasons:
///
/// * Invalid path: The [`sled::Db`] could not be opened at the supplied
/// path.
/// * Migration error: The migration to a newer version of the schema
/// failed, see `SledStoreError::MigrationConflict`.
pub fn build(&mut self) -> Result<SledStateStore> {
let is_temp = self.path.is_none();
let mut cfg = Config::new().temporary(is_temp);
let path = if let Some(path) = &self.path {
let path = path.join("matrix-sdk-state");
cfg = cfg.path(&path);
Some(path)
} else {
None
let (db, path) = match &self.db_or_path {
None => {
let db = Config::new().temporary(true).open().map_err(StoreError::backend)?;
(db, None)
}
Some(DbOrPath::Db(db)) => (db.clone(), None),
Some(DbOrPath::Path(path)) => {
let path = path.join("matrix-sdk-state");
let db = Config::new().path(&path).open().map_err(StoreError::backend)?;
(db, Some(path))
}
};
let db = cfg.open().map_err(StoreError::backend)?;
let store_cipher = if let Some(passphrase) = &self.passphrase {
if let Some(inner) = db.get("store_cipher".encode())? {
Some(StoreCipher::import(passphrase, &inner)?.into())

View File

@@ -5,7 +5,7 @@ edition = "2021"
description = "Helpers for encrypted storage keys for the Matrix SDK"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
license = "Apache-2.0"
rust-version = "1.62"
rust-version = { workspace = true }
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = "1.62"
rust-version = { workspace = true }
version = "0.6.2"
[package.metadata.docs.rs]
@@ -42,8 +42,7 @@ appservice = ["ruma/appservice-api-s"]
image-proc = ["dep:image"]
image-rayon = ["image-proc", "image?/jpeg_rayon"]
experimental-room-preview = []
experimental-timeline = ["ruma/unstable-msc2676", "ruma/unstable-msc2677"]
experimental-timeline = ["ruma/unstable-msc2677"]
sliding-sync = [
"matrix-sdk-base/sliding-sync",

View File

@@ -645,7 +645,7 @@ impl Account {
/// if let Some(raw_content) = maybe_content {
/// let content = raw_content.deserialize()?;
/// println!("Ignored users:");
/// for user_id in content.ignored_users {
/// for user_id in content.ignored_users.keys() {
/// println!("- {user_id}");
/// }
/// }
@@ -676,7 +676,8 @@ impl Account {
/// # let client = Client::new("http://localhost:8080".parse()?).await?;
/// # let account = client.account();
/// use matrix_sdk::ruma::{
/// events::ignored_user_list::IgnoredUserListEventContent, user_id,
/// events::ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
/// user_id,
/// };
///
/// let mut content = account
@@ -685,7 +686,9 @@ impl Account {
/// .map(|c| c.deserialize())
/// .transpose()?
/// .unwrap_or_default();
/// content.ignored_users.push(user_id!("@foo:bar.com").to_owned());
/// content
/// .ignored_users
/// .insert(user_id!("@foo:bar.com").to_owned(), IgnoredUser::new());
/// account.set_account_data(content).await?;
/// # anyhow::Ok(()) };
/// ```

View File

@@ -380,7 +380,7 @@ impl ClientBuilder {
if let Some(issuer) = well_known.authentication.map(|auth| auth.issuer) {
authentication_issuer = Url::parse(&issuer).ok();
};
}
well_known.homeserver.base_url
}
@@ -400,6 +400,7 @@ impl ClientBuilder {
#[cfg(feature = "e2e-encryption")]
key_claim_lock: Default::default(),
members_request_locks: Default::default(),
encryption_state_request_locks: Default::default(),
typing_notice_times: Default::default(),
event_handlers: Default::default(),
notification_handlers: Default::default(),

View File

@@ -27,8 +27,8 @@ use dashmap::DashMap;
use futures_core::stream::Stream;
use futures_signals::signal::Signal;
use matrix_sdk_base::{
deserialized_responses::SyncResponse, BaseClient, SendOutsideWasm, Session, SessionMeta,
SessionTokens, StateStore, SyncOutsideWasm,
BaseClient, RoomType, SendOutsideWasm, Session, SessionMeta, SessionTokens, StateStore,
SyncOutsideWasm,
};
use matrix_sdk_common::{
instant::Instant,
@@ -81,7 +81,9 @@ use crate::{
EventHandler, EventHandlerDropGuard, EventHandlerHandle, EventHandlerStore, SyncEvent,
},
http_client::HttpClient,
room, Account, Error, Media, RefreshTokenError, Result, RumaApiError,
room,
sync::SyncResponse,
Account, Error, Media, RefreshTokenError, Result, RumaApiError,
};
mod builder;
@@ -147,6 +149,8 @@ pub(crate) struct ClientInner {
#[cfg(feature = "e2e-encryption")]
pub(crate) key_claim_lock: Mutex<()>,
pub(crate) members_request_locks: DashMap<OwnedRoomId, Arc<Mutex<()>>>,
/// Locks for requests on the encryption state of rooms.
pub(crate) encryption_state_request_locks: DashMap<OwnedRoomId, Arc<Mutex<()>>>,
pub(crate) typing_notice_times: DashMap<OwnedRoomId, Instant>,
/// Event handlers. See `add_event_handler`.
pub(crate) event_handlers: EventHandlerStore,
@@ -308,11 +312,8 @@ impl Client {
/// The OIDC Provider that is trusted by the homeserver.
pub async fn authentication_issuer(&self) -> Option<Url> {
if let Some(server) = &self.inner.authentication_issuer {
Some(server.read().await.clone())
} else {
None
}
let server = self.inner.authentication_issuer.as_ref()?;
Some(server.read().await.clone())
}
fn session_meta(&self) -> Option<&SessionMeta> {
@@ -1310,11 +1311,8 @@ impl Client {
let lock = self.inner.refresh_token_lock.try_lock();
if let Some(mut guard) = lock {
let mut session_tokens = if let Some(tokens) = self.session_tokens() {
tokens
} else {
let Some(mut session_tokens) = self.session_tokens() else {
*guard = Err(RefreshTokenError::RefreshTokenRequired);
return Err(RefreshTokenError::RefreshTokenRequired.into());
};
@@ -1496,12 +1494,11 @@ impl Client {
/// # Arguments
///
/// * `room_id` - The `RoomId` of the room to be joined.
pub async fn join_room_by_id(
&self,
room_id: &RoomId,
) -> HttpResult<join_room_by_id::v3::Response> {
pub async fn join_room_by_id(&self, room_id: &RoomId) -> Result<room::Joined> {
let request = join_room_by_id::v3::Request::new(room_id);
self.send(request, None).await
let response = self.send(request, None).await?;
let base_room = self.base_client().room_joined(&response.room_id).await?;
room::Joined::new(self, base_room).ok_or(Error::InconsistentState)
}
/// Join a room by `RoomId`.
@@ -1517,11 +1514,13 @@ impl Client {
&self,
alias: &RoomOrAliasId,
server_names: &[OwnedServerName],
) -> HttpResult<join_room_by_id_or_alias::v3::Response> {
) -> Result<room::Joined> {
let request = assign!(join_room_by_id_or_alias::v3::Request::new(alias), {
server_name: server_names,
});
self.send(request, None).await
let response = self.send(request, None).await?;
let base_room = self.base_client().room_joined(&response.room_id).await?;
room::Joined::new(self, base_room).ok_or(Error::InconsistentState)
}
/// Search the homeserver's directory of public rooms.
@@ -1601,9 +1600,13 @@ impl Client {
pub async fn create_room(
&self,
room: impl Into<create_room::v3::Request<'_>>,
) -> HttpResult<create_room::v3::Response> {
) -> HttpResult<room::Joined> {
let request = room.into();
self.send(request, None).await
let response = self.send(request, None).await?;
let base_room =
self.base_client().get_or_create_room(&response.room_id, RoomType::Joined).await;
Ok(room::Joined::new(self, base_room).unwrap())
}
/// Search the homeserver's directory for public rooms with a filter.
@@ -1699,26 +1702,21 @@ impl Client {
// If this is an `M_UNKNOWN_TOKEN` error and refresh token handling is active,
// try to refresh the token and retry the request.
if self.inner.handle_refresh_tokens {
// FIXME: Use if-let chain once available
if let Err(Some(RumaApiError::ClientApi(error))) =
res.as_ref().map_err(HttpError::as_ruma_api_error)
if let Err(Some(ErrorKind::UnknownToken { .. })) =
res.as_ref().map_err(HttpError::client_api_error_kind)
{
if matches!(error.kind, ErrorKind::UnknownToken { .. }) {
let refresh_res = self.refresh_access_token().await;
if let Err(refresh_error) = refresh_res {
match &refresh_error {
HttpError::RefreshToken(RefreshTokenError::RefreshTokenRequired) => {
// Refreshing access tokens is not supported by
// this `Session`, ignore.
}
_ => {
return Err(refresh_error);
}
if let Err(refresh_error) = self.refresh_access_token().await {
match &refresh_error {
HttpError::RefreshToken(RefreshTokenError::RefreshTokenRequired) => {
// Refreshing access tokens is not supported by
// this `Session`, ignore.
}
_ => {
return Err(refresh_error);
}
} else {
return self.send_inner(request, config, None).await;
}
} else {
return self.send_inner(request, config, None).await;
}
}
}
@@ -1744,26 +1742,21 @@ impl Client {
// If this is an `M_UNKNOWN_TOKEN` error and refresh token handling is active,
// try to refresh the token and retry the request.
if self.inner.handle_refresh_tokens {
// FIXME: Use if-let chain once available
if let Err(Some(RumaApiError::ClientApi(error))) =
res.as_ref().map_err(HttpError::as_ruma_api_error)
if let Err(Some(ErrorKind::UnknownToken { .. })) =
res.as_ref().map_err(HttpError::client_api_error_kind)
{
if matches!(error.kind, ErrorKind::UnknownToken { .. }) {
let refresh_res = self.refresh_access_token().await;
if let Err(refresh_error) = refresh_res {
match &refresh_error {
HttpError::RefreshToken(RefreshTokenError::RefreshTokenRequired) => {
// Refreshing access tokens is not supported by
// this `Session`, ignore.
}
_ => {
return Err(refresh_error);
}
if let Err(refresh_error) = self.refresh_access_token().await {
match &refresh_error {
HttpError::RefreshToken(RefreshTokenError::RefreshTokenRequired) => {
// Refreshing access tokens is not supported by
// this `Session`, ignore.
}
_ => {
return Err(refresh_error);
}
} else {
return self.send_inner(request, config, homeserver).await;
}
} else {
return self.send_inner(request, config, homeserver).await;
}
}
}
@@ -1782,8 +1775,11 @@ impl Client {
Request: OutgoingRequest + Debug,
HttpError: From<FromHttpResponseError<Request::EndpointError>>,
{
let homeserver =
if let Some(h) = homeserver { h } else { self.homeserver().await.to_string() };
let homeserver = match homeserver {
Some(hs) => hs,
None => self.homeserver().await.to_string(),
};
self.inner
.http_client
.send(
@@ -1876,13 +1872,7 @@ impl Client {
///
/// ```no_run
/// # use matrix_sdk::{
/// # ruma::{
/// # api::{
/// # client::uiaa,
/// # error::{FromHttpResponseError, ServerError},
/// # },
/// # device_id,
/// # },
/// # ruma::{api::client::uiaa, device_id},
/// # Client, Error, config::SyncSettings,
/// # };
/// # use futures::executor::block_on;
@@ -2042,6 +2032,7 @@ impl Client {
}
let response = self.send(request, Some(request_config)).await?;
let next_batch = response.next_batch.clone();
let response = self.process_sync(response).await?;
#[cfg(feature = "e2e-encryption")]
@@ -2051,7 +2042,7 @@ impl Client {
self.inner.sync_beat.notify(usize::MAX);
Ok(response)
Ok(SyncResponse::new(next_batch, response))
}
/// Repeatedly synchronize the client state with the server.
@@ -2347,7 +2338,7 @@ impl Client {
/// Get the current, if any, sync token of the client.
/// This will be None if the client didn't sync at least once.
pub async fn sync_token(&self) -> Option<String> {
pub(crate) async fn sync_token(&self) -> Option<String> {
self.inner.base_client.sync_token().await
}

View File

@@ -164,9 +164,9 @@ Please note that, unless a client is specifically set up to ignore
unverified devices, verifying devices is **not** necessary for encryption
to work.
1. Make sure the `encryption` feature is enabled.
1. Make sure the `e2e-encryption` feature is enabled.
2. To persist the encryption keys, you can use [`ClientBuilder::store_config`]
or of the other `_store` methods on [`ClientBuilder`].
or one of the other `_store` methods on [`ClientBuilder`].
## Restoring a client
@@ -215,7 +215,7 @@ is **not** supported using the default store.
| Failure | Cause | Fix |
| ------------------- | ----- | ----------- |
| No messages get encrypted nor decrypted | The `encryption` feature is disabled | [Enable the feature in your `Cargo.toml` file] |
| No messages get encrypted nor decrypted | The `e2e-encryption` feature is disabled | [Enable the feature in your `Cargo.toml` file] |
| Messages that were decryptable aren't after a restart | Storage isn't setup to be persistent | Ensure you've activated the persistent storage backend feature, e.g. `sled` |
| Messages are encrypted but can't be decrypted | The access token that the client is using is tied to another device | Clear storage to create a new device, read the [Restoring a Client] section |
| Messages don't get encrypted but get decrypted | The `m.room.encryption` event is missing | Make sure encryption is [enabled] for the room and the event isn't [filtered] out, otherwise it might be a deserialization bug |

View File

@@ -475,12 +475,8 @@ impl OtherUserIdentity {
room.invite_user_by_id(self.inner.user_id()).await?;
}
room.clone()
} else if let Some(room) =
self.client.create_dm_room(self.inner.user_id().to_owned()).await?
{
room
} else {
return Err(RequestVerificationError::RoomCreation(self.inner.user_id().to_owned()));
self.client.create_dm_room(self.inner.user_id().to_owned()).await?
};
let response = room

View File

@@ -38,7 +38,6 @@ pub use matrix_sdk_base::crypto::{
use matrix_sdk_base::crypto::{
CrossSigningStatus, OutgoingRequest, RoomMessageRequest, ToDeviceRequest,
};
use matrix_sdk_common::instant::Duration;
#[cfg(feature = "e2e-encryption")]
use ruma::OwnedDeviceId;
use ruma::{
@@ -227,16 +226,11 @@ impl Client {
}
#[cfg(feature = "e2e-encryption")]
pub(crate) async fn create_dm_room(
&self,
user_id: OwnedUserId,
) -> Result<Option<room::Joined>> {
pub(crate) async fn create_dm_room(&self, user_id: OwnedUserId) -> Result<room::Joined> {
use ruma::{
api::client::room::create_room::v3::RoomPreset, events::direct::DirectEventContent,
};
const SYNC_WAIT_TIME: Duration = Duration::from_secs(3);
// First we create the DM room, where we invite the user and tell the
// invitee that the room should be a DM.
let invite = &[user_id.clone()];
@@ -247,7 +241,7 @@ impl Client {
preset: Some(RoomPreset::TrustedPrivateChat),
});
let response = self.send(request, None).await?;
let room = self.create_room(request).await?;
// Now we need to mark the room as a DM for ourselves, we fetch the
// existing `m.direct` event and append the room to the list of DMs we
@@ -260,21 +254,14 @@ impl Client {
.transpose()?
.unwrap_or_default();
content.entry(user_id.to_owned()).or_default().push(response.room_id.to_owned());
content.entry(user_id.to_owned()).or_default().push(room.room_id().to_owned());
// TODO We should probably save the fact that we need to send this out
// because otherwise we might end up in a state where we have a DM that
// isn't marked as one.
self.account().set_account_data(content).await?;
// If the room is already in our store, fetch it, otherwise wait for a
// sync to be done which should put the room into our store.
if let Some(room) = self.get_joined_room(&response.room_id) {
Ok(Some(room))
} else {
self.inner.sync_beat.listen().wait_timeout(SYNC_WAIT_TIME);
Ok(self.get_joined_room(&response.room_id))
}
Ok(room)
}
/// Claim one-time keys creating new Olm sessions.
@@ -492,11 +479,8 @@ impl Encryption {
/// This can be used to check which private cross signing keys we have
/// stored locally.
pub async fn cross_signing_status(&self) -> Option<CrossSigningStatus> {
if let Some(machine) = self.client.olm_machine() {
Some(machine.cross_signing_status().await)
} else {
None
}
let machine = self.client.olm_machine()?;
Some(machine.cross_signing_status().await)
}
/// Get all the tracked users we know about
@@ -575,12 +559,9 @@ impl Encryption {
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<Device>, CryptoStoreError> {
if let Some(machine) = self.client.olm_machine() {
let device = machine.get_device(user_id, device_id, None).await?;
Ok(device.map(|d| Device { inner: d, client: self.client.clone() }))
} else {
Ok(None)
}
let Some(machine) = self.client.olm_machine() else { return Ok(None) };
let device = machine.get_device(user_id, device_id, None).await?;
Ok(device.map(|d| Device { inner: d, client: self.client.clone() }))
}
/// Get a map holding all the devices of an user.
@@ -656,20 +637,17 @@ impl Encryption {
) -> Result<Option<crate::encryption::identities::UserIdentity>, CryptoStoreError> {
use crate::encryption::identities::UserIdentity;
if let Some(olm) = self.client.olm_machine() {
let identity = olm.get_identity(user_id, None).await?;
let Some(olm) = self.client.olm_machine() else { return Ok(None) };
let identity = olm.get_identity(user_id, None).await?;
Ok(identity.map(|i| match i {
matrix_sdk_base::crypto::UserIdentities::Own(i) => {
UserIdentity::new_own(self.client.clone(), i)
}
matrix_sdk_base::crypto::UserIdentities::Other(i) => {
UserIdentity::new(self.client.clone(), i, self.client.get_dm_room(user_id))
}
}))
} else {
Ok(None)
}
Ok(identity.map(|i| match i {
matrix_sdk_base::crypto::UserIdentities::Own(i) => {
UserIdentity::new_own(self.client.clone(), i)
}
matrix_sdk_base::crypto::UserIdentities::Other(i) => {
UserIdentity::new(self.client.clone(), i, self.client.get_dm_room(user_id))
}
}))
}
/// Create and upload a new cross signing identity.
@@ -873,7 +851,7 @@ mod tests {
};
use serde_json::json;
use wiremock::{
matchers::{method, path_regex},
matchers::{header, method, path_regex},
Mock, MockServer, ResponseTemplate,
};
@@ -887,6 +865,16 @@ mod tests {
let event_id = event_id!("$2:example.org");
let room_id = &test_json::DEFAULT_SYNC_ROOM_ID;
Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?"))
.and(header("authorization", "Bearer 1234"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(&*test_json::sync_events::ENCRYPTION_CONTENT),
)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m%2Ereaction/.*".to_owned()))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
@@ -907,7 +895,7 @@ mod tests {
client.base_client().receive_sync_response(response).await.unwrap();
let room = client.get_joined_room(room_id).expect("Room should exist");
assert!(room.is_encrypted());
assert!(room.is_encrypted().await.expect("Getting encryption state"));
let event_id = event_id!("$1:example.org");
let reaction = ReactionEventContent::new(Relation::new(event_id.into(), "🐈".to_owned()));

View File

@@ -141,26 +141,20 @@ impl VerificationRequest {
/// for the remainder of the verification flow.
#[cfg(feature = "qrcode")]
pub async fn scan_qr_code(&self, data: QrVerificationData) -> Result<Option<QrVerification>> {
if let Some(qr) = self.inner.scan_qr_code(data).await? {
if let Some(request) = qr.reciprocate() {
self.client.send_verification_request(request).await?;
}
Ok(Some(QrVerification { inner: qr, client: self.client.clone() }))
} else {
Ok(None)
let Some(qr) = self.inner.scan_qr_code(data).await? else { return Ok(None) };
if let Some(request) = qr.reciprocate() {
self.client.send_verification_request(request).await?;
}
Ok(Some(QrVerification { inner: qr, client: self.client.clone() }))
}
/// Transition from this verification request into a SAS verification flow.
pub async fn start_sas(&self) -> Result<Option<SasVerification>> {
if let Some((sas, request)) = self.inner.start_sas().await? {
self.client.send_verification_request(request).await?;
let Some((sas, request)) = self.inner.start_sas().await? else { return Ok(None) };
self.client.send_verification_request(request).await?;
Ok(Some(SasVerification { inner: sas, client: self.client.clone() }))
} else {
Ok(None)
}
Ok(Some(SasVerification { inner: sas, client: self.client.clone() }))
}
/// Cancel the verification request

View File

@@ -16,7 +16,6 @@
use std::io::Error as IoError;
use http::StatusCode;
#[cfg(feature = "qrcode")]
use matrix_sdk_base::crypto::ScanError;
#[cfg(feature = "e2e-encryption")]
@@ -28,7 +27,7 @@ use reqwest::Error as ReqwestError;
use ruma::{
api::{
client::uiaa::{UiaaInfo, UiaaResponse},
error::{FromHttpResponseError, IntoHttpError, ServerError},
error::{FromHttpResponseError, IntoHttpError},
},
events::tag::InvalidUserTagName,
IdParseError,
@@ -103,10 +102,6 @@ pub enum HttpError {
#[error(transparent)]
IntoHttp(#[from] IntoHttpError),
/// The server returned a status code that should be retried.
#[error("Server returned an error {0}")]
Server(StatusCode),
/// The given request can't be cloned and thus can't be retried.
#[error("The request cannot be cloned")]
UnableToCloneRequest,
@@ -119,13 +114,13 @@ pub enum HttpError {
#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
impl HttpError {
/// If `self` is
/// <code>[Api](Self::Api)([Server](FromHttpResponseError::Server)([Known](ServerError::Known)(e)))</code>,
/// <code>[Api](Self::Api)([Server](FromHttpResponseError::Server)(e))</code>,
/// returns `Some(e)`.
///
/// Otherwise, returns `None`.
pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> {
match self {
Self::Api(FromHttpResponseError::Server(ServerError::Known(e))) => Some(e),
Self::Api(FromHttpResponseError::Server(e)) => Some(e),
_ => None,
}
}
@@ -136,6 +131,15 @@ impl HttpError {
self.as_ruma_api_error().and_then(RumaApiError::as_client_api_error)
}
/// If `self` is a server error in the `errcode` + `error` format expected
/// for client-API endpoints, returns the error kind (`errcode`).
pub fn client_api_error_kind(&self) -> Option<&ruma::api::client::error::ErrorKind> {
self.as_client_api_error().and_then(|e| match &e.body {
ruma::api::client::error::ErrorBody::Standard { kind, .. } => Some(kind),
_ => None,
})
}
/// Try to destructure the error into an universal interactive auth info.
///
/// Some requests require universal interactive auth, doing such a request
@@ -238,6 +242,11 @@ pub enum Error {
#[error(transparent)]
SlidingSync(#[from] crate::sliding_sync::Error),
/// The client is in inconsistent state. This happens when we set a room to
/// a specific type, but then cannot get it in this type.
#[error("The internal client state is inconsistent.")]
InconsistentState,
/// An other error was raised
/// this might happen because encryption was enabled on the base-crate
/// but not here and that raised.
@@ -248,7 +257,7 @@ pub enum Error {
#[rustfmt::skip] // stop rustfmt breaking the `<code>` in docs across multiple lines
impl Error {
/// If `self` is
/// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)([Known](ServerError::Known)(e))))</code>,
/// <code>[Http](Self::Http)([Api](HttpError::Api)([Server](FromHttpResponseError::Server)(e)))</code>,
/// returns `Some(e)`.
///
/// Otherwise, returns `None`.
@@ -265,6 +274,15 @@ impl Error {
self.as_ruma_api_error().and_then(RumaApiError::as_client_api_error)
}
/// If `self` is a server error in the `errcode` + `error` format expected
/// for client-API endpoints, returns the error kind (`errcode`).
pub fn client_api_error_kind(&self) -> Option<&ruma::api::client::error::ErrorKind> {
self.as_client_api_error().and_then(|e| match &e.body {
ruma::api::client::error::ErrorBody::Standard { kind, .. } => Some(kind),
_ => None,
})
}
/// Try to destructure the error into an universal interactive auth info.
///
/// Some requests require universal interactive auth, doing such a request
@@ -314,19 +332,19 @@ pub enum RoomKeyImportError {
impl From<FromHttpResponseError<ruma::api::client::Error>> for HttpError {
fn from(err: FromHttpResponseError<ruma::api::client::Error>) -> Self {
Self::Api(err.map(|e| e.map(RumaApiError::ClientApi)))
Self::Api(err.map(RumaApiError::ClientApi))
}
}
impl From<FromHttpResponseError<UiaaResponse>> for HttpError {
fn from(err: FromHttpResponseError<UiaaResponse>) -> Self {
Self::Api(err.map(|e| e.map(RumaApiError::Uiaa)))
Self::Api(err.map(RumaApiError::Uiaa))
}
}
impl From<FromHttpResponseError<ruma::api::error::MatrixError>> for HttpError {
fn from(err: FromHttpResponseError<ruma::api::error::MatrixError>) -> Self {
Self::Api(err.map(|e| e.map(RumaApiError::Other)))
Self::Api(err.map(RumaApiError::Other))
}
}

View File

@@ -0,0 +1,147 @@
use ruma::{
events::{
EventContent, MessageLikeEventContent, MessageLikeEventType, OriginalSyncMessageLikeEvent,
OriginalSyncStateEvent, RedactedEventContent, RedactedMessageLikeEventContent,
RedactedStateEventContent, RedactedSyncMessageLikeEvent, RedactedSyncStateEvent, Relations,
StateEventContent, StateEventType, StateUnsigned,
},
serde::from_raw_json_value,
EventId, MilliSecondsSinceUnixEpoch, TransactionId, UserId,
};
use serde::{de, Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
#[allow(clippy::large_enum_variant)]
pub(crate) enum SyncTimelineEventWithoutContent {
OriginalMessageLike(OriginalSyncMessageLikeEvent<NoMessageLikeEventContent>),
RedactedMessageLike(RedactedSyncMessageLikeEvent<NoMessageLikeEventContent>),
OriginalState(OriginalSyncStateEvent<NoStateEventContent>),
RedactedState(RedactedSyncStateEvent<NoStateEventContent>),
}
impl SyncTimelineEventWithoutContent {
pub(crate) fn event_id(&self) -> &EventId {
match self {
Self::OriginalMessageLike(ev) => &ev.event_id,
Self::RedactedMessageLike(ev) => &ev.event_id,
Self::OriginalState(ev) => &ev.event_id,
Self::RedactedState(ev) => &ev.event_id,
}
}
pub(crate) fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
match self {
SyncTimelineEventWithoutContent::OriginalMessageLike(ev) => ev.origin_server_ts,
SyncTimelineEventWithoutContent::RedactedMessageLike(ev) => ev.origin_server_ts,
SyncTimelineEventWithoutContent::OriginalState(ev) => ev.origin_server_ts,
SyncTimelineEventWithoutContent::RedactedState(ev) => ev.origin_server_ts,
}
}
pub(crate) fn relations(&self) -> Option<&Relations> {
match self {
SyncTimelineEventWithoutContent::OriginalMessageLike(ev) => {
ev.unsigned.relations.as_ref()
}
SyncTimelineEventWithoutContent::OriginalState(ev) => ev.unsigned.relations.as_ref(),
SyncTimelineEventWithoutContent::RedactedMessageLike(_)
| SyncTimelineEventWithoutContent::RedactedState(_) => None,
}
}
pub(crate) fn sender(&self) -> &UserId {
match self {
Self::OriginalMessageLike(ev) => &ev.sender,
Self::RedactedMessageLike(ev) => &ev.sender,
Self::OriginalState(ev) => &ev.sender,
Self::RedactedState(ev) => &ev.sender,
}
}
pub(crate) fn transaction_id(&self) -> Option<&TransactionId> {
match self {
SyncTimelineEventWithoutContent::OriginalMessageLike(ev) => {
ev.unsigned.transaction_id.as_deref()
}
SyncTimelineEventWithoutContent::OriginalState(ev) => {
ev.unsigned.transaction_id.as_deref()
}
SyncTimelineEventWithoutContent::RedactedMessageLike(_)
| SyncTimelineEventWithoutContent::RedactedState(_) => None,
}
}
}
#[derive(Deserialize)]
struct EventDeHelper {
state_key: Option<de::IgnoredAny>,
#[serde(default)]
unsigned: UnsignedDeHelper,
}
#[derive(Deserialize, Default)]
struct UnsignedDeHelper {
redacted_because: Option<de::IgnoredAny>,
}
impl<'de> Deserialize<'de> for SyncTimelineEventWithoutContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let EventDeHelper { state_key, unsigned } = from_raw_json_value(&json)?;
Ok(match (state_key.is_some(), unsigned.redacted_because.is_some()) {
(false, false) => Self::OriginalMessageLike(from_raw_json_value(&json)?),
(false, true) => Self::RedactedMessageLike(from_raw_json_value(&json)?),
(true, false) => Self::OriginalState(from_raw_json_value(&json)?),
(true, true) => Self::RedactedState(from_raw_json_value(&json)?),
})
}
}
#[derive(Serialize)]
pub(crate) struct NoMessageLikeEventContent {
#[serde(skip)]
pub event_type: MessageLikeEventType,
}
impl EventContent for NoMessageLikeEventContent {
type EventType = MessageLikeEventType;
fn event_type(&self) -> Self::EventType {
self.event_type.clone()
}
fn from_parts(event_type: &str, _content: &RawJsonValue) -> serde_json::Result<Self> {
Ok(Self { event_type: event_type.into() })
}
}
impl MessageLikeEventContent for NoMessageLikeEventContent {}
impl RedactedEventContent for NoMessageLikeEventContent {}
impl RedactedMessageLikeEventContent for NoMessageLikeEventContent {}
#[derive(Clone, Debug, Serialize)]
pub(crate) struct NoStateEventContent {
#[serde(skip)]
pub event_type: StateEventType,
}
impl EventContent for NoStateEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
self.event_type.clone()
}
fn from_parts(event_type: &str, _content: &RawJsonValue) -> serde_json::Result<Self> {
Ok(Self { event_type: event_type.into() })
}
}
impl StateEventContent for NoStateEventContent {
type StateKey = String;
type Unsigned = StateUnsigned<Self>;
}
impl RedactedEventContent for NoStateEventContent {}
impl RedactedStateEventContent for NoStateEventContent {}

View File

@@ -48,14 +48,13 @@ pub trait HttpSend: AsyncTraitDeps {
/// * `request` - The http request that has been converted from a ruma
/// `Request`.
///
/// * `request_config` - The config used for this request.
///
/// * `timeout` - A timeout for the full request > response cycle.
/// # Examples
///
/// ```
/// use matrix_sdk::{
/// async_trait, bytes::Bytes, config::RequestConfig, HttpError, HttpSend,
/// };
/// use std::time::Duration;
///
/// use matrix_sdk::{async_trait, bytes::Bytes, HttpError, HttpSend};
///
/// #[derive(Debug)]
/// struct Client(reqwest::Client);
@@ -75,7 +74,7 @@ pub trait HttpSend: AsyncTraitDeps {
/// async fn send_request(
/// &self,
/// request: http::Request<Bytes>,
/// config: RequestConfig,
/// timeout: Duration,
/// ) -> Result<http::Response<Bytes>, HttpError> {
/// Ok(self
/// .response_to_http_response(
@@ -90,7 +89,7 @@ pub trait HttpSend: AsyncTraitDeps {
async fn send_request(
&self,
request: http::Request<Bytes>,
config: RequestConfig,
timeout: Duration,
) -> Result<http::Response<Bytes>, HttpError>;
}
@@ -165,11 +164,63 @@ impl HttpClient {
let request = request.map(|body| body.freeze());
trace!("Sending request");
let response = self.inner.send_request(request, config).await?;
trace!("Got response: {:?}", response);
#[cfg(not(target_arch = "wasm32"))]
let response = {
use std::sync::atomic::{AtomicU64, Ordering};
use backoff::{future::retry, Error as RetryError, ExponentialBackoff};
use ruma::api::client::error::ErrorKind as ClientApiErrorKind;
let backoff =
ExponentialBackoff { max_elapsed_time: config.retry_timeout, ..Default::default() };
let retry_count = AtomicU64::new(1);
let send_request = || async {
let stop = if let Some(retry_limit) = config.retry_limit {
retry_count.fetch_add(1, Ordering::Relaxed) >= retry_limit
} else {
false
};
// Turn errors into permanent errors when the retry limit is reached
let error_type = if stop {
RetryError::Permanent
} else {
|err: HttpError| {
let retry_after = err.client_api_error_kind().and_then(|kind| match kind {
ClientApiErrorKind::LimitExceeded { retry_after_ms } => *retry_after_ms,
_ => None,
});
RetryError::Transient { err, retry_after }
}
};
let raw_response = self
.inner
.send_request(clone_request(&request), config.timeout)
.await
.map_err(error_type)?;
trace!("Got response: {raw_response:?}");
let response = Request::IncomingResponse::try_from_http_response(raw_response)
.map_err(|e| error_type(HttpError::from(e)))?;
Ok(response)
};
retry::<_, HttpError, _, _, _>(backoff, send_request).await?
};
#[cfg(target_arch = "wasm32")]
let response = {
let raw_response = self.inner.send_request(request, config.timeout).await?;
trace!("Got response: {raw_response:?}");
Request::IncomingResponse::try_from_http_response(raw_response)?
};
let response = Request::IncomingResponse::try_from_http_response(response)?;
Ok(response)
}
}
@@ -228,6 +279,18 @@ impl HttpSettings {
}
}
// Clones all request parts except the extensions which can't be cloned.
// See also https://github.com/hyperium/http/issues/395
#[cfg(not(target_arch = "wasm32"))]
fn clone_request(request: &http::Request<Bytes>) -> http::Request<Bytes> {
let mut builder = http::Request::builder()
.version(request.version())
.method(request.method())
.uri(request.uri());
*builder.headers_mut().unwrap() = request.headers().clone();
builder.body(request.body().clone()).unwrap()
}
async fn response_to_http_response(
mut response: Response,
) -> Result<http::Response<Bytes>, reqwest::Error> {
@@ -247,96 +310,25 @@ async fn response_to_http_response(
Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
}
#[cfg(any(target_arch = "wasm32"))]
async fn send_request(
client: &reqwest::Client,
request: http::Request<Bytes>,
_: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
let request = reqwest::Request::try_from(request)?;
let response = client.execute(request).await?;
Ok(response_to_http_response(response).await?)
}
#[cfg(all(not(target_arch = "wasm32")))]
async fn send_request(
client: &reqwest::Client,
request: http::Request<Bytes>,
config: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
use std::sync::atomic::{AtomicU64, Ordering};
use backoff::{future::retry, Error as RetryError, ExponentialBackoff};
use http::StatusCode;
use ruma::api::client::error::ErrorKind as ClientApiErrorKind;
let mut backoff = ExponentialBackoff::default();
let mut request = reqwest::Request::try_from(request)?;
let retry_limit = config.retry_limit;
let retry_count = AtomicU64::new(1);
*request.timeout_mut() = Some(config.timeout);
backoff.max_elapsed_time = config.retry_timeout;
let request = &request;
let retry_count = &retry_count;
let request = || async move {
let stop = if let Some(retry_limit) = retry_limit {
retry_count.fetch_add(1, Ordering::Relaxed) >= retry_limit
} else {
false
};
// Turn errors into permanent errors when the retry limit is reached
let error_type = if stop {
RetryError::Permanent
} else {
|err: HttpError| {
let retry_after = err.as_client_api_error().and_then(|e| match e.kind {
ClientApiErrorKind::LimitExceeded { retry_after_ms } => retry_after_ms,
_ => None,
});
RetryError::Transient { err, retry_after }
}
};
let request = request.try_clone().ok_or(HttpError::UnableToCloneRequest)?;
let response =
client.execute(request).await.map_err(|e| error_type(HttpError::Reqwest(e)))?;
let status_code = response.status();
// TODO TOO_MANY_REQUESTS will have a retry timeout which we should
// use.
if !stop
&& (status_code.is_server_error() || response.status() == StatusCode::TOO_MANY_REQUESTS)
{
return Err(error_type(HttpError::Server(status_code)));
}
let response = response_to_http_response(response)
.await
.map_err(|e| RetryError::Permanent(HttpError::Reqwest(e)))?;
Ok(response)
};
let response = retry(backoff, request).await?;
Ok(response)
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl HttpSend for reqwest::Client {
async fn send_request(
&self,
request: http::Request<Bytes>,
config: RequestConfig,
_timeout: Duration,
) -> Result<http::Response<Bytes>, HttpError> {
send_request(self, request, config).await
#[allow(unused_mut)]
let mut request = reqwest::Request::try_from(request)?;
// reqwest's timeout functionality is not available on WASM
#[cfg(not(target_arch = "wasm32"))]
{
*request.timeout_mut() = Some(_timeout);
}
let response = self.execute(request).await?;
Ok(response_to_http_response(response).await?)
}
}

View File

@@ -36,13 +36,15 @@ pub mod event_handler;
mod http_client;
pub mod media;
pub mod room;
mod sync;
pub mod sync;
#[cfg(feature = "sliding-sync")]
mod sliding_sync;
#[cfg(feature = "e2e-encryption")]
pub mod encryption;
#[cfg(feature = "experimental-timeline")]
mod events;
pub use account::Account;
#[cfg(feature = "sso-login")]

View File

@@ -115,50 +115,47 @@ impl Media {
if use_cache { self.client.store().get_media_content(request).await? } else { None };
if let Some(content) = content {
Ok(content)
} else {
let content: Vec<u8> = match &request.source {
MediaSource::Encrypted(file) => {
let request = get_content::v3::Request::from_url(&file.url)?;
let content: Vec<u8> = self.client.send(request, None).await?.file;
#[cfg(feature = "e2e-encryption")]
let content = {
let mut cursor = std::io::Cursor::new(content);
let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new(
&mut cursor,
file.as_ref().clone().into(),
)?;
let mut decrypted = Vec::new();
reader.read_to_end(&mut decrypted)?;
decrypted
};
content
}
MediaSource::Plain(uri) => {
if let MediaFormat::Thumbnail(size) = &request.format {
let request = get_content_thumbnail::v3::Request::from_url(
uri,
size.width,
size.height,
)?;
self.client.send(request, None).await?.file
} else {
let request = get_content::v3::Request::from_url(uri)?;
self.client.send(request, None).await?.file
}
}
};
if use_cache {
self.client.store().add_media_content(request, content.clone()).await?;
}
Ok(content)
return Ok(content);
}
let content: Vec<u8> = match &request.source {
MediaSource::Encrypted(file) => {
let request = get_content::v3::Request::from_url(&file.url)?;
let content: Vec<u8> = self.client.send(request, None).await?.file;
#[cfg(feature = "e2e-encryption")]
let content = {
let mut cursor = std::io::Cursor::new(content);
let mut reader = matrix_sdk_base::crypto::AttachmentDecryptor::new(
&mut cursor,
file.as_ref().clone().into(),
)?;
let mut decrypted = Vec::new();
reader.read_to_end(&mut decrypted)?;
decrypted
};
content
}
MediaSource::Plain(uri) => {
if let MediaFormat::Thumbnail(size) = &request.format {
let request =
get_content_thumbnail::v3::Request::from_url(uri, size.width, size.height)?;
self.client.send(request, None).await?.file
} else {
let request = get_content::v3::Request::from_url(uri)?;
self.client.send(request, None).await?.file
}
}
};
if use_cache {
self.client.store().add_media_content(request, content.clone()).await?;
}
Ok(content)
}
/// Remove a media file's content from the store.
@@ -200,17 +197,11 @@ impl Media {
event_content: impl MediaEventContent,
use_cache: bool,
) -> Result<Option<Vec<u8>>> {
if let Some(source) = event_content.source() {
Ok(Some(
self.get_media_content(
&MediaRequest { source, format: MediaFormat::File },
use_cache,
)
.await?,
))
} else {
Ok(None)
}
let Some(source) = event_content.source() else { return Ok(None) };
let file = self
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, use_cache)
.await?;
Ok(Some(file))
}
/// Remove the file of the given media event content from the cache.
@@ -223,7 +214,7 @@ impl Media {
/// * `event_content` - The media event content.
pub async fn remove_file(&self, event_content: impl MediaEventContent) -> Result<()> {
if let Some(source) = event_content.source() {
self.remove_media_content(&MediaRequest { source, format: MediaFormat::File }).await?
self.remove_media_content(&MediaRequest { source, format: MediaFormat::File }).await?;
}
Ok(())
@@ -253,17 +244,14 @@ impl Media {
size: MediaThumbnailSize,
use_cache: bool,
) -> Result<Option<Vec<u8>>> {
if let Some(source) = event_content.thumbnail_source() {
Ok(Some(
self.get_media_content(
&MediaRequest { source, format: MediaFormat::Thumbnail(size) },
use_cache,
)
.await?,
))
} else {
Ok(None)
}
let Some(source) = event_content.thumbnail_source() else { return Ok(None) };
let thumbnail = self
.get_media_content(
&MediaRequest { source, format: MediaFormat::Thumbnail(size) },
use_cache,
)
.await?;
Ok(Some(thumbnail))
}
/// Remove the thumbnail of the given media event content from the cache.

View File

@@ -3,6 +3,7 @@ use std::{borrow::Borrow, collections::BTreeMap, ops::Deref, sync::Arc};
use matrix_sdk_base::{
deserialized_responses::{MembersResponse, TimelineEvent},
store::StateStoreExt,
StateChanges,
};
use matrix_sdk_common::locks::Mutex;
#[cfg(feature = "e2e-encryption")]
@@ -13,18 +14,21 @@ use ruma::events::{
use ruma::{
api::client::{
config::set_global_account_data,
error::ErrorKind,
filter::RoomEventFilter,
membership::{get_member_events, join_room_by_id, leave_room},
message::get_message_events::{self, v3::Direction},
message::get_message_events,
room::get_room_event,
state::get_state_events_for_key,
tag::{create_tag, delete_tag},
Direction,
},
assign,
events::{
direct::DirectEventContent,
room::{
history_visibility::HistoryVisibility, server_acl::RoomServerAclEventContent,
MediaSource,
encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
server_acl::RoomServerAclEventContent, MediaSource,
},
tag::{TagInfo, TagName},
AnyRoomAccountDataEvent, AnyStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
@@ -39,10 +43,11 @@ use serde::de::DeserializeOwned;
#[cfg(feature = "experimental-timeline")]
use super::timeline::Timeline;
use super::Joined;
use crate::{
event_handler::{EventHandler, EventHandlerHandle, SyncEvent},
media::{MediaFormat, MediaRequest},
room::{RoomMember, RoomType},
room::{Left, RoomMember, RoomType},
BaseRoom, Client, Error, HttpError, HttpResult, Result,
};
@@ -95,21 +100,22 @@ impl Common {
/// Leave this room.
///
/// Only invited and joined rooms can be left.
pub(crate) async fn leave(&self) -> Result<()> {
pub(crate) async fn leave(&self) -> Result<Left> {
let request = leave_room::v3::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
self.client.send(request, None).await?;
Ok(())
let base_room = self.client.base_client().room_left(self.room_id()).await?;
Left::new(&self.client, base_room).ok_or(Error::InconsistentState)
}
/// Join this room.
///
/// Only invited and left rooms can be joined via this method.
pub(crate) async fn join(&self) -> Result<()> {
pub(crate) async fn join(&self) -> Result<Joined> {
let request = join_room_by_id::v3::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
let response = self.client.send(request, None).await?;
let base_room = self.client.base_client().room_joined(&response.room_id).await?;
Joined::new(&self.client, base_room).ok_or(Error::InconsistentState)
}
/// Get the inner client saved in this room instance.
@@ -119,6 +125,12 @@ impl Common {
self.client.clone()
}
/// Get the sync state of this room, i.e. whether it was fully synced with
/// the server.
pub fn is_synced(&self) -> bool {
self.inner.is_state_fully_synced()
}
/// Gets the avatar of this room, if set.
///
/// Returns the avatar.
@@ -149,12 +161,9 @@ impl Common {
/// # })
/// ```
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
if let Some(url) = self.avatar_url() {
let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format };
Ok(Some(self.client.media().get_media_content(&request, true).await?))
} else {
Ok(None)
}
let Some(url) = self.avatar_url() else { return Ok(None) };
let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format };
Ok(Some(self.client.media().get_media_content(&request, true).await?))
}
/// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and
@@ -314,6 +323,70 @@ impl Common {
}
}
async fn request_encryption_state(&self) -> Result<Option<RoomEncryptionEventContent>> {
if let Some(mutex) = self
.client
.inner
.encryption_state_request_locks
.get(self.inner.room_id())
.map(|m| m.clone())
{
// If a encryption state request is already going on, await the release of
// the lock.
_ = mutex.lock().await;
Ok(None)
} else {
let mutex = Arc::new(Mutex::new(()));
self.client
.inner
.encryption_state_request_locks
.insert(self.inner.room_id().to_owned(), mutex.clone());
let _guard = mutex.lock().await;
let request = get_state_events_for_key::v3::Request::new(
self.inner.room_id(),
StateEventType::RoomEncryption,
"",
);
let response = match self.client.send(request, None).await {
Ok(response) => {
Some(response.content.deserialize_as::<RoomEncryptionEventContent>()?)
}
Err(err) if err.client_api_error_kind() == Some(&ErrorKind::NotFound) => None,
Err(err) => return Err(err.into()),
};
let sync_lock = self.client.base_client().sync_lock().read().await;
let mut room_info = self.inner.clone_info();
room_info.mark_encryption_state_synced();
room_info.set_encryption_event(response.clone());
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.client.store().save_changes(&changes).await?;
self.update_summary(room_info);
drop(sync_lock);
self.client.inner.encryption_state_request_locks.remove(self.inner.room_id());
Ok(response)
}
}
/// Check whether this room is encrypted. If the room encryption state is
/// not synced yet, it will send a request to fetch it.
///
/// Returns true if the room is encrypted, otherwise false.
pub async fn is_encrypted(&self) -> Result<bool> {
if !self.is_encryption_state_synced() {
let encryption = self.request_encryption_state().await?;
Ok(encryption.is_some())
} else {
Ok(self.inner.is_encrypted())
}
}
async fn ensure_members(&self) -> Result<()> {
if !self.are_events_visible() {
return Ok(());

View File

@@ -2,6 +2,7 @@ use std::ops::Deref;
use thiserror::Error;
use super::{Joined, Left};
use crate::{
room::{Common, RoomMember},
BaseRoom, Client, Error, Result, RoomType,
@@ -52,12 +53,12 @@ impl Invited {
}
/// Reject the invitation.
pub async fn reject_invitation(&self) -> Result<()> {
pub async fn reject_invitation(&self) -> Result<Left> {
self.inner.leave().await
}
/// Accept the invitation.
pub async fn accept_invitation(&self) -> Result<()> {
pub async fn accept_invitation(&self) -> Result<Joined> {
self.inner.join().await
}

View File

@@ -35,6 +35,7 @@ use tracing::debug;
#[cfg(feature = "e2e-encryption")]
use tracing::instrument;
use super::Left;
use crate::{
attachment::AttachmentConfig, error::HttpResult, room::Common, BaseRoom, Client, Result,
RoomType,
@@ -83,7 +84,7 @@ impl Joined {
}
/// Leave this room.
pub async fn leave(&self) -> Result<()> {
pub async fn leave(&self) -> Result<Left> {
self.inner.leave().await
}
@@ -250,10 +251,10 @@ impl Joined {
fully_read: &EventId,
read_receipt: Option<&EventId>,
) -> Result<()> {
let request =
assign!(set_read_marker::v3::Request::new(self.inner.room_id(), fully_read), {
read_receipt
});
let request = assign!(set_read_marker::v3::Request::new(self.inner.room_id()), {
fully_read: Some(fully_read),
read_receipt,
});
self.client.send(request, None).await?;
Ok(())
@@ -296,7 +297,7 @@ impl Joined {
};
const SYNC_WAIT_TIME: Duration = Duration::from_secs(3);
if !self.is_encrypted() {
if !self.is_encrypted().await? {
let content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
self.send_state_event(content).await?;
@@ -382,6 +383,19 @@ impl Joined {
Ok(())
}
/// Wait for the room to be fully synced.
///
/// This method makes sure the room that was returned when joining a room
/// has been echoed back in the sync.
/// Warning: This waits until a sync happens and does not return if no sync
/// is happening! It can also return early when the room is not a joined
/// room anymore!
pub async fn sync_up(&self) {
while !self.is_synced() && self.room_type() == RoomType::Joined {
self.client.inner.sync_beat.listen().wait_timeout(Duration::from_secs(1));
}
}
/// Send a room message to this room.
///
/// Returns the parsed response from the server.
@@ -550,7 +564,7 @@ impl Joined {
};
#[cfg(feature = "e2e-encryption")]
let (content, event_type) = if self.is_encrypted() {
let (content, event_type) = if self.is_encrypted().await? {
// Reactions are currently famously not encrypted, skip encrypting
// them until they are.
if event_type == "m.reaction" {
@@ -729,7 +743,7 @@ impl Joined {
config: AttachmentConfig<'_>,
) -> Result<send_message_event::v3::Response> {
#[cfg(feature = "e2e-encryption")]
let content = if self.is_encrypted() {
let content = if self.is_encrypted().await? {
self.client
.prepare_encrypted_attachment_message(
body,

View File

@@ -2,6 +2,7 @@ use std::ops::Deref;
use ruma::api::client::membership::forget_room;
use super::Joined;
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
/// A room in the left state.
@@ -31,7 +32,7 @@ impl Left {
}
/// Join this room.
pub async fn join(&self) -> Result<()> {
pub async fn join(&self) -> Result<Joined> {
self.inner.join().await
}

View File

@@ -60,11 +60,8 @@ impl RoomMember {
/// # })
/// ```
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
if let Some(url) = self.avatar_url() {
let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format };
Ok(Some(self.client.media().get_media_content(&request, true).await?))
} else {
Ok(None)
}
let Some(url) = self.avatar_url() else { return Ok(None) };
let request = MediaRequest { source: MediaSource::Plain(url.to_owned()), format };
Ok(Some(self.client.media().get_media_content(&request, true).await?))
}
}

View File

@@ -12,23 +12,30 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeSet;
use std::{collections::HashMap, sync::Arc};
use futures_signals::signal_vec::MutableVecLockMut;
use indexmap::map::Entry;
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::crypto::OlmMachine;
use matrix_sdk_base::{deserialized_responses::EncryptionInfo, locks::MutexGuard};
#[cfg(feature = "e2e-encryption")]
use ruma::RoomId;
use ruma::{
events::{
fully_read::FullyReadEvent,
reaction::ReactionEventContent,
reaction::{ReactionEventContent, Relation as AnnotationRelation},
room::{
encrypted::{self, RoomEncryptedEventContent},
message::{self, Replacement, RoomMessageEventContent},
message::{self, MessageType, Replacement, RoomMessageEventContent},
redaction::{
OriginalSyncRoomRedactionEvent, RoomRedactionEventContent, SyncRoomRedactionEvent,
},
},
AnyMessageLikeEventContent, AnyStateEventContent, AnySyncMessageLikeEvent,
AnySyncTimelineEvent, Relations,
AnySyncTimelineEvent, MessageLikeEventType, Relations, StateEventType,
},
serde::Raw,
uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
@@ -38,27 +45,36 @@ use tracing::{debug, error, info, warn};
use super::{
event_item::{BundledReactions, TimelineDetails},
find_event, find_fully_read, EventTimelineItem, Message, TimelineInner, TimelineItem,
TimelineItemContent, TimelineKey, VirtualTimelineItem,
find_event, find_read_marker, EventTimelineItem, Message, TimelineInner, TimelineInnerMetadata,
TimelineItem, TimelineItemContent, TimelineKey, VirtualTimelineItem,
};
use crate::events::SyncTimelineEventWithoutContent;
impl TimelineInner {
pub(super) fn handle_live_event(
pub(super) async fn handle_live_event(
&self,
raw: Raw<AnySyncTimelineEvent>,
encryption_info: Option<EncryptionInfo>,
own_user_id: &UserId,
) {
self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::End)
let mut timeline_meta = self.metadata.lock().await;
handle_remote_event(
raw,
own_user_id,
encryption_info,
TimelineItemPosition::End,
&mut self.items.lock_mut(),
&mut timeline_meta,
);
}
pub(super) fn handle_local_event(
pub(super) async fn handle_local_event(
&self,
txn_id: OwnedTransactionId,
content: AnyMessageLikeEventContent,
own_user_id: &UserId,
) {
let meta = TimelineEventMetadata {
let event_meta = TimelineEventMetadata {
sender: own_user_id.to_owned(),
is_own_event: true,
relations: None,
@@ -69,54 +85,30 @@ impl TimelineInner {
let flow = Flow::Local { txn_id };
let kind = TimelineEventKind::Message { content };
TimelineEventHandler::new(meta, flow, self).handle_event(kind)
let mut timeline_meta = self.metadata.lock().await;
let mut timeline_items = self.items.lock_mut();
TimelineEventHandler::new(event_meta, flow, &mut timeline_items, &mut timeline_meta)
.handle_event(kind);
}
pub(super) fn handle_back_paginated_event(
pub(super) async fn handle_back_paginated_event(
&self,
raw: Raw<AnySyncTimelineEvent>,
encryption_info: Option<EncryptionInfo>,
own_user_id: &UserId,
) {
self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::Start)
}
fn handle_remote_event(
&self,
raw: Raw<AnySyncTimelineEvent>,
encryption_info: Option<EncryptionInfo>,
own_user_id: &UserId,
position: TimelineItemPosition,
) {
let event = match raw.deserialize() {
Ok(ev) => ev,
Err(_e) => {
// TODO: Add some sort of error timeline item
return;
}
};
let sender = event.sender().to_owned();
let is_own_event = sender == own_user_id;
let meta = TimelineEventMetadata {
sender,
is_own_event,
relations: event.relations().cloned(),
let mut metadata_lock = self.metadata.lock().await;
handle_remote_event(
raw,
own_user_id,
encryption_info,
};
let flow = Flow::Remote {
event_id: event.event_id().to_owned(),
origin_server_ts: event.origin_server_ts(),
raw_event: raw,
txn_id: event.transaction_id().map(ToOwned::to_owned),
position,
};
TimelineEventHandler::new(meta, flow, self).handle_event(event.into())
TimelineItemPosition::Start,
&mut self.items.lock_mut(),
&mut metadata_lock,
);
}
pub(super) fn handle_fully_read(&self, raw: Raw<FullyReadEvent>) {
pub(super) async fn handle_fully_read(&self, raw: Raw<FullyReadEvent>) {
let fully_read_event = match raw.deserialize() {
Ok(ev) => ev.content.event_id,
Err(error) => {
@@ -125,48 +117,170 @@ impl TimelineInner {
}
};
self.set_fully_read_event(fully_read_event);
self.set_fully_read_event(fully_read_event).await;
}
pub(super) fn set_fully_read_event(&self, fully_read_event: OwnedEventId) {
{
let mut fully_read_lock = self.fully_read_event.lock().unwrap();
pub(super) async fn set_fully_read_event(&self, fully_read_event_id: OwnedEventId) {
let mut metadata_lock = self.metadata.lock().await;
if fully_read_lock.as_ref() == Some(&fully_read_event) {
return;
}
*fully_read_lock = Some(fully_read_event);
if metadata_lock.fully_read_event.as_ref().map_or(false, |id| *id == fully_read_event_id) {
return;
}
self.update_fully_read_item();
}
fn update_fully_read_item(&self) {
let fully_read_lock = self.fully_read_event.lock().unwrap();
let fully_read_event = match &*fully_read_lock {
Some(event) => event,
None => return,
};
metadata_lock.fully_read_event = Some(fully_read_event_id);
let mut items_lock = self.items.lock_mut();
let old_idx = find_fully_read(&items_lock);
let new_idx = find_event(&items_lock, fully_read_event).map(|(idx, _)| idx + 1);
let metadata = &mut *metadata_lock;
update_read_marker(
&mut items_lock,
metadata.fully_read_event.as_deref(),
&mut metadata.fully_read_event_in_timeline,
);
}
match (old_idx, new_idx) {
(None, None) => {}
(None, Some(idx)) => {
*self.fully_read_event_in_timeline.lock().unwrap() = true;
let item = TimelineItem::Virtual(VirtualTimelineItem::ReadMarker);
items_lock.insert_cloned(idx, item.into());
}
(Some(_), None) => {
// Keep the current position of the read marker, hopefully we
// should have a new position later.
*self.fully_read_event_in_timeline.lock().unwrap() = false;
}
(Some(from), Some(to)) => {
#[cfg(feature = "e2e-encryption")]
pub(super) async fn retry_event_decryption(
&self,
room_id: &RoomId,
olm_machine: &OlmMachine,
session_ids: BTreeSet<&str>,
own_user_id: &UserId,
) {
use super::EncryptedMessage;
let utds_for_session: Vec<_> = self
.items
.lock_ref()
.iter()
.enumerate()
.filter_map(|(idx, item)| {
let event_item = &item.as_event()?;
let utd = event_item.content.as_unable_to_decrypt()?;
match utd {
EncryptedMessage::MegolmV1AesSha2 { session_id, .. }
if session_ids.contains(session_id.as_str()) =>
{
let TimelineKey::EventId(event_id) = &event_item.key else {
error!("Key for unable-to-decrypt timeline item is not an event ID");
return None;
};
let Some(raw) = event_item.raw.clone() else {
error!("No raw event in unable-to-decrypt timeline item");
return None;
};
Some((idx, event_id.to_owned(), session_id.to_owned(), raw))
}
EncryptedMessage::MegolmV1AesSha2 { .. }
| EncryptedMessage::OlmV1Curve25519AesSha2 { .. }
| EncryptedMessage::Unknown => None,
}
})
.collect();
if utds_for_session.is_empty() {
return;
}
let mut metadata_lock = self.metadata.lock().await;
for (idx, event_id, session_id, utd) in utds_for_session.iter().rev() {
let event = match olm_machine.decrypt_room_event(utd.cast_ref(), room_id).await {
Ok(ev) => ev,
Err(e) => {
info!(
%event_id, %session_id,
"Failed to decrypt event after receiving room key: {e}"
);
continue;
}
};
// Because metadata is always locked before we attempt to lock the
// items, this will never be contended.
// Because there is an `.await` in this loop, we have to re-lock
// this mutex every iteration because holding it across `.await`
// makes the future `!Send`, which makes it not event-handler-safe.
let mut items_lock = self.items.lock_mut();
handle_remote_event(
event.event.cast(),
own_user_id,
event.encryption_info,
TimelineItemPosition::Update(*idx),
&mut items_lock,
&mut metadata_lock,
);
}
}
}
fn handle_remote_event(
raw: Raw<AnySyncTimelineEvent>,
own_user_id: &UserId,
encryption_info: Option<EncryptionInfo>,
position: TimelineItemPosition,
timeline_items: &mut MutableVecLockMut<'_, Arc<TimelineItem>>,
timeline_meta: &mut MutexGuard<'_, TimelineInnerMetadata>,
) {
let (event_id, sender, origin_server_ts, txn_id, relations, event_kind) =
match raw.deserialize() {
Ok(event) => (
event.event_id().to_owned(),
event.sender().to_owned(),
event.origin_server_ts(),
event.transaction_id().map(ToOwned::to_owned),
event.relations().cloned(),
event.into(),
),
Err(e) => match raw.deserialize_as::<SyncTimelineEventWithoutContent>() {
Ok(event) => (
event.event_id().to_owned(),
event.sender().to_owned(),
event.origin_server_ts(),
event.transaction_id().map(ToOwned::to_owned),
event.relations().cloned(),
TimelineEventKind::failed_to_parse(event, e),
),
Err(e) => {
warn!("Failed to deserialize timeline event: {e}");
return;
}
},
};
let is_own_event = sender == own_user_id;
let event_meta = TimelineEventMetadata { sender, is_own_event, relations, encryption_info };
let flow = Flow::Remote { event_id, origin_server_ts, raw_event: raw, txn_id, position };
TimelineEventHandler::new(event_meta, flow, timeline_items, timeline_meta)
.handle_event(event_kind)
}
fn update_read_marker(
items_lock: &mut MutableVecLockMut<'_, Arc<TimelineItem>>,
fully_read_event: Option<&EventId>,
fully_read_event_in_timeline: &mut bool,
) {
let Some(fully_read_event) = fully_read_event else { return };
let read_marker_idx = find_read_marker(items_lock);
let fully_read_event_idx = find_event(items_lock, fully_read_event).map(|(idx, _)| idx);
match (read_marker_idx, fully_read_event_idx) {
(None, None) => {}
(None, Some(idx)) => {
*fully_read_event_in_timeline = true;
let item = TimelineItem::Virtual(VirtualTimelineItem::ReadMarker);
items_lock.insert_cloned(idx + 1, item.into());
}
(Some(_), None) => {
// Keep the current position of the read marker, hopefully we
// should have a new position later.
*fully_read_event_in_timeline = false;
}
(Some(from), Some(to)) => {
*fully_read_event_in_timeline = true;
// The read marker can't move backwards.
if from < to {
items_lock.move_from_to(from, to);
}
}
@@ -216,6 +330,56 @@ struct TimelineEventMetadata {
encryption_info: Option<EncryptionInfo>,
}
#[derive(Clone)]
enum TimelineEventKind {
Message {
content: AnyMessageLikeEventContent,
},
RedactedMessage,
Redaction {
redacts: OwnedEventId,
content: RoomRedactionEventContent,
},
// FIXME: Split further for state keys of different type
State {
_content: AnyStateEventContent,
},
RedactedState, // AnyRedactedStateEventContent
FailedToParseMessageLike {
event_type: MessageLikeEventType,
error: Arc<serde_json::Error>,
},
FailedToParseState {
event_type: StateEventType,
state_key: String,
error: Arc<serde_json::Error>,
},
}
impl TimelineEventKind {
fn failed_to_parse(event: SyncTimelineEventWithoutContent, error: serde_json::Error) -> Self {
let error = Arc::new(error);
match event {
SyncTimelineEventWithoutContent::OriginalMessageLike(ev) => {
Self::FailedToParseMessageLike { event_type: ev.content.event_type, error }
}
SyncTimelineEventWithoutContent::RedactedMessageLike(ev) => {
Self::FailedToParseMessageLike { event_type: ev.content.event_type, error }
}
SyncTimelineEventWithoutContent::OriginalState(ev) => Self::FailedToParseState {
event_type: ev.content.event_type,
state_key: ev.state_key,
error,
},
SyncTimelineEventWithoutContent::RedactedState(ev) => Self::FailedToParseState {
event_type: ev.content.event_type,
state_key: ev.state_key,
error,
},
}
}
}
impl From<AnySyncTimelineEvent> for TimelineEventKind {
fn from(event: AnySyncTimelineEvent) -> Self {
match event {
@@ -238,35 +402,43 @@ impl From<AnySyncTimelineEvent> for TimelineEventKind {
}
}
#[derive(Clone)]
enum TimelineEventKind {
Message { content: AnyMessageLikeEventContent },
RedactedMessage,
Redaction { redacts: OwnedEventId, content: RoomRedactionEventContent },
// FIXME: Split further for state keys of different type
State { _content: AnyStateEventContent },
RedactedState, // AnyRedactedStateEventContent
}
enum TimelineItemPosition {
Start,
End,
#[cfg(feature = "e2e-encryption")]
Update(usize),
}
// Bundles together a few things that are needed throughout the different stages
// of handling an event (figuring out whether it should update an existing
// timeline item, transforming that item or creating a new one, updating the
// reactive Vec).
struct TimelineEventHandler<'a> {
struct TimelineEventHandler<'a, 'i> {
meta: TimelineEventMetadata,
flow: Flow,
timeline: &'a TimelineInner,
timeline_items: &'a mut MutableVecLockMut<'i, Arc<TimelineItem>>,
reaction_map: &'a mut HashMap<TimelineKey, (OwnedUserId, AnnotationRelation)>,
fully_read_event: &'a mut Option<OwnedEventId>,
fully_read_event_in_timeline: &'a mut bool,
event_added: bool,
}
impl<'a> TimelineEventHandler<'a> {
fn new(meta: TimelineEventMetadata, flow: Flow, timeline: &'a TimelineInner) -> Self {
Self { meta, flow, timeline, event_added: false }
impl<'a, 'i> TimelineEventHandler<'a, 'i> {
fn new(
event_meta: TimelineEventMetadata,
flow: Flow,
timeline_items: &'a mut MutableVecLockMut<'i, Arc<TimelineItem>>,
timeline_meta: &'a mut TimelineInnerMetadata,
) -> Self {
Self {
meta: event_meta,
flow,
timeline_items,
reaction_map: &mut timeline_meta.reaction_map,
fully_read_event: &mut timeline_meta.fully_read_event,
fully_read_event_in_timeline: &mut timeline_meta.fully_read_event_in_timeline,
event_added: false,
}
}
fn handle_event(mut self, event_kind: TimelineEventKind) {
@@ -284,8 +456,15 @@ impl<'a> TimelineEventHandler<'a> {
TimelineEventKind::Redaction { redacts, content } => {
self.handle_redaction(redacts, content)
}
// TODO: State events
_ => {}
TimelineEventKind::State { .. } | TimelineEventKind::RedactedState => {
// TODO
}
TimelineEventKind::FailedToParseMessageLike { event_type, error } => {
self.add(NewEventTimelineItem::failed_to_parse_message_like(event_type, error));
}
TimelineEventKind::FailedToParseState { event_type, state_key, error } => {
self.add(NewEventTimelineItem::failed_to_parse_state(event_type, state_key, error));
}
}
if !self.event_added {
@@ -304,10 +483,10 @@ impl<'a> TimelineEventHandler<'a> {
}
}
fn handle_room_message_edit(&mut self, replacement: Replacement) {
fn handle_room_message_edit(&mut self, replacement: Replacement<MessageType>) {
let event_id = &replacement.event_id;
self.maybe_update_timeline_item(event_id, "edit", |item| {
maybe_update_timeline_item(self.timeline_items, event_id, "edit", |item| {
if self.meta.sender != item.sender() {
info!(
%event_id, original_sender = %item.sender(), edit_sender = %self.meta.sender,
@@ -319,10 +498,7 @@ impl<'a> TimelineEventHandler<'a> {
let msg = match &item.content {
TimelineItemContent::Message(msg) => msg,
TimelineItemContent::RedactedMessage => {
info!(
%event_id,
"Edit event applies to a redacted message, discarding"
);
info!(%event_id, "Edit event applies to a redacted message, discarding");
return None;
}
TimelineItemContent::UnableToDecrypt(_) => {
@@ -332,10 +508,18 @@ impl<'a> TimelineEventHandler<'a> {
);
return None;
}
TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. } => {
info!(
%event_id,
"Edit event applies to event that couldn't be parsed, discarding"
);
return None;
}
};
let content = TimelineItemContent::Message(Message {
msgtype: replacement.new_content.msgtype,
msgtype: replacement.new_content,
in_reply_to: msg.in_reply_to.clone(),
edited: true,
});
@@ -348,13 +532,8 @@ impl<'a> TimelineEventHandler<'a> {
fn handle_reaction(&mut self, c: ReactionEventContent) {
let event_id: &EventId = &c.relates_to.event_id;
// This lock should never be contended, same as the timeline item lock.
// If this is ever run in parallel for some reason though, make sure the
// reaction lock is held for the entire time of the timeline items being
// locked so these two things can't get out of sync.
let mut lock = self.timeline.reaction_map.lock().unwrap();
let did_update = self.maybe_update_timeline_item(event_id, "reaction", |item| {
let items = &mut *self.timeline_items;
let did_update = maybe_update_timeline_item(items, event_id, "reaction", |item| {
// Handling of reactions on redacted events is an open question.
// For now, ignore reactions on redacted events like Element does.
if let TimelineItemContent::RedactedMessage = item.content {
@@ -375,7 +554,7 @@ impl<'a> TimelineEventHandler<'a> {
});
if did_update {
lock.insert(self.flow.to_key(), (self.meta.sender.clone(), c.relates_to));
self.reaction_map.insert(self.flow.to_key(), (self.meta.sender.clone(), c.relates_to));
}
}
@@ -395,16 +574,15 @@ impl<'a> TimelineEventHandler<'a> {
fn handle_redaction(&mut self, redacts: OwnedEventId, _content: RoomRedactionEventContent) {
let mut did_update = false;
// Don't release this lock until after update_timeline_item.
// See first comment in handle_reaction for why.
let mut lock = self.timeline.reaction_map.lock().unwrap();
if let Some((sender, rel)) = lock.remove(&TimelineKey::EventId(redacts.clone())) {
did_update = self.maybe_update_timeline_item(&rel.event_id, "redaction", |item| {
if let Some((sender, rel)) =
self.reaction_map.remove(&TimelineKey::EventId(redacts.clone()))
{
let items = &mut *self.timeline_items;
did_update = maybe_update_timeline_item(items, &rel.event_id, "redaction", |item| {
let mut reactions = item.reactions.clone();
let mut details_entry = match reactions.bundled.entry(rel.key) {
Entry::Occupied(o) => o,
Entry::Vacant(_) => return None,
let Entry::Occupied(mut details_entry) = reactions.bundled.entry(rel.key) else {
return None;
};
let details = details_entry.get_mut();
details.count -= uint!(1);
@@ -414,17 +592,14 @@ impl<'a> TimelineEventHandler<'a> {
return Some(item.with_reactions(reactions));
}
let senders = match &mut details.senders {
TimelineDetails::Ready(senders) => senders,
_ => {
// FIXME: We probably want to support this somehow in
// the future, but right now it's not possible.
warn!(
"inconsistent state: shouldn't have a reaction_map entry for a \
timeline item with incomplete reactions"
);
return None;
}
let TimelineDetails::Ready(senders) = &mut details.senders else {
// FIXME: We probably want to support this somehow in
// the future, but right now it's not possible.
warn!(
"inconsistent state: shouldn't have a reaction_map entry for a \
timeline item with incomplete reactions"
);
return None;
};
if let Some(idx) = senders.iter().position(|s| *s == sender) {
@@ -454,7 +629,8 @@ impl<'a> TimelineEventHandler<'a> {
// Even if the event being redacted is a reaction (found in
// `reaction_map`), it can still be present in the timeline items
// directly with the raw event timeline feature (not yet implemented).
did_update |= self.update_timeline_item(&redacts, "redaction", |item| item.to_redacted());
let items = &mut *self.timeline_items;
did_update |= update_timeline_item(items, &redacts, "redaction", |item| item.to_redacted());
if !did_update {
// We will want to know this when debugging redaction issues.
@@ -479,17 +655,16 @@ impl<'a> TimelineEventHandler<'a> {
};
let item = Arc::new(TimelineItem::Event(item));
let mut lock = self.timeline.items.lock_mut();
match &self.flow {
Flow::Local { .. } => {
lock.push_cloned(item);
self.timeline_items.push_cloned(item);
}
Flow::Remote { txn_id, event_id, position, raw_event, .. } => {
if let Some(txn_id) = txn_id {
if let Some((idx, _old_item)) = find_event(&lock, txn_id) {
if let Some((idx, _old_item)) = find_event(self.timeline_items, txn_id) {
// TODO: Check whether anything is different about the
// old and new item?
lock.set_cloned(idx, item);
self.timeline_items.set_cloned(idx, item);
return;
} else {
warn!(
@@ -500,7 +675,7 @@ impl<'a> TimelineEventHandler<'a> {
}
}
if let Some((idx, old_item)) = find_event(&lock, event_id) {
if let Some((idx, old_item)) = find_event(self.timeline_items, event_id) {
warn!(
?item,
?old_item,
@@ -511,62 +686,57 @@ impl<'a> TimelineEventHandler<'a> {
// With /messages and /sync sometimes disagreeing on order
// of messages, we might want to change the position in some
// circumstances, but for now this should be good enough.
lock.set_cloned(idx, item);
self.timeline_items.set_cloned(idx, item);
return;
}
match position {
TimelineItemPosition::Start => lock.insert_cloned(0, item),
TimelineItemPosition::End => lock.push_cloned(item),
TimelineItemPosition::Start => self.timeline_items.insert_cloned(0, item),
TimelineItemPosition::End => self.timeline_items.push_cloned(item),
#[cfg(feature = "e2e-encryption")]
TimelineItemPosition::Update(idx) => self.timeline_items.set_cloned(*idx, item),
}
}
}
drop(lock);
// See if we got the event corresponding to the fully read marker now.
let fully_read_event_in_timeline =
*self.timeline.fully_read_event_in_timeline.lock().unwrap();
if !fully_read_event_in_timeline {
self.timeline.update_fully_read_item();
// See if we got the event corresponding to the read marker now.
if !*self.fully_read_event_in_timeline {
update_read_marker(
self.timeline_items,
self.fully_read_event.as_deref(),
self.fully_read_event_in_timeline,
);
}
}
}
/// Returns whether an update happened
fn maybe_update_timeline_item(
&self,
event_id: &EventId,
action: &str,
update: impl FnOnce(&EventTimelineItem) -> Option<EventTimelineItem>,
) -> bool {
// No point in trying to update items with relations when back-
// paginating, the event the relation applies to can't be processed yet.
if matches!(self.flow, Flow::Remote { position: TimelineItemPosition::Start, .. }) {
return false;
/// Returns whether an update happened
fn maybe_update_timeline_item(
timeline_items: &mut MutableVecLockMut<'_, Arc<TimelineItem>>,
event_id: &EventId,
action: &str,
update: impl FnOnce(&EventTimelineItem) -> Option<EventTimelineItem>,
) -> bool {
if let Some((idx, item)) = find_event(timeline_items, event_id) {
if let Some(new_item) = update(item) {
timeline_items.set_cloned(idx, Arc::new(TimelineItem::Event(new_item)));
return true;
}
let mut lock = self.timeline.items.lock_mut();
if let Some((idx, item)) = find_event(&lock, event_id) {
if let Some(new_item) = update(item) {
lock.set_cloned(idx, Arc::new(TimelineItem::Event(new_item)));
return true;
}
} else {
debug!(%event_id, "Timeline item not found, discarding {action}");
}
false
} else {
debug!(%event_id, "Timeline item not found, discarding {action}");
}
/// Returns whether an update happened
fn update_timeline_item(
&self,
event_id: &EventId,
action: &str,
update: impl FnOnce(&EventTimelineItem) -> EventTimelineItem,
) -> bool {
self.maybe_update_timeline_item(event_id, action, move |item| Some(update(item)))
}
false
}
/// Returns whether an update happened
fn update_timeline_item(
timeline_items: &mut MutableVecLockMut<'_, Arc<TimelineItem>>,
event_id: &EventId,
action: &str,
update: impl FnOnce(&EventTimelineItem) -> EventTimelineItem,
) -> bool {
maybe_update_timeline_item(timeline_items, event_id, action, move |item| Some(update(item)))
}
struct NewEventTimelineItem {
@@ -595,16 +765,29 @@ impl NewEventTimelineItem {
}
fn unable_to_decrypt(content: RoomEncryptedEventContent) -> Self {
Self {
content: TimelineItemContent::UnableToDecrypt(content.into()),
reactions: BundledReactions::default(),
}
Self::from_content(TimelineItemContent::UnableToDecrypt(content.into()))
}
fn redacted_message() -> Self {
Self {
content: TimelineItemContent::RedactedMessage,
reactions: BundledReactions::default(),
}
Self::from_content(TimelineItemContent::RedactedMessage)
}
fn failed_to_parse_message_like(
event_type: MessageLikeEventType,
error: Arc<serde_json::Error>,
) -> NewEventTimelineItem {
Self::from_content(TimelineItemContent::FailedToParseMessageLike { event_type, error })
}
fn failed_to_parse_state(
event_type: StateEventType,
state_key: String,
error: Arc<serde_json::Error>,
) -> NewEventTimelineItem {
Self::from_content(TimelineItemContent::FailedToParseState { event_type, state_key, error })
}
fn from_content(content: TimelineItemContent) -> Self {
Self { content, reactions: BundledReactions::default() }
}
}

View File

@@ -12,12 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt;
use std::{fmt, sync::Arc};
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
#[cfg(feature = "experimental-room-preview")]
use ruma::events::room::message::{OriginalSyncRoomMessageEvent, Relation};
use ruma::{
events::{
relation::{AnnotationChunk, AnnotationType},
@@ -25,7 +23,7 @@ use ruma::{
encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent},
message::MessageType,
},
AnySyncTimelineEvent,
AnySyncTimelineEvent, MessageLikeEventType, StateEventType,
},
serde::Raw,
uint, EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedTransactionId,
@@ -85,37 +83,6 @@ macro_rules! build {
}
impl EventTimelineItem {
#[cfg(feature = "experimental-room-preview")]
#[doc(hidden)] // FIXME: Remove. Used for matrix-sdk-ffi temporarily.
pub fn _new(ev: OriginalSyncRoomMessageEvent, raw: Raw<AnySyncTimelineEvent>) -> Self {
let edited = ev.unsigned.relations.as_ref().map_or(false, |r| r.replace.is_some());
let reactions = ev
.unsigned
.relations
.and_then(|r| r.annotation)
.map(BundledReactions::from)
.unwrap_or_default();
Self {
key: TimelineKey::EventId(ev.event_id),
event_id: None,
sender: ev.sender,
content: TimelineItemContent::Message(Message {
msgtype: ev.content.msgtype,
in_reply_to: ev.content.relates_to.and_then(|rel| match rel {
Relation::Reply { in_reply_to } => Some(in_reply_to.event_id),
_ => None,
}),
edited,
}),
reactions,
origin_server_ts: Some(ev.origin_server_ts),
is_own: false, // FIXME: Potentially wrong
encryption_info: None, // FIXME: Potentially wrong
raw: Some(raw),
}
}
/// Get the [`TimelineKey`] of this item.
pub fn key(&self) -> &TimelineKey {
&self.key
@@ -299,10 +266,31 @@ pub enum TimelineItemContent {
/// An `m.room.encrypted` event that could not be decrypted.
UnableToDecrypt(EncryptedMessage),
/// A message-like event that failed to deserialize.
FailedToParseMessageLike {
/// The event `type`.
event_type: MessageLikeEventType,
/// The deserialization error.
error: Arc<serde_json::Error>,
},
/// A state event that failed to deserialize.
FailedToParseState {
/// The event `type`.
event_type: StateEventType,
/// The state key.
state_key: String,
/// The deserialization error.
error: Arc<serde_json::Error>,
},
}
impl TimelineItemContent {
/// If `self` is of the [`Message`][Self:Message] variant, return the inner
/// If `self` is of the [`Message`][Self::Message] variant, return the inner
/// [`Message`].
pub fn as_message(&self) -> Option<&Message> {
match self {
@@ -310,6 +298,15 @@ impl TimelineItemContent {
_ => None,
}
}
/// If `self` is of the [`UnableToDecrypt`][Self::UnableToDecrypt] variant,
/// return the inner [`EncryptedMessage`].
pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
match self {
Self::UnableToDecrypt(v) => Some(v),
_ => None,
}
}
}
/// An `m.room.message` event or extensible event, including edits.

View File

@@ -18,12 +18,15 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
sync::{Arc, Mutex as StdMutex},
};
use futures_core::Stream;
use futures_signals::signal_vec::{MutableVec, SignalVec, SignalVecExt, VecDiff};
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
use matrix_sdk_base::{
deserialized_responses::{EncryptionInfo, SyncTimelineEvent},
locks::Mutex,
};
use ruma::{
assign,
events::{
@@ -64,28 +67,51 @@ pub use self::{
pub struct Timeline {
inner: TimelineInner,
room: room::Common,
start_token: Mutex<Option<String>>,
_end_token: Mutex<Option<String>>,
start_token: StdMutex<Option<String>>,
_end_token: StdMutex<Option<String>>,
_timeline_event_handler_guard: EventHandlerDropGuard,
_fully_read_handler_guard: EventHandlerDropGuard,
#[cfg(feature = "e2e-encryption")]
_room_key_handler_guard: EventHandlerDropGuard,
}
#[derive(Clone, Debug, Default)]
struct TimelineInner {
items: MutableVec<Arc<TimelineItem>>,
metadata: Arc<Mutex<TimelineInnerMetadata>>,
}
/// Non-signalling parts of `TimelineInner`.
#[derive(Debug, Default)]
struct TimelineInnerMetadata {
// Reaction event / txn ID => sender and reaction data
reaction_map: Arc<Mutex<HashMap<TimelineKey, (OwnedUserId, AnnotationRelation)>>>,
fully_read_event: Arc<Mutex<Option<OwnedEventId>>>,
fully_read_event_in_timeline: Arc<Mutex<bool>>,
reaction_map: HashMap<TimelineKey, (OwnedUserId, AnnotationRelation)>,
fully_read_event: Option<OwnedEventId>,
fully_read_event_in_timeline: bool,
}
impl Timeline {
pub(super) async fn new(room: &room::Common) -> Self {
Self::with_events(room, None, Vec::new()).await
}
pub(crate) async fn with_events(
room: &room::Common,
prev_token: Option<String>,
events: Vec<SyncTimelineEvent>,
) -> Self {
let inner = TimelineInner::default();
let own_user_id = room.own_user_id();
for ev in events.into_iter().rev() {
inner
.handle_back_paginated_event(ev.event.cast(), ev.encryption_info, own_user_id)
.await;
}
match room.account_data_static::<FullyReadEventContent>().await {
Ok(Some(fully_read)) => match fully_read.deserialize() {
Ok(fully_read) => inner.set_fully_read_event(fully_read.content.event_id),
Ok(fully_read) => inner.set_fully_read_event(fully_read.content.event_id).await,
Err(error) => {
error!(?error, "Failed to deserialize `m.fully_read` account data")
}
@@ -101,7 +127,7 @@ impl Timeline {
move |event, encryption_info: Option<EncryptionInfo>, room: Room| {
let inner = inner.clone();
async move {
inner.handle_live_event(event, encryption_info, room.own_user_id());
inner.handle_live_event(event, encryption_info, room.own_user_id()).await;
}
}
});
@@ -113,19 +139,67 @@ impl Timeline {
move |event| {
let inner = inner.clone();
async move {
inner.handle_fully_read(event);
inner.handle_fully_read(event).await;
}
}
});
let _fully_read_handler_guard = room.client.event_handler_drop_guard(fully_read_handle);
// Not using room.add_event_handler here because RoomKey events are
// to-device events that are not received in the context of a room.
#[cfg(feature = "e2e-encryption")]
let room_id = room.room_id().to_owned();
#[cfg(feature = "e2e-encryption")]
let room_key_handle = room.client.add_event_handler({
use std::iter;
use ruma::events::room_key::ToDeviceRoomKeyEvent;
use crate::Client;
let inner = inner.clone();
move |event: ToDeviceRoomKeyEvent, client: Client| {
let inner = inner.clone();
let room_id = room_id.clone();
async move {
if event.content.room_id != room_id {
return;
}
let Some(olm_machine) = client.olm_machine() else {
error!("The olm machine isn't yet available");
return;
};
let session_id = event.content.session_id;
let Some(own_user_id) = client.user_id() else {
error!("The user's own ID isn't available");
return;
};
inner
.retry_event_decryption(
&room_id,
olm_machine,
iter::once(session_id.as_str()).collect(),
own_user_id,
)
.await;
}
}
});
#[cfg(feature = "e2e-encryption")]
let _room_key_handler_guard = room.client.event_handler_drop_guard(room_key_handle);
Timeline {
inner,
room: room.clone(),
start_token: Mutex::new(None),
_end_token: Mutex::new(None),
start_token: StdMutex::new(prev_token),
_end_token: StdMutex::new(None),
_timeline_event_handler_guard,
_fully_read_handler_guard,
#[cfg(feature = "e2e-encryption")]
_room_key_handler_guard,
}
}
@@ -146,16 +220,64 @@ impl Timeline {
let own_user_id = self.room.own_user_id();
for room_ev in messages.chunk {
self.inner.handle_back_paginated_event(
room_ev.event.cast(),
room_ev.encryption_info,
own_user_id,
);
self.inner
.handle_back_paginated_event(
room_ev.event.cast(),
room_ev.encryption_info,
own_user_id,
)
.await;
}
Ok(outcome)
}
/// Retry decryption of previously un-decryptable events given a list of
/// session IDs whose keys have been imported.
///
/// # Example
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # room::timeline::Timeline, ruma::room_id,
/// # };
/// # async {
/// # let mut client: Client = todo!();
/// # let room_id = ruma::room_id!("!example:example.org");
/// # let timeline: Timeline = todo!();
/// let path = PathBuf::from("/home/example/e2e-keys.txt");
/// let result =
/// client.encryption().import_room_keys(path, "secret-passphrase").await?;
///
/// // Given a timeline for a specific room_id
/// if let Some(keys_for_users) = result.keys.get(room_id) {
/// let session_ids = keys_for_users.values().flatten();
/// timeline.retry_decryption(session_ids).await;
/// }
/// # anyhow::Ok(()) };
/// ```
#[cfg(feature = "e2e-encryption")]
pub async fn retry_decryption<'a, S: AsRef<str> + 'a>(
&'a self,
session_ids: impl IntoIterator<Item = &'a S>,
) {
self.inner
.retry_event_decryption(
self.room.room_id(),
self.room.client.olm_machine().expect("Olm machine wasn't started"),
session_ids.into_iter().map(AsRef::as_ref).collect(),
self.room.own_user_id(),
)
.await;
}
/// Get the latest of the timeline's items.
pub fn latest(&self) -> Option<Arc<TimelineItem>> {
self.inner.items.lock_ref().last().cloned()
}
/// Get a signal of the timeline's items.
///
/// You can poll this signal to receive updates, the first of which will
@@ -207,7 +329,9 @@ impl Timeline {
txn_id: Option<&TransactionId>,
) -> Result<()> {
let txn_id = txn_id.map_or_else(TransactionId::new, ToOwned::to_owned);
self.inner.handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id());
self.inner
.handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id())
.await;
// If this room isn't actually in joined state, we'll get a server error.
// Not ideal, but works for now.
@@ -262,7 +386,7 @@ fn find_event(
.rfind(|(_, it)| key == it.key)
}
fn find_fully_read(lock: &[Arc<TimelineItem>]) -> Option<usize> {
fn find_read_marker(lock: &[Arc<TimelineItem>]) -> Option<usize> {
lock.iter()
.enumerate()
.rfind(|(_, item)| {

View File

@@ -23,27 +23,33 @@ use assert_matches::assert_matches;
use futures_core::Stream;
use futures_signals::signal_vec::{SignalVecExt, VecDiff};
use futures_util::StreamExt;
use matrix_sdk_base::crypto::OlmMachine;
use matrix_sdk_test::async_test;
use once_cell::sync::Lazy;
use ruma::{
assign,
assign, event_id,
events::{
reaction::{self, ReactionEventContent},
room::{
encrypted::{
EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
},
message::{self, Replacement, RoomMessageEventContent},
message::{self, MessageType, Replacement, RoomMessageEventContent},
redaction::OriginalSyncRoomRedactionEvent,
},
MessageLikeEventContent, OriginalSyncMessageLikeEvent,
MessageLikeEventContent, MessageLikeEventType, OriginalSyncMessageLikeEvent,
StateEventType,
},
room_id,
serde::Raw,
server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
server_name, uint, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
};
use serde_json::{json, Value as JsonValue};
use super::{EncryptedMessage, TimelineInner, TimelineItem, TimelineItemContent};
use super::{
EncryptedMessage, TimelineInner, TimelineItem, TimelineItemContent, TimelineKey,
VirtualTimelineItem,
};
static ALICE: Lazy<&UserId> = Lazy::new(|| user_id!("@alice:server.name"));
static BOB: Lazy<&UserId> = Lazy::new(|| user_id!("@bob:other.server"));
@@ -53,7 +59,7 @@ async fn reaction_redaction() {
let timeline = TestTimeline::new(&ALICE);
let mut stream = timeline.stream();
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("hi!"));
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("hi!")).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event = item.as_event().unwrap();
assert_eq!(event.reactions().len(), 0);
@@ -61,7 +67,7 @@ async fn reaction_redaction() {
let msg_event_id = event.event_id().unwrap();
let rel = reaction::Relation::new(msg_event_id.to_owned(), "+1".to_owned());
timeline.handle_live_message_event(&BOB, ReactionEventContent::new(rel));
timeline.handle_live_message_event(&BOB, ReactionEventContent::new(rel)).await;
let item =
assert_matches!(stream.next().await, Some(VecDiff::UpdateAt { index: 0, value }) => value);
let event = item.as_event().unwrap();
@@ -71,7 +77,7 @@ async fn reaction_redaction() {
let reaction_event_id = event.event_id().unwrap();
timeline.handle_live_redaction(&BOB, reaction_event_id);
timeline.handle_live_redaction(&BOB, reaction_event_id).await;
let item =
assert_matches!(stream.next().await, Some(VecDiff::UpdateAt { index: 0, value }) => value);
let event = item.as_event().unwrap();
@@ -83,7 +89,7 @@ async fn invalid_edit() {
let timeline = TestTimeline::new(&ALICE);
let mut stream = timeline.stream();
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("test"));
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("test")).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event = item.as_event().unwrap();
let msg = event.content.as_message().unwrap();
@@ -94,11 +100,11 @@ async fn invalid_edit() {
let edit = assign!(RoomMessageEventContent::text_plain(" * fake"), {
relates_to: Some(message::Relation::Replacement(Replacement::new(
msg_event_id.to_owned(),
Box::new(RoomMessageEventContent::text_plain("fake")),
MessageType::text_plain("fake"),
))),
});
// Edit is from a different user than the previous event
timeline.handle_live_message_event(&BOB, edit);
timeline.handle_live_message_event(&BOB, edit).await;
// Can't easily test the non-arrival of an item using the stream. Instead
// just assert that there is still just a single item in the timeline.
@@ -111,23 +117,25 @@ async fn edit_redacted() {
let mut stream = timeline.stream();
// Ruma currently fails to serialize most redacted events correctly
timeline.handle_live_custom_event(json!({
"content": {},
"event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I",
"origin_server_ts": 10,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"redacted_because": {
"content": {},
"redacts": "$eeG0HA0FAZ37wP8kXlNkxx3K",
"event_id": "$N6eUCBc3vu58PL8TobGaVQzM",
"sender": "@alice:example.org",
"origin_server_ts": 5,
"type": "m.room.redaction",
timeline
.handle_live_custom_event(json!({
"content": {},
"event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I",
"origin_server_ts": 10,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"redacted_because": {
"content": {},
"redacts": "$eeG0HA0FAZ37wP8kXlNkxx3K",
"event_id": "$N6eUCBc3vu58PL8TobGaVQzM",
"sender": "@alice:example.org",
"origin_server_ts": 5,
"type": "m.room.redaction",
},
},
},
}));
}))
.await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let redacted_event_id = item.as_event().unwrap().event_id().unwrap();
@@ -135,42 +143,219 @@ async fn edit_redacted() {
let edit = assign!(RoomMessageEventContent::text_plain(" * test"), {
relates_to: Some(message::Relation::Replacement(Replacement::new(
redacted_event_id.to_owned(),
Box::new(RoomMessageEventContent::text_plain("test")),
MessageType::text_plain("test"),
))),
});
timeline.handle_live_message_event(&ALICE, edit);
timeline.handle_live_message_event(&ALICE, edit).await;
assert_eq!(timeline.inner.items.lock_ref().len(), 1);
}
#[cfg(not(target_arch = "wasm32"))]
#[async_test]
async fn unable_to_decrypt() {
use std::{io::Cursor, iter};
use matrix_sdk_base::crypto::decrypt_room_key_export;
const SESSION_ID: &str = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
const SESSION_KEY: &[u8] = b"\
-----BEGIN MEGOLM SESSION DATA-----\n\
ASKcWoiAVUM97482UAi83Avce62hSLce7i5JhsqoF6xeAAAACqt2Cg3nyJPRWTTMXxXH7TXnkfdlmBXbQtq5\
bpHo3LRijcq2Gc6TXilESCmJN14pIsfKRJrWjZ0squ/XsoTFytuVLWwkNaW3QF6obeg2IoVtJXLMPdw3b2vO\
vgwGY3OMP0XafH13j1vcb6YLzvgLkZQLnYvd47hv3yK/9GmKS9tokuaQ7dCVYckYcIOS09EDTs70YdxUd5WG\
rQynATCLFP1p/NAGv70r9MK7Cy/mNpjD0r4qC7UEDIoi1kOWzHgnLo19wtvwsb8Fg8ATxcs3Wmtj8hIUYpDx\
ia4sM10zbytUuaPUAfCDf42IyxdmOnGe1CueXhgI71y+RW0s0argNqUt7jB70JT0o9CyX6UBGRaqLk2MPY9T\
hUu5J8X3UgIa6rcbWigzohzWm9rdbEHFrSWqjpfQYMaAKQQgETrjSy4XTrp2RhC2oNqG/hylI4ab+F4X6fpH\
DYP1NqNMP5g36xNu7LhDnrUB5qsPjYOmWORxGLfudpF3oLYCSlr3DgHqEIB6HjQblLZ3KQuPBse3zxyROTnS\
AhdPH4a/z1wioFtKNVph3hecsiKEdqnz4Y2coSIdhz58mJ9JWNQoFAENE5CSsoEZAGvafYZVpW4C75YY2zq1\
wIeiFi1dT43/jLAUGkslsi1VvnyfUu8qO404RxYO3XHoGLMFoFLOO+lZ+VGci2Vz10AhxJhEBHxRKxw4k2uB\
HztoSJUr/2Y\n\
-----END MEGOLM SESSION DATA-----";
let timeline = TestTimeline::new(&ALICE);
timeline.handle_live_message_event(
&BOB,
RoomEncryptedEventContent::new(
EncryptedEventScheme::MegolmV1AesSha2(
MegolmV1AesSha2ContentInit {
ciphertext: "This can't be decrypted".to_owned(),
sender_key: "whatever".to_owned(),
device_id: "MyDevice".into(),
session_id: "MySession".into(),
}
.into(),
let mut stream = timeline.stream();
timeline
.handle_live_message_event(
&BOB,
RoomEncryptedEventContent::new(
EncryptedEventScheme::MegolmV1AesSha2(
MegolmV1AesSha2ContentInit {
ciphertext: "\
AwgAEtABPRMavuZMDJrPo6pGQP4qVmpcuapuXtzKXJyi3YpEsjSWdzuRKIgJzD4P\
cSqJM1A8kzxecTQNJsC5q22+KSFEPxPnI4ltpm7GFowSoPSW9+bFdnlfUzEP1jPq\
YevHAsMJp2fRKkzQQbPordrUk1gNqEpGl4BYFeRqKl9GPdKFwy45huvQCLNNueql\
CFZVoYMuhxrfyMiJJAVNTofkr2um2mKjDTlajHtr39pTG8k0eOjSXkLOSdZvNOMz\
hGhSaFNeERSA2G2YbeknOvU7MvjiO0AKuxaAe1CaVhAI14FCgzrJ8g0y5nly+n7x\
QzL2G2Dn8EoXM5Iqj8W99iokQoVsSrUEnaQ1WnSIfewvDDt4LCaD/w7PGETMCQ"
.to_owned(),
sender_key: "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA".to_owned(),
device_id: "NLAZCWIOCO".into(),
session_id: SESSION_ID.into(),
}
.into(),
),
None,
),
None,
),
);
let timeline_items = timeline.inner.items.lock_ref();
assert_eq!(timeline_items.len(), 1);
let event = timeline_items[0].as_event().unwrap();
)
.await;
assert_eq!(timeline.inner.items.lock_ref().len(), 1);
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event = item.as_event().unwrap();
let session_id = assert_matches!(
event.content(),
TimelineItemContent::UnableToDecrypt(
EncryptedMessage::MegolmV1AesSha2 { session_id, .. },
) => session_id
);
assert_eq!(session_id, "MySession");
assert_eq!(session_id, SESSION_ID);
let own_user_id = user_id!("@example:morheus.localhost");
let exported_keys = decrypt_room_key_export(Cursor::new(SESSION_KEY), "1234").unwrap();
let olm_machine = OlmMachine::new(own_user_id, "SomeDeviceId".into()).await;
olm_machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
timeline
.inner
.retry_event_decryption(
room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"),
&olm_machine,
iter::once(SESSION_ID).collect(),
own_user_id,
)
.await;
assert_eq!(timeline.inner.items.lock_ref().len(), 1);
let item =
assert_matches!(stream.next().await, Some(VecDiff::UpdateAt { index: 0, value }) => value);
let event = item.as_event().unwrap();
assert_matches!(&event.encryption_info, Some(_));
let text = assert_matches!(event.content(), TimelineItemContent::Message(msg) => msg.body());
assert_eq!(text, "It's a secret to everybody");
}
#[async_test]
async fn update_read_marker() {
let timeline = TestTimeline::new(&ALICE);
let mut stream = timeline.stream();
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("A")).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event_id = item.as_event().unwrap().event_id().unwrap().to_owned();
timeline.inner.set_fully_read_event(event_id).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
assert_matches!(item.as_virtual(), Some(VirtualTimelineItem::ReadMarker));
timeline.handle_live_message_event(&BOB, RoomMessageEventContent::text_plain("B")).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event_id = item.as_event().unwrap().event_id().unwrap().to_owned();
timeline.inner.set_fully_read_event(event_id.clone()).await;
assert_matches!(stream.next().await, Some(VecDiff::Move { old_index: 1, new_index: 2 }));
// Nothing should happen if the fully read event isn't found.
timeline.inner.set_fully_read_event(event_id!("$fake_event_id").to_owned()).await;
// Nothing should happen if the fully read event is set back to the same event
// as before.
timeline.inner.set_fully_read_event(event_id).await;
timeline.handle_live_message_event(&ALICE, RoomMessageEventContent::text_plain("C")).await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event_id = item.as_event().unwrap().event_id().unwrap().to_owned();
timeline.inner.set_fully_read_event(event_id).await;
assert_matches!(stream.next().await, Some(VecDiff::Move { old_index: 2, new_index: 3 }));
}
#[async_test]
async fn invalid_event_content() {
let timeline = TestTimeline::new(&ALICE);
let mut stream = timeline.stream();
// m.room.message events must have a msgtype and body in content, so this
// event with an empty content object should fail to deserialize.
timeline
.handle_live_custom_event(json!({
"content": {},
"event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I",
"origin_server_ts": 10,
"sender": "@alice:example.org",
"type": "m.room.message",
}))
.await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event_item = item.as_event().unwrap();
assert_eq!(event_item.sender(), "@alice:example.org");
assert_eq!(
*event_item.key(),
TimelineKey::EventId(event_id!("$eeG0HA0FAZ37wP8kXlNkxx3I").to_owned())
);
assert_eq!(event_item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(10))));
let event_type = assert_matches!(
event_item.content(),
TimelineItemContent::FailedToParseMessageLike { event_type, .. } => event_type
);
assert_eq!(*event_type, MessageLikeEventType::RoomMessage);
// Similar to above, the m.room.member state event must also not have an
// empty content object.
timeline
.handle_live_custom_event(json!({
"content": {},
"event_id": "$d5G0HA0FAZ37wP8kXlNkxx3I",
"origin_server_ts": 2179,
"sender": "@alice:example.org",
"type": "m.room.member",
"state_key": "@alice:example.org",
}))
.await;
let item = assert_matches!(stream.next().await, Some(VecDiff::Push { value }) => value);
let event_item = item.as_event().unwrap();
assert_eq!(event_item.sender(), "@alice:example.org");
assert_eq!(
*event_item.key(),
TimelineKey::EventId(event_id!("$d5G0HA0FAZ37wP8kXlNkxx3I").to_owned())
);
assert_eq!(event_item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(2179))));
let (event_type, state_key) = assert_matches!(
event_item.content(),
TimelineItemContent::FailedToParseState {
event_type,
state_key,
..
} => (event_type, state_key)
);
assert_eq!(*event_type, StateEventType::RoomMember);
assert_eq!(*state_key, "@alice:example.org");
}
#[async_test]
async fn invalid_event() {
let timeline = TestTimeline::new(&ALICE);
// This event is missing the sender field which the homeserver must add to
// all timeline events. Because the event is malformed, it will be ignored.
timeline
.handle_live_custom_event(json!({
"content": {
"body": "hello world",
"msgtype": "m.text"
},
"event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I",
"origin_server_ts": 10,
"type": "m.room.message",
}))
.await;
assert_eq!(timeline.inner.items.lock_ref().len(), 0);
}
struct TestTimeline {
@@ -187,7 +372,7 @@ impl TestTimeline {
self.inner.items.signal_vec_cloned().to_stream()
}
fn handle_live_message_event<C>(&self, sender: &UserId, content: C)
async fn handle_live_message_event<C>(&self, sender: &UserId, content: C)
where
C: MessageLikeEventContent,
{
@@ -199,15 +384,15 @@ impl TestTimeline {
unsigned: Default::default(),
};
let raw = Raw::new(&ev).unwrap().cast();
self.inner.handle_live_event(raw, None, &self.own_user_id);
self.inner.handle_live_event(raw, None, &self.own_user_id).await;
}
fn handle_live_custom_event(&self, event: JsonValue) {
async fn handle_live_custom_event(&self, event: JsonValue) {
let raw = Raw::new(&event).unwrap().cast();
self.inner.handle_live_event(raw, None, &self.own_user_id);
self.inner.handle_live_event(raw, None, &self.own_user_id).await;
}
fn handle_live_redaction(&self, sender: &UserId, redacts: &EventId) {
async fn handle_live_redaction(&self, sender: &UserId, redacts: &EventId) {
let ev = OriginalSyncRoomRedactionEvent {
content: Default::default(),
redacts: redacts.to_owned(),
@@ -217,7 +402,7 @@ impl TestTimeline {
unsigned: Default::default(),
};
let raw = Raw::new(&ev).unwrap().cast();
self.inner.handle_live_event(raw, None, &self.own_user_id);
self.inner.handle_live_event(raw, None, &self.own_user_id).await;
}
}

View File

@@ -17,7 +17,7 @@ use std::{fmt::Debug, sync::Arc};
use futures_core::stream::Stream;
use futures_signals::signal::Mutable;
use matrix_sdk_base::deserialized_responses::{SyncResponse, SyncTimelineEvent};
use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, sync::SyncResponse};
use ruma::{
api::client::sync::sync_events::v4::{
self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ToDeviceConfig,
@@ -29,6 +29,8 @@ use ruma::{
use thiserror::Error;
use url::Url;
#[cfg(feature = "experimental-timeline")]
use crate::room::timeline::Timeline;
use crate::{Client, Result};
/// Internal representation of errors in Sliding Sync
@@ -106,6 +108,7 @@ pub type AliveRoomTimeline = Arc<futures_signals::signal_vec::MutableVec<SyncTim
/// Room info as giving by the SlidingSync Feature.
#[derive(Debug, Clone)]
pub struct SlidingSyncRoom {
client: Client,
room_id: OwnedRoomId,
inner: v4::SlidingSyncRoom,
is_loading_more: Mutable<bool>,
@@ -115,6 +118,7 @@ pub struct SlidingSyncRoom {
impl SlidingSyncRoom {
fn from(
client: Client,
room_id: OwnedRoomId,
mut inner: v4::SlidingSyncRoom,
timeline: Vec<SyncTimelineEvent>,
@@ -122,6 +126,7 @@ impl SlidingSyncRoom {
// we overwrite to only keep one copy
inner.timeline = vec![];
Self {
client,
room_id,
is_loading_more: Mutable::new(false),
prev_batch: Mutable::new(inner.prev_batch.clone()),
@@ -146,10 +151,20 @@ impl SlidingSyncRoom {
}
/// `AliveTimeline` of this room
#[cfg(not(feature = "experimental-timeline"))]
pub fn timeline(&self) -> AliveRoomTimeline {
self.timeline.clone()
}
/// `Timeline` of this room
#[cfg(feature = "experimental-timeline")]
pub async fn timeline(&self) -> Timeline {
let current_timeline = self.timeline.lock_ref().to_vec();
let prev_batch = self.prev_batch.lock_ref().clone();
let room = self.client.get_room(&self.room_id).unwrap();
Timeline::with_events(&room, prev_batch, current_timeline).await
}
/// This rooms name as calculated by the server, if any
pub fn name(&self) -> Option<&str> {
self.inner.name.as_deref()
@@ -232,7 +247,7 @@ pub struct UpdateSummary {
#[derive(Clone, Debug, Builder)]
#[builder(pattern = "owned", derive(Clone, Debug))]
pub struct SlidingSync {
/// Customize the homeserver for sliding sync onlye
/// Customize the homeserver for sliding sync only
#[builder(setter(strip_option))]
homeserver: Option<Url>,
@@ -490,7 +505,7 @@ impl SlidingSync {
} else {
rooms_map.insert_cloned(
id.clone(),
SlidingSyncRoom::from(id.clone(), room_data, timeline),
SlidingSyncRoom::from(self.client.clone(), id.clone(), room_data, timeline),
);
rooms.push(id);
}
@@ -509,7 +524,7 @@ impl SlidingSync {
/// Run this stream to receive new updates from the server.
pub async fn stream<'a>(
&self,
) -> Result<impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_, crate::Error> {
) -> Result<impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_> {
let views = self.views.lock_ref().to_vec();
let extensions = self.extensions.clone();
let client = self.client.clone();
@@ -525,7 +540,7 @@ impl SlidingSync {
let mut new_remaining_generators = Vec::new();
let mut new_remaining_views = Vec::new();
for (mut generator, view) in std::iter::zip(remaining_generators, remaining_views) {
for (mut generator, view) in std::iter::zip(remaining_generators, remaining_views) {
if let Some(request) = generator.next() {
requests.push(request);
new_remaining_generators.push(generator);
@@ -1103,6 +1118,7 @@ impl Client {
) -> Result<SyncResponse> {
let response = self.base_client().process_sliding_sync(response).await?;
tracing::debug!("done processing on base_client");
self.handle_sync_response(response).await
self.handle_sync_response(&response).await?;
Ok(response)
}
}

View File

@@ -1,32 +1,95 @@
use std::time::Duration;
//! The SDK's representation of the result of a `/sync` request.
use std::{collections::BTreeMap, time::Duration};
pub use matrix_sdk_base::sync::*;
use matrix_sdk_base::{
deserialized_responses::{JoinedRoom, LeftRoom, SyncResponse},
instant::Instant,
deserialized_responses::AmbiguityChanges, instant::Instant,
sync::SyncResponse as BaseSyncResponse,
};
use ruma::api::client::sync::sync_events;
use ruma::{
api::client::{
push::get_notifications::v3::Notification,
sync::sync_events::{self, v3::Presence, DeviceLists},
},
events::{AnyGlobalAccountDataEvent, AnyToDeviceEvent},
serde::Raw,
DeviceKeyAlgorithm, OwnedRoomId,
};
use serde::{Deserialize, Serialize};
use tracing::{error, warn};
use crate::{event_handler::HandlerKind, Client, Result};
/// The processed response of a `/sync` request.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SyncResponse {
/// The batch token to supply in the `since` param of the next `/sync`
/// request.
pub next_batch: String,
/// Updates to rooms.
pub rooms: Rooms,
/// Updates to the presence status of other users.
pub presence: Presence,
/// The global private data created by this user.
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
/// Messages sent directly between devices.
pub to_device_events: Vec<Raw<AnyToDeviceEvent>>,
/// Information on E2E device updates.
///
/// Only present on an incremental sync.
pub device_lists: DeviceLists,
/// For each key algorithm, the number of unclaimed one-time keys
/// currently held on the server for a device.
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, u64>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
/// New notifications per room.
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
}
impl SyncResponse {
pub(crate) fn new(next_batch: String, base_response: BaseSyncResponse) -> Self {
let BaseSyncResponse {
rooms,
presence,
account_data,
to_device_events,
device_lists,
device_one_time_keys_count,
ambiguity_changes,
notifications,
} = base_response;
Self {
next_batch,
rooms,
presence,
account_data,
to_device_events,
device_lists,
device_one_time_keys_count,
ambiguity_changes,
notifications,
}
}
}
/// Internal functionality related to getting events from the server
/// (`sync_events` endpoint)
impl Client {
pub(crate) async fn process_sync(
&self,
response: sync_events::v3::Response,
) -> Result<SyncResponse> {
) -> Result<BaseSyncResponse> {
let response = self.base_client().receive_sync_response(response).await?;
self.handle_sync_response(response).await
self.handle_sync_response(&response).await?;
Ok(response)
}
#[tracing::instrument(skip(self, response))]
pub(crate) async fn handle_sync_response(
&self,
response: SyncResponse,
) -> Result<SyncResponse> {
let SyncResponse {
next_batch: _,
pub(crate) async fn handle_sync_response(&self, response: &BaseSyncResponse) -> Result<()> {
let BaseSyncResponse {
rooms,
presence,
account_data,
@@ -35,7 +98,7 @@ impl Client {
device_one_time_keys_count: _,
ambiguity_changes: _,
notifications,
} = &response;
} = response;
self.handle_sync_events(HandlerKind::GlobalAccountData, &None, account_data).await?;
self.handle_sync_events(HandlerKind::Presence, &None, &presence.events).await?;
@@ -93,12 +156,9 @@ impl Client {
let mut futures = Vec::new();
for handler in &*self.notification_handlers().await {
for (room_id, room_notifications) in notifications {
let room = match self.get_room(room_id) {
Some(room) => room,
None => {
warn!(%room_id, "Can't call notification handler, room not found");
continue;
}
let Some(room) = self.get_room(room_id) else {
warn!(%room_id, "Can't call notification handler, room not found");
continue;
};
futures.extend(room_notifications.iter().map(|notification| {
@@ -113,7 +173,7 @@ impl Client {
fut.await;
}
Ok(response)
Ok(())
}
async fn sleep() {

View File

@@ -185,15 +185,20 @@ async fn login_error() {
.await;
if let Err(err) = client.login_username("example", "wordpass").send().await {
if let Some(RumaApiError::ClientApi(client_api::Error { kind, message, status_code })) =
if let Some(RumaApiError::ClientApi(client_api::Error { status_code, body })) =
err.as_ruma_api_error()
{
if *kind != client_api::error::ErrorKind::Forbidden {
panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden");
}
assert_eq!(message, "Invalid password");
assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap());
if let client_api::error::ErrorBody::Standard { kind, message } = body {
if *kind != client_api::error::ErrorKind::Forbidden {
panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden");
}
assert_eq!(message, "Invalid password");
} else {
panic!("non-standard error body")
}
} else {
panic!("found the wrong `Error` type {err:?}, expected `Error::RumaResponse");
}
@@ -225,17 +230,20 @@ async fn register_error() {
if let Err(err) = client.register(user).await {
if let Some(RumaApiError::Uiaa(UiaaResponse::MatrixError(client_api::Error {
kind,
message,
status_code,
body,
}))) = err.as_ruma_api_error()
{
if *kind != client_api::error::ErrorKind::Forbidden {
panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden");
}
assert_eq!(message, "Invalid password");
assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap());
if let client_api::error::ErrorBody::Standard { kind, message } = body {
if *kind != client_api::error::ErrorKind::Forbidden {
panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden");
}
assert_eq!(message, "Invalid password");
} else {
panic!("non-standard error body")
}
} else {
panic!("found the wrong `Error` type {err:#?}, expected `UiaaResponse`");
}
@@ -255,8 +263,6 @@ async fn sync() {
let response = client.sync_once(sync_settings).await.unwrap();
assert_ne!(response.next_batch, "");
assert!(client.sync_token().await.is_some());
}
#[async_test]
@@ -362,7 +368,7 @@ async fn join_leave_room() {
let room = client.get_joined_room(room_id);
assert!(room.is_none());
client.sync_once(SyncSettings::default()).await.unwrap();
let sync_token = client.sync_once(SyncSettings::default()).await.unwrap().next_batch;
let room = client.get_left_room(room_id);
assert!(room.is_none());
@@ -370,7 +376,6 @@ async fn join_leave_room() {
let room = client.get_joined_room(room_id);
assert!(room.is_some());
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, &*test_json::LEAVE_SYNC_EVENT, Some(sync_token.clone())).await;
client.sync_once(SyncSettings::default().token(sync_token)).await.unwrap();
@@ -398,7 +403,7 @@ async fn join_room_by_id() {
assert_eq!(
// this is the `join_by_room_id::Response` but since no PartialEq we check the RoomId
// field
client.join_room_by_id(room_id).await.unwrap().room_id,
client.join_room_by_id(room_id).await.unwrap().room_id(),
room_id
);
}
@@ -423,7 +428,7 @@ async fn join_room_by_id_or_alias() {
.join_room_by_id_or_alias(room_id, &["server.com".try_into().unwrap()])
.await
.unwrap()
.room_id,
.room_id(),
room_id!("!testroom:example.org")
);
}

View File

@@ -2,10 +2,11 @@
#![cfg(not(target_arch = "wasm32"))]
use matrix_sdk::{config::RequestConfig, Client, ClientBuilder, Session};
use matrix_sdk_test::test_json;
use ruma::{api::MatrixVersion, device_id, user_id};
use serde::Serialize;
use wiremock::{
matchers::{header, method, path, query_param, query_param_is_missing},
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
Mock, MockServer, ResponseTemplate,
};
@@ -69,3 +70,27 @@ async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Op
.mount(server)
.await;
}
/// Mount a Mock on the given server to handle the `GET
/// /rooms/.../state/m.room.encryption` endpoint with an option whether it
/// should return an encryption event or not.
async fn mock_encryption_state(server: &MockServer, is_encrypted: bool) {
let builder = Mock::given(method("GET"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.*room.*encryption.?"))
.and(header("authorization", "Bearer 1234"));
if is_encrypted {
builder
.respond_with(
ResponseTemplate::new(200)
.set_body_json(&*test_json::sync_events::ENCRYPTION_CONTENT),
)
.mount(server)
.await;
} else {
builder
.respond_with(ResponseTemplate::new(404).set_body_json(&*test_json::NOT_FOUND))
.mount(server)
.await;
}
}

View File

@@ -6,13 +6,11 @@ use futures::{
};
use futures_signals::signal::SignalExt;
use matches::assert_matches;
use matrix_sdk::{
config::RequestConfig, executor::spawn, HttpError, RefreshTokenError, RumaApiError, Session,
};
use matrix_sdk::{config::RequestConfig, executor::spawn, HttpError, RefreshTokenError, Session};
use matrix_sdk_test::{async_test, test_json};
use ruma::{
api::{
client::{account::register, error::ErrorKind, Error as ClientApiError},
client::{account::register, error::ErrorKind},
MatrixVersion,
},
assign, device_id, user_id,
@@ -229,10 +227,7 @@ async fn refresh_token_not_handled() {
.await;
let res = client.whoami().await.unwrap_err();
assert_matches!(
res.as_ruma_api_error(),
Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. }))
);
assert_matches!(res.client_api_error_kind(), Some(ErrorKind::UnknownToken { .. }));
}
#[async_test]
@@ -360,10 +355,7 @@ async fn refresh_token_handled_failure() {
.await;
let res = client.whoami().await.unwrap_err();
assert_matches!(
res.as_ruma_api_error(),
Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. }))
)
assert_matches!(res.client_api_error_kind(), Some(ErrorKind::UnknownToken { .. }))
}
#[async_test]

View File

@@ -62,14 +62,13 @@ async fn room_names() {
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let _response = client.sync_once(sync_settings).await.unwrap();
let sync_token = client.sync_once(sync_settings).await.unwrap().next_batch;
assert_eq!(client.rooms().len(), 1);
let room = client.get_joined_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap();
assert_eq!(DisplayName::Aliased("tutorial".to_owned()), room.display_name().await.unwrap());
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, &*test_json::INVITE_SYNC, Some(sync_token.clone())).await;
let _response = client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
@@ -203,7 +202,7 @@ async fn room_route() {
);
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
client.sync_once(SyncSettings::new()).await.unwrap();
let sync_token = client.sync_once(SyncSettings::new()).await.unwrap().next_batch;
let room = client.get_room(room_id).unwrap();
let route = room.route().await.unwrap();
@@ -214,9 +213,9 @@ async fn room_route() {
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk(
bulk_room_members(batch, 0..1, "localhost", &MembershipState::Join),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 1);
@@ -227,9 +226,9 @@ async fn room_route() {
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk(
bulk_room_members(batch, 0..15, "notarealhs", &MembershipState::Join),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 2);
@@ -241,9 +240,9 @@ async fn room_route() {
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk(
bulk_room_members(batch, 0..5, "mymatrix", &MembershipState::Join),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 3);
@@ -256,9 +255,9 @@ async fn room_route() {
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_state_bulk(
bulk_room_members(batch, 0..10, "yourmatrix", &MembershipState::Join),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 3);
@@ -281,9 +280,9 @@ async fn room_route() {
"type": "m.room.power_levels",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 3);
@@ -307,9 +306,9 @@ async fn room_route() {
"type": "m.room.power_levels",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
let route = room.route().await.unwrap();
assert_eq!(route.len(), 3);
@@ -332,7 +331,6 @@ async fn room_route() {
"type": "m.room.server_acl",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
@@ -366,7 +364,7 @@ async fn room_permalink() {
)),
);
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
client.sync_once(SyncSettings::new()).await.unwrap();
let sync_token = client.sync_once(SyncSettings::new()).await.unwrap().next_batch;
let room = client.get_room(room_id).unwrap();
assert_eq!(
@@ -391,9 +389,9 @@ async fn room_permalink() {
"type": "m.room.canonical_alias",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
let sync_token =
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap().next_batch;
assert_eq!(
room.matrix_to_permalink().await.unwrap().to_string(),
@@ -415,7 +413,6 @@ async fn room_permalink() {
"type": "m.room.canonical_alias",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();
@@ -457,7 +454,7 @@ async fn room_event_permalink() {
)),
);
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
client.sync_once(SyncSettings::new()).await.unwrap();
let sync_token = client.sync_once(SyncSettings::new()).await.unwrap().next_batch;
let room = client.get_room(room_id).unwrap();
assert_eq!(
@@ -483,7 +480,6 @@ async fn room_event_permalink() {
"type": "m.room.canonical_alias",
})),
));
let sync_token = client.sync_token().await.unwrap();
mock_sync(&server, ev_builder.build_json_sync_response(), Some(sync_token.clone())).await;
client.sync_once(SyncSettings::new().token(sync_token)).await.unwrap();

View File

@@ -19,7 +19,7 @@ use wiremock::{
Mock, ResponseTemplate,
};
use crate::{logged_in_client, mock_sync};
use crate::{logged_in_client, mock_encryption_state, mock_sync};
#[async_test]
async fn invite_user_by_id() {
@@ -256,6 +256,7 @@ async fn room_message_send() {
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
mock_encryption_state(&server, false).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
@@ -297,6 +298,7 @@ async fn room_attachment_send() {
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
mock_encryption_state(&server, false).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
@@ -346,6 +348,7 @@ async fn room_attachment_send_info() {
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
mock_encryption_state(&server, false).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
@@ -397,6 +400,7 @@ async fn room_attachment_send_wrong_info() {
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
mock_encryption_state(&server, false).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
@@ -455,6 +459,7 @@ async fn room_attachment_send_info_thumbnail() {
.await;
mock_sync(&server, &*test_json::SYNC, None).await;
mock_encryption_state(&server, false).await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));

View File

@@ -2,6 +2,7 @@ use std::time::Duration;
use matrix_sdk::config::SyncSettings;
use matrix_sdk_test::{async_test, test_json};
use serde_json::json;
use wiremock::{
matchers::{header, method, path_regex},
Mock, ResponseTemplate,
@@ -30,3 +31,28 @@ async fn forget_room() {
room.forget().await.unwrap();
}
#[async_test]
async fn rejoin_room() {
let (client, server) = logged_in_client().await;
Mock::given(method("POST"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/join"))
.and(header("authorization", "Bearer 1234"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({ "room_id": *test_json::DEFAULT_SYNC_ROOM_ID })),
)
.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 room = client.get_left_room(&test_json::DEFAULT_SYNC_ROOM_ID).unwrap();
let joined_room = room.join().await.unwrap();
assert!(!joined_room.is_state_fully_synced())
}

View File

@@ -26,7 +26,7 @@ use wiremock::{
Mock, ResponseTemplate,
};
use crate::{logged_in_client, mock_sync};
use crate::{logged_in_client, mock_encryption_state, mock_sync};
#[async_test]
async fn edit() {
@@ -157,6 +157,8 @@ async fn echo() {
let event_id = event_id!("$wWgymRfo7ri1uQx0NXO40vLJ");
let txn_id: &TransactionId = "my-txn-id".into();
mock_encryption_state(&server, false).await;
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
.and(header("authorization", "Bearer 1234"))

View File

@@ -8,9 +8,8 @@ use matrix_sdk_appservice::{
events::room::member::{MembershipState, OriginalSyncRoomMemberEvent},
UserId,
},
RumaApiError,
},
ruma::api::client::{error::ErrorKind, uiaa::UiaaResponse},
ruma::api::client::error::ErrorKind,
AppService, AppServiceBuilder, AppServiceRegistration, Result,
};
use tracing::trace;
@@ -24,11 +23,11 @@ pub async fn handle_room_member(
trace!("not an appservice user: {}", event.state_key);
} else if let MembershipState::Invite = event.content.membership {
let user_id = UserId::parse(event.state_key.as_str())?;
if let Err(error) = appservice.register_virtual_user(user_id.localpart(), None).await {
if let Err(error) = appservice.register_user(user_id.localpart(), None).await {
error_if_user_not_in_use(error)?;
}
let client = appservice.virtual_user(Some(user_id.localpart())).await?;
let client = appservice.user(Some(user_id.localpart())).await?;
client.join_room_by_id(room.room_id()).await?;
}
@@ -39,15 +38,12 @@ pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<(
// FIXME: Use if-let chain once available
match &error {
// If user is already in use that's OK.
matrix_sdk_appservice::Error::Matrix(err) => match err.as_ruma_api_error() {
Some(RumaApiError::Uiaa(UiaaResponse::MatrixError(error)))
if matches!(error.kind, ErrorKind::UserInUse) =>
{
Ok(())
}
// In all other cases return with an error.
_ => Err(error),
},
matrix_sdk_appservice::Error::Matrix(err)
if err.client_api_error_kind() == Some(&ErrorKind::UserInUse) =>
{
Ok(())
}
// In all other cases return with an error.
_ => Err(error),
}
}
@@ -68,10 +64,10 @@ pub async fn main() -> anyhow::Result<()> {
appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await;
let virtual_user = appservice.virtual_user(None).await?;
let user = appservice.user(None).await?;
virtual_user.add_event_handler_context(appservice.clone());
virtual_user.add_event_handler(
user.add_event_handler_context(appservice.clone());
user.add_event_handler(
move |event: OriginalSyncRoomMemberEvent, room: Room, Ctx(appservice): Ctx<AppService>| {
handle_room_member(appservice, room, event)
},

View File

@@ -4,19 +4,18 @@ use matrix_sdk::{
config::SyncSettings,
room::Room,
ruma::events::room::message::{
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent, TextMessageEventContent,
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
},
Client,
};
async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
if let Room::Joined(room) = room {
let msg_body = match event.content.msgtype {
MessageType::Text(TextMessageEventContent { body, .. }) => body,
_ => return,
let MessageType::Text(text_content) = event.content.msgtype else {
return;
};
if msg_body.contains("!party") {
if text_content.body.contains("!party") {
let content = RoomMessageEventContent::text_plain("🎉🎊🥳 let's PARTY!! 🥳🎊🎉");
println!("sending");
@@ -63,14 +62,14 @@ async fn login_and_sync(
// An initial sync to set up state and so our bot doesn't respond to old
// messages. If the `StateStore` finds saved state in the location given the
// initial sync will be skipped in favor of loading state from the store
client.sync_once(SyncSettings::default()).await.unwrap();
let response = client.sync_once(SyncSettings::default()).await.unwrap();
// add our CommandBot to be notified of incoming messages, we do this after the
// initial sync to avoid responding to messages before the bot was running.
client.add_event_handler(on_room_message);
// since we called `sync_once` before we entered our sync loop we must pass
// that sync token to `sync`
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
let settings = SyncSettings::default().token(response.next_batch);
// this keeps state from the server streaming in to CommandBot via the
// EventHandler trait
client.sync(settings).await?;

View File

@@ -19,7 +19,7 @@ use matrix_sdk::{
macros::EventContent,
room::{
member::StrippedRoomMemberEvent,
message::{MessageType, OriginalSyncRoomMessageEvent, TextMessageEventContent},
message::{MessageType, OriginalSyncRoomMessageEvent},
},
},
OwnedEventId,
@@ -51,19 +51,15 @@ pub struct AckEventContent {
// we want to start the ping-ack-flow on "!ping" messages.
async fn on_regular_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
if let Room::Joined(room) = room {
let msg_body = match event.content.msgtype {
MessageType::Text(TextMessageEventContent { body, .. }) => body,
_ => return,
};
let Room::Joined(room) = room else { return };
let MessageType::Text(text_content) = event.content.msgtype else { return };
if msg_body.contains("!ping") {
let content = PingEventContent {};
if text_content.body.contains("!ping") {
let content = PingEventContent {};
println!("sending ping");
room.send(content, None).await.unwrap();
println!("ping sent");
}
println!("sending ping");
room.send(content, None).await.unwrap();
println!("ping sent");
}
}
@@ -86,7 +82,7 @@ async fn on_ping_event(event: SyncPingEvent, room: Room) {
async fn sync_loop(client: Client) -> anyhow::Result<()> {
// invite acceptance as in the getting-started-client
client.add_event_handler(on_stripped_state_member);
client.sync_once(SyncSettings::default()).await.unwrap();
let response = client.sync_once(SyncSettings::default()).await.unwrap();
// our customisation:
// - send `PingEvent` on `!ping` in any room
@@ -94,7 +90,7 @@ async fn sync_loop(client: Client) -> anyhow::Result<()> {
// - send `AckEvent` on `PingEvent` in any room
client.add_event_handler(on_ping_event);
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
let settings = SyncSettings::default().token(response.next_batch);
client.sync(settings).await?; // this essentially loops until we kill the bot
Ok(())

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