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::{auth_scheme::AccessToken, 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 + AsRef<[u8]>>(
115            self,
116            base_url: &str,
117            access_token: ruma_common::api::auth_scheme::SendAccessToken<'_>,
118            considering: std::borrow::Cow<'_, ruma_common::api::SupportedVersions>,
119        ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
120            use ruma_common::api::auth_scheme::AuthScheme;
121
122            let query_string =
123                serde_html_form::to_string(RequestQuery { timestamp: self.timestamp })?;
124
125            let mut http_request = http::Request::builder()
126                .method(Self::METHOD)
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(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
134                .body(ruma_common::serde::json_to_buf(&self.body)?)?;
135
136            Self::Authentication::add_authentication(&mut http_request, access_token).map_err(
137                |error| ruma_common::api::error::IntoHttpError::Authentication(error.into()),
138            )?;
139
140            Ok(http_request)
141        }
142    }
143
144    #[cfg(feature = "server")]
145    impl ruma_common::api::IncomingRequest for Request {
146        type EndpointError = crate::Error;
147        type OutgoingResponse = Response;
148
149        fn try_from_http_request<B, S>(
150            request: http::Request<B>,
151            path_args: &[S],
152        ) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
153        where
154            B: AsRef<[u8]>,
155            S: AsRef<str>,
156        {
157            Self::check_request_method(request.method())?;
158
159            // FIXME: find a way to make this if-else collapse with serde recognizing trailing
160            // Option
161            let (room_id, event_type, state_key): (OwnedRoomId, StateEventType, String) =
162                if path_args.len() == 3 {
163                    serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
164                        _,
165                        serde::de::value::Error,
166                    >::new(
167                        path_args.iter().map(::std::convert::AsRef::as_ref),
168                    ))?
169                } else {
170                    let (a, b) =
171                        serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::<
172                            _,
173                            serde::de::value::Error,
174                        >::new(
175                            path_args.iter().map(::std::convert::AsRef::as_ref),
176                        ))?;
177
178                    (a, b, "".into())
179                };
180
181            let request_query: RequestQuery =
182                serde_html_form::from_str(request.uri().query().unwrap_or(""))?;
183
184            let body = serde_json::from_slice(request.body().as_ref())?;
185
186            Ok(Self { room_id, event_type, state_key, body, timestamp: request_query.timestamp })
187        }
188    }
189
190    /// Data in the request's query string.
191    #[derive(Debug)]
192    #[cfg_attr(feature = "client", derive(serde::Serialize))]
193    #[cfg_attr(feature = "server", derive(serde::Deserialize))]
194    struct RequestQuery {
195        /// Timestamp to use for the `origin_server_ts` of the event.
196        #[serde(rename = "ts", skip_serializing_if = "Option::is_none")]
197        timestamp: Option<MilliSecondsSinceUnixEpoch>,
198    }
199
200    #[cfg(feature = "client")]
201    #[test]
202    fn serialize() {
203        use std::borrow::Cow;
204
205        use ruma_common::{
206            api::{
207                auth_scheme::SendAccessToken, MatrixVersion, OutgoingRequest as _,
208                SupportedVersions,
209            },
210            owned_room_id,
211        };
212        use ruma_events::{room::name::RoomNameEventContent, EmptyStateKey};
213
214        let supported = SupportedVersions {
215            versions: [MatrixVersion::V1_1].into(),
216            features: Default::default(),
217        };
218
219        // This used to panic in make_endpoint_url because of a mismatch in the path parameter count
220        let req = Request::new(
221            owned_room_id!("!room:server.tld"),
222            &EmptyStateKey,
223            &RoomNameEventContent::new("Test room".to_owned()),
224        )
225        .unwrap()
226        .try_into_http_request::<Vec<u8>>(
227            "https://server.tld",
228            SendAccessToken::IfRequired("access_token"),
229            Cow::Owned(supported),
230        )
231        .unwrap();
232
233        assert_eq!(
234            req.uri(),
235            "https://server.tld/_matrix/client/v3/rooms/!room:server.tld/state/m.room.name/"
236        );
237    }
238}