1use std::borrow::Cow;
4
5use js_int::UInt;
6use serde::{de, Deserialize, Serialize};
7use serde_json::value::RawValue as RawJsonValue;
8
9use crate::{
10 directory::PublicRoomJoinRule,
11 serde::{from_raw_json_value, StringEnum},
12 space::SpaceRoomJoinRule,
13 EventEncryptionAlgorithm, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, PrivOwnedStr,
14 RoomVersionId,
15};
16
17#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
19#[derive(Clone, PartialEq, Eq, StringEnum)]
20#[non_exhaustive]
21pub enum RoomType {
22 #[ruma_enum(rename = "m.space")]
24 Space,
25
26 #[doc(hidden)]
28 _Custom(PrivOwnedStr),
29}
30
31#[derive(Debug, Clone, Serialize)]
33#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
34pub struct RoomSummary {
35 pub room_id: OwnedRoomId,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
43 #[cfg_attr(
44 feature = "compat-empty-string-null",
45 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
46 )]
47 pub canonical_alias: Option<OwnedRoomAliasId>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub name: Option<String>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub topic: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
62 #[cfg_attr(
63 feature = "compat-empty-string-null",
64 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
65 )]
66 pub avatar_url: Option<OwnedMxcUri>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub room_type: Option<RoomType>,
71
72 pub num_joined_members: UInt,
74
75 #[serde(flatten, skip_serializing_if = "ruma_common::serde::is_default")]
77 pub join_rule: JoinRuleSummary,
78
79 pub world_readable: bool,
81
82 pub guest_can_join: bool,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub encryption: Option<EventEncryptionAlgorithm>,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub room_version: Option<RoomVersionId>,
94}
95
96impl RoomSummary {
97 pub fn new(
99 room_id: OwnedRoomId,
100 join_rule: JoinRuleSummary,
101 guest_can_join: bool,
102 num_joined_members: UInt,
103 world_readable: bool,
104 ) -> Self {
105 Self {
106 room_id,
107 canonical_alias: None,
108 name: None,
109 topic: None,
110 avatar_url: None,
111 room_type: None,
112 num_joined_members,
113 join_rule,
114 world_readable,
115 guest_can_join,
116 encryption: None,
117 room_version: None,
118 }
119 }
120}
121
122impl<'de> Deserialize<'de> for RoomSummary {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: de::Deserializer<'de>,
126 {
127 #[derive(Deserialize)]
130 struct RoomSummaryDeHelper {
131 room_id: OwnedRoomId,
132 #[cfg_attr(
133 feature = "compat-empty-string-null",
134 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
135 )]
136 canonical_alias: Option<OwnedRoomAliasId>,
137 name: Option<String>,
138 topic: Option<String>,
139 #[cfg_attr(
140 feature = "compat-empty-string-null",
141 serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none")
142 )]
143 avatar_url: Option<OwnedMxcUri>,
144 room_type: Option<RoomType>,
145 num_joined_members: UInt,
146 world_readable: bool,
147 guest_can_join: bool,
148 encryption: Option<EventEncryptionAlgorithm>,
149 room_version: Option<RoomVersionId>,
150 }
151
152 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
153 let RoomSummaryDeHelper {
154 room_id,
155 canonical_alias,
156 name,
157 topic,
158 avatar_url,
159 room_type,
160 num_joined_members,
161 world_readable,
162 guest_can_join,
163 encryption,
164 room_version,
165 } = from_raw_json_value(&json)?;
166 let join_rule: JoinRuleSummary = from_raw_json_value(&json)?;
167
168 Ok(Self {
169 room_id,
170 canonical_alias,
171 name,
172 topic,
173 avatar_url,
174 room_type,
175 num_joined_members,
176 join_rule,
177 world_readable,
178 guest_can_join,
179 encryption,
180 room_version,
181 })
182 }
183}
184
185#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
190#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191#[serde(tag = "join_rule", rename_all = "snake_case")]
192pub enum JoinRuleSummary {
193 Invite,
196
197 Knock,
201
202 Private,
204
205 Restricted(RestrictedSummary),
208
209 KnockRestricted(RestrictedSummary),
212
213 #[default]
215 Public,
216
217 #[doc(hidden)]
218 #[serde(skip_serializing)]
219 _Custom(PrivOwnedStr),
220}
221
222impl JoinRuleSummary {
223 pub fn as_str(&self) -> &str {
225 match self {
226 Self::Invite => "invite",
227 Self::Knock => "knock",
228 Self::Private => "private",
229 Self::Restricted(_) => "restricted",
230 Self::KnockRestricted(_) => "knock_restricted",
231 Self::Public => "public",
232 Self::_Custom(rule) => &rule.0,
233 }
234 }
235}
236
237impl From<JoinRuleSummary> for PublicRoomJoinRule {
238 fn from(value: JoinRuleSummary) -> Self {
239 match value {
240 JoinRuleSummary::Invite => Self::Invite,
241 JoinRuleSummary::Knock => Self::Knock,
242 JoinRuleSummary::Private => Self::Private,
243 JoinRuleSummary::Restricted(_) => Self::Restricted,
244 JoinRuleSummary::KnockRestricted(_) => Self::KnockRestricted,
245 JoinRuleSummary::Public => Self::Public,
246 JoinRuleSummary::_Custom(custom) => Self::_Custom(custom),
247 }
248 }
249}
250
251impl From<JoinRuleSummary> for SpaceRoomJoinRule {
252 fn from(value: JoinRuleSummary) -> Self {
253 match value {
254 JoinRuleSummary::Invite => Self::Invite,
255 JoinRuleSummary::Knock => Self::Knock,
256 JoinRuleSummary::Private => Self::Private,
257 JoinRuleSummary::Restricted(_) => Self::Restricted,
258 JoinRuleSummary::KnockRestricted(_) => Self::KnockRestricted,
259 JoinRuleSummary::Public => Self::Public,
260 JoinRuleSummary::_Custom(custom) => Self::_Custom(custom),
261 }
262 }
263}
264
265impl<'de> Deserialize<'de> for JoinRuleSummary {
266 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267 where
268 D: de::Deserializer<'de>,
269 {
270 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
271
272 #[derive(Deserialize)]
273 struct ExtractType<'a> {
274 #[serde(borrow)]
275 join_rule: Option<Cow<'a, str>>,
276 }
277
278 let Some(join_rule) = serde_json::from_str::<ExtractType<'_>>(json.get())
279 .map_err(de::Error::custom)?
280 .join_rule
281 else {
282 return Ok(Self::default());
283 };
284
285 match join_rule.as_ref() {
286 "invite" => Ok(Self::Invite),
287 "knock" => Ok(Self::Knock),
288 "private" => Ok(Self::Private),
289 "restricted" => from_raw_json_value(&json).map(Self::Restricted),
290 "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
291 "public" => Ok(Self::Public),
292 _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
293 }
294 }
295}
296
297#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
300pub struct RestrictedSummary {
301 #[serde(default)]
303 pub allowed_room_ids: Vec<OwnedRoomId>,
304}
305
306impl RestrictedSummary {
307 pub fn new(allowed_room_ids: Vec<OwnedRoomId>) -> Self {
309 Self { allowed_room_ids }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use assert_matches2::assert_matches;
316 use js_int::uint;
317 use ruma_common::owned_room_id;
318 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
319
320 use super::{JoinRuleSummary, RestrictedSummary, RoomSummary};
321
322 #[test]
323 fn deserialize_summary_no_join_rule() {
324 let json = json!({
325 "room_id": "!room:localhost",
326 "num_joined_members": 5,
327 "world_readable": false,
328 "guest_can_join": false,
329 });
330
331 let summary: RoomSummary = from_json_value(json).unwrap();
332 assert_eq!(summary.room_id, "!room:localhost");
333 assert_eq!(summary.num_joined_members, uint!(5));
334 assert!(!summary.world_readable);
335 assert!(!summary.guest_can_join);
336 assert_matches!(summary.join_rule, JoinRuleSummary::Public);
337 }
338
339 #[test]
340 fn deserialize_summary_private_join_rule() {
341 let json = json!({
342 "room_id": "!room:localhost",
343 "num_joined_members": 5,
344 "world_readable": false,
345 "guest_can_join": false,
346 "join_rule": "private",
347 });
348
349 let summary: RoomSummary = from_json_value(json).unwrap();
350 assert_eq!(summary.room_id, "!room:localhost");
351 assert_eq!(summary.num_joined_members, uint!(5));
352 assert!(!summary.world_readable);
353 assert!(!summary.guest_can_join);
354 assert_matches!(summary.join_rule, JoinRuleSummary::Private);
355 }
356
357 #[test]
358 fn deserialize_summary_restricted_join_rule() {
359 let json = json!({
360 "room_id": "!room:localhost",
361 "num_joined_members": 5,
362 "world_readable": false,
363 "guest_can_join": false,
364 "join_rule": "restricted",
365 "allowed_room_ids": ["!otherroom:localhost"],
366 });
367
368 let summary: RoomSummary = from_json_value(json).unwrap();
369 assert_eq!(summary.room_id, "!room:localhost");
370 assert_eq!(summary.num_joined_members, uint!(5));
371 assert!(!summary.world_readable);
372 assert!(!summary.guest_can_join);
373 assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
374 assert_eq!(restricted.allowed_room_ids.len(), 1);
375 }
376
377 #[test]
378 fn deserialize_summary_restricted_join_rule_no_allowed_room_ids() {
379 let json = json!({
380 "room_id": "!room:localhost",
381 "num_joined_members": 5,
382 "world_readable": false,
383 "guest_can_join": false,
384 "join_rule": "restricted",
385 });
386
387 let summary: RoomSummary = from_json_value(json).unwrap();
388 assert_eq!(summary.room_id, "!room:localhost");
389 assert_eq!(summary.num_joined_members, uint!(5));
390 assert!(!summary.world_readable);
391 assert!(!summary.guest_can_join);
392 assert_matches!(summary.join_rule, JoinRuleSummary::Restricted(restricted));
393 assert_eq!(restricted.allowed_room_ids.len(), 0);
394 }
395
396 #[test]
397 fn serialize_summary_knock_join_rule() {
398 let summary = RoomSummary::new(
399 owned_room_id!("!room:localhost"),
400 JoinRuleSummary::Knock,
401 false,
402 uint!(5),
403 false,
404 );
405
406 assert_eq!(
407 to_json_value(&summary).unwrap(),
408 json!({
409 "room_id": "!room:localhost",
410 "num_joined_members": 5,
411 "world_readable": false,
412 "guest_can_join": false,
413 "join_rule": "knock",
414 })
415 );
416 }
417
418 #[test]
419 fn serialize_summary_restricted_join_rule() {
420 let summary = RoomSummary::new(
421 owned_room_id!("!room:localhost"),
422 JoinRuleSummary::Restricted(RestrictedSummary::new(vec![owned_room_id!(
423 "!otherroom:localhost"
424 )])),
425 false,
426 uint!(5),
427 false,
428 );
429
430 assert_eq!(
431 to_json_value(&summary).unwrap(),
432 json!({
433 "room_id": "!room:localhost",
434 "num_joined_members": 5,
435 "world_readable": false,
436 "guest_can_join": false,
437 "join_rule": "restricted",
438 "allowed_room_ids": ["!otherroom:localhost"],
439 })
440 );
441 }
442}