Skip to main content

ruma_events/secret/
request.rs

1//! Types for the [`m.secret.request`] event.
2//!
3//! [`m.secret.request`]: https://spec.matrix.org/latest/client-server-api/#msecretrequest
4
5use ruma_common::{OwnedDeviceId, OwnedTransactionId, serde::StringEnum};
6use ruma_macros::EventContent;
7use serde::{Deserialize, Serialize};
8
9use crate::{GlobalAccountDataEventType, PrivOwnedStr};
10
11/// The content of an `m.secret.request` event.
12///
13/// Event sent by a client to request a secret from another device or to cancel a previous request.
14///
15/// It is sent as an unencrypted to-device event.
16#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
17#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
18#[ruma_event(type = "m.secret.request", kind = ToDevice)]
19pub struct ToDeviceSecretRequestEventContent {
20    /// The action for the request.
21    #[serde(flatten)]
22    pub action: RequestAction,
23
24    /// The ID of the device requesting the event.
25    pub requesting_device_id: OwnedDeviceId,
26
27    /// A random string uniquely identifying (with respect to the requester and the target) the
28    /// target for a secret.
29    ///
30    /// If the secret is requested from multiple devices at the same time, the same ID may be used
31    /// for every target. The same ID is also used in order to cancel a previous request.
32    pub request_id: OwnedTransactionId,
33}
34
35impl ToDeviceSecretRequestEventContent {
36    /// Creates a new `ToDeviceRequestEventContent` with the given action, requesting device ID and
37    /// request ID.
38    pub fn new(
39        action: RequestAction,
40        requesting_device_id: OwnedDeviceId,
41        request_id: OwnedTransactionId,
42    ) -> Self {
43        Self { action, requesting_device_id, request_id }
44    }
45}
46
47/// Action for an `m.secret.request` event.
48#[derive(Clone, Debug, Deserialize, Serialize)]
49#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
50#[serde(tag = "action", rename_all = "snake_case")]
51pub enum RequestAction {
52    /// Request a secret by its name.
53    Request(SecretRequestAction),
54
55    /// Cancel a request for a secret.
56    RequestCancellation,
57
58    #[doc(hidden)]
59    #[serde(untagged)]
60    _Custom(CustomRequestAction),
61}
62
63impl RequestAction {
64    /// Access the `action` field of this action.
65    pub fn action(&self) -> &str {
66        match self {
67            Self::Request(_) => "request",
68            Self::RequestCancellation => "request_cancellation",
69            Self::_Custom(custom) => &custom.action,
70        }
71    }
72}
73
74impl From<SecretRequestAction> for RequestAction {
75    fn from(value: SecretRequestAction) -> Self {
76        Self::Request(value)
77    }
78}
79
80/// Details about a secret to request in a [`RequestAction`].
81#[derive(Clone, Debug, Serialize, Deserialize)]
82#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
83pub struct SecretRequestAction {
84    /// The name of the requested secret.
85    pub name: SecretName,
86}
87
88impl SecretRequestAction {
89    /// Construct a new `SecretRequestAction` for the given secret name.
90    pub fn new(name: SecretName) -> Self {
91        Self { name }
92    }
93}
94
95/// The name of a secret.
96#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
97#[derive(Clone, StringEnum)]
98#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
99pub enum SecretName {
100    /// Cross-signing master key (m.cross_signing.master).
101    #[ruma_enum(rename = "m.cross_signing.master")]
102    CrossSigningMasterKey,
103
104    /// Cross-signing user-signing key (m.cross_signing.user_signing).
105    #[ruma_enum(rename = "m.cross_signing.user_signing")]
106    CrossSigningUserSigningKey,
107
108    /// Cross-signing self-signing key (m.cross_signing.self_signing).
109    #[ruma_enum(rename = "m.cross_signing.self_signing")]
110    CrossSigningSelfSigningKey,
111
112    /// Recovery key (m.megolm_backup.v1).
113    #[ruma_enum(rename = "m.megolm_backup.v1")]
114    RecoveryKey,
115
116    #[doc(hidden)]
117    _Custom(PrivOwnedStr),
118}
119
120impl From<SecretName> for GlobalAccountDataEventType {
121    fn from(value: SecretName) -> Self {
122        GlobalAccountDataEventType::from(value.as_str())
123    }
124}
125
126/// A custom [`RequestAction`].
127#[doc(hidden)]
128#[derive(Clone, Debug, Serialize, Deserialize)]
129pub struct CustomRequestAction {
130    /// The action of the request.
131    action: String,
132}
133
134#[cfg(test)]
135mod tests {
136    use assert_matches2::assert_matches;
137    use ruma_common::canonical_json::assert_to_canonical_json_eq;
138    use serde_json::{from_value as from_json_value, json};
139
140    use super::{
141        RequestAction, SecretName, SecretRequestAction, ToDeviceSecretRequestEventContent,
142    };
143
144    #[test]
145    fn secret_request_serialization() {
146        let content = ToDeviceSecretRequestEventContent::new(
147            RequestAction::Request(SecretRequestAction::new("org.example.some.secret".into())),
148            "ABCDEFG".into(),
149            "randomly_generated_id_9573".into(),
150        );
151
152        assert_to_canonical_json_eq!(
153            content,
154            json!({
155                "name": "org.example.some.secret",
156                "action": "request",
157                "requesting_device_id": "ABCDEFG",
158                "request_id": "randomly_generated_id_9573",
159            }),
160        );
161    }
162
163    #[test]
164    fn secret_request_recovery_key_serialization() {
165        let content = ToDeviceSecretRequestEventContent::new(
166            RequestAction::Request(SecretRequestAction::new(SecretName::RecoveryKey)),
167            "XYZxyz".into(),
168            "this_is_a_request_id".into(),
169        );
170
171        assert_to_canonical_json_eq!(
172            content,
173            json!({
174                "name": "m.megolm_backup.v1",
175                "action": "request",
176                "requesting_device_id": "XYZxyz",
177                "request_id": "this_is_a_request_id",
178            }),
179        );
180    }
181
182    #[test]
183    fn secret_request_cancellation_serialization() {
184        let content = ToDeviceSecretRequestEventContent::new(
185            RequestAction::RequestCancellation,
186            "ABCDEFG".into(),
187            "randomly_generated_id_9573".into(),
188        );
189
190        assert_to_canonical_json_eq!(
191            content,
192            json!({
193                "action": "request_cancellation",
194                "requesting_device_id": "ABCDEFG",
195                "request_id": "randomly_generated_id_9573",
196            }),
197        );
198    }
199
200    #[test]
201    fn secret_request_deserialization() {
202        let json = json!({
203            "name": "org.example.some.secret",
204            "action": "request",
205            "requesting_device_id": "ABCDEFG",
206            "request_id": "randomly_generated_id_9573"
207        });
208
209        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
210        assert_eq!(content.requesting_device_id, "ABCDEFG");
211        assert_eq!(content.request_id, "randomly_generated_id_9573");
212        assert_matches!(content.action, RequestAction::Request(secret));
213        assert_eq!(secret.name.as_str(), "org.example.some.secret");
214    }
215
216    #[test]
217    fn secret_request_cancellation_deserialization() {
218        let json = json!({
219            "action": "request_cancellation",
220            "requesting_device_id": "ABCDEFG",
221            "request_id": "randomly_generated_id_9573"
222        });
223
224        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
225        assert_eq!(content.requesting_device_id, "ABCDEFG");
226        assert_eq!(content.request_id, "randomly_generated_id_9573");
227        assert_matches!(content.action, RequestAction::RequestCancellation);
228    }
229
230    #[test]
231    fn secret_request_recovery_key_deserialization() {
232        let json = json!({
233            "name": "m.megolm_backup.v1",
234            "action": "request",
235            "requesting_device_id": "XYZxyz",
236            "request_id": "this_is_a_request_id"
237        });
238
239        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json).unwrap();
240        assert_eq!(content.requesting_device_id, "XYZxyz");
241        assert_eq!(content.request_id, "this_is_a_request_id");
242        assert_matches!(content.action, RequestAction::Request(secret));
243        assert_eq!(secret.name, SecretName::RecoveryKey);
244    }
245
246    #[test]
247    fn secret_custom_action_serialization_roundtrip() {
248        let json = json!({
249            "action": "my_custom_action",
250            "requesting_device_id": "XYZxyz",
251            "request_id": "this_is_a_request_id"
252        });
253
254        let content = from_json_value::<ToDeviceSecretRequestEventContent>(json.clone()).unwrap();
255        assert_eq!(content.requesting_device_id, "XYZxyz");
256        assert_eq!(content.request_id, "this_is_a_request_id");
257        assert_eq!(content.action.action(), "my_custom_action");
258
259        assert_to_canonical_json_eq!(content, json);
260    }
261}