1use std::{borrow::Cow, collections::BTreeMap};
6
7use js_int::UInt;
8use ruma_common::{serde::JsonObject, OwnedDeviceId, OwnedEventId};
9use ruma_macros::EventContent;
10use serde::{Deserialize, Serialize};
11
12use super::message;
13use crate::relation::{Annotation, CustomRelation, InReplyTo, Reference, RelationType, Thread};
14
15mod relation_serde;
16
17#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
19#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
20#[ruma_event(type = "m.room.encrypted", kind = MessageLike)]
21pub struct RoomEncryptedEventContent {
22 #[serde(flatten)]
24 pub scheme: EncryptedEventScheme,
25
26 #[serde(rename = "m.relates_to", skip_serializing_if = "Option::is_none")]
28 pub relates_to: Option<Relation>,
29}
30
31impl RoomEncryptedEventContent {
32 pub fn new(scheme: EncryptedEventScheme, relates_to: Option<Relation>) -> Self {
34 Self { scheme, relates_to }
35 }
36}
37
38impl From<EncryptedEventScheme> for RoomEncryptedEventContent {
39 fn from(scheme: EncryptedEventScheme) -> Self {
40 Self { scheme, relates_to: None }
41 }
42}
43
44#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
46#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
47#[ruma_event(type = "m.room.encrypted", kind = ToDevice)]
48pub struct ToDeviceRoomEncryptedEventContent {
49 #[serde(flatten)]
51 pub scheme: EncryptedEventScheme,
52}
53
54impl ToDeviceRoomEncryptedEventContent {
55 pub fn new(scheme: EncryptedEventScheme) -> Self {
57 Self { scheme }
58 }
59}
60
61impl From<EncryptedEventScheme> for ToDeviceRoomEncryptedEventContent {
62 fn from(scheme: EncryptedEventScheme) -> Self {
63 Self { scheme }
64 }
65}
66
67#[derive(Clone, Debug, Deserialize, Serialize)]
69#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
70#[serde(tag = "algorithm")]
71pub enum EncryptedEventScheme {
72 #[serde(rename = "m.olm.v1.curve25519-aes-sha2")]
74 OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content),
75
76 #[serde(rename = "m.megolm.v1.aes-sha2")]
78 MegolmV1AesSha2(MegolmV1AesSha2Content),
79}
80
81#[derive(Clone, Debug)]
85#[allow(clippy::manual_non_exhaustive)]
86#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
87pub enum Relation {
88 Reply {
90 in_reply_to: InReplyTo,
92 },
93
94 Replacement(Replacement),
96
97 Reference(Reference),
99
100 Annotation(Annotation),
102
103 Thread(Thread),
105
106 #[doc(hidden)]
107 _Custom(CustomRelation),
108}
109
110impl Relation {
111 pub fn rel_type(&self) -> Option<RelationType> {
115 match self {
116 Relation::Reply { .. } => None,
117 Relation::Replacement(_) => Some(RelationType::Replacement),
118 Relation::Reference(_) => Some(RelationType::Reference),
119 Relation::Annotation(_) => Some(RelationType::Annotation),
120 Relation::Thread(_) => Some(RelationType::Thread),
121 Relation::_Custom(c) => c.rel_type(),
122 }
123 }
124
125 pub fn data(&self) -> Cow<'_, JsonObject> {
134 if let Relation::_Custom(CustomRelation(data)) = self {
135 Cow::Borrowed(data)
136 } else {
137 Cow::Owned(self.serialize_data())
138 }
139 }
140}
141
142impl<C> From<message::Relation<C>> for Relation {
143 fn from(rel: message::Relation<C>) -> Self {
144 match rel {
145 message::Relation::Reply { in_reply_to } => Self::Reply { in_reply_to },
146 message::Relation::Replacement(re) => {
147 Self::Replacement(Replacement { event_id: re.event_id })
148 }
149 message::Relation::Thread(t) => Self::Thread(Thread {
150 event_id: t.event_id,
151 in_reply_to: t.in_reply_to,
152 is_falling_back: t.is_falling_back,
153 }),
154 message::Relation::_Custom(c) => Self::_Custom(c),
155 }
156 }
157}
158
159#[derive(Clone, Debug, Deserialize, Serialize)]
167#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
168#[serde(tag = "rel_type", rename = "m.replace")]
169pub struct Replacement {
170 pub event_id: OwnedEventId,
172}
173
174impl Replacement {
175 pub fn new(event_id: OwnedEventId) -> Self {
177 Self { event_id }
178 }
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize)]
183#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
184pub struct OlmV1Curve25519AesSha2Content {
185 pub ciphertext: BTreeMap<String, CiphertextInfo>,
187
188 pub sender_key: String,
190}
191
192impl OlmV1Curve25519AesSha2Content {
193 pub fn new(ciphertext: BTreeMap<String, CiphertextInfo>, sender_key: String) -> Self {
195 Self { ciphertext, sender_key }
196 }
197}
198
199#[derive(Clone, Debug, Deserialize, Serialize)]
203#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
204pub struct CiphertextInfo {
205 pub body: String,
207
208 #[serde(rename = "type")]
210 pub message_type: UInt,
211}
212
213impl CiphertextInfo {
214 pub fn new(body: String, message_type: UInt) -> Self {
216 Self { body, message_type }
217 }
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize)]
225#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
226pub struct MegolmV1AesSha2Content {
227 pub ciphertext: String,
229
230 #[deprecated = "this field still needs to be sent but should not be used when received"]
232 pub sender_key: String,
233
234 #[deprecated = "this field still needs to be sent but should not be used when received"]
236 pub device_id: OwnedDeviceId,
237
238 pub session_id: String,
240}
241
242#[derive(Debug)]
247#[allow(clippy::exhaustive_structs)]
248pub struct MegolmV1AesSha2ContentInit {
249 pub ciphertext: String,
251
252 pub sender_key: String,
254
255 pub device_id: OwnedDeviceId,
257
258 pub session_id: String,
260}
261
262impl From<MegolmV1AesSha2ContentInit> for MegolmV1AesSha2Content {
263 fn from(init: MegolmV1AesSha2ContentInit) -> Self {
265 let MegolmV1AesSha2ContentInit { ciphertext, sender_key, device_id, session_id } = init;
266 #[allow(deprecated)]
267 Self { ciphertext, sender_key, device_id, session_id }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use assert_matches2::assert_matches;
274 use js_int::uint;
275 use ruma_common::{owned_event_id, serde::Raw};
276 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
277
278 use super::{
279 EncryptedEventScheme, InReplyTo, MegolmV1AesSha2ContentInit, Relation,
280 RoomEncryptedEventContent,
281 };
282
283 #[test]
284 fn serialization() {
285 let key_verification_start_content = RoomEncryptedEventContent {
286 scheme: EncryptedEventScheme::MegolmV1AesSha2(
287 MegolmV1AesSha2ContentInit {
288 ciphertext: "ciphertext".into(),
289 sender_key: "sender_key".into(),
290 device_id: "device_id".into(),
291 session_id: "session_id".into(),
292 }
293 .into(),
294 ),
295 relates_to: Some(Relation::Reply {
296 in_reply_to: InReplyTo { event_id: owned_event_id!("$h29iv0s8:example.com") },
297 }),
298 };
299
300 let json_data = json!({
301 "algorithm": "m.megolm.v1.aes-sha2",
302 "ciphertext": "ciphertext",
303 "sender_key": "sender_key",
304 "device_id": "device_id",
305 "session_id": "session_id",
306 "m.relates_to": {
307 "m.in_reply_to": {
308 "event_id": "$h29iv0s8:example.com"
309 }
310 },
311 });
312
313 assert_eq!(to_json_value(&key_verification_start_content).unwrap(), json_data);
314 }
315
316 #[test]
317 #[allow(deprecated)]
318 fn deserialization() {
319 let json_data = json!({
320 "algorithm": "m.megolm.v1.aes-sha2",
321 "ciphertext": "ciphertext",
322 "sender_key": "sender_key",
323 "device_id": "device_id",
324 "session_id": "session_id",
325 "m.relates_to": {
326 "m.in_reply_to": {
327 "event_id": "$h29iv0s8:example.com"
328 }
329 },
330 });
331
332 let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap();
333
334 assert_matches!(content.scheme, EncryptedEventScheme::MegolmV1AesSha2(scheme));
335 assert_eq!(scheme.ciphertext, "ciphertext");
336 assert_eq!(scheme.sender_key, "sender_key");
337 assert_eq!(scheme.device_id, "device_id");
338 assert_eq!(scheme.session_id, "session_id");
339
340 assert_matches!(content.relates_to, Some(Relation::Reply { in_reply_to }));
341 assert_eq!(in_reply_to.event_id, "$h29iv0s8:example.com");
342 }
343
344 #[test]
345 fn deserialization_olm() {
346 let json_data = json!({
347 "sender_key": "test_key",
348 "ciphertext": {
349 "test_curve_key": {
350 "body": "encrypted_body",
351 "type": 1
352 }
353 },
354 "algorithm": "m.olm.v1.curve25519-aes-sha2"
355 });
356 let content: RoomEncryptedEventContent = from_json_value(json_data).unwrap();
357
358 assert_matches!(content.scheme, EncryptedEventScheme::OlmV1Curve25519AesSha2(c));
359 assert_eq!(c.sender_key, "test_key");
360 assert_eq!(c.ciphertext.len(), 1);
361 assert_eq!(c.ciphertext["test_curve_key"].body, "encrypted_body");
362 assert_eq!(c.ciphertext["test_curve_key"].message_type, uint!(1));
363
364 assert_matches!(content.relates_to, None);
365 }
366
367 #[test]
368 fn deserialization_failure() {
369 from_json_value::<Raw<RoomEncryptedEventContent>>(
370 json!({ "algorithm": "m.megolm.v1.aes-sha2" }),
371 )
372 .unwrap()
373 .deserialize()
374 .unwrap_err();
375 }
376}