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")]); + } + } }