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    room::{JoinRuleKind, RoomSummary, RoomType},
11    OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
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 custom room type.
242    #[doc(hidden)]
243    _Custom(PrivOwnedStr),
244}
245
246impl RoomTypeFilter {
247    /// Get the string representation of this `RoomTypeFilter`.
248    ///
249    /// [`RoomTypeFilter::Default`] returns `None`.
250    pub fn as_str(&self) -> Option<&str> {
251        match self {
252            RoomTypeFilter::Default => None,
253            RoomTypeFilter::Space => Some("m.space"),
254            RoomTypeFilter::_Custom(s) => Some(&s.0),
255        }
256    }
257}
258
259impl<T> From<Option<T>> for RoomTypeFilter
260where
261    T: AsRef<str> + Into<Box<str>>,
262{
263    fn from(s: Option<T>) -> Self {
264        match s {
265            None => Self::Default,
266            Some(s) => match s.as_ref() {
267                "m.space" => Self::Space,
268                _ => Self::_Custom(PrivOwnedStr(s.into())),
269            },
270        }
271    }
272}
273
274impl From<Option<RoomType>> for RoomTypeFilter {
275    fn from(t: Option<RoomType>) -> Self {
276        match t {
277            None => Self::Default,
278            Some(s) => match s {
279                RoomType::Space => Self::Space,
280                _ => Self::from(Some(s.as_str())),
281            },
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use assert_matches2::assert_matches;
289    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
290
291    use super::{Filter, RoomNetwork, RoomTypeFilter};
292    use crate::room::RoomType;
293
294    #[test]
295    fn test_from_room_type() {
296        let test = RoomType::Space;
297        let other: RoomTypeFilter = RoomTypeFilter::from(Some(test));
298        assert_eq!(other, RoomTypeFilter::Space);
299    }
300
301    #[test]
302    fn serialize_matrix_network_only() {
303        let json = json!({});
304        assert_eq!(to_json_value(RoomNetwork::Matrix).unwrap(), json);
305    }
306
307    #[test]
308    fn deserialize_matrix_network_only() {
309        let json = json!({ "include_all_networks": false });
310        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
311    }
312
313    #[test]
314    fn serialize_default_network_is_empty() {
315        let json = json!({});
316        assert_eq!(to_json_value(RoomNetwork::default()).unwrap(), json);
317    }
318
319    #[test]
320    fn deserialize_empty_network_is_default() {
321        let json = json!({});
322        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::Matrix);
323    }
324
325    #[test]
326    fn serialize_include_all_networks() {
327        let json = json!({ "include_all_networks": true });
328        assert_eq!(to_json_value(RoomNetwork::All).unwrap(), json);
329    }
330
331    #[test]
332    fn deserialize_include_all_networks() {
333        let json = json!({ "include_all_networks": true });
334        assert_eq!(from_json_value::<RoomNetwork>(json).unwrap(), RoomNetwork::All);
335    }
336
337    #[test]
338    fn serialize_third_party_network() {
339        let json = json!({ "third_party_instance_id": "freenode" });
340        assert_eq!(to_json_value(RoomNetwork::ThirdParty("freenode".to_owned())).unwrap(), json);
341    }
342
343    #[test]
344    fn deserialize_third_party_network() {
345        let json = json!({ "third_party_instance_id": "freenode" });
346        assert_eq!(
347            from_json_value::<RoomNetwork>(json).unwrap(),
348            RoomNetwork::ThirdParty("freenode".into())
349        );
350    }
351
352    #[test]
353    fn deserialize_include_all_networks_and_third_party_exclusivity() {
354        let json = json!({ "include_all_networks": true, "third_party_instance_id": "freenode" });
355        assert_eq!(
356            from_json_value::<RoomNetwork>(json).unwrap_err().to_string().as_str(),
357            "`include_all_networks = true` and `third_party_instance_id` are mutually exclusive."
358        );
359    }
360
361    #[test]
362    fn serialize_filter_empty() {
363        let filter = Filter::default();
364        let json = json!({});
365        assert_eq!(to_json_value(filter).unwrap(), json);
366    }
367
368    #[test]
369    fn deserialize_filter_empty() {
370        let json = json!({});
371        let filter = from_json_value::<Filter>(json).unwrap();
372        assert_eq!(filter.generic_search_term, None);
373        assert_eq!(filter.room_types.len(), 0);
374    }
375
376    #[test]
377    fn serialize_filter_room_types() {
378        let filter = Filter {
379            generic_search_term: None,
380            room_types: vec![
381                RoomTypeFilter::Default,
382                RoomTypeFilter::Space,
383                Some("custom_type").into(),
384            ],
385        };
386        let json = json!({ "room_types": [null, "m.space", "custom_type"] });
387        assert_eq!(to_json_value(filter).unwrap(), json);
388    }
389
390    #[test]
391    fn deserialize_filter_room_types() {
392        let json = json!({ "room_types": [null, "m.space", "custom_type"] });
393        let filter = from_json_value::<Filter>(json).unwrap();
394        assert_eq!(filter.room_types.len(), 3);
395        assert_eq!(filter.room_types[0], RoomTypeFilter::Default);
396        assert_eq!(filter.room_types[1], RoomTypeFilter::Space);
397        assert_matches!(&filter.room_types[2], RoomTypeFilter::_Custom(_));
398        assert_eq!(filter.room_types[2].as_str(), Some("custom_type"));
399    }
400}