Skip to main content

ruma_common/
directory.rs

1//! Common types for room directory endpoints.
2
3use js_int::UInt;
4use serde::{Deserialize, Serialize};
5
6mod filter_room_type_serde;
7mod room_network_serde;
8
9use crate::{
10    OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
11    room::{JoinRuleKind, RoomSummary, RoomType},
12};
13
14/// A chunk of a room list response, describing one room.
15///
16/// To create an instance of this type, first create a [`PublicRoomsChunkInit`] and convert it via
17/// `PublicRoomsChunk::from` / `.into()`. It is also possible to construct this type from or convert
18/// it to a [`RoomSummary`].
19#[derive(Clone, Debug, Deserialize, Serialize)]
20#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
21pub struct PublicRoomsChunk {
22    /// The canonical alias of the room, if any.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    #[cfg_attr(
25        feature = "compat-empty-string-null",
26        serde(default, deserialize_with = "crate::serde::empty_string_as_none")
27    )]
28    pub canonical_alias: Option<OwnedRoomAliasId>,
29
30    /// The name of the room, if any.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub name: Option<String>,
33
34    /// The number of members joined to the room.
35    pub num_joined_members: UInt,
36
37    /// The ID of the room.
38    pub room_id: OwnedRoomId,
39
40    /// The topic of the room, if any.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub topic: Option<String>,
43
44    /// Whether the room may be viewed by guest users without joining.
45    pub world_readable: bool,
46
47    /// Whether guest users may join the room and participate in it.
48    ///
49    /// If they can, they will be subject to ordinary power level rules like any other user.
50    pub guest_can_join: bool,
51
52    /// The URL for the room's avatar, if one is set.
53    ///
54    /// If you activate the `compat-empty-string-null` feature, this field being an empty string in
55    /// JSON will result in `None` here during deserialization.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    #[cfg_attr(
58        feature = "compat-empty-string-null",
59        serde(default, deserialize_with = "crate::serde::empty_string_as_none")
60    )]
61    pub avatar_url: Option<OwnedMxcUri>,
62
63    /// The join rule of the room.
64    #[serde(default, skip_serializing_if = "crate::serde::is_default")]
65    pub join_rule: JoinRuleKind,
66
67    /// The type of room from `m.room.create`, if any.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub room_type: Option<RoomType>,
70}
71
72/// Initial set of mandatory fields of `PublicRoomsChunk`.
73///
74/// This struct will not be updated even if additional fields are added to `PublicRoomsChunk` in a
75/// new (non-breaking) release of the Matrix specification.
76#[derive(Debug)]
77#[allow(clippy::exhaustive_structs)]
78pub struct PublicRoomsChunkInit {
79    /// The number of members joined to the room.
80    pub num_joined_members: UInt,
81
82    /// The ID of the room.
83    pub room_id: OwnedRoomId,
84
85    /// Whether the room may be viewed by guest users without joining.
86    pub world_readable: bool,
87
88    /// Whether guest users may join the room and participate in it.
89    ///
90    /// If they can, they will be subject to ordinary power level rules like any other user.
91    pub guest_can_join: bool,
92}
93
94impl From<PublicRoomsChunkInit> for PublicRoomsChunk {
95    fn from(init: PublicRoomsChunkInit) -> Self {
96        let PublicRoomsChunkInit { num_joined_members, room_id, world_readable, guest_can_join } =
97            init;
98
99        Self {
100            canonical_alias: None,
101            name: None,
102            num_joined_members,
103            room_id,
104            topic: None,
105            world_readable,
106            guest_can_join,
107            avatar_url: None,
108            join_rule: JoinRuleKind::default(),
109            room_type: None,
110        }
111    }
112}
113
114impl From<RoomSummary> for PublicRoomsChunk {
115    fn from(value: RoomSummary) -> Self {
116        let RoomSummary {
117            room_id,
118            canonical_alias,
119            name,
120            topic,
121            avatar_url,
122            room_type,
123            num_joined_members,
124            join_rule,
125            world_readable,
126            guest_can_join,
127            ..
128        } = value;
129
130        Self {
131            canonical_alias,
132            name,
133            num_joined_members,
134            room_id,
135            topic,
136            world_readable,
137            guest_can_join,
138            avatar_url,
139            join_rule: join_rule.as_str().into(),
140            room_type,
141        }
142    }
143}
144
145impl From<PublicRoomsChunk> for RoomSummary {
146    fn from(value: PublicRoomsChunk) -> Self {
147        let PublicRoomsChunk {
148            room_id,
149            canonical_alias,
150            name,
151            topic,
152            avatar_url,
153            room_type,
154            num_joined_members,
155            join_rule,
156            world_readable,
157            guest_can_join,
158        } = value;
159
160        Self {
161            canonical_alias,
162            name,
163            num_joined_members,
164            room_id,
165            topic,
166            world_readable,
167            guest_can_join,
168            avatar_url,
169            join_rule: join_rule.into(),
170            room_type,
171            encryption: None,
172            room_version: None,
173        }
174    }
175}
176
177/// A filter for public rooms lists.
178#[derive(Clone, Debug, Default, Deserialize, Serialize)]
179#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
180pub struct Filter {
181    /// A string to search for in the room metadata, e.g. name, topic, canonical alias etc.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub generic_search_term: Option<String>,
184
185    /// The room types to include in the results.
186    ///
187    /// Includes all room types if it is empty.
188    ///
189    /// If the `compat-null` feature is enabled, a `null` value is allowed in deserialization, and
190    /// treated the same way as an empty list.
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    #[cfg_attr(feature = "compat-null", serde(deserialize_with = "crate::serde::none_as_default"))]
193    pub room_types: Vec<RoomTypeFilter>,
194}
195
196impl Filter {
197    /// Creates an empty `Filter`.
198    pub fn new() -> Self {
199        Default::default()
200    }
201
202    /// Returns `true` if the filter is empty.
203    pub fn is_empty(&self) -> bool {
204        self.generic_search_term.is_none()
205    }
206}
207
208/// Information about which networks/protocols from application services on the
209/// homeserver from which to request rooms.
210#[derive(Clone, Debug, Default, PartialEq, Eq)]
211#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
212pub enum RoomNetwork {
213    /// Return rooms from the Matrix network.
214    #[default]
215    Matrix,
216
217    /// Return rooms from all the networks/protocols the homeserver knows about.
218    All,
219
220    /// Return rooms from a specific third party network/protocol.
221    ThirdParty(String),
222}
223
224/// An enum of possible room types to filter.
225///
226/// This type can hold an arbitrary string. To build this with a custom value, convert it from an
227/// `Option<string>` with `::from()` / `.into()`. [`RoomTypeFilter::Default`] can be constructed
228/// from `None`.
229///
230/// To check for values that are not available as a documented variant here, use its string
231/// representation, obtained through [`.as_str()`](Self::as_str()).
232#[derive(Clone, Debug, PartialEq, Eq)]
233#[non_exhaustive]
234pub enum RoomTypeFilter {
235    /// The default room type, defined without a `room_type`.
236    Default,
237
238    /// A space.
239    Space,
240
241    /// A call room as specified in [MSC3417].
242    ///
243    /// [MSC3417]: <https://github.com/matrix-org/matrix-spec-proposals/pull/3417>
244    #[cfg(feature = "unstable-msc3417")]
245    Call,
246
247    /// A custom room type.
248    #[doc(hidden)]
249    _Custom(PrivOwnedStr),
250}
251
252impl RoomTypeFilter {
253    /// Get the string representation of this `RoomTypeFilter`.
254    ///
255    /// [`RoomTypeFilter::Default`] returns `None`.
256    pub fn as_str(&self) -> Option<&str> {
257        match self {
258            RoomTypeFilter::Default => None,
259            RoomTypeFilter::Space => Some("m.space"),
260            #[cfg(feature = "unstable-msc3417")]
261            RoomTypeFilter::Call => Some("org.matrix.msc3417.call"),
262            RoomTypeFilter::_Custom(s) => Some(&s.0),
263        }
264    }
265}
266
267impl<T> From<Option<T>> for RoomTypeFilter
268where
269    T: AsRef<str> + Into<Box<str>>,
270{
271    fn from(s: Option<T>) -> Self {
272        match s {
273            None => Self::Default,
274            Some(s) => match s.as_ref() {
275                "m.space" => Self::Space,
276                #[cfg(feature = "unstable-msc3417")]
277                "org.matrix.msc3417.call" => Self::Call,
278                _ => Self::_Custom(PrivOwnedStr(s.into())),
279            },
280        }
281    }
282}
283
284impl From<Option<RoomType>> for RoomTypeFilter {
285    fn from(t: Option<RoomType>) -> Self {
286        match t {
287            None => Self::Default,
288            Some(s) => match s {
289                RoomType::Space => Self::Space,
290                #[cfg(feature = "unstable-msc3417")]
291                RoomType::Call => Self::Call,
292                _ => Self::from(Some(s.as_str())),
293            },
294        }
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use assert_matches2::assert_matches;
301    use serde_json::{from_value as from_json_value, json};
302
303    use super::{Filter, RoomNetwork, RoomTypeFilter};
304    use crate::{assert_to_canonical_json_eq, room::RoomType};
305
306    #[test]
307    fn test_from_room_type() {
308        let test = RoomType::Space;
309        let other: RoomTypeFilter = RoomTypeFilter::from(Some(test));
310        assert_eq!(other, RoomTypeFilter::Space);
311    }
312
313    #[test]
314    #[cfg(feature = "unstable-msc3417")]
315    fn test_from_call_room_type() {
316        let test = RoomType::Call;
317        let other: RoomTypeFilter = RoomTypeFilter::from(Some(test));
318        assert_eq!(other, RoomTypeFilter::Call);
319    }
320
321    #[test]
322    fn serialize_matrix_network_only() {
323        assert_to_canonical_json_eq!(RoomNetwork::Matrix, json!({}));
324    }
325
326    #[test]
327    fn deserialize_matrix_network_only() {
328        let json = json!({ "include_all_networks": false });
329        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
330    }
331
332    #[test]
333    fn serialize_default_network_is_empty() {
334        assert_to_canonical_json_eq!(RoomNetwork::default(), json!({}));
335    }
336
337    #[test]
338    fn deserialize_empty_network_is_default() {
339        let json = json!({});
340        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
341    }
342
343    #[test]
344    fn serialize_include_all_networks() {
345        assert_to_canonical_json_eq!(RoomNetwork::All, json!({ "include_all_networks": true }));
346    }
347
348    #[test]
349    fn deserialize_include_all_networks() {
350        let json = json!({ "include_all_networks": true });
351        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::All);
352    }
353
354    #[test]
355    fn serialize_third_party_network() {
356        assert_to_canonical_json_eq!(
357            RoomNetwork::ThirdParty("freenode".to_owned()),
358            json!({ "third_party_instance_id": "freenode" }),
359        );
360    }
361
362    #[test]
363    fn deserialize_third_party_network() {
364        let json = json!({ "third_party_instance_id": "freenode" });
365        assert_eq!(
366            from_json_value::<RoomNetwork>(json).unwrap(),
367            RoomNetwork::ThirdParty("freenode".into())
368        );
369    }
370
371    #[test]
372    fn deserialize_include_all_networks_and_third_party_exclusivity() {
373        let json = json!({ "include_all_networks": true, "third_party_instance_id": "freenode" });
374        assert_eq!(
375            from_json_value::<RoomNetwork>(json).unwrap_err().to_string().as_str(),
376            "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive."
377        );
378    }
379
380    #[test]
381    fn serialize_filter_empty() {
382        assert_to_canonical_json_eq!(Filter::default(), json!({}));
383    }
384
385    #[test]
386    fn deserialize_filter_empty() {
387        let json = json!({});
388        let filter = from_json_value::<Filter>(json).unwrap();
389        assert_eq!(filter.generic_search_term, None);
390        assert_eq!(filter.room_types.len(), 0);
391    }
392
393    #[test]
394    fn serialize_filter_room_types() {
395        let filter = Filter {
396            generic_search_term: None,
397            room_types: vec![
398                RoomTypeFilter::Default,
399                RoomTypeFilter::Space,
400                Some("custom_type").into(),
401            ],
402        };
403        assert_to_canonical_json_eq!(
404            filter,
405            json!({ "room_types": [null, "m.space", "custom_type"] }),
406        );
407    }
408
409    #[test]
410    #[cfg(feature = "unstable-msc3417")]
411    fn serialize_filter_call_room_types() {
412        let filter = Filter {
413            generic_search_term: None,
414            room_types: vec![RoomTypeFilter::Default, RoomTypeFilter::Call],
415        };
416        assert_to_canonical_json_eq!(
417            filter,
418            json!({ "room_types": [null, "org.matrix.msc3417.call"] }),
419        );
420    }
421
422    #[test]
423    fn deserialize_filter_room_types() {
424        let json = json!({ "room_types": [null, "m.space", "custom_type"] });
425        let filter = from_json_value::<Filter>(json).unwrap();
426        assert_eq!(filter.room_types.len(), 3);
427        assert_eq!(filter.room_types[0], RoomTypeFilter::Default);
428        assert_eq!(filter.room_types[1], RoomTypeFilter::Space);
429        assert_matches!(&filter.room_types[2], RoomTypeFilter::_Custom(_));
430        assert_eq!(filter.room_types[2].as_str(), Some("custom_type"));
431    }
432
433    #[test]
434    #[cfg(feature = "unstable-msc3417")]
435    fn deserialize_filter_call_room_types() {
436        let json = json!({ "room_types": [null, "m.space", "org.matrix.msc3417.call"] });
437        let filter = from_json_value::<Filter>(json).unwrap();
438        assert_eq!(filter.room_types.len(), 3);
439        assert_eq!(filter.room_types[0], RoomTypeFilter::Default);
440        assert_eq!(filter.room_types[1], RoomTypeFilter::Space);
441        assert_matches!(&filter.room_types[2], RoomTypeFilter::Call);
442    }
443}