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    pub replaces_state: Option<OwnedEventId>,
104
105    /// Optional previous content of the event.
106    pub prev_content: Option<C>,
107
108    /// [Bundled aggregations] of related child events.
109    ///
110    /// [Bundled aggregations]: https://spec.matrix.org/v1.19/client-server-api/#aggregations-of-child-events
111    #[serde(rename = "m.relations", default)]
112    pub relations: BundledStateRelations,
113
114    /// Milliseconds remaining until this sticky event expires.
115    ///
116    /// Only present in `/sync` responses for sticky events. See [MSC4354].
117    ///
118    /// [MSC4354]: https://github.com/matrix-org/matrix-spec-proposals/pull/4354
119    #[cfg(feature = "unstable-msc4354")]
120    #[serde(rename = "msc4354_sticky_duration_ttl_ms", default)]
121    #[serde(with = "ruma_common::serde::duration::opt_ms")]
122    pub sticky_duration_ttl_ms: Option<Duration>,
123}
124
125impl<C: PossiblyRedactedStateEventContent> StateUnsigned<C> {
126    /// Create a new `Unsigned` with fields set to `None`.
127    pub fn new() -> Self {
128        Self {
129            age: None,
130            transaction_id: None,
131            replaces_state: None,
132            prev_content: None,
133            relations: Default::default(),
134            #[cfg(feature = "unstable-msc4354")]
135            sticky_duration_ttl_ms: None,
136        }
137    }
138}
139
140impl<C: PossiblyRedactedStateEventContent> CanBeEmpty for StateUnsigned<C> {
141    /// Whether this unsigned data is empty (all fields are `None`).
142    ///
143    /// This method is used to determine whether to skip serializing the `unsigned` field in room
144    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
145    /// could still have been present but contained none of the known fields.
146    fn is_empty(&self) -> bool {
147        let empty = self.age.is_none()
148            && self.transaction_id.is_none()
149            && self.prev_content.is_none()
150            && self.relations.is_empty();
151        #[cfg(feature = "unstable-msc4354")]
152        let empty = empty && self.sticky_duration_ttl_ms.is_none();
153        empty
154    }
155}
156
157impl<C: PossiblyRedactedStateEventContent> Default for StateUnsigned<C> {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163/// Extra information about a redacted event that is not incorporated into the event's hash.
164#[derive(Clone, Debug, Deserialize)]
165#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
166pub struct RedactedUnsigned {
167    /// The event that redacted this event, if any.
168    pub redacted_because: Raw<AnyRedactionEvent>,
169}
170
171impl RedactedUnsigned {
172    /// Create a new `RedactedUnsigned` with the given redaction event.
173    pub fn new(redacted_because: Raw<AnyRedactionEvent>) -> Self {
174        Self { redacted_because }
175    }
176}
177
178/// Any event that can redact another event, i.e. an event that can be found in
179/// `unsigned.redacted_because`.
180#[derive(Clone, Debug)]
181#[non_exhaustive]
182#[allow(clippy::large_enum_variant)]
183pub enum AnyRedactionEvent {
184    /// m.room.redaction
185    RoomRedaction(UnsignedRoomRedactionEvent),
186
187    /// m.room.member
188    #[cfg(feature = "unstable-msc4293")]
189    RoomMember(super::room::member::SyncRoomMemberEvent),
190
191    #[doc(hidden)]
192    _Custom(CustomRedactionEvent),
193}
194
195impl AnyRedactionEvent {
196    /// Returns the `type` of this event.
197    pub fn event_type(&self) -> TimelineEventType {
198        match self {
199            Self::RoomRedaction(_) => TimelineEventType::RoomRedaction,
200            #[cfg(feature = "unstable-msc4293")]
201            Self::RoomMember(_) => TimelineEventType::RoomMember,
202            Self::_Custom(e) => TimelineEventType::from(&*e.event_type),
203        }
204    }
205
206    /// Returns the `origin_server_ts` of this event.
207    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
208        match self {
209            Self::RoomRedaction(e) => e.origin_server_ts,
210            #[cfg(feature = "unstable-msc4293")]
211            Self::RoomMember(e) => e.origin_server_ts(),
212            Self::_Custom(e) => e.origin_server_ts,
213        }
214    }
215
216    /// Returns the `event_id` of this event.
217    pub fn event_id(&self) -> &EventId {
218        match self {
219            Self::RoomRedaction(e) => &e.event_id,
220            #[cfg(feature = "unstable-msc4293")]
221            Self::RoomMember(e) => e.event_id(),
222            Self::_Custom(e) => &e.event_id,
223        }
224    }
225
226    /// Returns the `sender` of this event.
227    pub fn sender(&self) -> &UserId {
228        match self {
229            Self::RoomRedaction(e) => &e.sender,
230            #[cfg(feature = "unstable-msc4293")]
231            Self::RoomMember(e) => e.sender(),
232            Self::_Custom(e) => &e.sender,
233        }
234    }
235}
236
237/// An `m.room.redaction` event as found in `unsigned.redacted_because`.
238///
239/// While servers usually send this with the `redacts` field (unless nested), the ID of the event
240/// being redacted is known from context wherever this type is used, so it's not reflected as a
241/// field here.
242///
243/// It is intentionally not possible to create an instance of this type other than through `Clone`
244/// or `Deserialize`.
245#[derive(Clone, Debug, Deserialize)]
246#[non_exhaustive]
247pub struct UnsignedRoomRedactionEvent {
248    /// Data specific to the event type.
249    pub content: RoomRedactionEventContent,
250
251    /// The globally unique event identifier for the user who sent the event.
252    pub event_id: OwnedEventId,
253
254    /// The fully-qualified ID of the user who sent this event.
255    pub sender: OwnedUserId,
256
257    /// Timestamp in milliseconds on originating homeserver when this event was sent.
258    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
259
260    /// Additional key-value pairs not signed by the homeserver.
261    #[serde(default)]
262    pub unsigned: MessageLikeUnsigned<RoomRedactionEventContent>,
263}
264
265/// A custom redaction event.
266#[doc(hidden)]
267#[derive(Clone, Debug)]
268pub struct CustomRedactionEvent {
269    /// The type of the event
270    event_type: Box<str>,
271
272    /// The globally unique event identifier for the user who sent the event.
273    event_id: OwnedEventId,
274
275    /// The fully-qualified ID of the user who sent this event.
276    sender: OwnedUserId,
277
278    /// Timestamp in milliseconds on originating homeserver when this event was sent.
279    origin_server_ts: MilliSecondsSinceUnixEpoch,
280}
281
282#[cfg(test)]
283mod tests {
284    use assert_matches2::assert_matches;
285    use js_int::uint;
286    use serde_json::{from_value as from_json_value, json};
287
288    use super::AnyRedactionEvent;
289    use crate::TimelineEventType;
290
291    #[test]
292    fn deserialize_any_redaction_event_room_redaction() {
293        let json = json!({
294            "type": "m.room.redaction",
295            "content": {
296                "redacts": "$redactedevent",
297            },
298            "event_id": "$redactionevent",
299            "origin_server_ts": 1,
300            "sender": "@carl:example.com",
301        });
302
303        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
304        assert_eq!(event.event_id(), "$redactionevent");
305        assert_eq!(event.origin_server_ts().0, uint!(1));
306        assert_eq!(event.sender(), "@carl:example.com");
307        assert_eq!(event.event_type(), TimelineEventType::RoomRedaction);
308        assert_matches!(event, AnyRedactionEvent::RoomRedaction(_));
309    }
310
311    #[test]
312    fn deserialize_any_redaction_event_custom() {
313        let json = json!({
314            "type": "local.dev.custom_type",
315            "content": {},
316            "event_id": "$redactionevent",
317            "origin_server_ts": 1,
318            "sender": "@carl:example.com",
319        });
320
321        let event = from_json_value::<AnyRedactionEvent>(json).unwrap();
322        assert_eq!(event.event_id(), "$redactionevent");
323        assert_eq!(event.origin_server_ts().0, uint!(1));
324        assert_eq!(event.sender(), "@carl:example.com");
325        assert_eq!(event.event_type().to_string(), "local.dev.custom_type");
326    }
327}