Skip to main content

tuwunel_service/rooms/state_accessor/
erased.rs

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