From f4e0cab11fcaae7a08bf01722bb58cbbec73f86f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 11 Oct 2022 19:56:48 +0200 Subject: [PATCH 01/66] feat(sdk)!: Merge HttpError::UiaaError into HttpError::Api --- crates/matrix-sdk/src/error.rs | 36 +++++++++++-------- crates/matrix-sdk/tests/integration/client.rs | 8 +++-- examples/appservice_autojoin/src/main.rs | 8 +++-- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 754dbb19e..9d6111d8f 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -27,7 +27,7 @@ use matrix_sdk_base::{Error as SdkBaseError, StoreError}; use reqwest::Error as ReqwestError; use ruma::{ api::{ - client::uiaa::{UiaaInfo, UiaaResponse as UiaaError}, + client::uiaa::{UiaaInfo, UiaaResponse}, error::{FromHttpResponseError, IntoHttpError, ServerError}, }, events::tag::InvalidUserTagName, @@ -51,6 +51,15 @@ pub enum RumaApiError { #[error(transparent)] ClientApi(ruma::api::client::Error), + /// A user-interactive authentication API error. + /// + /// When registering or authenticating, the Matrix server can send a + /// `UiaaResponse` as the error type, this is a User-Interactive + /// Authentication API response. This represents an error with + /// information about how to authenticate the user. + #[error(transparent)] + Uiaa(UiaaResponse), + /// Another API response error. #[error(transparent)] Other(ruma::api::error::MatrixError), @@ -81,15 +90,6 @@ pub enum HttpError { #[error(transparent)] IntoHttp(#[from] IntoHttpError), - /// An error occurred while authenticating. - /// - /// When registering or authenticating the Matrix server can send a - /// `UiaaResponse` as the error type, this is a User-Interactive - /// Authentication API response. This represents an error with - /// information about how to authenticate the user. - #[error(transparent)] - UiaaError(#[from] FromHttpResponseError), - /// The server returned a status code that should be retried. #[error("Server returned an error {0}")] Server(StatusCode), @@ -229,8 +229,8 @@ impl HttpError { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( - UiaaError::AuthResponse(i), + if let HttpError::Api(FromHttpResponseError::Server(ServerError::Known( + RumaApiError::Uiaa(UiaaResponse::AuthResponse(i)), ))) = self { Some(i) @@ -253,9 +253,9 @@ impl Error { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let Error::Http(HttpError::UiaaError(FromHttpResponseError::Server( - ServerError::Known(UiaaError::AuthResponse(i)), - ))) = self + if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( + RumaApiError::Uiaa(UiaaResponse::AuthResponse(i)), + )))) = self { Some(i) } else { @@ -270,6 +270,12 @@ impl From> for HttpError { } } +impl From> for HttpError { + fn from(err: FromHttpResponseError) -> Self { + Self::Api(err.map(|e| e.map(RumaApiError::Uiaa))) + } +} + impl From> for HttpError { fn from(err: FromHttpResponseError) -> Self { Self::Api(err.map(|e| e.map(RumaApiError::Other))) diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index e6fd39606..8286b9aef 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -228,8 +228,12 @@ async fn register_error() { }); if let Err(err) = client.register(user).await { - if let HttpError::UiaaError(FromHttpResponseError::Server(ServerError::Known( - UiaaResponse::MatrixError(client_api::Error { kind, message, status_code }), + if let HttpError::Api(FromHttpResponseError::Server(ServerError::Known( + RumaApiError::Uiaa(UiaaResponse::MatrixError(client_api::Error { + kind, + message, + status_code, + })), ))) = err { if let client_api::error::ErrorKind::Forbidden = kind { diff --git a/examples/appservice_autojoin/src/main.rs b/examples/appservice_autojoin/src/main.rs index a68ab3a73..b1ceebc58 100644 --- a/examples/appservice_autojoin/src/main.rs +++ b/examples/appservice_autojoin/src/main.rs @@ -9,7 +9,7 @@ use matrix_sdk_appservice::{ events::room::member::{MembershipState, OriginalSyncRoomMemberEvent}, UserId, }, - HttpError, + HttpError, RumaApiError, }, ruma::api::{ client::{error::ErrorKind, uiaa::UiaaResponse}, @@ -42,8 +42,10 @@ pub async fn handle_room_member( pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> { match error { // If user is already in use that's OK. - matrix_sdk_appservice::Error::Matrix(matrix_sdk::Error::Http(HttpError::UiaaError( - FromHttpResponseError::Server(ServerError::Known(UiaaResponse::MatrixError(error))), + matrix_sdk_appservice::Error::Matrix(matrix_sdk::Error::Http(HttpError::Api( + FromHttpResponseError::Server(ServerError::Known(RumaApiError::Uiaa( + UiaaResponse::MatrixError(error), + ))), ))) if matches!(error.kind, ErrorKind::UserInUse) => Ok(()), // In all other cases return with an error. error => Err(error), From 7bfa622a574c041494ad6140b818e5fb39733db5 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 11 Oct 2022 20:07:21 +0200 Subject: [PATCH 02/66] feat(sdk): Add HttpError::as_ruma_api_error --- crates/matrix-sdk/src/error.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 9d6111d8f..8133f87d9 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -103,6 +103,18 @@ pub enum HttpError { RefreshToken(#[from] RefreshTokenError), } +impl HttpError { + /// If `self` is `Api(Server(Known(e)))`, 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), + _ => None, + } + } +} + /// Internal representation of errors. #[derive(Error, Debug)] #[non_exhaustive] From 23c5929cfa1ccddf478456c809a0e8c3204f1627 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 11 Oct 2022 20:08:54 +0200 Subject: [PATCH 03/66] feat(sdk): Add Error::as_ruma_api_error --- crates/matrix-sdk/src/error.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 8133f87d9..1e56ff766 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -200,6 +200,18 @@ pub enum Error { UnknownError(Box), } +impl Error { + /// If `self` is `Http(Api(Server(Known(e))))`, returns `Some(e)`. + /// + /// Otherwise, returns `None`. + pub fn as_ruma_api_error(&self) -> Option<&RumaApiError> { + match self { + Error::Http(e) => e.as_ruma_api_error(), + _ => None, + } + } +} + /// Error for the room key importing functionality. #[cfg(feature = "e2e-encryption")] #[derive(Error, Debug)] From d18b57dcddbd377f20160f00ec408cac36feb37a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 12 Oct 2022 11:18:34 +0200 Subject: [PATCH 04/66] refactor: Make use of as_ruma_api_error internally --- bindings/matrix-sdk-ffi/src/client.rs | 24 +++---- crates/matrix-sdk/src/client/mod.rs | 26 ++++---- crates/matrix-sdk/src/error.rs | 20 ++---- crates/matrix-sdk/tests/integration/client.rs | 64 +++++++++---------- .../tests/integration/refresh_token.rs | 19 ++---- examples/appservice_autojoin/src/main.rs | 28 ++++---- 6 files changed, 77 insertions(+), 104 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 452b3491a..fdffe5271 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -5,22 +5,19 @@ use matrix_sdk::{ config::SyncSettings, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, ruma::{ - api::{ - client::{ - account::whoami, - error::ErrorKind, - filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, - media::get_content_thumbnail::v3::Method, - session::get_login_types, - sync::sync_events::v3::Filter, - }, - error::{FromHttpResponseError, ServerError}, + api::client::{ + account::whoami, + error::ErrorKind, + filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, + media::get_content_thumbnail::v3::Method, + session::get_login_types, + sync::sync_events::v3::Filter, }, events::room::MediaSource, serde::Raw, TransactionId, UInt, }, - Client as MatrixClient, Error, HttpError, LoopCtrl, RumaApiError, Session, + Client as MatrixClient, Error, LoopCtrl, RumaApiError, Session, }; use super::{ @@ -268,10 +265,7 @@ impl Client { /// Process a sync error and return loop control accordingly fn process_sync_error(&self, sync_error: Error) -> LoopCtrl { let mut control = LoopCtrl::Continue; - if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = sync_error - { + 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() { diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index ec9fb18dd..6d164cd69 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -58,7 +58,7 @@ use ruma::{ sync::sync_events, uiaa::{AuthData, UserIdentifier}, }, - error::{FromHttpResponseError, ServerError}, + error::FromHttpResponseError, MatrixVersion, OutgoingRequest, SendAccessToken, }, assign, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, @@ -1338,13 +1338,11 @@ impl Client { Ok(Some(res)) } Err(error) => { - *guard = if let HttpError::Api(FromHttpResponseError::Server( - ServerError::Known(RumaApiError::ClientApi(api_error)), - )) = &error - { - Err(RefreshTokenError::ClientApi(api_error.to_owned())) - } else { - Err(RefreshTokenError::UnableToRefreshToken) + *guard = match error.as_ruma_api_error() { + Some(RumaApiError::ClientApi(api_error)) => { + Err(RefreshTokenError::ClientApi(api_error.to_owned())) + } + _ => Err(RefreshTokenError::UnableToRefreshToken), }; Err(error) @@ -1688,9 +1686,9 @@ 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 { - if let Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = &res + // 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 matches!(error.kind, ErrorKind::UnknownToken { .. }) { let refresh_res = self.refresh_access_token().await; @@ -1733,9 +1731,9 @@ 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 { - if let Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(error), - )))) = &res + // 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 matches!(error.kind, ErrorKind::UnknownToken { .. }) { let refresh_res = self.refresh_access_token().await; diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 1e56ff766..ad28e05de 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -253,13 +253,9 @@ impl HttpError { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::Uiaa(UiaaResponse::AuthResponse(i)), - ))) = self - { - Some(i) - } else { - None + match self.as_ruma_api_error() { + Some(RumaApiError::Uiaa(UiaaResponse::AuthResponse(i))) => Some(i), + _ => None, } } } @@ -277,13 +273,9 @@ impl Error { /// This method is an convenience method to get to the info the server /// returned on the first, failed request. pub fn uiaa_response(&self) -> Option<&UiaaInfo> { - if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::Uiaa(UiaaResponse::AuthResponse(i)), - )))) = self - { - Some(i) - } else { - None + match self.as_ruma_api_error() { + Some(RumaApiError::Uiaa(UiaaResponse::AuthResponse(i))) => Some(i), + _ => None, } } } diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 8286b9aef..ef69fb287 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -3,23 +3,20 @@ use std::{collections::BTreeMap, str::FromStr, time::Duration}; use matrix_sdk::{ config::SyncSettings, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, - Error, HttpError, RumaApiError, Session, + RumaApiError, Session, }; use matrix_sdk_test::{async_test, test_json}; use ruma::{ - api::{ - client::{ - self as client_api, - account::register::{v3::Request as RegistrationRequest, RegistrationKind}, - directory::{ - get_public_rooms, - get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, - }, - media::get_content_thumbnail::v3::Method, - session::get_login_types::v3::LoginType, - uiaa::{self, UiaaResponse}, + api::client::{ + self as client_api, + account::register::{v3::Request as RegistrationRequest, RegistrationKind}, + directory::{ + get_public_rooms, + get_public_rooms_filtered::{self, v3::Request as PublicRoomsFilterRequest}, }, - error::{FromHttpResponseError, ServerError}, + media::get_content_thumbnail::v3::Method, + session::get_login_types::v3::LoginType, + uiaa::{self, UiaaResponse}, }, assign, device_id, directory::Filter, @@ -188,18 +185,17 @@ async fn login_error() { .await; if let Err(err) = client.login_username("example", "wordpass").send().await { - if let Error::Http(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(client_api::Error { kind, message, status_code }), - )))) = err + if let Some(RumaApiError::ClientApi(client_api::Error { kind, message, status_code })) = + err.as_ruma_api_error() { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + if *kind != client_api::error::ErrorKind::Forbidden { + panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden"); } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + + assert_eq!(message, "Invalid password"); + assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap()); } else { - panic!("found the wrong `Error` type {:?}, expected `Error::RumaResponse", err); + panic!("found the wrong `Error` type {err:?}, expected `Error::RumaResponse"); } } else { panic!("this request should return an `Err` variant") @@ -228,22 +224,20 @@ async fn register_error() { }); if let Err(err) = client.register(user).await { - if let HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::Uiaa(UiaaResponse::MatrixError(client_api::Error { - kind, - message, - status_code, - })), - ))) = err + if let Some(RumaApiError::Uiaa(UiaaResponse::MatrixError(client_api::Error { + kind, + message, + status_code, + }))) = err.as_ruma_api_error() { - if let client_api::error::ErrorKind::Forbidden = kind { - } else { - panic!("found the wrong `ErrorKind` {:?}, expected `Forbidden", kind); + if *kind != client_api::error::ErrorKind::Forbidden { + panic!("found the wrong `ErrorKind` {kind:?}, expected `Forbidden"); } - assert_eq!(message, "Invalid password".to_owned()); - assert_eq!(status_code, http::StatusCode::from_u16(403).unwrap()); + + assert_eq!(message, "Invalid password"); + assert_eq!(*status_code, http::StatusCode::from_u16(403).unwrap()); } else { - panic!("found the wrong `Error` type {:#?}, expected `UiaaResponse`", err); + panic!("found the wrong `Error` type {err:#?}, expected `UiaaResponse`"); } } else { panic!("this request should return an `Err` variant") diff --git a/crates/matrix-sdk/tests/integration/refresh_token.rs b/crates/matrix-sdk/tests/integration/refresh_token.rs index a433388a5..eedc4cb6c 100644 --- a/crates/matrix-sdk/tests/integration/refresh_token.rs +++ b/crates/matrix-sdk/tests/integration/refresh_token.rs @@ -13,7 +13,6 @@ use matrix_sdk_test::{async_test, test_json}; use ruma::{ api::{ client::{account::register, error::ErrorKind, Error as ClientApiError}, - error::{FromHttpResponseError, ServerError}, MatrixVersion, }, assign, device_id, user_id, @@ -229,13 +228,11 @@ async fn refresh_token_not_handled() { .mount(&server) .await; - let res = client.whoami().await; + let res = client.whoami().await.unwrap_err(); assert_matches!( - res, - Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(ClientApiError { kind, .. }) - )))) if matches!(kind, ErrorKind::UnknownToken { .. }) - ) + res.as_ruma_api_error(), + Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. })) + ); } #[async_test] @@ -362,12 +359,10 @@ async fn refresh_token_handled_failure() { .mount(&server) .await; - let res = client.whoami().await; + let res = client.whoami().await.unwrap_err(); assert_matches!( - res, - Err(HttpError::Api(FromHttpResponseError::Server(ServerError::Known( - RumaApiError::ClientApi(ClientApiError { kind, .. }) - )))) if matches!(kind, ErrorKind::UnknownToken { .. }) + res.as_ruma_api_error(), + Some(RumaApiError::ClientApi(ClientApiError { kind: ErrorKind::UnknownToken { .. }, .. })) ) } diff --git a/examples/appservice_autojoin/src/main.rs b/examples/appservice_autojoin/src/main.rs index b1ceebc58..de95c0d3e 100644 --- a/examples/appservice_autojoin/src/main.rs +++ b/examples/appservice_autojoin/src/main.rs @@ -2,19 +2,15 @@ use std::env; use matrix_sdk_appservice::{ matrix_sdk::{ - self, event_handler::Ctx, room::Room, ruma::{ events::room::member::{MembershipState, OriginalSyncRoomMemberEvent}, UserId, }, - HttpError, RumaApiError, - }, - ruma::api::{ - client::{error::ErrorKind, uiaa::UiaaResponse}, - error::{FromHttpResponseError, ServerError}, + RumaApiError, }, + ruma::api::client::{error::ErrorKind, uiaa::UiaaResponse}, AppService, AppServiceBuilder, AppServiceRegistration, Result, }; use tracing::trace; @@ -40,15 +36,19 @@ pub async fn handle_room_member( } pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> { - match error { + // FIXME: Use if-let chain once available + match &error { // If user is already in use that's OK. - matrix_sdk_appservice::Error::Matrix(matrix_sdk::Error::Http(HttpError::Api( - FromHttpResponseError::Server(ServerError::Known(RumaApiError::Uiaa( - UiaaResponse::MatrixError(error), - ))), - ))) if matches!(error.kind, ErrorKind::UserInUse) => Ok(()), - // In all other cases return with an error. - error => Err(error), + 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), + }, + _ => Err(error), } } From cbadd90efff23be0098647a47070e6ce150d3dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 18 Aug 2022 12:38:32 +0200 Subject: [PATCH 05/66] feat(crypto): Start responding to megolm v2 room key requests --- .../src/gossiping/machine.rs | 88 ++++++++++--------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 228ce4580..5e4d53330 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -46,9 +46,7 @@ use crate::{ forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent}, olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, room::encrypted::EncryptedEvent, - room_key_request::{ - Action, MegolmV1AesSha2Content, RequestedKeyInfo, RoomKeyRequestEvent, - }, + room_key_request::{Action, RequestedKeyInfo, RoomKeyRequestEvent}, secret_send::SecretSendContent, EventType, }, @@ -310,27 +308,13 @@ impl GossipMachine { }) } - async fn handle_megolm_v1_request( + /// Answer a room key request after we found the matching + /// `InboundGroupSession`. + async fn answer_room_key_request( &self, event: &RoomKeyRequestEvent, - key_info: &MegolmV1AesSha2Content, + session: InboundGroupSession, ) -> OlmResult> { - let session = - self.store.get_inbound_group_session(&key_info.room_id, &key_info.session_id).await?; - - let session = if let Some(s) = session { - s - } else { - debug!( - user_id = event.sender.as_str(), - device_id = event.content.requesting_device_id.as_str(), - session_id = key_info.session_id.as_str(), - room_id = key_info.room_id.as_str(), - "Received a room key request for an unknown inbound group session", - ); - return Ok(None); - }; - let device = self.store.get_device(&event.sender, &event.content.requesting_device_id).await?; @@ -342,7 +326,7 @@ impl GossipMachine { 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" + their Curve25519 sender key" ); } else { debug!( @@ -357,21 +341,21 @@ impl GossipMachine { } Ok(message_index) => { info!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), - room_id = key_info.room_id.as_str(), + 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.share_session(&session, &device, message_index).await { + match self.forward_room_key(&session, &device, message_index).await { Ok(s) => Ok(Some(s)), Err(OlmError::MissingSession) => { info!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), + 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", ); @@ -381,9 +365,9 @@ impl GossipMachine { } Err(OlmError::SessionExport(e)) => { warn!( - user_id = device.user_id().as_str(), - device_id = device.device_id().as_str(), - session_id = key_info.session_id.as_str(), + 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: \ {:?}", @@ -397,8 +381,8 @@ impl GossipMachine { } } else { warn!( - user_id = event.sender.as_str(), - device_id = event.content.requesting_device_id.as_str(), + user_id = %event.sender, + device_id = %event.content.requesting_device_id, "Received a key request from an unknown device", ); self.store.update_tracked_user(&event.sender, true).await?; @@ -407,18 +391,40 @@ impl GossipMachine { } } + async fn handle_supported_key_request( + &self, + event: &RoomKeyRequestEvent, + room_id: &RoomId, + session_id: &str, + ) -> OlmResult> { + let session = self.store.get_inbound_group_session(room_id, session_id).await?; + + if let Some(s) = session { + self.answer_room_key_request(event, s).await + } else { + debug!( + user_id = %event.sender, + device_id = %event.content.requesting_device_id, + session_id, + %room_id, + "Received a room key request for an unknown inbound group session", + ); + + Ok(None) + } + } + /// Handle a single incoming key request. async fn handle_key_request(&self, event: &RoomKeyRequestEvent) -> OlmResult> { match &event.content.action { Action::Request(info) => match info { RequestedKeyInfo::MegolmV1AesSha2(i) => { - self.handle_megolm_v1_request(event, i).await + self.handle_supported_key_request(event, &i.room_id, &i.session_id).await } - // V2 room key requests don't have a sender_key field, we - // currently can't fetch an inbound group session without a - // sender key, so ignore the request. #[cfg(feature = "experimental-algorithms")] - RequestedKeyInfo::MegolmV2AesSha2(_) => Ok(None), + RequestedKeyInfo::MegolmV2AesSha2(i) => { + self.handle_supported_key_request(event, &i.room_id, &i.session_id).await + } RequestedKeyInfo::Unknown(i) => { debug!( sender = %event.sender, @@ -458,7 +464,7 @@ impl GossipMachine { Ok(used_session) } - async fn share_session( + async fn forward_room_key( &self, session: &InboundGroupSession, device: &Device, From 7c97c27441d4668259e20f27cbd475040ae9d05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 18 Aug 2022 17:01:00 +0200 Subject: [PATCH 06/66] feat(crypto): Start sending out megolm v2 room key requests --- crates/matrix-sdk-crypto/src/gossiping/mod.rs | 66 +++++++++---------- crates/matrix-sdk-crypto/src/requests.rs | 13 ++++ .../src/types/events/room_key_request.rs | 12 ++++ 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 2c2b0450d..db598e9c2 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -20,13 +20,14 @@ use dashmap::{DashMap, DashSet}; pub(crate) use machine::GossipMachine; use ruma::{ events::{ - room_key_request::{Action, RequestedKeyInfo, ToDeviceRoomKeyRequestEventContent}, + room_key_request::{Action, ToDeviceRoomKeyRequestEventContent}, secret::request::{ RequestAction, SecretName, ToDeviceSecretRequestEvent as SecretRequestEvent, ToDeviceSecretRequestEventContent as SecretRequestEventContent, }, - AnyToDeviceEventContent, + AnyToDeviceEventContent, ToDeviceEventType, }, + serde::Raw, to_device::DeviceIdOrAllDevices, DeviceId, OwnedDeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId, }; @@ -36,7 +37,9 @@ use tracing::error; use crate::{ requests::{OutgoingRequest, ToDeviceRequest}, - types::events::room_key_request::{RoomKeyRequestEvent, SupportedKeyInfo}, + types::events::room_key_request::{ + RoomKeyRequestContent, RoomKeyRequestEvent, SupportedKeyInfo, + }, Device, }; @@ -132,45 +135,42 @@ impl GossipRequest { } fn to_request(&self, own_device_id: &DeviceId) -> OutgoingRequest { - let content = match &self.info { + let request = match &self.info { SecretInfo::KeyRequest(r) => { - let info = match r { - SupportedKeyInfo::MegolmV1AesSha2(c) => RequestedKeyInfo::new( - ruma::EventEncryptionAlgorithm::MegolmV1AesSha2, - c.room_id.to_owned(), - c.sender_key.to_base64(), - c.session_id.to_owned(), - ), - #[cfg(feature = "experimental-algorithms")] - #[allow(clippy::todo)] - SupportedKeyInfo::MegolmV2AesSha2(_) => { - todo!("Requesting megolm.v2 room keys is not supported yet") - } - }; - - AnyToDeviceEventContent::RoomKeyRequest(ToDeviceRoomKeyRequestEventContent::new( - Action::Request, - Some(info), + let content = RoomKeyRequestContent::new_request( + r.clone().into(), own_device_id.to_owned(), + self.request_id.to_owned(), + ); + let content = Raw::new(&content) + .expect("We can always serialize a room key request info") + .cast(); + + ToDeviceRequest::with_id_raw( + &self.request_recipient, + DeviceIdOrAllDevices::AllDevices, + content, + ToDeviceEventType::RoomKeyRequest, self.request_id.clone(), - )) + ) } SecretInfo::SecretRequest(s) => { - AnyToDeviceEventContent::SecretRequest(SecretRequestEventContent::new( - RequestAction::Request(s.clone()), - own_device_id.to_owned(), + let content = + AnyToDeviceEventContent::SecretRequest(SecretRequestEventContent::new( + RequestAction::Request(s.clone()), + own_device_id.to_owned(), + self.request_id.clone(), + )); + + ToDeviceRequest::with_id( + &self.request_recipient, + DeviceIdOrAllDevices::AllDevices, + content, self.request_id.clone(), - )) + ) } }; - let request = ToDeviceRequest::with_id( - &self.request_recipient, - DeviceIdOrAllDevices::AllDevices, - content, - self.request_id.clone(), - ); - OutgoingRequest { request_id: request.txn_id.clone(), request: Arc::new(request.into()) } } diff --git a/crates/matrix-sdk-crypto/src/requests.rs b/crates/matrix-sdk-crypto/src/requests.rs index fa382bb52..5cec2b344 100644 --- a/crates/matrix-sdk-crypto/src/requests.rs +++ b/crates/matrix-sdk-crypto/src/requests.rs @@ -116,6 +116,19 @@ impl ToDeviceRequest { } } + pub(crate) fn with_id_raw( + recipient: &UserId, + recipient_device: impl Into, + content: Raw, + event_type: ToDeviceEventType, + txn_id: OwnedTransactionId, + ) -> Self { + let user_messages = iter::once((recipient_device.into(), content)).collect(); + let messages = iter::once((recipient.to_owned(), user_messages)).collect(); + + ToDeviceRequest { event_type, txn_id, messages } + } + pub(crate) fn with_id( recipient: &UserId, recipient_device: impl Into, diff --git a/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs b/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs index dd6ab19ee..a87a71691 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room_key_request.rs @@ -51,6 +51,18 @@ pub struct RoomKeyRequestContent { pub request_id: OwnedTransactionId, } +impl RoomKeyRequestContent { + /// Create a new content for a `m.room_key_request` event with the action + /// set to request a room key with the given `RequestedKeyInfo`. + pub fn new_request( + info: RequestedKeyInfo, + requesting_device_id: OwnedDeviceId, + request_id: OwnedTransactionId, + ) -> Self { + Self { action: Action::Request(info), requesting_device_id, request_id } + } +} + impl EventType for RoomKeyRequestContent { const EVENT_TYPE: &'static str = "m.room_key_request"; } From 2425cd99cff54d17d7421a6f588246466e3c28e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 18 Aug 2022 17:16:20 +0200 Subject: [PATCH 07/66] fix(crypto): Introduce the proper megolm v2 forwarded room key content --- .../src/gossiping/machine.rs | 102 ++++++++--------- .../src/olm/group_sessions/inbound.rs | 104 +++++++++++------- .../src/olm/group_sessions/mod.rs | 91 +++++++++------ .../src/types/events/forwarded_room_key.rs | 58 +++++++++- 4 files changed, 221 insertions(+), 134 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index 5e4d53330..fe7b67a48 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -41,16 +41,13 @@ use crate::{ requests::{OutgoingRequest, ToDeviceRequest}, session_manager::GroupSessionCache, store::{Changes, CryptoStoreError, SecretImportError, Store}, - types::{ - events::{ - forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent}, - olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, - room::encrypted::EncryptedEvent, - room_key_request::{Action, RequestedKeyInfo, RoomKeyRequestEvent}, - secret_send::SecretSendContent, - EventType, - }, - EventEncryptionAlgorithm, + types::events::{ + forwarded_room_key::ForwardedRoomKeyContent, + olm_v1::{DecryptedForwardedRoomKeyEvent, DecryptedSecretSendEvent}, + room::encrypted::EncryptedEvent, + room_key_request::{Action, RequestedKeyInfo, RoomKeyRequestEvent}, + secret_send::SecretSendContent, + EventType, }, Device, MegolmError, }; @@ -695,18 +692,6 @@ impl GossipMachine { Ok(()) } - /// Get an outgoing key info that matches the forwarded room key content. - async fn get_key_info( - &self, - event: &DecryptedForwardedRoomKeyEvent, - ) -> Result, CryptoStoreError> { - if let Some(info) = event.room_key_info().map(|i| i.into()) { - self.store.get_secret_request_by_info(&info).await - } else { - Ok(None) - } - } - /// Delete the given outgoing key info. async fn delete_key_info(&self, info: &GossipRequest) -> Result<(), CryptoStoreError> { self.store.delete_outgoing_secret_requests(&info.request_id).await @@ -874,18 +859,16 @@ impl GossipMachine { async fn accept_forwarded_room_key( &self, info: &GossipRequest, - sender: &UserId, sender_key: Curve25519PublicKey, - algorithm: EventEncryptionAlgorithm, - content: &ForwardedMegolmV1AesSha2Content, + event: &DecryptedForwardedRoomKeyEvent, ) -> Result, CryptoStoreError> { - match InboundGroupSession::from_forwarded_key(&algorithm, content) { + match InboundGroupSession::try_from(event) { Ok(session) => { if self.store.compare_group_session(&session).await? == SessionOrdering::Better { self.mark_as_done(info).await?; info!( - %sender, + sender = %event.sender, %sender_key, claimed_sender_key = %session.sender_key(), room_id = session.room_id().as_str(), @@ -897,7 +880,7 @@ impl GossipMachine { Ok(Some(session)) } else { info!( - %sender, + sender = %event.sender, %sender_key, claimed_sender_key = %session.sender_key(), room_id = %session.room_id, @@ -911,11 +894,8 @@ impl GossipMachine { } Err(e) => { warn!( - %sender, - sender_key = sender_key.to_base64(), - claimed_sender_key = content.claimed_sender_key.to_base64(), - room_id = content.room_id.as_str(), - %algorithm, + sender = %event.sender, + %sender_key, "Couldn't create a group session from a received room key" ); Err(e.into()) @@ -944,36 +924,44 @@ impl GossipMachine { &self, sender_key: Curve25519PublicKey, event: &DecryptedForwardedRoomKeyEvent, - content: &ForwardedMegolmV1AesSha2Content, ) -> Result, CryptoStoreError> { - let algorithm = event.content.algorithm(); + 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", + ); - if let Some(info) = self.get_key_info(event).await? { - if self.should_accept_forward(&info, sender_key).await? { - self.accept_forwarded_room_key(&info, &event.sender, sender_key, algorithm, content) - .await + Ok(None) + } } else { warn!( sender = %event.sender, - %sender_key, - room_id = %content.room_id, - session_id = content.session_id.as_str(), - claimed_sender_key = %content.claimed_sender_key, - "Received a forwarded room key from an unknown device, or \ - from a device that the key request recipient doesn't own", + sender_key = %sender_key, + room_id = %info.room_id(), + session_id = info.session_id(), + sender_key = %sender_key, + algorithm = %info.algorithm(), + "Received a forwarded room key that we didn't request", ); Ok(None) } } else { warn!( - sender = %event.sender, - sender_key = %sender_key, - room_id = %content.room_id, - session_id = content.session_id.as_str(), - claimed_sender_key = %content.claimed_sender_key, - algorithm = %algorithm, - "Received a forwarded room key that we didn't request", + sender = event.sender.as_str(), + sender_key = sender_key.to_base64(), + algorithm = %event.content.algorithm(), + "Received a forwarded room key with an unsupported algorithm", ); Ok(None) @@ -986,13 +974,13 @@ impl GossipMachine { sender_key: Curve25519PublicKey, event: &DecryptedForwardedRoomKeyEvent, ) -> Result, CryptoStoreError> { - match &event.content { - ForwardedRoomKeyContent::MegolmV1AesSha2(content) => { - self.receive_supported_keys(sender_key, event, content).await + match event.content { + ForwardedRoomKeyContent::MegolmV1AesSha2(_) => { + self.receive_supported_keys(sender_key, event).await } #[cfg(feature = "experimental-algorithms")] - ForwardedRoomKeyContent::MegolmV2AesSha2(content) => { - self.receive_supported_keys(sender_key, event, content).await + ForwardedRoomKeyContent::MegolmV2AesSha2(_) => { + self.receive_supported_keys(sender_key, event).await } ForwardedRoomKeyContent::Unknown(_) => { warn!( diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs index 6d62b60b1..5ba13c7c7 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/inbound.rs @@ -14,6 +14,7 @@ use std::{ fmt, + ops::Deref, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, @@ -44,7 +45,11 @@ use crate::{ types::{ deserialize_curve_key, events::{ - forwarded_room_key::ForwardedMegolmV1AesSha2Content, + forwarded_room_key::{ + ForwardedMegolmV1AesSha2Content, ForwardedMegolmV2AesSha2Content, + ForwardedRoomKeyContent, + }, + olm_v1::DecryptedForwardedRoomKeyEvent, room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme}, }, serialize_curve_key, EventEncryptionAlgorithm, SigningKeys, @@ -157,42 +162,6 @@ impl InboundGroupSession { }) } - /// Create a new inbound group session from a forwarded room key content. - /// - /// # Arguments - /// - /// * `sender_key` - The public curve25519 key of the account that - /// sent us the session - /// - /// * `content` - A forwarded room key content that contains the session key - /// to create the `InboundGroupSession`. - pub fn from_forwarded_key( - algorithm: &EventEncryptionAlgorithm, - content: &ForwardedMegolmV1AesSha2Content, - ) -> Result { - let config = OutboundGroupSession::session_config(algorithm)?; - - let session = InnerSession::import(&content.session_key, config); - - let first_known_index = session.first_known_index(); - - let mut sender_claimed_key = SigningKeys::new(); - sender_claimed_key.insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); - - Ok(InboundGroupSession { - inner: Mutex::new(session).into(), - session_id: content.session_id.as_str().into(), - sender_key: content.claimed_sender_key, - first_known_index, - history_visibility: None.into(), - signing_keys: sender_claimed_key.into(), - room_id: (*content.room_id).into(), - imported: true, - backed_up: AtomicBool::new(false).into(), - algorithm: algorithm.to_owned().into(), - }) - } - /// Store the group session as a base64 encoded string. /// /// # Arguments @@ -498,6 +467,67 @@ impl TryFrom<&ExportedRoomKey> for InboundGroupSession { } } +impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession { + fn from(value: &ForwardedMegolmV1AesSha2Content) -> Self { + let session = InnerSession::import(&value.session_key, SessionConfig::version_1()); + let session_id = session.session_id().into(); + let first_known_index = session.first_known_index(); + + InboundGroupSession { + inner: Mutex::new(session).into(), + session_id, + sender_key: value.claimed_sender_key, + history_visibility: None.into(), + first_known_index, + signing_keys: SigningKeys::from([( + DeviceKeyAlgorithm::Ed25519, + value.claimed_ed25519_key.into(), + )]) + .into(), + room_id: value.room_id.to_owned().into(), + imported: true, + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(), + backed_up: AtomicBool::from(false).into(), + } + } +} + +impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession { + fn from(value: &ForwardedMegolmV2AesSha2Content) -> Self { + let session = InnerSession::import(&value.session_key, SessionConfig::version_2()); + let session_id = session.session_id().into(); + let first_known_index = session.first_known_index(); + + InboundGroupSession { + inner: Mutex::new(session).into(), + session_id, + sender_key: value.claimed_sender_key, + history_visibility: None.into(), + first_known_index, + signing_keys: value.claimed_signing_keys.to_owned().into(), + room_id: value.room_id.to_owned().into(), + imported: true, + algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(), + backed_up: AtomicBool::from(false).into(), + } + } +} + +impl TryFrom<&DecryptedForwardedRoomKeyEvent> for InboundGroupSession { + type Error = SessionCreationError; + + fn try_from(value: &DecryptedForwardedRoomKeyEvent) -> Result { + match &value.content { + ForwardedRoomKeyContent::MegolmV1AesSha2(c) => Ok(Self::from(c.deref())), + #[cfg(feature = "experimental-algorithms")] + ForwardedRoomKeyContent::MegolmV2AesSha2(c) => Ok(Self::from(c.deref())), + ForwardedRoomKeyContent::Unknown(c) => { + Err(SessionCreationError::Algorithm(c.algorithm.to_owned())) + } + } + } +} + #[cfg(test)] mod test { use matrix_sdk_test::async_test; diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs index 75461453b..0a1484ec9 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs @@ -27,6 +27,8 @@ use thiserror::Error; pub use vodozemac::megolm::{ExportedSessionKey, SessionKey}; use vodozemac::{megolm::SessionKeyDecodeError, Curve25519PublicKey}; +#[cfg(feature = "experimental-algorithms")] +use crate::types::events::forwarded_room_key::ForwardedMegolmV2AesSha2Content; use crate::types::{ deserialize_curve_key, deserialize_curve_key_vec, events::forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent}, @@ -128,35 +130,50 @@ impl TryFrom for ForwardedRoomKeyContent { /// This will fail if the exported room key doesn't contain an Ed25519 /// claimed sender key. fn try_from(room_key: ExportedRoomKey) -> Result { - // The forwarded room key content only supports a single claimed sender - // key and it requires it to be a Ed25519 key. This here will be lossy - // conversion since we're dropping all other key types. - // - // This isn't yet a problem since no other key types exist, but still - // something that will need to be addressed sooner or later. - if let Some(SigningKey::Ed25519(claimed_ed25519_key)) = - room_key.sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519) - { - if room_key.algorithm == EventEncryptionAlgorithm::MegolmV1AesSha2 { - Ok(ForwardedRoomKeyContent::MegolmV1AesSha2( - ForwardedMegolmV1AesSha2Content { + match room_key.algorithm { + EventEncryptionAlgorithm::MegolmV1AesSha2 => { + // The forwarded room key content only supports a single claimed sender + // key and it requires it to be a Ed25519 key. This here will be lossy + // conversion since we're dropping all other key types. + // + // This was fixed by the megolm v2 content. Hopefully we'll deprecate megolm v1 + // before we have multiple signing keys. + if let Some(SigningKey::Ed25519(claimed_ed25519_key)) = + room_key.sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519) + { + Ok(ForwardedRoomKeyContent::MegolmV1AesSha2( + ForwardedMegolmV1AesSha2Content { + room_id: room_key.room_id, + session_id: room_key.session_id, + session_key: room_key.session_key, + claimed_sender_key: room_key.sender_key, + claimed_ed25519_key: *claimed_ed25519_key, + forwarding_curve25519_key_chain: room_key + .forwarding_curve25519_key_chain + .clone(), + other: Default::default(), + } + .into(), + )) + } else { + Err(SessionExportError::MissingEd25519Key) + } + } + #[cfg(feature = "experimental-algorithms")] + EventEncryptionAlgorithm::MegolmV2AesSha2 => { + Ok(ForwardedRoomKeyContent::MegolmV2AesSha2( + ForwardedMegolmV2AesSha2Content { room_id: room_key.room_id, session_id: room_key.session_id, session_key: room_key.session_key, claimed_sender_key: room_key.sender_key, - claimed_ed25519_key: *claimed_ed25519_key, - forwarding_curve25519_key_chain: room_key - .forwarding_curve25519_key_chain - .clone(), + claimed_signing_keys: room_key.sender_claimed_keys, other: Default::default(), } .into(), )) - } else { - Err(SessionExportError::MissingEd25519Key) } - } else { - Err(SessionExportError::Algorithm(room_key.algorithm)) + _ => Err(SessionExportError::Algorithm(room_key.algorithm)), } } } @@ -180,26 +197,32 @@ impl TryFrom for ExportedRoomKey { fn try_from(forwarded_key: ForwardedRoomKeyContent) -> Result { let algorithm = forwarded_key.algorithm(); - let handle_key = |content: Box| { - let mut sender_claimed_keys = SigningKeys::new(); - sender_claimed_keys - .insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); + match forwarded_key { + ForwardedRoomKeyContent::MegolmV1AesSha2(content) => { + let mut sender_claimed_keys = SigningKeys::new(); + sender_claimed_keys + .insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.into()); - Ok(Self { + Ok(Self { + algorithm, + room_id: content.room_id, + session_id: content.session_id, + forwarding_curve25519_key_chain: content.forwarding_curve25519_key_chain, + sender_claimed_keys, + sender_key: content.claimed_sender_key, + session_key: content.session_key, + }) + } + #[cfg(feature = "experimental-algorithms")] + ForwardedRoomKeyContent::MegolmV2AesSha2(content) => Ok(Self { algorithm, room_id: content.room_id, session_id: content.session_id, - forwarding_curve25519_key_chain: content.forwarding_curve25519_key_chain, - sender_claimed_keys, + forwarding_curve25519_key_chain: Default::default(), + sender_claimed_keys: content.claimed_signing_keys, sender_key: content.claimed_sender_key, session_key: content.session_key, - }) - }; - - match forwarded_key { - ForwardedRoomKeyContent::MegolmV1AesSha2(content) => handle_key(content), - #[cfg(feature = "experimental-algorithms")] - ForwardedRoomKeyContent::MegolmV2AesSha2(content) => handle_key(content), + }), ForwardedRoomKeyContent::Unknown(c) => Err(SessionExportError::Algorithm(c.algorithm)), } } diff --git a/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs b/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs index c830b033c..ab15f9a59 100644 --- a/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs +++ b/crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; -use ruma::OwnedRoomId; +use ruma::{DeviceKeyAlgorithm, OwnedRoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKey}; @@ -24,7 +24,7 @@ use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKe use super::{EventType, ToDeviceEvent}; use crate::types::{ deserialize_curve_key, deserialize_curve_key_vec, deserialize_ed25519_key, serialize_curve_key, - serialize_curve_key_vec, serialize_ed25519_key, EventEncryptionAlgorithm, + serialize_curve_key_vec, serialize_ed25519_key, EventEncryptionAlgorithm, SigningKeys, }; /// The `m.forwarded_room_key` to-device event. @@ -53,7 +53,7 @@ pub enum ForwardedRoomKeyContent { /// The `m.megolm.v2.aes-sha2` variant of the `m.forwarded_room_key` /// content. #[cfg(feature = "experimental-algorithms")] - MegolmV2AesSha2(Box), + MegolmV2AesSha2(Box), /// An unknown and unsupported variant of the `m.forwarded_room_key` /// content. Unknown(UnknownRoomKeyContent), @@ -79,7 +79,7 @@ impl EventType for ForwardedRoomKeyContent { const EVENT_TYPE: &'static str = "m.forwarded_room_key"; } -/// The `m.megolm.v1.aes-sha2` variant of the `m.room_key` content. +/// The `m.megolm.v1.aes-sha2` variant of the `m.forwarded_room_key` content. #[derive(Deserialize, Serialize)] pub struct ForwardedMegolmV1AesSha2Content { /// The room where the key is used. @@ -131,7 +131,42 @@ pub struct ForwardedMegolmV1AesSha2Content { pub(crate) other: BTreeMap, } -/// An unknown and unsupported `m.room_key` algorithm. +/// The `m.megolm.v2.aes-sha2` variant of the `m.forwarded_room_key` content. +#[derive(Deserialize, Serialize)] +pub struct ForwardedMegolmV2AesSha2Content { + /// The room where the key is used. + pub room_id: OwnedRoomId, + + /// The ID of the session that the key is for. + pub session_id: String, + + /// The key to be exchanged. Can be used to create a [`InboundGroupSession`] + /// that can be used to decrypt room events. + /// + /// [`InboundGroupSession`]: vodozemac::megolm::InboundGroupSession + pub session_key: ExportedSessionKey, + + /// The Curve25519 key of the device which initiated the session originally. + /// + /// It is ‘claimed’ because the receiving device has no way to tell that + /// the original room_key actually came from a device which owns the private + /// part of this key. + #[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")] + pub claimed_sender_key: Curve25519PublicKey, + + /// The Ed25519 key of the device which initiated the session originally. + /// + /// It is ‘claimed’ because the receiving device has no way to tell that + /// the original room_key actually came from a device which owns the private + /// part of this key. + #[serde(default)] + pub claimed_signing_keys: SigningKeys, + + #[serde(flatten)] + pub(crate) other: BTreeMap, +} + +/// An unknown and unsupported `m.forwarded_room_key` algorithm. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UnknownRoomKeyContent { /// The algorithm of the unknown room key. @@ -153,6 +188,17 @@ impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content { } } +impl std::fmt::Debug for ForwardedMegolmV2AesSha2Content { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ForwardedMegolmV2AesSha2Content") + .field("room_id", &self.room_id) + .field("session_id", &self.session_id) + .field("claimed_sender_key", &self.claimed_sender_key) + .field("sender_claimed_keys", &self.claimed_signing_keys) + .finish_non_exhaustive() + } +} + #[derive(Deserialize, Serialize)] struct RoomKeyHelper { algorithm: EventEncryptionAlgorithm, @@ -171,7 +217,7 @@ impl TryFrom for ForwardedRoomKeyContent { } #[cfg(feature = "experimental-algorithms")] EventEncryptionAlgorithm::MegolmV2AesSha2 => { - let content: ForwardedMegolmV1AesSha2Content = serde_json::from_value(value.other)?; + let content: ForwardedMegolmV2AesSha2Content = serde_json::from_value(value.other)?; Self::MegolmV2AesSha2(content.into()) } _ => Self::Unknown(UnknownRoomKeyContent { From 7e1485723953a043e13a240874c30955ac7957e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 18 Aug 2022 17:17:16 +0200 Subject: [PATCH 08/66] test(crypto): Test that megolm v2 key forwards work --- .../src/gossiping/machine.rs | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/machine.rs b/crates/matrix-sdk-crypto/src/gossiping/machine.rs index fe7b67a48..2a1fc1ef7 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/machine.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/machine.rs @@ -1023,14 +1023,19 @@ mod tests { olm::{Account, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount}, session_manager::GroupSessionCache, store::{Changes, CryptoStore, MemoryStore, Store}, - types::events::{ - forwarded_room_key::ForwardedRoomKeyContent, - olm_v1::{AnyDecryptedOlmEvent, DecryptedOlmV1Event}, - room::encrypted::{EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent}, - EventType, ToDeviceEvent, + types::{ + events::{ + forwarded_room_key::ForwardedRoomKeyContent, + olm_v1::{AnyDecryptedOlmEvent, DecryptedOlmV1Event}, + room::encrypted::{ + EncryptedEvent, EncryptedToDeviceEvent, RoomEncryptedEventContent, + }, + EventType, ToDeviceEvent, + }, + EventEncryptionAlgorithm, }, verification::VerificationMachine, - OutgoingRequest, OutgoingRequests, + EncryptionSettings, OutgoingRequest, OutgoingRequests, }; fn alice_id() -> &'static UserId { @@ -1116,6 +1121,7 @@ mod tests { async fn machines_for_key_share( other_machine_owner: &UserId, create_sessions: bool, + algorithm: EventEncryptionAlgorithm, ) -> (GossipMachine, Account, OutboundGroupSession, GossipMachine) { let alice_machine = get_machine().await; let alice_account = Account { @@ -1145,8 +1151,13 @@ mod tests { bob_machine.store.save_sessions(&[bob_session]).await.unwrap(); } - let (group_session, inbound_group_session) = - bob_machine.store.account().create_group_session_pair_with_defaults(room_id()).await; + let settings = EncryptionSettings { algorithm, ..Default::default() }; + let (group_session, inbound_group_session) = bob_machine + .store + .account() + .create_group_session_pair(room_id(), settings) + .await + .unwrap(); let content = group_session.encrypt(json!({}), "m.dummy").await; let event = wrap_encrypted_content(bob_machine.user_id(), content); @@ -1497,10 +1508,9 @@ mod tests { assert_matches!(machine.should_share_key(&own_device, &other_inbound).await, Ok(None)); } - #[async_test] - async fn key_share_cycle() { + async fn key_share_cycle(algorithm: EventEncryptionAlgorithm) { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(alice_id(), true).await; + machines_for_key_share(alice_id(), true, algorithm).await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); @@ -1559,7 +1569,7 @@ mod tests { #[async_test] async fn reject_forward_from_another_user() { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(bob_id(), true).await; + machines_for_key_share(bob_id(), true, EventEncryptionAlgorithm::MegolmV1AesSha2).await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); @@ -1605,6 +1615,17 @@ mod tests { } } + #[async_test] + async fn key_share_cycle_megolm_v1() { + key_share_cycle(EventEncryptionAlgorithm::MegolmV1AesSha2).await; + } + + #[cfg(feature = "experimental-algorithms")] + #[async_test] + async fn key_share_cycle_megolm_v2() { + key_share_cycle(EventEncryptionAlgorithm::MegolmV2AesSha2).await; + } + #[async_test] async fn secret_share_cycle() { let alice_machine = get_machine().await; @@ -1678,7 +1699,8 @@ mod tests { #[async_test] async fn key_share_cycle_without_session() { let (alice_machine, alice_account, group_session, bob_machine) = - machines_for_key_share(alice_id(), false).await; + machines_for_key_share(alice_id(), false, EventEncryptionAlgorithm::MegolmV1AesSha2) + .await; // Get the request and convert it into a event. let requests = alice_machine.outgoing_to_device_requests().await.unwrap(); From 67d968d4fa1f169faa9070bcbf6bd38301746a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 19 Aug 2022 10:22:14 +0200 Subject: [PATCH 09/66] refactor(crypto): Remove the device ID from the megolm v2 m.room.encrypted content --- .../matrix-sdk-crypto-js/src/responses.rs | 2 +- .../matrix-sdk-crypto-nodejs/src/responses.rs | 2 +- .../src/deserialized_responses.rs | 2 +- crates/matrix-sdk-crypto/src/machine.rs | 25 ++++++++----------- .../src/olm/group_sessions/outbound.rs | 9 +++---- .../src/types/events/room/encrypted.rs | 18 +------------ 6 files changed, 18 insertions(+), 40 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/responses.rs b/bindings/matrix-sdk-crypto-js/src/responses.rs index 888847f48..24d51cd48 100644 --- a/bindings/matrix-sdk-crypto-js/src/responses.rs +++ b/bindings/matrix-sdk-crypto-js/src/responses.rs @@ -156,7 +156,7 @@ impl DecryptedRoomEvent { /// trusted. #[wasm_bindgen(getter, js_name = "senderDevice")] pub fn sender_device(&self) -> Option { - Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone())) + Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into()) } /// The Curve25519 key of the device that created the megolm diff --git a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs index 0efc96976..d6488f528 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/responses.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/responses.rs @@ -152,7 +152,7 @@ impl DecryptedRoomEvent { /// trusted. #[napi(getter)] pub fn sender_device(&self) -> Option { - Some(identifiers::DeviceId::from(self.encryption_info.as_ref()?.sender_device.clone())) + Some(self.encryption_info.as_ref()?.sender_device.as_ref()?.clone().into()) } /// The Curve25519 key of the device that created the megolm diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index a928d9132..ffe6e06d8 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -79,7 +79,7 @@ pub struct EncryptionInfo { pub sender: OwnedUserId, /// The device ID of the device that sent us the event, note this is /// untrusted data unless `verification_state` is as well trusted. - pub sender_device: OwnedDeviceId, + pub sender_device: Option, /// Information about the algorithm that was used to encrypt the event. pub algorithm_info: AlgorithmInfo, /// The verification state of the device that sent us the event, note this diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 8b43edf6a..6c6edad6f 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -38,8 +38,8 @@ use ruma::{ secret::request::SecretName, AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEventContent, }, serde::Raw, - DeviceId, DeviceKeyAlgorithm, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, RoomId, - TransactionId, UInt, UserId, + DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, + RoomId, TransactionId, UInt, UserId, }; use serde_json::{value::to_raw_value, Value}; use tracing::{debug, error, info, trace, warn}; @@ -1037,16 +1037,16 @@ impl OlmMachine { &self, session: &InboundGroupSession, sender: &UserId, - device_id: &DeviceId, - ) -> StoreResult { + ) -> StoreResult<(VerificationState, Option)> { Ok( // First find the device corresponding to the Curve25519 identity // key that sent us the session (recorded upon successful // decryption of the `m.room_key` to-device message). if let Some(device) = self - .get_device(sender, device_id, None) + .get_user_devices(sender, None) .await? - .filter(|d| d.curve25519_key().map(|k| k == session.sender_key()).unwrap_or(false)) + .devices() + .find(|d| d.curve25519_key() == Some(session.sender_key())) { // If the `Device` is confirmed to be the owner of the // `InboundGroupSession` we will consider the session (i.e. @@ -1058,14 +1058,14 @@ impl OlmMachine { if device.is_owner_of_session(session) && (device.is_our_own_device() || device.is_verified()) { - VerificationState::Trusted + (VerificationState::Trusted, Some(device.device_id().to_owned())) } else { - VerificationState::Untrusted + (VerificationState::Untrusted, Some(device.device_id().to_owned())) } } else { // We didn't find a device, no way to know if we should trust // the `InboundGroupSession` or not. - VerificationState::UnknownDevice + (VerificationState::UnknownDevice, None) }, ) } @@ -1079,12 +1079,10 @@ impl OlmMachine { &self, session: &InboundGroupSession, sender: &UserId, - device_id: &DeviceId, ) -> StoreResult { - let verification_state = self.get_verification_state(session, sender, device_id).await?; + let (verification_state, device_id) = self.get_verification_state(session, sender).await?; let sender = sender.to_owned(); - let device_id = device_id.to_owned(); Ok(EncryptionInfo { sender, @@ -1143,8 +1141,7 @@ impl OlmMachine { } } - let encryption_info = - self.get_encryption_info(&session, &event.sender, content.device_id()).await?; + let encryption_info = self.get_encryption_info(&session, &event.sender).await?; Ok(TimelineEvent { encryption_info: Some(encryption_info), event: decrypted_event }) } else { diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index eb072e313..753b84dc7 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -325,13 +325,10 @@ impl OutboundGroupSession { } .into(), #[cfg(feature = "experimental-algorithms")] - EventEncryptionAlgorithm::MegolmV2AesSha2 => MegolmV2AesSha2Content { - ciphertext, - session_id: self.session_id().to_owned(), - sender_key: self.account_identity_keys.curve25519, - device_id: (*self.device_id).to_owned(), + EventEncryptionAlgorithm::MegolmV2AesSha2 => { + MegolmV2AesSha2Content { ciphertext, session_id: self.session_id().to_owned() } + .into() } - .into(), _ => unreachable!( "An outbound group session is always using one of the supported algorithms" ), diff --git a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs index d8b9a2642..552d898ed 100644 --- a/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs +++ b/crates/matrix-sdk-crypto/src/types/events/room/encrypted.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; -use ruma::{DeviceId, OwnedDeviceId, RoomId}; +use ruma::{OwnedDeviceId, RoomId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use vodozemac::{megolm::MegolmMessage, olm::OlmMessage, Curve25519PublicKey}; @@ -250,15 +250,6 @@ impl SupportedEventEncryptionSchemes<'_> { } } - /// The ID of the sending device. - pub fn device_id(&self) -> &DeviceId { - match self { - SupportedEventEncryptionSchemes::MegolmV1AesSha2(c) => &c.device_id, - #[cfg(feature = "experimental-algorithms")] - SupportedEventEncryptionSchemes::MegolmV2AesSha2(c) => &c.device_id, - } - } - /// The algorithm that was used to encrypt the event content. pub fn algorithm(&self) -> EventEncryptionAlgorithm { match self { @@ -314,13 +305,6 @@ pub struct MegolmV2AesSha2Content { /// The ID of the session used to encrypt the message. pub session_id: String, - - /// The Curve25519 key of the sender. - #[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")] - pub sender_key: Curve25519PublicKey, - - /// The ID of the sending device. - pub device_id: OwnedDeviceId, } /// An unknown and unsupported `m.room.encrypted` event content. From 54fd40d8f59f87e5dfbceb0c4331afd31c3a3ed3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 9 Aug 2022 17:48:17 +0200 Subject: [PATCH 10/66] feat(sdk): Add new timeline API --- Cargo.lock | 25 +- bindings/matrix-sdk-ffi/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 6 +- crates/matrix-sdk/src/room/common.rs | 12 + crates/matrix-sdk/src/room/mod.rs | 2 + .../src/room/timeline/event_handler.rs | 486 ++++++++++++++++++ .../src/room/timeline/event_item.rs | 319 ++++++++++++ crates/matrix-sdk/src/room/timeline/mod.rs | 243 +++++++++ .../src/room/timeline/virtual_item.rs | 22 + crates/matrix-sdk/tests/integration/main.rs | 10 + .../matrix-sdk/tests/integration/room/mod.rs | 1 + .../tests/integration/room/timeline.rs | 427 +++++++++++++++ examples/timeline/Cargo.toml | 3 +- examples/timeline/src/main.rs | 28 +- 14 files changed, 1573 insertions(+), 13 deletions(-) create mode 100644 crates/matrix-sdk/src/room/timeline/event_handler.rs create mode 100644 crates/matrix-sdk/src/room/timeline/event_item.rs create mode 100644 crates/matrix-sdk/src/room/timeline/mod.rs create mode 100644 crates/matrix-sdk/src/room/timeline/virtual_item.rs create mode 100644 crates/matrix-sdk/tests/integration/room/timeline.rs diff --git a/Cargo.lock b/Cargo.lock index f4e01b012..f6089c197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "assign" version = "1.1.1" @@ -1364,6 +1370,7 @@ version = "0.1.0" dependencies = [ "anyhow", "futures", + "futures-signals", "matrix-sdk", "tokio", "tracing-subscriber", @@ -2302,6 +2309,7 @@ version = "0.6.0" dependencies = [ "anyhow", "anymap2", + "assert_matches", "async-once-cell", "async-stream", "async-trait", @@ -2320,6 +2328,7 @@ dependencies = [ "getrandom 0.2.7", "http", "image 0.24.3", + "indexmap", "matches", "matrix-sdk-base", "matrix-sdk-common", @@ -3660,9 +3669,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3daa593bddbe225bc78760329afaba54d0c653e015f18ce6405fa723ec0f34d5" +checksum = "8dc348e3a4a18abc4e97fffa5e2e623f6edd50ba3a1dd5f47eb249fea713b69f" dependencies = [ "assign", "js_int", @@ -3686,9 +3695,9 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2709c891d277ef94d56657c3ec92ed464779dbfff055e518425eedf11d9ecb7" +checksum = "5bcfd3a3853ffdd151fc228441dd9c9e3d835ac85560dface7abda50b3888791" dependencies = [ "assign", "bytes", @@ -3703,9 +3712,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dab5e934f2e280875cf3a863c14d876265bda169e4fd18334058e7307142d6" +checksum = "a1e629a01f359234798531a99ba83997abd4c15a65b5bcb8354c4171b59c25be" dependencies = [ "base64", "bytes", @@ -3756,9 +3765,9 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3e5a61180840ebfdeb4bcc4dc4a0d0c21aa22f587360b16b785c79058d99f3" +checksum = "9f7cd8cf8771aaca36042fb7659f4647b05e74a2058d843474dde5e51a56cd85" dependencies = [ "once_cell", "proc-macro-crate", diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 846c98800..23d029860 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -23,7 +23,7 @@ futures-signals = { version = "0.3.28" } 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", "markdown", "sliding-sync", "socks"], version = "0.6.0" } +matrix-sdk = { path = "../../crates/matrix-sdk", features = ["anyhow", "experimental-timeline", "markdown", "sliding-sync", "socks"], version = "0.6.0" } once_cell = "1.10.0" sanitize-filename-reader-friendly = "2.2.1" serde = { version = "1", features = ["derive"] } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 5ee456e48..d26985028 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -42,6 +42,8 @@ appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] +experimental-timeline = [] + sliding-sync = [ "matrix-sdk-base/sliding-sync", "anyhow", @@ -70,6 +72,7 @@ futures-core = "0.3.21" futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.21", default-features = false } http = "0.2.6" +indexmap = "1.9.1" matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", default-features = false, optional = true } @@ -107,7 +110,7 @@ features = [ optional = true [dependencies.ruma] -version = "0.7.0" +version = "0.7.4" features = ["client-api-c", "compat", "rand", "unstable-msc2448", "unstable-msc2965"] [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -120,6 +123,7 @@ tokio = { version = "1.17.0", default-features = false, features = ["fs", "rt"] [dev-dependencies] anyhow = "1.0.57" +assert_matches = "1.5.0" dirs = "4.0.0" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matches = "0.1.9" diff --git a/crates/matrix-sdk/src/room/common.rs b/crates/matrix-sdk/src/room/common.rs index 5cc0aa6fc..d9aeead01 100644 --- a/crates/matrix-sdk/src/room/common.rs +++ b/crates/matrix-sdk/src/room/common.rs @@ -37,6 +37,8 @@ use ruma::{ }; use serde::de::DeserializeOwned; +#[cfg(feature = "experimental-timeline")] +use super::timeline::Timeline; use crate::{ event_handler::{EventHandler, EventHandlerHandle, EventHandlerResult, SyncEvent}, media::{MediaFormat, MediaRequest}, @@ -251,6 +253,16 @@ impl Common { self.client.add_room_event_handler(self.room_id(), handler) } + /// Get a [`Timeline`] for this room. + /// + /// This offers a higher-level API than event handlers, in treating things + /// like edits and reactions as updates of existing items rather than new + /// independent events. + #[cfg(feature = "experimental-timeline")] + pub fn timeline(&self) -> Timeline { + Timeline::new(self) + } + /// Fetch the event with the given `EventId` in this room. pub async fn event(&self, event_id: &EventId) -> Result { let request = get_room_event::v3::Request::new(self.room_id(), event_id); diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 13905c733..633213bec 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -9,6 +9,8 @@ mod invited; mod joined; mod left; mod member; +#[cfg(feature = "experimental-timeline")] +pub mod timeline; pub use self::{ common::{Common, Messages, MessagesOptions}, diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs new file mode 100644 index 000000000..bc228d2f5 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -0,0 +1,486 @@ +// 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. + +use std::sync::Arc; + +use indexmap::map::Entry; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + events::{ + reaction::ReactionEventContent, + room::{ + message::{Relation, Replacement, RoomMessageEventContent}, + redaction::{ + OriginalSyncRoomRedactionEvent, RoomRedactionEventContent, SyncRoomRedactionEvent, + }, + }, + AnyMessageLikeEventContent, AnyStateEventContent, AnySyncMessageLikeEvent, + AnySyncTimelineEvent, Relations, + }, + serde::Raw, + uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + UserId, +}; +use tracing::{debug, error, info, warn}; + +use super::{ + event_item::{BundledReactions, TimelineDetails}, + find_event, EventTimelineItem, Message, TimelineInner, TimelineItem, TimelineItemContent, + TimelineKey, +}; + +pub(super) fn handle_live_event( + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + timeline: &TimelineInner, +) { + handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::End, timeline) +} + +pub(super) fn handle_local_event( + txn_id: OwnedTransactionId, + content: AnyMessageLikeEventContent, + own_user_id: &UserId, + timeline: &TimelineInner, +) { + let meta = TimelineEventMetadata { + sender: own_user_id.to_owned(), + origin_server_ts: None, + raw_event: None, + is_own_event: true, + relations: None, + // FIXME: Should we supply something here for encrypted rooms? + encryption_info: None, + }; + + let flow = Flow::Local { txn_id }; + let kind = TimelineEventKind::Message { content }; + + TimelineEventHandler::new(meta, flow, timeline).handle_event(kind) +} + +pub(super) fn handle_back_paginated_event( + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + timeline: &TimelineInner, +) { + handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::Start, timeline) +} + +fn handle_remote_event( + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + position: TimelineItemPosition, + timeline: &TimelineInner, +) { + 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 { + raw_event: Some(raw), + sender, + origin_server_ts: Some(event.origin_server_ts()), + is_own_event, + relations: event.relations().cloned(), + encryption_info, + }; + let flow = Flow::Remote { + event_id: event.event_id().to_owned(), + txn_id: event.transaction_id().map(ToOwned::to_owned), + position, + }; + + TimelineEventHandler::new(meta, flow, timeline).handle_event(event.into()) +} + +enum Flow { + Local { + txn_id: OwnedTransactionId, + }, + Remote { + event_id: OwnedEventId, + txn_id: Option, + position: TimelineItemPosition, + }, +} + +impl Flow { + fn to_key(&self) -> TimelineKey { + match self { + Self::Remote { event_id, .. } => TimelineKey::EventId(event_id.to_owned()), + Self::Local { txn_id } => TimelineKey::TransactionId(txn_id.to_owned()), + } + } +} + +struct TimelineEventMetadata { + raw_event: Option>, + sender: OwnedUserId, + origin_server_ts: Option, + is_own_event: bool, + relations: Option, + encryption_info: Option, +} + +impl From for TimelineEventKind { + fn from(event: AnySyncTimelineEvent) -> Self { + match event { + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction( + SyncRoomRedactionEvent::Original(OriginalSyncRoomRedactionEvent { + redacts, + content, + .. + }), + )) => Self::Redaction { redacts, content }, + AnySyncTimelineEvent::MessageLike(ev) => match ev.original_content() { + Some(content) => Self::Message { content }, + None => Self::RedactedMessage, + }, + AnySyncTimelineEvent::State(ev) => match ev.original_content() { + Some(_content) => Self::State { _content }, + None => Self::RedactedState, + }, + } + } +} + +#[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, +} + +// 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> { + meta: TimelineEventMetadata, + flow: Flow, + timeline: &'a TimelineInner, + event_added: bool, +} + +impl<'a> TimelineEventHandler<'a> { + fn new(meta: TimelineEventMetadata, flow: Flow, timeline: &'a TimelineInner) -> Self { + Self { meta, flow, timeline, event_added: false } + } + + fn handle_event(mut self, event_kind: TimelineEventKind) { + match event_kind { + TimelineEventKind::Message { content } => match content { + AnyMessageLikeEventContent::Reaction(c) => self.handle_reaction(c), + AnyMessageLikeEventContent::RoomMessage(c) => self.handle_room_message(c), + // TODO + _ => {} + }, + TimelineEventKind::RedactedMessage => { + self.add(NewEventTimelineItem::redacted_message()); + } + TimelineEventKind::Redaction { redacts, content } => { + self.handle_redaction(redacts, content) + } + // TODO: State events + _ => {} + } + + if !self.event_added { + // TODO: Add event as raw + } + } + + fn handle_room_message(&mut self, content: RoomMessageEventContent) { + match content.relates_to { + Some(Relation::Replacement(re)) => { + self.handle_room_message_edit(re); + } + _ => { + self.add(NewEventTimelineItem::message(content, self.meta.relations.clone())); + } + } + } + + fn handle_room_message_edit(&mut self, replacement: Replacement) { + let event_id = &replacement.event_id; + + self.maybe_update_timeline_item(event_id, "edit", |item| { + if self.meta.sender != item.sender() { + info!( + %event_id, original_sender = %item.sender(), edit_sender = %self.meta.sender, + "Event tries to edit another user's timeline item, discarding" + ); + return None; + } + + let msg = match &item.content { + TimelineItemContent::Message(msg) => msg, + TimelineItemContent::RedactedMessage => { + info!( + %event_id, + "Event tries to edit a non-editable timeline item, discarding" + ); + return None; + } + }; + + let content = TimelineItemContent::Message(Message { + msgtype: replacement.new_content.msgtype, + in_reply_to: msg.in_reply_to.clone(), + edited: true, + }); + + Some(item.with_content(content)) + }); + } + + // Redacted reaction events are no-ops so don't need to be handled + 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| { + // 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 { + debug!(%event_id, "Ignoring reaction on redacted event"); + None + } else { + let mut reactions = item.reactions.clone(); + let reaction_details = + reactions.bundled.entry(c.relates_to.key.clone()).or_default(); + + reaction_details.count += uint!(1); + if let TimelineDetails::Ready(senders) = &mut reaction_details.senders { + senders.push(self.meta.sender.clone()); + } + + Some(item.with_reactions(reactions)) + } + }); + + if did_update { + lock.insert(self.flow.to_key(), (self.meta.sender.clone(), c.relates_to)); + } + } + + // Redacted redactions are no-ops (unfortunately) + 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| { + 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 details = details_entry.get_mut(); + details.count -= uint!(1); + + if details.count == uint!(0) { + details_entry.remove(); + 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; + } + }; + + if let Some(idx) = senders.iter().position(|s| *s == sender) { + senders.remove(idx); + } else { + error!( + "inconsistent state: sender from reaction_map not in reaction sender list \ + of timeline item" + ); + return None; + } + + if u64::from(details.count) != senders.len() as u64 { + error!("inconsistent state: reaction count differs from number of senders"); + // Can't make things worse by updating the item, so no early + // return here. + } + + Some(item.with_reactions(reactions)) + }); + + if !did_update { + warn!("reaction_map out of sync with timeline items"); + } + } + + // 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()); + + if !did_update { + // We will want to know this when debugging redaction issues. + debug!(redaction_key = ?self.flow.to_key(), %redacts, "redaction affected no event"); + } + } + + fn add(&mut self, item: NewEventTimelineItem) { + self.event_added = true; + + let NewEventTimelineItem { content, reactions } = item; + let item = EventTimelineItem { + key: self.flow.to_key(), + event_id: None, + sender: self.meta.sender.to_owned(), + content, + reactions, + origin_server_ts: self.meta.origin_server_ts, + is_own: self.meta.is_own_event, + encryption_info: self.meta.encryption_info.clone(), + raw: self.meta.raw_event.clone(), + }; + + let item = Arc::new(TimelineItem::Event(item)); + let mut lock = self.timeline.items.lock_mut(); + match &self.flow { + Flow::Local { .. } + | Flow::Remote { position: TimelineItemPosition::End, txn_id: None, .. } => { + lock.push_cloned(item); + } + Flow::Remote { position: TimelineItemPosition::Start, txn_id: None, .. } => { + lock.insert_cloned(0, item); + } + Flow::Remote { txn_id: Some(txn_id), event_id, position } => { + if let Some((idx, _old_item)) = find_event(&lock, txn_id) { + // TODO: Check whether anything is different about the old and new item? + lock.set_cloned(idx, item); + } else { + debug!( + %txn_id, %event_id, + "Received event with transaction ID, but didn't find matching timeline item" + ); + + match position { + TimelineItemPosition::Start => lock.insert_cloned(0, item), + TimelineItemPosition::End => lock.push_cloned(item), + } + } + } + } + } + + /// Returns whether an update happened + fn maybe_update_timeline_item( + &self, + event_id: &EventId, + action: &str, + update: impl FnOnce(&EventTimelineItem) -> Option, + ) -> 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; + } + + 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 + } + + /// 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))) + } +} + +struct NewEventTimelineItem { + content: TimelineItemContent, + reactions: BundledReactions, +} + +impl NewEventTimelineItem { + // These constructors could also be `From` implementations, but that would + // allow users to call them directly, which should not be supported + pub(crate) fn message(c: RoomMessageEventContent, relations: Option) -> Self { + let edited = relations.as_ref().map_or(false, |r| r.replace.is_some()); + let content = TimelineItemContent::Message(Message { + msgtype: c.msgtype, + in_reply_to: c.relates_to.and_then(|rel| match rel { + Relation::Reply { in_reply_to } => Some(in_reply_to.event_id), + _ => None, + }), + edited, + }); + + let reactions = + relations.and_then(|r| r.annotation).map(BundledReactions::from).unwrap_or_default(); + + Self { content, reactions } + } + + pub(crate) fn redacted_message() -> Self { + Self { + content: TimelineItemContent::RedactedMessage, + reactions: BundledReactions::default(), + } + } +} diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs new file mode 100644 index 000000000..9300195f3 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -0,0 +1,319 @@ +// 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. + +use indexmap::IndexMap; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + events::{ + relation::{AnnotationChunk, AnnotationType}, + room::message::MessageType, + AnySyncTimelineEvent, + }, + serde::Raw, + uint, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, + TransactionId, UInt, UserId, +}; + +/// An item in the timeline that represents at least one event. +/// +/// There is always one main event that gives the `EventTimelineItem` its +/// identity (see [key](Self::key)) but in many cases, additional events like +/// reactions and edits are also part of the item. +#[derive(Clone, Debug)] +pub struct EventTimelineItem { + pub(super) key: TimelineKey, + // If this item is a local echo that has been acknowledged by the server + // but not remote-echoed yet, this field holds the event ID from the send + // response. + pub(super) event_id: Option, + pub(super) sender: OwnedUserId, + pub(super) content: TimelineItemContent, + pub(super) reactions: BundledReactions, + pub(super) origin_server_ts: Option, + pub(super) is_own: bool, + pub(super) encryption_info: Option, + // FIXME: Expose the raw JSON of aggregated events somehow + pub(super) raw: Option>, +} + +macro_rules! build { + ( + $ty:ident { + $( $field:ident $(: $value:expr)?, )* + ..$this:ident( $($this_field:ident),* $(,)? ) + } + ) => { + $ty { + $( $field $(: $value)?, )* + $( $this_field: $this.$this_field.clone() ),* + } + } +} + +impl EventTimelineItem { + /// Get the [`TimelineKey`] of this item. + pub fn key(&self) -> &TimelineKey { + &self.key + } + + /// Get the event ID of this item. + /// + /// If this returns `Some(_)`, the event was successfully created by the + /// server. + /// + /// Even if the [`key()`](Self::key) of this timeline item holds a + /// transaction ID, this can be `Some(_)` as the event ID can be known not + /// just from the remote echo via `sync_events`, but also from the response + /// of the send request that created the event. + pub fn event_id(&self) -> Option<&EventId> { + match &self.key { + TimelineKey::TransactionId(_) => self.event_id.as_deref(), + TimelineKey::EventId(id) => Some(id), + } + } + + /// Get the sender of this item. + pub fn sender(&self) -> &UserId { + &self.sender + } + + /// Get the content of this item. + pub fn content(&self) -> &TimelineItemContent { + &self.content + } + + /// Get the reactions of this item. + pub fn reactions(&self) -> &IndexMap { + // FIXME: Find out the state of incomplete bundled reactions, adjust + // Ruma if necessary, return the whole BundledReactions field + &self.reactions.bundled + } + + /// Get the origin server timestamp of this item. + /// + /// Returns `None` if this event hasn't been echoed back by the server yet. + pub fn origin_server_ts(&self) -> Option { + self.origin_server_ts + } + + /// Whether this timeline item was sent by the logged-in user themselves. + pub fn is_own(&self) -> bool { + self.is_own + } + + /// Get the raw JSON representation of the initial event (the one that + /// caused this timeline item to be created). + /// + /// Returns `None` if this event hasn't been echoed back by the server yet. + pub fn raw(&self) -> Option<&Raw> { + self.raw.as_ref() + } + + pub(super) fn to_redacted(&self) -> Self { + build!(Self { + // FIXME: Change when we support state events + content: TimelineItemContent::RedactedMessage, + reactions: BundledReactions::default(), + ..self(key, event_id, sender, origin_server_ts, is_own, encryption_info, raw) + }) + } + + pub(super) fn with_event_id(&self, event_id: Option) -> Self { + build!(Self { + event_id, + ..self(key, sender, content, reactions, origin_server_ts, is_own, encryption_info, raw,) + }) + } + + #[rustfmt::skip] + pub(super) fn with_content(&self, content: TimelineItemContent) -> Self { + build!(Self { + content, + ..self( + key, event_id, sender, reactions, origin_server_ts, is_own, encryption_info, raw, + ) + }) + } + + #[rustfmt::skip] + pub(super) fn with_reactions(&self, reactions: BundledReactions) -> Self { + build!(Self { + reactions, + ..self( + key, event_id, sender, content, origin_server_ts, is_own, encryption_info, raw, + ) + }) + } +} + +/// A unique identifier for a timeline item. +/// +/// This identifier is used to find the item in the timeline in order to update +/// its state. +/// +/// When an event is created locally, the timeline reflects this with an item +/// that has a [`TransactionId`](Self::TransactionId) key. Once the server has +/// acknowledged the event and given it an ID, that item's key is replaced by +/// [`EventId`](Self::EventId) containing the new ID. +/// +/// When an event related to the original event whose ID is stored in a +/// [`TimelineKey`] is received, the key is left untouched, but other parts of +/// the timeline item may be updated. Thus, the current data model is only able +/// to handle relations that reference the initial event that resulted in a +/// timeline item being created, not other related events. At the time of +/// writing, there is no relation that is meant to refer to other events that +/// only exist for their relation (e.g. edits, replies). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TimelineKey { + /// Transaction ID, for an event that was created locally and hasn't been + /// acknowledged by the server yet. + TransactionId(OwnedTransactionId), + /// Event ID, for an event that is synced with the server. + EventId(OwnedEventId), +} + +impl PartialEq for &TransactionId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::TransactionId(txn_id) if txn_id == self) + } +} + +impl PartialEq for &OwnedTransactionId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::TransactionId(txn_id) if txn_id == *self) + } +} + +impl PartialEq for &EventId { + fn eq(&self, key: &TimelineKey) -> bool { + matches!(key, TimelineKey::EventId(event_id) if event_id == self) + } +} + +/// Some details of an [`EventTimelineItem`] that may require server requests +/// other than just the regular +/// [`sync_events`][ruma::api::client::sync::sync_events]. +#[derive(Clone, Debug)] +pub enum TimelineDetails { + /// The details are not available yet, and have not been request from the + /// server. + Unavailable, + + /// The details are not available yet, but have been requested. + Pending, + + /// The details are available. + Ready(T), +} + +/// The content of an [`EventTimelineItem`]. +#[derive(Clone, Debug)] +pub enum TimelineItemContent { + /// An `m.room.message` event or extensible event, including edits. + Message(Message), + + /// A redacted message. + RedactedMessage, +} + +/// An `m.room.message` event or extensible event, including edits. +#[derive(Clone, Debug)] +pub struct Message { + pub(super) msgtype: MessageType, + // TODO: Add everything required to display the replied-to event, plus a + // 'loading' state that is entered at first, until the user requests the + // reply to be loaded. + pub(super) in_reply_to: Option, + pub(super) edited: bool, +} + +impl Message { + /// Get the `msgtype`-specific data of this message. + pub fn msgtype(&self) -> &MessageType { + &self.msgtype + } + + /// Get the event ID of the event this message is replying to, if any. + pub fn in_reply_to(&self) -> Option<&EventId> { + self.in_reply_to.as_deref() + } + + /// Get the edit state of this message (has been edited: `true` / `false`). + pub fn is_edited(&self) -> bool { + self.edited + } +} + +#[derive(Clone, Debug)] +pub struct BundledReactions { + /// Whether all reactions are known, or some may be missing. + /// + /// If this is `false`, the remaining reactions can be fetched via **TODO**. + pub complete: bool, // FIXME: Unclear whether this is needed + + /// The reactions. + /// + /// Key: The reaction, usually an emoji.\ + /// Value: The count. + pub bundled: IndexMap, +} + +impl From for BundledReactions { + fn from(ann: AnnotationChunk) -> Self { + let bundled = ann + .chunk + .into_iter() + .filter_map(|a| { + (a.annotation_type == AnnotationType::Reaction).then(|| { + let details = + ReactionDetails { count: a.count, senders: TimelineDetails::Unavailable }; + (a.key, details) + }) + }) + .collect(); + + BundledReactions { bundled, complete: ann.next_batch.is_none() } + } +} + +impl Default for BundledReactions { + fn default() -> Self { + Self { complete: true, bundled: IndexMap::new() } + } +} + +/// The details of a group of reaction events on the same event with the same +/// key. +#[derive(Clone, Debug)] +pub struct ReactionDetails { + /// The amount of reactions with this key. + pub count: UInt, + + /// The senders of the reactions. + pub senders: TimelineDetails>, +} + +impl Default for ReactionDetails { + fn default() -> Self { + Self { count: uint!(0), senders: TimelineDetails::Ready(Vec::new()) } + } +} + +/// The result of a successful pagination request. +#[derive(Debug)] +#[non_exhaustive] +pub struct PaginationOutcome { + /// Whether there's more messages to be paginated. + pub more_messages: bool, +} diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs new file mode 100644 index 000000000..73ed300d4 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -0,0 +1,243 @@ +// 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. + +//! A high-level view into a room's contents. +//! +//! See [`Timeline`] for details. + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use futures_core::Stream; +use futures_signals::signal_vec::{MutableVec, SignalVec, SignalVecExt, VecDiff}; +use matrix_sdk_base::deserialized_responses::EncryptionInfo; +use ruma::{ + assign, + events::{reaction::Relation as AnnotationRelation, AnyMessageLikeEventContent}, + OwnedEventId, OwnedUserId, TransactionId, UInt, +}; +use tracing::{debug, error, instrument}; + +use super::{Joined, Room}; +use crate::{ + event_handler::EventHandlerDropGuard, + room::{self, MessagesOptions}, + Result, +}; + +mod event_handler; +mod event_item; +mod virtual_item; + +use self::event_handler::{handle_back_paginated_event, handle_live_event, handle_local_event}; +pub use self::{ + event_item::{ + EventTimelineItem, Message, PaginationOutcome, ReactionDetails, TimelineDetails, + TimelineItemContent, TimelineKey, + }, + virtual_item::VirtualTimelineItem, +}; + +/// A high-level view into a regular¹ room's contents. +/// +/// ¹ This type is meant to be used in the context of rooms without a +/// `room_type`, that is rooms that are primarily used to exchange text +/// messages. +#[derive(Debug)] +pub struct Timeline { + inner: TimelineInner, + room: room::Common, + start_token: Mutex>, + _end_token: Mutex>, + _event_handler_guard: EventHandlerDropGuard, +} + +#[derive(Clone, Debug, Default)] +struct TimelineInner { + items: MutableVec>, + // Reaction event / txn ID => sender and reaction data + reaction_map: Arc>>, +} + +impl Timeline { + pub(super) fn new(room: &room::Common) -> Self { + let inner = TimelineInner::default(); + + let handle = room.add_event_handler({ + let inner = inner.clone(); + move |event, encryption_info: Option, room: Room| { + let inner = inner.clone(); + async move { + handle_live_event(event, encryption_info, room.own_user_id(), &inner); + } + } + }); + let _event_handler_guard = room.client.event_handler_drop_guard(handle); + + Timeline { + inner, + room: room.clone(), + start_token: Mutex::new(None), + _end_token: Mutex::new(None), + _event_handler_guard, + } + } + + /// Add more events to the start of the timeline. + #[instrument(skip(self), fields(room_id = %self.room.room_id()))] + pub async fn paginate_backwards(&self, limit: UInt) -> Result { + let start = self.start_token.lock().unwrap().clone(); + let messages = self + .room + .messages(assign!(MessagesOptions::backward(), { + from: start.as_deref(), + limit, + })) + .await?; + + let outcome = PaginationOutcome { more_messages: messages.end.is_some() }; + *self.start_token.lock().unwrap() = messages.end; + + let own_user_id = self.room.own_user_id(); + for room_ev in messages.chunk { + handle_back_paginated_event( + room_ev.event.cast(), + room_ev.encryption_info, + own_user_id, + &self.inner, + ); + } + + Ok(outcome) + } + + /// Get a signal of the timeline's items. + /// + /// You can poll this signal to receive updates, the first of which will + /// be the full list of items currently available. + /// + /// See [`SignalVecExt`](futures_signals::signal_vec::SignalVecExt) for a + /// high-level API on top of [`SignalVec`]. + pub fn signal(&self) -> impl SignalVec> { + self.inner.items.signal_vec_cloned() + } + + /// Get a stream of timeline changes. + /// + /// This is a convenience shorthand for `timeline.signal().to_stream()`. + pub fn stream(&self) -> impl Stream>> { + self.signal().to_stream() + } + + /// Send a message to the room, and add it to the timeline as a local echo. + /// + /// For simplicity, this method doesn't currently allow custom message + /// types. + /// + /// If the encryption feature is enabled, this method will transparently + /// encrypt the room message if the room is encrypted. + /// + /// # Arguments + /// + /// * `content` - The content of the message event. + /// + /// * `txn_id` - A locally-unique ID describing a message transaction with + /// the homeserver. Unless you're doing something special, you can pass in + /// `None` which will create a suitable one for you automatically. + /// * On the sending side, this field is used for re-trying earlier + /// failed transactions. Subsequent messages *must never* re-use an + /// earlier transaction ID. + /// * On the receiving side, the field is used for recognizing our own + /// messages when they arrive down the sync: the server includes the + /// ID in the [`MessageLikeUnsigned`] field `transaction_id` of the + /// corresponding [`SyncMessageLikeEvent`], but only for the *sending* + /// device. Other devices will not see it. + /// + /// [`MessageLikeUnsigned`]: ruma::events::MessageLikeUnsigned + /// [`SyncMessageLikeEvent`]: ruma::events::SyncMessageLikeEvent + #[instrument(skip(self, content), fields(room_id = %self.room.room_id()))] + pub async fn send( + &self, + content: AnyMessageLikeEventContent, + txn_id: Option<&TransactionId>, + ) -> Result<()> { + let txn_id = txn_id.map_or_else(TransactionId::new, ToOwned::to_owned); + handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id(), &self.inner); + + // If this room isn't actually in joined state, we'll get a server error. + // Not ideal, but works for now. + let room = Joined { inner: self.room.clone() }; + + let response = room.send(content, Some(&txn_id)).await?; + add_event_id(&self.inner, &txn_id, response.event_id); + + Ok(()) + } +} + +/// A single entry in timeline. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum TimelineItem { + /// An event or aggregation of multiple events. + Event(EventTimelineItem), + /// An item that doesn't correspond to an event, for example the user's own + /// read marker. + Virtual(VirtualTimelineItem), +} + +impl TimelineItem { + /// Get the inner `EventTimelineItem`, if this is a `TimelineItem::Event`. + pub fn as_event(&self) -> Option<&EventTimelineItem> { + match self { + Self::Event(v) => Some(v), + _ => None, + } + } +} + +// FIXME: Put an upper bound on timeline size or add a separate map to look up +// the index of a timeline item by its key, to avoid large linear scans. +fn find_event( + lock: &[Arc], + key: impl PartialEq, +) -> Option<(usize, &EventTimelineItem)> { + lock.iter() + .enumerate() + .filter_map(|(idx, item)| Some((idx, item.as_event()?))) + .rfind(|(_, it)| key == it.key) +} + +fn add_event_id(items: &TimelineInner, txn_id: &TransactionId, event_id: OwnedEventId) { + let mut lock = items.items.lock_mut(); + if let Some((idx, item)) = find_event(&lock, txn_id) { + match &item.key { + TimelineKey::TransactionId(_) => { + lock.set_cloned( + idx, + Arc::new(TimelineItem::Event(item.with_event_id(Some(event_id)))), + ); + } + TimelineKey::EventId(ev_id) => { + if *ev_id != event_id { + error!("remote echo and send-event response disagree on the event ID"); + } + } + } + } else { + debug!(%txn_id, "Timeline item not found, can't mark as sent"); + } +} diff --git a/crates/matrix-sdk/src/room/timeline/virtual_item.rs b/crates/matrix-sdk/src/room/timeline/virtual_item.rs new file mode 100644 index 000000000..dedc18bf3 --- /dev/null +++ b/crates/matrix-sdk/src/room/timeline/virtual_item.rs @@ -0,0 +1,22 @@ +// 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. + +/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event. +#[derive(Clone, Debug)] +pub enum VirtualTimelineItem { + /// A divider between messages of two days. + DayDivider, + /// The user's own read marker. + ReadMarker, +} diff --git a/crates/matrix-sdk/tests/integration/main.rs b/crates/matrix-sdk/tests/integration/main.rs index 709342fa9..e33adebbf 100644 --- a/crates/matrix-sdk/tests/integration/main.rs +++ b/crates/matrix-sdk/tests/integration/main.rs @@ -13,6 +13,16 @@ mod client; mod refresh_token; mod room; +#[cfg(all(test, not(target_arch = "wasm32")))] +#[ctor::ctor] +fn init_logging() { + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .init(); +} + async fn test_client_builder() -> (ClientBuilder, MockServer) { let server = MockServer::start().await; let builder = diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs index b9e0e2c78..3325d1320 100644 --- a/crates/matrix-sdk/tests/integration/room/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -1,3 +1,4 @@ mod common; mod joined; mod left; +mod timeline; diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs new file mode 100644 index 000000000..16844c4fa --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -0,0 +1,427 @@ +#![cfg(feature = "experimental-timeline")] + +use std::{sync::Arc, time::Duration}; + +use assert_matches::assert_matches; +use futures_signals::signal_vec::{SignalVecExt, VecDiff}; +use futures_util::StreamExt; +use matrix_sdk::{ + config::SyncSettings, + room::timeline::{TimelineDetails, TimelineItemContent, TimelineKey}, + ruma::MilliSecondsSinceUnixEpoch, +}; +use matrix_sdk_common::executor::spawn; +use matrix_sdk_test::{async_test, test_json, EventBuilder, JoinedRoomBuilder, TimelineTestEvent}; +use ruma::{ + event_id, + events::room::message::{MessageType, RoomMessageEventContent}, + room_id, uint, user_id, TransactionId, +}; +use serde_json::json; +use wiremock::{ + matchers::{header, method, path_regex}, + Mock, ResponseTemplate, +}; + +use crate::{logged_in_client, mock_sync}; + +#[async_test] +async fn edit() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "body": "hello", + "msgtype": "m.text", + }, + "event_id": "$msda7m:localhost", + "origin_server_ts": 152037280, + "sender": "@alice:example.org", + "type": "m.room.message", + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let first = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let msg = assert_matches!( + first.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + assert_matches!(msg.msgtype(), MessageType::Text(_)); + assert_matches!(msg.in_reply_to(), None); + assert!(!msg.is_edited()); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "Test", + "formatted_body": "Test", + "msgtype": "m.text", + "format": "org.matrix.custom.html", + }, + "event_id": "$7at8sd:localhost", + "origin_server_ts": 152038280, + "sender": "@bob:example.org", + "type": "m.room.message", + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": " * hi", + "m.new_content": { + "body": "hi", + "msgtype": "m.text", + }, + "m.relates_to": { + "event_id": "$msda7m:localhost", + "rel_type": "m.replace", + }, + "msgtype": "m.text", + }, + "event_id": "$msda7m2:localhost", + "origin_server_ts": 159056300, + "sender": "@alice:example.org", + "type": "m.room.message", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let second = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let item = second.as_event().unwrap(); + assert_eq!(item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(152038280)))); + assert!(item.event_id().is_some()); + assert!(!item.is_own()); + assert!(item.raw().is_some()); + + let msg = assert_matches!(item.content(), TimelineItemContent::Message(msg) => msg); + assert_matches!(msg.msgtype(), MessageType::Text(_)); + assert_matches!(msg.in_reply_to(), None); + assert!(!msg.is_edited()); + + let edit = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let edited = assert_matches!( + edit.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(edited.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "hi"); + assert_matches!(edited.in_reply_to(), None); + assert!(edited.is_edited()); +} + +#[async_test] +async fn echo() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = Arc::new(room.timeline()); + let mut timeline_stream = timeline.signal().to_stream(); + + let event_id = event_id!("$wWgymRfo7ri1uQx0NXO40vLJ"); + let txn_id: &TransactionId = "my-txn-id".into(); + + Mock::given(method("PUT")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&json!({ "event_id": event_id }))) + .mount(&server) + .await; + + // Don't move the original timeline, it must live until the end of the test + let timeline = timeline.clone(); + let send_hdl = spawn(async move { + timeline + .send(RoomMessageEventContent::text_plain("Hello, World!").into(), Some(txn_id)) + .await + }); + + let local_echo = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + let item = local_echo.as_event().unwrap(); + assert!(item.event_id().is_none()); + assert!(item.is_own()); + assert_matches!(item.key(), TimelineKey::TransactionId(_)); + assert_eq!(item.origin_server_ts(), None); + assert_matches!(item.raw(), None); + + let msg = assert_matches!(item.content(), TimelineItemContent::Message(msg) => msg); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "Hello, World!"); + + // Wait for the sending to finish and assert everything was successful + send_hdl.await.unwrap().unwrap(); + + let sent_confirmation = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let item = sent_confirmation.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(item.is_own()); + assert_matches!(item.key(), TimelineKey::TransactionId(_)); + assert_eq!(item.origin_server_ts(), None); + assert_matches!(item.raw(), None); + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "body": "Hello, World!", + "msgtype": "m.text", + }, + "event_id": "$7at8sd:localhost", + "origin_server_ts": 152038280, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { "transaction_id": txn_id, }, + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let remote_echo = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let item = remote_echo.as_event().unwrap(); + assert!(item.event_id().is_some()); + assert!(item.is_own()); + assert_eq!(item.origin_server_ts(), Some(MilliSecondsSinceUnixEpoch(uint!(152038280)))); + assert_matches!(item.key(), TimelineKey::EventId(_)); + assert_matches!(item.raw(), Some(_)); +} + +#[async_test] +async fn back_pagination() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = Arc::new(room.timeline()); + let mut timeline_stream = timeline.signal().to_stream(); + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::ROOM_MESSAGES_BATCH_1)) + .expect(1) + .named("messages_batch_1") + .mount(&server) + .await; + + timeline.paginate_backwards(uint!(10)).await.unwrap(); + + let message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::Push { value }) => value + ); + let msg = assert_matches!( + message.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "hello world"); + + let message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::InsertAt { index: 0, value }) => value + ); + let msg = assert_matches!( + message.as_event().unwrap().content(), + TimelineItemContent::Message(msg) => msg + ); + let text = assert_matches!(msg.msgtype(), MessageType::Text(text) => text); + assert_eq!(text.body, "the world is big"); +} + +#[async_test] +async fn reaction() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "body": "hello", + "msgtype": "m.text", + }, + "event_id": "$TTvQUp1e17qkw41rBSjpZ", + "origin_server_ts": 152037280, + "sender": "@alice:example.org", + "type": "m.room.message", + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": { + "m.relates_to": { + "event_id": "$TTvQUp1e17qkw41rBSjpZ", + "key": "👍", + "rel_type": "m.annotation", + }, + }, + "event_id": "$031IXQRi27504", + "origin_server_ts": 152038300, + "sender": "@bob:example.org", + "type": "m.reaction", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let message = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + assert_matches!(message.as_event().unwrap().content(), TimelineItemContent::Message(_)); + + let updated_message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let event_item = updated_message.as_event().unwrap(); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); + assert!(!msg.is_edited()); + assert_eq!(event_item.reactions().len(), 1); + let details = &event_item.reactions()["👍"]; + assert_eq!(details.count, uint!(1)); + let senders = assert_matches!(&details.senders, TimelineDetails::Ready(s) => s); + assert_eq!(*senders, vec![user_id!("@bob:example.org").to_owned()]); + + // TODO: After adding raw timeline items, check for one here + + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": {}, + "redacts": "$031IXQRi27504", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@bob:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + })), + )); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let updated_message = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + let event_item = updated_message.as_event().unwrap(); + let msg = assert_matches!(event_item.content(), TimelineItemContent::Message(msg) => msg); + assert!(!msg.is_edited()); + assert_eq!(event_item.reactions().len(), 0); +} + +#[async_test] +async fn redacted_message() { + let room_id = room_id!("!a98sd12bjh:example.org"); + let (client, server) = logged_in_client().await; + let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); + + let mut ev_builder = EventBuilder::new(); + ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id)); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let room = client.get_room(room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + ev_builder.add_joined_room( + JoinedRoomBuilder::new(room_id) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": {}, + "event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "origin_server_ts": 152035910, + "sender": "@alice:example.org", + "type": "m.room.message", + "unsigned": { + "redacted_because": { + "content": {}, + "redacts": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@alice:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + }, + }, + }))) + .add_timeline_event(TimelineTestEvent::Custom(json!({ + "content": {}, + "redacts": "$eeG0HA0FAZ37wP8kXlNkxx3I", + "event_id": "$N6eUCBc3vu58PL8TobGaVQzM", + "sender": "@alice:example.org", + "origin_server_ts": 152037280, + "type": "m.room.redaction", + }))), + ); + + mock_sync(&server, ev_builder.build_json_sync_response(), None).await; + let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + server.reset().await; + + let first = + assert_matches!(timeline_stream.next().await, Some(VecDiff::Push { value }) => value); + assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); + + // TODO: After adding raw timeline items, check for one here +} diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 9290178fa..dcd457ff6 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -11,11 +11,12 @@ test = false [dependencies] anyhow = "1" futures = "0.3" +futures-signals = { version = "0.3.30", default-features = false } tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.15" url = "2.2.2" [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" -features = ["sled"] +features = ["experimental-timeline", "sled"] version = "0.6.0" diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index b92ab877a..a00cf90d7 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -1,5 +1,7 @@ use std::{env, process::exit, sync::Mutex, time::Duration}; +use futures::StreamExt; +use futures_signals::signal_vec::SignalVecExt; use matrix_sdk::{ self, config::SyncSettings, @@ -7,6 +9,7 @@ use matrix_sdk::{ ruma::{ api::client::filter::{FilterDefinition, LazyLoadOptions}, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, + uint, }, Client, LoopCtrl, }; @@ -44,8 +47,29 @@ fn _event_content(event: AnySyncTimelineEvent) -> Option { } } -async fn print_timeline(_room: Room) { - // TODO +async fn print_timeline(room: Room) { + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + tokio::spawn(async move { + while let Some(_diff) = timeline_stream.next().await { + // Is a straight-forward CLI example of dynamic timeline items + // possible?? let event = event.unwrap(); + //if let Some(content) = + // event_content(event.event.deserialize().unwrap()) { + // println!("{content}"); + //} + } + }); + + loop { + match timeline.paginate_backwards(uint!(10)).await { + Ok(outcome) if !outcome.more_messages => break, + Ok(_) => {} + Err(e) => { + eprintln!("error paginating: {e}"); + } + } + } } #[tokio::main] From 086b6127e24323396e91b81946c573c2909baba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 11 Oct 2022 17:00:22 +0200 Subject: [PATCH 11/66] feat: Improve the timeline example --- Cargo.lock | 19 +++-- examples/timeline/Cargo.toml | 1 + examples/timeline/src/main.rs | 156 +++++++++++++--------------------- 3 files changed, 70 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6089c197..533649b87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -596,9 +596,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.20" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -608,7 +608,7 @@ dependencies = [ "once_cell", "strsim 0.10.0", "termcolor", - "textwrap 0.15.0", + "textwrap 0.15.1", ] [[package]] @@ -1312,7 +1312,7 @@ name = "example-emoji-verification" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.20", + "clap 3.2.22", "matrix-sdk", "tokio", "tracing-subscriber", @@ -1369,6 +1369,7 @@ name = "example-timeline" version = "0.1.0" dependencies = [ "anyhow", + "clap 3.2.22", "futures", "futures-signals", "matrix-sdk", @@ -4164,7 +4165,7 @@ name = "sled-state-inspector" version = "0.1.0" dependencies = [ "atty", - "clap 3.2.20", + "clap 3.2.22", "futures", "matrix-sdk-base", "matrix-sdk-sled", @@ -4380,9 +4381,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" @@ -4823,7 +4824,7 @@ dependencies = [ "askama", "bincode", "camino", - "clap 3.2.20", + "clap 3.2.22", "fs-err", "goblin", "heck 0.4.0", @@ -5358,7 +5359,7 @@ checksum = "4916a4a3cad759e499a3620523bf9545cc162d7a06163727dde97ce9aaa4cf39" name = "xtask" version = "0.1.0" dependencies = [ - "clap 3.2.20", + "clap 3.2.22", "serde", "serde_json", "xshell", diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index dcd457ff6..56bef7e00 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -10,6 +10,7 @@ test = false [dependencies] anyhow = "1" +clap = "3.2.22" futures = "0.3" futures-signals = { version = "0.3.30", default-features = false } tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index a00cf90d7..8f87c86af 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -1,123 +1,85 @@ -use std::{env, process::exit, sync::Mutex, time::Duration}; - +use anyhow::Result; +use clap::Parser; use futures::StreamExt; use futures_signals::signal_vec::SignalVecExt; -use matrix_sdk::{ - self, - config::SyncSettings, - room::Room, - ruma::{ - api::client::filter::{FilterDefinition, LazyLoadOptions}, - events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, - uint, - }, - Client, LoopCtrl, -}; -use tokio::sync::oneshot; +use matrix_sdk::{self, config::SyncSettings, ruma::OwnedRoomId, Client}; use url::Url; -async fn login(homeserver_url: String, username: &str, password: &str) -> Client { - let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL"); - let client = Client::builder() - .homeserver_url(homeserver_url) +#[derive(Parser, Debug)] +struct Cli { + /// The homeserver to connect to. + #[clap(value_parser)] + homeserver: Url, + + /// The user name that should be used for the login. + #[clap(value_parser)] + user_name: String, + + /// The password that should be used for the login. + #[clap(value_parser)] + password: String, + + /// Set the proxy that should be used for the connection. + #[clap(short, long)] + proxy: Option, + + /// Enable verbose logging output. + #[clap(short, long, action)] + verbose: bool, + + /// The room id that we should listen for the, + #[clap(value_parser)] + room_id: OwnedRoomId, +} + +async fn login(cli: Cli) -> Result { + let builder = Client::builder() + .homeserver_url(cli.homeserver) .sled_store("./", Some("some password")) .await - .unwrap() - .build() - .await .unwrap(); + let builder = if let Some(proxy) = cli.proxy { builder.proxy(proxy) } else { builder }; + + let client = builder.build().await?; + client - .login_username(username, password) + .login_username(&cli.user_name, &cli.password) .initial_device_display_name("rust-sdk") .send() - .await - .unwrap(); - client -} + .await?; -fn _event_content(event: AnySyncTimelineEvent) -> Option { - if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(event), - )) = event - { - Some(event.content.msgtype.body().to_owned()) - } else { - None - } -} - -async fn print_timeline(room: Room) { - let timeline = room.timeline(); - let mut timeline_stream = timeline.signal().to_stream(); - tokio::spawn(async move { - while let Some(_diff) = timeline_stream.next().await { - // Is a straight-forward CLI example of dynamic timeline items - // possible?? let event = event.unwrap(); - //if let Some(content) = - // event_content(event.event.deserialize().unwrap()) { - // println!("{content}"); - //} - } - }); - - loop { - match timeline.paginate_backwards(uint!(10)).await { - Ok(outcome) if !outcome.more_messages => break, - Ok(_) => {} - Err(e) => { - eprintln!("error paginating: {e}"); - } - } - } + Ok(client) } #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<()> { tracing_subscriber::fmt::init(); - let (homeserver_url, username, password, room_id) = - match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) { - (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d), - _ => { - eprintln!( - "Usage: {} ", - env::args().next().unwrap() - ); - exit(1) - } - }; + let cli = Cli::parse(); + let room_id = cli.room_id.clone(); + let client = login(cli).await?; - let client = login(homeserver_url, &username, &password).await; - - let mut filter = FilterDefinition::default(); - filter.room.include_leave = true; - filter.room.state.lazy_load_options = - LazyLoadOptions::Enabled { include_redundant_members: false }; - - let sync_settings = SyncSettings::new().timeout(Duration::from_secs(30)).filter(filter.into()); - let (sender, receiver) = oneshot::channel::<()>(); - let sender = Mutex::new(Some(sender)); - let client_clone = client.clone(); - tokio::spawn(async move { - client_clone - .sync_with_callback(sync_settings, |_| async { - if let Some(sender) = sender.lock().unwrap().take() { - sender.send(()).unwrap(); - } - LoopCtrl::Continue - }) - .await - .unwrap(); - }); + let sync_settings = SyncSettings::default(); // Wait for the first sync response println!("Wait for the first sync"); - receiver.await.unwrap(); - let room = client.get_room(room_id.as_str().try_into().unwrap()).unwrap(); + client.sync_once(sync_settings.clone()).await?; - print_timeline(room).await; + // Get the timeline stream and listen to it. + let room = client.get_room(&room_id).unwrap(); + let timeline = room.timeline(); + let mut timeline_stream = timeline.signal().to_stream(); + + tokio::spawn(async move { + while let Some(diff) = timeline_stream.next().await { + println!("Received a timeline diff {diff:#?}"); + } + }); + + // Sync forever + client.sync(sync_settings).await?; Ok(()) } From b83e6be01e2b39ec782bb0a8e99b472ab0642a29 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 5 Oct 2022 16:58:59 +0200 Subject: [PATCH 12/66] refactor(sdk): Use lower-level libraries for builtin SSO server --- Cargo.lock | 23 ++++++- crates/matrix-sdk/Cargo.toml | 5 +- crates/matrix-sdk/src/client/login_builder.rs | 62 ++++++++++++++----- xtask/src/ci.rs | 2 +- xtask/src/fixup.rs | 2 +- 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 533649b87..0ed9aac1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2328,6 +2328,7 @@ dependencies = [ "futures-util", "getrandom 0.2.7", "http", + "hyper", "image 0.24.3", "indexmap", "matches", @@ -2347,10 +2348,10 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tower", "tracing", "tracing-subscriber", "url", - "warp", "wasm-bindgen-test", "wasm-timer", "wiremock", @@ -4605,6 +4606,26 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.2" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index d26985028..e6bf6317f 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -37,7 +37,7 @@ markdown = ["ruma/markdown"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] -sso-login = ["warp", "dep:rand", "dep:tokio-stream"] +sso-login = ["dep:hyper", "dep:rand", "dep:tokio-stream", "dep:tower"] appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] @@ -73,6 +73,7 @@ futures-signals = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.21", default-features = false } http = "0.2.6" indexmap = "1.9.1" +hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true } matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false } matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", default-features = false, optional = true } @@ -84,9 +85,9 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" tokio-stream = { version = "0.1.8", features = ["net"], optional = true } +tower = { version = "0.4.13", features = ["make"], optional = true } tracing = "0.1.34" url = "2.2.2" -warp = { version = "0.3.2", default-features = false, optional = true } zeroize = "1.3.0" [dependencies.image] diff --git a/crates/matrix-sdk/src/client/login_builder.rs b/crates/matrix-sdk/src/client/login_builder.rs index d43114f90..34c4bdcf0 100644 --- a/crates/matrix-sdk/src/client/login_builder.rs +++ b/crates/matrix-sdk/src/client/login_builder.rs @@ -262,18 +262,20 @@ where /// Send the login request. #[instrument(target = "matrix_sdk::client", name = "login", skip_all, fields(method = "sso"))] pub async fn send(self) -> Result { + use std::convert::Infallible; use std::{ - collections::HashMap, io::{Error as IoError, ErrorKind as IoErrorKind}, ops::Range, sync::{Arc, Mutex}, }; + use http::{Method, StatusCode}; + use hyper::{server::conn::AddrIncoming, service::service_fn}; use rand::{thread_rng, Rng}; + use serde::Deserialize; use tokio::{net::TcpListener, sync::oneshot}; - use tokio_stream::wrappers::TcpListenerStream; + use tracing::debug; use url::Url; - use warp::Filter; /// The range of ports the SSO server will try to bind to randomly. /// @@ -302,14 +304,30 @@ where .unwrap_or("The Single Sign-On login process is complete. You can close this page now.") .to_owned(); - let route = warp::get().and(warp::query::>()).map( - move |p: HashMap| { - if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { - data_tx.send(p.get("loginToken").cloned()).unwrap(); - } - http::Response::builder().body(response.clone()) - }, - ); + #[derive(Deserialize)] + struct QueryParameters { + #[serde(rename = "loginToken")] + login_token: Option, + } + + let handle_request = move |request: http::Request<_>| { + if request.method() != Method::HEAD && request.method() != Method::GET { + return Err(StatusCode::METHOD_NOT_ALLOWED); + } + + if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { + let query_string = request.uri().query().unwrap_or(""); + let query: QueryParameters = + ruma::serde::urlencoded::from_str(query_string).map_err(|_| { + debug!("Failed to deserialize query parameters"); + StatusCode::BAD_REQUEST + })?; + + data_tx.send(query.login_token).unwrap(); + } + + Ok(http::Response::new(response.clone())) + }; let listener = { if redirect_url.port().expect("The redirect URL doesn't include a port") == 0 { @@ -338,12 +356,24 @@ where } }; - let server = warp::serve(route).serve_incoming_with_graceful_shutdown( - TcpListenerStream::new(listener), - async { + let incoming = AddrIncoming::from_listener(listener).unwrap(); + let server = hyper::Server::builder(incoming) + .serve(tower::make::Shared::new(service_fn(move |request| { + let handle_request = handle_request.clone(); + async move { + match handle_request(request) { + Ok(res) => Ok::<_, Infallible>(res.map(hyper::Body::from)), + Err(status_code) => { + let mut res = http::Response::new(hyper::Body::default()); + *res.status_mut() = status_code; + Ok(res) + } + } + } + }))) + .with_graceful_shutdown(async { signal_rx.await.ok(); - }, - ); + }); tokio::spawn(server); diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index bcce11bc1..2ec6e24b4 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -133,7 +133,7 @@ fn check_clippy() -> Result<()> { cmd!( "rustup run nightly cargo clippy --workspace --all-targets --exclude matrix-sdk-crypto --exclude xtask - --no-default-features --features native-tls,warp + --no-default-features --features native-tls,sso-login -- -D warnings" ) .run()?; diff --git a/xtask/src/fixup.rs b/xtask/src/fixup.rs index 70fa0ebf2..532bba9f1 100644 --- a/xtask/src/fixup.rs +++ b/xtask/src/fixup.rs @@ -66,7 +66,7 @@ fn fix_clippy() -> Result<()> { "rustup run nightly cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --exclude matrix-sdk-crypto --exclude xtask - --no-default-features --features native-tls,warp + --no-default-features --features native-tls,sso-login -- -D warnings" ) .run()?; From 95613be6c95f3a4a024541dbad8863b50ee470ef Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 10 Oct 2022 12:08:47 +0200 Subject: [PATCH 13/66] chore: Remove redundant cfg attribute --- testing/matrix-sdk-test/src/appservice.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/matrix-sdk-test/src/appservice.rs b/testing/matrix-sdk-test/src/appservice.rs index 46e6e02dc..133077b45 100644 --- a/testing/matrix-sdk-test/src/appservice.rs +++ b/testing/matrix-sdk-test/src/appservice.rs @@ -39,7 +39,6 @@ impl TransactionBuilder { } /// Build the transaction - #[cfg(feature = "appservice")] pub fn build_json_transaction(&self) -> Value { let body = serde_json::json! { { From 1e77e91ec4a6494231a8fc0a1306d8d1c8544f44 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 10 Oct 2022 15:05:46 +0200 Subject: [PATCH 14/66] refactor(appservice): Replace warp with axum --- Cargo.lock | 171 ++++++----- crates/matrix-sdk-appservice/Cargo.toml | 6 +- crates/matrix-sdk-appservice/src/error.rs | 12 +- crates/matrix-sdk-appservice/src/lib.rs | 285 +++++++++--------- crates/matrix-sdk-appservice/src/webserver.rs | 285 +++++++----------- crates/matrix-sdk/src/client/login_builder.rs | 6 +- testing/matrix-sdk-test/src/appservice.rs | 12 +- 7 files changed, 366 insertions(+), 411 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ed9aac1e..df175b797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,52 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.3", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "tower-layer", + "tower-service", +] + [[package]] name = "backoff" version = "0.4.0" @@ -1722,7 +1768,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tracing", ] @@ -1741,31 +1787,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "headers" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" -dependencies = [ - "base64", - "bitflags", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha-1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.3.3" @@ -1830,6 +1851,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "http-types" version = "2.12.0" @@ -2304,6 +2331,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "matrix-sdk" version = "0.6.0" @@ -2362,11 +2395,13 @@ dependencies = [ name = "matrix-sdk-appservice" version = "0.1.0" dependencies = [ + "axum", "dashmap", "http", + "http-body", + "hyper", "matrix-sdk", "matrix-sdk-test", - "percent-encoding", "regex", "ruma", "serde", @@ -2374,10 +2409,10 @@ dependencies = [ "serde_yaml", "thiserror", "tokio", + "tower", "tracing", "tracing-subscriber", "url", - "warp", "wiremock", ] @@ -4056,17 +4091,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.3", -] - [[package]] name = "sha2" version = "0.9.9" @@ -4315,6 +4339,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "synstructure" version = "0.12.6" @@ -4569,20 +4599,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.3" @@ -4612,7 +4628,9 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ + "futures-core", "futures-util", + "pin-project", "pin-project-lite", "tokio", "tower-layer", @@ -4620,6 +4638,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.1" @@ -5029,34 +5066,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers", - "http", - "hyper", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-stream", - "tokio-util 0.6.10", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index 565f30f29..8588a5f39 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -30,10 +30,12 @@ sso-login = ["matrix-sdk/sso-login"] docs = [] [dependencies] +axum = { version = "0.5.16", default-features = false, features = ["json"] } dashmap = "5.2.0" http = "0.2.6" +http-body = "0.4.5" +hyper = { version = "0.14.20", features = ["http1", "http2", "server"] } matrix-sdk = { version = "0.6.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } -percent-encoding = "2.1.0" regex = "1.5.5" ruma = { version = "0.7.0", features = ["client-api-c", "appservice-api-s"] } serde = "1.0.136" @@ -41,9 +43,9 @@ serde_json = "1.0.79" serde_yaml = "0.9.4" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } thiserror = "1.0.30" +tower = { version = "0.4.13", default-features = false } tracing = "0.1.34" url = "2.2.2" -warp = { version = "0.3.2", default-features = false } [dev-dependencies] matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", features = ["appservice"] } diff --git a/crates/matrix-sdk-appservice/src/error.rs b/crates/matrix-sdk-appservice/src/error.rs index 68a863ed5..69a825c2a 100644 --- a/crates/matrix-sdk-appservice/src/error.rs +++ b/crates/matrix-sdk-appservice/src/error.rs @@ -77,8 +77,8 @@ pub enum Error { #[error("utf8 error: {0}")] Utf8(#[from] std::str::Utf8Error), - #[error("warp rejection: {0}")] - WarpRejection(String), + #[error("hyper error: {0}")] + Hyper(#[from] hyper::Error), } impl Error { @@ -101,14 +101,6 @@ impl Error { } } -impl warp::reject::Reject for Error {} - -impl From for Error { - fn from(rejection: warp::Rejection) -> Self { - Self::WarpRejection(format!("{:?}", rejection)) - } -} - impl From for Error { fn from(e: matrix_sdk::HttpError) -> Self { matrix_sdk::Error::from(e).into() diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index 36df96cc5..f2af0854f 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -79,11 +79,13 @@ //! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228 //! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples -use std::sync::Arc; +use std::{convert::Infallible, fmt::Debug, future::Future, sync::Arc}; +use axum::body::{Bytes, HttpBody}; use dashmap::DashMap; pub use error::Error; use event_handler::AppserviceFn; +use http_body::combinators::UnsyncBoxBody; pub use matrix_sdk; #[doc(no_inline)] pub use matrix_sdk::ruma; @@ -103,6 +105,7 @@ use ruma::{ use serde::Deserialize; use thiserror::Error; use tokio::task::JoinHandle; +use tower::Service; use tracing::{debug, info, warn}; mod error; @@ -414,19 +417,19 @@ impl AppService { false } - /// Returns a [`warp::Filter`] to be used as [`warp::serve()`] route. - /// - /// Note that if you handle any of the [application-service-specific - /// routes], including the legacy routes, you will break the appservice - /// functionality. - /// - /// Hint: [`warp::Filter`]s can be converted to an `hyper::Service` using - /// [`warp::service`], which allows using it with tower-compatible - /// frameworks such as axum. - /// - /// [application-service-specific routes]: https://spec.matrix.org/unstable/application-service-api/#legacy-routes - pub fn warp_filter(&self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> { - webserver::warp_filter(self.clone()) + /// Returns a [`Service`] that processes appservice requests. + pub fn service( + &self, + // axum::Error is part of the signature because axum::Router::nest + // requires the inner service to have that exact response (body) type + // in 0.5.x. This will be fixed in axum 0.6.0. + ) -> impl HttpService> + Clone + where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + { + webserver::router(self.clone()) } /// Receive an incoming [transaction], pushing the contained events to @@ -565,6 +568,28 @@ impl AppService { } } +#[rustfmt::skip] +pub trait HttpService: + Service< + http::Request, + Response = http::Response, + Error = Infallible, + Future = >::Future, + > +{ + type Future: Future> + Send; + type ResBody; +} + +impl HttpService for S +where + S: Service, Response = http::Response, Error = Infallible>, + >>::Future: Send, +{ + type Future = >>::Future; + type ResBody = ResBody; +} + #[cfg(test)] mod tests { use std::{ @@ -572,6 +597,8 @@ mod tests { sync::{Arc, Mutex}, }; + use http::{Method, Request}; + use hyper::Body; use matrix_sdk::{ config::RequestConfig, ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent}, @@ -585,7 +612,7 @@ mod tests { serde::Raw, }; use serde_json::json; - use warp::{Filter, Reply}; + use tower::ServiceExt; use wiremock::{ matchers::{body_json, header, method, path}, Mock, MockServer, ResponseTemplate, @@ -656,21 +683,22 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); - let appservice = appservice(None, None).await?; - - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = appservice(None, None) + .await? + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -681,7 +709,7 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); let appservice = appservice(None, None).await?; @@ -695,17 +723,20 @@ mod tests { } }); - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); + let mut service = appservice.service(); - assert_eq!(status, 200); + let response = service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction.clone())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), 200); { let on_room_member_called = *on_state_member.lock().unwrap(); assert!(on_room_member_called); @@ -717,19 +748,20 @@ mod tests { *on_room_member_called = false; } - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); // According to https://spec.matrix.org/v1.2/application-service-api/#pushing-events // This should noop and return 200. - assert_eq!(status, 200); + assert_eq!(response.status(), 200); { let on_room_member_called = *on_state_member.lock().unwrap(); // This time we should not have called the event handler. @@ -746,16 +778,13 @@ mod tests { let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token"; - let status = warp::test::request() - .method("GET") - .path(uri) - .filter(&appservice.warp_filter()) + let response = appservice + .service() + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -767,16 +796,13 @@ mod tests { let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token"; - let status = warp::test::request() - .method("GET") - .path(uri) - .filter(&appservice.warp_filter()) + let response = appservice + .service() + .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 200); + assert_eq!(response.status(), 200); Ok(()) } @@ -786,23 +812,24 @@ mod tests { let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token"; let mut transaction_builder = TransactionBuilder::new(); - let transaction = transaction_builder - .add_timeline_event(TimelineTestEvent::Member) - .build_json_transaction(); + let transaction = + transaction_builder.add_timeline_event(TimelineTestEvent::Member).build_transaction(); let appservice = appservice(None, None).await?; - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + let response = appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await - .unwrap() - .into_response() - .status(); + .unwrap(); - assert_eq!(status, 401); + assert_eq!(response.status(), 401); Ok(()) } @@ -813,23 +840,23 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); let appservice = appservice(None, None).await?; - { - let status = warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) - .await - .unwrap() - .into_response() - .status(); + let response = appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) + .await + .unwrap(); - assert_eq!(status, 401); - } + assert_eq!(response.status(), 401); Ok(()) } @@ -852,13 +879,17 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction = transaction_builder.build_json_transaction(); + let transaction = transaction_builder.build_transaction(); - warp::test::request() - .method("PUT") - .path(uri) - .json(&transaction) - .filter(&appservice.warp_filter()) + appservice + .service() + .oneshot( + Request::builder() + .method(Method::PUT) + .uri(uri) + .body(Body::from(transaction)) + .unwrap(), + ) .await .unwrap(); @@ -868,30 +899,6 @@ mod tests { Ok(()) } - #[async_test] - async fn test_unrelated_path() -> Result<()> { - let appservice = appservice(None, None).await?; - - let status = { - let consumer_filter = warp::any() - .and(appservice.warp_filter()) - .or(warp::get().and(warp::path("unrelated").map(warp::reply))); - - let response = warp::test::request() - .method("GET") - .path("/unrelated") - .filter(&consumer_filter) - .await? - .into_response(); - - response.status() - }; - - assert_eq!(status, 200); - - Ok(()) - } - #[async_test] async fn test_appservice_on_sub_path() -> Result<()> { let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost"); @@ -900,29 +907,33 @@ mod tests { let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::Member); - let transaction_1 = transaction_builder.build_json_transaction(); + let transaction_1 = transaction_builder.build_transaction(); let mut transaction_builder = TransactionBuilder::new(); transaction_builder.add_timeline_event(TimelineTestEvent::MemberNameChange); - let transaction_2 = transaction_builder.build_json_transaction(); + let transaction_2 = transaction_builder.build_transaction(); let appservice = appservice(None, None).await?; + let mut service = axum::Router::new().nest("/sub_path", appservice.service()); - { - warp::test::request() - .method("PUT") - .path(uri_1) - .json(&transaction_1) - .filter(&warp::path("sub_path").and(appservice.warp_filter())) - .await?; - - warp::test::request() - .method("PUT") - .path(uri_2) - .json(&transaction_2) - .filter(&warp::path("sub_path").and(appservice.warp_filter())) - .await?; - }; + service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri_1) + .body(Body::from(transaction_1))?, + ) + .await + .unwrap(); + service + .call( + Request::builder() + .method(Method::PUT) + .uri(uri_2) + .body(Body::from(transaction_2))?, + ) + .await + .unwrap(); let members = appservice .virtual_user(None) @@ -1037,7 +1048,9 @@ mod tests { } mod registration { - use super::*; + use ruma::api::appservice::Registration; + + use crate::{tests::registration_string, AppServiceRegistration, Result}; #[test] fn test_registration() -> Result<()> { diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs index 49fa38894..ed18a6c16 100644 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ b/crates/matrix-sdk-appservice/src/webserver.rs @@ -14,20 +14,19 @@ use std::net::ToSocketAddrs; -use matrix_sdk::{ - bytes::Bytes, - ruma::{ - self, - api::{ - appservice::query::{ - query_room_alias::v1 as query_room, query_user_id::v1 as query_user, - }, - IncomingRequest, - }, - }, +use axum::{ + async_trait, + body::{Bytes, HttpBody}, + extract::{FromRequest, Path, RequestParts}, + middleware::{self, Next}, + response::{ErrorResponse, IntoResponse, Response}, + routing::{get, put}, + BoxError, Extension, Json, Router, }; -use serde::Serialize; -use warp::{filters::BoxedFilter, path::FullPath, Filter, Rejection, Reply}; +use http::StatusCode; +use matrix_sdk::ruma::{self, api::IncomingRequest}; +use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; use crate::{AppService, Error, Result}; @@ -36,195 +35,133 @@ pub async fn run_server( host: impl Into, port: impl Into, ) -> Result<()> { - let routes = warp_filter(appservice); + let router: Router = router(appservice); - let mut addr = format!("{}:{}", host.into(), port.into()).to_socket_addrs()?; + let mut addr = (host.into(), port.into()).to_socket_addrs()?; if let Some(addr) = addr.next() { - warp::serve(routes).run(addr).await; + hyper::Server::bind(&addr).serve(router.into_make_service()).await?; Ok(()) } else { Err(Error::HostPortToSocketAddrs) } } -pub fn warp_filter(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - // TODO: try to use a struct instead of needlessly cloning appservice multiple - // times on every request - warp::any() - .and(filters::transactions(appservice.clone())) - .or(filters::users(appservice.clone())) - .or(filters::rooms(appservice)) - .recover(handle_rejection) - .boxed() +pub fn router(appservice: AppService) -> Router +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + Router::new() + .route("/_matrix/app/v1/users/:user_id", get(handlers::user)) + .route("/_matrix/app/v1/rooms/:room_id", get(handlers::room)) + .route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction)) + .route("/users/:user_id", get(handlers::user)) + .route("/rooms/:room_id", get(handlers::room)) + .route("/transactions/:txn_id", put(handlers::transaction)) + // FIXME: Use Route::with_state instead of an Extension layer in axum 0.6 + .layer( + ServiceBuilder::new() + .layer(Extension(appservice)) + .layer(middleware::from_fn(validate_access_token)), + ) } -mod filters { - use super::*; +pub struct MatrixRequest(T); - pub fn users(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::get() - .and( - warp::path!("_matrix" / "app" / "v1" / "users" / String) - // legacy route - .or(warp::path!("users" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::user) - .boxed() - } +#[async_trait] +impl FromRequest for MatrixRequest +where + T: IncomingRequest, + B: HttpBody + Send, + B::Data: Send, + B::Error: Into, +{ + type Rejection = Response; - pub fn rooms(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::get() - .and( - warp::path!("_matrix" / "app" / "v1" / "rooms" / String) - // legacy route - .or(warp::path!("rooms" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::room) - .boxed() - } + async fn from_request(req: &mut RequestParts) -> Result { + let path_params = + req.extract::>>().await.map_err(IntoResponse::into_response)?; + let parts = req.extract::().await.map_err(|e| match e {})?; + let body = req.extract::().await.map_err(IntoResponse::into_response)?; - pub fn transactions(appservice: AppService) -> BoxedFilter<(impl Reply,)> { - warp::put() - .and( - warp::path!("_matrix" / "app" / "v1" / "transactions" / String) - // legacy route - .or(warp::path!("transactions" / String)) - .unify(), - ) - .and(warp::path::end()) - .and(common(appservice)) - .and_then(handlers::transaction) - .boxed() - } + let http_request = http::Request::from_parts(parts, body); - fn common(appservice: AppService) -> BoxedFilter<(AppService, http::Request)> { - warp::any() - .and(valid_access_token(appservice.registration().hs_token.clone())) - .map(move || appservice.clone()) - .and( - http_request().and_then(|request| async move { - Ok::, Rejection>(request) - }), - ) - .boxed() - } + let request = T::try_from_http_request(http_request, &path_params).map_err(|_e| { + // TODO: JSON error response + StatusCode::BAD_REQUEST.into_response() + })?; - pub fn valid_access_token(token: String) -> BoxedFilter<()> { - warp::any() - .map(move || token.clone()) - .and(warp::query::raw()) - .and_then(|token: String, query: String| async move { - let query: Vec<(String, String)> = - ruma::serde::urlencoded::from_str(&query).map_err(Error::from)?; - - if query.into_iter().any(|(key, value)| key == "access_token" && value == token) { - Ok::<(), Rejection>(()) - } else { - Err(warp::reject::custom(Unauthorized)) - } - }) - .untuple_one() - .boxed() - } - - pub fn http_request() -> impl Filter,), Error = Rejection> + Copy - { - // TODO: extract `hyper::Request` instead - // blocked by https://github.com/seanmonstar/warp/issues/139 - warp::any() - .and(warp::method()) - .and(warp::filters::path::full()) - .and(warp::filters::query::raw()) - .and(warp::header::headers_cloned()) - .and(warp::body::bytes()) - .and_then(|method, path: FullPath, query, headers, bytes| async move { - let uri = http::uri::Builder::new() - .path_and_query(format!("{}?{query}", path.as_str())) - .build() - .map_err(Error::from)?; - - let mut request = http::Request::builder() - .method(method) - .uri(uri) - .body(bytes) - .map_err(Error::from)?; - - *request.headers_mut() = headers; - - Ok::, Rejection>(request) - }) + Ok(Self(request)) } } mod handlers { - use percent_encoding::percent_decode_str; + use axum::{response::IntoResponse, Extension, Json}; + use http::StatusCode; + use ruma::api::appservice::{ + event::push_events, + query::{query_room_alias, query_user_id}, + }; use serde::Serialize; - use super::*; + use super::{ErrorMessage, MatrixRequest}; + use crate::AppService; #[derive(Serialize)] struct EmptyObject {} pub async fn user( - user_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { + Extension(appservice): Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() { - let user_id = percent_decode_str(&user_id).decode_utf8().map_err(Error::from)?; - let request = query_user::IncomingRequest::try_from_http_request(request, &[user_id]) - .map_err(Error::from)?; - return if user_exists(appservice.clone(), request).await { - Ok(warp::reply::json(&EmptyObject {})) + if user_exists(appservice.clone(), request).await { + Ok(Json(EmptyObject {})) } else { - Err(warp::reject::not_found()) - }; + Err(StatusCode::NOT_FOUND) + } + } else { + Ok(Json(EmptyObject {})) } - Ok(warp::reply::json(&EmptyObject {})) } pub async fn room( - room_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { + Extension(appservice): Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() { - let room_id = percent_decode_str(&room_id).decode_utf8().map_err(Error::from)?; - let request = query_room::IncomingRequest::try_from_http_request(request, &[room_id]) - .map_err(Error::from)?; - return if room_exists(appservice.clone(), request).await { - Ok(warp::reply::json(&EmptyObject {})) + if room_exists(appservice.clone(), request).await { + Ok(Json(&EmptyObject {})) } else { - Err(warp::reject::not_found()) - }; + Err(StatusCode::NOT_FOUND) + } + } else { + Ok(Json(&EmptyObject {})) } - Ok(warp::reply::json(&EmptyObject {})) } pub async fn transaction( - txn_id: String, - appservice: AppService, - request: http::Request, - ) -> Result { - let incoming_transaction: ruma::api::appservice::event::push_events::v1::IncomingRequest = - ruma::api::IncomingRequest::try_from_http_request(request, &[txn_id]) - .map_err(Error::from)?; - - appservice.receive_transaction(incoming_transaction).await?; - Ok(warp::reply::json(&EmptyObject {})) + appservice: Extension, + MatrixRequest(request): MatrixRequest, + ) -> impl IntoResponse { + match appservice.receive_transaction(request).await { + Ok(_) => Ok(Json(&EmptyObject {})), + Err(e) => { + let status_code = StatusCode::INTERNAL_SERVER_ERROR; + Err(( + status_code, + Json(ErrorMessage { code: status_code.as_u16(), message: e.to_string() }), + )) + } + } } } -#[derive(Debug)] -struct Unauthorized; - -impl warp::reject::Reject for Unauthorized {} +#[derive(Deserialize)] +struct QueryParameters { + access_token: String, +} #[derive(Serialize)] struct ErrorMessage { @@ -232,15 +169,23 @@ struct ErrorMessage { message: String, } -pub async fn handle_rejection(err: Rejection) -> Result { - if err.find::().is_some() || err.find::().is_some() { - let code = http::StatusCode::UNAUTHORIZED; - let message = "UNAUTHORIZED"; +async fn validate_access_token( + req: http::Request, + next: Next, +) -> Result { + let appservice = + req.extensions().get::().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - let json = - warp::reply::json(&ErrorMessage { code: code.as_u16(), message: message.into() }); - Ok(warp::reply::with_status(json, code)) - } else { - Err(err) + let query_string = req.uri().query().unwrap_or(""); + match ruma::serde::urlencoded::from_str::(query_string) { + Ok(query) if query.access_token == appservice.registration.hs_token => { + Ok(next.run(req).await) + } + _ => { + let status_code = StatusCode::UNAUTHORIZED; + let message = + ErrorMessage { code: status_code.as_u16(), message: "UNAUTHORIZED".into() }; + Err((status_code, Json(message)).into()) + } } } diff --git a/crates/matrix-sdk/src/client/login_builder.rs b/crates/matrix-sdk/src/client/login_builder.rs index 34c4bdcf0..0fbcb6924 100644 --- a/crates/matrix-sdk/src/client/login_builder.rs +++ b/crates/matrix-sdk/src/client/login_builder.rs @@ -262,8 +262,8 @@ where /// Send the login request. #[instrument(target = "matrix_sdk::client", name = "login", skip_all, fields(method = "sso"))] pub async fn send(self) -> Result { - use std::convert::Infallible; use std::{ + convert::Infallible, io::{Error as IoError, ErrorKind as IoErrorKind}, ops::Range, sync::{Arc, Mutex}, @@ -317,8 +317,8 @@ where if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { let query_string = request.uri().query().unwrap_or(""); - let query: QueryParameters = - ruma::serde::urlencoded::from_str(query_string).map_err(|_| { + let query: QueryParameters = ruma::serde::urlencoded::from_str(query_string) + .map_err(|_| { debug!("Failed to deserialize query parameters"); StatusCode::BAD_REQUEST })?; diff --git a/testing/matrix-sdk-test/src/appservice.rs b/testing/matrix-sdk-test/src/appservice.rs index 133077b45..9505accc2 100644 --- a/testing/matrix-sdk-test/src/appservice.rs +++ b/testing/matrix-sdk-test/src/appservice.rs @@ -38,15 +38,9 @@ impl TransactionBuilder { self } - /// Build the transaction - pub fn build_json_transaction(&self) -> Value { - let body = serde_json::json! { - { - "events": self.events - } - }; - - body + /// Build the transaction as a serialized HTTP body + pub fn build_transaction(&self) -> Vec { + serde_json::to_vec(&serde_json::json!({ "events": self.events })).unwrap() } pub fn clear(&mut self) { From a7cb80df1bc6dc961afe26b57ade533daf855b3a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 13 Oct 2022 10:56:59 +0200 Subject: [PATCH 15/66] refactor(appservice): Clean up implementation of user_id_is_in_namespace --- crates/matrix-sdk-appservice/src/lib.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index f2af0854f..ed5611355 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -406,15 +406,10 @@ impl AppService { } /// Check if given `user_id` is in any of the [`AppServiceRegistration`]'s - /// `users` namespaces + /// `users` namespaces. pub fn user_id_is_in_namespace(&self, user_id: impl AsRef) -> bool { - for regex in &self.namespaces.users { - if regex.is_match(user_id.as_ref()) { - return true; - } - } - - false + let user_id = user_id.as_ref(); + self.namespaces.users.iter().any(|regex| regex.is_match(user_id)) } /// Returns a [`Service`] that processes appservice requests. From 95face4aa4b086611c1cc1d589446ebf073c1c10 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 13 Oct 2022 16:27:28 +0200 Subject: [PATCH 16/66] chore(bindings): Delete unused code left over from old timeline API --- bindings/matrix-sdk-ffi/src/api.udl | 8 ---- .../matrix-sdk-ffi/src/backward_stream.rs | 42 ------------------- bindings/matrix-sdk-ffi/src/lib.rs | 5 +-- bindings/matrix-sdk-ffi/src/room.rs | 6 +-- 4 files changed, 3 insertions(+), 58 deletions(-) delete mode 100644 bindings/matrix-sdk-ffi/src/backward_stream.rs diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index 09f961707..bb383ef55 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -225,10 +225,6 @@ interface Client { void logout(); }; -callback interface RoomDelegate { - void did_receive_message(AnyMessage message); -}; - enum Membership { "Invited", "Joined", @@ -257,10 +253,6 @@ interface Room { void redact(string event_id, string? reason, string? txn_id); }; -interface BackwardsStream { - sequence paginate_backwards(u64 count); -}; - interface RoomMessageEventContent {}; interface AnyMessage { diff --git a/bindings/matrix-sdk-ffi/src/backward_stream.rs b/bindings/matrix-sdk-ffi/src/backward_stream.rs deleted file mode 100644 index 4b664675a..000000000 --- a/bindings/matrix-sdk-ffi/src/backward_stream.rs +++ /dev/null @@ -1,42 +0,0 @@ -use core::pin::Pin; -use std::sync::Arc; - -use futures_core::Stream; -use matrix_sdk::{deserialized_responses::SyncTimelineEvent, locks::Mutex, Result}; -use tokio_stream::StreamExt; -use tracing::error; - -use super::{ - messages::{sync_event_to_message, AnyMessage}, - RUNTIME, -}; - -type MsgStream = Pin> + Send>>; - -pub struct BackwardsStream { - stream: Arc>, -} - -impl BackwardsStream { - pub fn new(stream: MsgStream) -> Self { - BackwardsStream { stream: Arc::new(Mutex::new(Box::pin(stream))) } - } - - pub fn paginate_backwards(&self, count: u64) -> Vec> { - let stream = self.stream.clone(); - RUNTIME.block_on(async move { - let mut stream = stream.lock().await; - (&mut *stream) - .take(count as usize) - .filter_map(|r| match r { - Ok(ev) => sync_event_to_message(ev), - Err(e) => { - error!("Pagniation error: {e}"); - None - } - }) - .collect() - .await - }) - } -} diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 7cfce4947..2976d9155 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -3,7 +3,6 @@ #![allow(unused_qualifications)] pub mod authentication_service; -pub mod backward_stream; pub mod client; pub mod client_builder; mod helpers; @@ -30,8 +29,8 @@ pub static RUNTIME: Lazy = pub use matrix_sdk::ruma::{api::client::account::register, UserId}; pub use self::{ - authentication_service::*, backward_stream::*, client::*, messages::*, room::*, - session_verification::*, sliding_sync::*, + authentication_service::*, client::*, messages::*, room::*, session_verification::*, + sliding_sync::*, }; #[derive(Default, Debug)] diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 104b48b94..c418c1bc5 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -9,11 +9,7 @@ use matrix_sdk::{ }, }; -use super::{messages::AnyMessage, RUNTIME}; - -pub trait RoomDelegate: Sync + Send { - fn did_receive_message(&self, messages: Arc); -} +use super::RUNTIME; pub enum Membership { Invited, From db2771bd173ff9a449b08519b716aa3884e34c48 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 13 Oct 2022 16:23:02 +0200 Subject: [PATCH 17/66] feat(bindings): New timeline API --- bindings/matrix-sdk-ffi/Cargo.toml | 6 +- bindings/matrix-sdk-ffi/src/api.udl | 178 ++++++-- bindings/matrix-sdk-ffi/src/lib.rs | 33 +- bindings/matrix-sdk-ffi/src/messages.rs | 254 ----------- bindings/matrix-sdk-ffi/src/room.rs | 88 +++- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 32 +- bindings/matrix-sdk-ffi/src/timeline.rs | 405 ++++++++++++++++++ crates/matrix-sdk/Cargo.toml | 1 + .../src/room/timeline/event_item.rs | 36 +- 9 files changed, 708 insertions(+), 325 deletions(-) delete mode 100644 bindings/matrix-sdk-ffi/src/messages.rs create mode 100644 bindings/matrix-sdk-ffi/src/timeline.rs diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 23d029860..6ef7dc7f4 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -12,6 +12,10 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk" [lib] crate-type = ["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 = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a", features = ["builtin-bindgen"] } @@ -19,7 +23,7 @@ uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b anyhow = "1.0.51" extension-trait = "1.0.1" futures-core = "0.3.17" -futures-signals = { version = "0.3.28" } +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 diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index bb383ef55..b92e5eba6 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -243,6 +243,14 @@ interface Room { [Throws=ClientError] string? member_display_name(string user_id); + void add_timeline_listener(TimelineListener listener); + + // Loads older messages into the timeline. + // + // Raises an exception if there are no timeline listeners. + [Throws=ClientError] + PaginationOutcome paginate_backwards(u16 limit); + [Throws=ClientError] void send(RoomMessageEventContent msg, string? txn_id); @@ -253,46 +261,140 @@ interface Room { void redact(string event_id, string? reason, string? txn_id); }; +callback interface TimelineListener { + void on_update(TimelineDiff update); +}; + +interface TimelineDiff { + TimelineChange change(); + + [Self=ByArc] + sequence? replace(); + [Self=ByArc] + InsertAtData? insert_at(); + [Self=ByArc] + UpdateAtData? update_at(); + u32? remove_at(); + MoveData? move(); + [Self=ByArc] + TimelineItem? push(); +}; + +enum TimelineChange { + "Replace", + "InsertAt", + "UpdateAt", + "RemoveAt", + "Move", + "Push", + "Pop", + "Clear", +}; + +dictionary InsertAtData { + u32 index; + TimelineItem item; +}; + +dictionary UpdateAtData { + u32 index; + TimelineItem item; +}; + +dictionary MoveData { + u32 old_index; + u32 new_index; +}; + +interface TimelineItem {}; + +interface EventTimelineItem { + TimelineKey key(); + sequence reactions(); +}; + +[Enum] +interface TimelineKey { + TransactionId(string txn_id); + EventId(string event_id); +}; + +// Other methods defined via proc-macro +interface Message { + MessageType? msgtype(); +}; + +[Enum] +interface MessageType { + Emote(EmoteMessageContent content); + Image(ImageMessageContent content); + Notice(NoticeMessageContent content); + Text(TextMessageContent content); +}; + +dictionary EmoteMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary ImageMessageContent { + string body; + MediaSource source; + ImageInfo? info; +}; + +dictionary ImageInfo { + u64? height; + u64? width; + string? mimetype; + u64? size; + ThumbnailInfo? thumbnail_info; + MediaSource? thumbnail_source; + string? blurhash; +}; + +dictionary ThumbnailInfo { + u64? height; + u64? width; + string? mimetype; + u64? size; +}; + +dictionary NoticeMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary TextMessageContent { + string body; + FormattedBody? formatted; +}; + +dictionary FormattedBody { + MessageFormat format; + string body; +}; + +enum MessageFormat { + "Html", + "Unknown", +}; + +dictionary Reaction { + string key; + u64 count; + // senders to come +}; + +interface VirtualTimelineItem {}; + +dictionary PaginationOutcome { + // Whether there's more messages to be paginated. + boolean more_messages; +}; + interface RoomMessageEventContent {}; -interface AnyMessage { - TextMessage? text_message(); - ImageMessage? image_message(); - NoticeMessage? notice_message(); - EmoteMessage? emote_message(); -}; - -interface BaseMessage { - string id(); - string body(); - string sender(); - u64 origin_server_ts(); - string? transaction_id(); -}; - -interface TextMessage { - BaseMessage base_message(); - string? html_body(); -}; - -interface ImageMessage { - BaseMessage base_message(); - MediaSource source(); - u64? width(); - u64? height(); - string? blurhash(); -}; - -interface NoticeMessage { - BaseMessage base_message(); - string? html_body(); -}; - -interface EmoteMessage { - BaseMessage base_message(); - string? html_body(); -}; - interface MediaSource { string url(); }; diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index 2976d9155..fbc927c2c 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -2,14 +2,32 @@ #![allow(unused_qualifications)] +macro_rules! unwrap_or_clone_arc_into_variant { + ( + $arc:ident $(, .$field:tt)?, $pat:pat => $body:expr + ) => { + #[allow(unused_variables)] + match &(*$arc)$(.$field)? { + $pat => { + #[warn(unused_variables)] + match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? { + $pat => Some($body), + _ => unreachable!(), + } + }, + _ => None, + } + }; +} + pub mod authentication_service; pub mod client; pub mod client_builder; mod helpers; -pub mod messages; pub mod room; pub mod session_verification; pub mod sliding_sync; +pub mod timeline; mod uniffi_api; use std::io; @@ -26,11 +44,14 @@ pub use uniffi_api::*; pub static RUNTIME: Lazy = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); -pub use matrix_sdk::ruma::{api::client::account::register, UserId}; +pub use matrix_sdk::{ + room::timeline::PaginationOutcome, + ruma::{api::client::account::register, UserId}, +}; pub use self::{ - authentication_service::*, client::*, messages::*, room::*, session_verification::*, - sliding_sync::*, + authentication_service::*, client::*, room::*, session_verification::*, sliding_sync::*, + timeline::*, }; #[derive(Default, Debug)] @@ -78,12 +99,14 @@ mod uniffi_types { authentication_service::{AuthenticationService, HomeserverLoginDetails}, client::Client, client_builder::ClientBuilder, - messages::AnyMessage, room::Room, session_verification::SessionVerificationEmoji, sliding_sync::{ SlidingSync, SlidingSyncBuilder, SlidingSyncRoom, SlidingSyncView, StoppableSpawn, UnreadNotificationsCount, }, + timeline::{ + EventTimelineItem, Message, TimelineItem, TimelineItemContent, VirtualTimelineItem, + }, }; } diff --git a/bindings/matrix-sdk-ffi/src/messages.rs b/bindings/matrix-sdk-ffi/src/messages.rs deleted file mode 100644 index bae2d189e..000000000 --- a/bindings/matrix-sdk-ffi/src/messages.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::sync::Arc; - -use extension_trait::extension_trait; -pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource}; -use matrix_sdk::{ - deserialized_responses::SyncTimelineEvent, - ruma::events::{ - room::{ - message::{ImageMessageEventContent, MessageFormat, MessageType}, - ImageInfo, - }, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent, - }, -}; - -#[derive(Clone)] -pub struct BaseMessage { - id: String, - body: String, - sender: String, - origin_server_ts: u64, - transaction_id: Option, -} - -impl BaseMessage { - pub fn id(&self) -> String { - self.id.clone() - } - - pub fn body(&self) -> String { - self.body.clone() - } - - pub fn sender(&self) -> String { - self.sender.clone() - } - - pub fn origin_server_ts(&self) -> u64 { - self.origin_server_ts - } - - pub fn transaction_id(&self) -> Option { - self.transaction_id.clone() - } -} - -pub struct TextMessage { - base_message: Arc, - html_body: Option, -} - -impl TextMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct ImageMessage { - base_message: Arc, - source: Arc, - info: Option>, -} - -impl ImageMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn source(&self) -> Arc { - self.source.clone() - } - - pub fn width(&self) -> Option { - self.info.clone()?.width?.try_into().ok() - } - - pub fn height(&self) -> Option { - self.info.clone()?.height?.try_into().ok() - } - - pub fn blurhash(&self) -> Option { - self.info.clone()?.blurhash - } -} - -pub struct NoticeMessage { - base_message: Arc, - html_body: Option, -} - -impl NoticeMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct EmoteMessage { - base_message: Arc, - html_body: Option, -} - -impl EmoteMessage { - pub fn base_message(&self) -> Arc { - self.base_message.clone() - } - - pub fn html_body(&self) -> Option { - self.html_body.clone() - } -} - -pub struct AnyMessage { - text: Option>, - image: Option>, - notice: Option>, - emote: Option>, -} - -impl AnyMessage { - pub fn text_message(&self) -> Option> { - self.text.clone() - } - - pub fn image_message(&self) -> Option> { - self.image.clone() - } - - pub fn notice_message(&self) -> Option> { - self.notice.clone() - } - - pub fn emote_message(&self) -> Option> { - self.emote.clone() - } -} - -pub fn sync_event_to_message(sync_event: SyncTimelineEvent) -> Option> { - match sync_event.event.deserialize() { - Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(m), - ))) => { - let base_message = Arc::new(BaseMessage { - id: m.event_id.to_string(), - body: m.content.body().to_owned(), - sender: m.sender.to_string(), - origin_server_ts: m.origin_server_ts.as_secs().into(), - transaction_id: m.unsigned.transaction_id.map(|txn_id| txn_id.to_string()), - }); - - match m.content.msgtype { - MessageType::Image(ImageMessageEventContent { source, info, .. }) => { - let any_message = AnyMessage { - text: None, - image: Some(Arc::new(ImageMessage { - base_message, - source: Arc::new(source), - info, - })), - notice: None, - emote: None, - }; - - Some(Arc::new(any_message)) - } - MessageType::Text(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: Some(Arc::new(TextMessage { base_message, html_body })), - image: None, - notice: None, - emote: None, - }; - Some(Arc::new(any_message)) - } - MessageType::Notice(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: None, - image: None, - notice: Some(Arc::new(NoticeMessage { base_message, html_body })), - emote: None, - }; - Some(Arc::new(any_message)) - } - MessageType::Emote(content) => { - let mut html_body: Option = None; - if let Some(formatted_body) = content.formatted { - if formatted_body.format == MessageFormat::Html { - html_body = Some(formatted_body.body); - } - } - - let any_message = AnyMessage { - text: None, - image: None, - notice: None, - emote: Some(Arc::new(EmoteMessage { base_message, html_body })), - }; - Some(Arc::new(any_message)) - } - _ => { - let any_message = AnyMessage { - text: Some(Arc::new(TextMessage { base_message, html_body: None })), - image: None, - notice: None, - emote: None, - }; - Some(Arc::new(any_message)) - } - } - } - _ => None, - } -} - -#[uniffi::export] -pub fn media_source_from_url(url: String) -> Arc { - Arc::new(MediaSource::Plain(url.into())) -} - -#[uniffi::export] -pub fn message_event_content_from_markdown(md: String) -> Arc { - Arc::new(RoomMessageEventContent::text_markdown(md)) -} - -#[extension_trait] -pub impl MediaSourceExt for MediaSource { - fn url(&self) -> String { - match self { - MediaSource::Plain(url) => url.to_string(), - MediaSource::Encrypted(file) => file.url.to_string(), - } - } -} diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index c418c1bc5..70b2859e9 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -1,15 +1,24 @@ -use std::{convert::TryFrom, sync::Arc}; +use std::{ + convert::TryFrom, + sync::{Arc, RwLock}, +}; use anyhow::{bail, Context, Result}; +use futures_signals::signal_vec::SignalVecExt; use matrix_sdk::{ - room::Room as MatrixRoom, + room::{ + timeline::{PaginationOutcome, Timeline}, + Room as SdkRoom, + }, ruma::{ events::room::message::{RoomMessageEvent, RoomMessageEventContent}, EventId, UserId, }, }; +use tracing::error; use super::RUNTIME; +use crate::{TimelineDiff, TimelineListener}; pub enum Membership { Invited, @@ -18,7 +27,8 @@ pub enum Membership { } pub struct Room { - room: MatrixRoom, + room: SdkRoom, + timeline: RwLock>>, } #[uniffi::export] @@ -58,11 +68,19 @@ impl Room { pub fn is_tombstoned(&self) -> bool { self.room.is_tombstoned() } + + /// Removes the timeline. + /// + /// Timeline items cached in memory as well as timeline listeners are + /// dropped. + pub fn remove_timeline(&self) { + *self.timeline.write().unwrap() = None; + } } impl Room { - pub fn new(room: MatrixRoom) -> Self { - Room { room } + pub fn new(room: SdkRoom) -> Self { + Room { room, timeline: RwLock::default() } } pub fn display_name(&self) -> Result { @@ -94,20 +112,50 @@ impl Room { pub fn membership(&self) -> Membership { match &self.room { - MatrixRoom::Invited(_) => Membership::Invited, - MatrixRoom::Joined(_) => Membership::Joined, - MatrixRoom::Left(_) => Membership::Left, + SdkRoom::Invited(_) => Membership::Invited, + SdkRoom::Joined(_) => Membership::Joined, + SdkRoom::Left(_) => Membership::Left, + } + } + + pub fn add_timeline_listener(&self, listener: Box) { + let timeline_signal = self + .timeline + .write() + .unwrap() + .get_or_insert_with(|| Arc::new(self.room.timeline())) + .signal(); + + let listener: Arc = listener.into(); + RUNTIME.spawn(timeline_signal.for_each(move |diff| { + let listener = listener.clone(); + let fut = RUNTIME + .spawn_blocking(move || listener.on_update(Arc::new(TimelineDiff::new(diff)))); + + async move { + if let Err(e) = fut.await { + error!("Timeline listener error: {e}"); + } + } + })); + } + + pub fn paginate_backwards(&self, limit: u16) -> Result { + if let Some(timeline) = &*self.timeline.read().unwrap() { + RUNTIME.block_on(async move { Ok(timeline.paginate_backwards(limit.into()).await?) }) + } else { + bail!("No timeline listeners registered, can't paginate"); } } pub fn send(&self, msg: Arc, txn_id: Option) -> Result<()> { - let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), - _ => bail!("Can't send to a room that isn't in joined state"), + let timeline = match &*self.timeline.read().unwrap() { + Some(t) => Arc::clone(t), + None => bail!("Timeline not set up, can't send message"), }; RUNTIME.block_on(async move { - room.send((*msg).to_owned(), txn_id.as_deref().map(Into::into)).await?; + timeline.send((*msg).to_owned().into(), txn_id.as_deref().map(Into::into)).await?; Ok(()) }) } @@ -119,10 +167,15 @@ impl Room { txn_id: Option, ) -> Result<()> { let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), + SdkRoom::Joined(j) => j.clone(), _ => bail!("Can't send to a room that isn't in joined state"), }; + let timeline = match &*self.timeline.read().unwrap() { + Some(t) => Arc::clone(t), + None => bail!("Timeline not set up, can't send message"), + }; + let event_id: &EventId = in_reply_to_event_id.as_str().try_into().context("Failed to create EventId.")?; @@ -140,7 +193,7 @@ impl Room { let reply_content = RoomMessageEventContent::text_markdown(msg).make_reply_to(original_message); - room.send(reply_content, txn_id.as_deref().map(Into::into)).await?; + timeline.send(reply_content.into(), txn_id.as_deref().map(Into::into)).await?; Ok(()) }) @@ -163,7 +216,7 @@ impl Room { txn_id: Option, ) -> Result<()> { let room = match &self.room { - MatrixRoom::Joined(j) => j.clone(), + SdkRoom::Joined(j) => j.clone(), _ => bail!("Can't redact in a room that isn't in joined state"), }; @@ -176,8 +229,9 @@ impl Room { } impl std::ops::Deref for Room { - type Target = MatrixRoom; - fn deref(&self) -> &MatrixRoom { + type Target = SdkRoom; + + fn deref(&self) -> &SdkRoom { &self.room } } diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 36bc48086..9bfe47a1a 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -5,6 +5,10 @@ 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, @@ -19,7 +23,9 @@ pub use matrix_sdk::{ use tokio::task::JoinHandle; use super::{Client, Room, RUNTIME}; -use crate::{helpers::unwrap_or_clone_arc, messages::AnyMessage}; +use crate::helpers::unwrap_or_clone_arc; +#[cfg(feature = "experimental-room-preview")] +use crate::EventTimelineItem; pub struct StoppableSpawn { handle: Arc>>>, @@ -115,22 +121,30 @@ impl SlidingSyncRoom { Arc::new(self.inner.unread_notifications.clone().into()) } + pub fn full_room(&self) -> Option> { + self.client.get_room(self.inner.room_id()).map(|room| Arc::new(Room::new(room))) + } +} + +#[cfg(feature = "experimental-room-preview")] +#[uniffi::export] +impl SlidingSyncRoom { #[allow(clippy::significant_drop_in_scrutinee)] - pub fn latest_room_message(&self) -> Option> { + pub fn latest_room_message(&self) -> Option> { let messages = self.inner.timeline(); // room is having the latest events at the end, let lock = messages.lock_ref(); - for m in lock.iter().rev() { - if let Some(e) = crate::messages::sync_event_to_message(m.clone().into()) { - return Some(e); + for ev in lock.iter().rev() { + if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncRoomMessageEvent::Original(o), + ))) = ev.deserialize() + { + let inner = matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.clone()); + return Some(Arc::new(EventTimelineItem(inner))); } } None } - - pub fn full_room(&self) -> Option> { - self.client.get_room(self.inner.room_id()).map(|room| Arc::new(Room::new(room))) - } } pub struct UpdateSummary { diff --git a/bindings/matrix-sdk-ffi/src/timeline.rs b/bindings/matrix-sdk-ffi/src/timeline.rs new file mode 100644 index 000000000..7179d18ff --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/timeline.rs @@ -0,0 +1,405 @@ +use std::sync::Arc; + +use extension_trait::extension_trait; +use futures_signals::signal_vec::VecDiff; +pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource}; + +#[uniffi::export] +pub fn media_source_from_url(url: String) -> Arc { + Arc::new(MediaSource::Plain(url.into())) +} + +#[uniffi::export] +pub fn message_event_content_from_markdown(md: String) -> Arc { + Arc::new(RoomMessageEventContent::text_markdown(md)) +} + +pub trait TimelineListener: Sync + Send { + fn on_update(&self, diff: Arc); +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct TimelineDiff(VecDiff>); + +impl TimelineDiff { + pub(crate) fn new(inner: VecDiff>) -> Self { + TimelineDiff(match inner { + // Note: It's _probably_ valid to only transmute here too but not + // as clear, and less important because this only happens + // once when constructing the timeline. + VecDiff::Replace { values } => VecDiff::Replace { + values: values.into_iter().map(TimelineItem::from_arc).collect(), + }, + VecDiff::InsertAt { index, value } => { + VecDiff::InsertAt { index, value: TimelineItem::from_arc(value) } + } + VecDiff::UpdateAt { index, value } => { + VecDiff::UpdateAt { index, value: TimelineItem::from_arc(value) } + } + VecDiff::RemoveAt { index } => VecDiff::RemoveAt { index }, + VecDiff::Move { old_index, new_index } => VecDiff::Move { old_index, new_index }, + VecDiff::Push { value } => VecDiff::Push { value: TimelineItem::from_arc(value) }, + VecDiff::Pop {} => VecDiff::Pop {}, + VecDiff::Clear {} => VecDiff::Clear {}, + }) + } + + pub fn change(&self) -> TimelineChange { + match &self.0 { + VecDiff::Replace { .. } => TimelineChange::Replace, + VecDiff::InsertAt { .. } => TimelineChange::InsertAt, + VecDiff::UpdateAt { .. } => TimelineChange::UpdateAt, + VecDiff::RemoveAt { .. } => TimelineChange::RemoveAt, + VecDiff::Move { .. } => TimelineChange::Move, + VecDiff::Push { .. } => TimelineChange::Push, + VecDiff::Pop {} => TimelineChange::Pop, + VecDiff::Clear {} => TimelineChange::Clear, + } + } + + pub fn replace(self: Arc) -> Option>> { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::Replace { values } => values) + } + + pub fn insert_at(self: Arc) -> Option { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::InsertAt { index, value } => { + InsertAtData { index: index.try_into().unwrap(), item: value } + }) + } + + pub fn update_at(self: Arc) -> Option { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::UpdateAt { index, value } => { + UpdateAtData { index: index.try_into().unwrap(), item: value } + }) + } + + pub fn remove_at(&self) -> Option { + match &self.0 { + VecDiff::RemoveAt { index } => Some((*index).try_into().unwrap()), + _ => None, + } + } + + pub fn r#move(&self) -> Option { + match &self.0 { + VecDiff::Move { old_index, new_index } => Some(MoveData { + old_index: (*old_index).try_into().unwrap(), + new_index: (*new_index).try_into().unwrap(), + }), + _ => None, + } + } + + pub fn push(self: Arc) -> Option> { + unwrap_or_clone_arc_into_variant!(self, .0, VecDiff::Push { value } => value) + } +} + +pub struct InsertAtData { + pub index: u32, + pub item: Arc, +} + +pub struct UpdateAtData { + pub index: u32, + pub item: Arc, +} + +pub struct MoveData { + pub old_index: u32, + pub new_index: u32, +} + +#[derive(Clone, Copy)] +pub enum TimelineChange { + Replace, + InsertAt, + UpdateAt, + RemoveAt, + Move, + Push, + Pop, + Clear, +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct TimelineItem(matrix_sdk::room::timeline::TimelineItem); + +impl TimelineItem { + fn from_arc(arc: Arc) -> Arc { + // SAFETY: This is valid because Self is a repr(transparent) wrapper + // around the other Timeline type. + unsafe { Arc::from_raw(Arc::into_raw(arc) as _) } + } +} + +#[uniffi::export] +impl TimelineItem { + pub fn as_event(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItem as Item; + unwrap_or_clone_arc_into_variant!(self, .0, Item::Event(evt) => { + Arc::new(EventTimelineItem(evt)) + }) + } + + pub fn as_virtual(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItem as Item; + unwrap_or_clone_arc_into_variant!(self, .0, Item::Virtual(vt) => { + Arc::new(VirtualTimelineItem(vt)) + }) + } +} + +pub struct EventTimelineItem(pub(crate) matrix_sdk::room::timeline::EventTimelineItem); + +impl EventTimelineItem { + pub fn key(&self) -> TimelineKey { + self.0.key().into() + } + + pub fn reactions(&self) -> Vec { + self.0 + .reactions() + .iter() + .map(|(k, v)| Reaction { key: k.to_owned(), count: v.count.into() }) + .collect() + } +} + +#[uniffi::export] +impl EventTimelineItem { + pub fn event_id(&self) -> Option { + self.0.event_id().map(ToString::to_string) + } + + pub fn sender(&self) -> String { + self.0.sender().to_string() + } + + pub fn is_own(&self) -> bool { + self.0.is_own() + } + + pub fn content(&self) -> Arc { + Arc::new(TimelineItemContent(self.0.content().clone())) + } + + pub fn origin_server_ts(&self) -> Option { + self.0.origin_server_ts().map(|ts| ts.0.into()) + } + + pub fn raw(&self) -> Option { + self.0.raw().map(|r| r.json().get().to_owned()) + } +} + +#[derive(Clone, uniffi::Object)] +pub struct TimelineItemContent(matrix_sdk::room::timeline::TimelineItemContent); + +#[uniffi::export] +impl TimelineItemContent { + pub fn as_message(self: Arc) -> Option> { + use matrix_sdk::room::timeline::TimelineItemContent as C; + unwrap_or_clone_arc_into_variant!(self, .0, C::Message(msg) => Arc::new(Message(msg))) + } + + pub fn is_redacted_message(&self) -> bool { + use matrix_sdk::room::timeline::TimelineItemContent as C; + matches!(self.0, C::RedactedMessage) + } +} + +#[derive(Clone)] +pub struct Message(matrix_sdk::room::timeline::Message); + +impl Message { + pub fn msgtype(&self) -> Option { + use matrix_sdk::ruma::events::room::message::MessageType as MTy; + match self.0.msgtype() { + MTy::Emote(c) => Some(MessageType::Emote { + content: EmoteMessageContent { + body: c.body.clone(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + MTy::Image(c) => Some(MessageType::Image { + content: ImageMessageContent { + 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(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + MTy::Text(c) => Some(MessageType::Text { + content: TextMessageContent { + body: c.body.clone(), + formatted: c.formatted.as_ref().map(Into::into), + }, + }), + _ => None, + } + } +} + +#[uniffi::export] +impl Message { + pub fn body(&self) -> String { + self.0.msgtype().body().to_owned() + } + + // This event ID string will be replaced by something more useful later. + pub fn in_reply_to(&self) -> Option { + self.0.in_reply_to().map(ToString::to_string) + } + + pub fn is_edited(&self) -> bool { + self.0.is_edited() + } +} + +#[derive(Clone)] +pub enum MessageType { + Emote { content: EmoteMessageContent }, + Image { content: ImageMessageContent }, + Notice { content: NoticeMessageContent }, + Text { content: TextMessageContent }, +} + +#[derive(Clone)] +pub struct EmoteMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct ImageMessageContent { + pub body: String, + pub source: Arc, + pub info: Option, +} + +#[derive(Clone)] +pub struct ImageInfo { + pub height: Option, + pub width: Option, + pub mimetype: Option, + pub size: Option, + pub thumbnail_info: Option, + pub thumbnail_source: Option>, + pub blurhash: Option, +} + +#[derive(Clone)] +pub struct ThumbnailInfo { + pub height: Option, + pub width: Option, + pub mimetype: Option, + pub size: Option, +} + +#[derive(Clone)] +pub struct NoticeMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct TextMessageContent { + pub body: String, + pub formatted: Option, +} + +#[derive(Clone)] +pub struct FormattedBody { + pub format: MessageFormat, + pub body: String, +} + +impl From<&matrix_sdk::ruma::events::room::message::FormattedBody> for FormattedBody { + fn from(f: &matrix_sdk::ruma::events::room::message::FormattedBody) -> Self { + Self { + format: match &f.format { + matrix_sdk::ruma::events::room::message::MessageFormat::Html => MessageFormat::Html, + _ => MessageFormat::Unknown, + }, + body: f.body.clone(), + } + } +} + +#[derive(Clone, Copy)] +pub enum MessageFormat { + Html, + Unknown, +} + +impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo { + fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> 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 { + height: info.height.map(Into::into), + width: info.width.map(Into::into), + mimetype: info.mimetype.clone(), + size: info.size.map(Into::into), + thumbnail_info, + thumbnail_source: info.thumbnail_source.clone().map(Arc::new), + blurhash: info.blurhash.clone(), + } + } +} + +#[derive(Clone)] +pub struct Reaction { + pub key: String, + pub count: u64, + // TODO: Also expose senders +} + +#[derive(Clone)] +pub struct ReactionDetails { + pub id: TimelineKey, + pub sender: String, +} + +#[derive(Clone)] +pub enum TimelineKey { + TransactionId { txn_id: String }, + EventId { event_id: String }, +} + +impl From<&matrix_sdk::room::timeline::TimelineKey> for TimelineKey { + fn from(timeline_key: &matrix_sdk::room::timeline::TimelineKey) -> Self { + use matrix_sdk::room::timeline::TimelineKey::*; + + match timeline_key { + TransactionId(txn_id) => TimelineKey::TransactionId { txn_id: txn_id.to_string() }, + EventId(event_id) => TimelineKey::EventId { event_id: event_id.to_string() }, + } + } +} + +#[derive(Clone)] +pub struct VirtualTimelineItem(matrix_sdk::room::timeline::VirtualTimelineItem); + +#[extension_trait] +pub impl MediaSourceExt for MediaSource { + fn url(&self) -> String { + match self { + MediaSource::Plain(url) => url.to_string(), + MediaSource::Encrypted(file) => file.url.to_string(), + } + } +} diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index e6bf6317f..313688c7a 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -42,6 +42,7 @@ appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] +experimental-room-preview = [] experimental-timeline = [] sliding-sync = [ diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs index 9300195f3..c532aa7de 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -14,6 +14,8 @@ 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}, @@ -62,6 +64,37 @@ 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) -> 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 @@ -312,7 +345,8 @@ impl Default for ReactionDetails { /// The result of a successful pagination request. #[derive(Debug)] -#[non_exhaustive] +// TODO: non-exhaustive breaks UniFFI bridge +//#[non_exhaustive] pub struct PaginationOutcome { /// Whether there's more messages to be paginated. pub more_messages: bool, From 71257309175f52c9e44c8fe1d27d191ec87be7b1 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:12:43 +0100 Subject: [PATCH 18/66] ci(xtask): Switch to xtask for swift; run swift tasks on macOS Merge pull request #1023 from matrix-org/doug/swift-linux: Run Swift tests on macOS --- .github/workflows/bindings_ci.yml | 16 ++---- bindings/apple/Package.swift | 18 +++++-- bindings/apple/README.md | 10 ++-- xtask/src/ci.rs | 18 ++----- xtask/src/fixup.rs | 18 +------ xtask/src/main.rs | 5 ++ xtask/src/swift.rs | 83 +++++++++++++++++++++++++++++++ xtask/src/workspace.rs | 17 +++++++ 8 files changed, 129 insertions(+), 56 deletions(-) create mode 100644 xtask/src/swift.rs create mode 100644 xtask/src/workspace.rs diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index c97ea1bc5..61951fb12 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -149,11 +149,6 @@ jobs: profile: minimal override: true - - name: Install targets - run: | - rustup target add aarch64-apple-ios-sim --toolchain nightly - rustup target add x86_64-apple-ios --toolchain nightly - - name: Load cache uses: Swatinem/rust-cache@v1 @@ -164,14 +159,9 @@ jobs: # keep in sync with uniffi dependency in Cargo.toml's args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev 0eee77f67b716c4896494606e5931d249871b23a - - name: Generate .xcframework - working-directory: bindings/apple - run: sh ./debug_build_xcframework.sh x86_64 ci + - name: Build library & bindings + run: cargo xtask swift build-library - name: Run XCTests working-directory: bindings/apple - run: | - xcodebuild test \ - -scheme MatrixRustSDK \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 13' + run: swift test diff --git a/bindings/apple/Package.swift b/bindings/apple/Package.swift index 13b9a0fa4..05f4003ab 100644 --- a/bindings/apple/Package.swift +++ b/bindings/apple/Package.swift @@ -5,17 +5,25 @@ import PackageDescription let package = Package( name: "MatrixRustSDK", - platforms: [.iOS(.v15)], products: [ .library(name: "MatrixRustSDK", targets: ["MatrixRustSDK"]), ], targets: [ - .binaryTarget(name: "MatrixSDKFFI", path: "generated/MatrixSDKFFI.xcframework"), .target(name: "MatrixRustSDK", - dependencies: [.target(name: "MatrixSDKFFI")], - path: "generated/swift"), + path: "generated/swift", + swiftSettings: [ + .unsafeFlags(["-I", "./generated/matrix_sdk_ffi"]) + ]), .testTarget(name: "MatrixRustSDKTests", - dependencies: ["MatrixRustSDK"]), + dependencies: ["MatrixRustSDK"], + swiftSettings: [ + .unsafeFlags(["-I", "./generated/matrix_sdk_ffi"]) + ], + linkerSettings: [ + .linkedLibrary("matrix_sdk_ffi", .when(platforms: [.macOS])), + .linkedLibrary("matrix_sdk_ffiFFI", .when(platforms: [.linux])), + .unsafeFlags(["-L./generated/matrix_sdk_ffi"]) + ]) ] ) diff --git a/bindings/apple/README.md b/bindings/apple/README.md index 14b698939..9027cb77d 100644 --- a/bindings/apple/README.md +++ b/bindings/apple/README.md @@ -38,15 +38,11 @@ The `build_crypto_xcframework.sh` script will go through all the steps required 4. `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKCryptoFFI.xcframework` 5. cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework) -## Running the Xcode project +## Building & testing the Swift package -The Xcode project is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from. +`Package.swift` is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from. -It's pre-configured to link to the generated .xcframework and .swift files so successfully running the script first is necessary for it to compile. - -It makes the compiled code available to swift by importing the C header through its bridging header. - -Once all the generated components are available running it should be as easy as choosing a platform and clicking run. +It's pre-configured to link to the generated static lib and .swift files so successfully running `cargo xtask swift build-library` first is necessary for it to compile. Afterwards you can execute the tests with `swift test`. Note that for the moment this only works on macOS but we're planning to add Linux support in the future. ## Distribution diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 2ec6e24b4..d7911f1bc 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -1,10 +1,9 @@ -use std::{collections::BTreeMap, env, path::PathBuf}; +use std::collections::BTreeMap; use clap::{Args, Subcommand}; -use serde::Deserialize; use xshell::{cmd, pushd}; -use crate::{build_docs, DenyWarnings, Result}; +use crate::{build_docs, workspace, DenyWarnings, Result}; #[derive(Args)] pub struct CiArgs { @@ -79,7 +78,7 @@ enum WasmFeatureSet { impl CiArgs { pub fn run(self) -> Result<()> { - let _p = pushd(&workspace_root()?)?; + let _p = pushd(&workspace::root_path()?)?; match self.cmd { Some(cmd) => match cmd { @@ -362,14 +361,3 @@ fn run_wasm_pack_tests(cmd: Option) -> Result<()> { Ok(()) } - -fn workspace_root() -> Result { - #[derive(Deserialize)] - struct Metadata { - workspace_root: PathBuf, - } - - let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; - Ok(serde_json::from_str::(&metadata_json)?.workspace_root) -} diff --git a/xtask/src/fixup.rs b/xtask/src/fixup.rs index 532bba9f1..9788c03d2 100644 --- a/xtask/src/fixup.rs +++ b/xtask/src/fixup.rs @@ -1,10 +1,7 @@ -use std::{env, path::PathBuf}; - use clap::{Args, Subcommand}; -use serde::Deserialize; use xshell::{cmd, pushd}; -use crate::Result; +use crate::{workspace, Result}; #[derive(Args)] pub struct FixupArgs { @@ -24,7 +21,7 @@ enum FixupCommand { impl FixupArgs { pub fn run(self) -> Result<()> { - let _p = pushd(&workspace_root()?)?; + let _p = pushd(&workspace::root_path()?)?; match self.cmd { Some(cmd) => match cmd { @@ -78,14 +75,3 @@ fn fix_clippy() -> Result<()> { .run()?; Ok(()) } - -fn workspace_root() -> Result { - #[derive(Deserialize)] - struct Metadata { - workspace_root: PathBuf, - } - - let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); - let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; - Ok(serde_json::from_str::(&metadata_json)?.workspace_root) -} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 2f58419f5..15cfd9443 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,9 +1,12 @@ mod ci; mod fixup; +mod swift; +mod workspace; use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; +use swift::SwiftArgs; use xshell::cmd; type Result> = std::result::Result; @@ -26,6 +29,7 @@ enum Command { #[clap(long)] open: bool, }, + Swift(SwiftArgs), } fn main() -> Result<()> { @@ -33,6 +37,7 @@ fn main() -> Result<()> { Command::Ci(ci) => ci.run(), Command::Fixup(cfg) => cfg.run(), Command::Doc { open } => build_docs(open.then_some("--open"), DenyWarnings::No), + Command::Swift(cfg) => cfg.run(), } } diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs new file mode 100644 index 000000000..1c3eb3829 --- /dev/null +++ b/xtask/src/swift.rs @@ -0,0 +1,83 @@ +use std::fs; + +use clap::{Args, Subcommand}; +use xshell::{cmd, pushd}; + +use crate::{workspace, Result}; + +/// Builds the SDK for Swift as a Static Library or XCFramework. +#[derive(Args)] +pub struct SwiftArgs { + #[clap(subcommand)] + cmd: SwiftCommand, +} + +#[derive(Subcommand)] +enum SwiftCommand { + /// Builds the SDK for Swift as a static lib. + BuildLibrary, + /// Builds the SDK for Swift as an XCFramework. + BuildFramework, +} + +impl SwiftArgs { + pub fn run(self) -> Result<()> { + let _p = pushd(&workspace::root_path()?)?; + + match self.cmd { + SwiftCommand::BuildLibrary => build_library(), + SwiftCommand::BuildFramework => build_xcframework(), + } + } +} + +fn build_library() -> Result<()> { + println!("Running debug library build."); + + let root_directory = workspace::root_path()?; + let target_directory = root_directory.join("target"); + let ffi_directory = root_directory.join("bindings/apple/generated/matrix_sdk_ffi"); + let swift_directory = root_directory.join("bindings/apple/generated/swift"); + + let release_type = "debug"; + let static_lib_filename = "libmatrix_sdk_ffi.a"; + + fs::create_dir_all(ffi_directory.as_path())?; + fs::create_dir_all(swift_directory.as_path())?; + + cmd!("cargo build -p matrix-sdk-ffi").run()?; + + fs::rename( + target_directory.join(release_type).join(static_lib_filename), + ffi_directory.join(static_lib_filename), + )?; + + cmd!( + "uniffi-bindgen generate + --language swift + --lib-file {ffi_directory}/{static_lib_filename} + --out-dir {ffi_directory} + {root_directory}/bindings/matrix-sdk-ffi/src/api.udl" + ) + .run()?; + + let module_map_file = ffi_directory.join("module.modulemap"); + if module_map_file.exists() { + fs::remove_file(module_map_file.as_path())?; + } + + // TODO: Find the modulemap in the ffi directory. + fs::rename(ffi_directory.join("matrix_sdk_ffiFFI.modulemap"), module_map_file)?; + // TODO: Move all swift files. + fs::rename( + ffi_directory.join("matrix_sdk_ffi.swift"), + swift_directory.join("matrix_sdk_ffi.swift"), + )?; + + Ok(()) +} + +fn build_xcframework() -> Result<()> { + println!("XCFramework not yet implemented."); + Ok(()) +} diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs new file mode 100644 index 000000000..c97ffd5e6 --- /dev/null +++ b/xtask/src/workspace.rs @@ -0,0 +1,17 @@ +use std::{env, path::PathBuf}; + +use serde::Deserialize; +use xshell::cmd; + +use crate::Result; + +pub fn root_path() -> Result { + #[derive(Deserialize)] + struct Metadata { + workspace_root: PathBuf, + } + + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); + let metadata_json = cmd!("{cargo} metadata --no-deps --format-version 1").read()?; + Ok(serde_json::from_str::(&metadata_json)?.workspace_root) +} From dd82e0b8fcee9de8c4f2b55657482bd7639f67c5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:24:09 +0100 Subject: [PATCH 19/66] chore: Update bindings/README --- bindings/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/README.md b/bindings/README.md index ff55b5573..b4c07977e 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -5,18 +5,18 @@ maintained by the owners of the Matrix Rust SDK project. * [`apple`] or `matrix-rust-components-swift`, Swift bindings of the [`matrix-sdk`] crate via [`matrix-sdk-ffi`], -* [`matrix-sdk-crypto-ffi`], bindings of the [`matrix-sdk-crypto`] +* [`matrix-sdk-crypto-ffi`], UniFFI (Kotlin, Swift, Python, Ruby) bindings of the [`matrix-sdk-crypto`] crate, * [`matrix-sdk-crypto-js`], JavaScript bindings of the [`matrix-sdk-crypto`] crate, * [`matrix-sdk-crypto-nodejs`], Node.js bindings of the [`matrix-sdk-crypto`] crate, -* [`matrix-sdk-ffi`], bindings of the [`matrix-sdk`] crate, +* [`matrix-sdk-ffi`], UniFFI bindings of the [`matrix-sdk`] crate. [`apple`]: ./apple [`matrix-sdk-crypto-ffi`]: ./matrix-sdk-crypto-ffi -[`matrix-sdk-crypto-js`]: ../crates/matrix-sdk-crypto -[`matrix-sdk-crypto-nodejs`]: ../crates/matrix-sdk-crypto +[`matrix-sdk-crypto-js`]: ./matrix-sdk-crypto-js +[`matrix-sdk-crypto-nodejs`]: ./matrix-sdk-crypto-nodejs [`matrix-sdk-crypto`]: ../crates/matrix-sdk-crypto [`matrix-sdk-ffi`]: ./matrix-sdk-ffi [`matrix-sdk`]: ../crates/matrix-sdk From ab796cb32c32833acabad890497a99194ec70b1f Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 17 Oct 2022 15:31:41 +0200 Subject: [PATCH 20/66] ci(xtask): switch to using uniffi as a lib rather than globally installed binary --- xtask/Cargo.toml | 2 ++ xtask/src/swift.rs | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 20159e73e..019969f1c 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -13,3 +13,5 @@ clap = { version = "3.2.4", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" xshell = "0.1.17" +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } +camino = "1.0.8" diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs index 1c3eb3829..269afaacc 100644 --- a/xtask/src/swift.rs +++ b/xtask/src/swift.rs @@ -34,13 +34,21 @@ impl SwiftArgs { fn build_library() -> Result<()> { println!("Running debug library build."); + let release_type = "debug"; + let static_lib_filename = "libmatrix_sdk_ffi.a"; + let root_directory = workspace::root_path()?; let target_directory = root_directory.join("target"); let ffi_directory = root_directory.join("bindings/apple/generated/matrix_sdk_ffi"); let swift_directory = root_directory.join("bindings/apple/generated/swift"); - - let release_type = "debug"; - let static_lib_filename = "libmatrix_sdk_ffi.a"; + let udl_file = camino::Utf8PathBuf::from_path_buf( + root_directory.join("bindings/matrix-sdk-ffi/src/api.udl"), + ) + .expect("Root Dir contains non-utf8 characters"); + let outdir_overwrite = camino::Utf8PathBuf::from_path_buf(ffi_directory.clone()) + .expect("Root Dir contains non-utf8 characters"); + let library_file = camino::Utf8PathBuf::from_path_buf(ffi_directory.join(static_lib_filename)) + .expect("Root Dir contains non-utf8 characters"); fs::create_dir_all(ffi_directory.as_path())?; fs::create_dir_all(swift_directory.as_path())?; @@ -52,14 +60,14 @@ fn build_library() -> Result<()> { ffi_directory.join(static_lib_filename), )?; - cmd!( - "uniffi-bindgen generate - --language swift - --lib-file {ffi_directory}/{static_lib_filename} - --out-dir {ffi_directory} - {root_directory}/bindings/matrix-sdk-ffi/src/api.udl" - ) - .run()?; + uniffi_bindgen::generate_bindings( + udl_file.as_path(), + None, + vec!["swift"], + Some(outdir_overwrite.as_path()), + Some(library_file.as_path()), + false, + )?; let module_map_file = ffi_directory.join("module.modulemap"); if module_map_file.exists() { From 5e621b71328e8f9c0bb4c600abac193ba72bb743 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Mon, 17 Oct 2022 15:33:13 +0200 Subject: [PATCH 21/66] fix!: Switch to uniffi 0.21.0 and workspace-wide dependencies Upgrade MSRV to 1.64, the first stable release to support workspace-wide depenendencies: https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html#cargo-improvements-workspace-inheritance-and-multi-target-builds --- Cargo.lock | 30 ++++++++++++++--------- Cargo.toml | 6 +++++ README.md | 2 +- bindings/matrix-sdk-crypto-ffi/Cargo.toml | 6 ++--- bindings/matrix-sdk-ffi/Cargo.toml | 7 +++--- xtask/Cargo.toml | 4 +-- 6 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df175b797..da4ace940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4859,8 +4859,9 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uniffi" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54af5ada67d1173457a99a7bb44a7917f63e7466764cb4714865c7a6678b830" dependencies = [ "anyhow", "bytes", @@ -4875,8 +4876,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc4af3c0180c7e86c4a3acf2b6587af04ba2567b1e948993df10f421796621" dependencies = [ "anyhow", "askama", @@ -4897,8 +4899,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510287c368a9386eb731ebe824a6fc6c82a105e20d020af1aa20519c1c1561db" dependencies = [ "anyhow", "camino", @@ -4907,8 +4910,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8604503caa2cbcf271578dc51ca236d40e3b22e1514ffa2e638e2c39f6ad10" dependencies = [ "bincode", "camino", @@ -4925,8 +4929,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.20.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9417cc653937681436b93838d8c5f97a5b8c58697813982ee8810bd1dc3b57" dependencies = [ "serde", ] @@ -5236,7 +5241,8 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "git+https://github.com/mozilla/uniffi-rs?rev=0eee77f67b716c4896494606e5931d249871b23a#0eee77f67b716c4896494606e5931d249871b23a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" dependencies = [ "nom", ] @@ -5389,9 +5395,11 @@ checksum = "4916a4a3cad759e499a3620523bf9545cc162d7a06163727dde97ce9aaa4cf39" name = "xtask" version = "0.1.0" dependencies = [ + "camino", "clap 3.2.22", "serde", "serde_json", + "uniffi_bindgen", "xshell", ] diff --git a/Cargo.toml b/Cargo.toml index b5e57c990..5a0c93675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,12 @@ members = [ default-members = ["benchmarks", "crates/*"] resolver = "2" +[workspace.dependencies] +uniffi = "0.21.0" +uniffi_macros = "0.21.0" +uniffi_bindgen = "0.21.0" +uniffi_build = { version = "0.21.0", features = ["builtin-bindgen"] } + [profile.release] lto = true diff --git a/README.md b/README.md index 0fb8473f9..22cb464dd 100644 --- a/README.md +++ b/README.md @@ -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.62` +These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.64` ## Status diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index f0075e5eb..14bef41e3 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -27,8 +27,8 @@ thiserror = "1.0.30" tracing = "0.1.34" tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } # keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job -uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } +uniffi = { workspace = true } +uniffi_macros = { workspace = true } zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [dependencies.js_int] @@ -59,7 +59,7 @@ features = ["rt-multi-thread"] version = "0.3.0" [build-dependencies] -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a", features = ["builtin-bindgen"] } +uniffi_build = { workspace = true, features = ["builtin-bindgen"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 6ef7dc7f4..1bf815558 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -17,7 +17,7 @@ default = ["experimental-room-preview"] # the whole crate is still very experime experimental-room-preview = ["matrix-sdk/experimental-room-preview"] [build-dependencies] -uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a", features = ["builtin-bindgen"] } +uniffi_build = { workspace = true, features = ["builtin-bindgen"] } [dependencies] anyhow = "1.0.51" @@ -37,6 +37,5 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio-stream = "0.1.8" tracing = "0.1.32" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -# keep in sync with uniffi dependency in matrix-sdk-crypto-ffi, and uniffi_bindgen in ffi CI job -uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } +uniffi = { workspace = true } +uniffi_macros = { workspace = true } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 019969f1c..013ba4161 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,9 +9,9 @@ name = "xtask" test = false [dependencies] +camino = "1.0.8" clap = { version = "3.2.4", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" +uniffi_bindgen = { workspace = true } xshell = "0.1.17" -uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "0eee77f67b716c4896494606e5931d249871b23a" } -camino = "1.0.8" From 4e583bcb8afb7c5ed3517e27e0b18d1153e53549 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 17 Oct 2022 15:04:01 +0200 Subject: [PATCH 22/66] chore: Exclude xtask from tarpaulin coverage collection --- tarpaulin.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tarpaulin.toml b/tarpaulin.toml index 7733409f1..6a74337d3 100644 --- a/tarpaulin.toml +++ b/tarpaulin.toml @@ -21,4 +21,6 @@ exclude = [ # labs "jack-in", "sled-state-inspector", + # repo automation (ci, codegen) + "xtask", ] From 391efcec1229c41d478a82ae63ab0b62e5f3ceb3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 17 Oct 2022 17:03:37 +0200 Subject: [PATCH 23/66] feat(appservice): Return a concrete type from AppService::service --- Cargo.lock | 4 +- crates/matrix-sdk-appservice/src/lib.rs | 41 ++------- crates/matrix-sdk-appservice/src/webserver.rs | 91 +++++++++++++++---- 3 files changed, 81 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da4ace940..1bb895bd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4659,9 +4659,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" diff --git a/crates/matrix-sdk-appservice/src/lib.rs b/crates/matrix-sdk-appservice/src/lib.rs index ed5611355..cde90fb70 100644 --- a/crates/matrix-sdk-appservice/src/lib.rs +++ b/crates/matrix-sdk-appservice/src/lib.rs @@ -79,13 +79,12 @@ //! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228 //! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples -use std::{convert::Infallible, fmt::Debug, future::Future, sync::Arc}; +use std::{fmt::Debug, sync::Arc}; -use axum::body::{Bytes, HttpBody}; +use axum::body::HttpBody; use dashmap::DashMap; pub use error::Error; use event_handler::AppserviceFn; -use http_body::combinators::UnsyncBoxBody; pub use matrix_sdk; #[doc(no_inline)] pub use matrix_sdk::ruma; @@ -105,7 +104,6 @@ use ruma::{ use serde::Deserialize; use thiserror::Error; use tokio::task::JoinHandle; -use tower::Service; use tracing::{debug, info, warn}; mod error; @@ -117,6 +115,7 @@ mod webserver; pub use registration::AppServiceRegistration; use registration::NamespaceCache; pub use virtual_user::VirtualUserBuilder; +pub use webserver::AppServiceRouter; pub type Result = std::result::Result; @@ -412,13 +411,9 @@ impl AppService { self.namespaces.users.iter().any(|regex| regex.is_match(user_id)) } - /// Returns a [`Service`] that processes appservice requests. - pub fn service( - &self, - // axum::Error is part of the signature because axum::Router::nest - // requires the inner service to have that exact response (body) type - // in 0.5.x. This will be fixed in axum 0.6.0. - ) -> impl HttpService> + Clone + /// Returns a [`Service`][tower::Service] that processes appservice + /// requests. + pub fn service(&self) -> AppServiceRouter where B: HttpBody + Send + 'static, B::Data: Send, @@ -563,28 +558,6 @@ impl AppService { } } -#[rustfmt::skip] -pub trait HttpService: - Service< - http::Request, - Response = http::Response, - Error = Infallible, - Future = >::Future, - > -{ - type Future: Future> + Send; - type ResBody; -} - -impl HttpService for S -where - S: Service, Response = http::Response, Error = Infallible>, - >>::Future: Send, -{ - type Future = >>::Future; - type ResBody = ResBody; -} - #[cfg(test)] mod tests { use std::{ @@ -607,7 +580,7 @@ mod tests { serde::Raw, }; use serde_json::json; - use tower::ServiceExt; + use tower::{Service, ServiceExt}; use wiremock::{ matchers::{body_json, header, method, path}, Mock, MockServer, ResponseTemplate, diff --git a/crates/matrix-sdk-appservice/src/webserver.rs b/crates/matrix-sdk-appservice/src/webserver.rs index ed18a6c16..a021745fd 100644 --- a/crates/matrix-sdk-appservice/src/webserver.rs +++ b/crates/matrix-sdk-appservice/src/webserver.rs @@ -12,7 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::net::ToSocketAddrs; +use std::{ + convert::Infallible, + future::Future, + net::ToSocketAddrs, + pin::Pin, + task::{self, Poll}, +}; use axum::{ async_trait, @@ -20,13 +26,14 @@ use axum::{ extract::{FromRequest, Path, RequestParts}, middleware::{self, Next}, response::{ErrorResponse, IntoResponse, Response}, - routing::{get, put}, + routing::{future::RouteFuture, get, put}, BoxError, Extension, Json, Router, }; use http::StatusCode; +use hyper::Body; use matrix_sdk::ruma::{self, api::IncomingRequest}; use serde::{Deserialize, Serialize}; -use tower::ServiceBuilder; +use tower::{make, Service, ServiceBuilder}; use crate::{AppService, Error, Result}; @@ -35,36 +42,82 @@ pub async fn run_server( host: impl Into, port: impl Into, ) -> Result<()> { - let router: Router = router(appservice); + let router: AppServiceRouter = router(appservice); let mut addr = (host.into(), port.into()).to_socket_addrs()?; if let Some(addr) = addr.next() { - hyper::Server::bind(&addr).serve(router.into_make_service()).await?; + hyper::Server::bind(&addr).serve(make::Shared::new(router)).await?; Ok(()) } else { Err(Error::HostPortToSocketAddrs) } } -pub fn router(appservice: AppService) -> Router +pub fn router(appservice: AppService) -> AppServiceRouter where B: HttpBody + Send + 'static, B::Data: Send, B::Error: Into, { - Router::new() - .route("/_matrix/app/v1/users/:user_id", get(handlers::user)) - .route("/_matrix/app/v1/rooms/:room_id", get(handlers::room)) - .route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction)) - .route("/users/:user_id", get(handlers::user)) - .route("/rooms/:room_id", get(handlers::room)) - .route("/transactions/:txn_id", put(handlers::transaction)) - // FIXME: Use Route::with_state instead of an Extension layer in axum 0.6 - .layer( - ServiceBuilder::new() - .layer(Extension(appservice)) - .layer(middleware::from_fn(validate_access_token)), - ) + AppServiceRouter( + Router::new() + .route("/_matrix/app/v1/users/:user_id", get(handlers::user)) + .route("/_matrix/app/v1/rooms/:room_id", get(handlers::room)) + .route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction)) + .route("/users/:user_id", get(handlers::user)) + .route("/rooms/:room_id", get(handlers::room)) + .route("/transactions/:txn_id", put(handlers::transaction)) + // FIXME: Use Route::with_state instead of an Extension layer in axum 0.6 + .layer( + ServiceBuilder::new() + .layer(Extension(appservice)) + .layer(middleware::from_fn(validate_access_token)), + ), + ) +} + +#[derive(Debug)] +pub struct AppServiceRouter(Router); + +impl Clone for AppServiceRouter { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Service> for AppServiceRouter +where + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + // axum's Response type is part of the signature because axum::Router::nest + // requires the inner service to have that exact response (body) type in + // 0.5.x; this will be fixed in axum 0.6.0. + type Response = Response; + type Error = Infallible; + type Future = AppServiceRouteFuture; + + fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll> { + self.0.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + AppServiceRouteFuture(self.0.call(req)) + } +} + +pub struct AppServiceRouteFuture(RouteFuture); + +impl Future for AppServiceRouteFuture +where + B: HttpBody, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx) + } } pub struct MatrixRequest(T); From b309441dec5c590590306be9cd815cd22ee9d71c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 17 Oct 2022 18:35:55 +0200 Subject: [PATCH 24/66] fix(sdk): Don't log the access token in HttpClient::send --- crates/matrix-sdk/src/http_client.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/http_client.rs b/crates/matrix-sdk/src/http_client.rs index a4cd45a47..fd0ce54fb 100644 --- a/crates/matrix-sdk/src/http_client.rs +++ b/crates/matrix-sdk/src/http_client.rs @@ -105,7 +105,10 @@ impl HttpClient { HttpClient { inner, request_config } } - #[tracing::instrument(skip(self, request), fields(request_type = type_name::()))] + #[tracing::instrument( + skip(self, request, access_token), + fields(request_type = type_name::()), + )] pub async fn send( &self, request: Request, From 5b46fa73e177aac95d0b61c978feb9fd96415456 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 10:32:48 +0200 Subject: [PATCH 25/66] fix(sdk): Always send an access token for `get_display_name` --- crates/matrix-sdk/src/account.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index 8eb42b8cb..af5a89bbc 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -79,7 +79,8 @@ impl Account { pub async fn get_display_name(&self) -> Result> { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = get_display_name::v3::Request::new(user_id); - let response = self.client.send(request, None).await?; + let request_config = self.client.request_config().force_auth(); + let response = self.client.send(request, Some(request_config)).await?; Ok(response.displayname) } From e91cee7154690a273d9754c951458991f8812774 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 11:35:10 +0200 Subject: [PATCH 26/66] fix(sdk): Always send an access token for `get_profile` --- crates/matrix-sdk/src/account.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs index af5a89bbc..897366b05 100644 --- a/crates/matrix-sdk/src/account.rs +++ b/crates/matrix-sdk/src/account.rs @@ -240,7 +240,8 @@ impl Account { pub async fn get_profile(&self) -> Result { let user_id = self.client.user_id().ok_or(Error::AuthenticationRequired)?; let request = get_profile::v3::Request::new(user_id); - Ok(self.client.send(request, None).await?) + let request_config = self.client.request_config().force_auth(); + Ok(self.client.send(request, Some(request_config)).await?) } /// Change the password of the account. From f57b7782f45f0c8802341e650d910e091e79317c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 12:02:05 +0200 Subject: [PATCH 27/66] refactor: Use workspace dependencies for ruma --- Cargo.toml | 1 + benchmarks/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-ffi/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-nodejs/Cargo.toml | 2 +- crates/matrix-sdk-appservice/Cargo.toml | 2 +- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-common/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- crates/matrix-sdk-sled/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 7 ++----- labs/sled-state-inspector/Cargo.toml | 2 +- testing/matrix-sdk-test/Cargo.toml | 2 +- 14 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a0c93675..7a183e824 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ default-members = ["benchmarks", "crates/*"] resolver = "2" [workspace.dependencies] +ruma = { version = "0.7.4", features = ["client-api-c"] } uniffi = "0.21.0" uniffi_macros = "0.21.0" uniffi_bindgen = "0.21.0" diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index f5ebcb688..f9280879a 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -12,7 +12,7 @@ criterion = { version = "0.3.5", features = ["async", "async_tokio", "html_repor matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.6.0"} matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.2.0", default-features = false, features = ["crypto-store"] } matrix-sdk-test = { path = "../testing/matrix-sdk-test", version = "0.6.0"} -ruma = "0.7.0" +ruma = { workspace = true } serde_json = "1.0.79" tempfile = "3.3.0" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 14bef41e3..a68928977 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -19,7 +19,7 @@ hmac = "0.12.1" http = "0.2.6" pbkdf2 = "0.11.0" rand = "0.8.5" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10.2" diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 718c6df90..5b0577e67 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -31,7 +31,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 = { version = "0.7.0", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] } +ruma = { workspace = true, features = ["js", "rand", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { version = "0.3.0", features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index c5779d622..a741c6adc 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -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 = { version = "0.7.0", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } +ruma = { workspace = true, features = ["rand", "unstable-msc2676", "unstable-msc2677"] } napi = { version = "2.9.1", default-features = false, features = ["napi6", "tokio_rt"] } napi-derive = "2.9.1" serde_json = "1.0.79" diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index 8588a5f39..3301bab5b 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -37,7 +37,7 @@ http-body = "0.4.5" hyper = { version = "0.14.20", features = ["http1", "http2", "server"] } matrix-sdk = { version = "0.6.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] } regex = "1.5.5" -ruma = { version = "0.7.0", features = ["client-api-c", "appservice-api-s"] } +ruma = { workspace = true, features = ["appservice-api-s"] } serde = "1.0.136" serde_json = "1.0.79" serde_yaml = "0.9.4" diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 9b1f101db..ad111384a 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -38,7 +38,7 @@ lru = "0.8.0" matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } once_cell = "1.10.0" -ruma = { version = "0.7.0", features = ["client-api-c", "canonical-json"] } +ruma = { workspace = true, features = ["canonical-json"] } serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.79" thiserror = "1.0.30" diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 95b710a84..678a2e110 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -21,7 +21,7 @@ js = ["instant/wasm-bindgen", "instant/inaccurate", "wasm-bindgen-futures"] [dependencies] futures-core = "0.3.21" instant = "0.1.12" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index ed4530c73..0eff090cb 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -42,7 +42,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 = { version = "0.7.0", features = ["client-api-c", "rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } +ruma = { workspace = true, features = ["rand", "canonical-json", "unstable-msc2676", "unstable-msc2677"] } serde = { version = "1.0.136", features = ["derive", "rc"] } serde_json = "1.0.79" sha2 = "0.10.2" diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index b12a39be8..0d2ba8aef 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -30,7 +30,7 @@ matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", features matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } indexed_db_futures = "0.2.3" indexed_db_futures_nodejs = { version = "0.2.3", package = "indexed_db_futures", git = "https://github.com/Hywan/rust-indexed-db", branch = "feat-factory-nodejs", optional = true } -ruma = "0.7.0" +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index 46031db3c..3a4c82fbb 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -35,7 +35,7 @@ matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", optional = t matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } -ruma = "0.7.0" +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" sled = "0.34.7" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 313688c7a..e06622b95 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -81,7 +81,8 @@ matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", de matrix-sdk-sled = { version = "0.2.0", path = "../matrix-sdk-sled", default-features = false, optional = true } mime = "0.3.16" rand = { version = "0.8.5", optional = true } -reqwest = { version = "0.11.10", default_features = false} +reqwest = { version = "0.11.10", default_features = false } +ruma = { workspace = true, features = ["compat", "rand", "unstable-msc2448", "unstable-msc2965"] } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" @@ -111,10 +112,6 @@ features = [ ] optional = true -[dependencies.ruma] -version = "0.7.4" -features = ["client-api-c", "compat", "rand", "unstable-msc2448", "unstable-msc2965"] - [target.'cfg(target_arch = "wasm32")'.dependencies] async-once-cell = "0.4.2" wasm-timer = "0.2.5" diff --git a/labs/sled-state-inspector/Cargo.toml b/labs/sled-state-inspector/Cargo.toml index ce5201284..418d14bd8 100644 --- a/labs/sled-state-inspector/Cargo.toml +++ b/labs/sled-state-inspector/Cargo.toml @@ -14,7 +14,7 @@ clap = "3.2.4" futures = { version = "0.3.21", default-features = false, features = ["executor"] } matrix-sdk-base = { path = "../../crates/matrix-sdk-base", version = "0.6.0"} matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", version = "0.2.0"} -ruma = "0.7.0" +ruma = { workspace = true } rustyline = "10.0.0" rustyline-derive = "0.7.0" serde = "1.0.136" diff --git a/testing/matrix-sdk-test/Cargo.toml b/testing/matrix-sdk-test/Cargo.toml index bd76f9218..1719d2b8c 100644 --- a/testing/matrix-sdk-test/Cargo.toml +++ b/testing/matrix-sdk-test/Cargo.toml @@ -22,7 +22,7 @@ appservice = [] http = "0.2.6" matrix-sdk-test-macros = { version = "0.3.0", path = "../matrix-sdk-test-macros" } once_cell = "1.10.0" -ruma = { version = "0.7.0", features = ["client-api-c"] } +ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" From e6891addfb09c7b63bceb5148c23141a7fa6b983 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 12:09:29 +0200 Subject: [PATCH 28/66] refactor: Use workspace dependencies for vodozemac --- Cargo.toml | 1 + bindings/matrix-sdk-crypto-ffi/Cargo.toml | 4 +--- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-nodejs/Cargo.toml | 7 ++----- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-qrcode/Cargo.toml | 4 +--- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a183e824..05d28b9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ uniffi = "0.21.0" uniffi_macros = "0.21.0" uniffi_bindgen = "0.21.0" uniffi_build = { version = "0.21.0", features = ["builtin-bindgen"] } +vodozemac = "0.3.0" [profile.release] lto = true diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index a68928977..2a46252ce 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -29,6 +29,7 @@ tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } # keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job uniffi = { workspace = true } uniffi_macros = { workspace = true } +vodozemac = { workspace = true } zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [dependencies.js_int] @@ -55,9 +56,6 @@ version = "1.17.0" default_features = false features = ["rt-multi-thread"] -[dependencies.vodozemac] -version = "0.3.0" - [build-dependencies] uniffi_build = { workspace = true, features = ["builtin-bindgen"] } diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 5b0577e67..367e4f959 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -32,7 +32,7 @@ matrix-sdk-crypto = { version = "0.6.0", path = "../../crates/matrix-sdk-crypto" 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"] } -vodozemac = { version = "0.3.0", features = ["js"] } +vodozemac = { workspace = true, features = ["js"] } wasm-bindgen = "0.2.80" wasm-bindgen-futures = "0.4.30" js-sys = "0.3.49" diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index a741c6adc..1f09e5a6e 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -31,12 +31,9 @@ napi = { version = "2.9.1", default-features = false, features = ["napi6", "toki napi-derive = "2.9.1" serde_json = "1.0.79" http = "0.2.6" -zeroize = "1.3.0" tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true } - -[dependencies.vodozemac] -version = "0.3.0" -features = ["js"] +vodozemac = { workspace = true, features = ["js"]} +zeroize = "1.3.0" [build-dependencies] napi-build = "2.0.0" diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 0eff090cb..dee549b5e 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -48,7 +48,7 @@ serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" tracing = "0.1.34" -vodozemac = "0.3.0" +vodozemac = { workspace = true } zeroize = { version = "1.3.0", features = ["zeroize_derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index e4b3dc580..50cc9dcb8 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -21,9 +21,7 @@ byteorder = "1.4.3" qrcode = { version = "0.12.0", default-features = false } ruma-common = "0.10.0" thiserror = "1.0.30" - -[dependencies.vodozemac] -version = "0.3.0" +vodozemac = { workspace = true } [dev-dependencies] image = "0.23.0" From f25af209e19421f0bc5c46d09a97a22045a3136c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 12:17:09 +0200 Subject: [PATCH 29/66] refactor: Use workspace dependencies for zeroize --- Cargo.toml | 1 + bindings/matrix-sdk-crypto-ffi/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-nodejs/Cargo.toml | 2 +- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-store-encryption/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05d28b9fe..d1615c63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ uniffi_macros = "0.21.0" uniffi_bindgen = "0.21.0" uniffi_build = { version = "0.21.0", features = ["builtin-bindgen"] } vodozemac = "0.3.0" +zeroize = "1.3.0" [profile.release] lto = true diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 2a46252ce..05fa4ce20 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -30,7 +30,7 @@ tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } uniffi = { workspace = true } uniffi_macros = { workspace = true } vodozemac = { workspace = true } -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dependencies.js_int] version = "0.2.2" diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 367e4f959..e5fcbc6fc 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -42,4 +42,4 @@ http = "0.2.6" anyhow = "1.0.58" tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } -zeroize = "1.3.0" +zeroize = { workspace = true } diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index 1f09e5a6e..f3e8bff8b 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -33,7 +33,7 @@ serde_json = "1.0.79" http = "0.2.6" tracing-subscriber = { version = "0.3", default-features = false, features = ["tracing-log", "time", "smallvec", "fmt", "env-filter"], optional = true } vodozemac = { workspace = true, features = ["js"]} -zeroize = "1.3.0" +zeroize = { workspace = true } [build-dependencies] napi-build = "2.0.0" diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index ad111384a..e74f1d646 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -43,7 +43,7 @@ serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.79" thiserror = "1.0.30" tracing = "0.1.34" -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] futures = { version = "0.3.21", default-features = false, features = ["executor"] } diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index dee549b5e..1bcd2c814 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -49,7 +49,7 @@ sha2 = "0.10.2" thiserror = "1.0.30" tracing = "0.1.34" vodozemac = { workspace = true } -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.18", default-features = false, features = ["time"] } diff --git a/crates/matrix-sdk-store-encryption/Cargo.toml b/crates/matrix-sdk-store-encryption/Cargo.toml index 53ea8a1b0..35a423218 100644 --- a/crates/matrix-sdk-store-encryption/Cargo.toml +++ b/crates/matrix-sdk-store-encryption/Cargo.toml @@ -25,7 +25,7 @@ serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -zeroize = { version = "1.3.0", features = ["zeroize_derive"] } +zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] anyhow = "1.0.57" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index e06622b95..ac4f2bca6 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -90,7 +90,7 @@ tokio-stream = { version = "0.1.8", features = ["net"], optional = true } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = "0.1.34" url = "2.2.2" -zeroize = "1.3.0" +zeroize = { workspace = true } [dependencies.image] version = "0.24.2" From 4816d93e96c45607d21fa7c98759d25bcba44469 Mon Sep 17 00:00:00 2001 From: Flix Date: Tue, 18 Oct 2022 13:39:08 +0200 Subject: [PATCH 30/66] docs: Improve documentation for custom event handler context --- crates/matrix-sdk/README.md | 4 ++-- crates/matrix-sdk/src/client/mod.rs | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 9da5eef15..af0d40f42 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -43,8 +43,8 @@ async fn main() -> anyhow::Result<()> { }); // Syncing is important to synchronize the client state with the server. - // This method will never return. - client.sync(SyncSettings::default()).await; + // This method will never return unless there is an error. + client.sync(SyncSettings::default()).await?; Ok(()) } diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 6d164cd69..f75308dbd 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -532,7 +532,12 @@ impl Client { /// context argument types are only available for a subset of event types: /// /// * [`Room`][room::Room] is only available for room-specific events, i.e. - /// not for events like global account data events or presence events + /// not for events like global account data events or presence events. + /// + /// You can provide custom context via + /// [`add_event_handler_context`](Client::add_event_handler_context) and + /// then use [`Ctx`](crate::event_handler::Ctx) to extract the context + /// into the event handler. /// /// [`EventHandlerContext`]: crate::event_handler::EventHandlerContext /// @@ -544,6 +549,7 @@ impl Client { /// # let homeserver = Url::parse("http://localhost:8080").unwrap(); /// use matrix_sdk::{ /// deserialized_responses::EncryptionInfo, + /// event_handler::Ctx, /// room::Room, /// ruma::{ /// events::{ @@ -588,6 +594,16 @@ impl Client { /// }); /// client.remove_event_handler(handle); /// + /// // Registering custom event handler context: + /// #[derive(Debug, Clone)] // The context will be cloned for event handler. + /// struct MyContext { + /// number: usize, + /// } + /// client.add_event_handler_context(MyContext { number: 5 }); + /// client.add_event_handler(|ev: SyncRoomMessageEvent, context: Ctx| async move { + /// // Use the context + /// }); + /// /// // Custom events work exactly the same way, you just need to declare /// // the content struct and use the EventContent derive macro on it. /// #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] From 61452ad19005c769b50c4a0295b3136b8cd62fcf Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Mon, 10 Oct 2022 16:58:06 +0100 Subject: [PATCH 31/66] Replace QR with SAS verification --- .../matrix-sdk-crypto/src/identities/user.rs | 17 ++ crates/matrix-sdk-crypto/src/store/mod.rs | 2 +- .../src/verification/cache.rs | 23 ++- .../matrix-sdk-crypto/src/verification/mod.rs | 41 ++++- .../src/verification/requests.rs | 159 +++++++++++++++++- 5 files changed, 228 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index fb360216d..c5a0c7407 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -806,6 +806,23 @@ impl ReadOnlyOwnUserIdentity { }) } + #[cfg(test)] + pub(crate) async fn from_private(identity: &crate::olm::PrivateCrossSigningIdentity) -> Self { + let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key.clone(); + let self_signing_key = + identity.self_signing_key.lock().await.as_ref().unwrap().public_key.clone(); + let user_signing_key = + identity.user_signing_key.lock().await.as_ref().unwrap().public_key.clone(); + + Self { + user_id: identity.user_id().into(), + master_key, + self_signing_key, + user_signing_key, + verified: Arc::new(AtomicBool::new(true)), + } + } + /// Get the user id of this identity. pub fn user_id(&self) -> &UserId { &self.user_id diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index 69f4023fa..4cf48151b 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -325,7 +325,7 @@ impl Store { } #[cfg(test)] - /// Testing helper to allo to save only a set of devices + /// Testing helper to allow to save only a set of devices pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> { let changes = Changes { devices: DeviceChanges { changed: devices.to_vec(), ..Default::default() }, diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index bb674b4e4..92f7527a8 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use dashmap::DashMap; use ruma::{DeviceId, OwnedTransactionId, OwnedUserId, TransactionId, UserId}; -use tracing::trace; +use tracing::{trace, warn}; use super::{event_enums::OutgoingContent, Sas, Verification}; #[cfg(feature = "qrcode")] @@ -56,6 +56,8 @@ impl VerificationCache { let old_verification = old.value(); if !old_verification.is_cancelled() { + warn!("Received a new verification whilst another one with the same user is ongoing. Cancelling both verifications"); + if let Some(r) = old_verification.cancel() { self.add_request(r.into()) } @@ -78,11 +80,7 @@ impl VerificationCache { pub fn replace_sas(&self, sas: Sas) { let verification: Verification = sas.into(); - - self.verification - .entry(verification.other_user().to_owned()) - .or_default() - .insert(verification.flow_id().to_owned(), verification.clone()); + self.replace(verification); } #[cfg(feature = "qrcode")] @@ -90,6 +88,12 @@ impl VerificationCache { self.insert(qr) } + #[cfg(feature = "qrcode")] + pub fn replace_qr(&self, qr: QrVerification) { + let verification: Verification = qr.into(); + self.replace(verification); + } + #[cfg(feature = "qrcode")] pub fn get_qr(&self, sender: &UserId, flow_id: &str) -> Option { self.get(sender, flow_id).and_then(|v| { @@ -101,6 +105,13 @@ impl VerificationCache { }) } + pub fn replace(&self, verification: Verification) { + self.verification + .entry(verification.other_user().to_owned()) + .or_default() + .insert(verification.flow_id().to_owned(), verification.clone()); + } + pub fn get(&self, sender: &UserId, flow_id: &str) -> Option { self.verification.get(sender).and_then(|m| m.get(flow_id).map(|v| v.clone())) } diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 6eb7b01c5..afa457c37 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -826,7 +826,9 @@ mod test { use super::VerificationStore; use crate::{ - olm::PrivateCrossSigningIdentity, store::MemoryStore, ReadOnlyAccount, ReadOnlyDevice, + olm::PrivateCrossSigningIdentity, + store::{Changes, CryptoStore, IdentityChanges, MemoryStore}, + ReadOnlyAccount, ReadOnlyDevice, ReadOnlyOwnUserIdentity, ReadOnlyUserIdentity, }; pub fn alice_id() -> &'static UserId { @@ -848,28 +850,57 @@ mod test { pub(crate) async fn setup_stores() -> (VerificationStore, VerificationStore) { let alice = ReadOnlyAccount::new(alice_id(), alice_device_id()); let alice_store = MemoryStore::new(); - let alice_identity = Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())); + let (alice_private_identity, _, _) = + PrivateCrossSigningIdentity::with_account(&alice).await; + let alice_private_identity = Mutex::new(alice_private_identity); let bob = ReadOnlyAccount::new(bob_id(), bob_device_id()); let bob_store = MemoryStore::new(); - let bob_identity = Mutex::new(PrivateCrossSigningIdentity::empty(bob_id())); + let (bob_private_identity, _, _) = PrivateCrossSigningIdentity::with_account(&bob).await; + let bob_private_identity = Mutex::new(bob_private_identity); + + let alice_public_identity = + ReadOnlyUserIdentity::from_private(&*alice_private_identity.lock().await).await; + let alice_readonly_identity = + ReadOnlyOwnUserIdentity::from_private(&*alice_private_identity.lock().await).await; + let bob_public_identity = + ReadOnlyUserIdentity::from_private(&*bob_private_identity.lock().await).await; + let bob_readonly_identity = + ReadOnlyOwnUserIdentity::from_private(&*bob_private_identity.lock().await).await; let alice_device = ReadOnlyDevice::from_account(&alice).await; let bob_device = ReadOnlyDevice::from_account(&bob).await; + let alice_changes = Changes { + identities: IdentityChanges { + new: vec![alice_readonly_identity.into(), bob_public_identity.into()], + changed: vec![], + }, + ..Default::default() + }; + alice_store.save_changes(alice_changes).await.unwrap(); alice_store.save_devices(vec![bob_device]).await; + + let bob_changes = Changes { + identities: IdentityChanges { + new: vec![bob_readonly_identity.into(), alice_public_identity.into()], + changed: vec![], + }, + ..Default::default() + }; + bob_store.save_changes(bob_changes).await.unwrap(); bob_store.save_devices(vec![alice_device]).await; let alice_store = VerificationStore { account: alice, inner: Arc::new(alice_store), - private_identity: alice_identity.into(), + private_identity: alice_private_identity.into(), }; let bob_store = VerificationStore { account: bob.clone(), inner: Arc::new(bob_store), - private_identity: bob_identity.into(), + private_identity: bob_private_identity.into(), }; (alice_store, bob_store) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index fd33342a0..112a65146 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -336,7 +336,21 @@ impl VerificationRequest { if let Some(future) = fut { let qr_verification = future.await?; - self.verification_cache.insert_qr(qr_verification.clone()); + + // 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() + { + info!("Replacing existing QR verification"); + self.verification_cache.replace_qr(qr_verification.clone()); + } else { + info!("Inserting new QR verification"); + self.verification_cache.insert_qr(qr_verification.clone()); + } Ok(Some(qr_verification)) } else { @@ -634,7 +648,23 @@ impl VerificationRequest { if let Some((sas, content)) = s.clone().start_sas(self.we_started, self.inner.clone().into()).await? { - self.verification_cache.insert_sas(sas.clone()); + // We may have previously started QR verification and generated a QR code. If we + // now switch to SAS flow, the previous verification has to be replaced + if cfg!(feature = "qrcode") { + #[cfg(feature = "qrcode")] + if self + .verification_cache + .get_qr(sas.other_user_id(), sas.flow_id().as_str()) + .is_some() + { + info!("We have an ongoing QR verification, replacing with SAS"); + self.verification_cache.replace(sas.clone().into()) + } else { + self.verification_cache.insert_sas(sas.clone()); + } + } else { + self.verification_cache.insert_sas(sas.clone()); + } let request = match content { OutgoingContent::ToDevice(content) => ToDeviceRequest::with_id( @@ -1222,7 +1252,11 @@ mod tests { use std::convert::{TryFrom, TryInto}; + #[cfg(feature = "qrcode")] + use matrix_sdk_qrcode::QrVerificationData; use matrix_sdk_test::async_test; + #[cfg(feature = "qrcode")] + use ruma::events::key::verification::VerificationMethod; use ruma::{event_id, room_id}; use super::VerificationRequest; @@ -1385,4 +1419,125 @@ mod tests { assert!(alice_sas.started_from_request()); assert!(bob_sas.started_from_request()); } + + #[async_test] + #[cfg(feature = "qrcode")] + async fn can_scan_another_qr_after_creating_mine() { + let (alice_store, bob_store) = setup_stores().await; + + let flow_id = FlowId::ToDevice("TEST_FLOW_ID".into()); + + // We setup the initial verification request + let bob_request = VerificationRequest::new( + VerificationCache::new(), + bob_store, + flow_id.clone(), + alice_id(), + vec![], + Some(vec![VerificationMethod::QrCodeScanV1, VerificationMethod::QrCodeShowV1]), + ); + + let request = bob_request.request_to_device(); + let content: OutgoingContent = request.try_into().unwrap(); + let content = RequestContent::try_from(&content).unwrap(); + + let alice_request = VerificationRequest::from_request( + VerificationCache::new(), + alice_store, + bob_id(), + flow_id, + &content, + ); + + let content: OutgoingContent = alice_request + .accept_with_methods(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + ]) + .unwrap() + .try_into() + .unwrap(); + let content = ReadyContent::try_from(&content).unwrap(); + bob_request.receive_ready(alice_id(), &content); + + assert!(bob_request.is_ready()); + assert!(alice_request.is_ready()); + + // Each side can start its own QR verification flow by generating QR code + let alice_verification = alice_request.generate_qr_code().await.unwrap(); + let bob_verification = bob_request.generate_qr_code().await.unwrap(); + + assert!(alice_verification.is_some()); + assert!(bob_verification.is_some()); + + // Now only Alice scans Bob's code + let bob_qr_code = bob_verification.unwrap().to_bytes().unwrap(); + let bob_qr_code = QrVerificationData::from_bytes(bob_qr_code).unwrap(); + let alice_verification = alice_request.scan_qr_code(bob_qr_code).await.unwrap().unwrap(); + + // Finally we assert that the verification has been reciprocated rather than + // cancelled due to a duplicate verification flow + assert!(!alice_verification.is_cancelled()); + assert!(alice_verification.reciprocated()); + } + + #[async_test] + #[cfg(feature = "qrcode")] + async fn can_start_sas_after_generating_qr_code() { + let (alice_store, bob_store) = setup_stores().await; + + let flow_id = FlowId::ToDevice("TEST_FLOW_ID".into()); + + // We setup the initial verification request + let bob_request = VerificationRequest::new( + VerificationCache::new(), + bob_store, + flow_id.clone(), + alice_id(), + vec![], + Some(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + VerificationMethod::SasV1, + ]), + ); + + let request = bob_request.request_to_device(); + let content: OutgoingContent = request.try_into().unwrap(); + let content = RequestContent::try_from(&content).unwrap(); + + let alice_request = VerificationRequest::from_request( + VerificationCache::new(), + alice_store, + bob_id(), + flow_id, + &content, + ); + + let content: OutgoingContent = alice_request + .accept_with_methods(vec![ + VerificationMethod::QrCodeScanV1, + VerificationMethod::QrCodeShowV1, + ]) + .unwrap() + .try_into() + .unwrap(); + let content = ReadyContent::try_from(&content).unwrap(); + bob_request.receive_ready(alice_id(), &content); + + assert!(bob_request.is_ready()); + assert!(alice_request.is_ready()); + + // Each side can start its own QR verification flow by generating QR code + let alice_verification = alice_request.generate_qr_code().await.unwrap(); + let bob_verification = bob_request.generate_qr_code().await.unwrap(); + + assert!(alice_verification.is_some()); + assert!(bob_verification.is_some()); + + // Alice can now start SAS verification flow instead of QR without cancelling + // the request + let (sas, _) = alice_request.start_sas().await.unwrap().unwrap(); + assert!(!sas.is_cancelled()); + } } From 8bbdd28a8b6dea1d62ccbd500418c0a763e5e564 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 18 Oct 2022 11:05:39 +0100 Subject: [PATCH 32/66] Use cfg-if and debug logs --- Cargo.lock | 1 + crates/matrix-sdk-crypto/Cargo.toml | 1 + .../matrix-sdk-crypto/src/identities/user.rs | 2 +- .../src/verification/cache.rs | 6 ++- .../src/verification/requests.rs | 37 ++++++++++++------- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bb895bd9..e7cdc2137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2473,6 +2473,7 @@ dependencies = [ "base64", "bs58", "byteorder", + "cfg-if", "ctr", "dashmap", "event-listener", diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 1bcd2c814..5e10db58e 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -50,6 +50,7 @@ thiserror = "1.0.30" tracing = "0.1.34" vodozemac = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } +cfg-if = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1.18", default-features = false, features = ["time"] } diff --git a/crates/matrix-sdk-crypto/src/identities/user.rs b/crates/matrix-sdk-crypto/src/identities/user.rs index c5a0c7407..62ba60f5e 100644 --- a/crates/matrix-sdk-crypto/src/identities/user.rs +++ b/crates/matrix-sdk-crypto/src/identities/user.rs @@ -819,7 +819,7 @@ impl ReadOnlyOwnUserIdentity { master_key, self_signing_key, user_signing_key, - verified: Arc::new(AtomicBool::new(true)), + verified: Arc::new(AtomicBool::new(false)), } } diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index 92f7527a8..bb08a4776 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -56,7 +56,11 @@ impl VerificationCache { let old_verification = old.value(); if !old_verification.is_cancelled() { - warn!("Received a new verification whilst another one with the same user is ongoing. Cancelling both verifications"); + warn!( + user_id = verification.other_user().as_str(), + "Received a new verification whilst another one with \ + the same user is ongoing. Cancelling both verifications" + ); if let Some(r) = old_verification.cancel() { self.add_request(r.into()) diff --git a/crates/matrix-sdk-crypto/src/verification/requests.rs b/crates/matrix-sdk-crypto/src/verification/requests.rs index 112a65146..4f678694a 100644 --- a/crates/matrix-sdk-crypto/src/verification/requests.rs +++ b/crates/matrix-sdk-crypto/src/verification/requests.rs @@ -36,6 +36,8 @@ use ruma::{ DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId, }; +#[cfg(feature = "qrcode")] +use tracing::debug; use tracing::{info, trace, warn}; #[cfg(feature = "qrcode")] @@ -345,10 +347,18 @@ impl VerificationRequest { .get_qr(qr_verification.other_user_id(), qr_verification.flow_id().as_str()) .is_some() { - info!("Replacing existing QR verification"); + 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 { - info!("Inserting new QR verification"); + 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()); } @@ -650,20 +660,21 @@ impl VerificationRequest { { // We may have previously started QR verification and generated a QR code. If we // now switch to SAS flow, the previous verification has to be replaced - if cfg!(feature = "qrcode") { - #[cfg(feature = "qrcode")] - if self - .verification_cache - .get_qr(sas.other_user_id(), sas.flow_id().as_str()) - .is_some() - { - info!("We have an ongoing QR verification, replacing with SAS"); - self.verification_cache.replace(sas.clone().into()) + cfg_if::cfg_if! { + if #[cfg(feature = "qrcode")] { + if self.verification_cache.get_qr(sas.other_user_id(), sas.flow_id().as_str()).is_some() { + debug!( + user_id = %self.other_user(), + flow_id = self.flow_id().as_str(), + "We have an ongoing QR verification, replacing with SAS" + ); + self.verification_cache.replace(sas.clone().into()) + } else { + self.verification_cache.insert_sas(sas.clone()); + } } else { self.verification_cache.insert_sas(sas.clone()); } - } else { - self.verification_cache.insert_sas(sas.clone()); } let request = match content { From 1a466eb6678c1d2cc22133bf4b5241c22ab8e9df Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Tue, 18 Oct 2022 11:15:10 +0100 Subject: [PATCH 33/66] Add flow_id to logs --- crates/matrix-sdk-crypto/src/verification/cache.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/matrix-sdk-crypto/src/verification/cache.rs b/crates/matrix-sdk-crypto/src/verification/cache.rs index bb08a4776..5b6bb9c32 100644 --- a/crates/matrix-sdk-crypto/src/verification/cache.rs +++ b/crates/matrix-sdk-crypto/src/verification/cache.rs @@ -58,6 +58,8 @@ impl VerificationCache { if !old_verification.is_cancelled() { warn!( user_id = verification.other_user().as_str(), + old_flow_id = old_verification.flow_id(), + new_flow_id = verification.flow_id(), "Received a new verification whilst another one with \ the same user is ongoing. Cancelling both verifications" ); From 089f92d238f3f6fbd7e46275dc255ed7550c46a7 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 14:23:21 +0200 Subject: [PATCH 34/66] ci: Cancel all CI jobs for old commits when pushing to a PR branch (not just jobs from the ci workflow file) --- .github/workflows/cancel_others.yml | 12 ++++++++++++ .github/workflows/ci.yml | 9 --------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/cancel_others.yml diff --git a/.github/workflows/cancel_others.yml b/.github/workflows/cancel_others.yml new file mode 100644 index 000000000..0ab5f49a6 --- /dev/null +++ b/.github/workflows/cancel_others.yml @@ -0,0 +1,12 @@ +on: + pull_request: + branches: [main] + +jobs: + cancel-others: + runs-on: ubuntu-latest + steps: + - name: Cancel workflows for older commits + uses: styfle/cancel-workflow-action@0.9.1 + with: + workflow_id: all diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd1db671b..5c98b8c0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,15 +16,6 @@ env: CARGO_TERM_COLOR: always jobs: - cancel-others: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - xtask: runs-on: ubuntu-latest steps: From 31293c6c2bce8d864817c4638fe53f9e86f59af6 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 14:39:47 +0200 Subject: [PATCH 35/66] Testing with empty commit From bac95f4494f03cc757c13802e77f0d4252241bde Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 15:00:35 +0200 Subject: [PATCH 36/66] ci: Upgrade cancel-workflow-action --- .github/workflows/cancel_others.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cancel_others.yml b/.github/workflows/cancel_others.yml index 0ab5f49a6..2fef3bf43 100644 --- a/.github/workflows/cancel_others.yml +++ b/.github/workflows/cancel_others.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel workflows for older commits - uses: styfle/cancel-workflow-action@0.9.1 + uses: styfle/cancel-workflow-action@0.11.0 with: workflow_id: all From 52d96ceb60b117f0ab1c4b80be730f7b4bd5ca8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 18 Oct 2022 11:28:11 +0200 Subject: [PATCH 37/66] fix(bindings): Allow setting the store without a passphrase --- bindings/matrix-sdk-crypto-js/src/machine.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index b13d96686..0d3fb8e58 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -66,6 +66,7 @@ impl OlmMachine { #[cfg(target_arch = "wasm32")] (Some(store_name), Some(mut store_passphrase)) => { use std::sync::Arc; + use zeroize::Zeroize; let store = Some( @@ -82,9 +83,22 @@ impl OlmMachine { store } - (Some(_), None) => return Err(anyhow::Error::msg("The `store_name` has been set, and so, it expects a `store_passphrase`, which is not set; please provide one")), + #[cfg(target_arch = "wasm32")] + (Some(store_name), None) => { + use std::sync::Arc; + Some( + matrix_sdk_indexeddb::IndexeddbCryptoStore::open_with_name(&store_name) + .await + .map(Arc::new)?, + ) + } - (None, Some(_)) => return Err(anyhow::Error::msg("The `store_passphrase` has been set, but it has an effect only if `store_name` is set, which is not; please provide one")), + (None, Some(_)) => { + return Err(anyhow::Error::msg( + "The `store_passphrase` has been set, but it has an effect only if \ + `store_name` is set, which is not; please provide one", + )) + } _ => None, }; From 90865e2a0a71d0ffb651942e4f828a1436ce9600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 18 Oct 2022 11:28:41 +0200 Subject: [PATCH 38/66] chore(bindings): Clarify the OlmMachine constructor in the WASM bindings --- bindings/matrix-sdk-crypto-js/src/machine.rs | 27 ++++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 0d3fb8e58..3133d7331 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -30,20 +30,25 @@ pub struct OlmMachine { impl OlmMachine { /// Create a new memory based `OlmMachine`. /// - /// The created machine will keep the encryption keys only in - /// memory and once the objects is dropped, the keys will be lost. + /// The created machine will keep the encryption keys either in a IndexedDB + /// based store, or in a memory store and once the objects is dropped, + /// the keys will be lost. /// - /// `user_id` represents the unique ID of the user that owns this - /// machine. `device_id` represents the unique ID of the device + /// # Arguments + /// + /// * `user_id` - represents the unique ID of the user that owns this + /// machine. + /// + /// * `device_id` - represents the unique ID of the device /// that owns this machine. /// - /// `store_name` and `store_passphrase` are both optional, but - /// must be both set to have an effect. If they are both set, the - /// state of the machine will persist in a database named - /// `store_name` where its content is encrypted by the passphrase - /// given by `store_passphrase`. If they are not both set, the - /// created machine will keep the encryption keys only in memory, - /// and once the object is dropped, the keys will be lost. + /// * `store_name` - The name that should be used to open the IndexedDB + /// based database. If this isn't provided, a memory-only store will be + /// used. *Note* the memory-only store will lose your E2EE keys when the + /// `OlmMachine` gets dropped. + /// + /// * `store_passphrase` - The passphrase that should be used to encrypt the + /// IndexedDB based #[wasm_bindgen(constructor)] #[allow(clippy::new_ret_no_self)] pub fn new( From 57e9b36fac2921eea13a0ae084f84c20ef6471ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 18 Oct 2022 11:30:48 +0200 Subject: [PATCH 39/66] chore(bindings): Don't mention that the OlmMachine is a memory-only one --- bindings/matrix-sdk-crypto-nodejs/src/machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs index 5a51a7fcf..d98919653 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/machine.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/machine.rs @@ -36,7 +36,7 @@ impl OlmMachine { // the factory function. We also manually implement the // constructor to raise an error when called. - /// Create a new memory-based `OlmMachine` asynchronously. + /// Create a new `OlmMachine` asynchronously. /// /// The persistence of the encryption keys and all the inner /// objects are controlled by the `store_path` argument. From eb58dcc556dc56613f569f8539c1f0267185877c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 16:51:07 +0200 Subject: [PATCH 40/66] Try to make it work --- .github/workflows/cancel_others.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cancel_others.yml b/.github/workflows/cancel_others.yml index 2fef3bf43..0f1227f06 100644 --- a/.github/workflows/cancel_others.yml +++ b/.github/workflows/cancel_others.yml @@ -10,3 +10,4 @@ jobs: uses: styfle/cancel-workflow-action@0.11.0 with: workflow_id: all + all_but_latest: true From 95dfedb70aeb443a264196ecfa88a3fa529079a6 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 16:52:01 +0200 Subject: [PATCH 41/66] Another test commit From 3daaa457e16ce45bac78f676c555cfb14d42700e Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 12:35:47 +0200 Subject: [PATCH 42/66] fix(bindings): Make tracing dependency for crypto-js optional --- bindings/matrix-sdk-crypto-js/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index e5fcbc6fc..8187810ed 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -24,7 +24,7 @@ crate-type = ["cdylib"] [features] default = ["tracing", "qrcode"] qrcode = ["matrix-sdk-crypto/qrcode", "dep:matrix-sdk-qrcode"] -tracing = [] +tracing = ["dep:tracing"] [dependencies] matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] } @@ -40,6 +40,6 @@ console_error_panic_hook = "0.1.7" serde_json = "1.0.79" http = "0.2.6" anyhow = "1.0.58" -tracing = { version = "0.1.35", default-features = false, features = ["attributes"] } +tracing = { version = "0.1.35", default-features = false, features = ["attributes"], optional = true } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } zeroize = { workspace = true } From 0ee63344da7ab098433b919210a259c17844471d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 12:51:36 +0200 Subject: [PATCH 43/66] chore(base): Sort dev-dependencies --- crates/matrix-sdk-base/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index e74f1d646..1e6f3fdb1 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -46,12 +46,12 @@ tracing = "0.1.34" zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] -futures = { version = "0.3.21", default-features = false, features = ["executor"] } -tracing = { version = "0.1.26", features = ["log"] } -http = "0.2.6" assign = "1.1.1" env_logger = "0.9.0" +futures = { version = "0.3.21", default-features = false, features = ["executor"] } +http = "0.2.6" matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" } +tracing = { version = "0.1.26", features = ["log"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } From 6990c1ca5cfcaa9bcfdf00823fb2fd6a1667754b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 13:08:25 +0200 Subject: [PATCH 44/66] test(base): Use tracing-subscriber for test debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … same as in our other crates. --- Cargo.lock | 22 ++-------------------- crates/matrix-sdk-base/Cargo.toml | 4 ++-- crates/matrix-sdk-base/src/lib.rs | 10 ++++++++++ crates/matrix-sdk-base/src/rooms/normal.rs | 6 ------ 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7cdc2137..04c221cae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,19 +1248,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "errno" version = "0.2.8" @@ -1890,12 +1877,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -2423,8 +2404,8 @@ dependencies = [ "assign", "async-stream", "async-trait", + "ctor", "dashmap", - "env_logger", "futures", "futures-channel", "futures-core", @@ -2442,6 +2423,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber", "wasm-bindgen-test", "zeroize", ] diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 1e6f3fdb1..d55dd65e6 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -47,11 +47,11 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] assign = "1.1.1" -env_logger = "0.9.0" +ctor = "0.1.23" futures = { version = "0.3.21", default-features = false, features = ["executor"] } http = "0.2.6" matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" } -tracing = { version = "0.1.26", features = ["log"] } +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 24afa1ef6..3861745cd 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -45,3 +45,13 @@ pub use store::{StateChanges, StateStore, StoreError}; pub use utils::{ MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent, }; + +#[cfg(all(test, not(target_arch = "wasm32")))] +#[ctor::ctor] +fn init_logging() { + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .init(); +} diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 6d3805dd0..21e0e35ac 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -748,7 +748,6 @@ mod test { #[async_test] async fn test_display_name_default() { - let _ = env_logger::try_init(); let (_, room) = make_room(RoomType::Joined); assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty); @@ -786,7 +785,6 @@ mod test { #[async_test] async fn test_display_name_dm_invited() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Invited); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -809,7 +807,6 @@ mod test { #[async_test] async fn test_display_name_dm_invited_no_heroes() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Invited); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -828,7 +825,6 @@ mod test { #[async_test] async fn test_display_name_dm_joined() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -860,7 +856,6 @@ mod test { #[async_test] async fn test_display_name_dm_joined_no_heroes() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); @@ -887,7 +882,6 @@ mod test { #[async_test] async fn test_display_name_dm_alone() { - let _ = env_logger::try_init(); let (store, room) = make_room(RoomType::Joined); let room_id = room_id!("!test:localhost"); let matthew = user_id!("@matthew:example.org"); From 0cfc7540cf6ba118da9fdc381373b832b6f94e82 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 18 Oct 2022 13:28:28 +0200 Subject: [PATCH 45/66] refactor: Use workspace dependencies for tracing --- Cargo.toml | 1 + bindings/matrix-sdk-crypto-ffi/Cargo.toml | 2 +- bindings/matrix-sdk-crypto-js/Cargo.toml | 2 +- bindings/matrix-sdk-ffi/Cargo.toml | 2 +- crates/matrix-sdk-appservice/Cargo.toml | 2 +- crates/matrix-sdk-base/Cargo.toml | 2 +- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-indexeddb/Cargo.toml | 2 +- crates/matrix-sdk-sled/Cargo.toml | 2 +- crates/matrix-sdk/Cargo.toml | 2 +- examples/appservice_autojoin/Cargo.toml | 2 +- testing/matrix-sdk-integration-testing/Cargo.toml | 2 +- 12 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d1615c63c..92cd733d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ resolver = "2" [workspace.dependencies] ruma = { version = "0.7.4", features = ["client-api-c"] } +tracing = { version = "0.1.36", default-features = false, features = ["std"] } uniffi = "0.21.0" uniffi_macros = "0.21.0" uniffi_bindgen = "0.21.0" diff --git a/bindings/matrix-sdk-crypto-ffi/Cargo.toml b/bindings/matrix-sdk-crypto-ffi/Cargo.toml index 05fa4ce20..e8fc2de34 100644 --- a/bindings/matrix-sdk-crypto-ffi/Cargo.toml +++ b/bindings/matrix-sdk-crypto-ffi/Cargo.toml @@ -24,7 +24,7 @@ serde = "1.0.136" serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true } tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } # keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job uniffi = { workspace = true } diff --git a/bindings/matrix-sdk-crypto-js/Cargo.toml b/bindings/matrix-sdk-crypto-js/Cargo.toml index 8187810ed..17b62cd99 100644 --- a/bindings/matrix-sdk-crypto-js/Cargo.toml +++ b/bindings/matrix-sdk-crypto-js/Cargo.toml @@ -40,6 +40,6 @@ console_error_panic_hook = "0.1.7" serde_json = "1.0.79" http = "0.2.6" anyhow = "1.0.58" -tracing = { version = "0.1.35", default-features = false, features = ["attributes"], optional = true } +tracing = { workspace = true, optional = true } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] } zeroize = { workspace = true } diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 1bf815558..c4f19f0d1 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -35,7 +35,7 @@ serde_json = { version = "1" } thiserror = "1.0.30" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tokio-stream = "0.1.8" -tracing = "0.1.32" +tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } uniffi = { workspace = true } uniffi_macros = { workspace = true } diff --git a/crates/matrix-sdk-appservice/Cargo.toml b/crates/matrix-sdk-appservice/Cargo.toml index 3301bab5b..c4bd10106 100644 --- a/crates/matrix-sdk-appservice/Cargo.toml +++ b/crates/matrix-sdk-appservice/Cargo.toml @@ -44,7 +44,7 @@ serde_yaml = "0.9.4" tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] } thiserror = "1.0.30" tower = { version = "0.4.13", default-features = false } -tracing = "0.1.34" +tracing = { workspace = true } url = "2.2.2" [dev-dependencies] diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index d55dd65e6..a9ae90981 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -42,7 +42,7 @@ ruma = { workspace = true, features = ["canonical-json"] } serde = { version = "1.0.136", features = ["rc"] } serde_json = "1.0.79" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } [dev-dependencies] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 5e10db58e..9a7dbd106 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -47,7 +47,7 @@ serde = { version = "1.0.136", features = ["derive", "rc"] } serde_json = "1.0.79" sha2 = "0.10.2" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true, features = ["attributes"] } vodozemac = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } cfg-if = "1.0" diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 0d2ba8aef..4b796b6b4 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -34,7 +34,7 @@ ruma = { workspace = true } serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" -tracing = "0.1.34" +tracing = { workspace = true } wasm-bindgen = { version = "0.2.80", features = ["serde-serialize"] } web-sys = { version = "0.3.57", features = ["IdbKeyRange"] } diff --git a/crates/matrix-sdk-sled/Cargo.toml b/crates/matrix-sdk-sled/Cargo.toml index 3a4c82fbb..b9b3b689a 100644 --- a/crates/matrix-sdk-sled/Cargo.toml +++ b/crates/matrix-sdk-sled/Cargo.toml @@ -41,7 +41,7 @@ serde_json = "1.0.79" sled = "0.34.7" thiserror = "1.0.30" tokio = { version = "1.17.0", default-features = false, features = ["sync", "fs"] } -tracing = "0.1.34" +tracing = { workspace = true } [dev-dependencies] glob = "0.3.0" diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index ac4f2bca6..5ec0ee230 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -88,7 +88,7 @@ serde_json = "1.0.79" thiserror = "1.0.30" tokio-stream = { version = "0.1.8", features = ["net"], optional = true } tower = { version = "0.4.13", features = ["make"], optional = true } -tracing = "0.1.34" +tracing = { workspace = true, features = ["attributes"] } url = "2.2.2" zeroize = { workspace = true } diff --git a/examples/appservice_autojoin/Cargo.toml b/examples/appservice_autojoin/Cargo.toml index 34a812fe1..431d94433 100644 --- a/examples/appservice_autojoin/Cargo.toml +++ b/examples/appservice_autojoin/Cargo.toml @@ -12,7 +12,7 @@ test = false anyhow = "1" tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.15" -tracing = "0.1.36" +tracing = { workspace = true } [dependencies.matrix-sdk-appservice] path = "../../crates/matrix-sdk-appservice" diff --git a/testing/matrix-sdk-integration-testing/Cargo.toml b/testing/matrix-sdk-integration-testing/Cargo.toml index 44e3eb710..d093a4157 100644 --- a/testing/matrix-sdk-integration-testing/Cargo.toml +++ b/testing/matrix-sdk-integration-testing/Cargo.toml @@ -13,5 +13,5 @@ matrix-sdk = { path = "../../crates/matrix-sdk" } once_cell = "1.13.0" tempfile = "3.3.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } -tracing = "0.1.36" +tracing = { workspace = true } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } From c92d946777fc71d0b76c2f25fa373e487cbc73a3 Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Fri, 30 Sep 2022 15:03:54 +0100 Subject: [PATCH 46/66] Set local trust --- bindings/apple/build_crypto_xcframework.sh | 7 ++++++- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 5 ++++- bindings/matrix-sdk-crypto-ffi/src/logger.rs | 1 + bindings/matrix-sdk-crypto-ffi/src/machine.rs | 8 +++++--- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 9 ++++++++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/bindings/apple/build_crypto_xcframework.sh b/bindings/apple/build_crypto_xcframework.sh index a4cfa81d3..11e29fd40 100755 --- a/bindings/apple/build_crypto_xcframework.sh +++ b/bindings/apple/build_crypto_xcframework.sh @@ -51,7 +51,12 @@ lipo -create \ -output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" # Generate uniffi files -uniffi-bindgen generate "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" --language swift --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" --out-dir ${GENERATED_DIR} +uniffi-bindgen generate \ + --language swift \ + --lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \ + --config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \ + --out-dir ${GENERATED_DIR} \ + "${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl" # Move headers to the right place HEADERS_DIR=${GENERATED_DIR}/headers diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index 79e874b6d..e35c26ae0 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -28,7 +28,10 @@ pub use error::{ use js_int::UInt; pub use logger::{set_logger, Logger}; pub use machine::{KeyRequestPair, OlmMachine}; -use matrix_sdk_crypto::types::{EventEncryptionAlgorithm, SigningKey}; +use matrix_sdk_crypto::{ + types::{EventEncryptionAlgorithm, SigningKey}, + LocalTrust, +}; pub use responses::{ BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest, Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest, diff --git a/bindings/matrix-sdk-crypto-ffi/src/logger.rs b/bindings/matrix-sdk-crypto-ffi/src/logger.rs index b047784dd..ce9ea0687 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/logger.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/logger.rs @@ -51,6 +51,7 @@ pub fn set_logger(logger: Box) { let _ = tracing_subscriber::fmt() .with_writer(logger) .with_env_filter(filter) + .with_ansi(false) .without_time() .try_init(); } diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 6cc3b09b1..bd662a831 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -283,11 +283,13 @@ impl OlmMachine { } } - /// Mark the device of the given user with the given device ID as trusted. - pub fn mark_device_as_trusted( + /// Set local trust state for the device of the given user without creating + /// or uploading any signatures if verified + pub fn set_local_trust( &self, user_id: &str, device_id: &str, + trust_state: LocalTrust, ) -> Result<(), CryptoStoreError> { let user_id = parse_user_id(user_id)?; @@ -295,7 +297,7 @@ impl OlmMachine { self.runtime.block_on(self.inner.get_device(&user_id, device_id.into(), None))?; if let Some(device) = device { - self.runtime.block_on(device.set_local_trust(LocalTrust::Verified))?; + self.runtime.block_on(device.set_local_trust(trust_state))?; } Ok(()) diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index a30f65407..8432e36de 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -247,6 +247,13 @@ enum RequestType { "RoomMessage", }; +enum LocalTrust { + "Verified", + "BlackListed", + "Ignored", + "Unset", +}; + interface OlmMachine { [Throws=CryptoStoreError] constructor( @@ -282,7 +289,7 @@ interface OlmMachine { [Throws=CryptoStoreError] Device? get_device([ByRef] string user_id, [ByRef] string device_id, u32 timeout); [Throws=CryptoStoreError] - void mark_device_as_trusted([ByRef] string user_id, [ByRef] string device_id); + void set_local_trust([ByRef] string user_id, [ByRef] string device_id, LocalTrust trust_state); [Throws=SignatureError] SignatureUploadRequest verify_device([ByRef] string user_id, [ByRef] string device_id); [Throws=CryptoStoreError] From 8470b494f499f50a1fbe21907b24cc6b398aa8c7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 19 Oct 2022 14:18:19 +0300 Subject: [PATCH 47/66] chore(bindings): Replace various expectations with anyhow contexts --- bindings/matrix-sdk-ffi/src/client.rs | 16 ++++++++-------- bindings/matrix-sdk-ffi/src/room.rs | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index fdffe5271..1b89bbe99 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, RwLock}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use matrix_sdk::{ config::SyncSettings, media::{MediaFormat, MediaRequest, MediaThumbnailSize}, @@ -129,7 +129,7 @@ impl Client { pub fn restore_token(&self) -> anyhow::Result { RUNTIME.block_on(async move { - let session = self.client.session().expect("Missing session"); + let session = self.client.session().context("Missing session")?; let homeurl = self.client.homeserver().await.into(); Ok(serde_json::to_string(&RestoreToken { session, @@ -141,14 +141,14 @@ impl Client { } pub fn user_id(&self) -> anyhow::Result { - let user_id = self.client.user_id().expect("No User ID found"); + let user_id = self.client.user_id().context("No User ID found")?; Ok(user_id.to_string()) } pub fn display_name(&self) -> anyhow::Result { let l = self.client.clone(); RUNTIME.block_on(async move { - let display_name = l.account().get_display_name().await?.expect("No User ID found"); + let display_name = l.account().get_display_name().await?.context("No User ID found")?; Ok(display_name) }) } @@ -156,13 +156,13 @@ impl Client { pub fn avatar_url(&self) -> anyhow::Result { let l = self.client.clone(); RUNTIME.block_on(async move { - let avatar_url = l.account().get_avatar_url().await?.expect("No User ID found"); + let avatar_url = l.account().get_avatar_url().await?.context("No User ID found")?; Ok(avatar_url.to_string()) }) } pub fn device_id(&self) -> anyhow::Result { - let device_id = self.client.device_id().expect("No Device ID found"); + let device_id = self.client.device_id().context("No Device ID found")?; Ok(device_id.to_string()) } @@ -235,13 +235,13 @@ impl Client { return Ok(Arc::new(session_verification_controller.clone())); } - let user_id = self.client.user_id().expect("Failed retrieving current user_id"); + let user_id = self.client.user_id().context("Failed retrieving current user_id")?; let user_identity = self .client .encryption() .get_user_identity(user_id) .await? - .expect("Failed retrieving user identity"); + .context("Failed retrieving user identity")?; let session_verification_controller = SessionVerificationController::new(user_identity); diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 70b2859e9..ea77f0177 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -92,8 +92,8 @@ impl Room { let room = self.room.clone(); let user_id = user_id; RUNTIME.block_on(async move { - let user_id = <&UserId>::try_from(&*user_id).expect("Invalid user id."); - let member = room.get_member(user_id).await?.expect("No user found"); + let user_id = <&UserId>::try_from(&*user_id).context("Invalid user id.")?; + let member = room.get_member(user_id).await?.context("No user found")?; let avatar_url_string = member.avatar_url().map(|m| m.to_string()); Ok(avatar_url_string) }) @@ -103,8 +103,8 @@ impl Room { let room = self.room.clone(); let user_id = user_id; RUNTIME.block_on(async move { - let user_id = <&UserId>::try_from(&*user_id).expect("Invalid user id."); - let member = room.get_member(user_id).await?.expect("No user found"); + let user_id = <&UserId>::try_from(&*user_id).context("Invalid user id.")?; + let member = room.get_member(user_id).await?.context("No user found")?; let avatar_url_string = member.display_name().map(|m| m.to_owned()); Ok(avatar_url_string) }) From 7851eefb61d422fcb852c7b2fd1b492bb881ab4a Mon Sep 17 00:00:00 2001 From: Flix Date: Wed, 19 Oct 2022 16:28:27 +0200 Subject: [PATCH 48/66] refactor(base)!: Get rid of String in StoreError --- Cargo.lock | 1 + crates/matrix-sdk-base/Cargo.toml | 3 ++- crates/matrix-sdk-base/src/store/mod.rs | 7 +++++-- .../matrix-sdk-indexeddb/src/state_store.rs | 20 +++--------------- crates/matrix-sdk-sled/src/state_store.rs | 21 +++++-------------- 5 files changed, 16 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04c221cae..decb39054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2415,6 +2415,7 @@ dependencies = [ "lru", "matrix-sdk-common", "matrix-sdk-crypto", + "matrix-sdk-store-encryption", "matrix-sdk-test", "once_cell", "ruma", diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index a9ae90981..d7bc4acef 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] e2e-encryption = ["dep:matrix-sdk-crypto"] -js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js"] +js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"] qrcode = ["matrix-sdk-crypto?/qrcode"] sliding-sync = ["ruma/unstable-msc3575"] @@ -37,6 +37,7 @@ http = { version = "0.2.6", optional = true } lru = "0.8.0" matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" } matrix-sdk-crypto = { version = "0.6.0", path = "../matrix-sdk-crypto", optional = true } +matrix-sdk-store-encryption = { version = "0.2.0", path = "../matrix-sdk-store-encryption" } once_cell = "1.10.0" ruma = { workspace = true, features = ["canonical-json"] } serde = { version = "1.0.136", features = ["rc"] } diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index bcc5e67c6..5f603f660 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -26,6 +26,7 @@ use std::{ ops::Deref, pin::Pin, result::Result as StdResult, + str::Utf8Error, sync::Arc, }; @@ -41,6 +42,7 @@ use dashmap::DashMap; use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps}; #[cfg(feature = "e2e-encryption")] use matrix_sdk_crypto::store::{CryptoStore, IntoCryptoStore}; +pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; use ruma::{ api::client::push::get_notifications::v3::Notification, events::{ @@ -98,10 +100,11 @@ pub enum StoreError { UnencryptedStore, /// The store failed to encrypt or decrypt some data. #[error("Error encrypting or decrypting data from the store: {0}")] - Encryption(String), + Encryption(#[from] StoreEncryptionError), + /// The store failed to encode or decode some data. #[error("Error encoding or decoding data from the store: {0}")] - Codec(String), + Codec(#[from] Utf8Error), /// The database format has changed in a backwards incompatible way. #[error( diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 1d0675d58..8f45dbbf0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -97,17 +97,7 @@ impl From for StoreError { match e { IndexeddbStateStoreError::Json(e) => StoreError::Json(e), IndexeddbStateStoreError::StoreError(e) => e, - IndexeddbStateStoreError::Encryption(e) => match e { - EncryptionError::Random(e) => StoreError::Encryption(e.to_string()), - EncryptionError::Serialization(e) => StoreError::Json(e), - EncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()), - EncryptionError::Version(found, expected) => StoreError::Encryption(format!( - "Bad Database Encryption Version: expected {expected}, found {found}", - )), - EncryptionError::Length(found, expected) => StoreError::Encryption(format!( - "The database key an invalid length: expected {expected}, found {found}", - )), - }, + IndexeddbStateStoreError::Encryption(e) => StoreError::Encryption(e), _ => StoreError::backend(e), } } @@ -1140,9 +1130,7 @@ impl IndexeddbStateStore { } async fn get_custom_value(&self, key: &[u8]) -> Result>> { - let jskey = &JsValue::from_str( - core::str::from_utf8(key).map_err(|e| StoreError::Codec(format!("{:}", e)))?, - ); + let jskey = &JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); self.get_custom_value_for_js(jskey).await } @@ -1157,9 +1145,7 @@ impl IndexeddbStateStore { } async fn set_custom_value(&self, key: &[u8], value: Vec) -> Result>> { - let jskey = JsValue::from_str( - core::str::from_utf8(key).map_err(|e| StoreError::Codec(format!("{:}", e)))?, - ); + let jskey = JsValue::from_str(core::str::from_utf8(key).map_err(StoreError::Codec)?); let prev = self.get_custom_value_for_js(&jskey).await?; diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index 677c20c79..88a11edbf 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -104,25 +104,14 @@ impl From> for SledStoreError { } } -#[allow(clippy::from_over_into)] -impl Into for SledStoreError { - fn into(self) -> StoreError { - match self { +impl From for StoreError { + fn from(err: SledStoreError) -> StoreError { + match err { SledStoreError::Json(e) => StoreError::Json(e), SledStoreError::Identifier(e) => StoreError::Identifier(e), - SledStoreError::Encryption(e) => match e { - KeyEncryptionError::Random(e) => StoreError::Encryption(e.to_string()), - KeyEncryptionError::Serialization(e) => StoreError::Json(e), - KeyEncryptionError::Encryption(e) => StoreError::Encryption(e.to_string()), - KeyEncryptionError::Version(found, expected) => StoreError::Encryption(format!( - "Bad Database Encryption Version: expected {expected}, found {found}", - )), - KeyEncryptionError::Length(found, expected) => StoreError::Encryption(format!( - "The database key an invalid length: expected {expected}, found {found}", - )), - }, + SledStoreError::Encryption(e) => StoreError::Encryption(e), SledStoreError::StoreError(e) => e, - _ => StoreError::backend(self), + _ => StoreError::backend(err), } } } From a50743874f83e0624bd26d0b1a3f6f3fabef266a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 10:10:23 +0200 Subject: [PATCH 49/66] chore(base): Merge consecutive impls for the same type --- crates/matrix-sdk-base/src/store/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 5f603f660..3bcd870fc 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -513,9 +513,7 @@ impl Store { Self::new(inner) } -} -impl Store { /// Create a new store, wrappning the given `StateStore` pub fn new(inner: Arc) -> Self { Self { From 0aba22855bb271ce0d9b2ca0e4c3b03dd686da1f Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 10:38:45 +0200 Subject: [PATCH 50/66] fix(sdk): Don't include access and refresh token in Debug output of Session --- crates/matrix-sdk-base/src/session.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/session.rs b/crates/matrix-sdk-base/src/session.rs index de3b30539..6a01ca8f2 100644 --- a/crates/matrix-sdk-base/src/session.rs +++ b/crates/matrix-sdk-base/src/session.rs @@ -15,6 +15,8 @@ //! User sessions. +use std::fmt; + use ruma::{api::client::session::refresh_token, OwnedDeviceId, OwnedUserId}; use serde::{Deserialize, Serialize}; @@ -36,7 +38,7 @@ use serde::{Deserialize, Serialize}; /// /// assert_eq!(session.device_id.as_str(), "MYDEVICEID"); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct Session { /// The access token used for this session. pub access_token: String, @@ -66,6 +68,15 @@ impl Session { } } +impl fmt::Debug for Session { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Session") + .field("user_id", &self.user_id) + .field("device_id", &self.device_id) + .finish_non_exhaustive() + } +} + impl From for Session { fn from(response: ruma::api::client::session::login::v3::Response) -> Self { Self { From 584a82b19bcaa6bb2e106c90cf8e03168d868ef6 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 10:39:30 +0200 Subject: [PATCH 51/66] fix(sdk): Remove Debug implementation for SessionTokens --- crates/matrix-sdk-base/src/client.rs | 3 +-- crates/matrix-sdk-base/src/session.rs | 3 ++- crates/matrix-sdk-base/src/store/mod.rs | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 724e3f5c2..9f5605d52 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -94,9 +94,8 @@ impl fmt::Debug for BaseClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("session_meta", &self.store.session_meta()) - .field("session_tokens", &self.store.session_tokens) .field("sync_token", &self.store.sync_token) - .finish() + .finish_non_exhaustive() } } diff --git a/crates/matrix-sdk-base/src/session.rs b/crates/matrix-sdk-base/src/session.rs index 6a01ca8f2..35b58213b 100644 --- a/crates/matrix-sdk-base/src/session.rs +++ b/crates/matrix-sdk-base/src/session.rs @@ -99,7 +99,8 @@ pub struct SessionMeta { /// The mutable parts of the session: the access token and optional refresh /// token. -#[derive(Clone, Debug)] +#[derive(Clone)] +#[allow(missing_debug_implementations)] pub struct SessionTokens { /// The access token used for this session. pub access_token: String, diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index 3bcd870fc..c96f40397 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -23,6 +23,7 @@ use std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, + fmt, ops::Deref, pin::Pin, result::Result as StdResult, @@ -495,7 +496,7 @@ where /// /// This adds additional higher level store functionality on top of a /// `StateStore` implementation. -#[derive(Debug, Clone)] +#[derive(Clone)] pub(crate) struct Store { pub(super) inner: Arc, session_meta: Arc>, @@ -635,6 +636,18 @@ impl Store { } } +impl fmt::Debug for Store { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Store") + .field("inner", &self.inner) + .field("session_meta", &self.session_meta) + .field("sync_token", &self.sync_token) + .field("rooms", &self.rooms) + .field("stripped_rooms", &self.stripped_rooms) + .finish_non_exhaustive() + } +} + impl Deref for Store { type Target = dyn StateStore; From 01dc166293b00a7600f99f6354efae0b3444a5df Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 12:14:27 +0200 Subject: [PATCH 52/66] chore: Upgrade UniFFI --- .github/workflows/bindings_ci.yml | 4 ++-- Cargo.lock | 18 ++++++------------ Cargo.toml | 8 ++++---- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/workflows/bindings_ci.yml b/.github/workflows/bindings_ci.yml index 61951fb12..5c877ffd2 100644 --- a/.github/workflows/bindings_ci.yml +++ b/.github/workflows/bindings_ci.yml @@ -156,8 +156,8 @@ jobs: uses: actions-rs/cargo@v1 with: command: install - # keep in sync with uniffi dependency in Cargo.toml's - args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev 0eee77f67b716c4896494606e5931d249871b23a + # keep in sync with uniffi dependency in root Cargo.toml + args: uniffi_bindgen --git https://github.com/mozilla/uniffi-rs --rev fdb769b567865d9c5c7c682a18d0c1301a039c85 - name: Build library & bindings run: cargo xtask swift build-library diff --git a/Cargo.lock b/Cargo.lock index decb39054..1c2bf8de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4844,8 +4844,7 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "uniffi" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54af5ada67d1173457a99a7bb44a7917f63e7466764cb4714865c7a6678b830" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "bytes", @@ -4861,8 +4860,7 @@ dependencies = [ [[package]] name = "uniffi_bindgen" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc4af3c0180c7e86c4a3acf2b6587af04ba2567b1e948993df10f421796621" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "askama", @@ -4884,8 +4882,7 @@ dependencies = [ [[package]] name = "uniffi_build" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510287c368a9386eb731ebe824a6fc6c82a105e20d020af1aa20519c1c1561db" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "anyhow", "camino", @@ -4895,8 +4892,7 @@ dependencies = [ [[package]] name = "uniffi_macros" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8604503caa2cbcf271578dc51ca236d40e3b22e1514ffa2e638e2c39f6ad10" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "bincode", "camino", @@ -4914,8 +4910,7 @@ dependencies = [ [[package]] name = "uniffi_meta" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9417cc653937681436b93838d8c5f97a5b8c58697813982ee8810bd1dc3b57" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "serde", ] @@ -5225,8 +5220,7 @@ dependencies = [ [[package]] name = "weedle2" version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +source = "git+https://github.com/mozilla/uniffi-rs?rev=fdb769b567865d9c5c7c682a18d0c1301a039c85#fdb769b567865d9c5c7c682a18d0c1301a039c85" dependencies = [ "nom", ] diff --git a/Cargo.toml b/Cargo.toml index 92cd733d2..a5f5618f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,10 @@ resolver = "2" [workspace.dependencies] ruma = { version = "0.7.4", features = ["client-api-c"] } tracing = { version = "0.1.36", default-features = false, features = ["std"] } -uniffi = "0.21.0" -uniffi_macros = "0.21.0" -uniffi_bindgen = "0.21.0" -uniffi_build = { version = "0.21.0", features = ["builtin-bindgen"] } +uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85" } +uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "fdb769b567865d9c5c7c682a18d0c1301a039c85", features = ["builtin-bindgen"] } vodozemac = "0.3.0" zeroize = "1.3.0" From 6b9075aa42dbd30253468a6c41c84cce2a67d06c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 15 Oct 2022 00:49:33 +0200 Subject: [PATCH 53/66] refactor(bindings): Use new uniffi::Enum derive macro in crypto-ffi --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 7 +- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 287 +++++++++--------- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 25 -- .../matrix-sdk-crypto-ffi/src/responses.rs | 1 + .../matrix-sdk-crypto-ffi/src/verification.rs | 5 + 5 files changed, 158 insertions(+), 167 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index e35c26ae0..caa7c4ddc 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -474,7 +474,12 @@ fn parse_user_id(user_id: &str) -> Result { } mod uniffi_types { - pub use crate::{backup_recovery_key::BackupRecoveryKey, machine::OlmMachine}; + pub use crate::{ + backup_recovery_key::BackupRecoveryKey, + machine::OlmMachine, + responses::OutgoingVerificationRequest, + verification::{CancelInfo, QrCode, Sas, ScanResult, Verification}, + }; } #[cfg(test)] diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index bd662a831..7418a571a 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -87,6 +87,152 @@ impl OlmMachine { HashMap::from([("ed25519".to_owned(), ed25519_key), ("curve25519".to_owned(), curve_key)]) } + + /// Accept a verification requests that we share with the given user with + /// the given flow id. + /// + /// This will move the verification request into the ready state. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// verification requests. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `methods` - A list of verification methods that we want to advertise + /// as supported. + pub fn accept_verification_request( + &self, + user_id: String, + flow_id: String, + methods: Vec, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + if let Some(verification) = self.inner.get_verification_request(&user_id, &flow_id) { + verification.accept_with_methods(methods).map(|r| r.into()) + } else { + None + } + } + + /// Get a verification flow object for the given user with the given flow + /// id. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to fetch the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_verification(&self, user_id: String, flow_id: String) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + self.inner.get_verification(&user_id, &flow_id).map(|v| match v { + RustVerification::SasV1(s) => Verification::SasV1 { sas: s.into() }, + RustVerification::QrV1(qr) => Verification::QrCodeV1 { qrcode: qr.into() }, + _ => unreachable!(), + }) + } + + /// Cancel a verification for the given user with the given flow id using + /// the given cancel code. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to cancel the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `cancel_code` - The error code for why the verification was cancelled, + /// manual cancellatio usually happens with `m.user` cancel code. The full + /// list of cancel codes can be found in the [spec] + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel + pub fn cancel_verification( + &self, + user_id: String, + flow_id: String, + cancel_code: String, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + if let Some(request) = self.inner.get_verification_request(&user_id, &flow_id) { + request.cancel().map(|r| r.into()) + } else if let Some(verification) = self.inner.get_verification(&user_id, &flow_id) { + match verification { + RustVerification::SasV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + RustVerification::QrV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + _ => unreachable!(), + } + } else { + None + } + } + + /// Pass data from a scanned QR code to an active verification request and + /// transition into QR code verification. + /// + /// This requires an active `VerificationRequest` to succeed, returns `None` + /// if no `VerificationRequest` is found or if the QR code data is invalid. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// QR code verification. + /// + /// * `flow_id` - The ID of the verification request that initiated the + /// verification flow. + /// + /// * `data` - The data that was extracted from the scanned QR code as an + /// base64 encoded string, without padding. + pub fn scan_qr_code( + &self, + user_id: String, + flow_id: String, + data: String, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + let data = decode_config(data, STANDARD_NO_PAD).ok()?; + let data = QrVerificationData::from_bytes(data).ok()?; + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + if let Some(qr) = self.runtime.block_on(verification.scan_qr_code(data)).ok()? { + let request = qr.reciprocate()?; + + Some(ScanResult { qr: qr.into(), request: request.into() }) + } else { + None + } + } else { + None + } + } + + /// Accept that we're going forward with the short auth string verification. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// SAS verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn accept_sas_verification( + &self, + user_id: String, + flow_id: String, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + self.inner.get_verification(&user_id, &flow_id)?.sas_v1()?.accept().map(|r| r.into()) + } } impl OlmMachine { @@ -822,36 +968,6 @@ impl OlmMachine { self.inner.get_verification_request(&user_id, flow_id).map(|v| v.into()) } - /// Accept a verification requests that we share with the given user with - /// the given flow id. - /// - /// This will move the verification request into the ready state. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to accept the - /// verification requests. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - /// - /// * `methods` - A list of verification methods that we want to advertise - /// as supported. - pub fn accept_verification_request( - &self, - user_id: &str, - flow_id: &str, - methods: Vec, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - let methods = methods.into_iter().map(VerificationMethod::from).collect(); - - if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { - verification.accept_with_methods(methods).map(|r| r.into()) - } else { - None - } - } - /// Get an m.key.verification.request content for the given user. /// /// # Arguments @@ -994,65 +1110,6 @@ impl OlmMachine { }) } - /// Get a verification flow object for the given user with the given flow - /// id. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to fetch the - /// verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - pub fn get_verification(&self, user_id: &str, flow_id: &str) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - self.inner.get_verification(&user_id, flow_id).map(|v| match v { - RustVerification::SasV1(s) => Verification::SasV1 { sas: s.into() }, - RustVerification::QrV1(qr) => Verification::QrCodeV1 { qrcode: qr.into() }, - _ => unreachable!(), - }) - } - - /// Cancel a verification for the given user with the given flow id using - /// the given cancel code. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to cancel the - /// verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - /// - /// * `cancel_code` - The error code for why the verification was cancelled, - /// manual cancellatio usually happens with `m.user` cancel code. The full - /// list of cancel codes can be found in the [spec] - /// - /// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel - pub fn cancel_verification( - &self, - user_id: &str, - flow_id: &str, - cancel_code: &str, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - if let Some(request) = self.inner.get_verification_request(&user_id, flow_id) { - request.cancel().map(|r| r.into()) - } else if let Some(verification) = self.inner.get_verification(&user_id, flow_id) { - match verification { - RustVerification::SasV1(v) => { - v.cancel_with_code(cancel_code.into()).map(|r| r.into()) - } - RustVerification::QrV1(v) => { - v.cancel_with_code(cancel_code.into()).map(|r| r.into()) - } - _ => unreachable!(), - } - } else { - None - } - } - /// Confirm a verification was successful. /// /// This method should be called either if a short auth string should be @@ -1145,40 +1202,6 @@ impl OlmMachine { .and_then(|v| v.qr_v1().and_then(|qr| qr.to_bytes().map(encode).ok())) } - /// Pass data from a scanned QR code to an active verification request and - /// transition into QR code verification. - /// - /// This requires an active `VerificationRequest` to succeed, returns `None` - /// if no `VerificationRequest` is found or if the QR code data is invalid. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to start the - /// QR code verification. - /// - /// * `flow_id` - The ID of the verification request that initiated the - /// verification flow. - /// - /// * `data` - The data that was extracted from the scanned QR code as an - /// base64 encoded string, without padding. - pub fn scan_qr_code(&self, user_id: &str, flow_id: &str, data: &str) -> Option { - let user_id = UserId::parse(user_id).ok()?; - let data = decode_config(data, STANDARD_NO_PAD).ok()?; - let data = QrVerificationData::from_bytes(data).ok()?; - - if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { - if let Some(qr) = self.runtime.block_on(verification.scan_qr_code(data)).ok()? { - let request = qr.reciprocate()?; - - Some(ScanResult { qr: qr.into(), request: request.into() }) - } else { - None - } - } else { - None - } - } - /// Transition from a verification request into short auth string based /// verification. /// @@ -1239,24 +1262,6 @@ impl OlmMachine { ) } - /// Accept that we're going forward with the short auth string verification. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to accept the - /// SAS verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - pub fn accept_sas_verification( - &self, - user_id: &str, - flow_id: &str, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - self.inner.get_verification(&user_id, flow_id)?.sas_v1()?.accept().map(|r| r.into()) - } - /// Get a list of emoji indices of the emoji representation of the short /// auth string. /// diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 8432e36de..9092fc256 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -160,11 +160,6 @@ dictionary Sas { CancelInfo? cancel_info; }; -dictionary ScanResult { - QrCode qr; - OutgoingVerificationRequest request; -}; - dictionary QrCode { string other_user_id; string other_device_id; @@ -205,12 +200,6 @@ dictionary ConfirmVerificationResult { SignatureUploadRequest? signature_request; }; -[Enum] -interface Verification { - SasV1(Sas sas); - QrCodeV1(QrCode qrcode); -}; - dictionary KeyRequestPair { Request? cancellation; Request key_request; @@ -307,7 +296,6 @@ interface OlmMachine { void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id); sequence get_verification_requests([ByRef] string user_id); VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id); - Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id); [Throws=CryptoStoreError] VerificationRequest? request_verification( @@ -330,31 +318,18 @@ interface OlmMachine { sequence methods ); - OutgoingVerificationRequest? accept_verification_request( - [ByRef] string user_id, - [ByRef] string flow_id, - sequence methods - ); - [Throws=CryptoStoreError] ConfirmVerificationResult? confirm_verification([ByRef] string user_id, [ByRef] string flow_id); - OutgoingVerificationRequest? cancel_verification( - [ByRef] string user_id, - [ByRef] string flow_id, - [ByRef] string cancel_code - ); [Throws=CryptoStoreError] StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id); [Throws=CryptoStoreError] StartSasResult? start_sas_verification([ByRef] string user_id, [ByRef] string flow_id); - OutgoingVerificationRequest? accept_sas_verification([ByRef] string user_id, [ByRef] string flow_id); sequence? get_emoji_index([ByRef] string user_id, [ByRef] string flow_id); sequence? get_decimals([ByRef] string user_id, [ByRef] string flow_id); [Throws=CryptoStoreError] QrCode? start_qr_verification([ByRef] string user_id, [ByRef] string flow_id); - ScanResult? scan_qr_code([ByRef] string user_id, [ByRef] string flow_id, [ByRef] string data); string? generate_qr_code([ByRef] string user_id, [ByRef] string flow_id); [Throws=DecryptionError] diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index feb6b89c9..476ef9f70 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -82,6 +82,7 @@ impl From<(RustUploadSigningKeysRequest, RustSignatureUploadRequest)> } } +#[derive(uniffi::Enum)] pub enum OutgoingVerificationRequest { ToDevice { request_id: String, event_type: String, body: String }, InRoom { request_id: String, room_id: String, event_type: String, content: String }, diff --git a/bindings/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs index ad4ce52c6..662d99246 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/verification.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/verification.rs @@ -6,6 +6,7 @@ use matrix_sdk_crypto::{ use crate::{OutgoingVerificationRequest, SignatureUploadRequest}; /// Enum representing the different verification flows we support. +#[derive(uniffi::Enum)] pub enum Verification { /// The `m.sas.v1` verification flow. SasV1 { @@ -21,6 +22,7 @@ pub enum Verification { } /// The `m.sas.v1` verification flow. +#[derive(uniffi::Record)] pub struct Sas { /// The other user that is participating in the verification flow pub other_user_id: String, @@ -53,6 +55,7 @@ pub struct Sas { /// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1` /// verification flow. +#[derive(uniffi::Record)] pub struct QrCode { /// The other user that is participating in the verification flow pub other_user_id: String, @@ -100,6 +103,7 @@ impl From for QrCode { } /// Information on why a verification flow has been cancelled and by whom. +#[derive(uniffi::Record)] pub struct CancelInfo { /// The textual representation of the cancel reason pub reason: String, @@ -129,6 +133,7 @@ pub struct StartSasResult { } /// A result type for scanning QR codes. +#[derive(uniffi::Record)] pub struct ScanResult { /// The QR code verification object that got created. pub qr: QrCode, From 411095425c6ea084ac1c1acce49e1bf2e9d6531c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 13:35:06 +0200 Subject: [PATCH 54/66] refactor(sdk): Make ClientBuilder::{sled_store, indexeddb_store} simple setters --- bindings/matrix-sdk-ffi/src/client_builder.rs | 2 +- crates/matrix-sdk/src/client/builder.rs | 91 +++++++++++++++---- examples/custom_events/src/main.rs | 6 +- examples/getting_started/src/main.rs | 24 ++--- examples/timeline/src/main.rs | 11 +-- .../src/helpers.rs | 1 - 6 files changed, 91 insertions(+), 44 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client_builder.rs b/bindings/matrix-sdk-ffi/src/client_builder.rs index 7a9317d39..b665633b7 100644 --- a/bindings/matrix-sdk-ffi/src/client_builder.rs +++ b/bindings/matrix-sdk-ffi/src/client_builder.rs @@ -74,7 +74,7 @@ impl ClientBuilder { let data_path = PathBuf::from(base_path).join(sanitize(username)); fs::create_dir_all(&data_path)?; - inner_builder = RUNTIME.block_on(inner_builder.sled_store(data_path, None))?; + inner_builder = inner_builder.sled_store(data_path, None); } // Determine server either from URL, server name or user ID. diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index d08ed6299..5b993702a 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::{fmt, sync::Arc}; #[cfg(target_arch = "wasm32")] use async_once_cell::OnceCell; @@ -82,7 +82,7 @@ use crate::{ pub struct ClientBuilder { homeserver_cfg: Option, http_cfg: Option, - store_config: StoreConfig, + store_config: BuilderStoreConfig, request_config: RequestConfig, respect_login_well_known: bool, appservice_mode: bool, @@ -95,7 +95,7 @@ impl ClientBuilder { Self { homeserver_cfg: None, http_cfg: None, - store_config: Default::default(), + store_config: BuilderStoreConfig::Custom(StoreConfig::default()), request_config: Default::default(), respect_login_well_known: true, appservice_mode: false, @@ -126,30 +126,36 @@ impl ClientBuilder { /// Set up the store configuration for a sled store. /// - /// This is a shorthand for + /// This is the same as /// .[store_config](Self::store_config)([matrix_sdk_sled]::[make_store_config](matrix_sdk_sled::make_store_config)(path, passphrase)?). + /// except it delegates the actual store config creation to when + /// `.build().await` is called. #[cfg(feature = "sled")] - pub async fn sled_store( - self, + pub fn sled_store( + mut self, path: impl AsRef, passphrase: Option<&str>, - ) -> Result { - let config = matrix_sdk_sled::make_store_config(path, passphrase).await?; - Ok(self.store_config(config)) + ) -> Self { + self.store_config = BuilderStoreConfig::Sled { + path: path.as_ref().to_owned(), + passphrase: passphrase.map(ToOwned::to_owned), + }; + self } /// Set up the store configuration for a IndexedDB store. /// - /// This is a shorthand for - /// .[store_config](Self::store_config)([matrix_sdk_indexeddb]::[make_store_config](matrix_sdk_indexeddb::make_store_config)(path, passphrase).await?). + /// This is the same as + /// .[store_config](Self::store_config)([matrix_sdk_indexeddb]::[make_store_config](matrix_sdk_indexeddb::make_store_config)(path, passphrase).await?), + /// except it delegates the actual store config creation to when + /// `.build().await` is called. #[cfg(feature = "indexeddb")] - pub async fn indexeddb_store( - self, - name: &str, - passphrase: Option<&str>, - ) -> Result { - let config = matrix_sdk_indexeddb::make_store_config(name, passphrase).await?; - Ok(self.store_config(config)) + pub fn indexeddb_store(mut self, name: &str, passphrase: Option<&str>) -> Self { + self.store_config = BuilderStoreConfig::IndexedDb { + name: name.to_owned(), + passphrase: passphrase.map(ToOwned::to_owned), + }; + self } /// Set up the store configuration. @@ -172,7 +178,7 @@ impl ClientBuilder { /// let client_builder = Client::builder().store_config(store_config); /// ``` pub fn store_config(mut self, store_config: StoreConfig) -> Self { - self.store_config = store_config; + self.store_config = BuilderStoreConfig::Custom(store_config); self } @@ -336,7 +342,20 @@ impl ClientBuilder { HttpConfig::Custom(c) => c, }; - let base_client = BaseClient::with_store_config(self.store_config); + #[allow(clippy::infallible_destructuring_match)] + let store_config = match self.store_config { + #[cfg(feature = "sled")] + BuilderStoreConfig::Sled { path, passphrase } => { + matrix_sdk_sled::make_store_config(&path, passphrase.as_deref()).await? + } + #[cfg(feature = "indexeddb")] + BuilderStoreConfig::IndexedDb { name, passphrase } => { + matrix_sdk_indexeddb::make_store_config(&name, passphrase.as_deref()).await? + } + BuilderStoreConfig::Custom(config) => config, + }; + + let base_client = BaseClient::with_store_config(store_config); let http_client = HttpClient::new(inner_http_client.clone(), self.request_config); let mut authentication_issuer: Option = None; @@ -439,6 +458,38 @@ impl Default for HttpConfig { } } +#[derive(Clone)] +enum BuilderStoreConfig { + #[cfg(feature = "sled")] + Sled { + path: std::path::PathBuf, + passphrase: Option, + }, + #[cfg(feature = "indexeddb")] + IndexedDb { + name: String, + passphrase: Option, + }, + Custom(StoreConfig), +} + +impl fmt::Debug for BuilderStoreConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[allow(clippy::infallible_destructuring_match)] + match self { + #[cfg(feature = "sled")] + Self::Sled { path, .. } => { + f.debug_struct("Sled").field("path", path).finish_non_exhaustive() + } + #[cfg(feature = "indexeddb")] + Self::IndexedDb { name, .. } => { + f.debug_struct("IndexedDb").field("name", name).finish_non_exhaustive() + } + Self::Custom(store_config) => f.debug_tuple("Custom").field(store_config).finish(), + } + } +} + /// Errors that can happen in [`ClientBuilder::build`]. #[derive(Debug, Error)] pub enum ClientBuildError { diff --git a/examples/custom_events/src/main.rs b/examples/custom_events/src/main.rs index ee2a2df74..ff2a32066 100644 --- a/examples/custom_events/src/main.rs +++ b/examples/custom_events/src/main.rs @@ -107,11 +107,9 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - client_builder = client_builder.sled_store(home, None).await?; - let client = client_builder.build().await?; + let client = + Client::builder().homeserver_url(homeserver_url).sled_store(home, None).build().await?; client .login_username(username, password) .initial_device_display_name("getting started bot") diff --git a/examples/getting_started/src/main.rs b/examples/getting_started/src/main.rs index dd6ca5828..162b4ca60 100644 --- a/examples/getting_started/src/main.rs +++ b/examples/getting_started/src/main.rs @@ -60,28 +60,28 @@ async fn login_and_sync( username: &str, password: &str, ) -> anyhow::Result<()> { - // first, we set up the client. We use the convenient client builder to set our - // custom homeserver URL on it - #[allow(unused_mut)] - let mut client_builder = Client::builder().homeserver_url(homeserver_url); + // First, we set up the client. - // Matrix-SDK has support for pluggable, configurable state and crypto-store - // support we use the default sled-store (enabled by default on native - // architectures), to configure a local cache and store for our crypto keys let home = dirs::data_dir().expect("no home directory found").join("getting_started"); - client_builder = client_builder.sled_store(home, None).await?; - // alright, let's make that into a client - let client = client_builder.build().await?; + let client = Client::builder() + // We use the convenient client builder to set our custom homeserver URL on it. + .homeserver_url(homeserver_url) + // Matrix-SDK has support for pluggable, configurable state and crypto-store + // support we use the default sled-store (enabled by default on native + // architectures), to configure a local cache and store for our crypto keys + .sled_store(home, None) + .build() + .await?; - // then let's log that client in + // Then let's log that client in client .login_username(username, password) .initial_device_display_name("getting started bot") .send() .await?; - // it worked! + // It worked! println!("logged in as {username}"); // Now, we want our client to react to invites. Invites sent us stripped member diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index 8f87c86af..c4f5c92b9 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -33,13 +33,12 @@ struct Cli { } async fn login(cli: Cli) -> Result { - let builder = Client::builder() - .homeserver_url(cli.homeserver) - .sled_store("./", Some("some password")) - .await - .unwrap(); + let mut builder = + Client::builder().homeserver_url(cli.homeserver).sled_store("./", Some("some password")); - let builder = if let Some(proxy) = cli.proxy { builder.proxy(proxy) } else { builder }; + if let Some(proxy) = cli.proxy { + builder = builder.proxy(proxy); + } let client = builder.build().await?; diff --git a/testing/matrix-sdk-integration-testing/src/helpers.rs b/testing/matrix-sdk-integration-testing/src/helpers.rs index 622d2de65..97466fb39 100644 --- a/testing/matrix-sdk-integration-testing/src/helpers.rs +++ b/testing/matrix-sdk-integration-testing/src/helpers.rs @@ -42,7 +42,6 @@ pub async fn get_client_for_user(username: String) -> Result { let client = Client::builder() .user_agent("matrix-sdk-integation-tests") .sled_store(tmp_dir.path(), None) - .await? .homeserver_url(homeserver_url) .build() .await?; From e05444554df2794ac0ba83408edbb690b62a95f7 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 14:14:18 +0200 Subject: [PATCH 55/66] chore: Fix typo in comment --- crates/matrix-sdk-base/src/store/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/store/mod.rs b/crates/matrix-sdk-base/src/store/mod.rs index c96f40397..3eee556ea 100644 --- a/crates/matrix-sdk-base/src/store/mod.rs +++ b/crates/matrix-sdk-base/src/store/mod.rs @@ -515,7 +515,7 @@ impl Store { Self::new(inner) } - /// Create a new store, wrappning the given `StateStore` + /// Create a new store, wrapping the given `StateStore` pub fn new(inner: Arc) -> Self { Self { inner, From 1aa48beca9773bb608298ffd9b670611b7a10b21 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 20 Oct 2022 17:14:49 +0200 Subject: [PATCH 56/66] Revert "refactor(bindings): Use new uniffi::Enum derive macro in crypto-ffi" This reverts commit 6b9075aa42dbd30253468a6c41c84cce2a67d06c. --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 7 +- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 287 +++++++++--------- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 25 ++ .../matrix-sdk-crypto-ffi/src/responses.rs | 1 - .../matrix-sdk-crypto-ffi/src/verification.rs | 5 - 5 files changed, 167 insertions(+), 158 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index caa7c4ddc..e35c26ae0 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -474,12 +474,7 @@ fn parse_user_id(user_id: &str) -> Result { } mod uniffi_types { - pub use crate::{ - backup_recovery_key::BackupRecoveryKey, - machine::OlmMachine, - responses::OutgoingVerificationRequest, - verification::{CancelInfo, QrCode, Sas, ScanResult, Verification}, - }; + pub use crate::{backup_recovery_key::BackupRecoveryKey, machine::OlmMachine}; } #[cfg(test)] diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 7418a571a..bd662a831 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -87,152 +87,6 @@ impl OlmMachine { HashMap::from([("ed25519".to_owned(), ed25519_key), ("curve25519".to_owned(), curve_key)]) } - - /// Accept a verification requests that we share with the given user with - /// the given flow id. - /// - /// This will move the verification request into the ready state. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to accept the - /// verification requests. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - /// - /// * `methods` - A list of verification methods that we want to advertise - /// as supported. - pub fn accept_verification_request( - &self, - user_id: String, - flow_id: String, - methods: Vec, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - let methods = methods.into_iter().map(VerificationMethod::from).collect(); - - if let Some(verification) = self.inner.get_verification_request(&user_id, &flow_id) { - verification.accept_with_methods(methods).map(|r| r.into()) - } else { - None - } - } - - /// Get a verification flow object for the given user with the given flow - /// id. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to fetch the - /// verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - pub fn get_verification(&self, user_id: String, flow_id: String) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - self.inner.get_verification(&user_id, &flow_id).map(|v| match v { - RustVerification::SasV1(s) => Verification::SasV1 { sas: s.into() }, - RustVerification::QrV1(qr) => Verification::QrCodeV1 { qrcode: qr.into() }, - _ => unreachable!(), - }) - } - - /// Cancel a verification for the given user with the given flow id using - /// the given cancel code. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to cancel the - /// verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - /// - /// * `cancel_code` - The error code for why the verification was cancelled, - /// manual cancellatio usually happens with `m.user` cancel code. The full - /// list of cancel codes can be found in the [spec] - /// - /// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel - pub fn cancel_verification( - &self, - user_id: String, - flow_id: String, - cancel_code: String, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - if let Some(request) = self.inner.get_verification_request(&user_id, &flow_id) { - request.cancel().map(|r| r.into()) - } else if let Some(verification) = self.inner.get_verification(&user_id, &flow_id) { - match verification { - RustVerification::SasV1(v) => { - v.cancel_with_code(cancel_code.into()).map(|r| r.into()) - } - RustVerification::QrV1(v) => { - v.cancel_with_code(cancel_code.into()).map(|r| r.into()) - } - _ => unreachable!(), - } - } else { - None - } - } - - /// Pass data from a scanned QR code to an active verification request and - /// transition into QR code verification. - /// - /// This requires an active `VerificationRequest` to succeed, returns `None` - /// if no `VerificationRequest` is found or if the QR code data is invalid. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to start the - /// QR code verification. - /// - /// * `flow_id` - The ID of the verification request that initiated the - /// verification flow. - /// - /// * `data` - The data that was extracted from the scanned QR code as an - /// base64 encoded string, without padding. - pub fn scan_qr_code( - &self, - user_id: String, - flow_id: String, - data: String, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - let data = decode_config(data, STANDARD_NO_PAD).ok()?; - let data = QrVerificationData::from_bytes(data).ok()?; - - if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { - if let Some(qr) = self.runtime.block_on(verification.scan_qr_code(data)).ok()? { - let request = qr.reciprocate()?; - - Some(ScanResult { qr: qr.into(), request: request.into() }) - } else { - None - } - } else { - None - } - } - - /// Accept that we're going forward with the short auth string verification. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user for which we would like to accept the - /// SAS verification. - /// - /// * `flow_id` - The ID that uniquely identifies the verification flow. - pub fn accept_sas_verification( - &self, - user_id: String, - flow_id: String, - ) -> Option { - let user_id = UserId::parse(user_id).ok()?; - - self.inner.get_verification(&user_id, &flow_id)?.sas_v1()?.accept().map(|r| r.into()) - } } impl OlmMachine { @@ -968,6 +822,36 @@ impl OlmMachine { self.inner.get_verification_request(&user_id, flow_id).map(|v| v.into()) } + /// Accept a verification requests that we share with the given user with + /// the given flow id. + /// + /// This will move the verification request into the ready state. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// verification requests. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `methods` - A list of verification methods that we want to advertise + /// as supported. + pub fn accept_verification_request( + &self, + user_id: &str, + flow_id: &str, + methods: Vec, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + verification.accept_with_methods(methods).map(|r| r.into()) + } else { + None + } + } + /// Get an m.key.verification.request content for the given user. /// /// # Arguments @@ -1110,6 +994,65 @@ impl OlmMachine { }) } + /// Get a verification flow object for the given user with the given flow + /// id. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to fetch the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_verification(&self, user_id: &str, flow_id: &str) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + self.inner.get_verification(&user_id, flow_id).map(|v| match v { + RustVerification::SasV1(s) => Verification::SasV1 { sas: s.into() }, + RustVerification::QrV1(qr) => Verification::QrCodeV1 { qrcode: qr.into() }, + _ => unreachable!(), + }) + } + + /// Cancel a verification for the given user with the given flow id using + /// the given cancel code. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to cancel the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `cancel_code` - The error code for why the verification was cancelled, + /// manual cancellatio usually happens with `m.user` cancel code. The full + /// list of cancel codes can be found in the [spec] + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel + pub fn cancel_verification( + &self, + user_id: &str, + flow_id: &str, + cancel_code: &str, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + if let Some(request) = self.inner.get_verification_request(&user_id, flow_id) { + request.cancel().map(|r| r.into()) + } else if let Some(verification) = self.inner.get_verification(&user_id, flow_id) { + match verification { + RustVerification::SasV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + RustVerification::QrV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + _ => unreachable!(), + } + } else { + None + } + } + /// Confirm a verification was successful. /// /// This method should be called either if a short auth string should be @@ -1202,6 +1145,40 @@ impl OlmMachine { .and_then(|v| v.qr_v1().and_then(|qr| qr.to_bytes().map(encode).ok())) } + /// Pass data from a scanned QR code to an active verification request and + /// transition into QR code verification. + /// + /// This requires an active `VerificationRequest` to succeed, returns `None` + /// if no `VerificationRequest` is found or if the QR code data is invalid. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// QR code verification. + /// + /// * `flow_id` - The ID of the verification request that initiated the + /// verification flow. + /// + /// * `data` - The data that was extracted from the scanned QR code as an + /// base64 encoded string, without padding. + pub fn scan_qr_code(&self, user_id: &str, flow_id: &str, data: &str) -> Option { + let user_id = UserId::parse(user_id).ok()?; + let data = decode_config(data, STANDARD_NO_PAD).ok()?; + let data = QrVerificationData::from_bytes(data).ok()?; + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + if let Some(qr) = self.runtime.block_on(verification.scan_qr_code(data)).ok()? { + let request = qr.reciprocate()?; + + Some(ScanResult { qr: qr.into(), request: request.into() }) + } else { + None + } + } else { + None + } + } + /// Transition from a verification request into short auth string based /// verification. /// @@ -1262,6 +1239,24 @@ impl OlmMachine { ) } + /// Accept that we're going forward with the short auth string verification. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// SAS verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn accept_sas_verification( + &self, + user_id: &str, + flow_id: &str, + ) -> Option { + let user_id = UserId::parse(user_id).ok()?; + + self.inner.get_verification(&user_id, flow_id)?.sas_v1()?.accept().map(|r| r.into()) + } + /// Get a list of emoji indices of the emoji representation of the short /// auth string. /// diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 9092fc256..8432e36de 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -160,6 +160,11 @@ dictionary Sas { CancelInfo? cancel_info; }; +dictionary ScanResult { + QrCode qr; + OutgoingVerificationRequest request; +}; + dictionary QrCode { string other_user_id; string other_device_id; @@ -200,6 +205,12 @@ dictionary ConfirmVerificationResult { SignatureUploadRequest? signature_request; }; +[Enum] +interface Verification { + SasV1(Sas sas); + QrCodeV1(QrCode qrcode); +}; + dictionary KeyRequestPair { Request? cancellation; Request key_request; @@ -296,6 +307,7 @@ interface OlmMachine { void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id); sequence get_verification_requests([ByRef] string user_id); VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id); + Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id); [Throws=CryptoStoreError] VerificationRequest? request_verification( @@ -318,18 +330,31 @@ interface OlmMachine { sequence methods ); + OutgoingVerificationRequest? accept_verification_request( + [ByRef] string user_id, + [ByRef] string flow_id, + sequence methods + ); + [Throws=CryptoStoreError] ConfirmVerificationResult? confirm_verification([ByRef] string user_id, [ByRef] string flow_id); + OutgoingVerificationRequest? cancel_verification( + [ByRef] string user_id, + [ByRef] string flow_id, + [ByRef] string cancel_code + ); [Throws=CryptoStoreError] StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id); [Throws=CryptoStoreError] StartSasResult? start_sas_verification([ByRef] string user_id, [ByRef] string flow_id); + OutgoingVerificationRequest? accept_sas_verification([ByRef] string user_id, [ByRef] string flow_id); sequence? get_emoji_index([ByRef] string user_id, [ByRef] string flow_id); sequence? get_decimals([ByRef] string user_id, [ByRef] string flow_id); [Throws=CryptoStoreError] QrCode? start_qr_verification([ByRef] string user_id, [ByRef] string flow_id); + ScanResult? scan_qr_code([ByRef] string user_id, [ByRef] string flow_id, [ByRef] string data); string? generate_qr_code([ByRef] string user_id, [ByRef] string flow_id); [Throws=DecryptionError] diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index 476ef9f70..feb6b89c9 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -82,7 +82,6 @@ impl From<(RustUploadSigningKeysRequest, RustSignatureUploadRequest)> } } -#[derive(uniffi::Enum)] pub enum OutgoingVerificationRequest { ToDevice { request_id: String, event_type: String, body: String }, InRoom { request_id: String, room_id: String, event_type: String, content: String }, diff --git a/bindings/matrix-sdk-crypto-ffi/src/verification.rs b/bindings/matrix-sdk-crypto-ffi/src/verification.rs index 662d99246..ad4ce52c6 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/verification.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/verification.rs @@ -6,7 +6,6 @@ use matrix_sdk_crypto::{ use crate::{OutgoingVerificationRequest, SignatureUploadRequest}; /// Enum representing the different verification flows we support. -#[derive(uniffi::Enum)] pub enum Verification { /// The `m.sas.v1` verification flow. SasV1 { @@ -22,7 +21,6 @@ pub enum Verification { } /// The `m.sas.v1` verification flow. -#[derive(uniffi::Record)] pub struct Sas { /// The other user that is participating in the verification flow pub other_user_id: String, @@ -55,7 +53,6 @@ pub struct Sas { /// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1` /// verification flow. -#[derive(uniffi::Record)] pub struct QrCode { /// The other user that is participating in the verification flow pub other_user_id: String, @@ -103,7 +100,6 @@ impl From for QrCode { } /// Information on why a verification flow has been cancelled and by whom. -#[derive(uniffi::Record)] pub struct CancelInfo { /// The textual representation of the cancel reason pub reason: String, @@ -133,7 +129,6 @@ pub struct StartSasResult { } /// A result type for scanning QR codes. -#[derive(uniffi::Record)] pub struct ScanResult { /// The QR code verification object that got created. pub qr: QrCode, From 4a6208f8088ae30f1cbfe5ff38a45b10b0b98b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 18 Oct 2022 13:28:06 +0200 Subject: [PATCH 57/66] feat(crypto): Add signaling to the SAS verification This patch adds a way for users to listen to changes in the state of a SAS verification. This makes it much more pleasant to go through the verification flow and incidentally easier to document it. --- Cargo.lock | 107 +++-- crates/matrix-sdk-crypto/Cargo.toml | 3 + crates/matrix-sdk-crypto/src/lib.rs | 3 +- .../matrix-sdk-crypto/src/verification/mod.rs | 2 +- .../src/verification/sas/mod.rs | 387 ++++++++++++++++-- .../src/verification/sas/sas_state.rs | 39 +- .../src/encryption/verification/mod.rs | 5 +- .../src/encryption/verification/sas.rs | 109 ++++- examples/emoji_verification/Cargo.toml | 9 +- examples/emoji_verification/src/main.rs | 141 +++---- examples/timeline/Cargo.toml | 2 +- 11 files changed, 616 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c2bf8de6..c829ffaf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "anymap2" @@ -648,8 +648,8 @@ checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", - "clap_derive", - "clap_lex", + "clap_derive 3.2.18", + "clap_lex 0.2.4", "indexmap", "once_cell", "strsim 0.10.0", @@ -657,6 +657,21 @@ dependencies = [ "textwrap 0.15.1", ] +[[package]] +name = "clap" +version = "4.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef582e2c00a63a0c0aa1fb4a4870781c4f5729f51196d3537fa7c1c1992eaa3" +dependencies = [ + "atty", + "bitflags", + "clap_derive 4.0.13", + "clap_lex 0.3.0", + "once_cell", + "strsim 0.10.0", + "termcolor", +] + [[package]] name = "clap_derive" version = "3.2.18" @@ -670,6 +685,19 @@ dependencies = [ "syn", ] +[[package]] +name = "clap_derive" +version = "4.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -679,6 +707,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard-win" version = "4.4.2" @@ -1345,7 +1382,8 @@ name = "example-emoji-verification" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.22", + "clap 4.0.16", + "futures", "matrix-sdk", "tokio", "tracing-subscriber", @@ -1402,7 +1440,7 @@ name = "example-timeline" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.22", + "clap 4.0.16", "futures", "futures-signals", "matrix-sdk", @@ -1507,11 +1545,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -1613,9 +1650,9 @@ dependencies = [ [[package]] name = "futures-signals" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6874899c7dc7416c6b30f13b62fb63098671e29ccf3999987270258114f2938" +checksum = "a3acc659ba666cff13fdf65242d16428f2f11935b688f82e4024ad39667a5132" dependencies = [ "discard", "futures-channel", @@ -1949,11 +1986,10 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -2451,6 +2487,7 @@ name = "matrix-sdk-crypto" version = "0.6.0" dependencies = [ "aes", + "anyhow", "async-trait", "atomic", "base64", @@ -2461,6 +2498,8 @@ dependencies = [ "dashmap", "event-listener", "futures", + "futures-core", + "futures-signals", "futures-util", "hmac", "http", @@ -2901,6 +2940,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-format" version = "0.4.0" @@ -3083,6 +3132,12 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.0.0" @@ -3168,9 +3223,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" @@ -4511,9 +4566,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -4521,7 +4576,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "pin-project-lite", "socket2", "tokio-macros", @@ -4679,9 +4733,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -4711,12 +4765,12 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -4939,13 +4993,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", "serde", ] diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 9a7dbd106..c4e37cc48 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -34,7 +34,9 @@ byteorder = "1.4.3" ctr = "0.9.1" dashmap = "5.2.0" event-listener = "2.5.2" +futures-core = "0.3.24" futures-util = { version = "0.3.21", default-features = false, features = ["alloc"] } +futures-signals = { version = "0.3.31", default-features = false } hmac = "0.12.1" http = { version = "0.2.6", optional = true } # feature = testing only matrix-sdk-qrcode = { version = "0.4.0", path = "../matrix-sdk-qrcode", optional = true } @@ -56,6 +58,7 @@ cfg-if = "1.0" tokio = { version = "1.18", default-features = false, features = ["time"] } [dev-dependencies] +anyhow = "1.0.65" futures = { version = "0.3.21", default-features = false, features = ["executor"] } http = "0.2.6" indoc = "1.0.4" diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index eb5c40b20..7c44c785c 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -88,7 +88,8 @@ pub use requests::{ }; pub use store::{CrossSigningKeyExport, CryptoStoreError, SecretImportError, SecretInfo}; pub use verification::{ - format_emojis, AcceptSettings, CancelInfo, Emoji, Sas, Verification, VerificationRequest, + format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, Sas, + SasState, Verification, VerificationRequest, }; #[cfg(feature = "qrcode")] pub use verification::{QrVerification, ScanError}; diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index afa457c37..7799b1d17 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -47,7 +47,7 @@ use ruma::{ DeviceId, EventId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, UserId, }; -pub use sas::{AcceptSettings, Sas}; +pub use sas::{AcceptSettings, AcceptedProtocols, EmojiShortAuthString, Sas, SasState}; use tracing::{error, info, trace, warn}; use crate::{ diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index ab802967b..ec36f3189 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -18,17 +18,18 @@ mod sas_state; use std::sync::{Arc, Mutex}; +use futures_core::Stream; +use futures_signals::signal::{Mutable, SignalExt}; use inner_sas::InnerSas; -#[cfg(test)] -use matrix_sdk_common::instant::Instant; use ruma::{ api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest, events::{ - key::verification::{cancel::CancelCode, ShortAuthenticationString}, + key::verification::{cancel::CancelCode, start::SasV1Content, ShortAuthenticationString}, AnyMessageLikeEventContent, AnyToDeviceEventContent, }, DeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, TransactionId, UserId, }; +pub use sas_state::AcceptedProtocols; use tracing::trace; use super::{ @@ -47,6 +48,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct Sas { inner: Arc>, + state: Arc>, account: ReadOnlyAccount, identities_being_verified: IdentitiesBeingVerified, flow_id: Arc, @@ -54,6 +56,133 @@ pub struct Sas { request_handle: Option, } +/// The short auth string for the emoji method of SAS verification. +#[derive(Debug, Clone)] +pub struct EmojiShortAuthString { + /// A list of seven indices that should be used for the SAS verification. + /// + /// The indices can be put into the emoji table in the [spec] to figure out + /// the symbols and descriptions. + /// + /// If you have a table of [translated descriptions] for the emojis you will + /// want to use this field. + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji + /// [translated descriptions]: https://github.com/matrix-org/matrix-doc/blob/master/data-definitions/ + pub indices: [u8; 7], + + /// A list of seven emojis that should be used for the SAS verification. + pub emojis: [Emoji; 7], +} + +/// An Enum describing the state the SAS verification is in. +#[derive(Debug, Clone)] +pub enum SasState { + /// The verification has been started, the protocols that should be used + /// have been proposed and can be accepted. + Started { + /// The protocols that were proposed in the `m.key.verification.start` + /// event. + protocols: SasV1Content, + }, + /// The verification has been accepted and both sides agreed to a set of + /// protocols that will be used for the verification process. + Accepted { + /// The protocols that were accepted in the `m.key.verification.accept` + /// event. + accepted_protocols: AcceptedProtocols, + }, + /// The public keys have been exchanged and the short auth string can be + /// presented to the user. + KeysExchanged { + /// The emojis that represent the short auth string, will be `None` if + /// the emoji SAS method wasn't part of the [`AcceptedProtocols`]. + emojis: Option, + /// The list of decimals that represent the short auth string. + decimals: (u16, u16, u16), + }, + /// The verification process has been confirmed from our side, we're waiting + /// for the other side to confirm as well. + Confirmed, + /// The verification process has been successfully concluded. + Done { + /// The list of devices that has been verified. + verified_devices: Vec, + /// The list of user identities that has been verified. + verified_identities: Vec, + }, + /// The verification process has been cancelled. + Cancelled(CancelInfo), +} + +impl PartialEq for SasState { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Self::Started { .. }, Self::Started { .. }) + | (Self::Accepted { .. }, Self::Accepted { .. }) + | (Self::KeysExchanged { .. }, Self::KeysExchanged { .. }) + | (Self::Confirmed, Self::Confirmed) + | (Self::Done { .. }, Self::Done { .. }) + | (Self::Cancelled(_), Self::Cancelled(_)) + ) + } +} + +impl From<&InnerSas> for SasState { + fn from(value: &InnerSas) -> Self { + match value { + InnerSas::Created(s) => { + Self::Started { protocols: s.state.protocol_definitions.to_owned() } + } + InnerSas::Started(s) => { + Self::Started { protocols: s.state.protocol_definitions.to_owned() } + } + InnerSas::Accepted(s) => { + Self::Accepted { accepted_protocols: s.state.accepted_protocols.to_owned() } + } + InnerSas::WeAccepted(s) => { + Self::Accepted { accepted_protocols: s.state.accepted_protocols.to_owned() } + } + InnerSas::KeyReceived(s) => { + let emojis = if value.supports_emoji() { + let emojis = s.get_emoji(); + let indices = s.get_emoji_index(); + + Some(EmojiShortAuthString { emojis, indices }) + } else { + None + }; + + let decimals = s.get_decimal(); + + Self::KeysExchanged { emojis, decimals } + } + InnerSas::MacReceived(s) => { + let emojis = if value.supports_emoji() { + let emojis = s.get_emoji(); + let indices = s.get_emoji_index(); + + Some(EmojiShortAuthString { emojis, indices }) + } else { + None + }; + + let decimals = s.get_decimal(); + + Self::KeysExchanged { emojis, decimals } + } + InnerSas::Confirmed(_) => Self::Confirmed, + InnerSas::WaitingForDone(_) => Self::Confirmed, + InnerSas::Done(s) => Self::Done { + verified_devices: s.verified_devices().to_vec(), + verified_identities: s.verified_identities().to_vec(), + }, + InnerSas::Cancelled(c) => Self::Cancelled(c.state.as_ref().clone().into()), + } + } +} + impl Sas { /// Get our own user id. pub fn user_id(&self) -> &UserId { @@ -137,7 +266,7 @@ impl Sas { #[cfg(test)] #[allow(dead_code)] - pub(crate) fn set_creation_time(&self, time: Instant) { + pub(crate) fn set_creation_time(&self, time: matrix_sdk_common::instant::Instant) { self.inner.lock().unwrap().set_creation_time(time) } @@ -156,11 +285,13 @@ 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(), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -241,10 +372,12 @@ 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(), account, identities_being_verified: identities, flow_id: flow_id.into(), @@ -271,28 +404,41 @@ impl Sas { &self, settings: AcceptSettings, ) -> Option { - let mut guard = self.inner.lock().unwrap(); - let sas: InnerSas = (*guard).clone(); - let methods = settings.allowed_methods; + let (request, state) = { + let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); + let methods = settings.allowed_methods; - if let Some((sas, content)) = sas.accept(methods) { - *guard = sas; + if let Some((sas, content)) = sas.accept(methods) { + let state: SasState = (&sas).into(); - 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 + *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), + ) + } else { + (None, None) + } + }; + + if let Some(new_state) = state { + self.update_state(new_state); } + + request } /// Confirm the Sas verification. @@ -306,13 +452,15 @@ impl Sas { &self, ) -> Result<(Vec, Option), CryptoStoreError> { - let (contents, done) = { + let (contents, done, state) = { let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); let (sas, contents) = sas.confirm(); + let state: SasState = (&sas).into(); *guard = sas; - (contents, guard.is_done()) + (contents, guard.is_done(), state) }; let mac_requests = contents @@ -339,10 +487,17 @@ impl Sas { VerificationResult::Cancel(c) => { Ok((self.cancel_with_code(c).into_iter().collect(), None)) } - VerificationResult::Ok => Ok((mac_requests, None)), - VerificationResult::SignatureUpload(r) => Ok((mac_requests, Some(r))), + VerificationResult::Ok => { + self.update_state(state); + Ok((mac_requests, None)) + } + VerificationResult::SignatureUpload(r) => { + self.update_state(state); + Ok((mac_requests, Some(r))) + } } } else { + self.update_state(state); Ok((mac_requests, None)) } } @@ -377,21 +532,32 @@ impl Sas { /// /// [`cancel()`]: #method.cancel pub fn cancel_with_code(&self, code: CancelCode) -> Option { - let mut guard = self.inner.lock().unwrap(); + let (content, state) = { + let mut guard = self.inner.lock().unwrap(); - if let Some(request) = &self.request_handle { - request.cancel_with_code(&code); - } - - let sas: InnerSas = (*guard).clone(); - let (sas, content) = sas.cancel(true, code); - *guard = sas; - content.map(|c| match c { - OutgoingContent::Room(room_id, content) => { - RoomMessageRequest { room_id, txn_id: TransactionId::new(), content }.into() + if let Some(request) = &self.request_handle { + request.cancel_with_code(&code); } - OutgoingContent::ToDevice(c) => self.content_to_request(c).into(), - }) + + 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, + ) + }; + + self.update_state(state); + + content } pub(crate) fn cancel_if_timed_out(&self) -> Option { @@ -451,15 +617,132 @@ impl Sas { self.inner.lock().unwrap().decimals() } + /// Listen for changes in the SAS verification process. + /// + /// The changes are presented as a stream of [`SasState`] values. + /// + /// This method can be used to react to changes in the state of the + /// verification process, or rather the method can be used to handle + /// each step of the verification process. + /// + /// # Flowchart + /// + /// The flow of the verification process is pictured bellow. Please note + /// that the process can be cancelled at each step of the process. + /// Either side can cancel the process. + /// + /// ```text + /// ┌───────┐ + /// │Started│ + /// └───┬───┘ + /// │ + /// ┌────⌄───┐ + /// │Accepted│ + /// └────┬───┘ + /// │ + /// ┌───────⌄──────┐ + /// │Keys Exchanged│ + /// └───────┬──────┘ + /// │ + /// ________⌄________ + /// ╱ ╲ ┌─────────┐ + /// ╱ Does the short ╲______│Cancelled│ + /// ╲ auth string match ╱ no └─────────┘ + /// ╲_________________╱ + /// │yes + /// │ + /// ┌────⌄────┐ + /// │Confirmed│ + /// └────┬────┘ + /// │ + /// ┌───⌄───┐ + /// │ Done │ + /// └───────┘ + /// ``` + /// # Example + /// + /// ```no_run + /// use futures::stream::{Stream, StreamExt}; + /// use matrix_sdk_crypto::{Sas, SasState}; + /// + /// # futures::executor::block_on(async { + /// # let sas: Sas = unimplemented!(); + /// + /// while let Some(state) = sas.changes().next().await { + /// match state { + /// SasState::KeysExchanged { emojis, decimals: _ } => { + /// let emojis = + /// emojis.expect("We only support emoji verification"); + /// println!("Do these emojis match {emojis:#?}"); + /// + /// // Ask the user to confirm or cancel here. + /// } + /// SasState::Done { .. } => { + /// let device = sas.other_device(); + /// + /// println!( + /// "Successfully verified device {} {} {:?}", + /// device.user_id(), + /// device.device_id(), + /// device.local_trust_state() + /// ); + /// + /// break; + /// } + /// SasState::Cancelled(cancel_info) => { + /// println!( + /// "The verification has been cancelled, reason: {}", + /// cancel_info.reason() + /// ); + /// break; + /// } + /// SasState::Started { .. } + /// | SasState::Accepted { .. } + /// | SasState::Confirmed => (), + /// } + /// } + /// # anyhow::Ok(()) }); + /// ``` + pub fn changes(&self) -> impl Stream { + self.state.signal_cloned().to_stream() + } + + /// 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; + } + } + pub(crate) fn receive_any_event( &self, sender: &UserId, content: &AnyVerificationContent<'_>, ) -> Option { - let mut guard = self.inner.lock().unwrap(); - let sas: InnerSas = (*guard).clone(); - let (sas, content) = sas.receive_any_event(sender, content); - *guard = sas; + let (content, state) = { + let mut guard = self.inner.lock().unwrap(); + let sas: InnerSas = (*guard).clone(); + let (sas, content) = sas.receive_any_event(sender, content); + + let state: SasState = (&sas).into(); + + *guard = sas; + + (content, state) + }; + + self.update_state(state); content } @@ -527,7 +810,7 @@ mod tests { event_enums::{AcceptContent, KeyContent, MacContent, OutgoingContent, StartContent}, VerificationStore, }, - ReadOnlyAccount, ReadOnlyDevice, + ReadOnlyAccount, ReadOnlyDevice, SasState, }; fn alice_id() -> &'static UserId { @@ -573,18 +856,25 @@ mod tests { let (alice, content) = Sas::start(identities, TransactionId::new(), true, None); + matches!(alice.state(), SasState::Started { .. }); + let flow_id = alice.flow_id().to_owned(); let content = StartContent::try_from(&content).unwrap(); let identities = bob_store.get_identities(alice_device).await.unwrap(); let bob = Sas::from_start_event(flow_id, &content, identities, None, false).unwrap(); + matches!(bob.state(), SasState::Started { .. }); + let request = bob.accept().unwrap(); + let content = OutgoingContent::try_from(request).unwrap(); let content = AcceptContent::try_from(&content).unwrap(); let content = alice.receive_any_event(bob.user_id(), &content.into()).unwrap(); + matches!(alice.state(), SasState::Accepted { .. }); + matches!(bob.state(), SasState::Accepted { .. }); assert!(!alice.can_be_presented()); assert!(!bob.can_be_presented()); @@ -592,22 +882,27 @@ mod tests { let content = bob.receive_any_event(alice.user_id(), &content.into()).unwrap(); assert!(bob.can_be_presented()); + matches!(bob.state(), SasState::KeysExchanged { .. }); let content = KeyContent::try_from(&content).unwrap(); alice.receive_any_event(bob.user_id(), &content.into()); + matches!(alice.state(), SasState::KeysExchanged { .. }); assert!(alice.can_be_presented()); assert_eq!(alice.emoji().unwrap(), bob.emoji().unwrap()); assert_eq!(alice.decimals().unwrap(), bob.decimals().unwrap()); let mut requests = alice.confirm().await.unwrap().0; + matches!(alice.state(), SasState::Confirmed); assert!(requests.len() == 1); let request = requests.pop().unwrap(); let content = OutgoingContent::try_from(request).unwrap(); let content = MacContent::try_from(&content).unwrap(); bob.receive_any_event(alice.user_id(), &content.into()); + matches!(bob.state(), SasState::KeysExchanged { .. }); let mut requests = bob.confirm().await.unwrap().0; + matches!(bob.state(), SasState::Confirmed); assert!(requests.len() == 1); let request = requests.pop().unwrap(); let content = OutgoingContent::try_from(request).unwrap(); @@ -616,5 +911,7 @@ mod tests { assert!(alice.verified_devices().unwrap().contains(alice.other_device())); assert!(bob.verified_devices().unwrap().contains(bob.other_device())); + matches!(alice.state(), SasState::Done { .. }); + matches!(bob.state(), SasState::Done { .. }); } } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs index ac72d0b35..5223c123d 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/sas_state.rs @@ -34,7 +34,7 @@ use ruma::{ ToDeviceKeyVerificationStartEventContent, }, HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, Relation, - ShortAuthenticationString, VerificationMethod, + ShortAuthenticationString, }, AnyMessageLikeEventContent, AnyToDeviceEventContent, }, @@ -92,12 +92,16 @@ const MAX_EVENT_TIMEOUT: Duration = Duration::from_secs(60); /// Struct containing the protocols that were agreed to be used for the SAS /// flow. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct AcceptedProtocols { - pub method: VerificationMethod, + /// The key agreement protocol the device is choosing to use. pub key_agreement_protocol: KeyAgreementProtocol, + /// The hash method the device is choosing to use. pub hash: HashAlgorithm, + /// The message authentication code the device is choosing to use pub message_auth_code: MessageAuthenticationCode, + /// The SAS methods both devices involved in the verification process + /// understand. pub short_auth_string: Vec, } @@ -116,7 +120,6 @@ impl TryFrom for AcceptedProtocols { Err(CancelCode::UnknownMethod) } else { Ok(Self { - method: VerificationMethod::SasV1, hash: content.hash, key_agreement_protocol: content.key_agreement_protocol, message_auth_code: content.message_authentication_code, @@ -163,7 +166,6 @@ impl TryFrom<&SasV1Content> for AcceptedProtocols { } Ok(Self { - method: VerificationMethod::SasV1, hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519HkdfSha256, message_auth_code: MessageAuthenticationCode::HkdfHmacSha256, @@ -177,7 +179,6 @@ impl TryFrom<&SasV1Content> for AcceptedProtocols { impl Default for AcceptedProtocols { fn default() -> Self { AcceptedProtocols { - method: VerificationMethod::SasV1, hash: HashAlgorithm::Sha256, key_agreement_protocol: KeyAgreementProtocol::Curve25519HkdfSha256, message_auth_code: MessageAuthenticationCode::HkdfHmacSha256, @@ -239,21 +240,22 @@ impl std::fmt::Debug for SasState { /// The initial SAS state. #[derive(Clone, Debug)] pub struct Created { - protocol_definitions: SasV1Content, + pub protocol_definitions: SasV1Content, } /// The initial SAS state if the other side started the SAS verification. #[derive(Clone, Debug)] pub struct Started { commitment: Base64, - pub accepted_protocols: Arc, + pub protocol_definitions: SasV1Content, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after the other side accepted our /// verification start event. #[derive(Clone, Debug)] pub struct Accepted { - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, start_content: Arc, commitment: Base64, } @@ -263,7 +265,7 @@ pub struct Accepted { #[derive(Clone, Debug)] pub struct WeAccepted { we_started: bool, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, commitment: Base64, } @@ -275,7 +277,7 @@ pub struct WeAccepted { pub struct KeyReceived { sas: Arc>, we_started: bool, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after the user has confirmed that the @@ -284,7 +286,7 @@ pub struct KeyReceived { #[derive(Clone, Debug)] pub struct Confirmed { sas: Arc>, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after we receive a MAC event from the @@ -296,7 +298,7 @@ pub struct MacReceived { we_started: bool, verified_devices: Arc<[ReadOnlyDevice]>, verified_master_keys: Arc<[ReadOnlyUserIdentities]>, - pub accepted_protocols: Arc, + pub accepted_protocols: AcceptedProtocols, } /// The SAS state we're going to be in after we receive a MAC event in a DM. DMs @@ -485,7 +487,7 @@ impl SasState { state: Arc::new(Accepted { start_content, commitment: content.commitment.clone(), - accepted_protocols: accepted_protocols.into(), + accepted_protocols, }), }) } else { @@ -565,7 +567,8 @@ impl SasState { verification_flow_id: flow_id, state: Arc::new(Started { - accepted_protocols: accepted_protocols.into(), + protocol_definitions: method_content.to_owned(), + accepted_protocols, commitment, }), }) @@ -578,7 +581,7 @@ impl SasState { } pub fn into_we_accepted(self, methods: Vec) -> SasState { - let mut accepted_protocols = self.state.accepted_protocols.as_ref().to_owned(); + let mut accepted_protocols = self.state.accepted_protocols.to_owned(); accepted_protocols.short_auth_string = methods; // Decimal is required per spec. @@ -596,7 +599,7 @@ impl SasState { started_from_request: self.started_from_request, state: Arc::new(WeAccepted { we_started: false, - accepted_protocols: accepted_protocols.into(), + accepted_protocols, commitment: self.state.commitment.clone(), }), } @@ -658,7 +661,7 @@ impl SasState { state: Arc::new(Accepted { start_content, commitment: content.commitment.clone(), - accepted_protocols: accepted_protocols.into(), + accepted_protocols, }), }) } else { diff --git a/crates/matrix-sdk/src/encryption/verification/mod.rs b/crates/matrix-sdk/src/encryption/verification/mod.rs index 8209ef63c..6de95f3bf 100644 --- a/crates/matrix-sdk/src/encryption/verification/mod.rs +++ b/crates/matrix-sdk/src/encryption/verification/mod.rs @@ -35,7 +35,10 @@ mod qrcode; mod requests; mod sas; -pub use matrix_sdk_base::crypto::{format_emojis, AcceptSettings, CancelInfo, Emoji}; +pub use matrix_sdk_base::crypto::{ + format_emojis, AcceptSettings, AcceptedProtocols, CancelInfo, Emoji, EmojiShortAuthString, + SasState, +}; #[cfg(feature = "qrcode")] pub use matrix_sdk_base::crypto::{ matrix_sdk_qrcode::{DecodingError, EncodingError, QrVerificationData}, diff --git a/crates/matrix-sdk/src/encryption/verification/sas.rs b/crates/matrix-sdk/src/encryption/verification/sas.rs index 9618daed8..176ac2d0a 100644 --- a/crates/matrix-sdk/src/encryption/verification/sas.rs +++ b/crates/matrix-sdk/src/encryption/verification/sas.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use matrix_sdk_base::crypto::{AcceptSettings, CancelInfo, Emoji, ReadOnlyDevice, Sas as BaseSas}; +use futures_core::Stream; +use matrix_sdk_base::crypto::{ + AcceptSettings, CancelInfo, Emoji, ReadOnlyDevice, Sas as BaseSas, SasState, +}; use ruma::{events::key::verification::cancel::CancelCode, UserId}; use crate::{error::Result, Client}; @@ -217,4 +220,108 @@ impl SasVerification { pub fn other_user_id(&self) -> &UserId { self.inner.other_user_id() } + + /// Listen for changes in the SAS verification process. + /// + /// The changes are presented as a stream of [`SasState`] values. + /// + /// This method can be used to react to changes in the state of the + /// verification process, or rather the method can be used to handle + /// each step of the verification process. + /// + /// # Flowchart + /// + /// The flow of the verification process is pictured bellow. Please note + /// that the process can be cancelled at each step of the process. + /// Either side can cancel the process. + /// + /// ```text + /// ┌───────┐ + /// │Started│ + /// └───┬───┘ + /// │ + /// ┌────⌄───┐ + /// │Accepted│ + /// └────┬───┘ + /// │ + /// ┌───────⌄──────┐ + /// │Keys Exchanged│ + /// └───────┬──────┘ + /// │ + /// ________⌄________ + /// ╱ ╲ ┌─────────┐ + /// ╱ Does the short ╲______│Cancelled│ + /// ╲ auth string match ╱ no └─────────┘ + /// ╲_________________╱ + /// │yes + /// │ + /// ┌────⌄────┐ + /// │Confirmed│ + /// └────┬────┘ + /// │ + /// ┌───⌄───┐ + /// │ Done │ + /// └───────┘ + /// ``` + /// # Example + /// + /// ```no_run + /// use futures::stream::{Stream, StreamExt}; + /// use matrix_sdk::encryption::verification::{SasState, SasVerification}; + /// + /// # futures::executor::block_on(async { + /// # let sas: SasVerification = unimplemented!(); + /// # let user_confirmed = false; + /// + /// while let Some(state) = sas.changes().next().await { + /// match state { + /// SasState::KeysExchanged { emojis, decimals: _ } => { + /// let emojis = + /// emojis.expect("We only support emoji verification"); + /// println!("Do these emojis match {emojis:#?}"); + /// + /// // Ask the user to confirm or cancel here. + /// if user_confirmed { + /// sas.confirm().await?; + /// } else { + /// sas.cancel().await?; + /// } + /// } + /// SasState::Done { .. } => { + /// let device = sas.other_device(); + /// + /// println!( + /// "Successfully verified device {} {} {:?}", + /// device.user_id(), + /// device.device_id(), + /// device.local_trust_state() + /// ); + /// + /// break; + /// } + /// SasState::Cancelled(cancel_info) => { + /// println!( + /// "The verification has been cancelled, reason: {}", + /// cancel_info.reason() + /// ); + /// break; + /// } + /// SasState::Started { .. } + /// | SasState::Accepted { .. } + /// | SasState::Confirmed => (), + /// } + /// } + /// # anyhow::Ok(()) }); + /// ``` + pub fn changes(&self) -> impl Stream { + self.inner.changes() + } + + /// Get the current state the verification process is in. + /// + /// To listen to changes to the [`SasState`] use the + /// [`SasVerification::changes`] method. + pub fn state(&self) -> SasState { + self.inner.state() + } } diff --git a/examples/emoji_verification/Cargo.toml b/examples/emoji_verification/Cargo.toml index 5aff532ab..b7e5b4637 100644 --- a/examples/emoji_verification/Cargo.toml +++ b/examples/emoji_verification/Cargo.toml @@ -10,10 +10,11 @@ test = false [dependencies] anyhow = "1" -tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } -clap = { version = "3.2.20", features = ["derive"] } -tracing-subscriber = "0.3.15" -url = "2.2.2" +tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } +clap = { version = "4.0.15", features = ["derive"] } +futures = "0.3.24" +tracing-subscriber = "0.3.16" +url = "2.3.1" [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" diff --git a/examples/emoji_verification/src/main.rs b/examples/emoji_verification/src/main.rs index 051b2ecaa..6ca0bb6e3 100644 --- a/examples/emoji_verification/src/main.rs +++ b/examples/emoji_verification/src/main.rs @@ -1,16 +1,14 @@ -use std::io::{self, Write}; +use std::io::Write; use anyhow::Result; use clap::Parser; +use futures::stream::StreamExt; use matrix_sdk::{ - self, config::SyncSettings, - encryption::verification::{format_emojis, SasVerification, Verification}, + encryption::verification::{format_emojis, Emoji, SasState, SasVerification, Verification}, ruma::{ events::{ key::verification::{ - done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, - key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, request::ToDeviceKeyVerificationRequestEvent, start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, }, @@ -22,40 +20,20 @@ use matrix_sdk::{ }; use url::Url; -async fn wait_for_confirmation(client: Client, sas: SasVerification) { - let emoji = sas.emoji().expect("The emoji should be available now"); - +async fn wait_for_confirmation(sas: SasVerification, emoji: [Emoji; 7]) { println!("\nDo the emojis match: \n{}", format_emojis(emoji)); print!("Confirm with `yes` or cancel with `no`: "); std::io::stdout().flush().expect("We should be able to flush stdout"); let mut input = String::new(); - io::stdin().read_line(&mut input).expect("error: unable to read user input"); + std::io::stdin().read_line(&mut input).expect("error: unable to read user input"); match input.trim().to_lowercase().as_ref() { - "yes" | "true" | "ok" => { - sas.confirm().await.unwrap(); - - if sas.is_done() { - print_result(&sas); - print_devices(sas.other_device().user_id(), &client).await; - } - } + "yes" | "true" | "ok" => sas.confirm().await.unwrap(), _ => sas.cancel().await.unwrap(), } } -fn print_result(sas: &SasVerification) { - let device = sas.other_device(); - - println!( - "Successfully verified device {} {} {:?}", - device.user_id(), - device.device_id(), - device.local_trust_state() - ); -} - async fn print_devices(user_id: &UserId, client: &Client) { println!("Devices of user {}", user_id); @@ -69,6 +47,47 @@ async fn print_devices(user_id: &UserId, client: &Client) { } } +async fn sas_verification_handler(client: Client, sas: SasVerification) { + println!( + "Starting verification with {} {}", + &sas.other_device().user_id(), + &sas.other_device().device_id() + ); + print_devices(sas.other_device().user_id(), &client).await; + sas.accept().await.unwrap(); + + while let Some(state) = sas.changes().next().await { + match state { + SasState::KeysExchanged { emojis, decimals: _ } => { + tokio::spawn(wait_for_confirmation( + sas.clone(), + emojis.expect("We only support verifications using emojis").emojis, + )); + } + SasState::Done { .. } => { + let device = sas.other_device(); + + println!( + "Successfully verified device {} {} {:?}", + device.user_id(), + device.device_id(), + device.local_trust_state() + ); + + print_devices(sas.other_device().user_id(), &client).await; + + break; + } + SasState::Cancelled(cancel_info) => { + println!("The verification has been cancelled, reason: {}", cancel_info.reason()); + + break; + } + SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (), + } + } +} + async fn sync(client: Client) -> matrix_sdk::Result<()> { client.add_event_handler( |ev: ToDeviceKeyVerificationRequestEvent, client: Client| async move { @@ -88,36 +107,7 @@ async fn sync(client: Client) -> matrix_sdk::Result<()> { .get_verification(&ev.sender, ev.content.transaction_id.as_str()) .await { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }); - - client.add_event_handler(|ev: ToDeviceKeyVerificationKeyEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client, sas)); - } - }); - - client.add_event_handler(|ev: ToDeviceKeyVerificationDoneEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.transaction_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } + tokio::spawn(sas_verification_handler(client, sas)); } }); @@ -140,40 +130,7 @@ async fn sync(client: Client) -> matrix_sdk::Result<()> { .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) .await { - println!( - "Starting verification with {} {}", - &sas.other_device().user_id(), - &sas.other_device().device_id() - ); - print_devices(&ev.sender, &client).await; - sas.accept().await.unwrap(); - } - }, - ); - - client.add_event_handler( - |ev: OriginalSyncKeyVerificationKeyEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - tokio::spawn(wait_for_confirmation(client.clone(), sas)); - } - }, - ); - - client.add_event_handler( - |ev: OriginalSyncKeyVerificationDoneEvent, client: Client| async move { - if let Some(Verification::SasV1(sas)) = client - .encryption() - .get_verification(&ev.sender, ev.content.relates_to.event_id.as_str()) - .await - { - if sas.is_done() { - print_result(&sas); - print_devices(&ev.sender, &client).await; - } + tokio::spawn(sas_verification_handler(client, sas)); } }, ); diff --git a/examples/timeline/Cargo.toml b/examples/timeline/Cargo.toml index 56bef7e00..0a0697c9e 100644 --- a/examples/timeline/Cargo.toml +++ b/examples/timeline/Cargo.toml @@ -10,7 +10,7 @@ test = false [dependencies] anyhow = "1" -clap = "3.2.22" +clap = "4.0.16" futures = "0.3" futures-signals = { version = "0.3.30", default-features = false } tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } From c03c90c1cfe813a2963e8881d2aaa6abd64f32b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 20 Oct 2022 18:17:37 +0200 Subject: [PATCH 58/66] feat(bindings)!: Allow passing the E2EE settings when sharing a room key --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 101 +++++++++++++++++- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 14 +-- bindings/matrix-sdk-crypto-ffi/src/olm.udl | 26 ++++- 3 files changed, 128 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index e35c26ae0..a4b2381c4 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -16,7 +16,7 @@ mod uniffi_api; mod users; mod verification; -use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc}; +use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc, time::Duration}; pub use backup_recovery_key::{ BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError, @@ -29,14 +29,17 @@ use js_int::UInt; pub use logger::{set_logger, Logger}; pub use machine::{KeyRequestPair, OlmMachine}; use matrix_sdk_crypto::{ - types::{EventEncryptionAlgorithm, SigningKey}, - LocalTrust, + types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey}, + EncryptionSettings as RustEncryptionSettings, LocalTrust, }; pub use responses::{ BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest, Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest, }; -use ruma::{DeviceId, DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId}; +use ruma::{ + events::room::history_visibility::HistoryVisibility as RustHistoryVisibility, DeviceId, + DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId, +}; use serde::{Deserialize, Serialize}; pub use users::UserIdentity; pub use verification::{ @@ -275,7 +278,7 @@ pub fn migrate( imported: session.imported, backed_up: session.backed_up, history_visibility: None, - algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2, + algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2, }; let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?; @@ -350,6 +353,94 @@ impl ProgressListener for T { } } +/// An encryption algorithm to be used to encrypt messages sent to a room. +pub enum EventEncryptionAlgorithm { + /// Olm version 1 using Curve25519, AES-256, and SHA-256. + OlmV1Curve25519AesSha2, + /// Megolm version 1 using AES-256 and SHA-256. + MegolmV1AesSha2, +} + +impl From for RustEventEncryptionAlgorithm { + fn from(a: EventEncryptionAlgorithm) -> Self { + match a { + EventEncryptionAlgorithm::OlmV1Curve25519AesSha2 => { + RustEventEncryptionAlgorithm::OlmV1Curve25519AesSha2 + } + EventEncryptionAlgorithm::MegolmV1AesSha2 => { + RustEventEncryptionAlgorithm::MegolmV1AesSha2 + } + } + } +} + +/// Who can see a room's history. +pub enum HistoryVisibility { + /// Previous events are accessible to newly joined members from the point + /// they were invited onwards. + /// + /// Events stop being accessible when the member's state changes to + /// something other than *invite* or *join*. + Invited, + + /// Previous events are accessible to newly joined members from the point + /// they joined the room onwards. + /// Events stop being accessible when the member's state changes to + /// something other than *join*. + Joined, + + /// Previous events are always accessible to newly joined members. + /// + /// All events in the room are accessible, even those sent when the member + /// was not a part of the room. + Shared, + + /// All events while this is the `HistoryVisibility` value may be shared by + /// any participating homeserver with anyone, regardless of whether they + /// have ever joined the room. + WorldReadable, +} + +impl From for RustHistoryVisibility { + fn from(h: HistoryVisibility) -> Self { + match h { + HistoryVisibility::Invited => RustHistoryVisibility::Invited, + HistoryVisibility::Joined => RustHistoryVisibility::Joined, + HistoryVisibility::Shared => RustHistoryVisibility::Shared, + HistoryVisibility::WorldReadable => RustHistoryVisibility::Shared, + } + } +} + +/// Settings for an encrypted room. +/// +/// This determines the algorithm and rotation periods of a group session. +pub struct EncryptionSettings { + /// The encryption algorithm that should be used in the room. + pub algorithm: EventEncryptionAlgorithm, + /// How long the session should be used before changing it. Time in seconds. + pub rotation_period: u64, + /// How many messages should be sent before changing the session. + pub rotation_period_msgs: u64, + /// The history visibility of the room when the session was created. + pub history_visibility: HistoryVisibility, + /// Should untrusted devices receive the room key, or should they be + /// excluded from the conversation. + pub only_allow_trusted_devices: bool, +} + +impl From for RustEncryptionSettings { + fn from(v: EncryptionSettings) -> Self { + RustEncryptionSettings { + algorithm: v.algorithm.into(), + rotation_period: Duration::from_secs(v.rotation_period), + rotation_period_msgs: v.rotation_period_msgs, + history_visibility: v.history_visibility.into(), + only_allow_trusted_devices: v.only_allow_trusted_devices, + } + } +} + /// An event that was successfully decrypted. pub struct DecryptedEvent { /// The decrypted version of the event. diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index bd662a831..3b9123730 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -11,9 +11,8 @@ 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, - EncryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentities, - Verification as RustVerification, + matrix_sdk_qrcode::QrVerificationData, olm::ExportedRoomKey, store::RecoveryKey, LocalTrust, + OlmMachine as InnerMachine, UserIdentities, Verification as RustVerification, }; use ruma::{ api::{ @@ -46,9 +45,9 @@ use crate::{ responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse}, BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, ConfirmVerificationResult, CrossSigningKeyExport, CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, - KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, QrCode, Request, - RequestType, RequestVerificationResult, RoomKeyCounts, ScanResult, SignatureUploadRequest, - StartSasResult, UserIdentity, Verification, VerificationRequest, + EncryptionSettings, KeyImportError, KeysImportResult, MegolmV1BackupKey, ProgressListener, + QrCode, Request, RequestType, RequestVerificationResult, RoomKeyCounts, ScanResult, + SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest, }; /// A high level state machine that handles E2EE for Matrix. @@ -521,6 +520,7 @@ impl OlmMachine { &self, room_id: &str, users: Vec, + settings: EncryptionSettings, ) -> Result, CryptoStoreError> { let users: Vec = users.into_iter().filter_map(|u| UserId::parse(u).ok()).collect(); @@ -529,7 +529,7 @@ impl OlmMachine { let requests = self.runtime.block_on(self.inner.share_room_key( &room_id, users.iter().map(Deref::deref), - EncryptionSettings::default(), + settings, ))?; Ok(requests.into_iter().map(|r| r.as_ref().into()).collect()) diff --git a/bindings/matrix-sdk-crypto-ffi/src/olm.udl b/bindings/matrix-sdk-crypto-ffi/src/olm.udl index 8432e36de..9044d09ec 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/olm.udl +++ b/bindings/matrix-sdk-crypto-ffi/src/olm.udl @@ -254,6 +254,26 @@ enum LocalTrust { "Unset", }; +enum EventEncryptionAlgorithm { + "OlmV1Curve25519AesSha2", + "MegolmV1AesSha2", +}; + +enum HistoryVisibility { + "Invited", + "Joined", + "Shared", + "WorldReadable", +}; + +dictionary EncryptionSettings { + EventEncryptionAlgorithm algorithm; + u64 rotation_period; + u64 rotation_period_msgs; + HistoryVisibility history_visibility; + boolean only_allow_trusted_devices; +}; + interface OlmMachine { [Throws=CryptoStoreError] constructor( @@ -301,7 +321,11 @@ interface OlmMachine { [Throws=CryptoStoreError] Request? get_missing_sessions(sequence users); [Throws=CryptoStoreError] - sequence share_room_key([ByRef] string room_id, sequence users); + sequence share_room_key( + [ByRef] string room_id, + sequence users, + EncryptionSettings settings + ); [Throws=CryptoStoreError] void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id); From 9de98dfa6528ef54dc73242d14e41db62dc3dd20 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 24 Oct 2022 12:53:25 +0200 Subject: [PATCH 59/66] fix(sdk): Remove token field from SyncSettings Debug output --- crates/matrix-sdk/src/config/sync.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk/src/config/sync.rs b/crates/matrix-sdk/src/config/sync.rs index e66ed3dc0..a848adfa8 100644 --- a/crates/matrix-sdk/src/config/sync.rs +++ b/crates/matrix-sdk/src/config/sync.rs @@ -48,7 +48,6 @@ impl<'a> fmt::Debug for SyncSettings<'a> { opt_field!(filter); opt_field!(timeout); - opt_field!(token); s.field("full_state", &self.full_state).finish() } From a59fdc08bbda5afd6c5c76243f285d2c13cbe471 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 25 Oct 2022 10:58:51 +0200 Subject: [PATCH 60/66] chore: Fix clippy lints Automated with cargo clippy --fix. --- crates/matrix-sdk-base/src/media.rs | 4 ++-- crates/matrix-sdk-crypto/src/gossiping/mod.rs | 2 +- crates/matrix-sdk-crypto/src/machine.rs | 8 ++++---- crates/matrix-sdk-crypto/src/olm/account.rs | 2 +- crates/matrix-sdk-crypto/src/verification/mod.rs | 7 +++---- crates/matrix-sdk-crypto/src/verification/sas/helpers.rs | 4 ++-- crates/matrix-sdk-indexeddb/src/crypto_store.rs | 6 +++--- crates/matrix-sdk-indexeddb/src/safe_encode.rs | 2 +- crates/matrix-sdk-indexeddb/src/state_store.rs | 4 ++-- crates/matrix-sdk-sled/src/crypto_store.rs | 2 +- crates/matrix-sdk-sled/src/state_store.rs | 4 ++-- crates/matrix-sdk/src/error.rs | 2 +- 12 files changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/matrix-sdk-base/src/media.rs b/crates/matrix-sdk-base/src/media.rs index c2018e89a..32356e989 100644 --- a/crates/matrix-sdk-base/src/media.rs +++ b/crates/matrix-sdk-base/src/media.rs @@ -60,7 +60,7 @@ pub struct MediaThumbnailSize { impl UniqueKey for MediaThumbnailSize { fn unique_key(&self) -> String { - format!("{}{}{}x{}", self.method, UNIQUE_SEPARATOR, self.width, self.height) + format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height) } } @@ -85,7 +85,7 @@ pub struct MediaRequest { impl UniqueKey for MediaRequest { fn unique_key(&self) -> String { - format!("{}{}{}", self.source.unique_key(), UNIQUE_SEPARATOR, self.format.unique_key()) + format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key()) } } /// Trait for media event content. diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index db598e9c2..3ce5c27de 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -96,7 +96,7 @@ impl SecretInfo { info.session_id(), &info.algorithm(), ), - SecretInfo::SecretRequest(ref sname) => format!("secretName:{:}", sname), + SecretInfo::SecretRequest(ref sname) => format!("secretName:{sname:}"), } } } diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index 6c6edad6f..fbdec2bf2 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -1981,7 +1981,7 @@ pub(crate) mod tests { if let AnyToDeviceEvent::Dummy(e) = event { assert_eq!(&e.sender, alice.user_id()); } else { - panic!("Wrong event type found {:?}", event); + panic!("Wrong event type found {event:?}"); } } @@ -2019,7 +2019,7 @@ pub(crate) mod tests { assert_eq!(&event.sender, alice.user_id()); assert!(event.content.session_key.is_empty()); } else { - panic!("expected RoomKeyEvent found {:?}", event); + panic!("expected RoomKeyEvent found {event:?}"); } let session = @@ -2298,7 +2298,7 @@ pub(crate) mod tests { // Bob verifies that the MAC is valid and also sends a "done" message. let msgs = bob.verification_machine.outgoing_messages(); - eprintln!("{:?}", msgs); + eprintln!("{msgs:?}"); assert!(msgs.len() == 1); let event = msgs.first().map(|r| outgoing_request_to_event(bob.user_id(), r)).unwrap(); @@ -2319,7 +2319,7 @@ pub(crate) mod tests { assert!(!alice_sas.is_done()); assert!(!bob_device.is_verified()); // Alices receives the done message - eprintln!("{:?}", event); + eprintln!("{event:?}"); alice.handle_verification_event(&event).await; assert!(alice_sas.is_done()); diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 943098cd8..1de7f31cb 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -138,7 +138,7 @@ impl OlmMessageHash { let sha = Sha256::new() .chain_update(sender_key.as_bytes()) .chain_update([message_type as u8]) - .chain_update(&ciphertext) + .chain_update(ciphertext) .finalize(); Self { sender_key, hash: encode(sha.as_slice()) } diff --git a/crates/matrix-sdk-crypto/src/verification/mod.rs b/crates/matrix-sdk-crypto/src/verification/mod.rs index 7799b1d17..b5018afa0 100644 --- a/crates/matrix-sdk-crypto/src/verification/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/mod.rs @@ -100,7 +100,7 @@ pub fn format_emojis(emojis: [Emoji; 7]) -> String { // Hack to make terminals behave properly when one of the above is printed. let emoji = if VARIATION_SELECTOR_EMOJIS.contains(&emoji) { - format!("{} ", emoji) + format!("{emoji} ") } else { emoji.to_owned() }; @@ -109,13 +109,12 @@ pub fn format_emojis(emojis: [Emoji; 7]) -> String { // monospace characters. let placeholder = ".".repeat(EMOJI_WIDTH); - format!("{:^12}", placeholder).replace(&placeholder, &emoji) + format!("{placeholder:^12}").replace(&placeholder, &emoji) }; let emoji_string = emojis.iter().map(|e| center_emoji(e)).collect::>().join(""); - let description = - descriptions.iter().map(|d| format!("{:^12}", d)).collect::>().join(""); + let description = descriptions.iter().map(|d| format!("{d:^12}")).collect::>().join(""); format!("{emoji_string}\n{description}") } diff --git a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs index da617e73a..860f797bb 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/helpers.rs @@ -62,7 +62,7 @@ pub fn calculate_commitment(public_key: Curve25519PublicKey, content: &StartCont Base64::new( Sha256::new() .chain_update(public_key.to_base64()) - .chain_update(&content_string) + .chain_update(content_string) .finalize() .as_slice() .to_owned(), @@ -228,7 +228,7 @@ pub fn receive_mac_event( if let Some(key) = ids.other_device.keys().get(&key_id) { let calculated_mac = Base64::parse( - sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{}{}", info, key_id)), + sas.calculate_mac_invalid_base64(&key.to_base64(), &format!("{info}{key_id}")), ) .expect("Can't base64-decode SAS MAC"); diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store.rs b/crates/matrix-sdk-indexeddb/src/crypto_store.rs index b453a918b..b0d469359 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store.rs @@ -140,7 +140,7 @@ impl IndexeddbCryptoStore { prefix: &str, store_cipher: Option>, ) -> Result { - let name = format!("{:0}::matrix-sdk-crypto", prefix); + let name = format!("{prefix:0}::matrix-sdk-crypto"); // Open my_db v1 let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.1)?; @@ -232,7 +232,7 @@ impl IndexeddbCryptoStore { /// Open a new `IndexeddbCryptoStore` with given name and passphrase pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result { - let name = format!("{:0}::matrix-sdk-crypto-meta", prefix); + let name = format!("{prefix:0}::matrix-sdk-crypto-meta"); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&name, 1.0)?; db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { @@ -907,7 +907,7 @@ impl IndexeddbCryptoStore { if let Some(inner) = request { tx.object_store(KEYS::SECRET_REQUESTS_BY_INFO)? - .delete(&self.encode_key(KEYS::KEY_REQUEST, &inner.info.as_key()))?; + .delete(&self.encode_key(KEYS::KEY_REQUEST, inner.info.as_key()))?; } tx.object_store(KEYS::UNSENT_SECRET_REQUESTS)?.delete(&jskey)?; diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/safe_encode.rs index 55bfe4df4..e26486088 100644 --- a/crates/matrix-sdk-indexeddb/src/safe_encode.rs +++ b/crates/matrix-sdk-indexeddb/src/safe_encode.rs @@ -61,7 +61,7 @@ pub trait SafeEncode { /// encode self into a JsValue, internally using `as_encoded_string` /// to escape the value of self, and append the given counter fn encode_with_counter(&self, i: usize) -> JsValue { - format!("{}{}{:016x}", self.as_encoded_string(), KEY_SEPARATOR, i).into() + format!("{}{KEY_SEPARATOR}{i:016x}", self.as_encoded_string()).into() } /// encode self into a JsValue, internally using `as_secure_string` diff --git a/crates/matrix-sdk-indexeddb/src/state_store.rs b/crates/matrix-sdk-indexeddb/src/state_store.rs index 8f45dbbf0..62a8890d0 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store.rs @@ -191,7 +191,7 @@ fn create_stores(db: &IdbDatabase) -> Result<(), JsValue> { async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> { let now = JsDate::now(); - let backup_name = format!("backup-{}-{}", source.name(), now); + let backup_name = format!("backup-{}-{now}", source.name()); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&backup_name, source.version())?; db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { @@ -257,7 +257,7 @@ impl IndexeddbStateStoreBuilder { .unwrap_or(MigrationConflictStrategy::BackupAndDrop); let name = self.name.clone().unwrap_or_else(|| "state".to_owned()); - let meta_name = format!("{}::{}", name, KEYS::INTERNAL_STATE); + let meta_name = format!("{name}::{}", KEYS::INTERNAL_STATE); let mut db_req: OpenDbRequest = IdbDatabase::open_f64(&meta_name, KEYS::CURRENT_META_DB_VERSION)?; diff --git a/crates/matrix-sdk-sled/src/crypto_store.rs b/crates/matrix-sdk-sled/src/crypto_store.rs index 7beceea3d..1c244154c 100644 --- a/crates/matrix-sdk-sled/src/crypto_store.rs +++ b/crates/matrix-sdk-sled/src/crypto_store.rs @@ -792,7 +792,7 @@ impl CryptoStore for SledCryptoStore { let key = self.encode_key(INBOUND_GROUP_TABLE_NAME, (room_id, session_id)); let pickle = self .inbound_group_sessions - .get(&key) + .get(key) .map_err(CryptoStoreError::backend)? .map(|p| self.deserialize_value(&p)); diff --git a/crates/matrix-sdk-sled/src/state_store.rs b/crates/matrix-sdk-sled/src/state_store.rs index 88a11edbf..46ad8cff5 100644 --- a/crates/matrix-sdk-sled/src/state_store.rs +++ b/crates/matrix-sdk-sled/src/state_store.rs @@ -690,7 +690,7 @@ impl SledStateStore { let make_room_version = |room_id| { self.room_info - .get(&self.encode_key(ROOM_INFO, room_id)) + .get(self.encode_key(ROOM_INFO, room_id)) .ok() .flatten() .map(|r| self.deserialize_value::(&r)) @@ -1577,7 +1577,7 @@ mod migration { if let Err(SledStoreError::MigrationConflict { .. }) = res { // all good } else { - panic!("Didn't raise the expected error: {:?}", res); + panic!("Didn't raise the expected error: {res:?}"); } assert_eq!(std::fs::read_dir(folder.path())?.count(), 1); Ok(()) diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index ad28e05de..e1e6c565d 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -319,7 +319,7 @@ impl From for Error { _ => Self::UnknownError(anyhow::anyhow!(e).into()), #[cfg(all(not(feature = "eyre"), not(feature = "anyhow")))] _ => { - let e: Box = format!("{:?}", e).into(); + let e: Box = format!("{e:?}").into(); Self::UnknownError(e) } } From 285dc129c34af51b18cb42309ae6d140f7bffd67 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 25 Oct 2022 10:59:19 +0200 Subject: [PATCH 61/66] chore(crypto): Clean up SecretInfo::as_key implementation --- crates/matrix-sdk-crypto/src/gossiping/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/gossiping/mod.rs b/crates/matrix-sdk-crypto/src/gossiping/mod.rs index 3ce5c27de..4980a85c4 100644 --- a/crates/matrix-sdk-crypto/src/gossiping/mod.rs +++ b/crates/matrix-sdk-crypto/src/gossiping/mod.rs @@ -87,16 +87,16 @@ pub enum SecretInfo { impl SecretInfo { /// Serialize `SecretInfo` into `String` for usage as database keys and - /// comparison + /// comparison. pub fn as_key(&self) -> String { match &self { - SecretInfo::KeyRequest(ref info) => format!( + SecretInfo::KeyRequest(info) => format!( "keyRequest:{:}:{:}:{:}", info.room_id().as_str(), info.session_id(), &info.algorithm(), ), - SecretInfo::SecretRequest(ref sname) => format!("secretName:{sname:}"), + SecretInfo::SecretRequest(sname) => format!("secretName:{sname}"), } } } From 50a5ec89e90566e62b11bbfc56cce6f42cb8f685 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 25 Oct 2022 11:51:23 +0200 Subject: [PATCH 62/66] chore: Fix more clippy lints --- examples/cross_signing_bootstrap/src/main.rs | 2 +- examples/emoji_verification/src/main.rs | 2 +- examples/get_profiles/src/main.rs | 2 +- labs/jack-in/src/components/details.rs | 2 +- labs/jack-in/src/components/statusbar.rs | 2 +- labs/jack-in/src/main.rs | 8 ++++---- labs/sled-state-inspector/src/main.rs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/cross_signing_bootstrap/src/main.rs b/examples/cross_signing_bootstrap/src/main.rs index 3a35c2730..2217f96a6 100644 --- a/examples/cross_signing_bootstrap/src/main.rs +++ b/examples/cross_signing_bootstrap/src/main.rs @@ -30,7 +30,7 @@ async fn bootstrap(client: Client, user_id: OwnedUserId, password: String) { .await .expect("Couldn't bootstrap cross signing") } else { - panic!("Error during cross-signing bootstrap {:#?}", e); + panic!("Error during cross-signing bootstrap {e:#?}"); } } } diff --git a/examples/emoji_verification/src/main.rs b/examples/emoji_verification/src/main.rs index 6ca0bb6e3..e8aa88edc 100644 --- a/examples/emoji_verification/src/main.rs +++ b/examples/emoji_verification/src/main.rs @@ -35,7 +35,7 @@ async fn wait_for_confirmation(sas: SasVerification, emoji: [Emoji; 7]) { } async fn print_devices(user_id: &UserId, client: &Client) { - println!("Devices of user {}", user_id); + println!("Devices of user {user_id}"); for device in client.encryption().get_user_devices(user_id).await.unwrap().devices() { println!( diff --git a/examples/get_profiles/src/main.rs b/examples/get_profiles/src/main.rs index 73b6964f9..87ab43928 100644 --- a/examples/get_profiles/src/main.rs +++ b/examples/get_profiles/src/main.rs @@ -68,6 +68,6 @@ async fn main() -> anyhow::Result<()> { let user_id = UserId::parse(username).expect("Couldn't parse the MXID"); let profile = get_profile(client, &user_id).await?; - println!("{:#?}", profile); + println!("{profile:#?}"); Ok(()) } diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index 4b847d08a..2e2e71fe4 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -161,7 +161,7 @@ impl MockComponent for Details { let mut tabs = vec![]; for (title, count) in &self.state_events_counts { - tabs.push(Spans::from(format!("{}: {}", title.clone(), count))); + tabs.push(Spans::from(format!("{title}: {count}"))); } frame.render_widget( diff --git a/labs/jack-in/src/components/statusbar.rs b/labs/jack-in/src/components/statusbar.rs index 60ef104d4..0ca25027f 100644 --- a/labs/jack-in/src/components/statusbar.rs +++ b/labs/jack-in/src/components/statusbar.rs @@ -41,7 +41,7 @@ impl MockComponent for StatusBar { if let Some(dur) = self.sstate.time_to_full_sync() { tabs.push(Spans::from(format!("Full sync: {}ms", dur.as_millis()))); if let Some(count) = self.sstate.total_rooms_count() { - tabs.push(Spans::from(format!("{} rooms", count))); + tabs.push(Spans::from(format!("{count} rooms"))); } } else { tabs.push(Spans::from(format!( diff --git a/labs/jack-in/src/main.rs b/labs/jack-in/src/main.rs index d3b47a41c..1a33c2668 100644 --- a/labs/jack-in/src/main.rs +++ b/labs/jack-in/src/main.rs @@ -192,11 +192,11 @@ async fn main() -> Result<()> { .account() .get_display_name() .await? - .map(|s| format!("{} ({})", s, user_id)) - .unwrap_or_else(|| format!("{}", user_id)); + .map(|s| format!("{s} ({user_id})")) + .unwrap_or_else(|| format!("{user_id}")); let poller = MatrixPoller(rx); let mut model = Model::new(start_sync, model_tx, poller); - model.set_title(format!("{} via {}", display_name, opt.sliding_sync_proxy)); + model.set_title(format!("{display_name} via {}", opt.sliding_sync_proxy)); run_ui(model).await; Ok(()) @@ -213,7 +213,7 @@ async fn run_ui(mut model: Model) { // Tick match model.app.tick(PollStrategy::Once) { Err(err) => { - model.set_title(format!("Application error: {}", err)); + model.set_title(format!("Application error: {err}")); } Ok(messages) if !messages.is_empty() => { // NOTE: redraw if at least one msg has been processed diff --git a/labs/sled-state-inspector/src/main.rs b/labs/sled-state-inspector/src/main.rs index 748f9c721..8c939d275 100644 --- a/labs/sled-state-inspector/src/main.rs +++ b/labs/sled-state-inspector/src/main.rs @@ -172,7 +172,7 @@ impl Printer { let data = if self.json { serde_json::to_string_pretty(data).expect("Can't serialize struct") } else { - format!("{:#?}", data) + format!("{data:#?}") }; let syntax = if self.json { From 6cb37520ce969e8ac0b86732b3df4a798ac6b3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 25 Oct 2022 11:49:34 +0200 Subject: [PATCH 63/66] docs(bindings): Document the EncryptionSettings a bit better --- bindings/matrix-sdk-crypto-ffi/src/lib.rs | 15 ++++++++++----- bindings/matrix-sdk-crypto-ffi/src/machine.rs | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index a4b2381c4..b619c88fe 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -412,17 +412,22 @@ impl From for RustHistoryVisibility { } } -/// Settings for an encrypted room. +/// Settings that should be used when a room key is shared. /// -/// This determines the algorithm and rotation periods of a group session. +/// These settings control which algorithm the room key should use, how long a +/// room key should be used and some other important information that determines +/// the lifetime of a room key. pub struct EncryptionSettings { /// The encryption algorithm that should be used in the room. pub algorithm: EventEncryptionAlgorithm, - /// How long the session should be used before changing it. Time in seconds. + /// How long can the room key be used before it should be rotated. Time in + /// seconds. pub rotation_period: u64, - /// How many messages should be sent before changing the session. + /// How many messages should be sent before the room key should be rotated. pub rotation_period_msgs: u64, - /// The history visibility of the room when the session was created. + /// The current history visibility of the room. The visibility will be + /// tracked by the room key and the key will be rotated if the visibility + /// changes. pub history_visibility: HistoryVisibility, /// Should untrusted devices receive the room key, or should they be /// excluded from the conversation. diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index 3b9123730..fa078d7d3 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -516,6 +516,8 @@ impl OlmMachine { /// /// * `users` - The list of users which are considered to be members of the /// room and should receive the room key. + /// + /// * `settings` - The settings that should be used for the room key. pub fn share_room_key( &self, room_id: &str, From 2547d6ed64b2b7a4ce8721567f10842f6b8b4bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 25 Oct 2022 09:28:47 +0200 Subject: [PATCH 64/66] fix(crypto): Don't create an infinite amount of streams in the SAS examples --- crates/matrix-sdk-crypto/src/verification/sas/mod.rs | 4 +++- crates/matrix-sdk/src/encryption/verification/sas.rs | 4 +++- examples/emoji_verification/src/main.rs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs index ec36f3189..8f513b6da 100644 --- a/crates/matrix-sdk-crypto/src/verification/sas/mod.rs +++ b/crates/matrix-sdk-crypto/src/verification/sas/mod.rs @@ -668,7 +668,9 @@ impl Sas { /// # futures::executor::block_on(async { /// # let sas: Sas = unimplemented!(); /// - /// while let Some(state) = sas.changes().next().await { + /// let mut stream = sas.changes(); + /// + /// while let Some(state) = stream.next().await { /// match state { /// SasState::KeysExchanged { emojis, decimals: _ } => { /// let emojis = diff --git a/crates/matrix-sdk/src/encryption/verification/sas.rs b/crates/matrix-sdk/src/encryption/verification/sas.rs index 176ac2d0a..0df131760 100644 --- a/crates/matrix-sdk/src/encryption/verification/sas.rs +++ b/crates/matrix-sdk/src/encryption/verification/sas.rs @@ -273,7 +273,9 @@ impl SasVerification { /// # let sas: SasVerification = unimplemented!(); /// # let user_confirmed = false; /// - /// while let Some(state) = sas.changes().next().await { + /// let mut stream = sas.changes(); + /// + /// while let Some(state) = stream.next().await { /// match state { /// SasState::KeysExchanged { emojis, decimals: _ } => { /// let emojis = diff --git a/examples/emoji_verification/src/main.rs b/examples/emoji_verification/src/main.rs index e8aa88edc..496c4d18d 100644 --- a/examples/emoji_verification/src/main.rs +++ b/examples/emoji_verification/src/main.rs @@ -56,7 +56,9 @@ async fn sas_verification_handler(client: Client, sas: SasVerification) { print_devices(sas.other_device().user_id(), &client).await; sas.accept().await.unwrap(); - while let Some(state) = sas.changes().next().await { + let mut stream = sas.changes(); + + while let Some(state) = stream.next().await { match state { SasState::KeysExchanged { emojis, decimals: _ } => { tokio::spawn(wait_for_confirmation( From 2232092f11d453ac03466039ba3e01a128c3bf5b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 25 Oct 2022 13:03:52 +0200 Subject: [PATCH 65/66] refactor(sdk): Move timeline event handling fns to TimelineInner --- .../src/room/timeline/event_handler.rs | 132 +++++++++--------- crates/matrix-sdk/src/room/timeline/mod.rs | 8 +- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index bc228d2f5..f7ced1e72 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -40,79 +40,81 @@ use super::{ TimelineKey, }; -pub(super) fn handle_live_event( - raw: Raw, - encryption_info: Option, - own_user_id: &UserId, - timeline: &TimelineInner, -) { - handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::End, timeline) -} +impl TimelineInner { + pub(super) fn handle_live_event( + &self, + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + ) { + self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::End) + } -pub(super) fn handle_local_event( - txn_id: OwnedTransactionId, - content: AnyMessageLikeEventContent, - own_user_id: &UserId, - timeline: &TimelineInner, -) { - let meta = TimelineEventMetadata { - sender: own_user_id.to_owned(), - origin_server_ts: None, - raw_event: None, - is_own_event: true, - relations: None, - // FIXME: Should we supply something here for encrypted rooms? - encryption_info: None, - }; + pub(super) fn handle_local_event( + &self, + txn_id: OwnedTransactionId, + content: AnyMessageLikeEventContent, + own_user_id: &UserId, + ) { + let meta = TimelineEventMetadata { + sender: own_user_id.to_owned(), + origin_server_ts: None, + raw_event: None, + is_own_event: true, + relations: None, + // FIXME: Should we supply something here for encrypted rooms? + encryption_info: None, + }; - let flow = Flow::Local { txn_id }; - let kind = TimelineEventKind::Message { content }; + let flow = Flow::Local { txn_id }; + let kind = TimelineEventKind::Message { content }; - TimelineEventHandler::new(meta, flow, timeline).handle_event(kind) -} + TimelineEventHandler::new(meta, flow, self).handle_event(kind) + } -pub(super) fn handle_back_paginated_event( - raw: Raw, - encryption_info: Option, - own_user_id: &UserId, - timeline: &TimelineInner, -) { - handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::Start, timeline) -} + pub(super) fn handle_back_paginated_event( + &self, + raw: Raw, + encryption_info: Option, + own_user_id: &UserId, + ) { + self.handle_remote_event(raw, encryption_info, own_user_id, TimelineItemPosition::Start) + } -fn handle_remote_event( - raw: Raw, - encryption_info: Option, - own_user_id: &UserId, - position: TimelineItemPosition, - timeline: &TimelineInner, -) { - let event = match raw.deserialize() { - Ok(ev) => ev, - Err(_e) => { - // TODO: Add some sort of error timeline item - return; - } - }; + fn handle_remote_event( + &self, + raw: Raw, + encryption_info: Option, + 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 sender = event.sender().to_owned(); + let is_own_event = sender == own_user_id; - let meta = TimelineEventMetadata { - raw_event: Some(raw), - sender, - origin_server_ts: Some(event.origin_server_ts()), - is_own_event, - relations: event.relations().cloned(), - encryption_info, - }; - let flow = Flow::Remote { - event_id: event.event_id().to_owned(), - txn_id: event.transaction_id().map(ToOwned::to_owned), - position, - }; + let meta = TimelineEventMetadata { + raw_event: Some(raw), + sender, + origin_server_ts: Some(event.origin_server_ts()), + is_own_event, + relations: event.relations().cloned(), + encryption_info, + }; + let flow = Flow::Remote { + event_id: event.event_id().to_owned(), + txn_id: event.transaction_id().map(ToOwned::to_owned), + position, + }; - TimelineEventHandler::new(meta, flow, timeline).handle_event(event.into()) + TimelineEventHandler::new(meta, flow, self).handle_event(event.into()) + } } enum Flow { diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index 73ed300d4..210466656 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -42,7 +42,6 @@ mod event_handler; mod event_item; mod virtual_item; -use self::event_handler::{handle_back_paginated_event, handle_live_event, handle_local_event}; pub use self::{ event_item::{ EventTimelineItem, Message, PaginationOutcome, ReactionDetails, TimelineDetails, @@ -81,7 +80,7 @@ impl Timeline { move |event, encryption_info: Option, room: Room| { let inner = inner.clone(); async move { - handle_live_event(event, encryption_info, room.own_user_id(), &inner); + inner.handle_live_event(event, encryption_info, room.own_user_id()); } } }); @@ -113,11 +112,10 @@ impl Timeline { let own_user_id = self.room.own_user_id(); for room_ev in messages.chunk { - handle_back_paginated_event( + self.inner.handle_back_paginated_event( room_ev.event.cast(), room_ev.encryption_info, own_user_id, - &self.inner, ); } @@ -175,7 +173,7 @@ impl Timeline { txn_id: Option<&TransactionId>, ) -> Result<()> { let txn_id = txn_id.map_or_else(TransactionId::new, ToOwned::to_owned); - handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id(), &self.inner); + self.inner.handle_local_event(txn_id.clone(), content.clone(), self.room.own_user_id()); // If this room isn't actually in joined state, we'll get a server error. // Not ideal, but works for now. From a443e7277d72c8194aa7e8d228d5123088efc268 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 25 Oct 2022 13:42:43 +0200 Subject: [PATCH 66/66] feat: Add sliding sync timeline events and extensions (#1054) Add general extension framework for sliding sync, implementing the e2ee, to-device and account-data extensions as per existing proxy implementation. Add a new (ffi exposed) function to use activate the extensions. Also extends jack-in to have permanent login and storage support now, rather than posting an access token and expose messages inside the sliding-sync layer to actually use the decrypted messages if given. Contains a lot of fixes around these aspects, too, like uploading any remaining messages from the olm-machine on every sliding-sync-request or processing even if no room data is present (which can happen now as processing only extensions might takes place). --- Cargo.lock | 109 +++++++++++ bindings/matrix-sdk-crypto-ffi/src/machine.rs | 4 +- .../matrix-sdk-crypto-ffi/src/responses.rs | 2 +- .../matrix-sdk-crypto-js/src/sync_events.rs | 4 +- .../matrix-sdk-crypto-js/tests/device.test.js | 168 +++++++--------- bindings/matrix-sdk-crypto-js/tests/helper.js | 4 +- .../tests/machine.test.js | 10 +- .../src/sync_events.rs | 4 +- .../tests/machine.test.js | 12 +- bindings/matrix-sdk-ffi/src/api.udl | 3 + bindings/matrix-sdk-ffi/src/client.rs | 2 +- .../src/session_verification.rs | 6 +- bindings/matrix-sdk-ffi/src/sliding_sync.rs | 11 +- crates/matrix-sdk-base/src/client.rs | 79 +++++--- crates/matrix-sdk-base/src/sliding_sync.rs | 180 +++++++++--------- .../src/deserialized_responses.rs | 18 +- crates/matrix-sdk-crypto/README.md | 3 +- crates/matrix-sdk-crypto/src/machine.rs | 37 ++-- crates/matrix-sdk/src/sliding_sync.rs | 177 ++++++++++++++--- crates/matrix-sdk/src/sync.rs | 10 +- labs/jack-in/Cargo.toml | 18 +- labs/jack-in/src/client/mod.rs | 25 ++- labs/jack-in/src/components/details.rs | 2 +- labs/jack-in/src/main.rs | 131 ++++++++++--- 24 files changed, 669 insertions(+), 350 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c829ffaf9..0096903f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,18 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "app_dirs2" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a8d2d8dbda5fca0a522259fb88e4f55d2b10ad39f5f03adeebf85031eba501" +dependencies = [ + "jni", + "ndk-context", + "winapi", + "xdg", +] + [[package]] name = "arc-swap" version = "1.5.1" @@ -554,6 +566,12 @@ version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -742,6 +760,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "1.2.4" @@ -751,6 +779,20 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1161,6 +1203,17 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.9.0" @@ -1270,6 +1323,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -2164,12 +2223,17 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" name = "jack-in" version = "0.2.0" dependencies = [ + "app_dirs2", + "dialoguer", "eyre", "futures", "futures-signals", "log4rs", "matrix-sdk", "matrix-sdk-common", + "matrix-sdk-sled", + "sanitize-filename-reader-friendly", + "serde_json", "structopt", "tokio", "tracing", @@ -2179,6 +2243,26 @@ dependencies = [ "tuirealm", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -2904,6 +2988,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nibble_vec" version = "0.1.0" @@ -4440,6 +4530,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -5407,6 +5507,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + [[package]] name = "xshell" version = "0.1.17" diff --git a/bindings/matrix-sdk-crypto-ffi/src/machine.rs b/bindings/matrix-sdk-crypto-ffi/src/machine.rs index fa078d7d3..a859b432e 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/machine.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/machine.rs @@ -419,7 +419,7 @@ impl OlmMachine { key_counts: HashMap, unused_fallback_keys: Option>, ) -> Result { - let events: ToDevice = serde_json::from_str(events)?; + let to_device: ToDevice = serde_json::from_str(events)?; let device_changes: RumaDeviceLists = device_changes.into(); let key_counts: BTreeMap = key_counts .into_iter() @@ -437,7 +437,7 @@ impl OlmMachine { unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect()); let events = self.runtime.block_on(self.inner.receive_sync_changes( - events, + to_device.events, &device_changes, &key_counts, unused_fallback_keys.as_deref(), diff --git a/bindings/matrix-sdk-crypto-ffi/src/responses.rs b/bindings/matrix-sdk-crypto-ffi/src/responses.rs index feb6b89c9..0a2512804 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/responses.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/responses.rs @@ -19,7 +19,7 @@ use ruma::{ }, }, message::send_message_event::v3::Response as RoomMessageResponse, - sync::sync_events::v3::DeviceLists as RumaDeviceLists, + sync::sync_events::DeviceLists as RumaDeviceLists, to_device::send_event_to_device::v3::Response as ToDeviceResponse, }, assign, diff --git a/bindings/matrix-sdk-crypto-js/src/sync_events.rs b/bindings/matrix-sdk-crypto-js/src/sync_events.rs index 8e04c632c..a06815f9b 100644 --- a/bindings/matrix-sdk-crypto-js/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-js/src/sync_events.rs @@ -9,7 +9,7 @@ use crate::{identifiers, js::downcast}; #[wasm_bindgen] #[derive(Debug)] pub struct DeviceLists { - pub(crate) inner: ruma::api::client::sync::sync_events::v3::DeviceLists, + pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists, } #[wasm_bindgen] @@ -19,7 +19,7 @@ impl DeviceLists { /// `changed` and `left` must be an array of `UserId`. #[wasm_bindgen(constructor)] pub fn new(changed: Option, left: Option) -> Result { - let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); + let mut inner = ruma::api::client::sync::sync_events::DeviceLists::default(); inner.changed = changed .unwrap_or_default() diff --git a/bindings/matrix-sdk-crypto-js/tests/device.test.js b/bindings/matrix-sdk-crypto-js/tests/device.test.js index 532624e26..ec01a664e 100644 --- a/bindings/matrix-sdk-crypto-js/tests/device.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/device.test.js @@ -176,13 +176,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -230,13 +228,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification ready to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -287,13 +283,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the SAS start to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -335,13 +329,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.accept'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the SAS accept to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -364,13 +356,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS key to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -390,13 +380,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.key'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS key to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -463,13 +451,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS confirmation to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -491,13 +477,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.mac'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS confirmation to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -512,13 +496,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send te SAS done to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -538,13 +520,11 @@ describe('Key Verification', () => { toDeviceRequest = JSON.parse(toDeviceRequest.body); expect(toDeviceRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: toDeviceRequest.event_type, - content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: toDeviceRequest.event_type, + content: toDeviceRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send te SAS key to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -628,13 +608,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.request'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -685,13 +663,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.ready'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }], - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification ready to `m1`. await m1.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -848,13 +824,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.start'); - const toDeviceEvents = { - events: [{ - sender: userId1.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId1.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId2.toString()][deviceId2.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); @@ -872,13 +846,11 @@ describe('Key Verification', () => { outgoingVerificationRequest = JSON.parse(outgoingVerificationRequest.body); expect(outgoingVerificationRequest.event_type).toStrictEqual('m.key.verification.done'); - const toDeviceEvents = { - events: [{ - sender: userId2.toString(), - type: outgoingVerificationRequest.event_type, - content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], - }] - }; + const toDeviceEvents = [{ + sender: userId2.toString(), + type: outgoingVerificationRequest.event_type, + content: outgoingVerificationRequest.messages[userId1.toString()][deviceId1.toString()], + }]; // Let's send the verification request to `m2`. await m2.receiveSyncChanges(JSON.stringify(toDeviceEvents), new DeviceLists(), new Map(), new Set()); diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index 1ced03d1a..29f21b61a 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -11,14 +11,14 @@ function* zip(...arrays) { // Add a machine to another machine, i.e. be sure a machine knows // another exists. async function addMachineToMachine(machineToAdd, machine) { - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await machineToAdd.outgoingRequests(); diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 7eda1903c..c375015dd 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -128,26 +128,26 @@ describe(OlmMachine.name, () => { test('can receive sync changes', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); }); test('can get the outgoing requests that need to be send out', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await m.outgoingRequests(); @@ -182,7 +182,7 @@ describe(OlmMachine.name, () => { beforeAll(async () => { m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID')); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = new Map(); const unusedFallbackKeys = new Set(); diff --git a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs index 63e017021..4122bba2d 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/sync_events.rs @@ -7,7 +7,7 @@ use crate::identifiers; /// Information on E2E device updates. #[napi] pub struct DeviceLists { - pub(crate) inner: ruma::api::client::sync::sync_events::v3::DeviceLists, + pub(crate) inner: ruma::api::client::sync::sync_events::DeviceLists, } #[napi] @@ -18,7 +18,7 @@ impl DeviceLists { changed: Option>, left: Option>, ) -> Self { - let mut inner = ruma::api::client::sync::sync_events::v3::DeviceLists::default(); + let mut inner = ruma::api::client::sync::sync_events::DeviceLists::default(); inner.changed = changed.into_iter().flatten().map(|user| user.inner.clone()).collect(); inner.left = left.into_iter().flatten().map(|user| user.inner.clone()).collect(); diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index 020486bd2..a3b424683 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -51,26 +51,26 @@ describe(OlmMachine.name, () => { test('can receive sync changes', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); }); test('can get the outgoing requests that need to be send out', async () => { const m = await machine(); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; const receiveSyncChanges = JSON.parse(await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys)); - expect(receiveSyncChanges).toEqual({}); + expect(receiveSyncChanges).toEqual([]); const outgoingRequests = await m.outgoingRequests(); @@ -105,12 +105,12 @@ describe(OlmMachine.name, () => { beforeAll(async () => { m = await machine(new UserId('@alice:example.org'), new DeviceId('DEVICEID')); - const toDeviceEvents = JSON.stringify({}); + const toDeviceEvents = JSON.stringify([]); const changedDevices = new DeviceLists(); const oneTimeKeyCounts = {}; const unusedFallbackKeys = []; - const receiveSyncChanges = await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); + await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys); outgoingRequests = await m.outgoingRequests(); expect(outgoingRequests).toHaveLength(2); diff --git a/bindings/matrix-sdk-ffi/src/api.udl b/bindings/matrix-sdk-ffi/src/api.udl index b92e5eba6..61b7da5ee 100644 --- a/bindings/matrix-sdk-ffi/src/api.udl +++ b/bindings/matrix-sdk-ffi/src/api.udl @@ -175,6 +175,9 @@ interface SlidingSyncBuilder { [Self=ByArc] SlidingSyncBuilder add_view(SlidingSyncView view); + [Self=ByArc] + SlidingSyncBuilder with_common_extensions(); + [Throws=ClientError, Self=ByArc] SlidingSync build(); }; diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 1b89bbe99..28f2e961c 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -357,7 +357,7 @@ impl Client { &*session_verification_controller.read().await { session_verification_controller - .process_to_device_messages(sync_response.to_device) + .process_to_device_messages(sync_response.to_device_events) .await; } diff --git a/bindings/matrix-sdk-ffi/src/session_verification.rs b/bindings/matrix-sdk-ffi/src/session_verification.rs index bc9b3d59e..9db8f5c4c 100644 --- a/bindings/matrix-sdk-ffi/src/session_verification.rs +++ b/bindings/matrix-sdk-ffi/src/session_verification.rs @@ -6,8 +6,8 @@ use matrix_sdk::{ verification::{SasVerification, VerificationRequest}, }, ruma::{ - api::client::sync::sync_events::v3::ToDevice, events::{key::verification::VerificationMethod, AnyToDeviceEvent}, + serde::Raw, }, }; @@ -106,10 +106,10 @@ impl SessionVerificationController { }) } - pub async fn process_to_device_messages(&self, to_device: ToDevice) { + pub async fn process_to_device_messages(&self, to_device_events: Vec>) { let sas_verification = self.sas_verification.clone(); - for event in to_device.events.into_iter().filter_map(|e| e.deserialize().ok()) { + for event in to_device_events.into_iter().filter_map(|e| e.deserialize().ok()) { match event { AnyToDeviceEvent::KeyVerificationReady(event) => { if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) { diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index 9bfe47a1a..97eda7b9e 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -137,9 +137,10 @@ impl SlidingSyncRoom { for ev in lock.iter().rev() { if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( SyncRoomMessageEvent::Original(o), - ))) = ev.deserialize() + ))) = ev.event.deserialize() { - let inner = matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.clone()); + let inner = + matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.event.clone()); return Some(Arc::new(EventTimelineItem(inner))); } } @@ -584,6 +585,12 @@ impl SlidingSyncBuilder { Arc::new(builder) } + pub fn with_common_extensions(self: Arc) -> Arc { + let mut builder = unwrap_or_clone_arc(self); + builder.inner = builder.inner.with_common_extensions(); + Arc::new(builder) + } + pub fn build(self: Arc) -> anyhow::Result> { let builder = unwrap_or_clone_arc(self); Ok(Arc::new(SlidingSync::new(builder.inner.build()?, builder.client))) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 9f5605d52..0daff8a96 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -252,10 +252,12 @@ impl BaseClient { } #[allow(clippy::too_many_arguments)] - async fn handle_timeline( + pub(crate) async fn handle_timeline( &self, room: &Room, - ruma_timeline: api::sync::sync_events::v3::Timeline, + limited: bool, + events: Vec>, + prev_batch: Option, push_rules: &Ruleset, user_ids: &mut BTreeSet, room_info: &mut RoomInfo, @@ -264,10 +266,10 @@ impl BaseClient { ) -> Result { let room_id = room.room_id(); let user_id = room.own_user_id(); - let mut timeline = Timeline::new(ruma_timeline.limited, ruma_timeline.prev_batch.clone()); + let mut timeline = Timeline::new(limited, prev_batch); let mut push_context = self.get_push_room_context(room, room_info, changes).await?; - for event in ruma_timeline.events { + for event in events { #[allow(unused_mut)] let mut event: SyncTimelineEvent = event.into(); @@ -496,7 +498,7 @@ impl BaseClient { Ok(user_ids) } - async fn handle_room_account_data( + pub(crate) async fn handle_room_account_data( &self, room_id: &RoomId, events: &[Raw], @@ -509,7 +511,7 @@ impl BaseClient { } } - async fn handle_account_data( + pub(crate) async fn handle_account_data( &self, events: &[Raw], changes: &mut StateChanges, @@ -552,6 +554,31 @@ impl BaseClient { changes.account_data = account_data; } + #[cfg(feature = "e2e-encryption")] + pub(crate) async fn preprocess_to_device_events( + &self, + to_device_events: Vec>, + changed_devices: &api::sync::sync_events::DeviceLists, + one_time_keys_counts: &BTreeMap, + unused_fallback_keys: Option<&[ruma::DeviceKeyAlgorithm]>, + ) -> Result>> { + if let Some(o) = self.olm_machine() { + // Let the crypto machine handle the sync response, this + // decrypts to-device events, but leaves room events alone. + // This makes sure that we have the decryption keys for the room + // events at hand. + Ok(o.receive_sync_changes( + to_device_events, + changed_devices, + one_time_keys_counts, + unused_fallback_keys, + ) + .await?) + } else { + Ok(to_device_events) + } + } + /// Receive a response from a sync call. /// /// # Arguments @@ -582,25 +609,17 @@ impl BaseClient { } let now = Instant::now(); + let to_device_events = to_device.events; #[cfg(feature = "e2e-encryption")] - let to_device = { - if let Some(o) = self.olm_machine() { - // Let the crypto machine handle the sync response, this - // decrypts to-device events, but leaves room events alone. - // This makes sure that we have the decryption keys for the room - // events at hand. - o.receive_sync_changes( - to_device, - &device_lists, - &device_one_time_keys_count, - device_unused_fallback_key_types.as_deref(), - ) - .await? - } else { - to_device - } - }; + let to_device_events = self + .preprocess_to_device_events( + to_device_events, + &device_lists, + &device_one_time_keys_count, + device_unused_fallback_key_types.as_deref(), + ) + .await?; let mut changes = StateChanges::new(next_batch.clone()); let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); @@ -644,7 +663,9 @@ impl BaseClient { let timeline = self .handle_timeline( &room, - new_info.timeline, + new_info.timeline.limited, + new_info.timeline.events, + new_info.timeline.prev_batch, &push_rules, &mut user_ids, &mut room_info, @@ -683,7 +704,7 @@ impl BaseClient { JoinedRoom::new( timeline, new_info.state, - new_info.account_data, + new_info.account_data.events, new_info.ephemeral, notification_count, ), @@ -709,7 +730,9 @@ impl BaseClient { let timeline = self .handle_timeline( &room, - new_info.timeline, + new_info.timeline.limited, + new_info.timeline.events, + new_info.timeline.prev_batch, &push_rules, &mut user_ids, &mut room_info, @@ -771,8 +794,8 @@ impl BaseClient { next_batch, rooms: new_rooms, presence, - account_data, - to_device, + account_data: account_data.events, + to_device_events, device_lists, device_one_time_keys_count: device_one_time_keys_count .into_iter() diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 92c062864..c18c0293a 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -30,60 +30,68 @@ impl BaseClient { // next_batch, rooms, lists, + extensions, // FIXME: missing compared to v3::Response //presence, - //account_data, - //to_device, - //device_lists, - //device_one_time_keys_count, - //device_unused_fallback_key_types, .. } = response; - // FIXME not yet supported by sliding sync. see - // https://github.com/matrix-org/matrix-rust-sdk/issues/1014 - // #[cfg(feature = "encryption")] - // let to_device = { - // if let Some(o) = self.olm_machine().await { - // // Let the crypto machine handle the sync response, this - // // decrypts to-device events, but leaves room events alone. - // // This makes sure that we have the decryption keys for the room - // // events at hand. - // o.receive_sync_changes( - // to_device, - // &device_lists, - // &device_one_time_keys_count, - // device_unused_fallback_key_types.as_deref(), - // ) - // .await? - // } else { - // to_device - // } - // }; - - if rooms.is_empty() { - // nothing for us to handle here + if rooms.is_empty() && extensions.is_empty() { + // we received a room reshuffling event only, there won't be anything for us to + // process. stop early return Ok(SyncResponse::default()); }; + let v4::Extensions { to_device, e2ee, account_data, .. } = extensions; + + let to_device_events = to_device.map(|v4| v4.events).unwrap_or_default(); + + #[cfg(feature = "e2e-encryption")] + let to_device_events = { + if let Some(e2ee) = &e2ee { + self.preprocess_to_device_events( + to_device_events, + &e2ee.device_lists, + &e2ee.device_one_time_keys_count, + e2ee.device_unused_fallback_key_types.as_deref(), + ) + .await? + } else { + to_device_events + } + }; + + let (device_lists, device_one_time_keys_count) = e2ee + .map(|e2ee| { + ( + e2ee.device_lists, + e2ee.device_one_time_keys_count + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + }) + .unwrap_or_default(); + let store = self.store.clone(); let mut changes = StateChanges::default(); let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); - // FIXME not yet supported by sliding sync. - // self.handle_account_data(&account_data.events, &mut changes).await; + if let Some(global_data) = account_data.as_ref().map(|a| &a.global) { + self.handle_account_data(global_data, &mut changes).await; + } - let _push_rules = self.get_push_rules(&changes).await?; + let push_rules = self.get_push_rules(&changes).await?; let mut new_rooms = Rooms::default(); - for (room_id, room_data) in &rooms { + for (room_id, room_data) in rooms.into_iter() { if !room_data.invite_state.is_empty() { let invite_states = &room_data.invite_state; - let room = store.get_or_create_stripped_room(room_id).await; + let room = store.get_or_create_stripped_room(&room_id).await; let mut room_info = room.clone_info(); - if let Some(r) = store.get_room(room_id) { + 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 changes.add_room(room_info); @@ -96,7 +104,7 @@ impl BaseClient { v3::InvitedRoom::from(v3::InviteState::from(invite_states.clone())), ); } else { - let room = store.get_or_create_room(room_id, RoomType::Joined).await; + 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 @@ -105,18 +113,16 @@ impl BaseClient { room_info.set_prev_batch(room_data.prev_batch.as_deref()); - let user_ids = if room_data.required_state.is_empty() { - None - } else { - Some( - self.handle_state( - &room_data.required_state, - &mut room_info, - &mut changes, - &mut ambiguity_cache, - ) - .await?, + let mut user_ids = if !room_data.required_state.is_empty() { + self.handle_state( + &room_data.required_state, + &mut room_info, + &mut changes, + &mut ambiguity_cache, ) + .await? + } else { + Default::default() }; // FIXME not yet supported by sliding sync. see @@ -130,36 +136,34 @@ impl BaseClient { // changes.add_receipts(&room_id, event); // } - // FIXME not yet supported by sliding sync. - // self.handle_room_account_data(&room_id, &room_data.account_data.events, &mut - // changes) .await; + let room_account_data = if let Some(inner_account_data) = &account_data { + if let Some(events) = inner_account_data.rooms.get(&room_id) { + self.handle_room_account_data(&room_id, events, &mut changes).await; + Some(events.to_vec()) + } else { + None + } + } else { + None + }; - // FIXME not yet supported by sliding sync. - // if room_data.timeline.limited { - // room_info.mark_members_missing(); - // } + if room_data.limited { + room_info.mark_members_missing(); + } - // let timeline = self - // .handle_timeline( - // &room, - // room_data.timeline, - // &push_rules, - // &mut room_info, - // &mut changes, - // &mut ambiguity_cache, - // &mut user_ids, - // ) - // .await?; - - // let timeline_slice = TimelineSlice::new( - // timeline.events.clone(), - // next_batch.clone(), - // timeline.prev_batch.clone(), - // timeline.limited, - // true, - // ); - - // changes.add_timeline(&room_id, timeline_slice); + let timeline = self + .handle_timeline( + &room, + room_data.limited, + room_data.timeline, + room_data.prev_batch, + &push_rules, + &mut user_ids, + &mut room_info, + &mut changes, + &mut ambiguity_cache, + ) + .await?; #[cfg(feature = "e2e-encryption")] if room_info.is_encrypted() { @@ -168,15 +172,15 @@ impl BaseClient { // The room turned on encryption in this sync, we need // to also get all the existing users and mark them for // tracking. - let joined = store.get_joined_user_ids(room_id).await?; - let invited = store.get_invited_user_ids(room_id).await?; + let joined = store.get_joined_user_ids(&room_id).await?; + let invited = store.get_invited_user_ids(&room_id).await?; let user_ids: Vec<&UserId> = joined.iter().chain(&invited).map(Deref::deref).collect(); o.update_tracked_users(user_ids).await } - if let Some(user_ids) = user_ids { + if !user_ids.is_empty() { o.update_tracked_users(user_ids.iter().map(Deref::deref)).await; } } @@ -187,9 +191,9 @@ impl BaseClient { new_rooms.join.insert( room_id.clone(), JoinedRoom::new( - Default::default(), //timeline, + timeline, v3::State::with_events(room_data.required_state.clone()), - Default::default(), // room_info.account_data, + room_account_data.unwrap_or_default(), Default::default(), // room_info.ephemeral, notification_count, ), @@ -199,9 +203,13 @@ impl BaseClient { } } - // FIXME not yet supported by sliding sync. see - // https://github.com/matrix-org/matrix-rust-sdk/issues/1014 - // self.handle_account_data(&account_data.events, &mut changes).await; + // TODO remove this, we're processing account data events here again + // because we want to have the push rules in place before we process + // rooms and their events, but we want to create the rooms before we + // process the `m.direct` account data event. + if let Some(global_data) = account_data.as_ref().map(|a| &a.global) { + self.handle_account_data(global_data, &mut changes).await; + } // FIXME not yet supported by sliding sync. // changes.presence = presence @@ -228,10 +236,10 @@ impl BaseClient { notifications: changes.notifications, // FIXME not yet supported by sliding sync. presence: Default::default(), - account_data: Default::default(), - to_device: Default::default(), - device_lists: Default::default(), - device_one_time_keys_count: Default::default(), + account_data: account_data.map(|a| a.global).unwrap_or_default(), + to_device_events, + device_lists, + device_one_time_keys_count, }) } } diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index ffe6e06d8..e573b59dd 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -4,11 +4,8 @@ use ruma::{ api::client::{ push::get_notifications::v3::Notification, sync::sync_events::{ - v3::{ - DeviceLists, Ephemeral, GlobalAccountData, InvitedRoom, Presence, RoomAccountData, - State, ToDevice, - }, - UnreadNotificationsCount as RumaUnreadNotificationsCount, + v3::{Ephemeral, InvitedRoom, Presence, RoomAccountData, State}, + DeviceLists, UnreadNotificationsCount as RumaUnreadNotificationsCount, }, }, events::{ @@ -16,7 +13,8 @@ use ruma::{ MembershipState, RoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent, SyncRoomMemberEvent, }, - AnySyncTimelineEvent, AnyTimelineEvent, + AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnySyncTimelineEvent, AnyTimelineEvent, + AnyToDeviceEvent, }, serde::Raw, DeviceKeyAlgorithm, EventId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, @@ -133,9 +131,9 @@ pub struct SyncResponse { /// Updates to the presence status of other users. pub presence: Presence, /// The global private data created by this user. - pub account_data: GlobalAccountData, + pub account_data: Vec>, /// Messages sent directly between devices. - pub to_device: ToDevice, + pub to_device_events: Vec>, /// Information on E2E device updates. /// /// Only present on an incremental sync. @@ -187,7 +185,7 @@ pub struct JoinedRoom { /// true). pub state: State, /// The private data that this user has attached to this room. - pub account_data: RoomAccountData, + pub account_data: Vec>, /// The ephemeral events in the room that aren't recorded in the timeline or /// state of the room. e.g. typing. pub ephemeral: Ephemeral, @@ -197,7 +195,7 @@ impl JoinedRoom { pub fn new( timeline: Timeline, state: State, - account_data: RoomAccountData, + account_data: Vec>, ephemeral: Ephemeral, unread_notifications: UnreadNotificationsCount, ) -> Self { diff --git a/crates/matrix-sdk-crypto/README.md b/crates/matrix-sdk-crypto/README.md index b8dc9ad80..a7a7f7a69 100644 --- a/crates/matrix-sdk-crypto/README.md +++ b/crates/matrix-sdk-crypto/README.md @@ -30,14 +30,13 @@ async fn main() -> Result<(), OlmError> { let alice = user_id!("@alice:example.org"); let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await; - let to_device_events = ToDevice::default(); let changed_devices = DeviceLists::default(); let one_time_key_counts = BTreeMap::default(); let unused_fallback_keys = Some(Vec::new()); // Push changes that the server sent to us in a sync response. let decrypted_to_device = machine.receive_sync_changes( - to_device_events, + vec![], &changed_devices, &one_time_key_counts, unused_fallback_keys.as_deref(), diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index fbdec2bf2..382727794 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -31,11 +31,12 @@ use ruma::{ upload_keys, upload_signatures::v3::Request as UploadSignaturesRequest, }, - sync::sync_events::v3::{DeviceLists, ToDevice}, + sync::sync_events::DeviceLists, }, assign, events::{ - secret::request::SecretName, AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEventContent, + secret::request::SecretName, AnyMessageLikeEvent, AnyTimelineEvent, AnyToDeviceEvent, + MessageLikeEventContent, }, serde::Raw, DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId, @@ -891,11 +892,11 @@ impl OlmMachine { /// [`decrypt_room_event`]: #method.decrypt_room_event pub async fn receive_sync_changes( &self, - to_device_events: ToDevice, + to_device_events: Vec>, changed_devices: &DeviceLists, one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[DeviceKeyAlgorithm]>, - ) -> OlmResult { + ) -> OlmResult>> { // Remove verification objects that have expired or are done. let mut events = self.verification_machine.garbage_collect(); @@ -912,7 +913,7 @@ impl OlmMachine { } } - for mut raw_event in to_device_events.events { + for mut raw_event in to_device_events { let event: ToDeviceEvents = match raw_event.deserialize_as() { Ok(e) => e, Err(e) => { @@ -1002,10 +1003,7 @@ impl OlmMachine { self.store.save_changes(changes).await?; - let mut to_device = ToDevice::new(); - to_device.events = events; - - Ok(to_device) + Ok(events) } /// Request a room key from our devices. @@ -1586,7 +1584,7 @@ pub(crate) mod tests { api::{ client::{ keys::{claim_keys, get_keys, upload_keys}, - sync::sync_events::v3::{DeviceLists, ToDevice}, + sync::sync_events::v3::DeviceLists, }, IncomingResponse, }, @@ -2005,15 +2003,12 @@ pub(crate) mod tests { let alice_session = alice.group_session_manager.get_outbound_group_session(room_id).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event); - let decrypted = bob - .receive_sync_changes(to_device, &Default::default(), &Default::default(), None) + .receive_sync_changes(vec![event], &Default::default(), &Default::default(), None) .await .unwrap(); - let event = decrypted.events[0].deserialize().unwrap(); + let event = decrypted[0].deserialize().unwrap(); if let AnyToDeviceEvent::RoomKey(event) = event { assert_eq!(&event.sender, alice.user_id()); @@ -2342,13 +2337,13 @@ pub(crate) mod tests { other: Default::default(), }; let event = json_convert(&event).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event); let changed_devices = DeviceLists::new(); let key_counts = Default::default(); - let _ = - bob.receive_sync_changes(to_device, &changed_devices, &key_counts, None).await.unwrap(); + let _ = bob + .receive_sync_changes(vec![event], &changed_devices, &key_counts, None) + .await + .unwrap(); let group_session = GroupSession::new(SessionConfig::version_1()); let session_key = group_session.session_key(); @@ -2378,10 +2373,8 @@ pub(crate) mod tests { ); let event: Raw = json_convert(&event).unwrap(); - let mut to_device = ToDevice::new(); - to_device.events.push(event.clone()); - bob.receive_sync_changes(to_device, &changed_devices, &key_counts, None).await.unwrap(); + bob.receive_sync_changes(vec![event], &changed_devices, &key_counts, None).await.unwrap(); let session = bob.store.get_inbound_group_session(room_id, &session_id).await; diff --git a/crates/matrix-sdk/src/sliding_sync.rs b/crates/matrix-sdk/src/sliding_sync.rs index ea2fc19cf..4ba3afe12 100644 --- a/crates/matrix-sdk/src/sliding_sync.rs +++ b/crates/matrix-sdk/src/sliding_sync.rs @@ -17,12 +17,14 @@ use std::{fmt::Debug, sync::Arc}; use anyhow::{bail, Context}; use futures_core::stream::Stream; -use matrix_sdk_base::deserialized_responses::SyncResponse; +use futures_signals::signal::Mutable; +use matrix_sdk_base::deserialized_responses::{SyncResponse, SyncTimelineEvent}; use ruma::{ - api::client::sync::sync_events::v4, + api::client::sync::sync_events::v4::{ + self, AccountDataConfig, E2EEConfig, ExtensionsConfig, ToDeviceConfig, + }, assign, - events::{AnySyncTimelineEvent, RoomEventType}, - serde::Raw, + events::RoomEventType, OwnedRoomId, RoomId, UInt, }; use url::Url; @@ -89,28 +91,30 @@ impl RoomListEntry { } } -pub type AliveRoomTimeline = - Arc>>; +pub type AliveRoomTimeline = Arc>; /// Room info as giving by the SlidingSync Feature. #[derive(Debug, Clone)] pub struct SlidingSyncRoom { room_id: OwnedRoomId, inner: v4::SlidingSyncRoom, - is_loading_more: futures_signals::signal::Mutable, - prev_batch: futures_signals::signal::Mutable>, + is_loading_more: Mutable, + prev_batch: Mutable>, timeline: AliveRoomTimeline, } impl SlidingSyncRoom { - fn from(room_id: OwnedRoomId, mut inner: v4::SlidingSyncRoom) -> Self { - let v4::SlidingSyncRoom { timeline, .. } = inner; + fn from( + room_id: OwnedRoomId, + mut inner: v4::SlidingSyncRoom, + timeline: Vec, + ) -> Self { // we overwrite to only keep one copy inner.timeline = vec![]; Self { room_id, - is_loading_more: futures_signals::signal::Mutable::new(false), - prev_batch: futures_signals::signal::Mutable::new(inner.prev_batch.clone()), + is_loading_more: Mutable::new(false), + prev_batch: Mutable::new(inner.prev_batch.clone()), timeline: Arc::new(futures_signals::signal_vec::MutableVec::new_with_values(timeline)), inner, } @@ -141,7 +145,7 @@ impl SlidingSyncRoom { self.inner.name.as_deref() } - fn update(&mut self, room_data: &v4::SlidingSyncRoom) { + fn update(&mut self, room_data: &v4::SlidingSyncRoom, timeline: Vec) { let v4::SlidingSyncRoom { name, initial, @@ -150,7 +154,6 @@ impl SlidingSyncRoom { unread_notifications, required_state, prev_batch, - timeline, .. } = room_data; @@ -178,8 +181,8 @@ impl SlidingSyncRoom { if !timeline.is_empty() { let mut ref_timeline = self.timeline.lock_mut(); - for e in timeline { - ref_timeline.push_cloned(e.clone()); + for e in timeline.into_iter() { + ref_timeline.push_cloned(e); } } } @@ -192,11 +195,11 @@ impl std::ops::Deref for SlidingSyncRoom { } } -type ViewState = futures_signals::signal::Mutable; -type SyncMode = futures_signals::signal::Mutable; -type PosState = futures_signals::signal::Mutable>; -type RangeState = futures_signals::signal::Mutable>; -type RoomsCount = futures_signals::signal::Mutable>; +type ViewState = Mutable; +type SyncMode = Mutable; +type PosState = Mutable>; +type RangeState = Mutable>; +type RoomsCount = Mutable>; type RoomsList = Arc>; type RoomsMap = Arc>; type RoomsSubscriptions = @@ -243,6 +246,9 @@ pub struct SlidingSync { /// The rooms details #[builder(private, default)] rooms: RoomsMap, + + #[builder(private, default)] + extensions: Mutable>, } impl SlidingSyncBuilder { @@ -275,6 +281,91 @@ impl SlidingSyncBuilder { self.views = Some(views); self } + + /// Activate e2ee, to-device-message and account data extensions if not yet + /// configured. + /// + /// Will leave any extension configuration found untouched, so the order + /// does not matter. + pub fn with_common_extensions(mut self) -> Self { + { + let mut lock = self.extensions.get_or_insert_with(Default::default).lock_mut(); + let mut cfg = lock.get_or_insert_with(Default::default); + if cfg.to_device.is_none() { + cfg.to_device = Some(assign!(ToDeviceConfig::default(), {enabled : Some(true)})); + } + + if cfg.e2ee.is_none() { + cfg.e2ee = Some(assign!(E2EEConfig::default(), {enabled : Some(true)})); + } + + if cfg.account_data.is_none() { + cfg.account_data = + Some(assign!(AccountDataConfig::default(), {enabled : Some(true)})); + } + } + self + } + + /// Set the E2EE extension configuration. + pub fn with_e2ee_extension(mut self, e2ee: E2EEConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .e2ee = Some(e2ee); + self + } + + /// Unset the E2EE extension configuration. + pub fn without_e2ee_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .e2ee = None; + self + } + + /// Set the ToDevice extension configuration. + pub fn with_to_device_extension(mut self, to_device: ToDeviceConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .to_device = Some(to_device); + self + } + + /// Unset the ToDevice extension configuration. + pub fn without_to_device_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .to_device = None; + self + } + + /// Set the account data extension configuration. + pub fn with_account_data_extension(mut self, account_data: AccountDataConfig) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .account_data = Some(account_data); + self + } + + /// Unset the account data extension configuration. + pub fn without_account_data_extension(mut self) -> Self { + self.extensions + .get_or_insert_with(Default::default) + .lock_mut() + .get_or_insert_with(Default::default) + .account_data = None; + self + } } impl SlidingSync { @@ -329,6 +420,15 @@ impl SlidingSync { self.rooms.lock_ref().get(&room_id).cloned() } + fn update_to_device_since(&self, since: String) { + self.extensions + .lock_mut() + .get_or_insert_with(Default::default) + .to_device + .get_or_insert_with(Default::default) + .since = Some(since); + } + /// Lookup a set of rooms pub fn get_rooms>( &self, @@ -343,7 +443,7 @@ impl SlidingSync { resp: v4::Response, views: &[SlidingSyncView], ) -> anyhow::Result { - self.client.process_sliding_sync(resp.clone()).await?; + let mut processed = self.client.process_sliding_sync(resp.clone()).await?; tracing::info!("main client processed."); self.pos.replace(Some(resp.pos)); let mut updated_views = Vec::new(); @@ -362,20 +462,33 @@ impl SlidingSync { let mut rooms = Vec::new(); let mut rooms_map = self.rooms.lock_mut(); - for (id, room_data) in resp.rooms.iter() { - if let Some(mut r) = rooms_map.remove(id) { - r.update(room_data); + for (id, mut room_data) in resp.rooms.into_iter() { + let timeline = if let Some(joined_room) = processed.rooms.join.remove(&id) { + joined_room.timeline.events + } else { + let events = room_data.timeline.into_iter().map(Into::into).collect(); + room_data.timeline = vec![]; + events + }; + + if let Some(mut r) = rooms_map.remove(&id) { + r.update(&room_data, timeline); rooms_map.insert_cloned(id.clone(), r); rooms.push(id.clone()); } else { rooms_map.insert_cloned( id.clone(), - SlidingSyncRoom::from(id.clone(), room_data.clone()), + SlidingSyncRoom::from(id.clone(), room_data, timeline), ); - rooms.push(id.clone()); + rooms.push(id); } } + // Update the `to-device` next-batch if found. + if let Some(to_device_since) = resp.extensions.to_device.map(|t| t.next_batch) { + self.update_to_device_since(to_device_since) + } + Ok(UpdateSummary { views: updated_views, rooms }) } @@ -386,9 +499,7 @@ impl SlidingSync { &self, ) -> anyhow::Result> + '_> { let views = self.views.lock_ref().to_vec(); - let _pos = self.pos.clone(); - - // FIXME: hack for while the sliding sync server is on a proxy + let extensions = self.extensions.clone(); let client = self.client.clone(); Ok(async_stream::try_stream! { @@ -398,6 +509,11 @@ impl SlidingSync { .map(SlidingSyncView::request_generator) .collect(); loop { + #[cfg(feature = "e2e-encryption")] + if let Err(e) = client.send_outgoing_requests().await { + tracing::error!(error = ?e, "Error while sending outgoing E2EE requests"); + } + let mut requests = Vec::new(); let mut new_remaining_generators = Vec::new(); let mut new_remaining_views = Vec::new(); @@ -431,6 +547,7 @@ impl SlidingSync { pos: pos.as_deref(), room_subscriptions, unsubscribe_rooms: &unsubscribe_rooms, + extensions: extensions.lock_mut().take().unwrap_or_default(), // extensions are sticky, we pop them here once }); tracing::debug!("requesting"); let resp = client diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index 1145dd971..48491d5e9 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -30,17 +30,16 @@ impl Client { rooms, presence, account_data, - to_device, + to_device_events, device_lists: _, device_one_time_keys_count: _, ambiguity_changes: _, notifications, } = &response; - self.handle_sync_events(HandlerKind::GlobalAccountData, &None, &account_data.events) - .await?; + self.handle_sync_events(HandlerKind::GlobalAccountData, &None, account_data).await?; self.handle_sync_events(HandlerKind::Presence, &None, &presence.events).await?; - self.handle_sync_events(HandlerKind::ToDevice, &None, &to_device.events).await?; + self.handle_sync_events(HandlerKind::ToDevice, &None, to_device_events).await?; for (room_id, room_info) in &rooms.join { let room = self.get_room(room_id); @@ -54,8 +53,7 @@ impl Client { self.handle_sync_events(HandlerKind::EphemeralRoomData, &room, &ephemeral.events) .await?; - self.handle_sync_events(HandlerKind::RoomAccountData, &room, &account_data.events) - .await?; + self.handle_sync_events(HandlerKind::RoomAccountData, &room, account_data).await?; self.handle_sync_state_events(&room, &state.events).await?; self.handle_sync_timeline_events(&room, &timeline.events).await?; } diff --git a/labs/jack-in/Cargo.toml b/labs/jack-in/Cargo.toml index d9ad0e3f0..47a5acb98 100644 --- a/labs/jack-in/Cargo.toml +++ b/labs/jack-in/Cargo.toml @@ -9,18 +9,22 @@ edition = "2021" file-logging = ["dep:log4rs"] [dependencies] -tuirealm = "~1.7.1" -matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync"] , version = "0.6.0" } -matrix-sdk-common = { path = "../../crates/matrix-sdk-common" , version = "0.6.0" } -structopt = "0.3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } +app_dirs2 = "2" +dialoguer = "0.10.2" +eyre = "0.6" futures = { version = "0.3.1" } futures-signals = "0.3.24" +matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync"], version = "0.6.0" } +matrix-sdk-common = { path = "../../crates/matrix-sdk-common", version = "0.6.0" } +matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", features = ["state-store", "crypto-store"], version = "0.2.0" } +sanitize-filename-reader-friendly = "2.2.1" +serde_json = "1.0.85" +structopt = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } tracing-flame = "0.2" tracing-subscriber = "0.3.15" -eyre = "0.6" - tui-logger = "0.8.0" +tuirealm = "~1.7.1" # file-logging specials tracing = { version = "0.1.35", features = ["log"] } diff --git a/labs/jack-in/src/client/mod.rs b/labs/jack-in/src/client/mod.rs index d9e0156f5..8181d8465 100644 --- a/labs/jack-in/src/client/mod.rs +++ b/labs/jack-in/src/client/mod.rs @@ -1,7 +1,7 @@ use eyre::{Result, WrapErr}; use futures::{pin_mut, StreamExt}; use tokio::sync::mpsc; -use tracing::{error, warn}; +use tracing::{error, info, warn}; pub mod state; @@ -12,13 +12,14 @@ pub async fn run_client( sliding_sync_proxy: String, tx: mpsc::Sender, ) -> Result<()> { - warn!("Starting sliding sync now"); + info!("Starting sliding sync now"); let builder = client.sliding_sync().await; let full_sync_view = SlidingSyncViewBuilder::default_with_fullsync().timeline_limit(10u32).build()?; let syncer = builder .homeserver(sliding_sync_proxy.parse().wrap_err("can't parse sync proxy")?) .add_view(full_sync_view) + .with_common_extensions() .build()?; let stream = syncer.stream().await.expect("we can build the stream"); let view = syncer.views.lock_ref().first().expect("we have the full syncer there").clone(); @@ -26,8 +27,13 @@ pub async fn run_client( let mut ssync_state = state::SlidingSyncState::new(view); tx.send(ssync_state.clone()).await?; + info!("starting polling"); + pin_mut!(stream); - let _first_poll = stream.next().await; + if let Some(Err(e)) = stream.next().await { + error!("Initial Query on sliding sync failed: {:#?}", e); + return Ok(()); + } let view_state = state.read_only().get_cloned(); if view_state != SlidingSyncState::CatchingUp { warn!("Sliding Query failed: {:#?}", view_state); @@ -38,25 +44,26 @@ pub async fn run_client( ssync_state.set_first_render_now(); tx.send(ssync_state.clone()).await?; } - warn!("Done initial sliding sync"); + info!("Done initial sliding sync"); loop { match stream.next().await { Some(Ok(_)) => { // we are switching into live updates mode next. ignoring + let state = state.read_only().get_cloned(); - if state.read_only().get_cloned() == SlidingSyncState::Live { - warn!("Reached live sync"); + if state == SlidingSyncState::Live { + info!("Reached live sync"); break; } let _ = tx.send(ssync_state.clone()).await; } Some(Err(e)) => { - warn!("Error: {:}", e); + error!("Error: {:}", e); break; } None => { - warn!("Never reached live state"); + error!("Never reached live state"); break; } } @@ -88,7 +95,7 @@ pub async fn run_client( } match update { Ok(update) => { - warn!("Live update received: {:?}", update); + info!("Live update received: {:?}", update); tx.send(ssync_state.clone()).await?; err_counter = 0; } diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index 2e2e71fe4..3610eed68 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -79,7 +79,7 @@ impl Details { .timeline() .lock_ref() .iter() - .filter_map(|d| d.deserialize().ok()) + .filter_map(|d| d.event.deserialize().ok()) .map(|e| e.into_full_event(room_id.clone())) .collect(); timeline.reverse(); diff --git a/labs/jack-in/src/main.rs b/labs/jack-in/src/main.rs index 1a33c2668..52c11bab7 100644 --- a/labs/jack-in/src/main.rs +++ b/labs/jack-in/src/main.rs @@ -4,16 +4,22 @@ use std::path::{Path, PathBuf}; +use app_dirs2::{app_root, AppDataType, AppInfo}; +use dialoguer::{theme::ColorfulTheme, Password}; use eyre::{eyre, Result}; use matrix_sdk::{ - ruma::{OwnedDeviceId, OwnedRoomId, OwnedUserId}, - Client, Session, + ruma::{OwnedRoomId, OwnedUserId}, + Client, }; -use tracing::{log::LevelFilter, warn}; +use matrix_sdk_sled::make_store_config; +use sanitize_filename_reader_friendly::sanitize; +use tracing::{log, warn}; use tracing_flame::FlameLayer; use tracing_subscriber::prelude::*; use tuirealm::{application::PollStrategy, Event, Update}; +const APP_INFO: AppInfo = AppInfo { name: "jack-in", author: "Matrix-Rust-SDK Core Team" }; + // -- internal mod app; mod client; @@ -75,14 +81,27 @@ struct Opt { #[structopt(short, long, default_value = "http://localhost:8008", env = "JACKIN_SYNC_PROXY")] sliding_sync_proxy: String, - /// Your access token to connect via the - #[structopt(short, long, env = "JACKIN_TOKEN")] - token: String, + /// The password of your account. If not given and no database found, it + /// will prompt you for it + #[structopt(short, long, env = "JACKIN_PASSWORD")] + password: Option, - /// The userID associated with this access token + /// Create a fresh database, drop all existing cache + #[structopt(long)] + fresh: bool, + + /// RUST_LOG log-levels + #[structopt(short, long, env = "JACKIN_LOG", default_value = "jack_in=info,warn")] + log: String, + + /// The userID to log in with #[structopt(short, long, env = "JACKIN_USER")] user: String, + /// The password to encrypt the store with + #[structopt(long, env = "JACKIN_STORE_PASSWORD")] + store_pass: Option, + #[structopt(long)] /// Activate tracing and write the flamegraph to the specified file flames: Option, @@ -112,7 +131,6 @@ async fn main() -> Result<()> { let opt = Opt::from_args(); let user_id: OwnedUserId = opt.user.clone().parse()?; - let device_id: OwnedDeviceId = "XdftAsd".into(); if let Some(ref p) = opt.flames { setup_flames(p.as_path()); @@ -138,41 +156,104 @@ async fn main() -> Result<()> { .logger( Logger::builder() .appender("file") - .build("matrix_sdk::sliding_sync", LevelFilter::Trace), + .build("matrix_sdk::sliding_sync", log::LevelFilter::Trace), ) .logger( Logger::builder() .appender("file") - .build("matrix_sdk::http_client", LevelFilter::Debug), + .build("matrix_sdk::http_client", log::LevelFilter::Debug), ) .logger( Logger::builder() .appender("file") - .build("matrix_sdk_base::sliding_sync", LevelFilter::Debug), + .build("matrix_sdk_base::sliding_sync", log::LevelFilter::Debug), ) - .logger(Logger::builder().appender("file").build("reqwest", LevelFilter::Trace)) - .logger(Logger::builder().appender("file").build("matrix_sdk", LevelFilter::Warn)) - .build(Root::builder().build(LevelFilter::Error)) + .logger( + Logger::builder().appender("file").build("reqwest", log::LevelFilter::Trace), + ) + .logger( + Logger::builder().appender("file").build("matrix_sdk", log::LevelFilter::Warn), + ) + .build(Root::builder().build(log::LevelFilter::Error)) .unwrap(); log4rs::init_config(config).expect("Logging with log4rs failed to initialize"); } #[cfg(not(feature = "file-logging"))] { - tui_logger::init_logger(LevelFilter::Trace).expect("Could not set up logging"); - tui_logger::set_default_level(LevelFilter::Warn); - tui_logger::set_level_for_target("matrix_sdk", LevelFilter::Warn); + tui_logger::init_logger(log::LevelFilter::Trace).unwrap(); + // Set default level for unknown targets to Trace + tui_logger::set_default_level(log::LevelFilter::Warn); + + for pair in opt.log.split(',') { + if let Some((name, lvl)) = pair.split_once('=') { + let level = match lvl.to_lowercase().as_str() { + "trace" => log::LevelFilter::Trace, + "debug" => log::LevelFilter::Debug, + "info" => log::LevelFilter::Info, + "warn" => log::LevelFilter::Warn, + "error" => log::LevelFilter::Error, + // nothing means error + _ => continue, + }; + tui_logger::set_level_for_target(name, level); + } else { + let level = match pair.to_lowercase().as_str() { + "trace" => log::LevelFilter::Trace, + "debug" => log::LevelFilter::Debug, + "info" => log::LevelFilter::Info, + "warn" => log::LevelFilter::Warn, + "error" => log::LevelFilter::Error, + // nothing means error + _ => continue, + }; + tui_logger::set_default_level(level); + } + } } } - let client = Client::builder().server_name(user_id.server_name()).build().await?; - let session = Session { - access_token: opt.token.clone(), - refresh_token: None, - user_id: user_id.clone(), - device_id, - }; - client.restore_login(session).await?; + let data_path = app_root(AppDataType::UserData, &APP_INFO)?.join(sanitize(user_id.as_str())); + if opt.fresh { + // drop the database first; + std::fs::remove_dir_all(&data_path)?; + } + std::fs::create_dir_all(&data_path)?; + let store_config = make_store_config(&data_path, opt.store_pass.as_deref()).await?; + + let client = Client::builder() + .user_agent("jack-in") + .server_name(user_id.server_name()) + .store_config(store_config) + .build() + .await?; + + let session_key = b"jackin::session_token"; + + if let Some(session) = client + .store() + .get_custom_value(session_key) + .await? + .map(|v| serde_json::from_slice(&v)) + .transpose()? + { + tracing::info!("Restoring session from store"); + client.restore_login(session).await?; + } else { + let theme = ColorfulTheme::default(); + let password = match opt.password { + Some(ref pw) => pw.clone(), + _ => Password::with_theme(&theme) + .with_prompt(format!("Password for {user_id:} :")) + .interact()?, + }; + client.login_username(&user_id, &password).send().await?; + } + + if let Some(session) = client.session() { + client.store().set_custom_value(session_key, serde_json::to_vec(&session)?).await?; + } + let sliding_client = client.clone(); let proxy = opt.sliding_sync_proxy.clone();