1use std::borrow::Cow;
6
7use as_variant::as_variant;
8use ruma_common::{
9 EventEncryptionAlgorithm, OwnedRoomId,
10 serde::{Base64, JsonObject, from_raw_json_value},
11};
12use ruma_macros::{EventContent, StringEnum};
13use serde::{Deserialize, Serialize, de};
14use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
15
16use crate::PrivOwnedStr;
17
18#[derive(Clone, Debug, Serialize, EventContent)]
24#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
25#[ruma_event(type = "m.room_key.withheld", kind = ToDevice)]
26pub struct ToDeviceRoomKeyWithheldEventContent {
27 pub algorithm: EventEncryptionAlgorithm,
31
32 #[serde(flatten)]
34 pub code: RoomKeyWithheldCodeInfo,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
40 pub reason: Option<String>,
41
42 pub sender_key: Base64,
44}
45
46impl ToDeviceRoomKeyWithheldEventContent {
47 pub fn new(
50 algorithm: EventEncryptionAlgorithm,
51 code: RoomKeyWithheldCodeInfo,
52 sender_key: Base64,
53 ) -> Self {
54 Self { algorithm, code, reason: None, sender_key }
55 }
56}
57
58impl<'de> Deserialize<'de> for ToDeviceRoomKeyWithheldEventContent {
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: de::Deserializer<'de>,
62 {
63 #[derive(Deserialize)]
64 struct ToDeviceRoomKeyWithheldEventContentDeHelper {
65 algorithm: EventEncryptionAlgorithm,
66 reason: Option<String>,
67 sender_key: Base64,
68 }
69
70 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
71
72 let ToDeviceRoomKeyWithheldEventContentDeHelper { algorithm, reason, sender_key } =
73 from_raw_json_value(&json)?;
74 let code = from_raw_json_value(&json)?;
75
76 Ok(Self { algorithm, code, reason, sender_key })
77 }
78}
79
80#[derive(Debug, Clone, Serialize)]
82#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
83#[serde(tag = "code")]
84pub enum RoomKeyWithheldCodeInfo {
85 #[serde(rename = "m.blacklisted")]
89 Blacklisted(Box<RoomKeyWithheldSessionData>),
90
91 #[serde(rename = "m.unverified")]
96 Unverified(Box<RoomKeyWithheldSessionData>),
97
98 #[serde(rename = "m.unauthorised")]
104 Unauthorized(Box<RoomKeyWithheldSessionData>),
105
106 #[serde(rename = "m.unavailable")]
111 Unavailable(Box<RoomKeyWithheldSessionData>),
112
113 #[serde(rename = "m.no_olm")]
117 NoOlm,
118
119 #[serde(rename = "m.history_not_shared")]
123 HistoryNotShared,
124
125 #[doc(hidden)]
126 #[serde(untagged)]
127 _Custom(Box<CustomRoomKeyWithheldCodeInfo>),
128}
129
130impl RoomKeyWithheldCodeInfo {
131 pub fn code(&self) -> RoomKeyWithheldCode {
133 match self {
134 Self::Blacklisted(_) => RoomKeyWithheldCode::Blacklisted,
135 Self::Unverified(_) => RoomKeyWithheldCode::Unverified,
136 Self::Unauthorized(_) => RoomKeyWithheldCode::Unauthorized,
137 Self::Unavailable(_) => RoomKeyWithheldCode::Unavailable,
138 Self::NoOlm => RoomKeyWithheldCode::NoOlm,
139 Self::HistoryNotShared => RoomKeyWithheldCode::HistoryNotShared,
140 Self::_Custom(info) => info.code.as_str().into(),
141 }
142 }
143}
144
145impl<'de> Deserialize<'de> for RoomKeyWithheldCodeInfo {
146 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147 where
148 D: de::Deserializer<'de>,
149 {
150 #[derive(Debug, Deserialize)]
151 struct ExtractCode<'a> {
152 #[serde(borrow)]
153 code: Cow<'a, str>,
154 }
155
156 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
157 let ExtractCode { code } = from_raw_json_value(&json)?;
158
159 Ok(match code.as_ref() {
160 "m.blacklisted" => Self::Blacklisted(from_raw_json_value(&json)?),
161 "m.unverified" => Self::Unverified(from_raw_json_value(&json)?),
162 "m.unauthorised" => Self::Unauthorized(from_raw_json_value(&json)?),
163 "m.unavailable" => Self::Unavailable(from_raw_json_value(&json)?),
164 "m.no_olm" => Self::NoOlm,
165 _ => {
166 let mut data = from_raw_json_value::<JsonObject, _>(&json)?;
167
168 data.remove("algorithm");
172 data.remove("sender_key");
173 data.remove("reason");
174
175 let code = as_variant!(
176 data.remove("code").expect("we already checked that the code field is present"),
177 JsonValue::String
178 )
179 .expect("we already checked that the code is a string");
180
181 Self::_Custom(CustomRoomKeyWithheldCodeInfo { code, data }.into())
182 }
183 })
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
190pub struct RoomKeyWithheldSessionData {
191 pub room_id: OwnedRoomId,
193
194 pub session_id: String,
196}
197
198impl RoomKeyWithheldSessionData {
199 pub fn new(room_id: OwnedRoomId, session_id: String) -> Self {
201 Self { room_id, session_id }
202 }
203}
204
205#[doc(hidden)]
207#[derive(Clone, Debug, Serialize)]
208pub struct CustomRoomKeyWithheldCodeInfo {
209 code: String,
211
212 #[serde(flatten)]
214 data: JsonObject,
215}
216
217#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
219#[derive(Clone, StringEnum)]
220#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
221#[non_exhaustive]
222pub enum RoomKeyWithheldCode {
223 Blacklisted,
227
228 Unverified,
233
234 Unauthorized,
240
241 Unavailable,
246
247 NoOlm,
251
252 HistoryNotShared,
256
257 #[doc(hidden)]
258 _Custom(PrivOwnedStr),
259}
260
261#[cfg(test)]
262mod tests {
263 use assert_matches2::assert_matches;
264 use ruma_common::{
265 EventEncryptionAlgorithm, canonical_json::assert_to_canonical_json_eq, owned_room_id,
266 serde::Base64,
267 };
268 use serde_json::{from_value as from_json_value, json};
269
270 use super::{
271 RoomKeyWithheldCodeInfo, RoomKeyWithheldSessionData, ToDeviceRoomKeyWithheldEventContent,
272 };
273
274 const PUBLIC_KEY: &[u8] = b"key";
275 const BASE64_ENCODED_PUBLIC_KEY: &str = "a2V5";
276
277 #[test]
278 fn serialization_no_olm() {
279 let content = ToDeviceRoomKeyWithheldEventContent::new(
280 EventEncryptionAlgorithm::MegolmV1AesSha2,
281 RoomKeyWithheldCodeInfo::NoOlm,
282 Base64::new(PUBLIC_KEY.to_owned()),
283 );
284
285 assert_to_canonical_json_eq!(
286 content,
287 json!({
288 "algorithm": "m.megolm.v1.aes-sha2",
289 "code": "m.no_olm",
290 "sender_key": BASE64_ENCODED_PUBLIC_KEY,
291 })
292 );
293 }
294
295 #[test]
296 fn serialization_blacklisted() {
297 let room_id = owned_room_id!("!roomid:localhost");
298 let content = ToDeviceRoomKeyWithheldEventContent::new(
299 EventEncryptionAlgorithm::MegolmV1AesSha2,
300 RoomKeyWithheldCodeInfo::Blacklisted(
301 RoomKeyWithheldSessionData::new(room_id.clone(), "unique_id".to_owned()).into(),
302 ),
303 Base64::new(PUBLIC_KEY.to_owned()),
304 );
305
306 assert_to_canonical_json_eq!(
307 content,
308 json!({
309 "algorithm": "m.megolm.v1.aes-sha2",
310 "code": "m.blacklisted",
311 "sender_key": BASE64_ENCODED_PUBLIC_KEY,
312 "room_id": room_id,
313 "session_id": "unique_id",
314 })
315 );
316 }
317
318 #[test]
319 fn deserialization_no_olm() {
320 let json = json!({
321 "algorithm": "m.megolm.v1.aes-sha2",
322 "code": "m.no_olm",
323 "sender_key": BASE64_ENCODED_PUBLIC_KEY,
324 "reason": "Could not find an olm session",
325 });
326
327 let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json).unwrap();
328 assert_eq!(content.algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
329 assert_eq!(content.sender_key, Base64::new(PUBLIC_KEY.to_owned()));
330 assert_eq!(content.reason.as_deref(), Some("Could not find an olm session"));
331 assert_matches!(content.code, RoomKeyWithheldCodeInfo::NoOlm);
332 }
333
334 #[test]
335 fn deserialization_blacklisted() {
336 let room_id = owned_room_id!("!roomid:localhost");
337 let json = json!({
338 "algorithm": "m.megolm.v1.aes-sha2",
339 "code": "m.blacklisted",
340 "sender_key": BASE64_ENCODED_PUBLIC_KEY,
341 "room_id": room_id,
342 "session_id": "unique_id",
343 });
344
345 let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json).unwrap();
346 assert_eq!(content.algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
347 assert_eq!(content.sender_key, Base64::new(PUBLIC_KEY.to_owned()));
348 assert_eq!(content.reason, None);
349 assert_matches!(content.code, RoomKeyWithheldCodeInfo::Blacklisted(session_data));
350 assert_eq!(session_data.room_id, room_id);
351 assert_eq!(session_data.session_id, "unique_id");
352 }
353
354 #[test]
355 fn custom_room_key_withheld_code_info_round_trip() {
356 let room_id = owned_room_id!("!roomid:localhost");
357 let json = json!({
358 "algorithm": "m.megolm.v1.aes-sha2",
359 "code": "dev.ruma.custom_code",
360 "sender_key": BASE64_ENCODED_PUBLIC_KEY,
361 "room_id": room_id,
362 "key": "value",
363 });
364
365 let content = from_json_value::<ToDeviceRoomKeyWithheldEventContent>(json.clone()).unwrap();
366 assert_eq!(content.code.code().as_str(), "dev.ruma.custom_code");
367
368 assert_to_canonical_json_eq!(content, json);
369 }
370}