ruma-client-api 0.25.0

Types for the endpoints in the Matrix client-server API.
Documentation
//! `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
//!
//! Send a state event to a room associated with a given state key.

pub mod v3 {
    //! `/v3/` ([spec])
    //!
    //! [spec]: https://spec.matrix.org/v1.19/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey

    use std::borrow::Borrow;

    use ruma_common::{
        MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
        api::{auth_scheme::AccessToken, error::Error, response},
        metadata,
        serde::Raw,
    };
    #[cfg(feature = "unstable-msc4354")]
    use ruma_events::sticky::StickyDurationMs;
    use ruma_events::{AnyStateEventContent, StateEventContent, StateEventType};
    use serde_json::value::to_raw_value as to_raw_json_value;

    metadata! {
        method: PUT,
        rate_limited: false,
        authentication: AccessToken,
        history: {
            1.0 => "/_matrix/client/r0/rooms/{room_id}/state/{event_type}/{state_key}",
            1.1 => "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}",
        }
    }

    /// Request type for the `send_state_event` endpoint.
    #[derive(Clone, Debug)]
    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
    pub struct Request {
        /// The room to set the state in.
        pub room_id: OwnedRoomId,

        /// The type of event to send.
        pub event_type: StateEventType,

        /// The state_key for the state to send.
        pub state_key: String,

        /// The event content to send.
        pub body: Raw<AnyStateEventContent>,

        /// Timestamp to use for the `origin_server_ts` of the event.
        ///
        /// This is called [timestamp massaging] and can only be used by Appservices.
        ///
        /// Note that this does not change the position of the event in the timeline.
        ///
        /// [timestamp massaging]: https://spec.matrix.org/v1.19/application-service-api/#timestamp-massaging
        pub timestamp: Option<MilliSecondsSinceUnixEpoch>,

        /// The duration to stick the event for.
        ///
        /// Valid values are the integer range 0-3600000 (1 hour).
        /// The presence of this field indicates that the event should be sticky, this
        /// will give this event additional delivery guarantees.
        ///
        /// See [MSC4354 sticky events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354).
        #[cfg(feature = "unstable-msc4354")]
        pub sticky_duration_ms: Option<StickyDurationMs>,
    }

    impl Request {
        /// Creates a new `Request` with the given room id, state key and event content.
        ///
        /// # Errors
        ///
        /// Since `Request` stores the request body in serialized form, this function can fail if
        /// `T`s [`Serialize`][serde::Serialize] implementation can fail.
        pub fn new<T, K>(
            room_id: OwnedRoomId,
            state_key: &K,
            content: &T,
        ) -> serde_json::Result<Self>
        where
            T: StateEventContent,
            T::StateKey: Borrow<K>,
            K: AsRef<str> + ?Sized,
        {
            Ok(Self {
                room_id,
                state_key: state_key.as_ref().to_owned(),
                event_type: content.event_type(),
                body: Raw::from_json(to_raw_json_value(content)?),
                timestamp: None,
                #[cfg(feature = "unstable-msc4354")]
                sticky_duration_ms: None,
            })
        }

        /// Creates a new `Request` with the given room id, event type, state key and raw event
        /// content.
        pub fn new_raw(
            room_id: OwnedRoomId,
            event_type: StateEventType,
            state_key: String,
            body: Raw<AnyStateEventContent>,
        ) -> Self {
            Self {
                room_id,
                event_type,
                state_key,
                body,
                timestamp: None,
                #[cfg(feature = "unstable-msc4354")]
                sticky_duration_ms: None,
            }
        }
    }

    /// Response type for the `send_state_event` endpoint.
    #[response]
    pub struct Response {
        /// A unique identifier for the event.
        pub event_id: OwnedEventId,
    }

    impl Response {
        /// Creates a new `Response` with the given event id.
        pub fn new(event_id: OwnedEventId) -> Self {
            Self { event_id }
        }
    }

    #[doc(hidden)]
    #[cfg(feature = "client")]
    #[derive(serde::Serialize, ruma_common::api::OutgoingBodyJson)]
    #[serde(transparent)]
    pub struct RequestBody(Raw<AnyStateEventContent>);

    #[cfg(feature = "client")]
    impl ruma_common::api::OutgoingRequest for Request {
        type Body = RequestBody;
        type EndpointError = Error;
        type IncomingResponse = Response;

        fn try_into_http_request_inner(
            self,
            base_url: &str,
            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
        ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
            use ruma_common::api::Metadata;

            let query_string = serde_html_form::to_string(RequestQuery {
                timestamp: self.timestamp,
                #[cfg(feature = "unstable-msc4354")]
                sticky_duration_ms: self.sticky_duration_ms,
            })?;

            let http_request = http::Request::builder()
                .method(Self::METHOD)
                .uri(Self::make_endpoint_url(
                    considering,
                    base_url,
                    &[&self.room_id, &self.event_type, &self.state_key],
                    &query_string,
                )?)
                .body(RequestBody(self.body))?;

            Ok(http_request)
        }
    }

    #[cfg(feature = "server")]
    impl ruma_common::api::IncomingRequest for Request {
        type EndpointError = Error;
        type OutgoingResponse = Response;

        fn try_from_http_request_inner(
            request: http::Request<&[u8]>,
            path_args: &[&str],
        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
            // FIXME: find a way to make this if-else collapse with serde recognizing trailing
            // Option
            let (room_id, event_type, state_key): (OwnedRoomId, StateEventType, String) =
                if path_args.len() == 3 {
                    serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
                        _,
                        serde::de::value::Error,
                    >::new(
                        path_args.iter().copied()
                    ))?
                } else {
                    let (a, b) =
                        serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
                            _,
                            serde::de::value::Error,
                        >::new(
                            path_args.iter().copied()
                        ))?;

                    (a, b, "".into())
                };

            let request_query: RequestQuery =
                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;

            let body: Raw<AnyStateEventContent> = ruma_common::serde::deserialize_raw_object(
                &mut serde_json::Deserializer::from_slice(request.into_body()),
            )?;

            Ok(Self {
                room_id,
                event_type,
                state_key,
                body,
                timestamp: request_query.timestamp,
                #[cfg(feature = "unstable-msc4354")]
                sticky_duration_ms: request_query.sticky_duration_ms,
            })
        }
    }

    /// Data in the request's query string.
    #[derive(Debug)]
    #[cfg_attr(feature = "client", derive(serde::Serialize))]
    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
    struct RequestQuery {
        /// Timestamp to use for the `origin_server_ts` of the event.
        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
        timestamp: Option<MilliSecondsSinceUnixEpoch>,

        #[cfg(feature = "unstable-msc4354")]
        #[serde(
            skip_serializing_if = "Option::is_none",
            rename = "org.matrix.msc4354.sticky_duration_ms"
        )]
        pub sticky_duration_ms: Option<StickyDurationMs>,
    }
}

#[cfg(all(test, feature = "client"))]
mod tests {
    use std::borrow::Cow;

    use ruma_common::{
        api::{
            MatrixVersion, OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
        },
        owned_room_id,
    };
    use ruma_events::{EmptyStateKey, room::name::RoomNameEventContent};

    use crate::state::send_state_event::v3::Request;

    #[test]
    fn serialize() {
        let supported = SupportedVersions {
            versions: [MatrixVersion::V1_1].into(),
            features: Default::default(),
        };

        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
        let req = Request::new(
            owned_room_id!("!room:server.tld"),
            &EmptyStateKey,
            &RoomNameEventContent::new("Test room".to_owned()),
        )
        .unwrap()
        .try_into_http_request::<Vec<u8>>(
            "https://server.tld",
            SendAccessToken::IfRequired("access_token"),
            Cow::Owned(supported),
        )
        .unwrap();

        assert_eq!(
            req.uri(),
            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
        );
    }

    #[test]
    #[cfg(feature = "unstable-msc4354")]
    fn test_send_sticky_state_serialize() {
        use ruma_events::sticky::StickyDurationMs;

        let supported = SupportedVersions {
            versions: [MatrixVersion::V1_1].into(),
            features: Default::default(),
        };

        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
        let mut req = Request::new(
            owned_room_id!("!room:server.tld"),
            &EmptyStateKey,
            &RoomNameEventContent::new("Test room".to_owned()),
        )
        .unwrap();

        req.sticky_duration_ms = Some(StickyDurationMs::new_clamped(1_000_u32));

        let http_req = req
            .try_into_http_request::<Vec<u8>>(
                "https://server.tld",
                SendAccessToken::IfRequired("access_token"),
                Cow::Owned(supported),
            )
            .unwrap();

        assert_eq!(http_req.uri().query().unwrap(), "org.matrix.msc4354.sticky_duration_ms=1000");
    }
}

#[cfg(all(test, feature = "server", feature = "unstable-msc4354"))]
mod server_tests {
    use ruma_common::{api::IncomingRequestExt as _, owned_room_id};

    use super::v3::Request;

    #[test]
    fn deserialize_sticky_duration() {
        let request = http::Request::builder()
            .method("PUT")
            .uri(
                "/_matrix/client/v3/rooms/!roomid:example.org/state/m.room.name/?org.matrix.msc4354.sticky_duration_ms=123456",
            )
            .body(br#"{"name":"A room"}"# as &[u8])
            .unwrap();

        let request =
            Request::try_from_http_request(request, &["!roomid:example.org", "m.room.name", ""])
                .unwrap();

        assert_eq!(request.room_id, owned_room_id!("!roomid:example.org"));
        assert_eq!(request.sticky_duration_ms.map(|duration| duration.get()), Some(123_456));
    }
}