ruma_common/canonical_json/
redaction.rs1use std::mem;
2
3use super::{
4 CanonicalJsonFieldError, CanonicalJsonObject, CanonicalJsonObjectExt, CanonicalJsonValue,
5};
6use crate::{room_version_rules::RedactionRules, serde::Raw};
7
8pub fn redact(
33 mut object: CanonicalJsonObject,
34 rules: &RedactionRules,
35 redacted_because: Option<RedactedBecause>,
36) -> Result<CanonicalJsonObject, CanonicalJsonFieldError> {
37 redact_in_place(&mut object, rules, redacted_because)?;
38 Ok(object)
39}
40
41pub fn redact_in_place(
45 event: &mut CanonicalJsonObject,
46 rules: &RedactionRules,
47 redacted_because: Option<RedactedBecause>,
48) -> Result<(), CanonicalJsonFieldError> {
49 retained_event_keys(event)?.apply(rules, event);
50
51 if let Some(redacted_because) = redacted_because {
52 let unsigned = CanonicalJsonObject::from_iter([(
53 "redacted_because".to_owned(),
54 redacted_because.0.into(),
55 )]);
56 event.insert("unsigned".to_owned(), unsigned.into());
57 }
58
59 Ok(())
60}
61
62pub fn redact_content_in_place(
67 content: &mut CanonicalJsonObject,
68 rules: &RedactionRules,
69 event_type: impl AsRef<str>,
70) {
71 retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content);
72}
73
74#[derive(Clone, Debug)]
76pub struct RedactedBecause(CanonicalJsonObject);
77
78impl RedactedBecause {
79 pub fn from_json(obj: CanonicalJsonObject) -> Self {
81 Self(obj)
82 }
83
84 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
88 ev.deserialize_as_unchecked().map(Self)
89 }
90}
91
92pub trait RedactionEvent {}
94
95type RetainKeyFn = dyn Fn(&RedactionRules, &str) -> RetainKey;
98
99enum RetainKey {
101 Yes {
103 child_retained_keys: Option<RetainedKeys>,
107 },
108
109 No,
111}
112
113impl From<bool> for RetainKey {
114 fn from(value: bool) -> Self {
115 if value { Self::Yes { child_retained_keys: None } } else { Self::No }
116 }
117}
118
119enum RetainedKeys {
121 All,
123
124 Some(Box<RetainKeyFn>),
126
127 None,
129}
130
131impl RetainedKeys {
132 fn some<F>(retain_key_fn: F) -> Self
134 where
135 F: Fn(&RedactionRules, &str) -> RetainKey + Clone + 'static,
136 {
137 Self::Some(Box::new(retain_key_fn))
138 }
139
140 fn apply(&self, rules: &RedactionRules, object: &mut CanonicalJsonObject) {
142 match self {
143 Self::All => {}
144 Self::Some(retain_key_fn) => {
145 let old_object = mem::take(object);
146
147 for (key, mut value) in old_object {
148 if let RetainKey::Yes { child_retained_keys } = retain_key_fn(rules, &key) {
149 if let Some(child_retained_keys) = child_retained_keys
150 && let CanonicalJsonValue::Object(child_object) = &mut value
151 {
152 child_retained_keys.apply(rules, child_object);
153 }
154
155 object.insert(key, value);
156 }
157 }
158 }
159 Self::None => object.clear(),
160 }
161 }
162}
163
164fn retained_event_keys(
166 event: &CanonicalJsonObject,
167) -> Result<RetainedKeys, CanonicalJsonFieldError> {
168 let event_type = event.get_as_required_string("type", "type")?.to_owned();
169
170 Ok(RetainedKeys::some(move |rules, key| match key {
171 "content" => RetainKey::Yes {
172 child_retained_keys: Some(retained_event_content_keys(&event_type, rules)),
173 },
174 "event_id" | "type" | "room_id" | "sender" | "state_key" | "hashes" | "signatures"
175 | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true.into(),
176 "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state.into(),
177 _ => false.into(),
178 }))
179}
180
181fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
183 match event_type {
184 "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
185 "m.room.create" => room_create_content_retained_keys(rules),
186 "m.room.join_rules" => RetainedKeys::some(is_room_join_rules_content_key_retained),
187 "m.room.power_levels" => RetainedKeys::some(is_room_power_levels_content_key_retained),
188 "m.room.history_visibility" => {
189 RetainedKeys::some(|_rules, key| is_room_history_visibility_content_key_retained(key))
190 }
191 "m.room.redaction" => room_redaction_content_retained_keys(rules),
192 "m.room.aliases" => room_aliases_content_retained_keys(rules),
193 #[cfg(feature = "unstable-msc2870")]
194 "m.room.server_acl" => RetainedKeys::some(is_room_server_acl_content_key_retained),
195 _ => RetainedKeys::None,
196 }
197}
198
199fn is_room_member_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
201 match key {
202 "membership" => true.into(),
203 "join_authorised_via_users_server" => {
204 rules.keep_room_member_join_authorised_via_users_server.into()
205 }
206 "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
207 RetainKey::Yes {
208 child_retained_keys: Some(RetainedKeys::some(|_rules, key| {
209 (key == "signed").into()
210 })),
211 }
212 }
213 _ => false.into(),
214 }
215}
216
217fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
219 if rules.keep_room_create_content {
220 RetainedKeys::All
221 } else {
222 RetainedKeys::some(|_rules, field| (field == "creator").into())
223 }
224}
225
226fn is_room_join_rules_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
229 match key {
230 "join_rule" => true,
231 "allow" => rules.keep_room_join_rules_allow,
232 _ => false,
233 }
234 .into()
235}
236
237fn is_room_power_levels_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
240 match key {
241 "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
242 | "users_default" => true,
243 "invite" => rules.keep_room_power_levels_invite,
244 _ => false,
245 }
246 .into()
247}
248
249fn is_room_history_visibility_content_key_retained(key: &str) -> RetainKey {
252 (key == "history_visibility").into()
253}
254
255fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
257 if rules.keep_room_redaction_redacts {
258 RetainedKeys::some(|_rules, field| (field == "redacts").into())
259 } else {
260 RetainedKeys::None
261 }
262}
263
264fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
266 if rules.keep_room_aliases_aliases {
267 RetainedKeys::some(|_rules, field| (field == "aliases").into())
268 } else {
269 RetainedKeys::None
270 }
271}
272
273#[cfg(feature = "unstable-msc2870")]
276fn is_room_server_acl_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
277 match key {
278 "allow" | "deny" | "allow_ip_literals" => {
279 rules.keep_room_server_acl_allow_deny_allow_ip_literals
280 }
281 _ => false,
282 }
283 .into()
284}