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}