Skip to main content

tuwunel_service/rooms/state_accessor/
erased.rs

1use futures::FutureExt;
2use ruma::{
3	CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, ServerName, UserId,
4	canonical_json::redact_in_place, events::room::member::MembershipState,
5};
6use tuwunel_core::{
7	implement,
8	matrix::{Event, Pdu},
9	utils::{
10		BoolExt,
11		future::BoolExt as _,
12		result::{FlatOk, LogErr},
13	},
14};
15
16/// MSC4025: prune a federation-served event: its sender is erased and
17/// `origin` had no user joined in the room state at the event. Composes with
18/// history visibility rather than replacing it.
19#[implement(super::Service)]
20pub async fn erased_for_server(
21	&self,
22	origin: &ServerName,
23	mut pdu: CanonicalJsonObject,
24) -> CanonicalJsonObject {
25	if !self
26		.services
27		.config
28		.enforce_erasure_over_federation
29	{
30		return pdu;
31	}
32
33	let sender = pdu
34		.get("sender")
35		.and_then(CanonicalJsonValue::as_str)
36		.map(<&UserId>::try_from)
37		.flat_ok();
38
39	let Some(sender) = sender else {
40		return pdu;
41	};
42
43	if !self.services.users.is_erased(sender).await {
44		return pdu;
45	}
46
47	let event_id = pdu
48		.get("event_id")
49		.and_then(CanonicalJsonValue::as_str)
50		.map(<&EventId>::try_from)
51		.flat_ok();
52
53	let Some(event_id) = event_id else {
54		return pdu;
55	};
56
57	if self.server_joined_at_pdu(origin, event_id).await {
58		return pdu;
59	}
60
61	let room_id = pdu
62		.get("room_id")
63		.and_then(CanonicalJsonValue::as_str)
64		.map(<&RoomId>::try_from)
65		.flat_ok();
66
67	let Some(room_id) = room_id else {
68		return pdu;
69	};
70
71	let Ok(rules) = self
72		.services
73		.state
74		.get_room_version_rules(room_id)
75		.await
76		.log_err()
77	else {
78		return pdu;
79	};
80
81	redact_in_place(&mut pdu, &rules.redaction, None)
82		.log_err()
83		.ok();
84
85	pdu
86}
87
88/// MSC4025: the pruned clone of `pdu` for recipient `user_id`, or `None` to
89/// serve the original.
90///
91/// A prune that cannot read the room version's redaction rules also yields
92/// `None`, so an unprunable event is served intact rather than withheld.
93#[implement(super::Service)]
94pub async fn erased_view(&self, user_id: &UserId, pdu: &Pdu) -> Option<Pdu> {
95	self.erased_for(user_id, pdu)
96		.map(|erased| erased.then_async(|| self.pruned(pdu)))
97		.flatten()
98		.await
99		.flatten()
100}
101
102/// MSC4025: whether `pdu` serves pruned to `user_id`: its sender is erased
103/// and the recipient was not joined in the room state at the event.
104///
105/// Both lookups are polled concurrently, the erasure check first. A false
106/// result there resolves the conjunction without waiting on the membership
107/// read.
108#[implement(super::Service)]
109pub async fn erased_for(&self, user_id: &UserId, pdu: &Pdu) -> bool {
110	let is_erased = self.services.users.is_erased(pdu.sender());
111	let not_joined = self
112		.user_membership_at_pdu(user_id, pdu)
113		.map(|membership| membership.ne(&MembershipState::Join));
114
115	is_erased.and(not_joined).await
116}
117
118/// Prune per the room version's redaction rules. The pruned form carries no
119/// `redacted_because`; no redaction event exists for a serve-time erasure.
120#[implement(super::Service)]
121async fn pruned(&self, pdu: &Pdu) -> Option<Pdu> {
122	let rules = self
123		.services
124		.state
125		.get_room_version_rules(pdu.room_id())
126		.await
127		.log_err()
128		.ok()?;
129
130	pdu.redacted(&rules.redaction).log_err().ok()
131}