1use imbl::HashSet;
4use indexmap::IndexSet;
5use ruma::{
6 RoomId,
7 push::{
8 AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
9 PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
10 },
11};
12
13use super::{RoomNotificationMode, command::Command, rule_commands::RuleCommands};
14use crate::{
15 error::NotificationSettingsError,
16 notification_settings::{IsEncrypted, IsOneToOne},
17};
18
19#[derive(Clone, Debug)]
20pub(crate) struct Rules {
21 pub ruleset: Ruleset,
22}
23
24impl Rules {
25 pub(crate) fn new(ruleset: Ruleset) -> Self {
26 Rules { ruleset }
27 }
28
29 pub(crate) fn get_custom_rules_for_room(&self, room_id: &RoomId) -> Vec<(RuleKind, String)> {
31 let mut custom_rules = vec![];
32
33 for rule in &self.ruleset.override_ {
35 if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
37 x,
38 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
39 )) {
40 custom_rules.push((RuleKind::Override, rule.rule_id.clone()));
42 }
43 }
44
45 if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
47 custom_rules.push((RuleKind::Room, rule.rule_id().to_owned()));
48 }
49
50 for rule in &self.ruleset.underride {
52 if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
54 x,
55 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
56 )) {
57 custom_rules.push((RuleKind::Underride, rule.rule_id.clone()));
59 }
60 }
61
62 custom_rules
63 }
64
65 pub(crate) fn get_user_defined_room_notification_mode(
67 &self,
68 room_id: &RoomId,
69 ) -> Option<RoomNotificationMode> {
70 if self.ruleset.override_.iter().any(|x| {
72 x.enabled &&
74 x.conditions.iter().any(|x| matches!(
77 x,
78 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
79 )) &&
80 !x.actions.iter().any(|x| x.should_notify())
82 }) {
83 return Some(RoomNotificationMode::Mute);
84 }
85
86 if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
88 if rule.triggers_notification() {
90 return Some(RoomNotificationMode::AllMessages);
91 }
92 return Some(RoomNotificationMode::MentionsAndKeywordsOnly);
93 }
94
95 None
97 }
98
99 pub(crate) fn get_default_room_notification_mode(
107 &self,
108 is_encrypted: IsEncrypted,
109 is_one_to_one: IsOneToOne,
110 ) -> RoomNotificationMode {
111 let predefined_rule_id = get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one);
113 let rule_id = predefined_rule_id.as_str();
114
115 if self
118 .ruleset
119 .get(RuleKind::Underride, rule_id)
120 .is_some_and(|r| r.enabled() && r.triggers_notification())
121 {
122 RoomNotificationMode::AllMessages
123 } else {
124 RoomNotificationMode::MentionsAndKeywordsOnly
126 }
127 }
128
129 pub(crate) fn get_rooms_with_user_defined_rules(&self, enabled: Option<bool>) -> Vec<String> {
131 let test_if_enabled = enabled.is_some();
132 let must_be_enabled = enabled.unwrap_or(false);
133
134 let mut room_ids = HashSet::new();
135 for rule in &self.ruleset {
136 if rule.is_server_default() {
137 continue;
138 }
139 if test_if_enabled && rule.enabled() != must_be_enabled {
140 continue;
141 }
142 match rule {
143 AnyPushRuleRef::Override(r) | AnyPushRuleRef::Underride(r) => {
144 for condition in &r.conditions {
145 if let PushCondition::EventMatch { key, pattern } = condition
146 && key == "room_id"
147 {
148 room_ids.insert(pattern.clone());
149 break;
150 }
151 }
152 }
153 AnyPushRuleRef::Room(r) => {
154 room_ids.insert(r.rule_id.to_string());
155 }
156 _ => {}
157 }
158 }
159 Vec::from_iter(room_ids)
160 }
161
162 fn is_user_mention_enabled(&self) -> bool {
164 if let Some(rule) =
167 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
168 {
169 return rule.enabled();
170 }
171
172 #[allow(deprecated)]
174 if let Some(rule) =
175 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName)
176 && rule.enabled()
177 && rule.triggers_notification()
178 {
179 return true;
180 }
181
182 #[allow(deprecated)]
183 if let Some(rule) =
184 self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
185 && rule.enabled()
186 && rule.triggers_notification()
187 {
188 return true;
189 }
190
191 false
192 }
193
194 fn is_room_mention_enabled(&self) -> bool {
196 if let Some(rule) =
199 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
200 {
201 return rule.enabled();
202 }
203
204 #[allow(deprecated)]
206 self.ruleset
207 .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
208 .is_some_and(|r| r.enabled() && r.triggers_notification())
209 }
210
211 pub(crate) fn contains_keyword_rules(&self) -> bool {
213 self.ruleset.content.iter().any(|r| !r.default && r.enabled)
215 }
216
217 pub(crate) fn enabled_keywords(&self) -> IndexSet<String> {
219 self.ruleset
220 .content
221 .iter()
222 .filter(|r| !r.default && r.enabled)
223 .map(|r| r.pattern.clone())
224 .collect()
225 }
226
227 pub(crate) fn keyword_rules(&self, keyword: &str) -> Vec<&PatternedPushRule> {
229 self.ruleset.content.iter().filter(|r| !r.default && r.pattern == keyword).collect()
230 }
231
232 pub(crate) fn is_enabled(
234 &self,
235 kind: RuleKind,
236 rule_id: &str,
237 ) -> Result<bool, NotificationSettingsError> {
238 if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() {
239 Ok(self.is_room_mention_enabled())
240 } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() {
241 Ok(self.is_user_mention_enabled())
242 } else if let Some(rule) = self.ruleset.get(kind, rule_id) {
243 Ok(rule.enabled())
244 } else {
245 Err(NotificationSettingsError::RuleNotFound(rule_id.to_owned()))
246 }
247 }
248
249 pub(crate) fn apply(&mut self, commands: RuleCommands) {
254 for command in commands.commands {
255 match command {
256 Command::DeletePushRule { kind, rule_id } => {
257 _ = self.ruleset.remove(kind, rule_id);
258 }
259 Command::SetRoomPushRule { .. }
260 | Command::SetOverridePushRule { .. }
261 | Command::SetKeywordPushRule { .. } => {
262 if let Ok(push_rule) = command.to_push_rule() {
263 _ = self.ruleset.insert(push_rule, None, None);
264 }
265 }
266 Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
267 _ = self.ruleset.set_enabled(kind, rule_id, enabled);
268 }
269 Command::SetPushRuleActions { kind, rule_id, actions } => {
270 _ = self.ruleset.set_actions(kind, rule_id, actions);
271 }
272 Command::SetCustomPushRule { rule } => {
273 _ = self.ruleset.insert(rule, None, None);
274 }
275 }
276 }
277 }
278}
279
280pub(crate) fn get_predefined_underride_room_rule_id(
288 is_encrypted: IsEncrypted,
289 is_one_to_one: IsOneToOne,
290) -> PredefinedUnderrideRuleId {
291 match (is_encrypted, is_one_to_one) {
292 (IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
293 (IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
294 (IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
295 (IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
296 }
297}
298
299pub(crate) fn get_predefined_underride_poll_start_rule_id(
306 is_one_to_one: IsOneToOne,
307) -> PredefinedUnderrideRuleId {
308 match is_one_to_one {
309 IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
310 IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
311 }
312}
313
314#[cfg(test)]
315pub(crate) mod tests {
316 use imbl::HashSet;
317 use matrix_sdk_test::{
318 async_test,
319 notification_settings::{
320 build_ruleset, get_server_default_ruleset, server_default_ruleset_with_legacy_mentions,
321 },
322 };
323 use ruma::{
324 OwnedRoomId, RoomId,
325 push::{
326 Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
327 PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
328 },
329 };
330
331 use super::RuleCommands;
332 use crate::{
333 error::NotificationSettingsError,
334 notification_settings::{
335 IsEncrypted, IsOneToOne, RoomNotificationMode,
336 rules::{self, Rules},
337 },
338 };
339
340 fn get_test_room_id() -> OwnedRoomId {
341 RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
342 }
343
344 #[async_test]
345 async fn test_get_custom_rules_for_room() {
346 let room_id = get_test_room_id();
347
348 let rules = Rules::new(get_server_default_ruleset());
349 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
350
351 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
353 let rules = Rules::new(ruleset);
354 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
355
356 let ruleset = build_ruleset(vec![
358 (RuleKind::Override, &room_id, false),
359 (RuleKind::Room, &room_id, false),
360 ]);
361 let rules = Rules::new(ruleset);
362 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
363 }
364
365 #[async_test]
366 async fn test_get_custom_rules_for_room_special_override_rule() {
367 let room_id = get_test_room_id();
368 let mut ruleset = get_server_default_ruleset();
369
370 let new_rule = NewConditionalPushRule::new(
373 "custom_rule_id".to_owned(),
374 vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
375 vec![Action::Notify],
376 );
377 ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
378
379 let rules = Rules::new(ruleset);
380 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
381 }
382
383 #[async_test]
384 async fn test_get_user_defined_room_notification_mode() {
385 let room_id = get_test_room_id();
386 let rules = Rules::new(get_server_default_ruleset());
387 assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
388
389 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
391 let rules = Rules::new(ruleset);
392 assert_eq!(
393 rules.get_user_defined_room_notification_mode(&room_id),
394 Some(RoomNotificationMode::Mute)
395 );
396
397 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
399 let rules = Rules::new(ruleset);
400 assert_eq!(
401 rules.get_user_defined_room_notification_mode(&room_id),
402 Some(RoomNotificationMode::MentionsAndKeywordsOnly)
403 );
404
405 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
407 let rules = Rules::new(ruleset);
408 assert_eq!(
409 rules.get_user_defined_room_notification_mode(&room_id),
410 Some(RoomNotificationMode::AllMessages)
411 );
412
413 let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
414 let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
415 let ruleset = build_ruleset(vec![
416 (RuleKind::Override, &room_id_a, false),
418 (RuleKind::Override, &room_id_b, true),
420 ]);
421 let rules = Rules::new(ruleset);
422 let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
423
424 assert_eq!(mode, Some(RoomNotificationMode::Mute));
427 }
428
429 #[async_test]
430 async fn test_get_predefined_underride_room_rule_id() {
431 assert_eq!(
432 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
433 PredefinedUnderrideRuleId::Message
434 );
435 assert_eq!(
436 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
437 PredefinedUnderrideRuleId::RoomOneToOne
438 );
439 assert_eq!(
440 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
441 PredefinedUnderrideRuleId::Encrypted
442 );
443 assert_eq!(
444 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
445 PredefinedUnderrideRuleId::EncryptedRoomOneToOne
446 );
447 }
448
449 #[async_test]
450 async fn test_get_predefined_underride_poll_start_rule_id() {
451 assert_eq!(
452 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
453 PredefinedUnderrideRuleId::PollStart
454 );
455 assert_eq!(
456 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
457 PredefinedUnderrideRuleId::PollStartOneToOne
458 );
459 }
460
461 #[async_test]
462 async fn test_get_default_room_notification_mode_mentions_and_keywords() {
463 let mut ruleset = get_server_default_ruleset();
464 ruleset
466 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
467 .unwrap();
468
469 let rules = Rules::new(ruleset);
470 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
471 assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
473 }
474
475 #[async_test]
476 async fn test_get_default_room_notification_mode_all_messages() {
477 let mut ruleset = get_server_default_ruleset();
478 ruleset
480 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
481 .unwrap();
482
483 let rules = Rules::new(ruleset);
484 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
485 assert_eq!(mode, RoomNotificationMode::AllMessages);
487 }
488
489 #[async_test]
490 async fn test_is_user_mention_enabled() {
491 let mut ruleset = server_default_ruleset_with_legacy_mentions();
494 ruleset
495 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
496 .unwrap();
497 #[allow(deprecated)]
498 ruleset
499 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
500 .unwrap();
501 #[allow(deprecated)]
502 ruleset
503 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
504 .unwrap();
505
506 let rules = Rules::new(ruleset);
507 assert!(rules.is_user_mention_enabled());
508 assert!(
511 rules
512 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
513 .unwrap()
514 );
515
516 let mut ruleset = server_default_ruleset_with_legacy_mentions();
519 ruleset
520 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
521 .unwrap();
522 #[allow(deprecated)]
523 ruleset
524 .set_actions(
525 RuleKind::Override,
526 PredefinedOverrideRuleId::ContainsDisplayName,
527 vec![Action::Notify],
528 )
529 .unwrap();
530 #[allow(deprecated)]
531 ruleset
532 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
533 .unwrap();
534 #[allow(deprecated)]
535 ruleset
536 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
537 .unwrap();
538
539 let rules = Rules::new(ruleset);
540 assert!(!rules.is_user_mention_enabled());
541 assert!(
544 !rules
545 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
546 .unwrap()
547 );
548 }
549
550 #[async_test]
551 async fn test_is_room_mention_enabled() {
552 let mut ruleset = server_default_ruleset_with_legacy_mentions();
555 ruleset
556 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
557 .unwrap();
558 #[allow(deprecated)]
559 ruleset
560 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
561 .unwrap();
562
563 let rules = Rules::new(ruleset);
564 assert!(rules.is_room_mention_enabled());
565 assert!(
568 rules
569 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
570 .unwrap()
571 );
572
573 let mut ruleset = server_default_ruleset_with_legacy_mentions();
576 ruleset
577 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
578 .unwrap();
579 #[allow(deprecated)]
580 ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
581
582 let rules = Rules::new(ruleset);
583 assert!(!rules.is_room_mention_enabled());
584 assert!(
587 !rules
588 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
589 .unwrap()
590 );
591 }
592
593 #[async_test]
594 async fn test_is_enabled_rule_not_found() {
595 let rules = Rules::new(get_server_default_ruleset());
596
597 assert_eq!(
598 rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
599 Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
600 );
601 }
602
603 #[async_test]
604 async fn test_apply_delete_command() {
605 let room_id = get_test_room_id();
606 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
608 let mut rules = Rules::new(ruleset);
609
610 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
612 rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
613
614 rules.apply(rules_commands);
615
616 assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
618 }
619
620 #[async_test]
621 async fn test_apply_set_command() {
622 let room_id = get_test_room_id();
623 let mut rules = Rules::new(get_server_default_ruleset());
624
625 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
627 rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
628
629 rules.apply(rules_commands);
630
631 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
633 }
634
635 #[async_test]
636 async fn test_apply_set_enabled_command() {
637 let mut rules = Rules::new(get_server_default_ruleset());
638
639 rules
640 .ruleset
641 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
642 .unwrap();
643
644 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
646 rules_commands
647 .set_rule_enabled(
648 RuleKind::Override,
649 PredefinedOverrideRuleId::Reaction.as_str(),
650 false,
651 )
652 .unwrap();
653
654 rules.apply(rules_commands);
655
656 assert!(
658 !rules
659 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
660 .unwrap()
661 );
662 }
663
664 #[async_test]
665 async fn test_get_rooms_with_user_defined_rules() {
666 let rules = Rules::new(get_server_default_ruleset());
668 let room_ids = rules.get_rooms_with_user_defined_rules(None);
669 assert!(room_ids.is_empty());
670
671 let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
673 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
674 let rules = Rules::new(ruleset);
675
676 let room_ids = rules.get_rooms_with_user_defined_rules(None);
677 assert_eq!(room_ids.len(), 1);
678
679 let ruleset = build_ruleset(vec![
681 (RuleKind::Override, &room_id, false),
682 (RuleKind::Underride, &room_id, false),
683 (RuleKind::Room, &room_id, false),
684 ]);
685 let rules = Rules::new(ruleset);
686
687 let room_ids = rules.get_rooms_with_user_defined_rules(None);
688 assert_eq!(room_ids.len(), 1);
689 assert_eq!(room_ids[0], room_id.to_string());
690
691 let ruleset = build_ruleset(vec![
693 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
694 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
695 (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
696 (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
697 (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
698 ]);
699 let rules = Rules::new(ruleset);
700
701 let room_ids = rules.get_rooms_with_user_defined_rules(None);
702 assert_eq!(room_ids.len(), 5);
703 let expected_set: HashSet<String> = vec![
704 "!room_a:matrix.org",
705 "!room_b:matrix.org",
706 "!room_c:matrix.org",
707 "!room_d:matrix.org",
708 "!room_e:matrix.org",
709 ]
710 .into_iter()
711 .collect();
712 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
713
714 let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
716 assert_eq!(room_ids.len(), 0);
717
718 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
720 assert_eq!(room_ids.len(), 5);
721
722 let mut ruleset = build_ruleset(vec![
723 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
724 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
725 (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
726 (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
727 ]);
728 ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
729 ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
730 let rules = Rules::new(ruleset);
731 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
733 assert_eq!(room_ids.len(), 2);
734 let expected_set: HashSet<String> =
735 vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
736 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
737 }
738}