Skip to main content

ruma_events/stream/
update.rs

1//! Types for the `m.stream.update` to-device event ([MSC4471]).
2//!
3//! After applying a stream update, clients should render the transient body
4//! and ignore any current `formatted_body`.
5//!
6//! [MSC4471]: https://github.com/matrix-org/matrix-spec-proposals/pull/4471
7
8use js_int::UInt;
9use ruma_common::{OwnedEventId, OwnedRoomId};
10use ruma_macros::EventContent;
11use serde::{Deserialize, Serialize};
12
13/// The content of a to-device `m.stream.update` event.
14///
15/// Sent by the publisher device to a subscriber device.
16#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
17#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
18#[ruma_event(
19    type = "org.matrix.msc4471.stream.update",
20    alias = "m.stream.update",
21    kind = ToDevice,
22)]
23pub struct ToDeviceStreamUpdateEventContent {
24    /// The room containing the stream descriptor.
25    pub room_id: OwnedRoomId,
26
27    /// The event containing the stream descriptor.
28    pub event_id: OwnedEventId,
29
30    /// A monotonically increasing sequence number for this subscriber
31    /// device's view of the stream.
32    ///
33    /// Clients should ignore updates whose `seq` is less than or equal to the
34    /// latest sequence number already applied for this stream.
35    pub seq: UInt,
36
37    /// The update operation.
38    #[serde(flatten)]
39    pub operation: StreamUpdateOperation,
40}
41
42impl ToDeviceStreamUpdateEventContent {
43    /// Creates a new `ToDeviceStreamUpdateEventContent` with the given room,
44    /// event, sequence number, and operation.
45    pub fn new(
46        room_id: OwnedRoomId,
47        event_id: OwnedEventId,
48        seq: UInt,
49        operation: StreamUpdateOperation,
50    ) -> Self {
51        Self { room_id, event_id, seq, operation }
52    }
53}
54
55/// A stream update operation.
56#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
57#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
58#[serde(tag = "op", content = "content", rename_all = "snake_case")]
59pub enum StreamUpdateOperation {
60    /// Replace the current body with the payload.
61    Replace(StreamUpdateContent),
62
63    /// Append the payload to the current body.
64    Append(StreamUpdateContent),
65}
66
67/// The payload of a message-like stream update.
68#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
69#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
70pub struct StreamUpdateContent {
71    /// Text for the operation.
72    pub body: String,
73}
74
75impl StreamUpdateContent {
76    /// Creates a new `StreamUpdateContent` with the given body.
77    pub fn new(body: String) -> Self {
78        Self { body }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use assert_matches2::assert_matches;
85    use js_int::uint;
86    use ruma_common::{
87        canonical_json::assert_to_canonical_json_eq, owned_event_id, owned_room_id, serde::Raw,
88    };
89    use serde_json::{from_value as from_json_value, json};
90
91    use super::{StreamUpdateContent, StreamUpdateOperation, ToDeviceStreamUpdateEventContent};
92    use crate::{AnyToDeviceEvent, ToDeviceEvent};
93
94    #[test]
95    fn replace_update_round_trip() {
96        let content = ToDeviceStreamUpdateEventContent::new(
97            owned_room_id!("!room:example.org"),
98            owned_event_id!("$event:example.org"),
99            uint!(1),
100            StreamUpdateOperation::Replace(StreamUpdateContent::new("hello".to_owned())),
101        );
102
103        assert_to_canonical_json_eq!(
104            content,
105            json!({
106                "room_id": "!room:example.org",
107                "event_id": "$event:example.org",
108                "seq": 1,
109                "op": "replace",
110                "content": {
111                    "body": "hello",
112                },
113            })
114        );
115
116        let deserialized: ToDeviceStreamUpdateEventContent =
117            Raw::new(&content).unwrap().deserialize().unwrap();
118        assert_eq!(deserialized.seq, uint!(1));
119        assert_matches!(deserialized.operation, StreamUpdateOperation::Replace(payload));
120        assert_eq!(payload.body, "hello");
121    }
122
123    #[test]
124    fn replace_update_seq_zero_round_trip() {
125        let content = ToDeviceStreamUpdateEventContent::new(
126            owned_room_id!("!room:example.org"),
127            owned_event_id!("$event:example.org"),
128            uint!(0),
129            StreamUpdateOperation::Replace(StreamUpdateContent::new("hello".to_owned())),
130        );
131
132        assert_to_canonical_json_eq!(
133            content,
134            json!({
135                "room_id": "!room:example.org",
136                "event_id": "$event:example.org",
137                "seq": 0,
138                "op": "replace",
139                "content": {
140                    "body": "hello",
141                },
142            })
143        );
144
145        let deserialized: ToDeviceStreamUpdateEventContent =
146            Raw::new(&content).unwrap().deserialize().unwrap();
147        assert_eq!(deserialized.seq, uint!(0));
148        assert_matches!(deserialized.operation, StreamUpdateOperation::Replace(payload));
149        assert_eq!(payload.body, "hello");
150    }
151
152    #[test]
153    fn append_update_round_trip() {
154        let content = ToDeviceStreamUpdateEventContent::new(
155            owned_room_id!("!room:example.org"),
156            owned_event_id!("$event:example.org"),
157            uint!(2),
158            StreamUpdateOperation::Append(StreamUpdateContent::new(" world".to_owned())),
159        );
160
161        assert_to_canonical_json_eq!(
162            content,
163            json!({
164                "room_id": "!room:example.org",
165                "event_id": "$event:example.org",
166                "seq": 2,
167                "op": "append",
168                "content": {
169                    "body": " world",
170                },
171            })
172        );
173    }
174
175    #[test]
176    fn any_to_device_update() {
177        let event = json!({
178            "sender": "@alice:example.org",
179            "type": "org.matrix.msc4471.stream.update",
180            "content": {
181                "room_id": "!room:example.org",
182                "event_id": "$event:example.org",
183                "seq": 1,
184                "op": "replace",
185                "content": {
186                    "body": "hello",
187                },
188            },
189        });
190
191        let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
192        assert_matches!(event, AnyToDeviceEvent::StreamUpdate(ToDeviceEvent { content, .. }));
193        assert_matches!(content.operation, StreamUpdateOperation::Replace(payload));
194        assert_eq!(payload.body, "hello");
195    }
196
197    #[test]
198    fn any_to_device_update_stable_alias() {
199        let event = json!({
200            "sender": "@alice:example.org",
201            "type": "m.stream.update",
202            "content": {
203                "room_id": "!room:example.org",
204                "event_id": "$event:example.org",
205                "seq": 1,
206                "op": "replace",
207                "content": {
208                    "body": "hello",
209                },
210            },
211        });
212
213        let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
214        assert_matches!(event, AnyToDeviceEvent::StreamUpdate(ToDeviceEvent { content, .. }));
215        assert_matches!(content.operation, StreamUpdateOperation::Replace(payload));
216        assert_eq!(payload.body, "hello");
217    }
218}