ruma_common/canonical_json/
redaction.rs1use std::{fmt, mem};
2
3use super::value::{CanonicalJsonObject, CanonicalJsonType, CanonicalJsonValue};
4use crate::{room_version_rules::RedactionRules, serde::Raw};
5
6pub fn redact(
31 mut object: CanonicalJsonObject,
32 rules: &RedactionRules,
33 redacted_because: Option<RedactedBecause>,
34) -> Result<CanonicalJsonObject, RedactionError> {
35 redact_in_place(&mut object, rules, redacted_because)?;
36 Ok(object)
37}
38
39pub fn redact_in_place(
43 event: &mut CanonicalJsonObject,
44 rules: &RedactionRules,
45 redacted_because: Option<RedactedBecause>,
46) -> Result<(), RedactionError> {
47 retained_event_keys(event)?.apply(rules, event);
48
49 if let Some(redacted_because) = redacted_because {
50 let unsigned = CanonicalJsonObject::from_iter([(
51 "redacted_because".to_owned(),
52 redacted_because.0.into(),
53 )]);
54 event.insert("unsigned".to_owned(), unsigned.into());
55 }
56
57 Ok(())
58}
59
60pub fn redact_content_in_place(
65 content: &mut CanonicalJsonObject,
66 rules: &RedactionRules,
67 event_type: impl AsRef<str>,
68) {
69 retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content);
70}
71
72#[derive(Clone, Debug)]
74pub struct RedactedBecause(CanonicalJsonObject);
75
76impl RedactedBecause {
77 pub fn from_json(obj: CanonicalJsonObject) -> Self {
79 Self(obj)
80 }
81
82 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
86 ev.deserialize_as_unchecked().map(Self)
87 }
88}
89
90pub trait RedactionEvent {}
92
93#[derive(Debug)]
95#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
96pub enum RedactionError {
97 InvalidType {
99 path: String,
101
102 expected: CanonicalJsonType,
104
105 found: CanonicalJsonType,
107 },
108
109 MissingField {
111 path: String,
113 },
114}
115
116impl fmt::Display for RedactionError {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 RedactionError::InvalidType { path, expected, found } => {
120 write!(f, "invalid type at `{path}`: expected {expected:?}, found {found:?}")
121 }
122 RedactionError::MissingField { path } => {
123 write!(f, "missing field: `{path}`")
124 }
125 }
126 }
127}
128
129impl std::error::Error for RedactionError {}
130
131type RetainKeyFn = dyn Fn(&RedactionRules, &str) -> RetainKey;
134
135enum RetainKey {
137 Yes {
139 child_retained_keys: Option<RetainedKeys>,
143 },
144
145 No,
147}
148
149impl From<bool> for RetainKey {
150 fn from(value: bool) -> Self {
151 if value { Self::Yes { child_retained_keys: None } } else { Self::No }
152 }
153}
154
155enum RetainedKeys {
157 All,
159
160 Some(Box<RetainKeyFn>),
162
163 None,
165}
166
167impl RetainedKeys {
168 fn some<F>(retain_key_fn: F) -> Self
170 where
171 F: Fn(&RedactionRules, &str) -> RetainKey + Clone + 'static,
172 {
173 Self::Some(Box::new(retain_key_fn))
174 }
175
176 fn apply(&self, rules: &RedactionRules, object: &mut CanonicalJsonObject) {
178 match self {
179 Self::All => {}
180 Self::Some(retain_key_fn) => {
181 let old_object = mem::take(object);
182
183 for (key, mut value) in old_object {
184 if let RetainKey::Yes { child_retained_keys } = retain_key_fn(rules, &key) {
185 if let Some(child_retained_keys) = child_retained_keys
186 && let CanonicalJsonValue::Object(child_object) = &mut value
187 {
188 child_retained_keys.apply(rules, child_object);
189 }
190
191 object.insert(key, value);
192 }
193 }
194 }
195 Self::None => object.clear(),
196 }
197 }
198}
199
200fn retained_event_keys(event: &CanonicalJsonObject) -> Result<RetainedKeys, RedactionError> {
202 let event_type = match event.get("type") {
203 Some(CanonicalJsonValue::String(event_type)) => event_type.clone(),
204 Some(value) => {
205 return Err(RedactionError::InvalidType {
206 path: "type".to_owned(),
207 expected: CanonicalJsonType::String,
208 found: value.json_type(),
209 });
210 }
211 None => return Err(RedactionError::MissingField { path: "type".to_owned() }),
212 };
213
214 Ok(RetainedKeys::some(move |rules, key| match key {
215 "content" => RetainKey::Yes {
216 child_retained_keys: Some(retained_event_content_keys(&event_type, rules)),
217 },
218 "event_id" | "type" | "room_id" | "sender" | "state_key" | "hashes" | "signatures"
219 | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true.into(),
220 "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state.into(),
221 _ => false.into(),
222 }))
223}
224
225fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
227 match event_type {
228 "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
229 "m.room.create" => room_create_content_retained_keys(rules),
230 "m.room.join_rules" => RetainedKeys::some(is_room_join_rules_content_key_retained),
231 "m.room.power_levels" => RetainedKeys::some(is_room_power_levels_content_key_retained),
232 "m.room.history_visibility" => {
233 RetainedKeys::some(|_rules, key| is_room_history_visibility_content_key_retained(key))
234 }
235 "m.room.redaction" => room_redaction_content_retained_keys(rules),
236 "m.room.aliases" => room_aliases_content_retained_keys(rules),
237 #[cfg(feature = "unstable-msc2870")]
238 "m.room.server_acl" => RetainedKeys::some(is_room_server_acl_content_key_retained),
239 _ => RetainedKeys::None,
240 }
241}
242
243fn is_room_member_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
245 match key {
246 "membership" => true.into(),
247 "join_authorised_via_users_server" => {
248 rules.keep_room_member_join_authorised_via_users_server.into()
249 }
250 "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
251 RetainKey::Yes {
252 child_retained_keys: Some(RetainedKeys::some(|_rules, key| {
253 (key == "signed").into()
254 })),
255 }
256 }
257 _ => false.into(),
258 }
259}
260
261fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
263 if rules.keep_room_create_content {
264 RetainedKeys::All
265 } else {
266 RetainedKeys::some(|_rules, field| (field == "creator").into())
267 }
268}
269
270fn is_room_join_rules_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
273 match key {
274 "join_rule" => true,
275 "allow" => rules.keep_room_join_rules_allow,
276 _ => false,
277 }
278 .into()
279}
280
281fn is_room_power_levels_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
284 match key {
285 "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
286 | "users_default" => true,
287 "invite" => rules.keep_room_power_levels_invite,
288 _ => false,
289 }
290 .into()
291}
292
293fn is_room_history_visibility_content_key_retained(key: &str) -> RetainKey {
296 (key == "history_visibility").into()
297}
298
299fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
301 if rules.keep_room_redaction_redacts {
302 RetainedKeys::some(|_rules, field| (field == "redacts").into())
303 } else {
304 RetainedKeys::None
305 }
306}
307
308fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
310 if rules.keep_room_aliases_aliases {
311 RetainedKeys::some(|_rules, field| (field == "aliases").into())
312 } else {
313 RetainedKeys::None
314 }
315}
316
317#[cfg(feature = "unstable-msc2870")]
320fn is_room_server_acl_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
321 match key {
322 "allow" | "deny" | "allow_ip_literals" => {
323 rules.keep_room_server_acl_allow_deny_allow_ip_literals
324 }
325 _ => false,
326 }
327 .into()
328}