matrix_sdk_base/rooms/
mod.rs

1#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
2
3mod members;
4pub(crate) mod normal;
5
6use std::{
7    collections::{BTreeMap, HashSet},
8    fmt,
9    hash::Hash,
10};
11
12use bitflags::bitflags;
13pub use members::RoomMember;
14pub use normal::{
15    apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
16    RoomMembersUpdate, RoomState, RoomStateFilter,
17};
18use regex::Regex;
19use ruma::{
20    assign,
21    events::{
22        beacon_info::BeaconInfoEventContent,
23        call::member::{CallMemberEventContent, CallMemberStateKey},
24        direct::OwnedDirectUserIdentifier,
25        macros::EventContent,
26        room::{
27            avatar::RoomAvatarEventContent,
28            canonical_alias::RoomCanonicalAliasEventContent,
29            create::{PreviousRoom, RoomCreateEventContent},
30            encryption::RoomEncryptionEventContent,
31            guest_access::RoomGuestAccessEventContent,
32            history_visibility::RoomHistoryVisibilityEventContent,
33            join_rules::RoomJoinRulesEventContent,
34            member::MembershipState,
35            name::RoomNameEventContent,
36            pinned_events::RoomPinnedEventsEventContent,
37            tombstone::RoomTombstoneEventContent,
38            topic::RoomTopicEventContent,
39        },
40        tag::{TagName, Tags},
41        AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
42        RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
43    },
44    room::RoomType,
45    EventId, OwnedUserId, RoomVersionId,
46};
47use serde::{Deserialize, Serialize};
48
49use crate::MinimalStateEvent;
50
51/// The name of the room, either from the metadata or calculated
52/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
53#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
54pub enum RoomDisplayName {
55    /// The room has been named explicitly as
56    Named(String),
57    /// The room has a canonical alias that should be used
58    Aliased(String),
59    /// The room has not given an explicit name but a name could be
60    /// calculated
61    Calculated(String),
62    /// The room doesn't have a name right now, but used to have one
63    /// e.g. because it was a DM and everyone has left the room
64    EmptyWas(String),
65    /// No useful name could be calculated or ever found
66    Empty,
67}
68
69const WHITESPACE_REGEX: &str = r"\s+";
70const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
71
72impl RoomDisplayName {
73    /// Transforms the current display name into the name part of a
74    /// `RoomAliasId`.
75    pub fn to_room_alias_name(&self) -> String {
76        let room_name = match self {
77            Self::Named(name) => name,
78            Self::Aliased(name) => name,
79            Self::Calculated(name) => name,
80            Self::EmptyWas(name) => name,
81            Self::Empty => "",
82        };
83
84        let whitespace_regex =
85            Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
86        let symbol_regex =
87            Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
88
89        // Replace whitespaces with `-`
90        let sanitised = whitespace_regex.replace_all(room_name, "-");
91        // Remove non-ASCII characters and ASCII control characters
92        let sanitised =
93            String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
94        // Remove other problematic ASCII symbols
95        let sanitised = symbol_regex.replace_all(&sanitised, "");
96        // Lowercased
97        sanitised.to_lowercase()
98    }
99}
100
101impl fmt::Display for RoomDisplayName {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            RoomDisplayName::Named(s)
105            | RoomDisplayName::Calculated(s)
106            | RoomDisplayName::Aliased(s) => {
107                write!(f, "{s}")
108            }
109            RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
110            RoomDisplayName::Empty => write!(f, "Empty Room"),
111        }
112    }
113}
114
115/// A base room info struct that is the backbone of normal as well as stripped
116/// rooms. Holds all the state events that are important to present a room to
117/// users.
118#[derive(Clone, Debug, Serialize, Deserialize)]
119pub struct BaseRoomInfo {
120    /// The avatar URL of this room.
121    pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
122    /// All shared live location beacons of this room.
123    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
124    pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
125    /// The canonical alias of this room.
126    pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
127    /// The `m.room.create` event content of this room.
128    pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
129    /// A list of user ids this room is considered as direct message, if this
130    /// room is a DM.
131    pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
132    /// The `m.room.encryption` event content that enabled E2EE in this room.
133    pub(crate) encryption: Option<RoomEncryptionEventContent>,
134    /// The guest access policy of this room.
135    pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
136    /// The history visibility policy of this room.
137    pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
138    /// The join rule policy of this room.
139    pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
140    /// The maximal power level that can be found in this room.
141    pub(crate) max_power_level: i64,
142    /// The `m.room.name` of this room.
143    pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
144    /// The `m.room.tombstone` event content of this room.
145    pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
146    /// The topic of this room.
147    pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
148    /// All minimal state events that containing one or more running matrixRTC
149    /// memberships.
150    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
151    pub(crate) rtc_member_events:
152        BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
153    /// Whether this room has been manually marked as unread.
154    #[serde(default)]
155    pub(crate) is_marked_unread: bool,
156    /// Some notable tags.
157    ///
158    /// We are not interested by all the tags. Some tags are more important than
159    /// others, and this field collects them.
160    #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
161    pub(crate) notable_tags: RoomNotableTags,
162    /// The `m.room.pinned_events` of this room.
163    pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
164}
165
166impl BaseRoomInfo {
167    /// Create a new, empty base room info.
168    pub fn new() -> Self {
169        Self::default()
170    }
171
172    /// Get the room version of this room.
173    ///
174    /// For room versions earlier than room version 11, if the event is
175    /// redacted, this will return the default of [`RoomVersionId::V1`].
176    pub fn room_version(&self) -> Option<&RoomVersionId> {
177        match self.create.as_ref()? {
178            MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
179            MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
180        }
181    }
182
183    /// Handle a state event for this room and update our info accordingly.
184    ///
185    /// Returns true if the event modified the info, false otherwise.
186    pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
187        match ev {
188            AnySyncStateEvent::BeaconInfo(b) => {
189                self.beacons.insert(b.state_key().clone(), b.into());
190            }
191            // No redacted branch - enabling encryption cannot be undone.
192            AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
193                self.encryption = Some(encryption.content.clone());
194            }
195            AnySyncStateEvent::RoomAvatar(a) => {
196                self.avatar = Some(a.into());
197            }
198            AnySyncStateEvent::RoomName(n) => {
199                self.name = Some(n.into());
200            }
201            AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
202                self.create = Some(c.into());
203            }
204            AnySyncStateEvent::RoomHistoryVisibility(h) => {
205                self.history_visibility = Some(h.into());
206            }
207            AnySyncStateEvent::RoomGuestAccess(g) => {
208                self.guest_access = Some(g.into());
209            }
210            AnySyncStateEvent::RoomJoinRules(c) => {
211                self.join_rules = Some(c.into());
212            }
213            AnySyncStateEvent::RoomCanonicalAlias(a) => {
214                self.canonical_alias = Some(a.into());
215            }
216            AnySyncStateEvent::RoomTopic(t) => {
217                self.topic = Some(t.into());
218            }
219            AnySyncStateEvent::RoomTombstone(t) => {
220                self.tombstone = Some(t.into());
221            }
222            AnySyncStateEvent::RoomPowerLevels(p) => {
223                self.max_power_level = p.power_levels().max().into();
224            }
225            AnySyncStateEvent::CallMember(m) => {
226                let Some(o_ev) = m.as_original() else {
227                    return false;
228                };
229
230                // we modify the event so that `origin_sever_ts` gets copied into
231                // `content.created_ts`
232                let mut o_ev = o_ev.clone();
233                o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
234
235                // Add the new event.
236                self.rtc_member_events
237                    .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
238
239                // Remove all events that don't contain any memberships anymore.
240                self.rtc_member_events.retain(|_, ev| {
241                    ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
242                });
243            }
244            AnySyncStateEvent::RoomPinnedEvents(p) => {
245                self.pinned_events = p.as_original().map(|p| p.content.clone());
246            }
247            _ => return false,
248        }
249
250        true
251    }
252
253    /// Handle a stripped state event for this room and update our info
254    /// accordingly.
255    ///
256    /// Returns true if the event modified the info, false otherwise.
257    pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
258        match ev {
259            AnyStrippedStateEvent::RoomEncryption(encryption) => {
260                if let Some(algorithm) = &encryption.content.algorithm {
261                    let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
262                        rotation_period_ms: encryption.content.rotation_period_ms,
263                        rotation_period_msgs: encryption.content.rotation_period_msgs,
264                    });
265                    self.encryption = Some(content);
266                }
267                // If encryption event is redacted, we don't care much. When
268                // entering the room, we will fetch the proper event before
269                // sending any messages.
270            }
271            AnyStrippedStateEvent::RoomAvatar(a) => {
272                self.avatar = Some(a.into());
273            }
274            AnyStrippedStateEvent::RoomName(n) => {
275                self.name = Some(n.into());
276            }
277            AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
278                self.create = Some(c.into());
279            }
280            AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
281                self.history_visibility = Some(h.into());
282            }
283            AnyStrippedStateEvent::RoomGuestAccess(g) => {
284                self.guest_access = Some(g.into());
285            }
286            AnyStrippedStateEvent::RoomJoinRules(c) => {
287                self.join_rules = Some(c.into());
288            }
289            AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
290                self.canonical_alias = Some(a.into());
291            }
292            AnyStrippedStateEvent::RoomTopic(t) => {
293                self.topic = Some(t.into());
294            }
295            AnyStrippedStateEvent::RoomTombstone(t) => {
296                self.tombstone = Some(t.into());
297            }
298            AnyStrippedStateEvent::RoomPowerLevels(p) => {
299                self.max_power_level = p.power_levels().max().into();
300            }
301            AnyStrippedStateEvent::CallMember(_) => {
302                // Ignore stripped call state events. Rooms that are not in Joined or Left state
303                // wont have call information.
304                return false;
305            }
306            AnyStrippedStateEvent::RoomPinnedEvents(p) => {
307                if let Some(pinned) = p.content.pinned.clone() {
308                    self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
309                }
310            }
311            _ => return false,
312        }
313
314        true
315    }
316
317    fn handle_redaction(&mut self, redacts: &EventId) {
318        let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
319
320        // FIXME: Use let chains once available to get rid of unwrap()s
321        if self.avatar.has_event_id(redacts) {
322            self.avatar.as_mut().unwrap().redact(&room_version);
323        } else if self.canonical_alias.has_event_id(redacts) {
324            self.canonical_alias.as_mut().unwrap().redact(&room_version);
325        } else if self.create.has_event_id(redacts) {
326            self.create.as_mut().unwrap().redact(&room_version);
327        } else if self.guest_access.has_event_id(redacts) {
328            self.guest_access.as_mut().unwrap().redact(&room_version);
329        } else if self.history_visibility.has_event_id(redacts) {
330            self.history_visibility.as_mut().unwrap().redact(&room_version);
331        } else if self.join_rules.has_event_id(redacts) {
332            self.join_rules.as_mut().unwrap().redact(&room_version);
333        } else if self.name.has_event_id(redacts) {
334            self.name.as_mut().unwrap().redact(&room_version);
335        } else if self.tombstone.has_event_id(redacts) {
336            self.tombstone.as_mut().unwrap().redact(&room_version);
337        } else if self.topic.has_event_id(redacts) {
338            self.topic.as_mut().unwrap().redact(&room_version);
339        } else {
340            self.rtc_member_events
341                .retain(|_, member_event| member_event.event_id() != Some(redacts));
342        }
343    }
344
345    pub fn handle_notable_tags(&mut self, tags: &Tags) {
346        let mut notable_tags = RoomNotableTags::empty();
347
348        if tags.contains_key(&TagName::Favorite) {
349            notable_tags.insert(RoomNotableTags::FAVOURITE);
350        }
351
352        if tags.contains_key(&TagName::LowPriority) {
353            notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
354        }
355
356        self.notable_tags = notable_tags;
357    }
358}
359
360bitflags! {
361    /// Notable tags, i.e. subset of tags that we are more interested by.
362    ///
363    /// We are not interested by all the tags. Some tags are more important than
364    /// others, and this struct describes them.
365    #[repr(transparent)]
366    #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
367    pub(crate) struct RoomNotableTags: u8 {
368        /// The `m.favourite` tag.
369        const FAVOURITE = 0b0000_0001;
370
371        /// THe `m.lowpriority` tag.
372        const LOW_PRIORITY = 0b0000_0010;
373    }
374}
375
376trait OptionExt {
377    fn has_event_id(&self, ev_id: &EventId) -> bool;
378}
379
380impl<C> OptionExt for Option<MinimalStateEvent<C>>
381where
382    C: StaticStateEventContent + RedactContent,
383    C::Redacted: RedactedStateEventContent,
384{
385    fn has_event_id(&self, ev_id: &EventId) -> bool {
386        self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
387    }
388}
389
390impl Default for BaseRoomInfo {
391    fn default() -> Self {
392        Self {
393            avatar: None,
394            beacons: BTreeMap::new(),
395            canonical_alias: None,
396            create: None,
397            dm_targets: Default::default(),
398            encryption: None,
399            guest_access: None,
400            history_visibility: None,
401            join_rules: None,
402            max_power_level: 100,
403            name: None,
404            tombstone: None,
405            topic: None,
406            rtc_member_events: BTreeMap::new(),
407            is_marked_unread: false,
408            notable_tags: RoomNotableTags::empty(),
409            pinned_events: None,
410        }
411    }
412}
413
414/// The content of an `m.room.create` event, with a required `creator` field.
415///
416/// Starting with room version 11, the `creator` field should be removed and the
417/// `sender` field of the event should be used instead. This is reflected on
418/// [`RoomCreateEventContent`].
419///
420/// This type was created as an alternative for ease of use. When it is used in
421/// the SDK, it is constructed by copying the `sender` of the original event as
422/// the `creator`.
423#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
424#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
425pub struct RoomCreateWithCreatorEventContent {
426    /// The `user_id` of the room creator.
427    ///
428    /// This is set by the homeserver.
429    ///
430    /// While this should be optional since room version 11, we copy the sender
431    /// of the event so we can still access it.
432    pub creator: OwnedUserId,
433
434    /// Whether or not this room's data should be transferred to other
435    /// homeservers.
436    #[serde(
437        rename = "m.federate",
438        default = "ruma::serde::default_true",
439        skip_serializing_if = "ruma::serde::is_true"
440    )]
441    pub federate: bool,
442
443    /// The version of the room.
444    ///
445    /// Defaults to `RoomVersionId::V1`.
446    #[serde(default = "default_create_room_version_id")]
447    pub room_version: RoomVersionId,
448
449    /// A reference to the room this room replaces, if the previous room was
450    /// upgraded.
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub predecessor: Option<PreviousRoom>,
453
454    /// The room type.
455    ///
456    /// This is currently only used for spaces.
457    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
458    pub room_type: Option<RoomType>,
459}
460
461impl RoomCreateWithCreatorEventContent {
462    /// Constructs a `RoomCreateWithCreatorEventContent` with the given original
463    /// content and sender.
464    pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self {
465        let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content;
466        Self { creator: sender, federate, room_version, predecessor, room_type }
467    }
468
469    fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
470        let Self { creator, federate, room_version, predecessor, room_type } = self;
471
472        #[allow(deprecated)]
473        let content = assign!(RoomCreateEventContent::new_v11(), {
474            creator: Some(creator.clone()),
475            federate,
476            room_version,
477            predecessor,
478            room_type,
479        });
480
481        (content, creator)
482    }
483}
484
485/// Redacted form of [`RoomCreateWithCreatorEventContent`].
486pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
487
488impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
489    type StateKey = EmptyStateKey;
490}
491
492impl RedactContent for RoomCreateWithCreatorEventContent {
493    type Redacted = RedactedRoomCreateWithCreatorEventContent;
494
495    fn redact(self, version: &RoomVersionId) -> Self::Redacted {
496        let (content, sender) = self.into_event_content();
497        // Use Ruma's redaction algorithm.
498        let content = content.redact(version);
499        Self::from_event_content(content, sender)
500    }
501}
502
503fn default_create_room_version_id() -> RoomVersionId {
504    RoomVersionId::V1
505}
506
507bitflags! {
508    /// Room membership filter as a bitset.
509    ///
510    /// Note that [`RoomMemberships::empty()`] doesn't filter the results and
511    /// [`RoomMemberships::all()`] filters out unknown memberships.
512    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
513    pub struct RoomMemberships: u16 {
514        /// The member joined the room.
515        const JOIN    = 0b00000001;
516        /// The member was invited to the room.
517        const INVITE  = 0b00000010;
518        /// The member requested to join the room.
519        const KNOCK   = 0b00000100;
520        /// The member left the room.
521        const LEAVE   = 0b00001000;
522        /// The member was banned.
523        const BAN     = 0b00010000;
524
525        /// The member is active in the room (i.e. joined or invited).
526        const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
527    }
528}
529
530impl RoomMemberships {
531    /// Whether the given membership matches this `RoomMemberships`.
532    pub fn matches(&self, membership: &MembershipState) -> bool {
533        if self.is_empty() {
534            return true;
535        }
536
537        let membership = match membership {
538            MembershipState::Ban => Self::BAN,
539            MembershipState::Invite => Self::INVITE,
540            MembershipState::Join => Self::JOIN,
541            MembershipState::Knock => Self::KNOCK,
542            MembershipState::Leave => Self::LEAVE,
543            _ => return false,
544        };
545
546        self.contains(membership)
547    }
548
549    /// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
550    pub fn as_vec(&self) -> Vec<MembershipState> {
551        let mut memberships = Vec::new();
552
553        if self.contains(Self::JOIN) {
554            memberships.push(MembershipState::Join);
555        }
556        if self.contains(Self::INVITE) {
557            memberships.push(MembershipState::Invite);
558        }
559        if self.contains(Self::KNOCK) {
560            memberships.push(MembershipState::Knock);
561        }
562        if self.contains(Self::LEAVE) {
563            memberships.push(MembershipState::Leave);
564        }
565        if self.contains(Self::BAN) {
566            memberships.push(MembershipState::Ban);
567        }
568
569        memberships
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use std::ops::Not;
576
577    use ruma::events::tag::{TagInfo, TagName, Tags};
578
579    use super::{BaseRoomInfo, RoomNotableTags};
580    use crate::RoomDisplayName;
581
582    #[test]
583    fn test_handle_notable_tags_favourite() {
584        let mut base_room_info = BaseRoomInfo::default();
585
586        let mut tags = Tags::new();
587        tags.insert(TagName::Favorite, TagInfo::default());
588
589        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
590        base_room_info.handle_notable_tags(&tags);
591        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
592        tags.clear();
593        base_room_info.handle_notable_tags(&tags);
594        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
595    }
596
597    #[test]
598    fn test_handle_notable_tags_low_priority() {
599        let mut base_room_info = BaseRoomInfo::default();
600
601        let mut tags = Tags::new();
602        tags.insert(TagName::LowPriority, TagInfo::default());
603
604        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
605        base_room_info.handle_notable_tags(&tags);
606        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
607        tags.clear();
608        base_room_info.handle_notable_tags(&tags);
609        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
610    }
611
612    #[test]
613    fn test_room_alias_from_room_display_name_lowercases() {
614        assert_eq!(
615            "roomalias",
616            RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
617        );
618    }
619
620    #[test]
621    fn test_room_alias_from_room_display_name_removes_whitespace() {
622        assert_eq!(
623            "room-alias",
624            RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
625        );
626    }
627
628    #[test]
629    fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
630        assert_eq!(
631            "roomalias",
632            RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
633        );
634    }
635
636    #[test]
637    fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
638        assert_eq!(
639            "roomalias",
640            RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
641        );
642    }
643}