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