Skip to main content

ruma_events/
unsigned.rs

1use js_int::Int;
2use ruma_common::{
3    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId,
4    serde::{CanBeEmpty, Raw},
5};
6use serde::{Deserialize, de::DeserializeOwned};
7
8use super::{
9    MessageLikeEventContent, OriginalSyncMessageLikeEvent, PossiblyRedactedStateEventContent,
10    relation::{BundledMessageLikeRelations, BundledStateRelations},
11    room::redaction::RoomRedactionEventContent,
12};
13use crate::TimelineEventType;
14
15mod redacted_because_serde;
16
17/// Extra information about a message event that is not incorporated into the event's hash.
18#[derive(Clone, Debug, Deserialize)]
19#[serde(bound = "OriginalSyncMessageLikeEvent<C>: DeserializeOwned")]
20#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
21pub struct MessageLikeUnsigned<C: MessageLikeEventContent> {
22    /// The time in milliseconds that has elapsed since the event was sent.
23    ///
24    /// This field is generated by the local homeserver, and may be incorrect if the local time on
25    /// at least one of the two servers is out of sync, which can cause the age to either be
26    /// negative or greater than it actually is.
27    pub age: Option<Int>,
28
29    /// The client-supplied transaction ID, if the client being given the event is the same one
30    /// which sent it.
31    pub transaction_id: Option<OwnedTransactionId>,
32
33    /// [Bundled aggregations] of related child events.
34    ///
35    /// [Bundled aggregations]: https://spec.matrix.org/v1.18/client-server-api/#aggregations-of-child-events
36    #[serde(rename = "m.relations", default)]
37    pub relations: BundledMessageLikeRelations<OriginalSyncMessageLikeEvent<C>>,
38}
39
40impl<C: MessageLikeEventContent> MessageLikeUnsigned<C> {
41    /// Create a new `Unsigned` with fields set to `None`.
42    pub fn new() -> Self {
43        Self { age: None, transaction_id: None, relations: BundledMessageLikeRelations::default() }
44    }
45}
46
47impl<C: MessageLikeEventContent> Default for MessageLikeUnsigned<C> {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl<C: MessageLikeEventContent> CanBeEmpty for MessageLikeUnsigned<C> {
54    /// Whether this unsigned data is empty (all fields are `None`).
55    ///
56    /// This method is used to determine whether to skip serializing the `unsigned` field in room
57    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
58    /// could still have been present but contained none of the known fields.
59    fn is_empty(&self) -> bool {
60        self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty()
61    }
62}
63
64/// Extra information about a state event that is not incorporated into the event's hash.
65#[derive(Clone, Debug, Deserialize)]
66#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
67pub struct StateUnsigned<C: PossiblyRedactedStateEventContent> {
68    /// The time in milliseconds that has elapsed since the event was sent.
69    ///
70    /// This field is generated by the local homeserver, and may be incorrect if the local time on
71    /// at least one of the two servers is out of sync, which can cause the age to either be
72    /// negative or greater than it actually is.
73    pub age: Option<Int>,
74
75    /// The client-supplied transaction ID, if the client being given the event is the same one
76    /// which sent it.
77    pub transaction_id: Option<OwnedTransactionId>,
78
79    /// The event ID of the state event replaced by this event.
80    pub replaces_state: Option<OwnedEventId>,
81
82    /// Optional previous content of the event.
83    pub prev_content: Option<C>,
84
85    /// [Bundled aggregations] of related child events.
86    ///
87    /// [Bundled aggregations]: https://spec.matrix.org/v1.18/client-server-api/#aggregations-of-child-events
88    #[serde(rename = "m.relations", default)]
89    pub relations: BundledStateRelations,
90}
91
92impl<C: PossiblyRedactedStateEventContent> StateUnsigned<C> {
93    /// Create a new `Unsigned` with fields set to `None`.
94    pub fn new() -> Self {
95        Self {
96            age: None,
97            transaction_id: None,
98            replaces_state: None,
99            prev_content: None,
100            relations: Default::default(),
101        }
102    }
103}
104
105impl<C: PossiblyRedactedStateEventContent> CanBeEmpty for StateUnsigned<C> {
106    /// Whether this unsigned data is empty (all fields are `None`).
107    ///
108    /// This method is used to determine whether to skip serializing the `unsigned` field in room
109    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
110    /// could still have been present but contained none of the known fields.
111    fn is_empty(&self) -> bool {
112        self.age.is_none()
113            && self.transaction_id.is_none()
114            && self.prev_content.is_none()
115            && self.relations.is_empty()
116    }
117}
118
119impl<C: PossiblyRedactedStateEventContent> Default for StateUnsigned<C> {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// Extra information about a redacted event that is not incorporated into the event's hash.
126#[derive(Clone, Debug, Deserialize)]
127#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
128pub struct RedactedUnsigned {
129    /// The event that redacted this event, if any.
130    pub redacted_because: Raw<AnyRedactionEvent>,
131}
132
133impl RedactedUnsigned {
134    /// Create a new `RedactedUnsigned` with the given redaction event.
135    pub fn new(redacted_because: Raw<AnyRedactionEvent>) -> Self {
136        Self { redacted_because }
137    }
138}
139
140/// Any event that can redact another event, i.e. an event that can be found in
141/// `unsigned.redacted_because`.
142#[derive(Clone, Debug)]
143#[non_exhaustive]
144#[allow(clippy::large_enum_variant)]
145pub enum AnyRedactionEvent {
146    /// m.room.redaction
147    RoomRedaction(UnsignedRoomRedactionEvent),
148
149    /// m.room.member
150    #[cfg(feature = "unstable-msc4293")]
151    RoomMember(super::room::member::SyncRoomMemberEvent),
152
153    #[doc(hidden)]
154    _Custom(CustomRedactionEvent),
155}
156
157impl AnyRedactionEvent {
158    /// Returns the `type` of this event.
159    pub fn event_type(&self) -> TimelineEventType {
160        match self {
161            Self::RoomRedaction(_) => TimelineEventType::RoomRedaction,
162            #[cfg(feature = "unstable-msc4293")]
163            Self::RoomMember(_) => TimelineEventType::RoomMember,
164            Self::_Custom(e) => TimelineEventType::from(&*e.event_type),
165        }
166    }
167
168    /// Returns the `origin_server_ts` of this event.
169    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
170        match self {
171            Self::RoomRedaction(e) => e.origin_server_ts,
172            #[cfg(feature = "unstable-msc4293")]
173            Self::RoomMember(e) => e.origin_server_ts(),
174            Self::_Custom(e) => e.origin_server_ts,
175        }
176    }
177
178    /// Returns the `event_id` of this event.
179    pub fn event_id(&self) -> &EventId {
180        match self {
181            Self::RoomRedaction(e) => &e.event_id,
182            #[cfg(feature = "unstable-msc4293")]
183            Self::RoomMember(e) => e.event_id(),
184            Self::_Custom(e) => &e.event_id,
185        }
186    }
187
188    /// Returns the `sender` of this event.
189    pub fn sender(&self) -> &UserId {
190        match self {
191            Self::RoomRedaction(e) => &e.sender,
192            #[cfg(feature = "unstable-msc4293")]
193            Self::RoomMember(e) => e.sender(),
194            Self::_Custom(e) => &e.sender,
195        }
196    }
197}
198
199/// An `m.room.redaction` event as found in `unsigned.redacted_because`.
200///
201/// While servers usually send this with the `redacts` field (unless nested), the ID of the event
202/// being redacted is known from context wherever this type is used, so it's not reflected as a
203/// field here.
204///
205/// It is intentionally not possible to create an instance of this type other than through `Clone`
206/// or `Deserialize`.
207#[derive(Clone, Debug, Deserialize)]
208#[non_exhaustive]
209pub struct UnsignedRoomRedactionEvent {
210    /// Data specific to the event type.
211    pub content: RoomRedactionEventContent,
212
213    /// The globally unique event identifier for the user who sent the event.
214    pub event_id: OwnedEventId,
215
216    /// The fully-qualified ID of the user who sent this event.
217    pub sender: OwnedUserId,
218
219    /// Timestamp in milliseconds on originating homeserver when this event was sent.
220    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
221
222    /// Additional key-value pairs not signed by the homeserver.
223    #[serde(default)]
224    pub unsigned: MessageLikeUnsigned<RoomRedactionEventContent>,
225}
226
227/// A custom redaction event.
228#[doc(hidden)]
229#[derive(Clone, Debug)]
230pub struct CustomRedactionEvent {
231    /// The type of the event
232    event_type: Box<str>,
233
234    /// The globally unique event identifier for the user who sent the event.
235    event_id: OwnedEventId,
236
237    /// The fully-qualified ID of the user who sent this event.
238    sender: OwnedUserId,
239
240    /// Timestamp in milliseconds on originating homeserver when this event was sent.
241    origin_server_ts: MilliSecondsSinceUnixEpoch,
242}
243
244#[cfg(test)]
245mod tests {
246    use assert_matches2::assert_matches;
247    use js_int::uint;
248    use serde_json::{from_value as from_json_value, json};
249
250    use super::AnyRedactionEvent;
251    use crate::TimelineEventType;
252
253    #[test]
254    fn deserialize_any_redaction_event_room_redaction() {
255        let json = json!({
256            "type": "m.room.redaction",
257            "content": {
258                "redacts": "$redactedevent",
259            },
260            "event_id": "$redactionevent",
261            "origin_server_ts": 1,
262            "sender": "@carl:example.com",
263        });
264
265        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
266        assert_eq!(event.event_id(), "$redactionevent");
267        assert_eq!(event.origin_server_ts().0, uint!(1));
268        assert_eq!(event.sender(), "@carl:example.com");
269        assert_eq!(event.event_type(), TimelineEventType::RoomRedaction);
270        assert_matches!(event, AnyRedactionEvent::RoomRedaction(_));
271    }
272
273    #[test]
274    fn deserialize_any_redaction_event_custom() {
275        let json = json!({
276            "type": "local.dev.custom_type",
277            "content": {},
278            "event_id": "$redactionevent",
279            "origin_server_ts": 1,
280            "sender": "@carl:example.com",
281        });
282
283        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
284        assert_eq!(event.event_id(), "$redactionevent");
285        assert_eq!(event.origin_server_ts().0, uint!(1));
286        assert_eq!(event.sender(), "@carl:example.com");
287        assert_eq!(event.event_type().to_string(), "local.dev.custom_type");
288    }
289}