Skip to main content

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