Skip to main content

tuwunel_api_federation/
send.rs

1use std::{
2	collections::BTreeMap,
3	iter::once,
4	net::IpAddr,
5	sync::atomic::{AtomicBool, Ordering},
6	time::{Duration, Instant},
7};
8
9use axum::extract::State;
10use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
11use ruma::{
12	CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
13	OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, TransactionId, UserId,
14	api::{
15		error::ErrorKind,
16		federation::transactions::{
17			edu::{
18				DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
19				PresenceUpdate, ReceiptContent, ReceiptData, ReceiptMap, SigningKeyUpdateContent,
20				TypingContent,
21			},
22			send_transaction_message,
23		},
24	},
25	events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
26	int,
27	serde::Raw,
28	to_device::DeviceIdOrAllDevices,
29	uint,
30};
31use tuwunel_core::{
32	Err, Error, Result, debug,
33	debug::INFO_SPAN_LEVEL,
34	debug_warn, defer, err, error,
35	itertools::Itertools,
36	result::LogErr,
37	smallvec::SmallVec,
38	trace,
39	utils::{
40		debug::str_truncated,
41		future::TryExtExt,
42		millis_since_unix_epoch,
43		stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, automatic_width},
44	},
45	warn,
46};
47use tuwunel_matrix::room::state::resolution::{
48	is_topologically_sorted_in_place, topological_sort,
49};
50use tuwunel_service::{
51	Services,
52	sending::{EDU_LIMIT, PDU_LIMIT},
53	users::DeviceListChange,
54};
55
56use crate::{ClientIp, Ruma};
57
58type ResolvedMap = BTreeMap<OwnedEventId, Result>;
59type RoomsPdus = SmallVec<[RoomPdus; 1]>;
60type RoomPdus = (OwnedRoomId, TxnPdus);
61type TxnPdus = SmallVec<[(usize, Pdu); 1]>;
62type Pdu = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
63
64/// Recipient devices of one `AllDevices` to-device send paired with their
65/// inbox counts.
66type Deliveries = SmallVec<[(OwnedDeviceId, u64); 1]>;
67
68/// # `PUT /_matrix/federation/v1/send/{txnId}`
69///
70/// Push EDUs and PDUs to this server.
71#[tracing::instrument(
72	name = "txn",
73	level = INFO_SPAN_LEVEL,
74	skip_all,
75	fields(
76		txn = str_truncated(body.transaction_id.as_str(), 20),
77		origin = body.origin().as_str(),
78		%client,
79	),
80)]
81pub(crate) async fn send_transaction_message_route(
82	State(services): State<crate::State>,
83	ClientIp(client): ClientIp,
84	body: Ruma<send_transaction_message::v1::Request>,
85) -> Result<send_transaction_message::v1::Response> {
86	if body.origin() != body.body.origin {
87		return Err!(Request(Forbidden(
88			"Not allowed to send transactions on behalf of other servers"
89		)));
90	}
91
92	if body.pdus.len() > PDU_LIMIT {
93		return Err!(Request(Forbidden(
94			"Not allowed to send more than {PDU_LIMIT} PDUs in one transaction"
95		)));
96	}
97
98	if body.edus.len() > EDU_LIMIT {
99		return Err!(Request(Forbidden(
100			"Not allowed to send more than {EDU_LIMIT} EDUs in one transaction"
101		)));
102	}
103
104	// Clear any failure bucket before processing consults the peer gate.
105	services
106		.sending
107		.notify_peer_alive(body.origin())
108		.await;
109
110	let txn_start_time = Instant::now();
111	trace!(
112		pdus = body.pdus.len(),
113		edus = body.edus.len(),
114		elapsed = ?txn_start_time.elapsed(),
115		"Starting txn",
116	);
117
118	let pdus = body
119		.pdus
120		.iter()
121		.stream()
122		.enumerate()
123		.broad_filter_map(|(i, pdu)| {
124			services
125				.event_handler
126				.parse_incoming_pdu(pdu)
127				.inspect_err(move |e| debug_warn!("Could not parse PDU[{i}]: {e}"))
128				.map_ok(move |pdu| (i, pdu))
129				.ok()
130		});
131
132	let edus = body
133		.edus
134		.iter()
135		.stream()
136		.enumerate()
137		.ready_filter_map(|(i, edu)| {
138			serde_json::from_str(edu.json().get())
139				.inspect_err(|e| debug_warn!("Could not parse EDU[{i}]: {e}"))
140				.map(|edu| (i, edu))
141				.ok()
142		});
143
144	let results = handle(
145		&services,
146		&client,
147		body.origin(),
148		&body.transaction_id,
149		txn_start_time,
150		pdus,
151		edus,
152	)
153	.await?;
154
155	debug!(
156		pdus = body.pdus.len(),
157		edus = body.edus.len(),
158		elapsed = ?txn_start_time.elapsed(),
159		"Finished txn",
160	);
161
162	for (id, result) in &results {
163		if let Err(e) = result
164			&& matches!(
165				e,
166				Error::BadRequest(ErrorKind::NotFound, _)
167					| Error::Request(ErrorKind::NotFound, ..)
168			) {
169			warn!("Incoming PDU failed {id}: {e:?}");
170		}
171	}
172
173	Ok(send_transaction_message::v1::Response {
174		pdus: results
175			.into_iter()
176			.map(|(e, r)| (e, r.map_err(error::sanitized_message)))
177			.collect(),
178	})
179}
180
181async fn handle(
182	services: &Services,
183	client: &IpAddr,
184	origin: &ServerName,
185	txn_id: &TransactionId,
186	started: Instant,
187	pdus: impl Stream<Item = (usize, Pdu)> + Send,
188	edus: impl Stream<Item = (usize, Edu)> + Send,
189) -> Result<ResolvedMap> {
190	let results = handle_pdus(services, client, origin, txn_id, started, pdus).await?;
191
192	handle_edus(services, client, origin, txn_id, edus).await?;
193
194	Ok(results)
195}
196
197async fn handle_pdus(
198	services: &Services,
199	client: &IpAddr,
200	origin: &ServerName,
201	txn_id: &TransactionId,
202	started: Instant,
203	pdus: impl Stream<Item = (usize, Pdu)> + Send,
204) -> Result<ResolvedMap> {
205	pdus.collect()
206		.map(Ok)
207		.map_ok(|pdus: TxnPdus| {
208			pdus.into_iter()
209				.sorted_by(|(_, (room_a, ..)), (_, (room_b, ..))| room_a.cmp(room_b))
210				.into_grouping_map_by(|(_, (room_id, ..))| room_id.clone())
211				.collect()
212				.into_iter()
213				.try_stream()
214		})
215		.try_flatten_stream()
216		.try_collect::<RoomsPdus>()
217		.map_ok(IntoIterator::into_iter)
218		.map_ok(IterStream::try_stream)
219		.try_flatten_stream()
220		.broad_and_then(async |(room_id, pdus)| {
221			handle_room(services, client, origin, txn_id, started, room_id, pdus)
222				.map_ok(ResolvedMap::into_iter)
223				.map_ok(IterStream::try_stream)
224				.await
225		})
226		.try_flatten()
227		.try_collect()
228		.await
229}
230
231#[tracing::instrument(
232	name = "room",
233	level = INFO_SPAN_LEVEL,
234	skip_all,
235	fields(%room_id)
236)]
237async fn handle_room(
238	services: &Services,
239	_client: &IpAddr,
240	origin: &ServerName,
241	txn_id: &TransactionId,
242	txn_start_time: Instant,
243	ref room_id: OwnedRoomId,
244	pdus: TxnPdus,
245) -> Result<ResolvedMap> {
246	let pdus = sort_pdus(pdus).await;
247
248	services
249		.event_handler
250		.mutex_federation
251		.lock(room_id)
252		.then(async |_lock| {
253			pdus.into_iter()
254				.enumerate()
255				.try_stream()
256				.and_then(async |pdu| {
257					services.server.check_running().map(|()| pdu) // interruption point
258				})
259				.and_then(|(ri, (ti, (room_id, event_id, value)))| {
260					let meta = (origin, txn_id, txn_start_time, ti);
261					let pdu = (ri, (room_id, event_id, value));
262					handle_pdu(services, meta, pdu).map(Ok)
263				})
264				.try_collect()
265				.await
266		})
267		.await
268}
269
270/// Reorder a room's transaction PDUs so each event follows the in-batch events
271/// it references. An already-ordered batch is returned unchanged; references to
272/// events outside the batch are non-edges. The sort is an optimization, so a
273/// failure falls back to the arrival order.
274async fn sort_pdus(mut pdus: TxnPdus) -> TxnPdus {
275	if already_sorted(&pdus) {
276		return pdus;
277	}
278
279	let event_ids: BTreeMap<&str, &OwnedEventId> = pdus
280		.iter()
281		.map(|(_, (_, event_id, _))| (event_id.as_str(), event_id))
282		.collect();
283
284	let graph = pdus
285		.iter()
286		.map(|(_, (_, event_id, value))| {
287			let references = prev_event_ids(value)
288				.filter_map(|prev| event_ids.get(prev).copied())
289				.map(ToOwned::to_owned)
290				.collect();
291
292			(event_id.clone(), references)
293		})
294		.collect();
295
296	// Causal order alone matters here, so the tie-break inputs are constant.
297	let query = async |_event_id: OwnedEventId| {
298		Ok((int!(0).into(), MilliSecondsSinceUnixEpoch(uint!(0))))
299	};
300
301	let Ok(order) = topological_sort(graph, &query).await else {
302		return pdus;
303	};
304
305	let position: BTreeMap<&str, usize> = order
306		.iter()
307		.enumerate()
308		.map(|(i, event_id)| (event_id.as_str(), i))
309		.collect();
310
311	pdus.sort_by_key(|(_, (_, event_id, _))| position.get(event_id.as_str()).copied());
312	pdus
313}
314
315/// Whether the batch is already in causal order, in which case the sort can be
316/// skipped.
317fn already_sorted(pdus: &[(usize, Pdu)]) -> bool {
318	is_topologically_sorted_in_place(
319		pdus,
320		|(_, (_, id, _))| id.as_str(),
321		|(_, (_, _, value))| prev_event_ids(value),
322	)
323}
324
325/// The `prev_events` of a PDU held as canonical JSON.
326fn prev_event_ids(value: &CanonicalJsonObject) -> impl Iterator<Item = &str> + '_ {
327	value
328		.get("prev_events")
329		.and_then(CanonicalJsonValue::as_array)
330		.into_iter()
331		.flatten()
332		.filter_map(CanonicalJsonValue::as_str)
333}
334
335#[tracing::instrument(
336	name = "pdu",
337	level = INFO_SPAN_LEVEL,
338	skip_all,
339	fields(%event_id, %ti, %ri)
340)]
341async fn handle_pdu(
342	services: &Services,
343	(origin, txn_id, txn_start_time, ti): (&ServerName, &TransactionId, Instant, usize),
344	(ri, (ref room_id, event_id, value)): (usize, Pdu),
345) -> (OwnedEventId, Result) {
346	let pdu_start_time = Instant::now();
347	let completed: AtomicBool = Default::default();
348	defer! {{
349		if completed.load(Ordering::Acquire) {
350			return;
351		}
352
353		if pdu_start_time.elapsed() >= Duration::from_secs(services.config.http.request_timeout) {
354			error!(
355				%origin, %txn_id, %room_id, %event_id, %ri, %ti,
356				elapsed = ?pdu_start_time.elapsed(),
357				"Incoming transaction processing timed out.",
358			);
359		} else {
360			debug_warn!(
361				%origin, %txn_id, %room_id, %event_id, %ri, %ti,
362				elapsed = ?pdu_start_time.elapsed(),
363				"Incoming transaction processing interrupted.",
364			);
365		}
366	}}
367
368	let result = services
369		.event_handler
370		.handle_pushed_pdu(origin, room_id, &event_id, value)
371		.map_ok(|_| ())
372		.await;
373
374	completed.store(true, Ordering::Release);
375	debug!(
376		%event_id, ri, ti,
377		pdu_elapsed = ?pdu_start_time.elapsed(),
378		txn_elapsed = ?txn_start_time.elapsed(),
379		"Finished PDU",
380	);
381
382	(event_id.clone(), result)
383}
384
385#[tracing::instrument(name = "edus", level = "debug", skip_all)]
386async fn handle_edus(
387	services: &Services,
388	client: &IpAddr,
389	origin: &ServerName,
390	txn_id: &TransactionId,
391	edus: impl Stream<Item = (usize, Edu)> + Send,
392) -> Result {
393	edus.for_each_concurrent(automatic_width(), |(i, edu)| {
394		handle_edu(services, client, origin, txn_id, i, edu)
395	})
396	.await;
397
398	Ok(())
399}
400
401#[tracing::instrument(
402	name = "edu",
403	level = "debug",
404	skip_all,
405	fields(%i),
406)]
407async fn handle_edu(
408	services: &Services,
409	client: &IpAddr,
410	origin: &ServerName,
411	_txn_id: &TransactionId,
412	i: usize,
413	edu: Edu,
414) {
415	match edu {
416		| Edu::Presence(presence) if services.server.config.presence.allow_incoming =>
417			handle_edu_presence(services, client, origin, presence).await,
418
419		| Edu::Receipt(receipt)
420			if services
421				.server
422				.config
423				.read_receipts
424				.allow_incoming =>
425			handle_edu_receipt(services, client, origin, receipt).await,
426
427		| Edu::Typing(typing) if services.server.config.typing.allow_incoming =>
428			handle_edu_typing(services, client, origin, typing).await,
429
430		| Edu::DeviceListUpdate(content) =>
431			handle_edu_device_list_update(services, client, origin, content).await,
432
433		| Edu::DirectToDevice(content) =>
434			handle_edu_direct_to_device(services, client, origin, content).await,
435
436		| Edu::SigningKeyUpdate(content) =>
437			handle_edu_signing_key_update(services, client, origin, content).await,
438
439		| Edu::_Custom(ref _custom) => debug_warn!(?i, ?edu, "received custom/unknown EDU"),
440
441		| _ => trace!(?i, ?edu, "skipped"),
442	}
443}
444
445async fn handle_edu_presence(
446	services: &Services,
447	_client: &IpAddr,
448	origin: &ServerName,
449	presence: PresenceContent,
450) {
451	presence
452		.push
453		.into_iter()
454		.stream()
455		.for_each_concurrent(automatic_width(), |update| {
456			handle_edu_presence_update(services, origin, update)
457		})
458		.await;
459}
460
461async fn handle_edu_presence_update(
462	services: &Services,
463	origin: &ServerName,
464	update: PresenceUpdate,
465) {
466	if update.user_id.server_name() != origin {
467		debug_warn!(
468			%update.user_id, %origin,
469			"received presence EDU for user not belonging to origin"
470		);
471		return;
472	}
473
474	services
475		.presence
476		.set_presence_from_federation(
477			&update.user_id,
478			&update.presence,
479			update.currently_active,
480			update.last_active_ago,
481			update.status_msg.clone(),
482		)
483		.await
484		.log_err()
485		.ok();
486}
487
488async fn handle_edu_receipt(
489	services: &Services,
490	_client: &IpAddr,
491	origin: &ServerName,
492	receipt: ReceiptContent,
493) {
494	receipt
495		.receipts
496		.into_iter()
497		.stream()
498		.for_each_concurrent(automatic_width(), |(room_id, room_updates)| {
499			handle_edu_receipt_room(services, origin, room_id, room_updates)
500		})
501		.await;
502}
503
504async fn handle_edu_receipt_room(
505	services: &Services,
506	origin: &ServerName,
507	room_id: OwnedRoomId,
508	room_updates: ReceiptMap,
509) {
510	if services
511		.event_handler
512		.acl_check(origin, &room_id)
513		.await
514		.is_err()
515	{
516		debug_warn!(
517			%origin, %room_id,
518			"received read receipt EDU from ACL'd server"
519		);
520		return;
521	}
522
523	let room_id = &room_id;
524	room_updates
525		.read
526		.into_iter()
527		.stream()
528		.for_each_concurrent(automatic_width(), async |(user_id, user_updates)| {
529			handle_edu_receipt_room_user(services, origin, room_id, &user_id, user_updates).await;
530		})
531		.await;
532}
533
534async fn handle_edu_receipt_room_user(
535	services: &Services,
536	origin: &ServerName,
537	room_id: &RoomId,
538	user_id: &UserId,
539	user_updates: ReceiptData,
540) {
541	if user_id.server_name() != origin {
542		debug_warn!(
543			%user_id, %origin,
544			"received read receipt EDU for user not belonging to origin"
545		);
546		return;
547	}
548
549	if !services
550		.state_cache
551		.server_in_room(origin, room_id)
552		.await
553	{
554		debug_warn!(
555			%user_id, %room_id, %origin,
556			"received read receipt EDU from server who does not have a member in the room",
557		);
558		return;
559	}
560
561	let data = &user_updates.data;
562	user_updates
563		.event_ids
564		.into_iter()
565		.stream()
566		.for_each_concurrent(automatic_width(), async |event_id| {
567			let user_data = [(user_id.to_owned(), data.clone())];
568			let receipts = [(ReceiptType::Read, BTreeMap::from(user_data))];
569			let content = [(event_id.clone(), BTreeMap::from(receipts))];
570			services
571				.read_receipt
572				.readreceipt_update(user_id, room_id, &ReceiptEvent {
573					content: ReceiptEventContent(content.into()),
574					room_id: room_id.to_owned(),
575				})
576				.await;
577		})
578		.await;
579}
580
581async fn handle_edu_typing(
582	services: &Services,
583	_client: &IpAddr,
584	origin: &ServerName,
585	typing: TypingContent,
586) {
587	if typing.user_id.server_name() != origin {
588		debug_warn!(
589			%typing.user_id, %origin,
590			"received typing EDU for user not belonging to origin"
591		);
592		return;
593	}
594
595	if services
596		.event_handler
597		.acl_check(typing.user_id.server_name(), &typing.room_id)
598		.await
599		.is_err()
600	{
601		debug_warn!(
602			%typing.user_id, %typing.room_id, %origin,
603			"received typing EDU for ACL'd user's server"
604		);
605		return;
606	}
607
608	if !services
609		.state_cache
610		.is_joined(&typing.user_id, &typing.room_id)
611		.await
612	{
613		debug_warn!(
614			%typing.user_id, %typing.room_id, %origin,
615			"received typing EDU for user not in room"
616		);
617		return;
618	}
619
620	if typing.typing {
621		let secs = services.server.config.typing.federation_timeout_s;
622		let timeout = millis_since_unix_epoch().saturating_add(secs.saturating_mul(1000));
623
624		services
625			.typing
626			.typing_add(&typing.user_id, &typing.room_id, timeout)
627			.await
628			.log_err()
629			.ok();
630	} else {
631		services
632			.typing
633			.typing_remove(&typing.user_id, &typing.room_id)
634			.await
635			.log_err()
636			.ok();
637	}
638}
639
640async fn handle_edu_device_list_update(
641	services: &Services,
642	_client: &IpAddr,
643	origin: &ServerName,
644	content: DeviceListUpdateContent,
645) {
646	let DeviceListUpdateContent { user_id, .. } = content;
647
648	if user_id.server_name() != origin {
649		debug_warn!(
650			%user_id, %origin,
651			"received device list update EDU for user not belonging to origin"
652		);
653		return;
654	}
655
656	services
657		.users
658		.mark_device_key_update(&user_id, DeviceListChange::Resync)
659		.await;
660}
661
662async fn handle_edu_direct_to_device(
663	services: &Services,
664	_client: &IpAddr,
665	origin: &ServerName,
666	content: DirectDeviceContent,
667) {
668	let DirectDeviceContent {
669		ref sender,
670		ref ev_type,
671		ref message_id,
672		messages,
673	} = content;
674
675	if sender.server_name() != origin {
676		debug_warn!(
677			%sender, %origin,
678			"received direct to device EDU for user not belonging to origin"
679		);
680		return;
681	}
682
683	// Check if this is a new transaction id
684	if services
685		.transaction_ids
686		.existing_txnid(sender, None, message_id)
687		.await
688		.is_ok()
689	{
690		return;
691	}
692
693	let ev_type = ev_type.to_string();
694
695	messages
696		.into_iter()
697		.stream()
698		.broad_filter_map(async |(target_user_id, map)| {
699			to_device_deliverable(services, &target_user_id)
700				.await
701				.then_some((target_user_id, map))
702		})
703		.for_each_concurrent(automatic_width(), |(target_user_id, map)| {
704			handle_edu_direct_to_device_user(services, target_user_id, sender, &ev_type, map)
705		})
706		.await;
707
708	// Save transaction id with empty data
709	services
710		.transaction_ids
711		.add_txnid(sender, None, message_id, &[]);
712}
713
714/// A local account we store or forward to-device events for.
715///
716/// Qualifying accounts are active, the server user once its account exists, or
717/// claimed by an appservice namespace so its puppet events reach the bridge.
718async fn to_device_deliverable(services: &Services, user_id: &UserId) -> bool {
719	services.globals.user_is_local(user_id)
720		&& ((user_id == services.globals.server_user && services.users.exists(user_id).await)
721			|| services.users.is_active(user_id).await
722			|| services
723				.appservice
724				.is_interested_in_user(user_id)
725				.await)
726}
727
728async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
729	services: &Services,
730	target_user_id: OwnedUserId,
731	sender: &UserId,
732	ev_type: &str,
733	map: BTreeMap<DeviceIdOrAllDevices, Raw<Event>>,
734) {
735	map.into_iter()
736		.stream()
737		.ready_filter_map(|(tid, raw)| {
738			raw.deserialize_as()
739				.map_err(|e| {
740					err!(Request(InvalidParam(error!("To-Device event is invalid: {e}"))))
741				})
742				.ok()
743				.map(|ev| (tid, ev))
744		})
745		.for_each_concurrent(automatic_width(), |(tid, ev)| {
746			handle_edu_direct_to_device_event(services, &target_user_id, sender, tid, ev_type, ev)
747		})
748		.await;
749}
750
751async fn handle_edu_direct_to_device_event(
752	services: &Services,
753	target_user_id: &UserId,
754	sender: &UserId,
755	target_device_id_maybe: DeviceIdOrAllDevices,
756	ev_type: &str,
757	event: serde_json::Value,
758) {
759	match target_device_id_maybe {
760		| DeviceIdOrAllDevices::DeviceId(ref target_device_id) => {
761			let count = services.users.add_to_device_event(
762				sender,
763				target_user_id,
764				target_device_id,
765				ev_type,
766				&event,
767			);
768
769			services
770				.sending
771				.send_to_device_appservices(
772					sender,
773					target_user_id,
774					once((&**target_device_id, count)),
775					ev_type,
776					&event,
777				)
778				.await
779				.log_err()
780				.ok();
781		},
782
783		| DeviceIdOrAllDevices::AllDevices => {
784			let interested = services
785				.appservice
786				.is_interested_in_user(target_user_id)
787				.await;
788
789			let deliveries: Deliveries = services
790				.users
791				.all_device_ids(target_user_id)
792				.map(|target_device_id| {
793					let count = services.users.add_to_device_event(
794						sender,
795						target_user_id,
796						target_device_id,
797						ev_type,
798						&event,
799					);
800
801					(target_device_id, count)
802				})
803				.ready_filter_map(|(target_device_id, count)| {
804					interested.then(|| (target_device_id.to_owned(), count))
805				})
806				.collect()
807				.await;
808
809			if !deliveries.is_empty() {
810				services
811					.sending
812					.send_to_device_appservices(
813						sender,
814						target_user_id,
815						deliveries
816							.iter()
817							.map(|(device_id, count)| (&**device_id, *count)),
818						ev_type,
819						&event,
820					)
821					.await
822					.log_err()
823					.ok();
824			}
825		},
826	}
827}
828
829async fn handle_edu_signing_key_update(
830	services: &Services,
831	_client: &IpAddr,
832	origin: &ServerName,
833	content: SigningKeyUpdateContent,
834) {
835	let SigningKeyUpdateContent { user_id, master_key, self_signing_key } = content;
836
837	if user_id.server_name() != origin {
838		debug_warn!(
839			%user_id, %origin,
840			"received signing key update EDU from server that does not belong to user's server"
841		);
842		return;
843	}
844
845	services
846		.users
847		.add_cross_signing_keys(&user_id, &master_key, &self_signing_key, &None, true)
848		.await
849		.log_err()
850		.ok();
851}
852
853#[cfg(test)]
854mod tests {
855	use ruma::{CanonicalJsonObject, OwnedEventId, event_id, room_id};
856	use serde_json::json;
857
858	use super::{Pdu, TxnPdus, already_sorted, prev_event_ids, sort_pdus};
859
860	fn pdu(index: usize, id: &OwnedEventId, prev: &[&OwnedEventId]) -> (usize, Pdu) {
861		let prev_events: Vec<&str> = prev.iter().map(|e| e.as_str()).collect();
862		let value: CanonicalJsonObject =
863			serde_json::from_value(json!({ "prev_events": prev_events }))
864				.expect("valid canonical json");
865
866		(index, (room_id!("!r:example.com").to_owned(), id.clone(), value))
867	}
868
869	fn ids() -> (OwnedEventId, OwnedEventId, OwnedEventId) {
870		(
871			event_id!("$a:example.com").to_owned(),
872			event_id!("$b:example.com").to_owned(),
873			event_id!("$c:example.com").to_owned(),
874		)
875	}
876
877	fn order(pdus: &[(usize, Pdu)]) -> Vec<&str> {
878		pdus.iter()
879			.map(|(_, (_, id, _))| id.as_str())
880			.collect()
881	}
882
883	#[test]
884	fn sorted_when_parents_lead() {
885		let (a, b, c) = ids();
886		let pdus = [pdu(0, &a, &[]), pdu(1, &b, &[&a]), pdu(2, &c, &[&b])];
887
888		assert!(already_sorted(&pdus));
889	}
890
891	#[test]
892	fn unsorted_when_child_leads() {
893		let (a, b, _c) = ids();
894		let pdus = [pdu(0, &b, &[&a]), pdu(1, &a, &[])];
895
896		assert!(!already_sorted(&pdus));
897	}
898
899	#[test]
900	fn sorted_ignores_out_of_batch_references() {
901		let (a, b, c) = ids();
902		let pdus = [pdu(0, &b, &[&c]), pdu(1, &a, &[&c])];
903
904		assert!(already_sorted(&pdus));
905	}
906
907	#[tokio::test]
908	async fn sort_orders_parents_before_children() {
909		let (a, b, c) = ids();
910		let pdus: TxnPdus = [pdu(0, &c, &[&b]), pdu(1, &b, &[&a]), pdu(2, &a, &[])]
911			.into_iter()
912			.collect();
913
914		let sorted = sort_pdus(pdus).await;
915
916		assert_eq!(order(&sorted), ["$a:example.com", "$b:example.com", "$c:example.com"]);
917	}
918
919	#[tokio::test]
920	async fn sort_is_noop_when_already_ordered() {
921		let (a, b, c) = ids();
922		let pdus: TxnPdus = [pdu(0, &a, &[]), pdu(1, &b, &[&a]), pdu(2, &c, &[&b])]
923			.into_iter()
924			.collect();
925
926		let sorted = sort_pdus(pdus.clone()).await;
927
928		assert_eq!(order(&sorted), order(&pdus));
929	}
930
931	#[tokio::test]
932	async fn sort_preserves_duplicates() {
933		let (a, b, _c) = ids();
934		let pdus: TxnPdus = [pdu(0, &b, &[&a]), pdu(1, &a, &[]), pdu(2, &b, &[&a])]
935			.into_iter()
936			.collect();
937
938		let sorted = sort_pdus(pdus).await;
939
940		assert_eq!(sorted.len(), 3);
941	}
942
943	#[tokio::test]
944	async fn sort_preserves_a_cycle() {
945		let (a, b, _c) = ids();
946		let pdus: TxnPdus = [pdu(0, &a, &[&b]), pdu(1, &b, &[&a])]
947			.into_iter()
948			.collect();
949
950		let sorted = sort_pdus(pdus).await;
951
952		assert_eq!(sorted.len(), 2);
953	}
954
955	#[test]
956	fn prev_event_ids_reads_the_array() {
957		let (a, b, _c) = ids();
958		let (_, (_, _, value)) = pdu(0, &a, &[&b]);
959
960		let prev: Vec<&str> = prev_event_ids(&value).collect();
961
962		assert_eq!(prev, ["$b:example.com"]);
963	}
964
965	#[test]
966	fn prev_event_ids_empty_when_absent() {
967		let value = CanonicalJsonObject::new();
968
969		assert_eq!(prev_event_ids(&value).count(), 0);
970	}
971}