Skip to main content

ruma_events/stream/
cancel.rs

1//! Types for the `m.stream.cancel` to-device event ([MSC4471]).
2//!
3//! [MSC4471]: https://github.com/matrix-org/matrix-spec-proposals/pull/4471
4
5use ruma_common::{OwnedDeviceId, OwnedEventId, OwnedRoomId, serde::StringEnum};
6use ruma_macros::EventContent;
7use serde::{Deserialize, Serialize};
8
9use crate::PrivOwnedStr;
10
11/// The content of a to-device `m.stream.cancel` event.
12///
13/// Sent by either side of a subscription to cancel it.
14#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
15#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
16#[ruma_event(
17    type = "org.matrix.msc4471.stream.cancel",
18    alias = "m.stream.cancel",
19    kind = ToDevice,
20)]
21pub struct ToDeviceStreamCancelEventContent {
22    /// The room containing the stream descriptor.
23    pub room_id: OwnedRoomId,
24
25    /// The event containing the stream descriptor.
26    pub event_id: OwnedEventId,
27
28    /// The subscriber device whose subscription is cancelled.
29    pub subscriber_device_id: OwnedDeviceId,
30
31    /// A machine-readable cancellation code.
32    pub code: StreamCancelCode,
33
34    /// A human-readable reason for debugging.
35    ///
36    /// Clients should not rely on this value.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub reason: Option<String>,
39}
40
41impl ToDeviceStreamCancelEventContent {
42    /// Creates a new `ToDeviceStreamCancelEventContent` with the given room,
43    /// event, subscriber device, and code.
44    pub fn new(
45        room_id: OwnedRoomId,
46        event_id: OwnedEventId,
47        subscriber_device_id: OwnedDeviceId,
48        code: StreamCancelCode,
49    ) -> Self {
50        Self { room_id, event_id, subscriber_device_id, code, reason: None }
51    }
52}
53
54/// A machine-readable cancellation code for an event stream subscription.
55///
56/// Custom error codes should use the Java package naming convention.
57#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
58#[derive(Clone, StringEnum)]
59#[ruma_enum(rename_all(prefix = "m.", rule = "snake_case"))]
60#[non_exhaustive]
61pub enum StreamCancelCode {
62    /// The publisher has no active stream for the requested descriptor, or the descriptor
63    /// has expired.
64    UnknownStream,
65
66    /// The subscription request is malformed or names an invalid subscriber device.
67    InvalidSubscription,
68
69    /// The subscriber is not allowed to receive updates.
70    Forbidden,
71
72    /// The publisher has hit an implementation limit.
73    LimitExceeded,
74
75    /// The subscriber no longer wants updates.
76    UserCancelled,
77
78    #[doc(hidden)]
79    _Custom(PrivOwnedStr),
80}
81
82#[cfg(test)]
83mod tests {
84    use assert_matches2::assert_matches;
85    use ruma_common::{
86        canonical_json::assert_to_canonical_json_eq, owned_device_id, owned_event_id, owned_room_id,
87    };
88    use serde_json::{from_value as from_json_value, json};
89
90    use super::{StreamCancelCode, ToDeviceStreamCancelEventContent};
91    use crate::{AnyToDeviceEvent, ToDeviceEvent};
92
93    #[test]
94    fn cancel_round_trip() {
95        let mut content = ToDeviceStreamCancelEventContent::new(
96            owned_room_id!("!room:example.org"),
97            owned_event_id!("$event:example.org"),
98            owned_device_id!("SUBSCRIBERDEVICE"),
99            StreamCancelCode::UnknownStream,
100        );
101        content.reason = Some("because".to_owned());
102
103        assert_to_canonical_json_eq!(
104            content,
105            json!({
106                "room_id": "!room:example.org",
107                "event_id": "$event:example.org",
108                "subscriber_device_id": "SUBSCRIBERDEVICE",
109                "code": "m.unknown_stream",
110                "reason": "because",
111            })
112        );
113    }
114
115    #[test]
116    fn cancel_code_serialization() {
117        for (variant, expected) in [
118            (StreamCancelCode::UnknownStream, "m.unknown_stream"),
119            (StreamCancelCode::InvalidSubscription, "m.invalid_subscription"),
120            (StreamCancelCode::Forbidden, "m.forbidden"),
121            (StreamCancelCode::LimitExceeded, "m.limit_exceeded"),
122            (StreamCancelCode::UserCancelled, "m.user_cancelled"),
123        ] {
124            assert_to_canonical_json_eq!(variant, json!(expected));
125        }
126    }
127
128    #[test]
129    fn cancel_code_deserialization() {
130        for (s, expected) in [
131            ("m.unknown_stream", StreamCancelCode::UnknownStream),
132            ("m.invalid_subscription", StreamCancelCode::InvalidSubscription),
133            ("m.forbidden", StreamCancelCode::Forbidden),
134            ("m.limit_exceeded", StreamCancelCode::LimitExceeded),
135            ("m.user_cancelled", StreamCancelCode::UserCancelled),
136        ] {
137            let code = from_json_value::<StreamCancelCode>(json!(s)).unwrap();
138            assert_eq!(code, expected);
139        }
140    }
141
142    #[test]
143    fn unknown_m_namespace_cancel_code_goes_to_custom() {
144        let code = from_json_value::<StreamCancelCode>(json!("m.future_code")).unwrap();
145        assert_to_canonical_json_eq!(code, json!("m.future_code"));
146        assert_ne!(code, StreamCancelCode::UnknownStream);
147        assert_ne!(code, StreamCancelCode::InvalidSubscription);
148        assert_ne!(code, StreamCancelCode::Forbidden);
149        assert_ne!(code, StreamCancelCode::LimitExceeded);
150        assert_ne!(code, StreamCancelCode::UserCancelled);
151    }
152
153    #[test]
154    fn custom_cancel_code_round_trips() {
155        let code = from_json_value::<StreamCancelCode>(json!("io.example.custom_reason")).unwrap();
156        assert_to_canonical_json_eq!(code, json!("io.example.custom_reason"));
157    }
158
159    #[test]
160    fn any_to_device_cancel() {
161        let event = json!({
162            "sender": "@alice:example.org",
163            "type": "org.matrix.msc4471.stream.cancel",
164            "content": {
165                "room_id": "!room:example.org",
166                "event_id": "$event:example.org",
167                "subscriber_device_id": "SUBSCRIBERDEVICE",
168                "code": "m.user_cancelled",
169            },
170        });
171
172        let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
173        assert_matches!(event, AnyToDeviceEvent::StreamCancel(ToDeviceEvent { content, .. }));
174        assert_eq!(content.code, StreamCancelCode::UserCancelled);
175    }
176
177    #[test]
178    fn any_to_device_cancel_stable_alias() {
179        let event = json!({
180            "sender": "@alice:example.org",
181            "type": "m.stream.cancel",
182            "content": {
183                "room_id": "!room:example.org",
184                "event_id": "$event:example.org",
185                "subscriber_device_id": "SUBSCRIBERDEVICE",
186                "code": "m.user_cancelled",
187            },
188        });
189
190        let event = from_json_value::<AnyToDeviceEvent>(event).unwrap();
191        assert_matches!(event, AnyToDeviceEvent::StreamCancel(ToDeviceEvent { content, .. }));
192        assert_eq!(content.code, StreamCancelCode::UserCancelled);
193    }
194}