Skip to main content

arminius_lib_events/
redaction.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Event redaction functionality.
6//!
7//! This module provides redaction capabilities for signed and verified events
8//! for each room version.
9
10use std::collections::BTreeMap;
11
12use ruma::{
13	CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch, UserId,
14	canonical_json::{self, RedactedBecause},
15	room_version_rules::{RedactionRules, RoomVersionRules},
16};
17use serde_json::value::RawValue as RawJsonValue;
18
19use crate::{
20	ContextualizedFields as _,
21	errors::{JsonSerDeError, RedactionError, ValidationError},
22	states::{Signed, Verified},
23	traits::Redactable,
24	transitions::RoomEvent,
25};
26
27/// Get the redaction rules for a room version.
28///
29/// This function extracts the appropriate `RedactionRules` from a
30/// `RoomVersionRules` instance.
31const fn get_redaction_rules(room_version: &RoomVersionRules) -> &RedactionRules {
32	&room_version.redaction
33}
34
35/// Build a RedactedBecause object for event redaction.
36///
37/// Creates the redaction event metadata to be attached to the
38/// `unsigned.redacted_because` field of redacted events.
39///
40/// Read: <https://spec.matrix.org/v1.17/client-server-api/#redactions>
41///
42/// ## Parameters
43///
44/// - `redactor`: User ID performing the redaction.
45/// - `reason`: Optional reason for the redaction.
46/// - `timestamp`: Timestamp for the redaction event.
47///
48/// ## Returns
49///
50/// A `RedactedBecause` instance representing the redaction event.
51fn build_redacted_because(
52	redactor: &UserId,
53	reason: Option<String>,
54	timestamp: MilliSecondsSinceUnixEpoch,
55) -> RedactedBecause {
56	let mut content = BTreeMap::new();
57	if let Some(reason_text) = reason {
58		content.insert("reason".to_owned(), CanonicalJsonValue::String(reason_text));
59	}
60
61	let mut redaction = BTreeMap::new();
62	redaction.insert("type".to_owned(), CanonicalJsonValue::String("m.room.redaction".to_owned()));
63	redaction.insert("sender".to_owned(), CanonicalJsonValue::String(redactor.to_string()));
64	redaction.insert("content".to_owned(), CanonicalJsonValue::Object(content));
65	redaction.insert(
66		"origin_server_ts".to_owned(),
67		CanonicalJsonValue::Integer(
68			ruma::Int::try_from(u64::from(timestamp.0)).unwrap_or(ruma::Int::MIN),
69		),
70	);
71
72	RedactedBecause::from_json(redaction)
73}
74
75/// Helper function to perform the common redaction logic.
76///
77/// This is the core redaction implementation used by trait implementations.
78fn redact_event_content(
79	event_object: CanonicalJsonObject,
80	room_version: &RoomVersionRules,
81	redacted_because: &RedactedBecause,
82) -> Result<Box<serde_json::value::RawValue>, ValidationError> {
83	let redaction_rules = get_redaction_rules(room_version);
84
85	// Redact the event using ruma's function
86	let redacted_object =
87		canonical_json::redact(event_object, redaction_rules, Some(redacted_because.clone()))
88			.map_err(RedactionError::Ruma)?;
89
90	// Extract the redacted content
91	let content_value = redacted_object.get("content").ok_or(RedactionError::MissingContent)?;
92
93	let content_json =
94		serde_json::to_string(&content_value).map_err(JsonSerDeError::JsonSerialization)?;
95
96	RawJsonValue::from_string(content_json).map_err(|serde_json_err| {
97		ValidationError::JsonSerDe(JsonSerDeError::ContentDeserialization(serde_json_err))
98	})
99}
100
101impl Redactable for RoomEvent<Signed> {
102	fn redact(&mut self, redactor: &UserId, reason: Option<String>) -> Result<(), ValidationError> {
103		let redacted_because =
104			build_redacted_because(redactor, reason, self.state.contextualized.origin_server_ts);
105		let event_object = self
106			.state
107			.build_event_object()
108			.map_err(|error| ValidationError::JsonCanonicalization(error.to_string()))?;
109		let new_content =
110			redact_event_content(event_object, &self.room_version().rules(), &redacted_because)?;
111		self.state
112			.contextualized
113			.draft
114			.redact(new_content)
115			.map_err(JsonSerDeError::ContentDeserialization)?;
116
117		Ok(())
118	}
119}
120
121impl Redactable for RoomEvent<Verified> {
122	fn redact(&mut self, redactor: &UserId, reason: Option<String>) -> Result<(), ValidationError> {
123		let redacted_because = build_redacted_because(
124			redactor,
125			reason,
126			self.state.signed.contextualized.origin_server_ts,
127		);
128		let event_object = self
129			.state
130			.signed
131			.contextualized
132			.build_canonical_event_object()
133			.map_err(|error| ValidationError::JsonCanonicalization(error.to_string()))?;
134		let new_content =
135			redact_event_content(event_object, &self.room_version().rules(), &redacted_because)?;
136		self.state
137			.signed
138			.contextualized
139			.draft
140			.redact(new_content)
141			.map_err(JsonSerDeError::ContentDeserialization)?;
142
143		Ok(())
144	}
145}