1use std::{collections::BTreeMap, fmt, hash::Hash, iter};
18
19pub use matrix_sdk_common::deserialized_responses::*;
20use once_cell::sync::Lazy;
21use regex::Regex;
22use ruma::{
23 events::{
24 room::{
25 member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
26 power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
27 },
28 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
29 PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
30 StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
31 },
32 serde::Raw,
33 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
34};
35use serde::Serialize;
36use unicode_normalization::UnicodeNormalization;
37
38#[derive(Clone, Debug)]
41#[non_exhaustive]
42pub struct AmbiguityChange {
43 pub member_id: OwnedUserId,
46 pub member_ambiguous: bool,
49 pub disambiguated_member: Option<OwnedUserId>,
51 pub ambiguated_member: Option<OwnedUserId>,
53}
54
55impl AmbiguityChange {
56 pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
58 iter::once(&*self.member_id)
59 .chain(self.disambiguated_member.as_deref())
60 .chain(self.ambiguated_member.as_deref())
61 }
62}
63
64#[derive(Clone, Debug, Default)]
66#[non_exhaustive]
67pub struct AmbiguityChanges {
68 pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
71}
72
73static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
74 Regex::new(DisplayName::MXID_PATTERN)
75 .expect("We should be able to create a regex from our static MXID pattern")
76});
77static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
78 Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
79 .expect("We should be able to create a regex from our static left-to-right pattern")
80});
81static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
82 Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
83 .expect("We should be able to create a regex from our static hidden characters pattern")
84});
85
86static I_REGEX: Lazy<Regex> = Lazy::new(|| {
91 Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
92});
93
94static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
99 Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
100});
101
102static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
107 Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
108});
109
110#[derive(Debug, Clone, Eq)]
136pub struct DisplayName {
137 raw: String,
138 decancered: Option<String>,
139}
140
141impl Hash for DisplayName {
142 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
143 if let Some(decancered) = &self.decancered {
144 decancered.hash(state);
145 } else {
146 self.raw.hash(state);
147 }
148 }
149}
150
151impl PartialEq for DisplayName {
152 fn eq(&self, other: &Self) -> bool {
153 match (self.decancered.as_deref(), other.decancered.as_deref()) {
154 (None, None) => self.raw == other.raw,
155 (None, Some(_)) | (Some(_), None) => false,
156 (Some(this), Some(other)) => this == other,
157 }
158 }
159}
160
161impl DisplayName {
162 const MXID_PATTERN: &'static str = "@.+[:.].+";
164
165 const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
169
170 const HIDDEN_CHARACTERS_PATTERN: &'static str =
180 "[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
181
182 pub fn new(raw: &str) -> Self {
191 let normalized = raw.nfd().collect::<String>();
192 let replaced = DOT_REGEX.replace_all(&normalized, ":");
193 let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
194
195 let decancered = decancer::cure!(&replaced).ok().map(|cured| {
196 let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
197 let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
198 let replaced = DOT_REGEX.replace_all(&replaced, ":");
201 let replaced = ZERO_REGEX.replace_all(&replaced, "o");
202
203 replaced.to_string()
204 });
205
206 Self { raw: raw.to_owned(), decancered }
207 }
208
209 pub fn is_inherently_ambiguous(&self) -> bool {
214 self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
216 }
217
218 pub fn as_raw_str(&self) -> &str {
221 &self.raw
222 }
223
224 pub fn as_normalized_str(&self) -> Option<&str> {
230 self.decancered.as_deref()
231 }
232
233 fn has_hidden_characters(&self) -> bool {
234 HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
235 }
236
237 fn looks_like_an_mxid(&self) -> bool {
238 self.decancered
239 .as_deref()
240 .map(|d| MXID_REGEX.is_match(d))
241 .unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
242 }
243}
244
245#[derive(Clone, Debug, Default)]
249pub struct MembersResponse {
250 pub chunk: Vec<RoomMemberEvent>,
252 pub ambiguity_changes: AmbiguityChanges,
254}
255
256#[derive(Clone, Debug, Serialize)]
258#[serde(untagged)]
259pub enum RawAnySyncOrStrippedTimelineEvent {
260 Sync(Raw<AnySyncTimelineEvent>),
262 Stripped(Raw<AnyStrippedStateEvent>),
264}
265
266impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
267 fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
268 Self::Sync(event)
269 }
270}
271
272impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
273 fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
274 Self::Stripped(event)
275 }
276}
277
278#[derive(Clone, Debug, Serialize)]
280#[serde(untagged)]
281pub enum RawAnySyncOrStrippedState {
282 Sync(Raw<AnySyncStateEvent>),
284 Stripped(Raw<AnyStrippedStateEvent>),
286}
287
288impl RawAnySyncOrStrippedState {
289 pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
291 match self {
292 Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
293 Self::Stripped(raw) => {
294 Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
295 }
296 }
297 }
298
299 pub fn cast<C>(self) -> RawSyncOrStrippedState<C>
302 where
303 C: StaticStateEventContent + RedactContent,
304 C::Redacted: RedactedStateEventContent,
305 {
306 match self {
307 Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast()),
308 Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast()),
309 }
310 }
311}
312
313#[derive(Clone, Debug)]
315pub enum AnySyncOrStrippedState {
316 Sync(Box<AnySyncStateEvent>),
321 Stripped(Box<AnyStrippedStateEvent>),
326}
327
328impl AnySyncOrStrippedState {
329 pub fn as_sync(&self) -> Option<&AnySyncStateEvent> {
332 match self {
333 Self::Sync(ev) => Some(ev),
334 Self::Stripped(_) => None,
335 }
336 }
337
338 pub fn as_stripped(&self) -> Option<&AnyStrippedStateEvent> {
341 match self {
342 Self::Sync(_) => None,
343 Self::Stripped(ev) => Some(ev),
344 }
345 }
346}
347
348#[derive(Clone, Debug, Serialize)]
350#[serde(untagged)]
351pub enum RawSyncOrStrippedState<C>
352where
353 C: StaticStateEventContent + RedactContent,
354 C::Redacted: RedactedStateEventContent,
355{
356 Sync(Raw<SyncStateEvent<C>>),
358 Stripped(Raw<StrippedStateEvent<C::PossiblyRedacted>>),
360}
361
362impl<C> RawSyncOrStrippedState<C>
363where
364 C: StaticStateEventContent + RedactContent,
365 C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
366{
367 pub fn deserialize(&self) -> serde_json::Result<SyncOrStrippedState<C>>
369 where
370 C: StaticStateEventContent + EventContentFromType + RedactContent,
371 C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + EventContentFromType,
372 C::PossiblyRedacted: PossiblyRedactedStateEventContent + EventContentFromType,
373 {
374 match self {
375 Self::Sync(ev) => Ok(SyncOrStrippedState::Sync(ev.deserialize()?)),
376 Self::Stripped(ev) => Ok(SyncOrStrippedState::Stripped(ev.deserialize()?)),
377 }
378 }
379}
380
381pub type RawMemberEvent = RawSyncOrStrippedState<RoomMemberEventContent>;
383
384#[derive(Clone, Debug)]
386pub enum SyncOrStrippedState<C>
387where
388 C: StaticStateEventContent + RedactContent,
389 C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
390{
391 Sync(SyncStateEvent<C>),
393 Stripped(StrippedStateEvent<C::PossiblyRedacted>),
395}
396
397impl<C> SyncOrStrippedState<C>
398where
399 C: StaticStateEventContent + RedactContent,
400 C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + fmt::Debug + Clone,
401 C::PossiblyRedacted: PossiblyRedactedStateEventContent<StateKey = C::StateKey>,
402{
403 pub fn as_sync(&self) -> Option<&SyncStateEvent<C>> {
405 match self {
406 Self::Sync(ev) => Some(ev),
407 Self::Stripped(_) => None,
408 }
409 }
410
411 pub fn as_stripped(&self) -> Option<&StrippedStateEvent<C::PossiblyRedacted>> {
414 match self {
415 Self::Sync(_) => None,
416 Self::Stripped(ev) => Some(ev),
417 }
418 }
419
420 pub fn sender(&self) -> &UserId {
422 match self {
423 Self::Sync(e) => e.sender(),
424 Self::Stripped(e) => &e.sender,
425 }
426 }
427
428 pub fn event_id(&self) -> Option<&EventId> {
430 match self {
431 Self::Sync(e) => Some(e.event_id()),
432 Self::Stripped(_) => None,
433 }
434 }
435
436 pub fn origin_server_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
438 match self {
439 Self::Sync(e) => Some(e.origin_server_ts()),
440 Self::Stripped(_) => None,
441 }
442 }
443
444 pub fn state_key(&self) -> &C::StateKey {
446 match self {
447 Self::Sync(e) => e.state_key(),
448 Self::Stripped(e) => &e.state_key,
449 }
450 }
451}
452
453impl<C> SyncOrStrippedState<C>
454where
455 C: StaticStateEventContent<PossiblyRedacted = C>
456 + RedactContent
457 + PossiblyRedactedStateEventContent,
458 C::Redacted: RedactedStateEventContent<StateKey = <C as StateEventContent>::StateKey>
459 + fmt::Debug
460 + Clone,
461{
462 pub fn original_content(&self) -> Option<&C> {
464 match self {
465 Self::Sync(e) => e.as_original().map(|e| &e.content),
466 Self::Stripped(e) => Some(&e.content),
467 }
468 }
469}
470
471pub type MemberEvent = SyncOrStrippedState<RoomMemberEventContent>;
473
474impl MemberEvent {
475 pub fn membership(&self) -> &MembershipState {
477 match self {
478 MemberEvent::Sync(e) => e.membership(),
479 MemberEvent::Stripped(e) => &e.content.membership,
480 }
481 }
482
483 pub fn user_id(&self) -> &UserId {
485 self.state_key()
486 }
487
488 pub fn display_name(&self) -> DisplayName {
493 DisplayName::new(
494 self.original_content()
495 .and_then(|c| c.displayname.as_deref())
496 .unwrap_or_else(|| self.user_id().localpart()),
497 )
498 }
499
500 pub fn reason(&self) -> Option<&str> {
502 match self {
503 MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
504 MemberEvent::Stripped(e) => e.content.reason.as_deref(),
505 _ => None,
506 }
507 }
508
509 pub fn timestamp(&self) -> Option<UInt> {
511 match self {
512 MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
513 _ => None,
514 }
515 }
516}
517
518impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
519 pub fn power_levels(&self) -> RoomPowerLevels {
521 match self {
522 Self::Sync(e) => e.power_levels(),
523 Self::Stripped(e) => e.power_levels(),
524 }
525 }
526}
527
528#[cfg(test)]
529mod test {
530 macro_rules! assert_display_name_eq {
531 ($left:expr, $right:expr $(, $desc:expr)?) => {{
532 let left = crate::deserialized_responses::DisplayName::new($left);
533 let right = crate::deserialized_responses::DisplayName::new($right);
534
535 similar_asserts::assert_eq!(
536 left,
537 right
538 $(, $desc)?
539 );
540 }};
541 }
542
543 macro_rules! assert_display_name_ne {
544 ($left:expr, $right:expr $(, $desc:expr)?) => {{
545 let left = crate::deserialized_responses::DisplayName::new($left);
546 let right = crate::deserialized_responses::DisplayName::new($right);
547
548 assert_ne!(
549 left,
550 right
551 $(, $desc)?
552 );
553 }};
554 }
555
556 macro_rules! assert_ambiguous {
557 ($name:expr) => {
558 let name = crate::deserialized_responses::DisplayName::new($name);
559
560 assert!(
561 name.is_inherently_ambiguous(),
562 "The display {:?} should be considered amgibuous",
563 name
564 );
565 };
566 }
567
568 macro_rules! assert_not_ambiguous {
569 ($name:expr) => {
570 let name = crate::deserialized_responses::DisplayName::new($name);
571
572 assert!(
573 !name.is_inherently_ambiguous(),
574 "The display {:?} should not be considered amgibuous",
575 name
576 );
577 };
578 }
579
580 #[test]
581 fn test_display_name_inherently_ambiguous() {
582 assert_not_ambiguous!("Alice");
585 assert_not_ambiguous!("Carol");
586 assert_not_ambiguous!("Car0l");
587 assert_not_ambiguous!("Ivan");
588 assert_not_ambiguous!("๐ฎ๐ถ๐ฝ๐ถ๐๐๐ถ๐ฝ๐๐ถ");
589 assert_not_ambiguous!("โโโโโขโกโโโโ");
590 assert_not_ambiguous!("๐
๐ฐ๐ท๐ฐ๐
๐
๐ฐ๐ท๐ป๐ฐ");
591 assert_not_ambiguous!("๏ผณ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ");
592 assert_not_ambiguous!("\u{202e}alharsahas");
594
595 assert_ambiguous!("Saฬดhasrahla");
597 assert_ambiguous!("Sahas\u{200D}rahla");
598 }
599
600 #[test]
601 fn test_display_name_equality_capitalization() {
602 assert_display_name_eq!("Alice", "alice");
604 }
605
606 #[test]
607 fn test_display_name_equality_different_names() {
608 assert_display_name_ne!("Alice", "Carol");
610 }
611
612 #[test]
613 fn test_display_name_equality_capital_l() {
614 assert_display_name_eq!("Hello", "HeIlo");
616 }
617
618 #[test]
619 fn test_display_name_equality_confusable_zero() {
620 assert_display_name_eq!("Carol", "Car0l");
622 }
623
624 #[test]
625 fn test_display_name_equality_cyrillic() {
626 assert_display_name_eq!("alice", "ะฐlice");
628 }
629
630 #[test]
631 fn test_display_name_equality_scriptures() {
632 assert_display_name_eq!("Sahasrahla", "๐ฎ๐ถ๐ฝ๐ถ๐๐๐ถ๐ฝ๐๐ถ");
634 }
635
636 #[test]
637 fn test_display_name_equality_frakturs() {
638 assert_display_name_eq!("Sahasrahla", "๐๐๐ฅ๐๐ฐ๐ฏ๐๐ฅ๐ฉ๐");
640 }
641
642 #[test]
643 fn test_display_name_equality_circled() {
644 assert_display_name_eq!("Sahasrahla", "โโโโโขโกโโโโ");
646 }
647
648 #[test]
649 fn test_display_name_equality_squared() {
650 assert_display_name_eq!("Sahasrahla", "๐
๐ฐ๐ท๐ฐ๐
๐
๐ฐ๐ท๐ป๐ฐ");
652 }
653
654 #[test]
655 fn test_display_name_equality_big_unicode() {
656 assert_display_name_eq!("Sahasrahla", "๏ผณ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ");
658 }
659
660 #[test]
661 fn test_display_name_equality_left_to_right() {
662 assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
664 }
665
666 #[test]
667 fn test_display_name_equality_diacritical() {
668 assert_display_name_eq!("Sahasrahla", "Saฬดhasrahla");
670 }
671
672 #[test]
673 fn test_display_name_equality_zero_width_joiner() {
674 assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
676 }
677
678 #[test]
679 fn test_display_name_equality_zero_width_space() {
680 assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
682 }
683
684 #[test]
685 fn test_display_name_equality_ligatures() {
686 assert_display_name_eq!("ff", "\u{FB00}");
688 }
689
690 #[test]
691 fn test_display_name_confusable_mxid_colon() {
692 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
693 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
694 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
695 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
696 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
697 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
698 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
699 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
700 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
701 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
702 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
703
704 assert_ambiguous!("@mxid\u{0589}domain.tld");
706 assert_ambiguous!("@mxid\u{05c3}domain.tld");
707 assert_ambiguous!("@mxid\u{0703}domain.tld");
708 assert_ambiguous!("@mxid\u{0a83}domain.tld");
709 assert_ambiguous!("@mxid\u{16ec}domain.tld");
710 assert_ambiguous!("@mxid\u{205a}domain.tld");
711 assert_ambiguous!("@mxid\u{2236}domain.tld");
712 assert_ambiguous!("@mxid\u{fe13}domain.tld");
713 assert_ambiguous!("@mxid\u{fe52}domain.tld");
714 assert_ambiguous!("@mxid\u{fe30}domain.tld");
715 assert_ambiguous!("@mxid\u{ff1a}domain.tld");
716 }
717
718 #[test]
719 fn test_display_name_confusable_mxid_dot() {
720 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
721 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
722 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
723 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
724 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
725 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
726
727 assert_ambiguous!("@mxid:domain\u{0701}tld");
729 assert_ambiguous!("@mxid:domain\u{0702}tld");
730 assert_ambiguous!("@mxid:domain\u{2024}tld");
731 assert_ambiguous!("@mxid:domain\u{fe52}tld");
732 assert_ambiguous!("@mxid:domain\u{ff0e}tld");
733 assert_ambiguous!("@mxid:domain\u{1d16d}tld");
734 }
735
736 #[test]
737 fn test_display_name_confusable_mxid_replacing_a() {
738 assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
739 assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
740
741 assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
743 assert_ambiguous!("@mxid:dom\u{0430}in.tld");
744 }
745
746 #[test]
747 fn test_display_name_confusable_mxid_replacing_l() {
748 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
749 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
750 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
751 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
752 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
753 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
754
755 assert_ambiguous!("@mxid:domain.tId");
757 assert_ambiguous!("@mxid:domain.t\u{217c}d");
758 assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
759 assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
760 assert_ambiguous!("@mxid:domain.t\u{1d695}d");
761 assert_ambiguous!("@mxid:domain.t\u{2223}d");
762 }
763}