Skip to main content

ruma_events/space/
child.rs

1//! Types for the [`m.space.child`] event.
2//!
3//! [`m.space.child`]: https://spec.matrix.org/latest/client-server-api/#mspacechild
4
5use std::{cmp::Ordering, ops::Deref};
6
7use ruma_common::{
8    MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedServerName, OwnedSpaceChildOrder, OwnedUserId,
9    RoomId, SpaceChildOrder,
10    serde::{JsonCastable, JsonObject},
11};
12use ruma_macros::{Event, EventContent};
13use serde::{Deserialize, Serialize};
14
15use crate::{StateEvent, SyncStateEvent};
16
17/// The content of an `m.space.child` event.
18///
19/// The admins of a space can advertise rooms and subspaces for their space by setting
20/// `m.space.child` state events.
21///
22/// The `state_key` is the ID of a child room or space, and the content must contain a `via` key
23/// which gives a list of candidate servers that can be used to join the room.
24#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
25#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
26#[ruma_event(type = "m.space.child", kind = State, state_key_type = OwnedRoomId)]
27pub struct SpaceChildEventContent {
28    /// List of candidate servers that can be used to join the room.
29    pub via: Vec<OwnedServerName>,
30
31    /// Provide a default ordering of siblings in the room list.
32    ///
33    /// Rooms are sorted based on a lexicographic ordering of the Unicode codepoints of the
34    /// characters in `order` values. Rooms with no `order` come last, in ascending numeric order
35    /// of the origin_server_ts of their m.room.create events, or ascending lexicographic order of
36    /// their room_ids in case of equal `origin_server_ts`. `order`s which are not strings, or do
37    /// not consist solely of ascii characters in the range `\x20` (space) to `\x7E` (`~`), or
38    /// consist of more than 50 characters, are forbidden and the field should be ignored if
39    /// received.
40    ///
41    /// During deserialization, this field is set to `None` if it is invalid.
42    #[serde(
43        default,
44        deserialize_with = "ruma_common::serde::default_on_error",
45        skip_serializing_if = "Option::is_none"
46    )]
47    pub order: Option<OwnedSpaceChildOrder>,
48
49    /// Space admins can mark particular children of a space as "suggested".
50    ///
51    /// This mainly serves as a hint to clients that that they can be displayed differently, for
52    /// example by showing them eagerly in the room list. A child which is missing the `suggested`
53    /// property is treated identically to a child with `"suggested": false`. A suggested child may
54    /// be a room or a subspace.
55    ///
56    /// Defaults to `false`.
57    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
58    pub suggested: bool,
59}
60
61impl SpaceChildEventContent {
62    /// Creates a new `SpaceChildEventContent` with the given routing servers.
63    pub fn new(via: Vec<OwnedServerName>) -> Self {
64        Self { via, order: None, suggested: false }
65    }
66}
67
68impl PossiblyRedactedSpaceChildEventContent {
69    /// Whether this `PossiblyRedactedSpaceChildEventContent` is valid according to the Matrix
70    /// specification.
71    ///
72    /// The room in the state key of the event should only be considered a child of this space
73    /// if this returns `true`.
74    ///
75    /// Returns `false` if the `via` field is `None`.
76    pub fn is_valid(&self) -> bool {
77        self.via.is_some()
78    }
79}
80
81/// An `m.space.child` event represented as a Stripped State Event with an added `origin_server_ts`
82/// key.
83#[derive(Clone, Debug, Event)]
84#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
85pub struct HierarchySpaceChildEvent {
86    /// The content of the space child event.
87    pub content: SpaceChildEventContent,
88
89    /// The fully-qualified ID of the user who sent this event.
90    pub sender: OwnedUserId,
91
92    /// The room ID of the child.
93    pub state_key: OwnedRoomId,
94
95    /// Timestamp in milliseconds on originating homeserver when this event was sent.
96    pub origin_server_ts: MilliSecondsSinceUnixEpoch,
97}
98
99impl PartialEq for HierarchySpaceChildEvent {
100    fn eq(&self, other: &Self) -> bool {
101        self.space_child_ord_fields().eq(&other.space_child_ord_fields())
102    }
103}
104
105impl Eq for HierarchySpaceChildEvent {}
106
107impl Ord for HierarchySpaceChildEvent {
108    fn cmp(&self, other: &Self) -> Ordering {
109        self.space_child_ord_fields().cmp(&other.space_child_ord_fields())
110    }
111}
112
113impl PartialOrd for HierarchySpaceChildEvent {
114    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
115        Some(self.cmp(other))
116    }
117}
118
119impl JsonCastable<HierarchySpaceChildEvent> for SpaceChildEvent {}
120
121impl JsonCastable<HierarchySpaceChildEvent> for OriginalSpaceChildEvent {}
122
123impl JsonCastable<HierarchySpaceChildEvent> for SyncSpaceChildEvent {}
124
125impl JsonCastable<HierarchySpaceChildEvent> for OriginalSyncSpaceChildEvent {}
126
127impl JsonCastable<JsonObject> for HierarchySpaceChildEvent {}
128
129/// Helper trait to sort `m.space.child` events using using the algorithm for [ordering children
130/// within a space].
131///
132/// This trait can be used to sort a slice using `.sort_by(SpaceChildOrd::cmp_space_child)`. It is
133/// also possible to use [`SpaceChildOrdHelper`] to sort the events in a `BTreeMap` or a `BTreeSet`.
134///
135/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
136pub trait SpaceChildOrd {
137    #[doc(hidden)]
138    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_>;
139
140    /// Return an [`Ordering`] between `self` and `other`, using the algorithm for [ordering
141    /// children within a space].
142    ///
143    /// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
144    fn cmp_space_child(&self, other: &impl SpaceChildOrd) -> Ordering {
145        self.space_child_ord_fields().cmp(&other.space_child_ord_fields())
146    }
147}
148
149/// Fields necessary to implement `Ord` for space child events using the algorithm for [ordering
150/// children within a space].
151///
152/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
153#[doc(hidden)]
154#[derive(PartialEq, Eq)]
155pub struct SpaceChildOrdFields<'a> {
156    order: Option<&'a SpaceChildOrder>,
157    origin_server_ts: MilliSecondsSinceUnixEpoch,
158    state_key: &'a RoomId,
159}
160
161impl<'a> SpaceChildOrdFields<'a> {
162    /// Construct a new `SpaceChildEventOrdFields` with the given values.
163    ///
164    /// Filters the order if it is invalid.
165    fn new(
166        order: Option<&'a SpaceChildOrder>,
167        origin_server_ts: MilliSecondsSinceUnixEpoch,
168        state_key: &'a RoomId,
169    ) -> Self {
170        Self { order, origin_server_ts, state_key }
171    }
172}
173
174impl<'a> Ord for SpaceChildOrdFields<'a> {
175    fn cmp(&self, other: &Self) -> Ordering {
176        match (self.order, other.order) {
177            // Events with order are ordered before events without order.
178            (Some(_), None) => Ordering::Less,
179            (None, Some(_)) => Ordering::Greater,
180            (Some(self_order), Some(other_order)) => self_order.cmp(other_order),
181            (None, None) => Ordering::Equal,
182        }
183        .then_with(|| self.origin_server_ts.cmp(&other.origin_server_ts))
184        .then_with(|| self.state_key.cmp(other.state_key))
185    }
186}
187
188impl<'a> PartialOrd for SpaceChildOrdFields<'a> {
189    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
190        Some(self.cmp(other))
191    }
192}
193
194impl<T> SpaceChildOrd for &T
195where
196    T: SpaceChildOrd,
197{
198    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
199        (*self).space_child_ord_fields()
200    }
201}
202
203impl SpaceChildOrd for OriginalSpaceChildEvent {
204    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
205        SpaceChildOrdFields::new(
206            self.content.order.as_deref(),
207            self.origin_server_ts,
208            &self.state_key,
209        )
210    }
211}
212
213impl SpaceChildOrd for RedactedSpaceChildEvent {
214    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
215        SpaceChildOrdFields::new(None, self.origin_server_ts, &self.state_key)
216    }
217}
218
219impl SpaceChildOrd for SpaceChildEvent {
220    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
221        match self {
222            StateEvent::Original(original) => original.space_child_ord_fields(),
223            StateEvent::Redacted(redacted) => redacted.space_child_ord_fields(),
224        }
225    }
226}
227
228impl SpaceChildOrd for OriginalSyncSpaceChildEvent {
229    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
230        SpaceChildOrdFields::new(
231            self.content.order.as_deref(),
232            self.origin_server_ts,
233            &self.state_key,
234        )
235    }
236}
237
238impl SpaceChildOrd for RedactedSyncSpaceChildEvent {
239    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
240        SpaceChildOrdFields::new(None, self.origin_server_ts, &self.state_key)
241    }
242}
243
244impl SpaceChildOrd for SyncSpaceChildEvent {
245    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
246        match self {
247            SyncStateEvent::Original(original) => original.space_child_ord_fields(),
248            SyncStateEvent::Redacted(redacted) => redacted.space_child_ord_fields(),
249        }
250    }
251}
252
253impl SpaceChildOrd for HierarchySpaceChildEvent {
254    fn space_child_ord_fields(&self) -> SpaceChildOrdFields<'_> {
255        SpaceChildOrdFields::new(
256            self.content.order.as_deref(),
257            self.origin_server_ts,
258            &self.state_key,
259        )
260    }
261}
262
263/// Helper type to sort `m.space.child` events using using the algorithm for [ordering children
264/// within a space].
265///
266/// This type can be use with `BTreeMap` or `BTreeSet` to order space child events.
267///
268/// [ordering children within a space]: https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space
269#[derive(Debug, Clone)]
270#[allow(clippy::exhaustive_structs)]
271pub struct SpaceChildOrdHelper<T: SpaceChildOrd>(pub T);
272
273impl<T: SpaceChildOrd> PartialEq for SpaceChildOrdHelper<T> {
274    fn eq(&self, other: &Self) -> bool {
275        self.0.space_child_ord_fields().eq(&other.0.space_child_ord_fields())
276    }
277}
278
279impl<T: SpaceChildOrd> Eq for SpaceChildOrdHelper<T> {}
280
281impl<T: SpaceChildOrd> Ord for SpaceChildOrdHelper<T> {
282    fn cmp(&self, other: &Self) -> Ordering {
283        self.0.cmp_space_child(&other.0)
284    }
285}
286
287impl<T: SpaceChildOrd> PartialOrd for SpaceChildOrdHelper<T> {
288    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
289        Some(self.cmp(other))
290    }
291}
292
293impl<T: SpaceChildOrd> Deref for SpaceChildOrdHelper<T> {
294    type Target = T;
295
296    fn deref(&self) -> &Self::Target {
297        &self.0
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use std::{collections::BTreeSet, iter::repeat_n};
304
305    use js_int::{UInt, uint};
306    use ruma_common::{
307        MilliSecondsSinceUnixEpoch, OwnedRoomId, SpaceChildOrder,
308        canonical_json::assert_to_canonical_json_eq, owned_room_id, owned_server_name,
309        owned_user_id, server_name,
310    };
311    use serde_json::{from_value as from_json_value, json};
312
313    use super::{
314        HierarchySpaceChildEvent, SpaceChildEventContent, SpaceChildOrd, SpaceChildOrdHelper,
315    };
316
317    #[test]
318    fn space_child_serialization() {
319        let content = SpaceChildEventContent {
320            via: vec![owned_server_name!("example.com")],
321            order: Some(SpaceChildOrder::parse("uwu").unwrap()),
322            suggested: false,
323        };
324
325        assert_to_canonical_json_eq!(
326            content,
327            json!({
328                "via": ["example.com"],
329                "order": "uwu",
330            }),
331        );
332    }
333
334    #[test]
335    fn space_child_empty_serialization() {
336        let content = SpaceChildEventContent { via: vec![], order: None, suggested: false };
337
338        assert_to_canonical_json_eq!(content, json!({ "via": [] }));
339    }
340
341    #[test]
342    fn space_child_content_deserialization_order() {
343        let via = server_name!("localhost");
344
345        // Valid string.
346        let json = json!({
347            "order": "aaa",
348            "via": [via],
349        });
350        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
351        assert_eq!(content.order.unwrap(), "aaa");
352        assert!(!content.suggested);
353        assert_eq!(content.via, &[via]);
354
355        // Not a string.
356        let json = json!({
357            "order": 2,
358            "via": [via],
359        });
360        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
361        assert_eq!(content.order, None);
362        assert!(!content.suggested);
363        assert_eq!(content.via, &[via]);
364
365        // Empty string.
366        let json = json!({
367            "order": "",
368            "via": [via],
369        });
370        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
371        assert_eq!(content.order.unwrap(), "");
372        assert!(!content.suggested);
373        assert_eq!(content.via, &[via]);
374
375        // String too long.
376        let order = repeat_n('a', 60).collect::<String>();
377        let json = json!({
378            "order": order,
379            "via": [via],
380        });
381        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
382        assert_eq!(content.order, None);
383        assert!(!content.suggested);
384        assert_eq!(content.via, &[via]);
385
386        // Invalid character.
387        let json = json!({
388            "order": "🔝",
389            "via": [via],
390        });
391        let content = from_json_value::<SpaceChildEventContent>(json).unwrap();
392        assert_eq!(content.order, None);
393        assert!(!content.suggested);
394        assert_eq!(content.via, &[via]);
395    }
396
397    #[test]
398    fn hierarchy_space_child_deserialization() {
399        let json = json!({
400            "content": {
401                "via": [
402                    "example.org"
403                ]
404            },
405            "origin_server_ts": 1_629_413_349,
406            "sender": "@alice:example.org",
407            "state_key": "!a:example.org",
408            "type": "m.space.child"
409        });
410
411        let ev = from_json_value::<HierarchySpaceChildEvent>(json).unwrap();
412        assert_eq!(ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1_629_413_349)));
413        assert_eq!(ev.sender, "@alice:example.org");
414        assert_eq!(ev.state_key, "!a:example.org");
415        assert_eq!(ev.content.via, ["example.org"]);
416        assert_eq!(ev.content.order, None);
417        assert!(!ev.content.suggested);
418    }
419
420    /// Construct a [`HierarchySpaceChildEvent`] with the given state key, order and timestamp.
421    fn hierarchy_space_child_event(
422        state_key: OwnedRoomId,
423        order: Option<&str>,
424        origin_server_ts: UInt,
425    ) -> HierarchySpaceChildEvent {
426        let mut content = SpaceChildEventContent::new(vec![owned_server_name!("example.org")]);
427        content.order = order.and_then(|order| SpaceChildOrder::parse(order).ok());
428
429        HierarchySpaceChildEvent {
430            content,
431            sender: owned_user_id!("@alice:example.org"),
432            state_key,
433            origin_server_ts: MilliSecondsSinceUnixEpoch(origin_server_ts),
434        }
435    }
436
437    #[test]
438    fn space_child_ord_spec_example() {
439        // Reproduce the example from the spec.
440        let child_a = hierarchy_space_child_event(
441            owned_room_id!("!a:example.org"),
442            Some("aaaa"),
443            uint!(1_640_141_000),
444        );
445        let child_b = hierarchy_space_child_event(
446            owned_room_id!("!b:example.org"),
447            Some(" "),
448            uint!(1_640_341_000),
449        );
450        let child_c = hierarchy_space_child_event(
451            owned_room_id!("!c:example.org"),
452            Some("first"),
453            uint!(1_640_841_000),
454        );
455        let child_d = hierarchy_space_child_event(
456            owned_room_id!("!d:example.org"),
457            None,
458            uint!(1_640_741_000),
459        );
460        let child_e = hierarchy_space_child_event(
461            owned_room_id!("!e:example.org"),
462            None,
463            uint!(1_640_641_000),
464        );
465
466        let events =
467            [child_a.clone(), child_b.clone(), child_c.clone(), child_d.clone(), child_e.clone()];
468
469        // Using slice::sort_by.
470        let mut sorted_events = events.clone();
471        sorted_events.sort_by(SpaceChildOrd::cmp_space_child);
472        assert_eq!(sorted_events[0].state_key, child_b.state_key);
473        assert_eq!(sorted_events[1].state_key, child_a.state_key);
474        assert_eq!(sorted_events[2].state_key, child_c.state_key);
475        assert_eq!(sorted_events[3].state_key, child_e.state_key);
476        assert_eq!(sorted_events[4].state_key, child_d.state_key);
477
478        // Using BTreeSet.
479        let sorted_events = events.clone().into_iter().collect::<BTreeSet<_>>();
480        let mut iter = sorted_events.iter();
481        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
482        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
483        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
484        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
485        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
486
487        // Using BTreeSet and helper.
488        let sorted_events = events.into_iter().map(SpaceChildOrdHelper).collect::<BTreeSet<_>>();
489        let mut iter = sorted_events.iter();
490        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
491        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
492        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
493        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
494        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
495    }
496
497    #[test]
498    fn space_child_ord_other_example() {
499        // We also check invalid order and state key comparison here.
500        let child_a = hierarchy_space_child_event(
501            owned_room_id!("!a:example.org"),
502            Some("🔝"),
503            uint!(1_640_141_000),
504        );
505        let child_b = hierarchy_space_child_event(
506            owned_room_id!("!b:example.org"),
507            Some(" "),
508            uint!(1_640_341_000),
509        );
510        let child_c = hierarchy_space_child_event(
511            owned_room_id!("!c:example.org"),
512            None,
513            uint!(1_640_841_000),
514        );
515        let child_d = hierarchy_space_child_event(
516            owned_room_id!("!d:example.org"),
517            None,
518            uint!(1_640_741_000),
519        );
520        let child_e = hierarchy_space_child_event(
521            owned_room_id!("!e:example.org"),
522            None,
523            uint!(1_640_741_000),
524        );
525
526        let mut events =
527            [child_a.clone(), child_b.clone(), child_c.clone(), child_d.clone(), child_e.clone()];
528
529        events.sort_by(SpaceChildOrd::cmp_space_child);
530
531        // Using slice::sort_by.
532        let mut sorted_events = events.clone();
533        sorted_events.sort_by(SpaceChildOrd::cmp_space_child);
534        assert_eq!(sorted_events[0].state_key, child_b.state_key);
535        assert_eq!(sorted_events[1].state_key, child_a.state_key);
536        assert_eq!(sorted_events[2].state_key, child_d.state_key);
537        assert_eq!(sorted_events[3].state_key, child_e.state_key);
538        assert_eq!(sorted_events[4].state_key, child_c.state_key);
539
540        // Using BTreeSet.
541        let sorted_events = events.clone().into_iter().collect::<BTreeSet<_>>();
542        let mut iter = sorted_events.iter();
543        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
544        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
545        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
546        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
547        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
548
549        // Using BTreeSet and helper.
550        let sorted_events = events.into_iter().map(SpaceChildOrdHelper).collect::<BTreeSet<_>>();
551        let mut iter = sorted_events.iter();
552        assert_eq!(iter.next().unwrap().state_key, child_b.state_key);
553        assert_eq!(iter.next().unwrap().state_key, child_a.state_key);
554        assert_eq!(iter.next().unwrap().state_key, child_d.state_key);
555        assert_eq!(iter.next().unwrap().state_key, child_e.state_key);
556        assert_eq!(iter.next().unwrap().state_key, child_c.state_key);
557    }
558}