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, de::DeserializeOwned};
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)]
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, ":");
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()
218 }
219
220 pub fn as_raw_str(&self) -> &str {
223 &self.raw
224 }
225
226 pub fn as_normalized_str(&self) -> Option<&str> {
232 self.decancered.as_deref()
233 }
234
235 fn has_hidden_characters(&self) -> bool {
236 HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
237 }
238
239 fn looks_like_an_mxid(&self) -> bool {
240 self.decancered
241 .as_deref()
242 .map(|d| MXID_REGEX.is_match(d))
243 .unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
244 }
245}
246
247#[derive(Clone, Debug, Default)]
251pub struct MembersResponse {
252 pub chunk: Vec<RoomMemberEvent>,
254 pub ambiguity_changes: AmbiguityChanges,
256}
257
258#[derive(Clone, Debug, Serialize)]
260#[serde(untagged)]
261pub enum RawAnySyncOrStrippedTimelineEvent {
262 Sync(Raw<AnySyncTimelineEvent>),
264 Stripped(Raw<AnyStrippedStateEvent>),
266}
267
268impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
269 fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
270 Self::Sync(event)
271 }
272}
273
274impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
275 fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
276 Self::Stripped(event)
277 }
278}
279
280#[derive(Clone, Debug, Serialize)]
282#[serde(untagged)]
283pub enum RawAnySyncOrStrippedState {
284 Sync(Raw<AnySyncStateEvent>),
286 Stripped(Raw<AnyStrippedStateEvent>),
288}
289
290impl RawAnySyncOrStrippedState {
291 pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
293 match self {
294 Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
295 Self::Stripped(raw) => {
296 Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
297 }
298 }
299 }
300
301 pub fn cast<C>(self) -> RawSyncOrStrippedState<C>
304 where
305 C: StaticStateEventContent + RedactContent,
306 C::Redacted: RedactedStateEventContent,
307 {
308 match self {
309 Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast_unchecked()),
310 Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast_unchecked()),
311 }
312 }
313}
314
315#[derive(Clone, Debug)]
317pub enum AnySyncOrStrippedState {
318 Sync(Box<AnySyncStateEvent>),
323 Stripped(Box<AnyStrippedStateEvent>),
328}
329
330impl AnySyncOrStrippedState {
331 pub fn as_sync(&self) -> Option<&AnySyncStateEvent> {
334 match self {
335 Self::Sync(ev) => Some(ev),
336 Self::Stripped(_) => None,
337 }
338 }
339
340 pub fn as_stripped(&self) -> Option<&AnyStrippedStateEvent> {
343 match self {
344 Self::Sync(_) => None,
345 Self::Stripped(ev) => Some(ev),
346 }
347 }
348}
349
350#[derive(Clone, Debug, Serialize)]
352#[serde(untagged)]
353pub enum RawSyncOrStrippedState<C>
354where
355 C: StaticStateEventContent + RedactContent,
356 C::Redacted: RedactedStateEventContent,
357{
358 Sync(Raw<SyncStateEvent<C>>),
360 Stripped(Raw<StrippedStateEvent<C::PossiblyRedacted>>),
362}
363
364impl<C> RawSyncOrStrippedState<C>
365where
366 C: StaticStateEventContent + RedactContent,
367 C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
368{
369 pub fn deserialize(&self) -> serde_json::Result<SyncOrStrippedState<C>>
371 where
372 C: StaticStateEventContent + EventContentFromType + RedactContent,
373 C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + EventContentFromType,
374 C::PossiblyRedacted: PossiblyRedactedStateEventContent + EventContentFromType,
375 C::Unsigned: DeserializeOwned,
376 {
377 match self {
378 Self::Sync(ev) => Ok(SyncOrStrippedState::Sync(ev.deserialize()?)),
379 Self::Stripped(ev) => Ok(SyncOrStrippedState::Stripped(ev.deserialize()?)),
380 }
381 }
382}
383
384pub type RawMemberEvent = RawSyncOrStrippedState<RoomMemberEventContent>;
386
387#[derive(Clone, Debug)]
389pub enum SyncOrStrippedState<C>
390where
391 C: StaticStateEventContent + RedactContent,
392 C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
393{
394 Sync(SyncStateEvent<C>),
396 Stripped(StrippedStateEvent<C::PossiblyRedacted>),
398}
399
400impl<C> SyncOrStrippedState<C>
401where
402 C: StaticStateEventContent + RedactContent,
403 C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + fmt::Debug + Clone,
404 C::PossiblyRedacted: PossiblyRedactedStateEventContent<StateKey = C::StateKey>,
405{
406 pub fn as_sync(&self) -> Option<&SyncStateEvent<C>> {
408 match self {
409 Self::Sync(ev) => Some(ev),
410 Self::Stripped(_) => None,
411 }
412 }
413
414 pub fn as_stripped(&self) -> Option<&StrippedStateEvent<C::PossiblyRedacted>> {
417 match self {
418 Self::Sync(_) => None,
419 Self::Stripped(ev) => Some(ev),
420 }
421 }
422
423 pub fn sender(&self) -> &UserId {
425 match self {
426 Self::Sync(e) => e.sender(),
427 Self::Stripped(e) => &e.sender,
428 }
429 }
430
431 pub fn event_id(&self) -> Option<&EventId> {
433 match self {
434 Self::Sync(e) => Some(e.event_id()),
435 Self::Stripped(_) => None,
436 }
437 }
438
439 pub fn origin_server_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
441 match self {
442 Self::Sync(e) => Some(e.origin_server_ts()),
443 Self::Stripped(_) => None,
444 }
445 }
446
447 pub fn state_key(&self) -> &C::StateKey {
449 match self {
450 Self::Sync(e) => e.state_key(),
451 Self::Stripped(e) => &e.state_key,
452 }
453 }
454}
455
456impl<C> SyncOrStrippedState<C>
457where
458 C: StaticStateEventContent<PossiblyRedacted = C>
459 + RedactContent
460 + PossiblyRedactedStateEventContent,
461 C::Redacted: RedactedStateEventContent<StateKey = <C as StateEventContent>::StateKey>
462 + fmt::Debug
463 + Clone,
464{
465 pub fn original_content(&self) -> Option<&C> {
467 match self {
468 Self::Sync(e) => e.as_original().map(|e| &e.content),
469 Self::Stripped(e) => Some(&e.content),
470 }
471 }
472}
473
474pub type MemberEvent = SyncOrStrippedState<RoomMemberEventContent>;
476
477impl MemberEvent {
478 pub fn membership(&self) -> &MembershipState {
480 match self {
481 MemberEvent::Sync(e) => e.membership(),
482 MemberEvent::Stripped(e) => &e.content.membership,
483 }
484 }
485
486 pub fn user_id(&self) -> &UserId {
488 self.state_key()
489 }
490
491 pub fn displayname_value(&self) -> Option<&str> {
496 match self {
497 Self::Sync(event) => event.as_original()?.content.displayname.as_deref(),
498 Self::Stripped(event) => event.content.displayname.as_deref(),
499 }
500 }
501
502 pub fn display_name(&self) -> DisplayName {
507 DisplayName::new(self.displayname_value().unwrap_or_else(|| self.user_id().localpart()))
508 }
509
510 pub fn avatar_url(&self) -> Option<&MxcUri> {
515 match self {
516 Self::Sync(event) => event.as_original()?.content.avatar_url.as_deref(),
517 Self::Stripped(event) => event.content.avatar_url.as_deref(),
518 }
519 }
520
521 pub fn reason(&self) -> Option<&str> {
523 match self {
524 MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
525 MemberEvent::Stripped(e) => e.content.reason.as_deref(),
526 _ => None,
527 }
528 }
529
530 pub fn timestamp(&self) -> Option<UInt> {
532 match self {
533 MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
534 _ => None,
535 }
536 }
537}
538
539impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
540 pub fn power_levels(
542 &self,
543 rules: &AuthorizationRules,
544 creators: Vec<OwnedUserId>,
545 ) -> RoomPowerLevels {
546 match self {
547 Self::Sync(e) => e.power_levels(rules, creators),
548 Self::Stripped(e) => e.power_levels(rules, creators),
549 }
550 }
551}
552
553#[cfg(test)]
554mod test {
555 macro_rules! assert_display_name_eq {
556 ($left:expr, $right:expr $(, $desc:expr)?) => {{
557 let left = crate::deserialized_responses::DisplayName::new($left);
558 let right = crate::deserialized_responses::DisplayName::new($right);
559
560 similar_asserts::assert_eq!(
561 left,
562 right
563 $(, $desc)?
564 );
565 }};
566 }
567
568 macro_rules! assert_display_name_ne {
569 ($left:expr, $right:expr $(, $desc:expr)?) => {{
570 let left = crate::deserialized_responses::DisplayName::new($left);
571 let right = crate::deserialized_responses::DisplayName::new($right);
572
573 assert_ne!(
574 left,
575 right
576 $(, $desc)?
577 );
578 }};
579 }
580
581 macro_rules! assert_ambiguous {
582 ($name:expr) => {
583 let name = crate::deserialized_responses::DisplayName::new($name);
584
585 assert!(
586 name.is_inherently_ambiguous(),
587 "The display {:?} should be considered amgibuous",
588 name
589 );
590 };
591 }
592
593 macro_rules! assert_not_ambiguous {
594 ($name:expr) => {
595 let name = crate::deserialized_responses::DisplayName::new($name);
596
597 assert!(
598 !name.is_inherently_ambiguous(),
599 "The display {:?} should not be considered amgibuous",
600 name
601 );
602 };
603 }
604
605 #[test]
606 fn test_display_name_inherently_ambiguous() {
607 assert_not_ambiguous!("Alice");
611 assert_not_ambiguous!("Carol");
612 assert_not_ambiguous!("Car0l");
613 assert_not_ambiguous!("Ivan");
614 assert_not_ambiguous!("๐ฎ๐ถ๐ฝ๐ถ๐๐๐ถ๐ฝ๐๐ถ");
615 assert_not_ambiguous!("โโโโโขโกโโโโ");
616 assert_not_ambiguous!("๐
๐ฐ๐ท๐ฐ๐
๐
๐ฐ๐ท๐ป๐ฐ");
617 assert_not_ambiguous!("๏ผณ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ");
618 assert_not_ambiguous!("\u{202e}alharsahas");
620
621 assert_ambiguous!("Saฬดhasrahla");
623 assert_ambiguous!("Sahas\u{200D}rahla");
624 }
625
626 #[test]
627 fn test_display_name_equality_capitalization() {
628 assert_display_name_eq!("Alice", "alice");
630 }
631
632 #[test]
633 fn test_display_name_equality_different_names() {
634 assert_display_name_ne!("Alice", "Carol");
636 }
637
638 #[test]
639 fn test_display_name_equality_capital_l() {
640 assert_display_name_eq!("Hello", "HeIlo");
642 }
643
644 #[test]
645 fn test_display_name_equality_confusable_zero() {
646 assert_display_name_eq!("Carol", "Car0l");
648 }
649
650 #[test]
651 fn test_display_name_equality_cyrillic() {
652 assert_display_name_eq!("alice", "ะฐlice");
654 }
655
656 #[test]
657 fn test_display_name_equality_scriptures() {
658 assert_display_name_eq!("Sahasrahla", "๐ฎ๐ถ๐ฝ๐ถ๐๐๐ถ๐ฝ๐๐ถ");
660 }
661
662 #[test]
663 fn test_display_name_equality_frakturs() {
664 assert_display_name_eq!("Sahasrahla", "๐๐๐ฅ๐๐ฐ๐ฏ๐๐ฅ๐ฉ๐");
666 }
667
668 #[test]
669 fn test_display_name_equality_circled() {
670 assert_display_name_eq!("Sahasrahla", "โโโโโขโกโโโโ");
672 }
673
674 #[test]
675 fn test_display_name_equality_squared() {
676 assert_display_name_eq!("Sahasrahla", "๐
๐ฐ๐ท๐ฐ๐
๐
๐ฐ๐ท๐ป๐ฐ");
678 }
679
680 #[test]
681 fn test_display_name_equality_big_unicode() {
682 assert_display_name_eq!("Sahasrahla", "๏ผณ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ๏ฝ");
684 }
685
686 #[test]
687 fn test_display_name_equality_left_to_right() {
688 assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
690 }
691
692 #[test]
693 fn test_display_name_equality_diacritical() {
694 assert_display_name_eq!("Sahasrahla", "Saฬดhasrahla");
696 }
697
698 #[test]
699 fn test_display_name_equality_zero_width_joiner() {
700 assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
702 }
703
704 #[test]
705 fn test_display_name_equality_zero_width_space() {
706 assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
708 }
709
710 #[test]
711 fn test_display_name_equality_ligatures() {
712 assert_display_name_eq!("ff", "\u{FB00}");
714 }
715
716 #[test]
717 fn test_display_name_confusable_mxid_colon() {
718 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
719 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
720 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
721 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
722 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
723 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
724 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
725 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
726 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
727 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
728 assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
729
730 assert_ambiguous!("@mxid\u{0589}domain.tld");
732 assert_ambiguous!("@mxid\u{05c3}domain.tld");
733 assert_ambiguous!("@mxid\u{0703}domain.tld");
734 assert_ambiguous!("@mxid\u{0a83}domain.tld");
735 assert_ambiguous!("@mxid\u{16ec}domain.tld");
736 assert_ambiguous!("@mxid\u{205a}domain.tld");
737 assert_ambiguous!("@mxid\u{2236}domain.tld");
738 assert_ambiguous!("@mxid\u{fe13}domain.tld");
739 assert_ambiguous!("@mxid\u{fe52}domain.tld");
740 assert_ambiguous!("@mxid\u{fe30}domain.tld");
741 assert_ambiguous!("@mxid\u{ff1a}domain.tld");
742 }
743
744 #[test]
745 fn test_display_name_confusable_mxid_dot() {
746 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
747 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
748 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
749 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
750 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
751 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
752
753 assert_ambiguous!("@mxid:domain\u{0701}tld");
755 assert_ambiguous!("@mxid:domain\u{0702}tld");
756 assert_ambiguous!("@mxid:domain\u{2024}tld");
757 assert_ambiguous!("@mxid:domain\u{fe52}tld");
758 assert_ambiguous!("@mxid:domain\u{ff0e}tld");
759 assert_ambiguous!("@mxid:domain\u{1d16d}tld");
760 }
761
762 #[test]
763 fn test_display_name_confusable_mxid_replacing_a() {
764 assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
765 assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
766
767 assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
769 assert_ambiguous!("@mxid:dom\u{0430}in.tld");
770 }
771
772 #[test]
773 fn test_display_name_confusable_mxid_replacing_l() {
774 assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
775 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
776 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
777 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
778 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
779 assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
780
781 assert_ambiguous!("@mxid:domain.tId");
783 assert_ambiguous!("@mxid:domain.t\u{217c}d");
784 assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
785 assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
786 assert_ambiguous!("@mxid:domain.t\u{1d695}d");
787 assert_ambiguous!("@mxid:domain.t\u{2223}d");
788 }
789}