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