mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 13:40:55 -04:00
Merge branch 'main' into ganfra/kotlin_bindings
This commit is contained in:
14
.github/workflows/bindings_ci.yml
vendored
14
.github/workflows/bindings_ci.yml
vendored
@@ -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
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
1036
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 $*
|
||||
|
||||
@@ -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} $*
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
57
bindings/matrix-sdk-ffi/src/platform.rs
Normal file
57
bindings/matrix-sdk-ffi/src/platform.rs
Normal 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)
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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;
|
||||
@@ -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]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
119
crates/matrix-sdk-base/src/deserialized_responses.rs
Normal file
119
crates/matrix-sdk-base/src/deserialized_responses.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
164
crates/matrix-sdk-base/src/sync.rs
Normal file
164
crates/matrix-sdk-base/src/sync.rs
Normal 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() }
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(()) };
|
||||
/// ```
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
147
crates/matrix-sdk/src/events.rs
Normal file
147
crates/matrix-sdk/src/events.rs
Normal 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 {}
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)| {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user