From 0c00b90b221b5f00a9d850200953b1c1963647d1 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 9 Sep 2024 11:26:23 +0200 Subject: [PATCH 1/8] client-api: Replace server_name with via in knock_room and join_room_by_id_or_alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to MSC4156 Co-authored-by: Kévin Commaille <76261501+zecakeh@users.noreply.github.com> --- crates/ruma-client-api/CHANGELOG.md | 2 + .../ruma-client-api/src/knock/knock_room.rs | 222 +++++++++++++++- .../membership/join_room_by_id_or_alias.rs | 246 +++++++++++++++++- 3 files changed, 450 insertions(+), 20 deletions(-) diff --git a/crates/ruma-client-api/CHANGELOG.md b/crates/ruma-client-api/CHANGELOG.md index 8afb0402..bce33bf0 100644 --- a/crates/ruma-client-api/CHANGELOG.md +++ b/crates/ruma-client-api/CHANGELOG.md @@ -10,6 +10,8 @@ Breaking changes: - The `content_disposition` fields of `media::get_content::v3::Response` and `media::get_content_as_filename::v3::Response` use now the strongly typed `ContentDisposition` instead of strings. +- Replace `server_name` on `knock::knock_room::v3::Request` and + `membership::join_room_by_id_or_alias::v3::Request` with `via` as per MSC4156. Improvements: diff --git a/crates/ruma-client-api/src/knock/knock_room.rs b/crates/ruma-client-api/src/knock/knock_room.rs index e49103bf..ae69319e 100644 --- a/crates/ruma-client-api/src/knock/knock_room.rs +++ b/crates/ruma-client-api/src/knock/knock_room.rs @@ -8,7 +8,7 @@ pub mod v3 { //! [spec]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3knockroomidoralias use ruma_common::{ - api::{request, response, Metadata}, + api::{response, Metadata}, metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, }; @@ -23,22 +23,137 @@ pub mod v3 { }; /// Request type for the `knock_room` endpoint. - #[request(error = crate::Error)] + #[derive(Clone, Debug)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Request { /// The room the user should knock on. - #[ruma_api(path)] pub room_id_or_alias: OwnedRoomOrAliasId, /// The reason for joining a room. - #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// The servers to attempt to knock on the room through. /// /// One of the servers must be participating in the room. - #[ruma_api(query)] + /// + /// When serializing, this field is mapped to both `server_name` and `via` + /// with identical values. + /// + /// When deserializing, the value is read from `via` if it's not missing or + /// empty and `server_name` otherwise. + pub via: Vec, + } + + /// Data in the request's query string. + #[cfg_attr(feature = "client", derive(serde::Serialize))] + #[cfg_attr(feature = "server", derive(serde::Deserialize))] + struct RequestQuery { + /// The servers to attempt to knock on the room through. #[serde(default, skip_serializing_if = "<[_]>::is_empty")] - pub server_name: Vec, + via: Vec, + + /// The servers to attempt to knock on the room through. + /// + /// Deprecated in Matrix >1.11 in favour of `via`. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + server_name: Vec, + } + + /// Data in the request's body. + #[cfg_attr(feature = "client", derive(serde::Serialize))] + #[cfg_attr(feature = "server", derive(serde::Deserialize))] + struct RequestBody { + /// The reason for joining a room. + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + } + + #[cfg(feature = "client")] + impl ruma_common::api::OutgoingRequest for Request { + type EndpointError = crate::Error; + type IncomingResponse = Response; + + const METADATA: Metadata = METADATA; + + fn try_into_http_request( + self, + base_url: &str, + access_token: ruma_common::api::SendAccessToken<'_>, + considering_versions: &'_ [ruma_common::api::MatrixVersion], + ) -> Result, ruma_common::api::error::IntoHttpError> { + use http::header::{self, HeaderValue}; + + let query_string = serde_html_form::to_string(RequestQuery { + server_name: self.via.clone(), + via: self.via, + })?; + + let http_request = http::Request::builder() + .method(METADATA.method) + .uri(METADATA.make_endpoint_url( + considering_versions, + base_url, + &[&self.room_id_or_alias], + &query_string, + )?) + .header(header::CONTENT_TYPE, "application/json") + .header( + header::AUTHORIZATION, + HeaderValue::from_str(&format!( + "Bearer {}", + access_token + .get_required_for_endpoint() + .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)? + ))?, + ) + .body(ruma_common::serde::json_to_buf(&RequestBody { reason: self.reason })?)?; + + Ok(http_request) + } + } + + #[cfg(feature = "server")] + impl ruma_common::api::IncomingRequest for Request { + type EndpointError = crate::Error; + type OutgoingResponse = Response; + + const METADATA: Metadata = METADATA; + + fn try_from_http_request( + request: http::Request, + path_args: &[S], + ) -> Result + where + B: AsRef<[u8]>, + S: AsRef, + { + if request.method() != METADATA.method { + return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch { + expected: METADATA.method, + received: request.method().clone(), + }); + } + + let (room_id_or_alias,) = + serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::< + _, + serde::de::value::Error, + >::new( + path_args.iter().map(::std::convert::AsRef::as_ref), + ))?; + + let request_query: RequestQuery = + serde_html_form::from_str(request.uri().query().unwrap_or(""))?; + let via = if request_query.via.is_empty() { + request_query.server_name + } else { + request_query.via + }; + + let body: RequestBody = serde_json::from_slice(request.body().as_ref())?; + + Ok(Self { room_id_or_alias, reason: body.reason, via }) + } } /// Response type for the `knock_room` endpoint. @@ -51,7 +166,7 @@ pub mod v3 { impl Request { /// Creates a new `Request` with the given room ID or alias. pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self { - Self { room_id_or_alias, reason: None, server_name: vec![] } + Self { room_id_or_alias, reason: None, via: vec![] } } } @@ -61,4 +176,97 @@ pub mod v3 { Self { room_id } } } + + #[cfg(all(test, any(feature = "client", feature = "server")))] + mod tests { + use ruma_common::{ + api::{IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken}, + owned_room_id, owned_server_name, + }; + + use super::Request; + + #[cfg(feature = "client")] + #[test] + fn serialize_request() { + let mut req = Request::new(owned_room_id!("!foo:b.ar").into()); + req.via = vec![owned_server_name!("f.oo")]; + let req = req + .try_into_http_request::>( + "https://matrix.org", + SendAccessToken::IfRequired("tok"), + &[MatrixVersion::V1_1], + ) + .unwrap(); + assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo")); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_wrong_method() { + Request::try_from_http_request( + http::Request::builder() + .method(http::Method::GET) + .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .expect_err("Should not deserialize request with illegal method"); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_only_via() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_only_server_name() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?server_name=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_via_and_server_name() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/knock/!foo:b.ar?via=f.oo&server_name=b.ar") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + } } diff --git a/crates/ruma-client-api/src/membership/join_room_by_id_or_alias.rs b/crates/ruma-client-api/src/membership/join_room_by_id_or_alias.rs index 1c7d4598..128ee153 100644 --- a/crates/ruma-client-api/src/membership/join_room_by_id_or_alias.rs +++ b/crates/ruma-client-api/src/membership/join_room_by_id_or_alias.rs @@ -8,7 +8,7 @@ pub mod v3 { //! [spec]: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3joinroomidoralias use ruma_common::{ - api::{request, response, Metadata}, + api::{response, Metadata}, metadata, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, }; @@ -25,27 +25,154 @@ pub mod v3 { }; /// Request type for the `join_room_by_id_or_alias` endpoint. - #[request(error = crate::Error)] + #[derive(Clone, Debug)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Request { /// The room where the user should be invited. - #[ruma_api(path)] pub room_id_or_alias: OwnedRoomOrAliasId, - /// The servers to attempt to join the room through. - /// - /// One of the servers must be participating in the room. - #[ruma_api(query)] - #[serde(default, skip_serializing_if = "<[_]>::is_empty")] - pub server_name: Vec, - /// The signature of a `m.third_party_invite` token to prove that this user owns a third /// party identity which has been invited to the room. - #[serde(skip_serializing_if = "Option::is_none")] pub third_party_signed: Option, /// Optional reason for joining the room. - #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, + + /// The servers to attempt to join the room through. + /// + /// One of the servers must be participating in the room. + /// + /// When serializing, this field is mapped to both `server_name` and `via` + /// with identical values. + /// + /// When deserializing, the value is read from `via` if it's not missing or + /// empty and `server_name` otherwise. + pub via: Vec, + } + + /// Data in the request's query string. + #[cfg_attr(feature = "client", derive(serde::Serialize))] + #[cfg_attr(feature = "server", derive(serde::Deserialize))] + struct RequestQuery { + /// The servers to attempt to join the room through. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + via: Vec, + + /// The servers to attempt to join the room through. + /// + /// Deprecated in Matrix >1.11 in favour of `via`. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + server_name: Vec, + } + + /// Data in the request's body. + #[cfg_attr(feature = "client", derive(serde::Serialize))] + #[cfg_attr(feature = "server", derive(serde::Deserialize))] + struct RequestBody { + /// The signature of a `m.third_party_invite` token to prove that this user owns a third + /// party identity which has been invited to the room. + #[serde(skip_serializing_if = "Option::is_none")] + third_party_signed: Option, + + /// Optional reason for joining the room. + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + } + + #[cfg(feature = "client")] + impl ruma_common::api::OutgoingRequest for Request { + type EndpointError = crate::Error; + type IncomingResponse = Response; + + const METADATA: Metadata = METADATA; + + fn try_into_http_request( + self, + base_url: &str, + access_token: ruma_common::api::SendAccessToken<'_>, + considering_versions: &'_ [ruma_common::api::MatrixVersion], + ) -> Result, ruma_common::api::error::IntoHttpError> { + use http::header::{self, HeaderValue}; + + let query_string = serde_html_form::to_string(RequestQuery { + server_name: self.via.clone(), + via: self.via, + })?; + + let http_request = http::Request::builder() + .method(METADATA.method) + .uri(METADATA.make_endpoint_url( + considering_versions, + base_url, + &[&self.room_id_or_alias], + &query_string, + )?) + .header(header::CONTENT_TYPE, "application/json") + .header( + header::AUTHORIZATION, + HeaderValue::from_str(&format!( + "Bearer {}", + access_token + .get_required_for_endpoint() + .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)? + ))?, + ) + .body(ruma_common::serde::json_to_buf(&RequestBody { + third_party_signed: self.third_party_signed, + reason: self.reason, + })?)?; + + Ok(http_request) + } + } + + #[cfg(feature = "server")] + impl ruma_common::api::IncomingRequest for Request { + type EndpointError = crate::Error; + type OutgoingResponse = Response; + + const METADATA: Metadata = METADATA; + + fn try_from_http_request( + request: http::Request, + path_args: &[S], + ) -> Result + where + B: AsRef<[u8]>, + S: AsRef, + { + if request.method() != METADATA.method { + return Err(ruma_common::api::error::FromHttpRequestError::MethodMismatch { + expected: METADATA.method, + received: request.method().clone(), + }); + } + + let (room_id_or_alias,) = + serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::< + _, + serde::de::value::Error, + >::new( + path_args.iter().map(::std::convert::AsRef::as_ref), + ))?; + + let request_query: RequestQuery = + serde_html_form::from_str(request.uri().query().unwrap_or(""))?; + let via = if request_query.via.is_empty() { + request_query.server_name + } else { + request_query.via + }; + + let body: RequestBody = serde_json::from_slice(request.body().as_ref())?; + + Ok(Self { + room_id_or_alias, + reason: body.reason, + third_party_signed: body.third_party_signed, + via, + }) + } } /// Response type for the `join_room_by_id_or_alias` endpoint. @@ -58,7 +185,7 @@ pub mod v3 { impl Request { /// Creates a new `Request` with the given room ID or alias ID. pub fn new(room_id_or_alias: OwnedRoomOrAliasId) -> Self { - Self { room_id_or_alias, server_name: vec![], third_party_signed: None, reason: None } + Self { room_id_or_alias, via: vec![], third_party_signed: None, reason: None } } } @@ -68,4 +195,97 @@ pub mod v3 { Self { room_id } } } + + #[cfg(all(test, any(feature = "client", feature = "server")))] + mod tests { + use ruma_common::{ + api::{IncomingRequest as _, MatrixVersion, OutgoingRequest, SendAccessToken}, + owned_room_id, owned_server_name, + }; + + use super::Request; + + #[cfg(feature = "client")] + #[test] + fn serialize_request() { + let mut req = Request::new(owned_room_id!("!foo:b.ar").into()); + req.via = vec![owned_server_name!("f.oo")]; + let req = req + .try_into_http_request::>( + "https://matrix.org", + SendAccessToken::IfRequired("tok"), + &[MatrixVersion::V1_1], + ) + .unwrap(); + assert_eq!(req.uri().query(), Some("via=f.oo&server_name=f.oo")); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_wrong_method() { + Request::try_from_http_request( + http::Request::builder() + .method(http::Method::GET) + .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .expect_err("Should not deserialize request with illegal method"); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_only_via() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_only_server_name() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?server_name=f.oo") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + + #[cfg(feature = "server")] + #[test] + fn deserialize_request_via_and_server_name() { + let req = Request::try_from_http_request( + http::Request::builder() + .method(http::Method::POST) + .uri("https://matrix.org/_matrix/client/v3/join/!foo:b.ar?via=f.oo&server_name=b.ar") + .body(b"{ \"reason\": \"Let me in already!\" }" as &[u8]) + .unwrap(), + &["!foo:b.ar"], + ) + .unwrap(); + + assert_eq!(req.room_id_or_alias, "!foo:b.ar"); + assert_eq!(req.reason, Some("Let me in already!".to_owned())); + assert_eq!(req.via, vec![owned_server_name!("f.oo")]); + } + } } From 9e418105ff0d7f18169b6c295a68760ccf508358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 10 Sep 2024 18:39:33 +0200 Subject: [PATCH 2/8] ci: Upgrade actions --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deps.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19633e52..cf598017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,10 +50,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Check spelling - uses: crate-ci/typos@v1.20.3 + uses: crate-ci/typos@v1.24.5 - name: Install cargo-sort - uses: taiki-e/cache-cargo-install-action@v1 + uses: taiki-e/cache-cargo-install-action@v2 with: tool: cargo-sort diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml index 7e3f0d49..a98b7aa5 100644 --- a/.github/workflows/deps.yml +++ b/.github/workflows/deps.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check bans licenses sources @@ -31,6 +31,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories From b1632fd07c80db2df8100df63e1f93a05a4fd051 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 9 Sep 2024 22:45:09 +0200 Subject: [PATCH 3/8] Remove examples from the repository They have moved into their own repo. --- Cargo.toml | 2 +- examples/README.md | 5 +- examples/hello_world/Cargo.toml | 11 -- examples/hello_world/README.md | 13 -- examples/hello_world/src/main.rs | 49 ------ examples/joke_bot/Cargo.toml | 21 --- examples/joke_bot/README.md | 30 ---- examples/joke_bot/src/main.rs | 278 ------------------------------- examples/message_log/Cargo.toml | 11 -- examples/message_log/README.md | 13 -- examples/message_log/src/main.rs | 85 ---------- 11 files changed, 2 insertions(+), 516 deletions(-) delete mode 100644 examples/hello_world/Cargo.toml delete mode 100644 examples/hello_world/README.md delete mode 100644 examples/hello_world/src/main.rs delete mode 100644 examples/joke_bot/Cargo.toml delete mode 100644 examples/joke_bot/README.md delete mode 100644 examples/joke_bot/src/main.rs delete mode 100644 examples/message_log/Cargo.toml delete mode 100644 examples/message_log/README.md delete mode 100644 examples/message_log/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 6445ef4a..f870f6ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "examples/*", "xtask"] +members = ["crates/*", "xtask"] # Only compile / check / document the public crates by default default-members = ["crates/*"] resolver = "2" diff --git a/examples/README.md b/examples/README.md index b7ec3031..23e9f0b1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1 @@ -# Ruma Examples - -These are example projects showcasing how to use the various crates in this repository. You can use -these as a base for starting your own project. +The examples have moved to another repository, at . diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml deleted file mode 100644 index 8ee569ea..00000000 --- a/examples/hello_world/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "hello_world" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } - -anyhow = "1.0.37" -tokio = { version = "1.0.1", features = ["macros", "rt"] } diff --git a/examples/hello_world/README.md b/examples/hello_world/README.md deleted file mode 100644 index 67e1e02d..00000000 --- a/examples/hello_world/README.md +++ /dev/null @@ -1,13 +0,0 @@ -A simple example to demonstrate `ruma-client` functionality. Sends "Hello -World!" to the given room. - -# Usage - -You will need to use an existing account on a homeserver that allows login with -a password. - -In this folder, you can run it with this command: - -```shell -cargo run -``` diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs deleted file mode 100644 index cdf715aa..00000000 --- a/examples/hello_world/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::{env, process::exit}; - -use ruma::{ - api::client::{alias::get_alias, membership::join_room_by_id, message::send_message_event}, - events::room::message::RoomMessageEventContent, - OwnedRoomAliasId, TransactionId, -}; - -type HttpClient = ruma::client::http_client::HyperNativeTls; - -async fn hello_world( - homeserver_url: String, - username: &str, - password: &str, - room_alias: OwnedRoomAliasId, -) -> anyhow::Result<()> { - let client = - ruma::Client::builder().homeserver_url(homeserver_url).build::().await?; - client.log_in(username, password, None, Some("ruma-example-client")).await?; - - let room_id = client.send_request(get_alias::v3::Request::new(room_alias)).await?.room_id; - client.send_request(join_room_by_id::v3::Request::new(room_id.clone())).await?; - client - .send_request(send_message_event::v3::Request::new( - room_id, - TransactionId::new(), - &RoomMessageEventContent::text_plain("Hello World!"), - )?) - .await?; - - Ok(()) -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> anyhow::Result<()> { - let (homeserver_url, username, password, room) = - 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) - } - }; - - hello_world(homeserver_url, &username, &password, room.try_into()?).await -} diff --git a/examples/joke_bot/Cargo.toml b/examples/joke_bot/Cargo.toml deleted file mode 100644 index 417a0b20..00000000 --- a/examples/joke_bot/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "joke_bot" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } -# For building locally: use the git dependencies below. -# Browse the source at this revision here: https://github.com/ruma/ruma/tree/f161c8117c706fc52089999e1f406cf34276ec9d -# ruma = { git = "https://github.com/ruma/ruma", rev = "f161c8117c706fc52089999e1f406cf34276ec9d", features = ["client-api-c", "client", "client-hyper-native-tls", "events"] } - -futures-util = { version = "0.3.21", default-features = false, features = ["std"] } -http = "1.1.0" -http-body-util = "0.1.1" -hyper = "1.3.1" -hyper-tls = "0.6.0" -hyper-util = { version = "0.1.3", features = ["client-legacy", "http1", "http2", "tokio"] } -serde_json = "1.0" -tokio = { version = "1", features = ["full"] } -tokio-stream = "0.1.7" diff --git a/examples/joke_bot/README.md b/examples/joke_bot/README.md deleted file mode 100644 index 2f7054c3..00000000 --- a/examples/joke_bot/README.md +++ /dev/null @@ -1,30 +0,0 @@ -A simple bot to demonstrate `ruma-client` functionality. Tells jokes when you ask for them. - -# Note on dependency versions - -This example was written against pre-release versions of `ruma` and -`ruma-client-api`. Check the comments in the `[dependencies]` section of -[`Cargo.toml`](Cargo.toml) for more information. - -# Usage - -Create a file called `config` and populate it with the following values in `key=value` format: - -- `homeserver`: Your homeserver URL. -- `username`: The Matrix ID for the bot. -- `password`: The password for the bot. - -For example: - -```ini -homeserver=https://example.com:8448/ -username=@user:example.com -password=yourpassword -``` - -You will need to pre-register the bot account; it doesn't do registration -automatically. The bot will automatically join rooms it is invited to though. - -Finally, run the bot (e.g. using `cargo run`) from the same directory as your -`config` file. The bot should respond to the request "Tell me a joke" in any -channel that it is invited to. diff --git a/examples/joke_bot/src/main.rs b/examples/joke_bot/src/main.rs deleted file mode 100644 index 6e4ebdd5..00000000 --- a/examples/joke_bot/src/main.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::{error::Error, io, process::exit, time::Duration}; - -use futures_util::future::{join, join_all}; -use http_body_util::BodyExt as _; -use hyper_util::rt::TokioExecutor; -use ruma::{ - api::client::{ - filter::FilterDefinition, membership::join_room_by_id, message::send_message_event, - sync::sync_events, - }, - assign, client, - events::{ - room::message::{MessageType, RoomMessageEventContent}, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent, - }, - presence::PresenceState, - serde::Raw, - OwnedRoomId, OwnedUserId, TransactionId, UserId, -}; -use serde_json::Value as JsonValue; -use tokio::fs; -use tokio_stream::StreamExt as _; - -#[tokio::main] -async fn main() -> Result<(), Box> { - if let Err(e) = run().await { - eprintln!("{e}"); - exit(1) - } - Ok(()) -} - -type HttpClient = client::http_client::HyperNativeTls; -type MatrixClient = client::Client; - -async fn run() -> Result<(), Box> { - let config = - read_config().await.map_err(|e| format!("configuration in ./config is invalid: {e}"))?; - let http_client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()) - .build(hyper_tls::HttpsConnector::new()); - let matrix_client = if let Some(state) = read_state().await.ok().flatten() { - ruma::Client::builder() - .homeserver_url(config.homeserver.clone()) - .access_token(Some(state.access_token)) - .http_client(http_client.clone()) - .await? - } else if config.password.is_some() { - let client = create_matrix_session(http_client.clone(), &config).await?; - - if let Err(err) = write_state(&State { - access_token: client.access_token().expect("Matrix access token is missing"), - }) - .await - { - eprintln!( - "Failed to persist access token to disk. \ - Re-authentication will be required on the next startup: {err}", - ); - } - client - } else { - return Err("No previous session found and no credentials stored in config".into()); - }; - - let filter = FilterDefinition::ignore_all().into(); - let initial_sync_response = matrix_client - .send_request(assign!(sync_events::v3::Request::new(), { - filter: Some(filter), - })) - .await?; - let user_id = &config.username; - let not_senders = vec![user_id.clone()]; - let filter = { - let mut filter = FilterDefinition::empty(); - filter.room.timeline.not_senders = not_senders; - filter - } - .into(); - - let mut sync_stream = Box::pin(matrix_client.sync( - Some(filter), - initial_sync_response.next_batch, - PresenceState::Online, - Some(Duration::from_secs(30)), - )); - - // Prevent the clients being moved by `async move` blocks - let http_client = &http_client; - let matrix_client = &matrix_client; - - println!("Listening..."); - while let Some(response) = sync_stream.try_next().await? { - let message_futures = response.rooms.join.iter().map(|(room_id, room_info)| async move { - // Use a regular for loop for the messages within one room to handle them sequentially - for e in &room_info.timeline.events { - if let Err(err) = - handle_message(http_client, matrix_client, e, room_id.to_owned(), user_id).await - { - eprintln!("failed to respond to message: {err}"); - } - } - }); - - let invite_futures = response.rooms.invite.into_keys().map(|room_id| async move { - if let Err(err) = handle_invitations(http_client, matrix_client, room_id.clone()).await - { - eprintln!("failed to accept invitation for room {room_id}: {err}"); - } - }); - - // Handle messages from different rooms as well as invites concurrently - join(join_all(message_futures), join_all(invite_futures)).await; - } - - Ok(()) -} - -async fn create_matrix_session( - http_client: HttpClient, - config: &Config, -) -> Result> { - if let Some(password) = &config.password { - let client = ruma::Client::builder() - .homeserver_url(config.homeserver.clone()) - .http_client(http_client) - .await?; - - if let Err(e) = client.log_in(config.username.as_ref(), password, None, None).await { - let reason = match e { - client::Error::AuthenticationRequired => "invalid credentials specified".to_owned(), - client::Error::Response(response_err) => { - format!("failed to get a response from the server: {response_err}") - } - client::Error::FromHttpResponse(parse_err) => { - format!("failed to parse log in response: {parse_err}") - } - _ => e.to_string(), - }; - return Err(format!("Failed to log in: {reason}").into()); - } - - Ok(client) - } else { - Err("Failed to create session: no password stored in config".to_owned().into()) - } -} - -async fn handle_message( - http_client: &HttpClient, - matrix_client: &MatrixClient, - e: &Raw, - room_id: OwnedRoomId, - bot_user_id: &UserId, -) -> Result<(), Box> { - if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(m), - ))) = e.deserialize() - { - // workaround because Conduit does not implement filtering. - if m.sender == bot_user_id { - return Ok(()); - } - - if let MessageType::Text(t) = m.content.msgtype { - println!("{}:\t{}", m.sender, t.body); - if t.body.to_ascii_lowercase().contains("joke") { - let joke = match get_joke(http_client).await { - Ok(joke) => joke, - Err(_) => "I thought of a joke... but I just forgot it.".to_owned(), - }; - let joke_content = RoomMessageEventContent::text_plain(joke); - - let txn_id = TransactionId::new(); - let req = send_message_event::v3::Request::new( - room_id.to_owned(), - txn_id, - &joke_content, - )?; - // Do nothing if we can't send the message. - let _ = matrix_client.send_request(req).await; - } - } - } - - Ok(()) -} - -async fn handle_invitations( - http_client: &HttpClient, - matrix_client: &MatrixClient, - room_id: OwnedRoomId, -) -> Result<(), Box> { - println!("invited to {room_id}"); - matrix_client.send_request(join_room_by_id::v3::Request::new(room_id.clone())).await?; - - let greeting = "Hello! My name is Mr. Bot! I like to tell jokes. Like this one: "; - let joke = get_joke(http_client).await.unwrap_or_else(|_| "err... never mind.".to_owned()); - let content = RoomMessageEventContent::text_plain(format!("{greeting}\n{joke}")); - let txn_id = TransactionId::new(); - let message = send_message_event::v3::Request::new(room_id, txn_id, &content)?; - matrix_client.send_request(message).await?; - Ok(()) -} - -async fn get_joke(client: &HttpClient) -> Result> { - let uri = "https://v2.jokeapi.dev/joke/Programming,Pun,Misc?safe-mode&type=single" - .parse::()?; - let rsp = client.get(uri).await?; - let bytes = rsp.into_body().collect().await?.to_bytes(); - let joke_obj = serde_json::from_slice::(&bytes) - .map_err(|_| "invalid JSON returned from joke API")?; - let joke = joke_obj["joke"].as_str().ok_or("joke field missing from joke API response")?; - Ok(joke.to_owned()) -} - -struct State { - access_token: String, -} - -async fn write_state(state: &State) -> io::Result<()> { - let content = &state.access_token; - fs::write("./session", content).await?; - Ok(()) -} - -async fn read_state() -> io::Result> { - match fs::read_to_string("./session").await { - Ok(access_token) => Ok(Some(State { access_token })), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(e), - } -} - -struct Config { - homeserver: String, - username: OwnedUserId, - password: Option, -} - -async fn read_config() -> io::Result { - let content = fs::read_to_string("./config").await?; - let lines = content.split('\n'); - - let mut homeserver = None; - let mut username = Err("required field `username` is missing".to_owned()); - let mut password = None; - for line in lines { - if let Some((key, value)) = line.split_once('=') { - match key.trim() { - "homeserver" => homeserver = Some(value.trim().to_owned()), - // TODO: infer domain from `homeserver` - "username" => { - username = - value.trim().to_owned().try_into().map_err(|e| { - format!("invalid Matrix user ID format for `username`: {e}") - }); - } - "password" => password = Some(value.trim().to_owned()), - _ => {} - } - } - } - - match (homeserver, username) { - (Some(homeserver), Ok(username)) => Ok(Config { homeserver, username, password }), - (homeserver, username) => { - let mut error = String::from("Invalid config specified:"); - if homeserver.is_none() { - error.push_str("\n required field `homeserver` is missing"); - } - if let Err(e) = username { - error.push_str("\n "); - error.push_str(&e); - } - Err(io::Error::new(io::ErrorKind::InvalidData, error)) - } - } -} diff --git a/examples/message_log/Cargo.toml b/examples/message_log/Cargo.toml deleted file mode 100644 index 868d3f5b..00000000 --- a/examples/message_log/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "message_log" -version = "0.1.0" -edition = "2021" -publish = false - -[dependencies] -anyhow = "1.0.37" -ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls"] } -tokio = { version = "1.0.1", features = ["macros", "rt"] } -tokio-stream = { version = "0.1.1", default-features = false } diff --git a/examples/message_log/README.md b/examples/message_log/README.md deleted file mode 100644 index 9c2bf8a6..00000000 --- a/examples/message_log/README.md +++ /dev/null @@ -1,13 +0,0 @@ -A simple example to demonstrate `ruma-client` functionality. Prints all the -received text messages to stdout. - -# Usage - -You will need to use an existing account on a homeserver that allows login with -a password. - -In this folder, you can run it with this command: - -```shell -cargo run -``` diff --git a/examples/message_log/src/main.rs b/examples/message_log/src/main.rs deleted file mode 100644 index fdfcfc31..00000000 --- a/examples/message_log/src/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::{env, process::exit, time::Duration}; - -use ruma::{ - api::client::{filter::FilterDefinition, sync::sync_events}, - assign, - events::{ - room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent}, - AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent, - SyncMessageLikeEvent, - }, - presence::PresenceState, -}; -use tokio_stream::StreamExt as _; - -type HttpClient = ruma::client::http_client::HyperNativeTls; - -async fn log_messages( - homeserver_url: String, - username: &str, - password: &str, -) -> anyhow::Result<()> { - let client = - ruma::Client::builder().homeserver_url(homeserver_url).build::().await?; - - client.log_in(username, password, None, None).await?; - - let filter = FilterDefinition::ignore_all().into(); - let initial_sync_response = client - .send_request(assign!(sync_events::v3::Request::new(), { - filter: Some(filter), - })) - .await?; - - let mut sync_stream = Box::pin(client.sync( - None, - initial_sync_response.next_batch, - PresenceState::Online, - Some(Duration::from_secs(30)), - )); - - while let Some(res) = sync_stream.try_next().await? { - // Only look at rooms the user hasn't left yet - for (room_id, room) in res.rooms.join { - for event in room.timeline.events.into_iter().flat_map(|r| r.deserialize()) { - // Filter out the text messages - if let AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(OriginalSyncMessageLikeEvent { - content: - RoomMessageEventContent { - msgtype: - MessageType::Text(TextMessageEventContent { - body: msg_body, .. - }), - .. - }, - sender, - .. - }), - )) = event - { - println!("{sender} in {room_id}: {msg_body}"); - } - } - } - } - - Ok(()) -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> anyhow::Result<()> { - let (homeserver_url, username, password) = - match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) { - (Some(a), Some(b), Some(c)) => (a, b, c), - _ => { - eprintln!( - "Usage: {} ", - env::args().next().unwrap() - ); - exit(1) - } - }; - - log_messages(homeserver_url, &username, &password).await -} From 56e56485cdb9fb081f3058ad216ea9cd1b48d8d3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Sep 2024 18:54:35 +0200 Subject: [PATCH 4/8] docs: Link to examples repo from main readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3f66d1ab..ae928eb2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ ruma = { git = "https://github.com/ruma/ruma", branch = "main", features = ["... them as a user. Check out the documentation [on docs.rs][docs] (or on [docs.ruma.dev][unstable-docs] if you use use the git dependency). +You can also find a small number of examples in our dedicated +[examples repository](https://github.com/ruma/examples). + [matrix-rust-sdk]: https://github.com/matrix-org/matrix-rust-sdk#readme [feat]: https://github.com/ruma/ruma/blob/1166af5a354210dcbced1eaf4a11f795c381d2ec/ruma/Cargo.toml#L35 From 70518920779792e30cf150a9a0794579b37d7dd9 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Sep 2024 21:22:02 +0200 Subject: [PATCH 5/8] Move integration tests one directory level deeper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and rename from tests to it (integration tests). This allows enabling the mod_module_files lint everywhere again. --- crates/ruma-common/tests/api/ruma_api.rs | 10 ---------- crates/ruma-common/tests/identifiers/id_macros.rs | 7 ------- crates/ruma-common/tests/{api/mod.rs => it/api.rs} | 0 crates/ruma-common/tests/{ => it}/api/conversions.rs | 0 .../ruma-common/tests/{ => it}/api/default_status.rs | 0 .../ruma-common/tests/{ => it}/api/header_override.rs | 0 .../tests/{ => it}/api/manual_endpoint_impl.rs | 0 crates/ruma-common/tests/{ => it}/api/no_fields.rs | 0 .../ruma-common/tests/{ => it}/api/optional_headers.rs | 0 .../ruma-common/tests/{ => it}/api/required_headers.rs | 0 crates/ruma-common/tests/it/api/ruma_api.rs | 10 ++++++++++ .../ruma-common/tests/{ => it}/api/ruma_api_macros.rs | 0 .../ruma-common/tests/{ => it}/api/status_override.rs | 0 .../tests/{ => it}/api/ui/api-sanity-check.rs | 0 .../tests/{ => it}/api/ui/deprecated-without-added.rs | 0 .../{ => it}/api/ui/deprecated-without-added.stderr | 2 +- crates/ruma-common/tests/{ => it}/api/ui/move-value.rs | 0 .../{ => it}/api/ui/removed-without-deprecated.rs | 0 .../{ => it}/api/ui/removed-without-deprecated.stderr | 2 +- .../ruma-common/tests/{ => it}/api/ui/request-only.rs | 0 .../ruma-common/tests/{ => it}/api/ui/response-only.rs | 0 .../tests/{identifiers/mod.rs => it/identifiers.rs} | 0 crates/ruma-common/tests/it/identifiers/id_macros.rs | 7 +++++++ .../{ => it}/identifiers/ui/01-valid-id-macros.rs | 0 .../{ => it}/identifiers/ui/02-invalid-id-macros.rs | 0 .../identifiers/ui/02-invalid-id-macros.stderr | 0 .../identifiers/ui/03-invalid-new-id-macros.rs | 0 .../identifiers/ui/03-invalid-new-id-macros.stderr | 6 +++--- crates/ruma-common/tests/{tests.rs => it/main.rs} | 0 crates/ruma-common/tests/{serde/mod.rs => it/serde.rs} | 0 .../ruma-common/tests/{ => it}/serde/empty_strings.rs | 0 crates/ruma-common/tests/{ => it}/serde/enum_derive.rs | 0 .../ruma-federation-api/tests/{tests.rs => it/main.rs} | 0 .../tests/{membership/mod.rs => it/membership.rs} | 0 .../tests/{ => it}/membership/create_join_event.rs | 0 35 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 crates/ruma-common/tests/api/ruma_api.rs delete mode 100644 crates/ruma-common/tests/identifiers/id_macros.rs rename crates/ruma-common/tests/{api/mod.rs => it/api.rs} (100%) rename crates/ruma-common/tests/{ => it}/api/conversions.rs (100%) rename crates/ruma-common/tests/{ => it}/api/default_status.rs (100%) rename crates/ruma-common/tests/{ => it}/api/header_override.rs (100%) rename crates/ruma-common/tests/{ => it}/api/manual_endpoint_impl.rs (100%) rename crates/ruma-common/tests/{ => it}/api/no_fields.rs (100%) rename crates/ruma-common/tests/{ => it}/api/optional_headers.rs (100%) rename crates/ruma-common/tests/{ => it}/api/required_headers.rs (100%) create mode 100644 crates/ruma-common/tests/it/api/ruma_api.rs rename crates/ruma-common/tests/{ => it}/api/ruma_api_macros.rs (100%) rename crates/ruma-common/tests/{ => it}/api/status_override.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/api-sanity-check.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/deprecated-without-added.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/deprecated-without-added.stderr (79%) rename crates/ruma-common/tests/{ => it}/api/ui/move-value.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/removed-without-deprecated.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/removed-without-deprecated.stderr (77%) rename crates/ruma-common/tests/{ => it}/api/ui/request-only.rs (100%) rename crates/ruma-common/tests/{ => it}/api/ui/response-only.rs (100%) rename crates/ruma-common/tests/{identifiers/mod.rs => it/identifiers.rs} (100%) create mode 100644 crates/ruma-common/tests/it/identifiers/id_macros.rs rename crates/ruma-common/tests/{ => it}/identifiers/ui/01-valid-id-macros.rs (100%) rename crates/ruma-common/tests/{ => it}/identifiers/ui/02-invalid-id-macros.rs (100%) rename crates/ruma-common/tests/{ => it}/identifiers/ui/02-invalid-id-macros.stderr (100%) rename crates/ruma-common/tests/{ => it}/identifiers/ui/03-invalid-new-id-macros.rs (100%) rename crates/ruma-common/tests/{ => it}/identifiers/ui/03-invalid-new-id-macros.stderr (78%) rename crates/ruma-common/tests/{tests.rs => it/main.rs} (100%) rename crates/ruma-common/tests/{serde/mod.rs => it/serde.rs} (100%) rename crates/ruma-common/tests/{ => it}/serde/empty_strings.rs (100%) rename crates/ruma-common/tests/{ => it}/serde/enum_derive.rs (100%) rename crates/ruma-federation-api/tests/{tests.rs => it/main.rs} (100%) rename crates/ruma-federation-api/tests/{membership/mod.rs => it/membership.rs} (100%) rename crates/ruma-federation-api/tests/{ => it}/membership/create_join_event.rs (100%) diff --git a/crates/ruma-common/tests/api/ruma_api.rs b/crates/ruma-common/tests/api/ruma_api.rs deleted file mode 100644 index 833fe018..00000000 --- a/crates/ruma-common/tests/api/ruma_api.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[test] -fn ui() { - let t = trybuild::TestCases::new(); - t.pass("tests/api/ui/api-sanity-check.rs"); - t.pass("tests/api/ui/move-value.rs"); - t.pass("tests/api/ui/request-only.rs"); - t.pass("tests/api/ui/response-only.rs"); - t.compile_fail("tests/api/ui/deprecated-without-added.rs"); - t.compile_fail("tests/api/ui/removed-without-deprecated.rs"); -} diff --git a/crates/ruma-common/tests/identifiers/id_macros.rs b/crates/ruma-common/tests/identifiers/id_macros.rs deleted file mode 100644 index 7c0aa7ec..00000000 --- a/crates/ruma-common/tests/identifiers/id_macros.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[test] -fn ui() { - let t = trybuild::TestCases::new(); - t.pass("tests/identifiers/ui/01-valid-id-macros.rs"); - t.compile_fail("tests/identifiers/ui/02-invalid-id-macros.rs"); - t.compile_fail("tests/identifiers/ui/03-invalid-new-id-macros.rs"); -} diff --git a/crates/ruma-common/tests/api/mod.rs b/crates/ruma-common/tests/it/api.rs similarity index 100% rename from crates/ruma-common/tests/api/mod.rs rename to crates/ruma-common/tests/it/api.rs diff --git a/crates/ruma-common/tests/api/conversions.rs b/crates/ruma-common/tests/it/api/conversions.rs similarity index 100% rename from crates/ruma-common/tests/api/conversions.rs rename to crates/ruma-common/tests/it/api/conversions.rs diff --git a/crates/ruma-common/tests/api/default_status.rs b/crates/ruma-common/tests/it/api/default_status.rs similarity index 100% rename from crates/ruma-common/tests/api/default_status.rs rename to crates/ruma-common/tests/it/api/default_status.rs diff --git a/crates/ruma-common/tests/api/header_override.rs b/crates/ruma-common/tests/it/api/header_override.rs similarity index 100% rename from crates/ruma-common/tests/api/header_override.rs rename to crates/ruma-common/tests/it/api/header_override.rs diff --git a/crates/ruma-common/tests/api/manual_endpoint_impl.rs b/crates/ruma-common/tests/it/api/manual_endpoint_impl.rs similarity index 100% rename from crates/ruma-common/tests/api/manual_endpoint_impl.rs rename to crates/ruma-common/tests/it/api/manual_endpoint_impl.rs diff --git a/crates/ruma-common/tests/api/no_fields.rs b/crates/ruma-common/tests/it/api/no_fields.rs similarity index 100% rename from crates/ruma-common/tests/api/no_fields.rs rename to crates/ruma-common/tests/it/api/no_fields.rs diff --git a/crates/ruma-common/tests/api/optional_headers.rs b/crates/ruma-common/tests/it/api/optional_headers.rs similarity index 100% rename from crates/ruma-common/tests/api/optional_headers.rs rename to crates/ruma-common/tests/it/api/optional_headers.rs diff --git a/crates/ruma-common/tests/api/required_headers.rs b/crates/ruma-common/tests/it/api/required_headers.rs similarity index 100% rename from crates/ruma-common/tests/api/required_headers.rs rename to crates/ruma-common/tests/it/api/required_headers.rs diff --git a/crates/ruma-common/tests/it/api/ruma_api.rs b/crates/ruma-common/tests/it/api/ruma_api.rs new file mode 100644 index 00000000..f863050b --- /dev/null +++ b/crates/ruma-common/tests/it/api/ruma_api.rs @@ -0,0 +1,10 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.pass("tests/it/api/ui/api-sanity-check.rs"); + t.pass("tests/it/api/ui/move-value.rs"); + t.pass("tests/it/api/ui/request-only.rs"); + t.pass("tests/it/api/ui/response-only.rs"); + t.compile_fail("tests/it/api/ui/deprecated-without-added.rs"); + t.compile_fail("tests/it/api/ui/removed-without-deprecated.rs"); +} diff --git a/crates/ruma-common/tests/api/ruma_api_macros.rs b/crates/ruma-common/tests/it/api/ruma_api_macros.rs similarity index 100% rename from crates/ruma-common/tests/api/ruma_api_macros.rs rename to crates/ruma-common/tests/it/api/ruma_api_macros.rs diff --git a/crates/ruma-common/tests/api/status_override.rs b/crates/ruma-common/tests/it/api/status_override.rs similarity index 100% rename from crates/ruma-common/tests/api/status_override.rs rename to crates/ruma-common/tests/it/api/status_override.rs diff --git a/crates/ruma-common/tests/api/ui/api-sanity-check.rs b/crates/ruma-common/tests/it/api/ui/api-sanity-check.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/api-sanity-check.rs rename to crates/ruma-common/tests/it/api/ui/api-sanity-check.rs diff --git a/crates/ruma-common/tests/api/ui/deprecated-without-added.rs b/crates/ruma-common/tests/it/api/ui/deprecated-without-added.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/deprecated-without-added.rs rename to crates/ruma-common/tests/it/api/ui/deprecated-without-added.rs diff --git a/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr b/crates/ruma-common/tests/it/api/ui/deprecated-without-added.stderr similarity index 79% rename from crates/ruma-common/tests/api/ui/deprecated-without-added.stderr rename to crates/ruma-common/tests/it/api/ui/deprecated-without-added.stderr index a53add42..23a452eb 100644 --- a/crates/ruma-common/tests/api/ui/deprecated-without-added.stderr +++ b/crates/ruma-common/tests/it/api/ui/deprecated-without-added.stderr @@ -1,5 +1,5 @@ error: no rules expected the token `deprecated` - --> tests/api/ui/deprecated-without-added.rs:9:16 + --> tests/it/api/ui/deprecated-without-added.rs:9:16 | 9 | 1.1 => deprecated, | ^^^^^^^^^^ no rules expected this token in macro call diff --git a/crates/ruma-common/tests/api/ui/move-value.rs b/crates/ruma-common/tests/it/api/ui/move-value.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/move-value.rs rename to crates/ruma-common/tests/it/api/ui/move-value.rs diff --git a/crates/ruma-common/tests/api/ui/removed-without-deprecated.rs b/crates/ruma-common/tests/it/api/ui/removed-without-deprecated.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/removed-without-deprecated.rs rename to crates/ruma-common/tests/it/api/ui/removed-without-deprecated.rs diff --git a/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr b/crates/ruma-common/tests/it/api/ui/removed-without-deprecated.stderr similarity index 77% rename from crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr rename to crates/ruma-common/tests/it/api/ui/removed-without-deprecated.stderr index 25b8582d..7f081c73 100644 --- a/crates/ruma-common/tests/api/ui/removed-without-deprecated.stderr +++ b/crates/ruma-common/tests/it/api/ui/removed-without-deprecated.stderr @@ -1,5 +1,5 @@ error: no rules expected the token `removed` - --> tests/api/ui/removed-without-deprecated.rs:9:16 + --> tests/it/api/ui/removed-without-deprecated.rs:9:16 | 9 | 1.1 => removed, | ^^^^^^^ no rules expected this token in macro call diff --git a/crates/ruma-common/tests/api/ui/request-only.rs b/crates/ruma-common/tests/it/api/ui/request-only.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/request-only.rs rename to crates/ruma-common/tests/it/api/ui/request-only.rs diff --git a/crates/ruma-common/tests/api/ui/response-only.rs b/crates/ruma-common/tests/it/api/ui/response-only.rs similarity index 100% rename from crates/ruma-common/tests/api/ui/response-only.rs rename to crates/ruma-common/tests/it/api/ui/response-only.rs diff --git a/crates/ruma-common/tests/identifiers/mod.rs b/crates/ruma-common/tests/it/identifiers.rs similarity index 100% rename from crates/ruma-common/tests/identifiers/mod.rs rename to crates/ruma-common/tests/it/identifiers.rs diff --git a/crates/ruma-common/tests/it/identifiers/id_macros.rs b/crates/ruma-common/tests/it/identifiers/id_macros.rs new file mode 100644 index 00000000..032c4c4f --- /dev/null +++ b/crates/ruma-common/tests/it/identifiers/id_macros.rs @@ -0,0 +1,7 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.pass("tests/it/identifiers/ui/01-valid-id-macros.rs"); + t.compile_fail("tests/it/identifiers/ui/02-invalid-id-macros.rs"); + t.compile_fail("tests/it/identifiers/ui/03-invalid-new-id-macros.rs"); +} diff --git a/crates/ruma-common/tests/identifiers/ui/01-valid-id-macros.rs b/crates/ruma-common/tests/it/identifiers/ui/01-valid-id-macros.rs similarity index 100% rename from crates/ruma-common/tests/identifiers/ui/01-valid-id-macros.rs rename to crates/ruma-common/tests/it/identifiers/ui/01-valid-id-macros.rs diff --git a/crates/ruma-common/tests/identifiers/ui/02-invalid-id-macros.rs b/crates/ruma-common/tests/it/identifiers/ui/02-invalid-id-macros.rs similarity index 100% rename from crates/ruma-common/tests/identifiers/ui/02-invalid-id-macros.rs rename to crates/ruma-common/tests/it/identifiers/ui/02-invalid-id-macros.rs diff --git a/crates/ruma-common/tests/identifiers/ui/02-invalid-id-macros.stderr b/crates/ruma-common/tests/it/identifiers/ui/02-invalid-id-macros.stderr similarity index 100% rename from crates/ruma-common/tests/identifiers/ui/02-invalid-id-macros.stderr rename to crates/ruma-common/tests/it/identifiers/ui/02-invalid-id-macros.stderr diff --git a/crates/ruma-common/tests/identifiers/ui/03-invalid-new-id-macros.rs b/crates/ruma-common/tests/it/identifiers/ui/03-invalid-new-id-macros.rs similarity index 100% rename from crates/ruma-common/tests/identifiers/ui/03-invalid-new-id-macros.rs rename to crates/ruma-common/tests/it/identifiers/ui/03-invalid-new-id-macros.rs diff --git a/crates/ruma-common/tests/identifiers/ui/03-invalid-new-id-macros.stderr b/crates/ruma-common/tests/it/identifiers/ui/03-invalid-new-id-macros.stderr similarity index 78% rename from crates/ruma-common/tests/identifiers/ui/03-invalid-new-id-macros.stderr rename to crates/ruma-common/tests/it/identifiers/ui/03-invalid-new-id-macros.stderr index cedf3724..798353c4 100644 --- a/crates/ruma-common/tests/identifiers/ui/03-invalid-new-id-macros.stderr +++ b/crates/ruma-common/tests/it/identifiers/ui/03-invalid-new-id-macros.stderr @@ -1,13 +1,13 @@ error[E0080]: evaluation of constant value failed - --> tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13 + --> tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13 | 2 | let _ = ruma_common::session_id!("invalid~"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'Invalid Session ID: contains invalid characters', $DIR/tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'Invalid Session ID: contains invalid characters', $DIR/tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13 | = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `ruma_common::session_id` (in Nightly builds, run with -Z macro-backtrace for more info) note: erroneous constant encountered - --> tests/identifiers/ui/03-invalid-new-id-macros.rs:2:13 + --> tests/it/identifiers/ui/03-invalid-new-id-macros.rs:2:13 | 2 | let _ = ruma_common::session_id!("invalid~"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ruma-common/tests/tests.rs b/crates/ruma-common/tests/it/main.rs similarity index 100% rename from crates/ruma-common/tests/tests.rs rename to crates/ruma-common/tests/it/main.rs diff --git a/crates/ruma-common/tests/serde/mod.rs b/crates/ruma-common/tests/it/serde.rs similarity index 100% rename from crates/ruma-common/tests/serde/mod.rs rename to crates/ruma-common/tests/it/serde.rs diff --git a/crates/ruma-common/tests/serde/empty_strings.rs b/crates/ruma-common/tests/it/serde/empty_strings.rs similarity index 100% rename from crates/ruma-common/tests/serde/empty_strings.rs rename to crates/ruma-common/tests/it/serde/empty_strings.rs diff --git a/crates/ruma-common/tests/serde/enum_derive.rs b/crates/ruma-common/tests/it/serde/enum_derive.rs similarity index 100% rename from crates/ruma-common/tests/serde/enum_derive.rs rename to crates/ruma-common/tests/it/serde/enum_derive.rs diff --git a/crates/ruma-federation-api/tests/tests.rs b/crates/ruma-federation-api/tests/it/main.rs similarity index 100% rename from crates/ruma-federation-api/tests/tests.rs rename to crates/ruma-federation-api/tests/it/main.rs diff --git a/crates/ruma-federation-api/tests/membership/mod.rs b/crates/ruma-federation-api/tests/it/membership.rs similarity index 100% rename from crates/ruma-federation-api/tests/membership/mod.rs rename to crates/ruma-federation-api/tests/it/membership.rs diff --git a/crates/ruma-federation-api/tests/membership/create_join_event.rs b/crates/ruma-federation-api/tests/it/membership/create_join_event.rs similarity index 100% rename from crates/ruma-federation-api/tests/membership/create_join_event.rs rename to crates/ruma-federation-api/tests/it/membership/create_join_event.rs From 1a138ed6c920725a670cc33740fe51c9b8907812 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Sep 2024 21:22:38 +0200 Subject: [PATCH 6/8] Use workspace lint configuration instead of rustflags hack --- .cargo/config.toml | 35 ------------------- Cargo.toml | 34 ++++++++++++++++++ crates/ruma-appservice-api/Cargo.toml | 3 ++ crates/ruma-client-api/Cargo.toml | 3 ++ crates/ruma-client/Cargo.toml | 3 ++ crates/ruma-common/Cargo.toml | 3 ++ crates/ruma-events/Cargo.toml | 3 ++ crates/ruma-federation-api/Cargo.toml | 3 ++ crates/ruma-html/Cargo.toml | 3 ++ crates/ruma-identifiers-validation/Cargo.toml | 3 ++ crates/ruma-identity-service-api/Cargo.toml | 3 ++ crates/ruma-macros/Cargo.toml | 3 ++ crates/ruma-push-gateway-api/Cargo.toml | 3 ++ crates/ruma-server-util/Cargo.toml | 3 ++ crates/ruma-signatures/Cargo.toml | 3 ++ crates/ruma-state-res/Cargo.toml | 3 ++ crates/ruma/Cargo.toml | 3 ++ xtask/Cargo.toml | 3 ++ 18 files changed, 82 insertions(+), 35 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 14d6a664..e3d9d7d2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,40 +4,5 @@ xtask = "run --package xtask --" [doc.extern-map.registries] crates-io = "https://docs.rs/" -[target.'cfg(all())'] -rustflags = [ - "-Wrust_2018_idioms", - "-Wsemicolon_in_expressions_from_macros", - "-Wunreachable_pub", - "-Wunused_import_braces", - "-Wunused_qualifications", - "-Wclippy::branches_sharing_code", - "-Wclippy::cloned_instead_of_copied", - "-Wclippy::dbg_macro", - "-Wclippy::disallowed_types", - "-Wclippy::empty_line_after_outer_attr", - "-Wclippy::exhaustive_enums", - "-Wclippy::exhaustive_structs", - "-Wclippy::inefficient_to_string", - "-Wclippy::macro_use_imports", - "-Wclippy::map_flatten", - "-Wclippy::missing_enforced_import_renames", - # Disabled because it triggers for tests/foo/mod.rs which can't be replaced - # easily. Locally allowing it also doesn't seem to work. - #"-Wclippy::mod_module_files", - "-Wclippy::mut_mut", - "-Aclippy::new_without_default", - "-Wclippy::nonstandard_macro_braces", - "-Wclippy::semicolon_if_nothing_returned", - "-Wclippy::str_to_string", - "-Wclippy::todo", - "-Wclippy::unreadable_literal", - "-Wclippy::unseparated_literal_suffix", - "-Wclippy::wildcard_imports", - # Disabled temporarily because it triggers false positives for types with - # generics. - "-Aclippy::arc_with_non_send_sync", -] - [unstable] rustdoc-map = true diff --git a/Cargo.toml b/Cargo.toml index f870f6ee..ee228792 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,40 @@ tracing = { version = "0.1.37", default-features = false, features = ["std"] } url = { version = "2.5.0" } web-time = "1.1.0" +[workspace.lints.rust] +rust_2018_idioms = { level = "warn", priority = -1 } +semicolon_in_expressions_from_macros = "warn" +unreachable_pub = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" + +[workspace.lints.clippy] +branches_sharing_code = "warn" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +disallowed_types = "warn" +empty_line_after_outer_attr = "warn" +exhaustive_enums = "warn" +exhaustive_structs = "warn" +inefficient_to_string = "warn" +macro_use_imports = "warn" +map_flatten = "warn" +missing_enforced_import_renames = "warn" +mod_module_files = "warn" +mut_mut = "warn" +nonstandard_macro_braces = "warn" +semicolon_if_nothing_returned = "warn" +str_to_string = "warn" +todo = "warn" +unreadable_literal = "warn" +unseparated_literal_suffix = "warn" +wildcard_imports = "warn" + +# Not that good of a lint +new_without_default = "allow" +# Disabled temporarily because it triggers false positives for types with generics. +arc_with_non_send_sync = "allow" + [profile.dev] # Speeds up test times by more than 10% in a simple test # Set to 1 or 2 if you want to use a debugger in this workspace diff --git a/crates/ruma-appservice-api/Cargo.toml b/crates/ruma-appservice-api/Cargo.toml index 1779c570..645dcfdf 100644 --- a/crates/ruma-appservice-api/Cargo.toml +++ b/crates/ruma-appservice-api/Cargo.toml @@ -32,3 +32,6 @@ serde_json = { workspace = true } [dev-dependencies] assert_matches2 = { workspace = true } serde_yaml = "0.9.14" + +[lints] +workspace = true diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index 01927db0..64f19f66 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -72,3 +72,6 @@ web-time = { workspace = true } [dev-dependencies] assert_matches2 = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-client/Cargo.toml b/crates/ruma-client/Cargo.toml index 80c0f1ce..39c802d0 100644 --- a/crates/ruma-client/Cargo.toml +++ b/crates/ruma-client/Cargo.toml @@ -50,3 +50,6 @@ tracing = { version = "0.1.30", default-features = false, features = ["std"] } [dev-dependencies] ruma-client-api = { workspace = true, features = ["client"] } tokio-stream = "0.1.8" + +[lints] +workspace = true diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml index 156c4cd1..3f7166bd 100644 --- a/crates/ruma-common/Cargo.toml +++ b/crates/ruma-common/Cargo.toml @@ -87,3 +87,6 @@ assert_matches2 = { workspace = true } assign = { workspace = true } maplit = { workspace = true } trybuild = "1.0.71" + +[lints] +workspace = true diff --git a/crates/ruma-events/Cargo.toml b/crates/ruma-events/Cargo.toml index c578f526..333fda3d 100644 --- a/crates/ruma-events/Cargo.toml +++ b/crates/ruma-events/Cargo.toml @@ -93,3 +93,6 @@ trybuild = "1.0.71" name = "event_deserialize" harness = false required-features = ["criterion"] + +[lints] +workspace = true diff --git a/crates/ruma-federation-api/Cargo.toml b/crates/ruma-federation-api/Cargo.toml index 11a671d9..16e79e9c 100644 --- a/crates/ruma-federation-api/Cargo.toml +++ b/crates/ruma-federation-api/Cargo.toml @@ -45,3 +45,6 @@ serde_json = { workspace = true } [dev-dependencies] assert_matches2 = { workspace = true } http = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-html/Cargo.toml b/crates/ruma-html/Cargo.toml index ad044eec..982c75eb 100644 --- a/crates/ruma-html/Cargo.toml +++ b/crates/ruma-html/Cargo.toml @@ -26,3 +26,6 @@ wildmatch = "2.0.0" [dev-dependencies] assert_matches2 = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-identifiers-validation/Cargo.toml b/crates/ruma-identifiers-validation/Cargo.toml index 63379d2b..a4cc6d60 100644 --- a/crates/ruma-identifiers-validation/Cargo.toml +++ b/crates/ruma-identifiers-validation/Cargo.toml @@ -25,3 +25,6 @@ compat-user-id = [] [dependencies] js_int = { workspace = true } thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-identity-service-api/Cargo.toml b/crates/ruma-identity-service-api/Cargo.toml index 62cd8748..1ca77119 100644 --- a/crates/ruma-identity-service-api/Cargo.toml +++ b/crates/ruma-identity-service-api/Cargo.toml @@ -25,3 +25,6 @@ serde = { workspace = true } [dev-dependencies] serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-macros/Cargo.toml b/crates/ruma-macros/Cargo.toml index 6c62f4a6..3fdb6dab 100644 --- a/crates/ruma-macros/Cargo.toml +++ b/crates/ruma-macros/Cargo.toml @@ -30,3 +30,6 @@ ruma-identifiers-validation = { workspace = true } serde = { workspace = true } syn = { version = "2.0.2", features = ["extra-traits", "full", "visit"] } toml = { version = "0.8.2", default-features = false, features = ["parse"] } + +[lints] +workspace = true diff --git a/crates/ruma-push-gateway-api/Cargo.toml b/crates/ruma-push-gateway-api/Cargo.toml index b41393fd..bb7ad52d 100644 --- a/crates/ruma-push-gateway-api/Cargo.toml +++ b/crates/ruma-push-gateway-api/Cargo.toml @@ -25,3 +25,6 @@ ruma-common = { workspace = true, features = ["api"] } ruma-events = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruma-server-util/Cargo.toml b/crates/ruma-server-util/Cargo.toml index 93fc6d70..a6fc4fd2 100644 --- a/crates/ruma-server-util/Cargo.toml +++ b/crates/ruma-server-util/Cargo.toml @@ -24,3 +24,6 @@ tracing = { workspace = true } [dev-dependencies] tracing-subscriber = "0.3.16" + +[lints] +workspace = true diff --git a/crates/ruma-signatures/Cargo.toml b/crates/ruma-signatures/Cargo.toml index 103875ef..b30a57e4 100644 --- a/crates/ruma-signatures/Cargo.toml +++ b/crates/ruma-signatures/Cargo.toml @@ -33,3 +33,6 @@ thiserror = { workspace = true } [dev-dependencies] assert_matches2 = { workspace = true } insta = "1.31.0" + +[lints] +workspace = true diff --git a/crates/ruma-state-res/Cargo.toml b/crates/ruma-state-res/Cargo.toml index a97ac7a1..0b6b7744 100644 --- a/crates/ruma-state-res/Cargo.toml +++ b/crates/ruma-state-res/Cargo.toml @@ -40,3 +40,6 @@ tracing-subscriber = "0.3.16" name = "state_res_bench" harness = false required-features = ["criterion"] + +[lints] +workspace = true diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index bfb40d56..63c6c275 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -310,3 +310,6 @@ ruma-push-gateway-api = { workspace = true, optional = true } [dev-dependencies] serde = { workspace = true } + +[lints] +workspace = true diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 0b774e40..d97ef4e4 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -17,3 +17,6 @@ serde_json = { workspace = true } toml = { version = "0.8.2", default-features = false, features = ["parse"] } toml_edit = { version = "0.20.2", optional = true } xshell = "0.1.17" + +[lints] +workspace = true From d92404d11401a4d8a718ed4c5252094b96b68c55 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Thu, 12 Sep 2024 08:28:06 +0200 Subject: [PATCH 7/8] events: introduce custom StateKey type for call member state events --- crates/ruma-events/src/call/member.rs | 79 +++++- .../src/call/member/member_state_key.rs | 225 ++++++++++++++++++ 2 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 crates/ruma-events/src/call/member/member_state_key.rs diff --git a/crates/ruma-events/src/call/member.rs b/crates/ruma-events/src/call/member.rs index faa8f6f5..c193249e 100644 --- a/crates/ruma-events/src/call/member.rs +++ b/crates/ruma-events/src/call/member.rs @@ -6,9 +6,11 @@ mod focus; mod member_data; +mod member_state_key; pub use focus::*; pub use member_data::*; +pub use member_state_key::*; use ruma_common::MilliSecondsSinceUnixEpoch; use ruma_macros::{EventContent, StringEnum}; use serde::{Deserialize, Serialize}; @@ -29,7 +31,7 @@ use crate::{ /// /// This struct also exposes allows to call the methods from [`CallMemberEventContent`]. #[derive(Clone, Debug, Serialize, Deserialize, EventContent, PartialEq)] -#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = String, custom_redacted, custom_possibly_redacted)] +#[ruma_event(type = "org.matrix.msc3401.call.member", kind = State, state_key_type = CallMemberStateKey, custom_redacted, custom_possibly_redacted)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[serde(untagged)] pub enum CallMemberEventContent { @@ -177,7 +179,7 @@ impl RedactContent for CallMemberEventContent { pub type PossiblyRedactedCallMemberEventContent = CallMemberEventContent; impl PossiblyRedactedStateEventContent for PossiblyRedactedCallMemberEventContent { - type StateKey = String; + type StateKey = CallMemberStateKey; } /// The Redacted version of [`CallMemberEventContent`]. @@ -193,7 +195,7 @@ impl ruma_events::content::EventContent for RedactedCallMemberEventContent { } impl RedactedStateEventContent for RedactedCallMemberEventContent { - type StateKey = String; + type StateKey = CallMemberStateKey; } /// Legacy content with an array of memberships. See also: [`CallMemberEventContent`] @@ -231,8 +233,11 @@ mod tests { use std::time::Duration; use assert_matches2::assert_matches; - use ruma_common::{MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, OwnedUserId}; - use serde_json::{from_value as from_json_value, json}; + use ruma_common::{ + device_id, user_id, MilliSecondsSinceUnixEpoch as TS, OwnedEventId, OwnedRoomId, + OwnedUserId, + }; + use serde_json::{from_value as from_json_value, json, Value as JsonValue}; use super::{ focus::{ActiveFocus, ActiveLivekitFocus, Focus, LivekitFocus}, @@ -467,8 +472,8 @@ mod tests { ); } - fn deserialize_member_event_helper(state_key: &str) { - let ev = json!({ + fn member_event_json(state_key: &str) -> JsonValue { + json!({ "content":{ "application": "m.call", "call_id": "", @@ -497,7 +502,11 @@ mod tests { "prev_content": {}, "prev_sender":"@user:example.org", } - }); + }) + } + + fn deserialize_member_event_helper(state_key: &str) { + let ev = member_event_json(state_key); assert_matches!( from_json_value(ev), @@ -507,7 +516,7 @@ mod tests { let event_id = OwnedEventId::try_from("$3qfxjGYSu4sL25FtR0ep6vePOc").unwrap(); let sender = OwnedUserId::try_from("@user:example.org").unwrap(); let room_id = OwnedRoomId::try_from("!1234:example.org").unwrap(); - assert_eq!(member_event.state_key, state_key); + assert_eq!(member_event.state_key.as_ref(), state_key); assert_eq!(member_event.event_id, event_id); assert_eq!(member_event.sender, sender); assert_eq!(member_event.room_id, room_id); @@ -555,12 +564,12 @@ mod tests { #[test] fn deserialize_member_event_with_scoped_state_key_prefixed() { - deserialize_member_event_helper("_@user:example.org:THIS_DEVICE"); + deserialize_member_event_helper("_@user:example.org_THIS_DEVICE"); } #[test] fn deserialize_member_event_with_scoped_state_key_unprefixed() { - deserialize_member_event_helper("@user:example.org:THIS_DEVICE"); + deserialize_member_event_helper("@user:example.org_THIS_DEVICE"); } fn timestamps() -> (TS, TS, TS) { @@ -632,4 +641,52 @@ mod tests { vec![] as Vec> ); } + + #[test] + fn test_parse_rtc_member_event_key() { + assert!(from_json_value::(member_event_json("abc")).is_err()); + assert!(from_json_value::(member_event_json("@nocolon")).is_err()); + assert!(from_json_value::(member_event_json("@noserverpart:")).is_err()); + assert!( + from_json_value::(member_event_json("@noserverpart:_suffix")).is_err() + ); + + let user_id = user_id!("@username:example.org").as_str(); + let device_id = device_id!("VALID_DEVICE_ID").as_str(); + + let parse_result = from_json_value::(member_event_json(user_id)); + assert_matches!(parse_result, Ok(_)); + assert_matches!( + from_json_value::(member_event_json(&format!("{user_id}_{device_id}"))), + Ok(_) + ); + + assert_matches!( + from_json_value::(member_event_json(&format!( + "{user_id}:invalid_suffix" + ))), + Err(_) + ); + + assert_matches!( + from_json_value::(member_event_json(&format!("_{user_id}"))), + Err(_) + ); + + assert_matches!( + from_json_value::(member_event_json(&format!("_{user_id}_{device_id}"))), + Ok(_) + ); + + assert_matches!( + from_json_value::(member_event_json(&format!( + "_{user_id}:invalid_suffix" + ))), + Err(_) + ); + assert_matches!( + from_json_value::(member_event_json(&format!("{user_id}_"))), + Err(_) + ); + } } diff --git a/crates/ruma-events/src/call/member/member_state_key.rs b/crates/ruma-events/src/call/member/member_state_key.rs new file mode 100644 index 00000000..b593d499 --- /dev/null +++ b/crates/ruma-events/src/call/member/member_state_key.rs @@ -0,0 +1,225 @@ +use std::str::FromStr; + +use ruma_common::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; +use serde::{ + de::{self, Deserialize, Deserializer, Unexpected}, + Serialize, Serializer, +}; +/// A type that can be used as the `state_key` for call member state events. +/// Those state keys can be a combination of UserId and DeviceId. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[allow(clippy::exhaustive_structs)] +pub struct CallMemberStateKey { + key: CallMemberStateKeyEnum, + raw: Box, +} + +impl CallMemberStateKey { + /// Constructs a new CallMemberStateKey there are three possible formats: + /// - "_{UserId}_{DeviceId}" example: "_@test:user.org_DEVICE". `device_id`: Some`, `underscore: + /// true` + /// - "{UserId}_{DeviceId}" example: "@test:user.org_DEVICE". `device_id`: Some`, `underscore: + /// false` + /// - "{UserId}" example example: "@test:user.org". `device_id`: None`, underscore is ignored: + /// `underscore: false|true` + /// + /// Dependent on the parameters the correct CallMemberStateKey will be constructed. + pub fn new(user_id: OwnedUserId, device_id: Option, underscore: bool) -> Self { + CallMemberStateKeyEnum::new(user_id, device_id, underscore).into() + } + + /// Returns the user id in this state key. + /// (This is a cheap operations. The id is already type checked on initialization. And does + /// only returns a reference to an existing OwnedUserId.) + pub fn user_id(&self) -> &UserId { + match &self.key { + CallMemberStateKeyEnum::UnderscoreUserDevice(u, _) => u, + CallMemberStateKeyEnum::UserDevice(u, _) => u, + CallMemberStateKeyEnum::User(u) => u, + } + } + + /// Returns the device id in this state key (if available) + /// (This is a cheap operations. The id is already type checked on initialization. And does + /// only returns a reference to an existing OwnedDeviceId.) + pub fn device_id(&self) -> Option<&DeviceId> { + match &self.key { + CallMemberStateKeyEnum::UnderscoreUserDevice(_, d) => Some(d), + CallMemberStateKeyEnum::UserDevice(_, d) => Some(d), + CallMemberStateKeyEnum::User(_) => None, + } + } +} + +impl AsRef for CallMemberStateKey { + fn as_ref(&self) -> &str { + &self.raw + } +} + +impl From for CallMemberStateKey { + fn from(value: CallMemberStateKeyEnum) -> Self { + let raw = value.to_string().into(); + Self { key: value, raw } + } +} + +impl FromStr for CallMemberStateKey { + type Err = KeyParseError; + + fn from_str(state_key: &str) -> Result { + // Intentionally do not use CallMemberStateKeyEnum.into since this would reconstruct the + // state key string. + Ok(Self { key: CallMemberStateKeyEnum::from_str(state_key)?, raw: state_key.into() }) + } +} + +impl<'de> Deserialize<'de> for CallMemberStateKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ruma_common::serde::deserialize_cow_str(deserializer)?; + Self::from_str(&s).map_err(|err| de::Error::invalid_value(Unexpected::Str(&s), &err)) + } +} + +impl Serialize for CallMemberStateKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +/// This enum represents all possible formats for a call member event state key. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum CallMemberStateKeyEnum { + UnderscoreUserDevice(OwnedUserId, OwnedDeviceId), + UserDevice(OwnedUserId, OwnedDeviceId), + User(OwnedUserId), +} + +impl CallMemberStateKeyEnum { + fn new(user_id: OwnedUserId, device_id: Option, underscore: bool) -> Self { + match (device_id, underscore) { + (Some(device_id), true) => { + CallMemberStateKeyEnum::UnderscoreUserDevice(user_id, device_id) + } + (Some(device_id), false) => CallMemberStateKeyEnum::UserDevice(user_id, device_id), + (None, _) => CallMemberStateKeyEnum::User(user_id), + } + } +} + +impl std::fmt::Display for CallMemberStateKeyEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + CallMemberStateKeyEnum::UnderscoreUserDevice(u, d) => write!(f, "_{u}_{d}"), + CallMemberStateKeyEnum::UserDevice(u, d) => write!(f, "{u}_{d}"), + CallMemberStateKeyEnum::User(u) => f.write_str(u.as_str()), + } + } +} + +impl FromStr for CallMemberStateKeyEnum { + type Err = KeyParseError; + + fn from_str(state_key: &str) -> Result { + // Ignore leading underscore if present + // (used for avoiding auth rules on @-prefixed state keys) + let (state_key, underscore) = match state_key.strip_prefix('_') { + Some(s) => (s, true), + None => (state_key, false), + }; + + // Fail early if we cannot find the index of the ":" + let Some(colon_idx) = state_key.find(':') else { + return Err(KeyParseError::InvalidUser { + user_id: state_key.to_owned(), + error: ruma_common::IdParseError::MissingColon, + }); + }; + + let (user_id, device_id) = match state_key[colon_idx + 1..].find('_') { + None => { + return match UserId::parse(state_key) { + Ok(user_id) => { + if underscore { + Err(KeyParseError::LeadingUnderscoreNoDevice) + } else { + Ok(CallMemberStateKeyEnum::new(user_id, None, underscore)) + } + } + Err(err) => Err(KeyParseError::InvalidUser { + error: err, + user_id: state_key.to_owned(), + }), + } + } + Some(suffix_idx) => { + (&state_key[..colon_idx + 1 + suffix_idx], &state_key[colon_idx + 2 + suffix_idx..]) + } + }; + + match (UserId::parse(user_id), OwnedDeviceId::from(device_id)) { + (Ok(user_id), device_id) => { + if device_id.as_str().is_empty() { + return Err(KeyParseError::EmptyDevice); + }; + Ok(CallMemberStateKeyEnum::new(user_id, Some(device_id), underscore)) + } + (Err(err), _) => { + Err(KeyParseError::InvalidUser { user_id: user_id.to_owned(), error: err }) + } + } + } +} + +/// Error when trying to parse a call member state key. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum KeyParseError { + /// The user part of the state key is invalid. + #[error("uses a malformatted UserId in the UserId defined section.")] + InvalidUser { + /// The user Id that the parser thinks it should have parsed. + user_id: String, + /// The user Id parse error why if failed to parse it. + error: ruma_common::IdParseError, + }, + /// Uses a leading underscore but no trailing device id. The part after the underscore is a + /// valid user id. + #[error("uses a leading underscore but no trailing device id. The part after the underscore is a valid user id.")] + LeadingUnderscoreNoDevice, + /// Uses an empty device id. (UserId with trailing underscore) + #[error("uses an empty device id. (UserId with trailing underscore)")] + EmptyDevice, +} + +impl de::Expected for KeyParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "correct call member event key format. The provided string, {})", self) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::call::member::{member_state_key::CallMemberStateKeyEnum, CallMemberStateKey}; + + #[test] + fn convert_state_key_enum_to_state_key() { + let key = "_@user:domain.org_DEVICE"; + let state_key_enum = CallMemberStateKeyEnum::from_str(key).unwrap(); + // This generates state_key.raw from the enum + let state_key: CallMemberStateKey = state_key_enum.into(); + // This compares state_key.raw (generated) with key (original) + assert_eq!(state_key.as_ref(), key); + // Compare to the from string without `CallMemberStateKeyEnum` step. + let state_key_direct = CallMemberStateKey::from_str(state_key.as_ref()).unwrap(); + assert_eq!(state_key, state_key_direct); + } +} From 7cfa3be0c69c9a63658bd6fa56c8c6c4d9fd4c71 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Thu, 12 Sep 2024 09:12:49 +0200 Subject: [PATCH 8/8] client-api: Implement MSC4186. (#1907) * client-api: Derive `Default` for `v4::SyncList`. * client-api: Implement MSC4186. --- crates/ruma-client-api/Cargo.toml | 1 + crates/ruma-client-api/src/error.rs | 4 +- .../ruma-client-api/src/error/kind_serde.rs | 4 +- .../ruma-client-api/src/sync/sync_events.rs | 3 + .../src/sync/sync_events/v4.rs | 139 ++- .../src/sync/sync_events/v5.rs | 856 ++++++++++++++++++ crates/ruma/Cargo.toml | 2 + 7 files changed, 1003 insertions(+), 6 deletions(-) create mode 100644 crates/ruma-client-api/src/sync/sync_events/v5.rs diff --git a/crates/ruma-client-api/Cargo.toml b/crates/ruma-client-api/Cargo.toml index 64f19f66..4b7d30a4 100644 --- a/crates/ruma-client-api/Cargo.toml +++ b/crates/ruma-client-api/Cargo.toml @@ -51,6 +51,7 @@ unstable-msc3983 = [] unstable-msc4108 = [] unstable-msc4121 = [] unstable-msc4140 = [] +unstable-msc4186 = [] [dependencies] as_variant = { workspace = true } diff --git a/crates/ruma-client-api/src/error.rs b/crates/ruma-client-api/src/error.rs index 9d34ba01..7d2ddeb6 100644 --- a/crates/ruma-client-api/src/error.rs +++ b/crates/ruma-client-api/src/error.rs @@ -173,7 +173,7 @@ pub enum ErrorKind { CannotOverwriteMedia, /// M_UNKNOWN_POS for sliding sync - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] UnknownPos, /// M_URL_NOT_SET @@ -271,7 +271,7 @@ impl AsRef for ErrorKind { Self::DuplicateAnnotation => "M_DUPLICATE_ANNOTATION", Self::NotYetUploaded => "M_NOT_YET_UPLOADED", Self::CannotOverwriteMedia => "M_CANNOT_OVERWRITE_MEDIA", - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] Self::UnknownPos => "M_UNKNOWN_POS", Self::UrlNotSet => "M_URL_NOT_SET", Self::BadStatus { .. } => "M_BAD_STATUS", diff --git a/crates/ruma-client-api/src/error/kind_serde.rs b/crates/ruma-client-api/src/error/kind_serde.rs index f6c5cd4c..5121b2f5 100644 --- a/crates/ruma-client-api/src/error/kind_serde.rs +++ b/crates/ruma-client-api/src/error/kind_serde.rs @@ -228,7 +228,7 @@ impl<'de> Visitor<'de> for ErrorKindVisitor { ErrCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation, ErrCode::NotYetUploaded => ErrorKind::NotYetUploaded, ErrCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia, - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] ErrCode::UnknownPos => ErrorKind::UnknownPos, ErrCode::UrlNotSet => ErrorKind::UrlNotSet, ErrCode::BadStatus => ErrorKind::BadStatus { @@ -301,7 +301,7 @@ enum ErrCode { NotYetUploaded, #[ruma_enum(alias = "FI.MAU.MSC2246_CANNOT_OVERWRITE_MEDIA")] CannotOverwriteMedia, - #[cfg(feature = "unstable-msc3575")] + #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))] UnknownPos, UrlNotSet, BadStatus, diff --git a/crates/ruma-client-api/src/sync/sync_events.rs b/crates/ruma-client-api/src/sync/sync_events.rs index 0e522aeb..84e2141b 100644 --- a/crates/ruma-client-api/src/sync/sync_events.rs +++ b/crates/ruma-client-api/src/sync/sync_events.rs @@ -11,6 +11,9 @@ pub mod v3; #[cfg(feature = "unstable-msc3575")] pub mod v4; +#[cfg(feature = "unstable-msc4186")] +pub mod v5; + /// Unread notifications count. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] diff --git a/crates/ruma-client-api/src/sync/sync_events/v4.rs b/crates/ruma-client-api/src/sync/sync_events/v4.rs index 1f9768ff..03cea5ef 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v4.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v4.rs @@ -22,7 +22,7 @@ use ruma_events::{ }; use serde::{de::Error as _, Deserialize, Serialize}; -use super::{DeviceLists, UnreadNotificationsCount}; +use super::{v5, DeviceLists, UnreadNotificationsCount}; const METADATA: Metadata = metadata! { method: POST, @@ -393,7 +393,7 @@ pub enum SlidingOp { } /// Updates to joined rooms. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct SyncList { /// The sync operation to apply, if any. @@ -927,6 +927,141 @@ impl Typing { } } +impl From for Request { + fn from(value: v5::Request) -> Self { + Self { + pos: value.pos, + conn_id: value.conn_id, + txn_id: value.txn_id, + timeout: value.timeout, + lists: value + .lists + .into_iter() + .map(|(list_name, list)| (list_name, list.into())) + .collect(), + room_subscriptions: value + .room_subscriptions + .into_iter() + .map(|(room_id, room_subscription)| (room_id, room_subscription.into())) + .collect(), + extensions: value.extensions.into(), + + ..Default::default() + } + } +} + +impl From for SyncRequestList { + fn from(value: v5::request::List) -> Self { + Self { + ranges: value.ranges, + room_details: value.room_details.into(), + include_heroes: value.include_heroes, + filters: value.filters.map(Into::into), + + // Defaults from MSC4186. + sort: vec!["by_recency".to_owned(), "by_name".to_owned()], + bump_event_types: vec![ + TimelineEventType::RoomMessage, + TimelineEventType::RoomEncrypted, + TimelineEventType::RoomCreate, + TimelineEventType::Sticker, + ], + + ..Default::default() + } + } +} + +impl From for RoomDetailsConfig { + fn from(value: v5::request::RoomDetails) -> Self { + Self { required_state: value.required_state, timeline_limit: value.timeline_limit } + } +} + +impl From for SyncRequestListFilters { + fn from(value: v5::request::ListFilters) -> Self { + Self { + is_invite: value.is_invite, + not_room_types: value.not_room_types, + ..Default::default() + } + } +} + +impl From for RoomSubscription { + fn from(value: v5::request::RoomSubscription) -> Self { + Self { + required_state: value.required_state, + timeline_limit: value.timeline_limit, + include_heroes: value.include_heroes, + } + } +} + +impl From for ExtensionsConfig { + fn from(value: v5::request::Extensions) -> Self { + Self { + to_device: value.to_device.into(), + e2ee: value.e2ee.into(), + account_data: value.account_data.into(), + receipts: value.receipts.into(), + typing: value.typing.into(), + + ..Default::default() + } + } +} + +impl From for ToDeviceConfig { + fn from(value: v5::request::ToDevice) -> Self { + Self { + enabled: value.enabled, + limit: value.limit, + since: value.since, + lists: value.lists, + rooms: value.rooms, + } + } +} + +impl From for E2EEConfig { + fn from(value: v5::request::E2EE) -> Self { + Self { enabled: value.enabled } + } +} + +impl From for AccountDataConfig { + fn from(value: v5::request::AccountData) -> Self { + Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } + } +} + +impl From for ReceiptsConfig { + fn from(value: v5::request::Receipts) -> Self { + Self { + enabled: value.enabled, + lists: value.lists, + rooms: value.rooms.map(|rooms| rooms.into_iter().map(Into::into).collect()), + } + } +} + +impl From for RoomReceiptConfig { + fn from(value: v5::request::ReceiptsRoom) -> Self { + match value { + v5::request::ReceiptsRoom::Room(room_id) => Self::Room(room_id), + _ => Self::AllSubscribed, + } + } +} + +impl From for TypingConfig { + fn from(value: v5::request::Typing) -> Self { + Self { enabled: value.enabled, lists: value.lists, rooms: value.rooms } + } +} + #[cfg(test)] mod tests { use ruma_common::owned_room_id; diff --git a/crates/ruma-client-api/src/sync/sync_events/v5.rs b/crates/ruma-client-api/src/sync/sync_events/v5.rs new file mode 100644 index 00000000..24e7f1bd --- /dev/null +++ b/crates/ruma-client-api/src/sync/sync_events/v5.rs @@ -0,0 +1,856 @@ +//! `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync` ([MSC4186]) +//! +//! A simplified version of sliding sync ([MSC3575]). +//! +//! Get all new events in a sliding window of rooms since the last sync or a given point in time. +//! +//! [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575 +//! [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186 + +use std::{collections::BTreeMap, time::Duration}; + +use js_int::UInt; +use js_option::JsOption; +use ruma_common::{ + api::{request, response, Metadata}, + metadata, + serde::{duration::opt_ms, Raw}, + OwnedMxcUri, OwnedRoomId, OwnedUserId, +}; +use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType}; +use serde::{Deserialize, Serialize}; + +use super::{v4, UnreadNotificationsCount}; + +const METADATA: Metadata = metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + history: { + unstable => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync", + // 1.4 => "/_matrix/client/v5/sync", + } +}; + +/// Request type for the `/sync` endpoint. +#[request(error = crate::Error)] +#[derive(Default)] +pub struct Request { + /// A point in time to continue a sync from. + /// + /// This is an opaque value taken from the `pos` field of a previous `/sync` + /// response. A `None` value asks the server to start a new _session_ (mind + /// it can be costly) + #[serde(skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub pos: Option, + + /// A unique string identifier for this connection to the server. + /// + /// If this is missing, only one sliding sync connection can be made to + /// the server at any one time. Clients need to set this to allow more + /// than one connection concurrently, so the server can distinguish between + /// connections. This must be provided with every request, if your client + /// needs more than one concurrent connection. + /// + /// Limitation: it must not contain more than 16 chars, due to it being + /// required with every request. + #[serde(skip_serializing_if = "Option::is_none")] + pub conn_id: Option, + + /// Allows clients to know what request params reached the server, + /// functionally similar to txn IDs on `/send` for events. + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + /// The maximum time to poll before responding to this request. + /// + /// `None` means no timeout, so virtually an infinite wait from the server. + #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")] + #[ruma_api(query)] + pub timeout: Option, + + /// Lists of rooms we are interested by, represented by ranges. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + /// Specific rooms we are interested by. + /// + /// It is useful to receive updates from rooms that are possibly + /// out-of-range of all the lists (see [`Self::lists`]). + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub room_subscriptions: BTreeMap, + + /// Extensions. + #[serde(default, skip_serializing_if = "request::Extensions::is_empty")] + pub extensions: request::Extensions, +} + +impl Request { + /// Creates an empty `Request`. + pub fn new() -> Self { + Default::default() + } +} + +/// HTTP types related to a [`Request`]. +pub mod request { + use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId}; + use serde::de::Error as _; + + use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt}; + + /// A sliding sync list request (see [`super::Request::lists`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct List { + /// The ranges of rooms we're interested in. + pub ranges: Vec<(UInt, UInt)>, + + /// The details to be included per room. + #[serde(flatten)] + pub room_details: RoomDetails, + + /// Request a stripped variant of membership events for the users used + /// to calculate the room name. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_heroes: Option, + + /// Filters to apply to the list before sorting. + #[serde(skip_serializing_if = "Option::is_none")] + pub filters: Option, + } + + /// A sliding sync list request filters (see [`List::filters`]). + /// + /// All fields are applied with _AND_ operators. The absence of fields + /// implies no filter on that criteria: it does NOT imply `false`. + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ListFilters { + /// Whether to return invited rooms, only joined rooms or both. + /// + /// Flag which only returns rooms the user is currently invited to. + /// If unset, both invited and joined rooms are returned. If false, + /// no invited rooms are returned. If true, only invited rooms are + /// returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_invite: Option, + + /// Only list rooms that are not of these create-types, or all. + /// + /// This can be used to filter out spaces from the room list. + #[serde(default, skip_serializing_if = "<[_]>::is_empty")] + pub not_room_types: Vec, + } + + /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct RoomSubscription { + /// Required state for each returned room. An array of event type and + /// state key tuples. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + /// The maximum number of timeline events to return per room. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, + + /// Include the room heroes. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_heroes: Option, + } + + /// Sliding sync request room details (see [`List::room_details`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct RoomDetails { + /// Required state for each returned room. An array of event type and state key tuples. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec<(StateEventType, String)>, + + /// The maximum number of timeline events to return per room. + #[serde(skip_serializing_if = "Option::is_none")] + pub timeline_limit: Option, + } + + /// Sliding sync request extensions (see [`super::Request::extensions`]). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Extensions { + /// Configure the to-device extension. + #[serde(default, skip_serializing_if = "ToDevice::is_empty")] + pub to_device: ToDevice, + + /// Configure the E2EE extension. + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + /// Configure the account data extension. + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + /// Configure the receipts extension. + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + /// Configure the typing extension. + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, + + /// Extensions may add further fields to the list. + #[serde(flatten)] + other: BTreeMap, + } + + impl Extensions { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.to_device.is_empty() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + && self.other.is_empty() + } + } + + /// To-device messages extension. + /// + /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ToDevice { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// Maximum number of to-device messages per response. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// Give messages since this token only. + #[serde(skip_serializing_if = "Option::is_none")] + pub since: Option, + + /// List of list names for which to-device events should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which to-device events should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl ToDevice { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() && self.limit.is_none() && self.since.is_none() + } + } + + /// E2EE extension configuration. + /// + /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884). + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct E2EE { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + } + + impl E2EE { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Account-data extension . + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct AccountData { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which account data should be enabled. + /// + /// This is specific to room account data (e.g. user-defined room tags). + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which account data should be enabled. + /// + /// This is specific to room account data (e.g. user-defined room tags). + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl AccountData { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Receipt extension. + /// + /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960) + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Receipts { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which receipts should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which receipts should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl Receipts { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } + + /// Single entry for a room-related read receipt configuration in + /// [`Receipts`]. + #[derive(Clone, Debug, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub enum ReceiptsRoom { + /// Get read receipts for all the subscribed rooms. + AllSubscribed, + + /// Get read receipts for this particular room. + Room(OwnedRoomId), + } + + impl Serialize for ReceiptsRoom { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::AllSubscribed => serializer.serialize_str("*"), + Self::Room(r) => r.serialize(serializer), + } + } + } + + impl<'de> Deserialize<'de> for ReceiptsRoom { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + match deserialize_cow_str(deserializer)?.as_ref() { + "*" => Ok(Self::AllSubscribed), + other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())), + } + } + } + + /// Typing extension configuration. + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Typing { + /// Activate or deactivate this extension. + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + + /// List of list names for which typing notifications should be enabled. + /// + /// If not defined, will be enabled for *all* the lists appearing in the + /// request. If defined and empty, will be disabled for all the lists. + #[serde(skip_serializing_if = "Option::is_none")] + pub lists: Option>, + + /// List of room names for which typing notifications should be enabled. + /// + /// If not defined, will be enabled for *all* the rooms appearing in the + /// room subscriptions. If defined and empty, will be disabled for all + /// the rooms. + #[serde(skip_serializing_if = "Option::is_none")] + pub rooms: Option>, + } + + impl Typing { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.enabled.is_none() + } + } +} + +/// Response type for the `/sync` endpoint. +#[response(error = crate::Error)] +pub struct Response { + /// Whether this response describes an initial sync. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub initial: bool, + + /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]). + #[serde(skip_serializing_if = "Option::is_none")] + pub txn_id: Option, + + /// The token to supply in the `pos` parameter of the next `/sync` request + /// (see [`Request::pos`]). + pub pos: String, + + /// Resulting details of the lists. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub lists: BTreeMap, + + /// The updated rooms. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap, + + /// Extensions. + #[serde(default, skip_serializing_if = "response::Extensions::is_empty")] + pub extensions: response::Extensions, +} + +impl Response { + /// Creates a new `Response` with the given `pos`. + pub fn new(pos: String) -> Self { + Self { + initial: Default::default(), + txn_id: None, + pos, + lists: Default::default(), + rooms: Default::default(), + extensions: Default::default(), + } + } +} + +/// HTTP types related to a [`Response`]. +pub mod response { + use ruma_common::DeviceKeyAlgorithm; + use ruma_events::{ + receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent, + AnyRoomAccountDataEvent, AnyToDeviceEvent, + }; + + use super::{ + super::DeviceLists, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, + BTreeMap, Deserialize, JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, + UInt, UnreadNotificationsCount, + }; + + /// A sliding sync response updates to joiend rooms (see + /// [`super::Response::lists`]). + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct List { + /// The total number of rooms found for this list. + pub count: UInt, + } + + /// A slising sync response updated room (see [`super::Response::rooms`]). + #[derive(Clone, Debug, Default, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Room { + /// The name as calculated by the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The avatar. + #[serde(default, skip_serializing_if = "JsOption::is_undefined")] + pub avatar: JsOption, + + /// Whether it is an initial response. + #[serde(skip_serializing_if = "Option::is_none")] + pub initial: Option, + + /// Whether it is a direct room. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_dm: Option, + + /// If this is `Some(_)`, this is a not-yet-accepted invite containing + /// the given stripped state events. + #[serde(skip_serializing_if = "Option::is_none")] + pub invite_state: Option>>, + + /// Number of unread notifications. + #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")] + pub unread_notifications: UnreadNotificationsCount, + + /// Message-like events and live state events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub timeline: Vec>, + + /// State events as configured by the request. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_state: Vec>, + + /// The `prev_batch` allowing you to paginate through the messages + /// before the given ones. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_batch: Option, + + /// True if the number of events returned was limited by the limit on + /// the filter. + #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")] + pub limited: bool, + + /// The number of users with membership of `join`, including the + /// client’s own user ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub joined_count: Option, + + /// The number of users with membership of `invite`. + #[serde(skip_serializing_if = "Option::is_none")] + pub invited_count: Option, + + /// The number of timeline events which have just occurred and are not + /// historical. + #[serde(skip_serializing_if = "Option::is_none")] + pub num_live: Option, + + /// The bump stamp of the room. + /// + /// It can be interpreted as a “recency stamp” or “streaming order + /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB` + /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC` + /// receives an update, its `bump_stamp` will be 3. + #[serde(skip_serializing_if = "Option::is_none")] + pub bump_stamp: Option, + + /// Heroes of the room, if requested. + #[serde(skip_serializing_if = "Option::is_none")] + pub heroes: Option>, + } + + impl Room { + /// Creates an empty `Room`. + pub fn new() -> Self { + Default::default() + } + } + + /// A sliding sync response room hero (see [`Room::heroes`]). + #[derive(Clone, Debug, Deserialize, Serialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Hero { + /// The user ID. + pub user_id: OwnedUserId, + + /// The name. + #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The avatar. + #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")] + pub avatar: Option, + } + + impl Hero { + /// Creates a new `Hero` with the given user ID. + pub fn new(user_id: OwnedUserId) -> Self { + Self { user_id, name: None, avatar: None } + } + } + + /// Extensions responses. + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Extensions { + /// To-device extension response. + #[serde(skip_serializing_if = "Option::is_none")] + pub to_device: Option, + + /// E2EE extension response. + #[serde(default, skip_serializing_if = "E2EE::is_empty")] + pub e2ee: E2EE, + + /// Account data extension response. + #[serde(default, skip_serializing_if = "AccountData::is_empty")] + pub account_data: AccountData, + + /// Receipts extension response. + #[serde(default, skip_serializing_if = "Receipts::is_empty")] + pub receipts: Receipts, + + /// Typing extension response. + #[serde(default, skip_serializing_if = "Typing::is_empty")] + pub typing: Typing, + } + + impl Extensions { + /// Whether the extension data is empty. + /// + /// True if neither to-device, e2ee nor account data are to be found. + pub fn is_empty(&self) -> bool { + self.to_device.is_none() + && self.e2ee.is_empty() + && self.account_data.is_empty() + && self.receipts.is_empty() + && self.typing.is_empty() + } + } + + /// To-device extension response. + /// + /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct ToDevice { + /// Fetch the next batch from this entry. + pub next_batch: String, + + /// The to-device events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec>, + } + + /// E2EE extension response. + /// + /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884). + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct E2EE { + /// Information on E2EE device updates. + #[serde(default, skip_serializing_if = "DeviceLists::is_empty")] + pub device_lists: DeviceLists, + + /// For each key algorithm, the number of unclaimed one-time keys + /// currently held on the server for a device. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub device_one_time_keys_count: BTreeMap, + + /// For each key algorithm, the number of unclaimed one-time keys + /// currently held on the server for a device. + /// + /// The presence of this field indicates that the server supports + /// fallback keys. + #[serde(skip_serializing_if = "Option::is_none")] + pub device_unused_fallback_key_types: Option>, + } + + impl E2EE { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.device_lists.is_empty() + && self.device_one_time_keys_count.is_empty() + && self.device_unused_fallback_key_types.is_none() + } + } + + /// Account-data extension response . + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct AccountData { + /// The global private data created by this user. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub global: Vec>, + + /// The private data that this user has attached to each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>>, + } + + impl AccountData { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.global.is_empty() && self.rooms.is_empty() + } + } + + /// Receipt extension response. + /// + /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960) + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Receipts { + /// The ephemeral receipt room event for each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, + } + + impl Receipts { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } + } + + /// Typing extension response. + /// + /// Not yet part of the spec proposal. Taken from the reference implementation + /// + #[derive(Clone, Debug, Default, Serialize, Deserialize)] + #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] + pub struct Typing { + /// The ephemeral typing event for each room. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub rooms: BTreeMap>, + } + + impl Typing { + /// Whether all fields are empty or `None`. + pub fn is_empty(&self) -> bool { + self.rooms.is_empty() + } + } +} + +impl From for Response { + fn from(value: v4::Response) -> Self { + Self { + pos: value.pos, + initial: value.initial, + txn_id: value.txn_id, + lists: value.lists.into_iter().map(|(room_id, list)| (room_id, list.into())).collect(), + rooms: value.rooms.into_iter().map(|(room_id, room)| (room_id, room.into())).collect(), + extensions: value.extensions.into(), + } + } +} + +impl From for response::List { + fn from(value: v4::SyncList) -> Self { + Self { count: value.count } + } +} + +impl From for response::Room { + fn from(value: v4::SlidingSyncRoom) -> Self { + Self { + name: value.name, + avatar: value.avatar, + initial: value.initial, + is_dm: value.is_dm, + invite_state: value.invite_state, + unread_notifications: value.unread_notifications, + timeline: value.timeline, + required_state: value.required_state, + prev_batch: value.prev_batch, + limited: value.limited, + joined_count: value.joined_count, + invited_count: value.invited_count, + num_live: value.num_live, + bump_stamp: value.timestamp.map(|t| t.0), + heroes: value.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()), + } + } +} + +impl From for response::Hero { + fn from(value: v4::SlidingSyncRoomHero) -> Self { + Self { user_id: value.user_id, name: value.name, avatar: value.avatar } + } +} + +impl From for response::Extensions { + fn from(value: v4::Extensions) -> Self { + Self { + to_device: value.to_device.map(Into::into), + e2ee: value.e2ee.into(), + account_data: value.account_data.into(), + receipts: value.receipts.into(), + typing: value.typing.into(), + } + } +} + +impl From for response::ToDevice { + fn from(value: v4::ToDevice) -> Self { + Self { next_batch: value.next_batch, events: value.events } + } +} + +impl From for response::E2EE { + fn from(value: v4::E2EE) -> Self { + Self { + device_lists: value.device_lists, + device_one_time_keys_count: value.device_one_time_keys_count, + device_unused_fallback_key_types: value.device_unused_fallback_key_types, + } + } +} + +impl From for response::AccountData { + fn from(value: v4::AccountData) -> Self { + Self { global: value.global, rooms: value.rooms } + } +} + +impl From for response::Receipts { + fn from(value: v4::Receipts) -> Self { + Self { rooms: value.rooms } + } +} + +impl From for response::Typing { + fn from(value: v4::Typing) -> Self { + Self { rooms: value.rooms } + } +} + +#[cfg(test)] +mod tests { + use ruma_common::owned_room_id; + + use super::request::ReceiptsRoom; + + #[test] + fn serialize_request_receipts_room() { + let entry = ReceiptsRoom::AllSubscribed; + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#); + + let entry = ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz")); + assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#); + } + + #[test] + fn deserialize_request_receipts_room() { + assert_eq!( + serde_json::from_str::(r#""*""#).unwrap(), + ReceiptsRoom::AllSubscribed + ); + + assert_eq!( + serde_json::from_str::(r#""!foo:bar.baz""#).unwrap(), + ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz")) + ); + } +} diff --git a/crates/ruma/Cargo.toml b/crates/ruma/Cargo.toml index 63c6c275..33c9759b 100644 --- a/crates/ruma/Cargo.toml +++ b/crates/ruma/Cargo.toml @@ -228,6 +228,7 @@ unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"] unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] unstable-msc4140 = ["ruma-client-api?/unstable-msc4140"] +unstable-msc4186 = ["ruma-client-api?/unstable-msc4186"] unstable-pdu = ["ruma-events?/unstable-pdu"] unstable-unspecified = [ "ruma-common/unstable-unspecified", @@ -279,6 +280,7 @@ __unstable-mscs = [ "unstable-msc4121", "unstable-msc4125", "unstable-msc4140", + "unstable-msc4186", ] __ci = [ "full",