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 Self {
151                room_id,
152                event_type,
153                state_key,
154                body,
155                timestamp,
156                #[cfg(feature = "unstable-msc4354")]
157                sticky_duration_ms,
158            } = self;
159
160            let query_string = serde_html_form::to_string(RequestQuery {
161                timestamp,
162                #[cfg(feature = "unstable-msc4354")]
163                sticky_duration_ms,
164            })?;
165
166            let http_request = http::Request::builder()
167                .method(Self::METHOD)
168                .uri(Self::make_endpoint_url(
169                    considering,
170                    base_url,
171                    &[&room_id, &event_type, &state_key],
172                    &query_string,
173                )?)
174                .body(RequestBody(body))?;
175
176            Ok(http_request)
177        }
178    }
179
180    #[cfg(feature = "server")]
181    impl ruma_common::api::IncomingRequest for Request {
182        type EndpointError = Error;
183        type OutgoingResponse = Response;
184
185        fn try_from_http_request_inner(
186            request: http::Request<&[u8]>,
187            path_args: &[&str],
188        ) -> Result<Self, ruma_common::api::error::DeserializationError> {
189            // FIXME: find a way to make this if-else collapse with serde recognizing trailing
190            // Option
191            let (room_id, event_type, state_key): (OwnedRoomId, StateEventType, String) =
192                if path_args.len() == 3 {
193                    serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
194                        _,
195                        serde::de::value::Error,
196                    >::new(
197                        path_args.iter().copied()
198                    ))?
199                } else {
200                    let (a, b) =
201                        serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
202                            _,
203                            serde::de::value::Error,
204                        >::new(
205                            path_args.iter().copied()
206                        ))?;
207
208                    (a, b, "".into())
209                };
210
211            let RequestQuery {
212                timestamp,
213                #[cfg(feature = "unstable-msc4354")]
214                sticky_duration_ms,
215            } = serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
216
217            let body: Raw<AnyStateEventContent> = ruma_common::serde::deserialize_raw_object(
218                &mut serde_json::Deserializer::from_slice(request.into_body()),
219            )?;
220
221            Ok(Self {
222                room_id,
223                event_type,
224                state_key,
225                body,
226                timestamp,
227                #[cfg(feature = "unstable-msc4354")]
228                sticky_duration_ms,
229            })
230        }
231    }
232
233    /// Data in the request's query string.
234    #[derive(Debug)]
235    #[cfg_attr(feature = "client", derive(serde::Serialize))]
236    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
237    struct RequestQuery {
238        /// Timestamp to use for the `origin_server_ts` of the event.
239        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
240        timestamp: Option<MilliSecondsSinceUnixEpoch>,
241
242        #[cfg(feature = "unstable-msc4354")]
243        #[serde(
244            skip_serializing_if = "Option::is_none",
245            rename = "org.matrix.msc4354.sticky_duration_ms"
246        )]
247        pub sticky_duration_ms: Option<StickyDurationMs>,
248    }
249}
250
251#[cfg(all(test, feature = "client"))]
252mod tests {
253    use std::borrow::Cow;
254
255    use ruma_common::{
256        api::{
257            MatrixVersion, OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
258        },
259        owned_room_id,
260    };
261    use ruma_events::{EmptyStateKey, room::name::RoomNameEventContent};
262
263    use crate::state::send_state_event::v3::Request;
264
265    #[test]
266    fn serialize() {
267        let supported = SupportedVersions {
268            versions: [MatrixVersion::V1_1].into(),
269            features: Default::default(),
270        };
271
272        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
273        let req = Request::new(
274            owned_room_id!("!room:server.tld"),
275            &EmptyStateKey,
276            &RoomNameEventContent::new("Test room".to_owned()),
277        )
278        .unwrap()
279        .try_into_http_request::<Vec<u8>>(
280            "https://server.tld",
281            SendAccessToken::IfRequired("access_token"),
282            Cow::Owned(supported),
283        )
284        .unwrap();
285
286        assert_eq!(
287            req.uri(),
288            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
289        );
290    }
291
292    #[test]
293    #[cfg(feature = "unstable-msc4354")]
294    fn test_send_sticky_state_serialize() {
295        use ruma_events::sticky::StickyDurationMs;
296
297        let supported = SupportedVersions {
298            versions: [MatrixVersion::V1_1].into(),
299            features: Default::default(),
300        };
301
302        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
303        let mut req = Request::new(
304            owned_room_id!("!room:server.tld"),
305            &EmptyStateKey,
306            &RoomNameEventContent::new("Test room".to_owned()),
307        )
308        .unwrap();
309
310        req.sticky_duration_ms = Some(StickyDurationMs::new_clamped(1_000_u32));
311
312        let http_req = req
313            .try_into_http_request::<Vec<u8>>(
314                "https://server.tld",
315                SendAccessToken::IfRequired("access_token"),
316                Cow::Owned(supported),
317            )
318            .unwrap();
319
320        assert_eq!(http_req.uri().query().unwrap(), "org.matrix.msc4354.sticky_duration_ms=1000");
321    }
322}
323
324#[cfg(all(test, feature = "server", feature = "unstable-msc4354"))]
325mod server_tests {
326    use ruma_common::api::IncomingRequestExt as _;
327
328    use super::v3::Request;
329
330    #[test]
331    fn deserialize_sticky_duration() {
332        let request = http::Request::builder()
333            .method("PUT")
334            .uri(
335                "/_matrix/client/v3/rooms/!roomid:example.org/state/m.room.name/?org.matrix.msc4354.sticky_duration_ms=123456",
336            )
337            .body(br#"{"name":"A room"}"# as &[u8])
338            .unwrap();
339
340        let request =
341            Request::try_from_http_request(request, &["!roomid:example.org", "m.room.name", ""])
342                .unwrap();
343
344        assert_eq!(request.room_id, "!roomid:example.org");
345        assert_eq!(request.sticky_duration_ms.map(|duration| duration.get()), Some(123_456));
346    }
347}