ruma_client_api/state/
send_state_event.rs

1//! `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
2//!
3//! Send a state event to a room associated with a given state key.
4
5pub mod v3 {
6    //! `/v3/` ([spec])
7    //!
8    //! [spec]: https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
9
10    use std::borrow::Borrow;
11
12    use ruma_common::{
13        api::{response, Metadata},
14        metadata,
15        serde::Raw,
16        MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
17    };
18    use ruma_events::{AnyStateEventContent, StateEventContent, StateEventType};
19    use serde_json::value::to_raw_value as to_raw_json_value;
20
21    metadata! {
22        method: PUT,
23        rate_limited: false,
24        authentication: AccessToken,
25        history: {
26            1.0 => "/_matrix/client/r0/rooms/{room_id}/state/{event_type}/{state_key}",
27            1.1 => "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}",
28        }
29    }
30
31    /// Request type for the `send_state_event` endpoint.
32    #[derive(Clone, Debug)]
33    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
34    pub struct Request {
35        /// The room to set the state in.
36        pub room_id: OwnedRoomId,
37
38        /// The type of event to send.
39        pub event_type: StateEventType,
40
41        /// The state_key for the state to send.
42        pub state_key: String,
43
44        /// The event content to send.
45        pub body: Raw<AnyStateEventContent>,
46
47        /// Timestamp to use for the `origin_server_ts` of the event.
48        ///
49        /// This is called [timestamp massaging] and can only be used by Appservices.
50        ///
51        /// Note that this does not change the position of the event in the timeline.
52        ///
53        /// [timestamp massaging]: https://spec.matrix.org/latest/application-service-api/#timestamp-massaging
54        pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
55    }
56
57    impl Request {
58        /// Creates a new `Request` with the given room id, state key and event content.
59        ///
60        /// # Errors
61        ///
62        /// Since `Request` stores the request body in serialized form, this function can fail if
63        /// `T`s [`Serialize`][serde::Serialize] implementation can fail.
64        pub fn new<T, K>(
65            room_id: OwnedRoomId,
66            state_key: &K,
67            content: &T,
68        ) -> serde_json::Result<Self>
69        where
70            T: StateEventContent,
71            T::StateKey: Borrow<K>,
72            K: AsRef<str> + ?Sized,
73        {
74            Ok(Self {
75                room_id,
76                state_key: state_key.as_ref().to_owned(),
77                event_type: content.event_type(),
78                body: Raw::from_json(to_raw_json_value(content)?),
79                timestamp: None,
80            })
81        }
82
83        /// Creates a new `Request` with the given room id, event type, state key and raw event
84        /// content.
85        pub fn new_raw(
86            room_id: OwnedRoomId,
87            event_type: StateEventType,
88            state_key: String,
89            body: Raw<AnyStateEventContent>,
90        ) -> Self {
91            Self { room_id, event_type, state_key, body, timestamp: None }
92        }
93    }
94
95    /// Response type for the `send_state_event` endpoint.
96    #[response(error = crate::Error)]
97    pub struct Response {
98        /// A unique identifier for the event.
99        pub event_id: OwnedEventId,
100    }
101
102    impl Response {
103        /// Creates a new `Response` with the given event id.
104        pub fn new(event_id: OwnedEventId) -> Self {
105            Self { event_id }
106        }
107    }
108
109    #[cfg(feature = "client")]
110    impl ruma_common::api::OutgoingRequest for Request {
111        type EndpointError = crate::Error;
112        type IncomingResponse = Response;
113
114        fn try_into_http_request<T: Default + bytes::BufMut>(
115            self,
116            base_url: &str,
117            access_token: ruma_common::api::SendAccessToken<'_>,
118            considering: &'_ ruma_common::api::SupportedVersions,
119        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
120            use http::header::{self, HeaderValue};
121
122            let query_string =
123                serde_html_form::to_string(RequestQuery { timestamp: self.timestamp })?;
124
125            let http_request = http::Request::builder()
126                .method(http::Method::PUT)
127                .uri(Self::make_endpoint_url(
128                    considering,
129                    base_url,
130                    &[&self.room_id, &self.event_type, &self.state_key],
131                    &query_string,
132                )?)
133                .header(header::CONTENT_TYPE, "application/json")
134                .header(
135                    header::AUTHORIZATION,
136                    HeaderValue::from_str(&format!(
137                        "Bearer {}",
138                        access_token
139                            .get_required_for_endpoint()
140                            .ok_or(ruma_common::api::error::IntoHttpError::NeedsAuthentication)?
141                    ))?,
142                )
143                .body(ruma_common::serde::json_to_buf(&self.body)?)?;
144
145            Ok(http_request)
146        }
147    }
148
149    #[cfg(feature = "server")]
150    impl ruma_common::api::IncomingRequest for Request {
151        type EndpointError = crate::Error;
152        type OutgoingResponse = Response;
153
154        fn try_from_http_request<B, S>(
155            request: http::Request<B>,
156            path_args: &[S],
157        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
158        where
159            B: AsRef<[u8]>,
160            S: AsRef<str>,
161        {
162            // FIXME: find a way to make this if-else collapse with serde recognizing trailing
163            // Option
164            let (room_id, event_type, state_key): (OwnedRoomId, StateEventType, String) =
165                if path_args.len() == 3 {
166                    serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
167                        _,
168                        serde::de::value::Error,
169                    >::new(
170                        path_args.iter().map(::std::convert::AsRef::as_ref),
171                    ))?
172                } else {
173                    let (a, b) =
174                        serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
175                            _,
176                            serde::de::value::Error,
177                        >::new(
178                            path_args.iter().map(::std::convert::AsRef::as_ref),
179                        ))?;
180
181                    (a, b, "".into())
182                };
183
184            let request_query: RequestQuery =
185                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
186
187            let body = serde_json::from_slice(request.body().as_ref())?;
188
189            Ok(Self { room_id, event_type, state_key, body, timestamp: request_query.timestamp })
190        }
191    }
192
193    /// Data in the request's query string.
194    #[derive(Debug)]
195    #[cfg_attr(feature = "client", derive(serde::Serialize))]
196    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
197    struct RequestQuery {
198        /// Timestamp to use for the `origin_server_ts` of the event.
199        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
200        timestamp: Option<MilliSecondsSinceUnixEpoch>,
201    }
202
203    #[cfg(feature = "client")]
204    #[test]
205    fn serialize() {
206        use ruma_common::{
207            api::{MatrixVersion, OutgoingRequest as _, SendAccessToken, SupportedVersions},
208            owned_room_id,
209        };
210        use ruma_events::{room::name::RoomNameEventContent, EmptyStateKey};
211
212        let supported = SupportedVersions {
213            versions: [MatrixVersion::V1_1].into(),
214            features: Default::default(),
215        };
216
217        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
218        let req = Request::new(
219            owned_room_id!("!room:server.tld"),
220            &EmptyStateKey,
221            &RoomNameEventContent::new("Test room".to_owned()),
222        )
223        .unwrap()
224        .try_into_http_request::<Vec<u8>>(
225            "https://server.tld",
226            SendAccessToken::IfRequired("access_token"),
227            &supported,
228        )
229        .unwrap();
230
231        assert_eq!(
232            req.uri(),
233            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
234        );
235    }
236}