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    #[derive(ruma_common::serde::_FakeDeriveSerde)]
133    #[cfg_attr(feature = "client", derive(serde::Serialize, ruma_common::api::OutgoingBodyJson))]
134    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
135    #[serde(transparent)]
136    // attribute will go away when we update IncomingRequest to also use RequestBody
137    #[cfg_attr(not(feature = "client"), expect(dead_code))]
138    pub struct RequestBody(Raw<AnyStateEventContent>);
139
140    #[cfg(feature = "client")]
141    impl ruma_common::api::OutgoingRequest for Request {
142        type Body = RequestBody;
143        type EndpointError = Error;
144        type IncomingResponse = Response;
145
146        fn try_into_http_request_inner(
147            self,
148            base_url: &str,
149            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
150        ) -> Result<http::Request<RequestBody>, ruma_common::api::error::IntoHttpError> {
151            use ruma_common::api::Metadata;
152
153            let query_string = serde_html_form::to_string(RequestQuery {
154                timestamp: self.timestamp,
155                #[cfg(feature = "unstable-msc4354")]
156                sticky_duration_ms: self.sticky_duration_ms,
157            })?;
158
159            let http_request = http::Request::builder()
160                .method(Self::METHOD)
161                .uri(Self::make_endpoint_url(
162                    considering,
163                    base_url,
164                    &[&self.room_id, &self.event_type, &self.state_key],
165                    &query_string,
166                )?)
167                .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
168                .body(RequestBody(self.body))?;
169
170            Ok(http_request)
171        }
172    }
173
174    #[cfg(feature = "server")]
175    impl ruma_common::api::IncomingRequest for Request {
176        type EndpointError = Error;
177        type OutgoingResponse = Response;
178
179        fn try_from_http_request<B, S>(
180            request: http::Request<B>,
181            path_args: &[S],
182        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
183        where
184            B: AsRef<[u8]>,
185            S: AsRef<str>,
186        {
187            Self::check_request_method(request.method())?;
188
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().map(::std::convert::AsRef::as_ref),
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().map(::std::convert::AsRef::as_ref),
206                        ))?;
207
208                    (a, b, "".into())
209                };
210
211            let request_query: RequestQuery =
212                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
213
214            let body: Raw<AnyStateEventContent> = ruma_common::serde::deserialize_raw_object(
215                &mut serde_json::Deserializer::from_slice(request.body().as_ref()),
216            )?;
217
218            Ok(Self {
219                room_id,
220                event_type,
221                state_key,
222                body,
223                timestamp: request_query.timestamp,
224                #[cfg(feature = "unstable-msc4354")]
225                sticky_duration_ms: request_query.sticky_duration_ms,
226            })
227        }
228    }
229
230    /// Data in the request's query string.
231    #[derive(Debug)]
232    #[cfg_attr(feature = "client", derive(serde::Serialize))]
233    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
234    struct RequestQuery {
235        /// Timestamp to use for the `origin_server_ts` of the event.
236        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
237        timestamp: Option<MilliSecondsSinceUnixEpoch>,
238
239        #[cfg(feature = "unstable-msc4354")]
240        #[serde(
241            skip_serializing_if = "Option::is_none",
242            rename = "org.matrix.msc4354.sticky_duration_ms"
243        )]
244        pub sticky_duration_ms: Option<StickyDurationMs>,
245    }
246}
247
248#[cfg(all(test, feature = "client"))]
249mod tests {
250    use std::borrow::Cow;
251
252    use ruma_common::{
253        api::{
254            MatrixVersion, OutgoingRequestExt as _, SupportedVersions, auth_scheme::SendAccessToken,
255        },
256        owned_room_id,
257    };
258    use ruma_events::{EmptyStateKey, room::name::RoomNameEventContent};
259
260    use crate::state::send_state_event::v3::Request;
261
262    #[test]
263    fn serialize() {
264        let supported = SupportedVersions {
265            versions: [MatrixVersion::V1_1].into(),
266            features: Default::default(),
267        };
268
269        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
270        let req = Request::new(
271            owned_room_id!("!room:server.tld"),
272            &EmptyStateKey,
273            &RoomNameEventContent::new("Test room".to_owned()),
274        )
275        .unwrap()
276        .try_into_http_request::<Vec<u8>>(
277            "https://server.tld",
278            SendAccessToken::IfRequired("access_token"),
279            Cow::Owned(supported),
280        )
281        .unwrap();
282
283        assert_eq!(
284            req.uri(),
285            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
286        );
287    }
288
289    #[test]
290    #[cfg(feature = "unstable-msc4354")]
291    fn test_send_sticky_state_serialize() {
292        use ruma_events::sticky::StickyDurationMs;
293
294        let supported = SupportedVersions {
295            versions: [MatrixVersion::V1_1].into(),
296            features: Default::default(),
297        };
298
299        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
300        let mut req = Request::new(
301            owned_room_id!("!room:server.tld"),
302            &EmptyStateKey,
303            &RoomNameEventContent::new("Test room".to_owned()),
304        )
305        .unwrap();
306
307        req.sticky_duration_ms = Some(StickyDurationMs::new_clamped(1_000_u32));
308
309        let http_req = req
310            .try_into_http_request::<Vec<u8>>(
311                "https://server.tld",
312                SendAccessToken::IfRequired("access_token"),
313                Cow::Owned(supported),
314            )
315            .unwrap();
316
317        assert_eq!(http_req.uri().query().unwrap(), "org.matrix.msc4354.sticky_duration_ms=1000");
318    }
319}