1#![allow(clippy::assign_op_pattern)] mod 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#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
54pub enum RoomDisplayName {
55    Named(String),
57    Aliased(String),
59    Calculated(String),
62    EmptyWas(String),
65    Empty,
67}
68
69const WHITESPACE_REGEX: &str = r"\s+";
70const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
71
72impl RoomDisplayName {
73    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        let sanitised = whitespace_regex.replace_all(room_name, "-");
91        let sanitised =
93            String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
94        let sanitised = symbol_regex.replace_all(&sanitised, "");
96        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#[derive(Clone, Debug, Serialize, Deserialize)]
119pub struct BaseRoomInfo {
120    pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
122    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
124    pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
125    pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
127    pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
129    pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
132    pub(crate) encryption: Option<RoomEncryptionEventContent>,
134    pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
136    pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
138    pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
140    pub(crate) max_power_level: i64,
142    pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
144    pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
146    pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
148    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
151    pub(crate) rtc_member_events:
152        BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
153    #[serde(default)]
155    pub(crate) is_marked_unread: bool,
156    #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
161    pub(crate) notable_tags: RoomNotableTags,
162    pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
164}
165
166impl BaseRoomInfo {
167    pub fn new() -> Self {
169        Self::default()
170    }
171
172    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    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            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                let mut o_ev = o_ev.clone();
233                o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
234
235                self.rtc_member_events
237                    .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
238
239                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    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                }
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                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        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    #[repr(transparent)]
366    #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
367    pub(crate) struct RoomNotableTags: u8 {
368        const FAVOURITE = 0b0000_0001;
370
371        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#[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    pub creator: OwnedUserId,
433
434    #[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    #[serde(default = "default_create_room_version_id")]
447    pub room_version: RoomVersionId,
448
449    #[serde(skip_serializing_if = "Option::is_none")]
452    pub predecessor: Option<PreviousRoom>,
453
454    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
458    pub room_type: Option<RoomType>,
459}
460
461impl RoomCreateWithCreatorEventContent {
462    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
485pub 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        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    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
513    pub struct RoomMemberships: u16 {
514        const JOIN    = 0b00000001;
516        const INVITE  = 0b00000010;
518        const KNOCK   = 0b00000100;
520        const LEAVE   = 0b00001000;
522        const BAN     = 0b00010000;
524
525        const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
527    }
528}
529
530impl RoomMemberships {
531    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    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}