Skip to main content

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/v1.19/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
9
10    use std::borrow::Borrow;
11
12    use ruma_common::{
13        MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
14        api::{auth_scheme::AccessToken, error::Error, response},
15        metadata,
16        serde::Raw,
17    };
18    #[cfg(feature = "unstable-msc4354")]
19    use ruma_events::sticky::StickyDurationMs;
20    use ruma_events::{AnyStateEventContent, StateEventContent, StateEventType};
21    use serde_json::value::to_raw_value as to_raw_json_value;
22
23    metadata! {
24        method: PUT,
25        rate_limited: false,
26        authentication: AccessToken,
27        history: {
28            1.0 => "/_matrix/client/r0/rooms/{room_id}/state/{event_type}/{state_key}",
29            1.1 => "/_matrix/client/v3/rooms/{room_id}/state/{event_type}/{state_key}",
30        }
31    }
32
33    /// Request type for the `send_state_event` endpoint.
34    #[derive(Clone, Debug)]
35    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
36    pub struct Request {
37        /// The room to set the state in.
38        pub room_id: OwnedRoomId,
39
40        /// The type of event to send.
41        pub event_type: StateEventType,
42
43        /// The state_key for the state to send.
44        pub state_key: String,
45
46        /// The event content to send.
47        pub body: Raw<AnyStateEventContent>,
48
49        /// Timestamp to use for the `origin_server_ts` of the event.
50        ///
51        /// This is called [timestamp massaging] and can only be used by Appservices.
52        ///
53        /// Note that this does not change the position of the event in the timeline.
54        ///
55        /// [timestamp massaging]: https://spec.matrix.org/v1.19/application-service-api/#timestamp-massaging
56        pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
57
58        /// The duration to stick the event for.
59        ///
60        /// Valid values are the integer range 0-3600000 (1 hour).
61        /// The presence of this field indicates that the event should be sticky, this
62        /// will give this event additional delivery guarantees.
63        ///
64        /// See [MSC4354 sticky events](https://github.com/matrix-org/matrix-spec-proposals/pull/4354).
65        #[cfg(feature = "unstable-msc4354")]
66        pub sticky_duration_ms: Option<StickyDurationMs>,
67    }
68
69    impl Request {
70        /// Creates a new `Request` with the given room id, state key and event content.
71        ///
72        /// # Errors
73        ///
74        /// Since `Request` stores the request body in serialized form, this function can fail if
75        /// `T`s [`Serialize`][serde::Serialize] implementation can fail.
76        pub fn new<T, K>(
77            room_id: OwnedRoomId,
78            state_key: &K,
79            content: &T,
80        ) -> serde_json::Result<Self>
81        where
82            T: StateEventContent,
83            T::StateKey: Borrow<K>,
84            K: AsRef<str> + ?Sized,
85        {
86            Ok(Self {
87                room_id,
88                state_key: state_key.as_ref().to_owned(),
89                event_type: content.event_type(),
90                body: Raw::from_json(to_raw_json_value(content)?),
91                timestamp: None,
92                #[cfg(feature = "unstable-msc4354")]
93                sticky_duration_ms: None,
94            })
95        }
96
97        /// Creates a new `Request` with the given room id, event type, state key and raw event
98        /// content.
99        pub fn new_raw(
100            room_id: OwnedRoomId,
101            event_type: StateEventType,
102            state_key: String,
103            body: Raw<AnyStateEventContent>,
104        ) -> Self {
105            Self {
106                room_id,
107                event_type,
108                state_key,
109                body,
110                timestamp: None,
111                #[cfg(feature = "unstable-msc4354")]
112                sticky_duration_ms: None,
113            }
114        }
115    }
116
117    /// Response type for the `send_state_event` endpoint.
118    #[response]
119    pub struct Response {
120        /// A unique identifier for the event.
121        pub event_id: OwnedEventId,
122    }
123
124    impl Response {
125        /// Creates a new `Response` with the given event id.
126        pub fn new(event_id: OwnedEventId) -> Self {
127            Self { event_id }
128        }
129    }
130
131    #[doc(hidden)]
132    #[cfg(feature = "client")]
133    #[derive(serde::Serialize, ruma_common::api::OutgoingBodyJson)]
134    #[serde(transparent)]
135    pub struct RequestBody(Raw<AnyStateEventContent>);
136
137    #[cfg(feature = "client")]
138    impl ruma_common::api::OutgoingRequest for Request {
139        type Body = RequestBody;
140        type EndpointError = Error;
141        type IncomingResponse = Response;
142
143        fn try_into_http_request_inner(
144            self,
145            base_url: &str,
146            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
147        ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
148            use ruma_common::api::Metadata;
149
150            let query_string = serde_html_form::to_string(RequestQuery {
151                timestamp: self.timestamp,
152                #[cfg(feature = "unstable-msc4354")]
153                sticky_duration_ms: self.sticky_duration_ms,
154            })?;
155
156            let http_request = http::Request::builder()
157                .method(Self::METHOD)
158                .uri(Self::make_endpoint_url(
159                    considering,
160                    base_url,
161                    &[&self.room_id, &self.event_type, &self.state_key],
162                    &query_string,
163                )?)
164                .body(RequestBody(self.body))?;
165
166            Ok(http_request)
167        }
168    }
169
170    #[cfg(feature = "server")]
171    impl ruma_common::api::IncomingRequest for Request {
172        type EndpointError = Error;
173        type OutgoingResponse = Response;
174
175        fn try_from_http_request_inner(
176            request: http::Request<&[u8]>,
177            path_args: &[&str],
178        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
179            // FIXME: find a way to make this if-else collapse with serde recognizing trailing
180            // Option
181            let (room_id, event_type, state_key): (OwnedRoomId, StateEventType, String) =
182                if path_args.len() == 3 {
183                    serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
184                        _,
185                        serde::de::value::Error,
186                    >::new(
187                        path_args.iter().copied()
188                    ))?
189                } else {
190                    let (a, b) =
191                        serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
192                            _,
193                            serde::de::value::Error,
194                        >::new(
195                            path_args.iter().copied()
196                        ))?;
197
198                    (a, b, "".into())
199                };
200
201            let request_query: RequestQuery =
202                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
203
204            let body: Raw<AnyStateEventContent> = ruma_common::serde::deserialize_raw_object(
205                &mut serde_json::Deserializer::from_slice(request.into_body()),
206            )?;
207
208            Ok(Self {
209                room_id,
210                event_type,
211                state_key,
212                body,
213                timestamp: request_query.timestamp,
214                #[cfg(feature = "unstable-msc4354")]
215                sticky_duration_ms: request_query.sticky_duration_ms,
216            })
217        }
218    }
219
220    /// Data in the request's query string.
221    #[derive(Debug)]
222    #[cfg_attr(feature = "client", derive(serde::Serialize))]
223    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
224    struct RequestQuery {
225        /// Timestamp to use for the `origin_server_ts` of the event.
226        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
227        timestamp: Option<MilliSecondsSinceUnixEpoch>,
228
229        #[cfg(feature = "unstable-msc4354")]
230        #[serde(
231            skip_serializing_if = "Option::is_none",
232            rename = "org.matrix.msc4354.sticky_duration_ms"
233        )]
234        pub sticky_duration_ms: Option<StickyDurationMs>,
235    }
236}
237
238#[cfg(all(test, feature = "client"))]
239mod tests {
240    use std::borrow::Cow;
241
242    use ruma_common::{
243        api::{
244            MatrixVersion, OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
245        },
246        owned_room_id,
247    };
248    use ruma_events::{EmptyStateKey, room::name::RoomNameEventContent};
249
250    use crate::state::send_state_event::v3::Request;
251
252    #[test]
253    fn serialize() {
254        let supported = SupportedVersions {
255            versions: [MatrixVersion::V1_1].into(),
256            features: Default::default(),
257        };
258
259        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
260        let req = Request::new(
261            owned_room_id!("!room:server.tld"),
262            &EmptyStateKey,
263            &RoomNameEventContent::new("Test room".to_owned()),
264        )
265        .unwrap()
266        .try_into_http_request::<Vec<u8>>(
267            "https://server.tld",
268            SendAccessToken::IfRequired("access_token"),
269            Cow::Owned(supported),
270        )
271        .unwrap();
272
273        assert_eq!(
274            req.uri(),
275            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
276        );
277    }
278
279    #[test]
280    #[cfg(feature = "unstable-msc4354")]
281    fn test_send_sticky_state_serialize() {
282        use ruma_events::sticky::StickyDurationMs;
283
284        let supported = SupportedVersions {
285            versions: [MatrixVersion::V1_1].into(),
286            features: Default::default(),
287        };
288
289        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
290        let mut req = Request::new(
291            owned_room_id!("!room:server.tld"),
292            &EmptyStateKey,
293            &RoomNameEventContent::new("Test room".to_owned()),
294        )
295        .unwrap();
296
297        req.sticky_duration_ms = Some(StickyDurationMs::new_clamped(1_000_u32));
298
299        let http_req = req
300            .try_into_http_request::<Vec<u8>>(
301                "https://server.tld",
302                SendAccessToken::IfRequired("access_token"),
303                Cow::Owned(supported),
304            )
305            .unwrap();
306
307        assert_eq!(http_req.uri().query().unwrap(), "org.matrix.msc4354.sticky_duration_ms=1000");
308    }
309}
310
311#[cfg(all(test, feature = "server", feature = "unstable-msc4354"))]
312mod server_tests {
313    use ruma_common::{api::IncomingRequestExt as _, owned_room_id};
314
315    use super::v3::Request;
316
317    #[test]
318    fn deserialize_sticky_duration() {
319        let request = http::Request::builder()
320            .method("PUT")
321            .uri(
322                "/_matrix/client/v3/rooms/!roomid:example.org/state/m.room.name/?org.matrix.msc4354.sticky_duration_ms=123456",
323            )
324            .body(br#"{"name":"A room"}"# as &[u8])
325            .unwrap();
326
327        let request =
328            Request::try_from_http_request(request, &["!roomid:example.org", "m.room.name", ""])
329                .unwrap();
330
331        assert_eq!(request.room_id, owned_room_id!("!roomid:example.org"));
332        assert_eq!(request.sticky_duration_ms.map(|duration| duration.get()), Some(123_456));
333    }
334}