Skip to main content

ruma_events/
unsigned.rs

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