ruma_events/room/
redaction.rs

1//! Types for the [`m.room.redaction`] event.
2//!
3//! [`m.room.redaction`]: https://spec.matrix.org/latest/client-server-api/#mroomredaction
4
5use as_variant::as_variant;
6use js_int::Int;
7#[cfg(feature = "canonical-json")]
8use ruma_common::canonical_json::RedactionEvent;
9use ruma_common::{
10    room_version_rules::RedactionRules,
11    serde::{CanBeEmpty, JsonCastable, JsonObject},
12    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedTransactionId,
13    OwnedUserId, RoomId, UserId,
14};
15use ruma_macros::{Event, EventContent};
16use serde::{Deserialize, Serialize};
17use tracing::error;
18
19use crate::{
20    BundledMessageLikeRelations, MessageLikeEventContent, MessageLikeEventType, RedactContent,
21    RedactedMessageLikeEventContent, RedactedUnsigned, StaticEventContent,
22};
23
24mod event_serde;
25
26/// A possibly-redacted redaction event.
27#[allow(clippy::exhaustive_enums)]
28#[derive(Clone, Debug)]
29pub enum RoomRedactionEvent {
30    /// Original, unredacted form of the event.
31    Original(OriginalRoomRedactionEvent),
32
33    /// Redacted form of the event with minimal fields.
34    Redacted(RedactedRoomRedactionEvent),
35}
36
37impl JsonCastable<SyncRoomRedactionEvent> for RoomRedactionEvent {}
38
39impl JsonCastable<JsonObject> for RoomRedactionEvent {}
40
41/// A possibly-redacted redaction event without a `room_id`.
42#[allow(clippy::exhaustive_enums)]
43#[derive(Clone, Debug)]
44pub enum SyncRoomRedactionEvent {
45    /// Original, unredacted form of the event.
46    Original(OriginalSyncRoomRedactionEvent),
47
48    /// Redacted form of the event with minimal fields.
49    Redacted(RedactedSyncRoomRedactionEvent),
50}
51
52impl JsonCastable<JsonObject> for SyncRoomRedactionEvent {}
53
54/// Redaction event.
55#[derive(Clone, Debug)]
56#[allow(clippy::exhaustive_structs)]
57pub struct OriginalRoomRedactionEvent {
58    /// Data specific to the event type.
59    pub content: RoomRedactionEventContent,
60
61    /// The ID of the event that was redacted.
62    ///
63    /// This field is required in room versions prior to 11.
64    pub redacts: Option<OwnedEventId>,
65
66    /// The globally unique event identifier for the user who sent the event.
67    pub event_id: OwnedEventId,
68
69    /// The fully-qualified ID of the user who sent this event.
70    pub sender: OwnedUserId,
71
72    /// Timestamp in milliseconds on originating homeserver when this event was sent.
73    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
74
75    /// The ID of the room associated with this event.
76    pub room_id: OwnedRoomId,
77
78    /// Additional key-value pairs not signed by the homeserver.
79    pub unsigned: RoomRedactionUnsigned,
80}
81
82impl From<OriginalRoomRedactionEvent> for OriginalSyncRoomRedactionEvent {
83    fn from(value: OriginalRoomRedactionEvent) -> Self {
84        let OriginalRoomRedactionEvent {
85            content,
86            redacts,
87            event_id,
88            sender,
89            origin_server_ts,
90            unsigned,
91            ..
92        } = value;
93
94        Self { content, redacts, event_id, sender, origin_server_ts, unsigned }
95    }
96}
97
98impl JsonCastable<OriginalSyncRoomRedactionEvent> for OriginalRoomRedactionEvent {}
99
100impl JsonCastable<RoomRedactionEvent> for OriginalRoomRedactionEvent {}
101
102impl JsonCastable<SyncRoomRedactionEvent> for OriginalRoomRedactionEvent {}
103
104impl JsonCastable<JsonObject> for OriginalRoomRedactionEvent {}
105
106/// Redacted redaction event.
107#[derive(Clone, Debug, Event)]
108#[allow(clippy::exhaustive_structs)]
109pub struct RedactedRoomRedactionEvent {
110    /// Data specific to the event type.
111    pub content: RedactedRoomRedactionEventContent,
112
113    /// The globally unique event identifier for the user who sent the event.
114    pub event_id: OwnedEventId,
115
116    /// The fully-qualified ID of the user who sent this event.
117    pub sender: OwnedUserId,
118
119    /// Timestamp in milliseconds on originating homeserver when this event was sent.
120    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
121
122    /// The ID of the room associated with this event.
123    pub room_id: OwnedRoomId,
124
125    /// Additional key-value pairs not signed by the homeserver.
126    pub unsigned: RedactedUnsigned,
127}
128
129impl JsonCastable<RedactedSyncRoomRedactionEvent> for RedactedRoomRedactionEvent {}
130
131impl JsonCastable<RoomRedactionEvent> for RedactedRoomRedactionEvent {}
132
133impl JsonCastable<SyncRoomRedactionEvent> for RedactedRoomRedactionEvent {}
134
135impl JsonCastable<JsonObject> for RedactedRoomRedactionEvent {}
136
137/// Redaction event without a `room_id`.
138#[derive(Clone, Debug)]
139#[allow(clippy::exhaustive_structs)]
140pub struct OriginalSyncRoomRedactionEvent {
141    /// Data specific to the event type.
142    pub content: RoomRedactionEventContent,
143
144    /// The ID of the event that was redacted.
145    ///
146    /// This field is required in room versions prior to 11.
147    pub redacts: Option<OwnedEventId>,
148
149    /// The globally unique event identifier for the user who sent the event.
150    pub event_id: OwnedEventId,
151
152    /// The fully-qualified ID of the user who sent this event.
153    pub sender: OwnedUserId,
154
155    /// Timestamp in milliseconds on originating homeserver when this event was sent.
156    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
157
158    /// Additional key-value pairs not signed by the homeserver.
159    pub unsigned: RoomRedactionUnsigned,
160}
161
162impl OriginalSyncRoomRedactionEvent {
163    /// Convert this sync event into a full event, one with a `room_id` field.
164    pub fn into_full_event(self, room_id: OwnedRoomId) -> OriginalRoomRedactionEvent {
165        let Self { content, redacts, event_id, sender, origin_server_ts, unsigned } = self;
166
167        OriginalRoomRedactionEvent {
168            content,
169            redacts,
170            event_id,
171            sender,
172            origin_server_ts,
173            room_id,
174            unsigned,
175        }
176    }
177
178    pub(crate) fn into_maybe_redacted(self) -> SyncRoomRedactionEvent {
179        SyncRoomRedactionEvent::Original(self)
180    }
181}
182
183impl JsonCastable<SyncRoomRedactionEvent> for OriginalSyncRoomRedactionEvent {}
184
185impl JsonCastable<JsonObject> for OriginalSyncRoomRedactionEvent {}
186
187/// Redacted redaction event without a `room_id`.
188#[derive(Clone, Debug, Event)]
189#[allow(clippy::exhaustive_structs)]
190pub struct RedactedSyncRoomRedactionEvent {
191    /// Data specific to the event type.
192    pub content: RedactedRoomRedactionEventContent,
193
194    /// The globally unique event identifier for the user who sent the event.
195    pub event_id: OwnedEventId,
196
197    /// The fully-qualified ID of the user who sent this event.
198    pub sender: OwnedUserId,
199
200    /// Timestamp in milliseconds on originating homeserver when this event was sent.
201    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
202
203    /// Additional key-value pairs not signed by the homeserver.
204    pub unsigned: RedactedUnsigned,
205}
206
207impl JsonCastable<SyncRoomRedactionEvent> for RedactedSyncRoomRedactionEvent {}
208
209impl JsonCastable<JsonObject> for RedactedSyncRoomRedactionEvent {}
210
211/// A redaction of an event.
212#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
213#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
214#[ruma_event(type = "m.room.redaction", kind = MessageLike, custom_redacted)]
215pub struct RoomRedactionEventContent {
216    /// The ID of the event that was redacted.
217    ///
218    /// This field is required starting from room version 11.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub redacts: Option<OwnedEventId>,
221
222    /// The reason for the redaction, if any.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub reason: Option<String>,
225}
226
227impl RoomRedactionEventContent {
228    /// Creates an empty `RoomRedactionEventContent` according to room versions 1 through 10.
229    pub fn new_v1() -> Self {
230        Self::default()
231    }
232
233    /// Creates a `RoomRedactionEventContent` with the required `redacts` field introduced in room
234    /// version 11.
235    pub fn new_v11(redacts: OwnedEventId) -> Self {
236        Self { redacts: Some(redacts), ..Default::default() }
237    }
238
239    /// Add the given reason to this `RoomRedactionEventContent`.
240    pub fn with_reason(mut self, reason: String) -> Self {
241        self.reason = Some(reason);
242        self
243    }
244}
245
246impl RedactContent for RoomRedactionEventContent {
247    type Redacted = RedactedRoomRedactionEventContent;
248
249    fn redact(self, rules: &RedactionRules) -> Self::Redacted {
250        let redacts = self.redacts.filter(|_| rules.keep_room_redaction_redacts);
251        RedactedRoomRedactionEventContent { redacts }
252    }
253}
254
255/// A redacted redaction event.
256#[derive(Clone, Debug, Default, Deserialize, Serialize)]
257#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
258pub struct RedactedRoomRedactionEventContent {
259    /// The ID of the event that was redacted.
260    ///
261    /// This field is required starting from room version 11.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub redacts: Option<OwnedEventId>,
264}
265
266impl StaticEventContent for RedactedRoomRedactionEventContent {
267    const TYPE: &'static str = RoomRedactionEventContent::TYPE;
268    type IsPrefix = <RoomRedactionEventContent as StaticEventContent>::IsPrefix;
269}
270
271impl RedactedMessageLikeEventContent for RedactedRoomRedactionEventContent {
272    fn event_type(&self) -> MessageLikeEventType {
273        MessageLikeEventType::RoomRedaction
274    }
275}
276
277impl RoomRedactionEvent {
278    /// Returns the `type` of this event.
279    pub fn event_type(&self) -> MessageLikeEventType {
280        match self {
281            Self::Original(ev) => ev.content.event_type(),
282            Self::Redacted(ev) => ev.content.event_type(),
283        }
284    }
285
286    /// Returns this event's `event_id` field.
287    pub fn event_id(&self) -> &EventId {
288        match self {
289            Self::Original(ev) => &ev.event_id,
290            Self::Redacted(ev) => &ev.event_id,
291        }
292    }
293
294    /// Returns this event's `sender` field.
295    pub fn sender(&self) -> &UserId {
296        match self {
297            Self::Original(ev) => &ev.sender,
298            Self::Redacted(ev) => &ev.sender,
299        }
300    }
301
302    /// Returns this event's `origin_server_ts` field.
303    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
304        match self {
305            Self::Original(ev) => ev.origin_server_ts,
306            Self::Redacted(ev) => ev.origin_server_ts,
307        }
308    }
309
310    /// Returns this event's `room_id` field.
311    pub fn room_id(&self) -> &RoomId {
312        match self {
313            Self::Original(ev) => &ev.room_id,
314            Self::Redacted(ev) => &ev.room_id,
315        }
316    }
317
318    /// Returns the ID of the event that this event redacts, according to the given redaction rules.
319    ///
320    /// # Panics
321    ///
322    /// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
323    /// possible if the event was modified after being deserialized.
324    pub fn redacts(&self, rules: &RedactionRules) -> Option<&EventId> {
325        match self {
326            Self::Original(ev) => Some(ev.redacts(rules)),
327            Self::Redacted(ev) => ev.content.redacts.as_deref(),
328        }
329    }
330
331    /// Get the inner `RoomRedactionEvent` if this is an unredacted event.
332    pub fn as_original(&self) -> Option<&OriginalRoomRedactionEvent> {
333        as_variant!(self, Self::Original)
334    }
335}
336
337impl SyncRoomRedactionEvent {
338    /// Returns the `type` of this event.
339    pub fn event_type(&self) -> MessageLikeEventType {
340        match self {
341            Self::Original(ev) => ev.content.event_type(),
342            Self::Redacted(ev) => ev.content.event_type(),
343        }
344    }
345
346    /// Returns this event's `event_id` field.
347    pub fn event_id(&self) -> &EventId {
348        match self {
349            Self::Original(ev) => &ev.event_id,
350            Self::Redacted(ev) => &ev.event_id,
351        }
352    }
353
354    /// Returns this event's `sender` field.
355    pub fn sender(&self) -> &UserId {
356        match self {
357            Self::Original(ev) => &ev.sender,
358            Self::Redacted(ev) => &ev.sender,
359        }
360    }
361
362    /// Returns this event's `origin_server_ts` field.
363    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
364        match self {
365            Self::Original(ev) => ev.origin_server_ts,
366            Self::Redacted(ev) => ev.origin_server_ts,
367        }
368    }
369
370    /// Returns the ID of the event that this event redacts, according to the given redaction rules.
371    ///
372    /// # Panics
373    ///
374    /// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
375    /// possible if the event was modified after being deserialized.
376    pub fn redacts(&self, rules: &RedactionRules) -> Option<&EventId> {
377        match self {
378            Self::Original(ev) => Some(ev.redacts(rules)),
379            Self::Redacted(ev) => ev.content.redacts.as_deref(),
380        }
381    }
382
383    /// Get the inner `SyncRoomRedactionEvent` if this is an unredacted event.
384    pub fn as_original(&self) -> Option<&OriginalSyncRoomRedactionEvent> {
385        as_variant!(self, Self::Original)
386    }
387
388    /// Convert this sync event into a full event (one with a `room_id` field).
389    pub fn into_full_event(self, room_id: OwnedRoomId) -> RoomRedactionEvent {
390        match self {
391            Self::Original(ev) => RoomRedactionEvent::Original(ev.into_full_event(room_id)),
392            Self::Redacted(ev) => RoomRedactionEvent::Redacted(ev.into_full_event(room_id)),
393        }
394    }
395}
396
397impl From<RoomRedactionEvent> for SyncRoomRedactionEvent {
398    fn from(full: RoomRedactionEvent) -> Self {
399        match full {
400            RoomRedactionEvent::Original(ev) => Self::Original(ev.into()),
401            RoomRedactionEvent::Redacted(ev) => Self::Redacted(ev.into()),
402        }
403    }
404}
405
406impl OriginalRoomRedactionEvent {
407    /// Returns the ID of the event that this event redacts, according to the proper `redacts` field
408    /// for the given redaction rules.
409    ///
410    /// If the `redacts` field is not the proper one for the given rules, this falls back to the one
411    /// that is available.
412    ///
413    /// # Panics
414    ///
415    /// Panics if both `redacts` field are `None`, which is only possible if the event was modified
416    /// after being deserialized.
417    pub fn redacts(&self, rules: &RedactionRules) -> &EventId {
418        redacts(rules, self.redacts.as_deref(), self.content.redacts.as_deref())
419    }
420}
421
422impl OriginalSyncRoomRedactionEvent {
423    /// Returns the ID of the event that this event redacts, according to the proper `redacts` field
424    /// for the given redaction rules.
425    ///
426    /// If the `redacts` field is not the proper one for the given rules, this falls back to the one
427    /// that is available.
428    ///
429    /// # Panics
430    ///
431    /// Panics if both `redacts` field are `None`, which is only possible if the event was modified
432    /// after being deserialized.
433    pub fn redacts(&self, rules: &RedactionRules) -> &EventId {
434        redacts(rules, self.redacts.as_deref(), self.content.redacts.as_deref())
435    }
436}
437
438#[cfg(feature = "canonical-json")]
439impl RedactionEvent for OriginalRoomRedactionEvent {}
440#[cfg(feature = "canonical-json")]
441impl RedactionEvent for OriginalSyncRoomRedactionEvent {}
442#[cfg(feature = "canonical-json")]
443impl RedactionEvent for RoomRedactionEvent {}
444#[cfg(feature = "canonical-json")]
445impl RedactionEvent for SyncRoomRedactionEvent {}
446
447/// Extra information about a redaction that is not incorporated into the event's hash.
448#[derive(Clone, Debug, Deserialize)]
449#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
450pub struct RoomRedactionUnsigned {
451    /// The time in milliseconds that has elapsed since the event was sent.
452    ///
453    /// This field is generated by the local homeserver, and may be incorrect if the local time on
454    /// at least one of the two servers is out of sync, which can cause the age to either be
455    /// negative or greater than it actually is.
456    pub age: Option<Int>,
457
458    /// The client-supplied transaction ID, if the client being given the event is the same one
459    /// which sent it.
460    pub transaction_id: Option<OwnedTransactionId>,
461
462    /// [Bundled aggregations] of related child events.
463    ///
464    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
465    #[serde(rename = "m.relations", default)]
466    pub relations: BundledMessageLikeRelations<OriginalSyncRoomRedactionEvent>,
467}
468
469impl RoomRedactionUnsigned {
470    /// Create a new `Unsigned` with fields set to `None`.
471    pub fn new() -> Self {
472        Self { age: None, transaction_id: None, relations: BundledMessageLikeRelations::default() }
473    }
474}
475
476impl Default for RoomRedactionUnsigned {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482impl CanBeEmpty for RoomRedactionUnsigned {
483    /// Whether this unsigned data is empty (all fields are `None`).
484    ///
485    /// This method is used to determine whether to skip serializing the `unsigned` field in room
486    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
487    /// could still have been present but contained none of the known fields.
488    fn is_empty(&self) -> bool {
489        self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty()
490    }
491}
492
493/// Returns the value of the proper `redacts` field for the given redaction rules.
494///
495/// If the `redacts` field is not the proper one for the given rules, this falls back to the one
496/// that is available.
497///
498/// # Panics
499///
500/// Panics if both `redacts` and `content_redacts` are `None`.
501fn redacts<'a>(
502    rules: &'_ RedactionRules,
503    redacts: Option<&'a EventId>,
504    content_redacts: Option<&'a EventId>,
505) -> &'a EventId {
506    if rules.content_field_redacts {
507        content_redacts.or_else(|| {
508            error!(
509                "Redacts field inside content not available, \
510                 falling back to the one at the event level"
511            );
512            redacts
513        })
514    } else {
515        redacts.or_else(|| {
516            error!(
517                "Redacts field at event level not available, \
518                 falling back to the one inside content"
519            );
520            content_redacts
521        })
522    }
523    .expect("At least one redacts field is set")
524}