palpo_core/federation/transaction.rs
1/// Endpoints for exchanging transaction messages between homeservers.
2
3/// `PUT /_matrix/federation/*/send/{txn_id}`
4///
5/// Send live activity messages to another server.
6/// `/v1/` ([spec])
7///
8/// [spec]: https://spec.matrix.org/latest/server-server-api/#put_matrixfederationv1sendtxnid
9use std::collections::BTreeMap;
10
11use reqwest::Url;
12use salvo::prelude::*;
13use serde::{Deserialize, Serialize, de};
14
15use crate::device::{DeviceListUpdateContent, DirectDeviceContent};
16use crate::encryption::CrossSigningKey;
17use crate::events::receipt::{Receipt, ReceiptContent};
18use crate::events::typing::TypingContent;
19use crate::identifiers::*;
20use crate::presence::PresenceContent;
21use crate::sending::{SendRequest, SendResult};
22use crate::serde::{JsonValue, RawJsonValue, from_raw_json_value};
23use crate::{OwnedServerName, UnixMillis};
24
25// const METADATA: Metadata = metadata! {
26// method: PUT,
27// rate_limited: false,
28// authentication: ServerSignatures,
29// history: {
30// 1.0 => "/_matrix/federation/v1/send/:transaction_id",
31// }
32// };
33
34pub fn send_messages_request(origin: &str, txn_id: &str, body: SendMessageReqBody) -> SendResult<SendRequest> {
35 let url = Url::parse(&format!("{origin}/_matrix/federation/v1/send/{txn_id}"))?;
36 crate::sending::put(url).stuff(body)
37}
38
39/// Request type for the `send_transaction_message` endpoint.
40
41#[derive(ToSchema, Deserialize, Serialize, Debug)]
42pub struct SendMessageReqBody {
43 // /// A transaction ID unique between sending and receiving homeservers.
44 // #[salvo(parameter(parameter_in = Path))]
45 // pub transaction_id: OwnedTransactionId,
46 /// The server_name of the homeserver sending this transaction.
47 pub origin: OwnedServerName,
48
49 /// POSIX timestamp in milliseconds on the originating homeserver when this transaction
50 /// started.
51 pub origin_server_ts: UnixMillis,
52
53 /// List of persistent updates to rooms.
54 ///
55 /// Must not be more than 50 items.
56 ///
57 /// With the `unstable-unspecified` feature, sending `pdus` is optional.
58 /// See [matrix-spec#705](https://github.com/matrix-org/matrix-spec/issues/705).
59 #[cfg_attr(
60 feature = "unstable-unspecified",
61 serde(default, skip_serializing_if = "<[_]>::is_empty")
62 )]
63 #[salvo(schema(value_type = Vec<Object>))]
64 pub pdus: Vec<Box<RawJsonValue>>,
65
66 /// List of ephemeral messages.
67 ///
68 /// Must not be more than 100 items.
69 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
70 pub edus: Vec<Edu>,
71}
72crate::json_body_modifier!(SendMessageReqBody);
73
74/// Response type for the `send_transaction_message` endpoint.
75#[derive(ToSchema, Serialize, Deserialize, Debug)]
76
77pub struct SendMessageResBody {
78 /// Map of event IDs and response for each PDU given in the request.
79 ///
80 /// With the `unstable-msc3618` feature, returning `pdus` is optional.
81 /// See [MSC3618](https://github.com/matrix-org/matrix-spec-proposals/pull/3618).
82 #[serde(default, with = "crate::serde::pdu_process_response")]
83 pub pdus: BTreeMap<OwnedEventId, Result<(), String>>,
84}
85crate::json_body_modifier!(SendMessageResBody);
86
87impl SendMessageResBody {
88 /// Creates a new `Response` with the given PDUs.
89 pub fn new(pdus: BTreeMap<OwnedEventId, Result<(), String>>) -> Self {
90 Self { pdus }
91 }
92}
93
94/// Type for passing ephemeral data to homeservers.
95#[derive(ToSchema, Clone, Debug, Serialize)]
96#[serde(tag = "edu_type", content = "content")]
97pub enum Edu {
98 /// An EDU representing presence updates for users of the sending homeserver.
99 #[serde(rename = "m.presence")]
100 Presence(PresenceContent),
101
102 /// An EDU representing receipt updates for users of the sending homeserver.
103 #[serde(rename = "m.receipt")]
104 Receipt(ReceiptContent),
105
106 /// A typing notification EDU for a user in a room.
107 #[serde(rename = "m.typing")]
108 Typing(TypingContent),
109
110 /// An EDU that lets servers push details to each other when one of their users adds
111 /// a new device to their account, required for E2E encryption to correctly target the
112 /// current set of devices for a given user.
113 #[serde(rename = "m.device_list_update")]
114 DeviceListUpdate(DeviceListUpdateContent),
115
116 /// An EDU that lets servers push send events directly to a specific device on a
117 /// remote server - for instance, to maintain an Olm E2E encrypted message channel
118 /// between a local and remote device.
119 #[serde(rename = "m.direct_to_device")]
120 DirectToDevice(DirectDeviceContent),
121
122 /// An EDU that lets servers push details to each other when one of their users updates their
123 /// cross-signing keys.
124 #[serde(rename = "m.signing_key_update")]
125 #[salvo(schema(value_type = Object))]
126 SigningKeyUpdate(SigningKeyUpdateContent),
127
128 #[doc(hidden)]
129 #[salvo(schema(value_type = Object))]
130 _Custom(JsonValue),
131}
132
133#[derive(Debug, Deserialize)]
134struct EduDeHelper {
135 /// The message type field
136 edu_type: String,
137 content: Box<RawJsonValue>,
138}
139
140impl<'de> Deserialize<'de> for Edu {
141 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142 where
143 D: de::Deserializer<'de>,
144 {
145 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
146 let EduDeHelper { edu_type, content } = from_raw_json_value(&json)?;
147
148 Ok(match edu_type.as_ref() {
149 "m.presence" => Self::Presence(from_raw_json_value(&content)?),
150 "m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
151 "m.typing" => Self::Typing(from_raw_json_value(&content)?),
152 "m.device_list_update" => Self::DeviceListUpdate(from_raw_json_value(&content)?),
153 "m.direct_to_device" => Self::DirectToDevice(from_raw_json_value(&content)?),
154 "m.signing_key_update" => Self::SigningKeyUpdate(from_raw_json_value(&content)?),
155 _ => Self::_Custom(from_raw_json_value(&content)?),
156 })
157 }
158}
159
160/// Mapping between user and `ReceiptData`.
161#[derive(ToSchema, Deserialize, Serialize, Clone, Debug)]
162pub struct ReceiptMap {
163 /// Read receipts for users in the room.
164 #[serde(rename = "m.read")]
165 pub read: BTreeMap<OwnedUserId, ReceiptData>,
166}
167
168impl ReceiptMap {
169 /// Creates a new `ReceiptMap`.
170 pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
171 Self { read }
172 }
173}
174
175/// Metadata about the event that was last read and when.
176#[derive(Clone, Debug, Deserialize, Serialize)]
177pub struct ReceiptData {
178 /// Metadata for the read receipt.
179 pub data: Receipt,
180
181 /// The extremity event ID the user has read up to.
182 pub event_ids: Vec<OwnedEventId>,
183}
184
185impl ReceiptData {
186 /// Creates a new `ReceiptData`.
187 pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
188 Self { data, event_ids }
189 }
190}
191
192/// The content for an `m.signing_key_update` EDU.
193#[derive(ToSchema, Deserialize, Serialize, Clone, Debug)]
194pub struct SigningKeyUpdateContent {
195 /// The user ID whose cross-signing keys have changed.
196 pub user_id: OwnedUserId,
197
198 /// The user's master key, if it was updated.
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub master_key: Option<CrossSigningKey>,
201
202 /// The users's self-signing key, if it was updated.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub self_signing_key: Option<CrossSigningKey>,
205}
206
207impl SigningKeyUpdateContent {
208 /// Creates a new `SigningKeyUpdateContent`.
209 pub fn new(user_id: OwnedUserId) -> Self {
210 Self {
211 user_id,
212 master_key: None,
213 self_signing_key: None,
214 }
215 }
216}
217
218// #[cfg(test)]
219// mod tests {
220// use crate::events::ToDeviceEventType;
221// use crate::{room_id, user_id};
222// use assert_matches2::assert_matches;
223// use serde_json::json;
224
225// use super::{DeviceListUpdateContent, Edu, ReceiptContent};
226
227// #[test]
228// fn device_list_update_edu() {
229// let json = json!({
230// "content": {
231// "deleted": false,
232// "device_display_name": "Mobile",
233// "device_id": "QBUAZIFURK",
234// "keys": {
235// "algorithms": [
236// "m.olm.v1.curve25519-aes-sha2",
237// "m.megolm.v1.aes-sha2"
238// ],
239// "device_id": "JLAFKJWSCS",
240// "keys": {
241// "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
242// "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
243// },
244// "signatures": {
245// "@alice:example.com": {
246// "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
247// }
248// },
249// "user_id": "@alice:example.com"
250// },
251// "stream_id": 6,
252// "user_id": "@john:example.com"
253// },
254// "edu_type": "m.device_list_update"
255// });
256
257// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
258// assert_matches!(
259// &edu,
260// Edu::DeviceListUpdate(DeviceListUpdateContent {
261// user_id,
262// device_id,
263// device_display_name,
264// stream_id,
265// prev_id,
266// deleted,
267// keys,
268// })
269// );
270
271// assert_eq!(user_id, "@john:example.com");
272// assert_eq!(device_id, "QBUAZIFURK");
273// assert_eq!(device_display_name.as_deref(), Some("Mobile"));
274// assert_eq!(*stream_id, u6);
275// assert_eq!(*prev_id, vec![]);
276// assert_eq!(*deleted, Some(false));
277// assert_matches!(keys, Some(_));
278
279// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
280// }
281
282// #[test]
283// fn minimal_device_list_update_edu() {
284// let json = json!({
285// "content": {
286// "device_id": "QBUAZIFURK",
287// "stream_id": 6,
288// "user_id": "@john:example.com"
289// },
290// "edu_type": "m.device_list_update"
291// });
292
293// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
294// assert_matches!(
295// &edu,
296// Edu::DeviceListUpdate(DeviceListUpdateContent {
297// user_id,
298// device_id,
299// device_display_name,
300// stream_id,
301// prev_id,
302// deleted,
303// keys,
304// })
305// );
306
307// assert_eq!(user_id, "@john:example.com");
308// assert_eq!(device_id, "QBUAZIFURK");
309// assert_eq!(*device_display_name, None);
310// assert_eq!(*stream_id, u6);
311// assert_eq!(*prev_id, vec![]);
312// assert_eq!(*deleted, None);
313// assert_matches!(keys, None);
314
315// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
316// }
317
318// #[test]
319// fn receipt_edu() {
320// let json = json!({
321// "content": {
322// "!some_room:example.org": {
323// "m.read": {
324// "@john:matrix.org": {
325// "data": {
326// "ts": 1_533_358
327// },
328// "event_ids": [
329// "$read_this_event:matrix.org"
330// ]
331// }
332// }
333// }
334// },
335// "edu_type": "m.receipt"
336// });
337
338// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
339// assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
340// assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
341
342// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
343// }
344
345// #[test]
346// fn typing_edu() {
347// let json = json!({
348// "content": {
349// "room_id": "!somewhere:matrix.org",
350// "typing": true,
351// "user_id": "@john:matrix.org"
352// },
353// "edu_type": "m.typing"
354// });
355
356// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
357// assert_matches!(&edu, Edu::Typing(content));
358// assert_eq!(content.room_id, "!somewhere:matrix.org");
359// assert_eq!(content.user_id, "@john:matrix.org");
360// assert!(content.typing);
361
362// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
363// }
364
365// #[test]
366// fn direct_to_device_edu() {
367// let json = json!({
368// "content": {
369// "message_id": "hiezohf6Hoo7kaev",
370// "messages": {
371// "@alice:example.org": {
372// "IWHQUZUIAH": {
373// "algorithm": "m.megolm.v1.aes-sha2",
374// "room_id": "!Cuyf34gef24t:localhost",
375// "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
376// "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
377// }
378// }
379// },
380// "sender": "@john:example.com",
381// "type": "m.room_key_request"
382// },
383// "edu_type": "m.direct_to_device"
384// });
385
386// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
387// assert_matches!(&edu, Edu::DirectToDevice(content));
388// assert_eq!(content.sender, "@john:example.com");
389// assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
390// assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
391// assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
392
393// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
394// }
395
396// #[test]
397// fn signing_key_update_edu() {
398// let json = json!({
399// "content": {
400// "master_key": {
401// "keys": {
402// "ed25519:alice+base64+public+key": "alice+base64+public+key",
403// "ed25519:base64+master+public+key": "base64+master+public+key"
404// },
405// "signatures": {
406// "@alice:example.com": {
407// "ed25519:alice+base64+master+key": "signature+of+key"
408// }
409// },
410// "usage": [
411// "master"
412// ],
413// "user_id": "@alice:example.com"
414// },
415// "self_signing_key": {
416// "keys": {
417// "ed25519:alice+base64+public+key": "alice+base64+public+key",
418// "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
419// },
420// "signatures": {
421// "@alice:example.com": {
422// "ed25519:alice+base64+master+key": "signature+of+key",
423// "ed25519:base64+master+public+key": "signature+of+self+signing+key"
424// }
425// },
426// "usage": [
427// "self_signing"
428// ],
429// "user_id": "@alice:example.com"
430// },
431// "user_id": "@alice:example.com"
432// },
433// "edu_type": "m.signing_key_update"
434// });
435
436// let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
437// assert_matches!(&edu, Edu::SigningKeyUpdate(content));
438// assert_eq!(content.user_id, "@alice:example.com");
439// assert!(content.master_key.is_some());
440// assert!(content.self_signing_key.is_some());
441
442// assert_eq!(serde_json::to_value(&edu).unwrap(), json);
443// }
444// }