Skip to main content

revolt_database/models/users/
model.rs

1use std::{collections::HashSet, str::FromStr, time::Duration};
2
3use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
4
5use authifier::config::{EmailVerificationConfig, Template};
6use futures::future::join_all;
7use iso8601_timestamp::Timestamp;
8use once_cell::sync::Lazy;
9use rand::seq::SliceRandom;
10use regex::{Regex, RegexBuilder};
11use revolt_config::{config, FeaturesLimits};
12use revolt_models::v0::{self, UserBadges, UserFlags};
13use revolt_presence::filter_online;
14use revolt_result::{create_error, Result};
15use serde_json::json;
16use ulid::Ulid;
17
18auto_derived_partial!(
19    /// # User
20    pub struct User {
21        /// Unique Id
22        #[serde(rename = "_id")]
23        pub id: String,
24        /// Username
25        pub username: String,
26        /// Discriminator
27        pub discriminator: String,
28        /// Display name
29        #[serde(skip_serializing_if = "Option::is_none")]
30        pub display_name: Option<String>,
31        #[serde(skip_serializing_if = "Option::is_none")]
32        /// Avatar attachment
33        pub avatar: Option<File>,
34        /// Relationships with other users
35        #[serde(skip_serializing_if = "Option::is_none")]
36        pub relations: Option<Vec<Relationship>>,
37
38        /// Bitfield of user badges
39        #[serde(skip_serializing_if = "Option::is_none")]
40        pub badges: Option<i32>,
41        /// User's current status
42        #[serde(skip_serializing_if = "Option::is_none")]
43        pub status: Option<UserStatus>,
44        /// User's profile page
45        #[serde(skip_serializing_if = "Option::is_none")]
46        pub profile: Option<UserProfile>,
47
48        /// Enum of user flags
49        #[serde(skip_serializing_if = "Option::is_none")]
50        pub flags: Option<i32>,
51        /// Whether this user is privileged
52        #[serde(skip_serializing_if = "crate::if_false", default)]
53        pub privileged: bool,
54        /// Bot information
55        #[serde(skip_serializing_if = "Option::is_none")]
56        pub bot: Option<BotInformation>,
57
58        /// Time until user is unsuspended
59        #[serde(skip_serializing_if = "Option::is_none")]
60        pub suspended_until: Option<Timestamp>,
61        /// Last acknowledged policy change
62        pub last_acknowledged_policy_change: Timestamp,
63    },
64    "PartialUser"
65);
66
67auto_derived!(
68    /// Optional fields on user object
69    pub enum FieldsUser {
70        Avatar,
71        StatusText,
72        StatusPresence,
73        ProfileContent,
74        ProfileBackground,
75        DisplayName,
76
77        // internal fields
78        Suspension,
79        None,
80    }
81
82    /// User's relationship with another user (or themselves)
83    pub enum RelationshipStatus {
84        None,
85        User,
86        Friend,
87        Outgoing,
88        Incoming,
89        Blocked,
90        BlockedOther,
91    }
92
93    /// Relationship entry indicating current status with other user
94    pub struct Relationship {
95        #[serde(rename = "_id")]
96        pub id: String,
97        pub status: RelationshipStatus,
98    }
99
100    /// Presence status
101    pub enum Presence {
102        /// User is online
103        Online,
104        /// User is not currently available
105        Idle,
106        /// User is focusing / will only receive mentions
107        Focus,
108        /// User is busy / will not receive any notifications
109        Busy,
110        /// User appears to be offline
111        Invisible,
112    }
113
114    /// User's active status
115    #[derive(Default)]
116    pub struct UserStatus {
117        /// Custom status text
118        #[serde(skip_serializing_if = "Option::is_none")]
119        pub text: Option<String>,
120        /// Current presence option
121        #[serde(skip_serializing_if = "Option::is_none")]
122        pub presence: Option<Presence>,
123    }
124
125    /// User's profile
126    #[derive(Default)]
127    pub struct UserProfile {
128        /// Text content on user's profile
129        #[serde(skip_serializing_if = "Option::is_none")]
130        pub content: Option<String>,
131        /// Background visible on user's profile
132        #[serde(skip_serializing_if = "Option::is_none")]
133        pub background: Option<File>,
134    }
135
136    /// Bot information for if the user is a bot
137    pub struct BotInformation {
138        /// Id of the owner of this bot
139        pub owner: String,
140    }
141
142    /// Enumeration providing a hint to the type of user we are handling
143    pub enum UserHint {
144        /// Could be either a user or a bot
145        Any,
146        /// Only match bots
147        Bot,
148        /// Only match users
149        User,
150    }
151);
152
153pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
154    let mut set = (2..9999)
155        .map(|v| format!("{:0>4}", v))
156        .collect::<HashSet<String>>();
157
158    for discrim in [
159        123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999, 1488,
160    ] {
161        set.remove(&format!("{:0>4}", discrim));
162    }
163
164    set.into_iter().collect()
165});
166
167static BLOCKED_USERNAME_PATTERNS: Lazy<Regex> = Lazy::new(|| {
168    RegexBuilder::new("`{3}|(discord|rvlt|guilded|stt)\\.gg|(revolt|stoat)\\.chat|https?:\\/\\/")
169        .case_insensitive(true)
170        .build()
171        .unwrap()
172});
173
174#[allow(clippy::derivable_impls)]
175impl Default for User {
176    fn default() -> Self {
177        Self {
178            id: Default::default(),
179            username: Default::default(),
180            discriminator: Default::default(),
181            display_name: Default::default(),
182            avatar: Default::default(),
183            relations: Default::default(),
184            badges: Default::default(),
185            status: Default::default(),
186            profile: Default::default(),
187            flags: Default::default(),
188            privileged: Default::default(),
189            bot: Default::default(),
190            suspended_until: Default::default(),
191            last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
192        }
193    }
194}
195
196#[allow(clippy::disallowed_methods)]
197impl User {
198    /// Create a new user
199    pub async fn create<I, D>(
200        db: &Database,
201        username: String,
202        account_id: I,
203        data: D,
204    ) -> Result<User>
205    where
206        I: Into<Option<String>>,
207        D: Into<Option<PartialUser>>,
208    {
209        let new_username = User::sanitise_username(&username).await?;
210        User::validate_username(&new_username)?;
211
212        let mut user = User {
213            id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
214            discriminator: User::find_discriminator(db, &new_username, None).await?,
215            username: new_username.clone(),
216            last_acknowledged_policy_change: Timestamp::now_utc(),
217            ..Default::default()
218        };
219
220        if let Some(data) = data.into() {
221            user.apply_options(data);
222        }
223
224        db.insert_user(&user).await?;
225        Ok(user)
226    }
227
228    /// Get limits for this user
229    pub async fn limits(&self) -> FeaturesLimits {
230        let config = config().await;
231        if ulid::Ulid::from_str(&self.id)
232            .expect("`ulid`")
233            .datetime()
234            .elapsed()
235            .expect("time went backwards")
236            <= Duration::from_secs(3600u64 * config.features.limits.global.new_user_hours as u64)
237        {
238            config.features.limits.new_user
239        } else {
240            config.features.limits.default
241        }
242    }
243
244    /// Get the relationship with another user
245    pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
246        if self.id == user_b {
247            return RelationshipStatus::User;
248        }
249
250        if let Some(relations) = &self.relations {
251            if let Some(relationship) = relations.iter().find(|x| x.id == user_b) {
252                return relationship.status.clone();
253            }
254        }
255
256        RelationshipStatus::None
257    }
258
259    pub fn is_friends_with(&self, user_b: &str) -> bool {
260        matches!(
261            self.relationship_with(user_b),
262            RelationshipStatus::Friend | RelationshipStatus::User
263        )
264    }
265
266    /// Check whether two users have a mutual connection
267    ///
268    /// This will check if user and user_b share a server or a group.
269    pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
270        Ok(!db
271            .fetch_mutual_server_ids(&self.id, user_b)
272            .await?
273            .is_empty()
274            || !db
275                .fetch_mutual_channel_ids(&self.id, user_b)
276                .await?
277                .is_empty())
278    }
279
280    /// Check if this user can acquire another server
281    pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
282        if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
283            Ok(())
284        } else {
285            Err(create_error!(TooManyServers {
286                max: self.limits().await.servers
287            }))
288        }
289    }
290
291    /// Validate a username
292    ///
293    /// This will check if the username is a blocked name or contains a blocked pattern.
294    fn validate_username(username: &str) -> Result<()> {
295        let username_lowercase = username.to_lowercase();
296
297        const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt", "stoat"];
298
299        if BLOCKED_USERNAMES.contains(&username_lowercase.as_str())
300            || BLOCKED_USERNAME_PATTERNS.is_match(username)
301        {
302            return Err(create_error!(InvalidUsername));
303        }
304
305        Ok(())
306    }
307
308    /// Sanitise a username
309    ///
310    /// This will clean up Unicode homoglyphs and pad to the min username length with underscores.
311    async fn sanitise_username(username: &str) -> Result<String> {
312        let options = decancer::Options::default().retain_capitalization();
313        let mut username = decancer::cure(username, options)
314            .map_err(|_| create_error!(InvalidUsername))?
315            .to_string();
316
317        let config = revolt_config::config().await;
318        let username_length_diff = config
319            .api
320            .users
321            .min_username_length
322            .saturating_sub(username.len());
323        if username_length_diff > 0 {
324            username.push_str(&"_".repeat(username_length_diff))
325        }
326
327        Ok(username)
328    }
329
330    /// Find a user and session ID from a given token and hint
331    #[async_recursion]
332    pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
333        match hint {
334            UserHint::Bot => Ok((
335                db.fetch_user(
336                    &db.fetch_bot_by_token(token)
337                        .await
338                        .map_err(|_| create_error!(InvalidSession))?
339                        .id,
340                )
341                .await?,
342                String::new(),
343            )),
344            UserHint::User => {
345                let session = db.fetch_session_by_token(token).await?;
346                Ok((db.fetch_user(&session.user_id).await?, session.id))
347            }
348            UserHint::Any => {
349                if let Ok(result) = User::from_token(db, token, UserHint::User).await {
350                    Ok(result)
351                } else {
352                    User::from_token(db, token, UserHint::Bot).await
353                }
354            }
355        }
356    }
357
358    /// Helper function to fetch many users as a mutually connected user
359    /// (while optimising the online ID query)
360    pub async fn fetch_many_ids_as_mutuals(
361        db: &Database,
362        perspective: &User,
363        ids: &[String],
364    ) -> Result<Vec<v0::User>> {
365        let online_ids = filter_online(ids).await;
366
367        Ok(
368            join_all(db.fetch_users(ids).await?.into_iter().map(|user| async {
369                let is_online = online_ids.contains(&user.id);
370                user.into_known(perspective, is_online).await
371            }))
372            .await,
373        )
374    }
375
376    /// Find a free discriminator for a given username
377    pub async fn find_discriminator(
378        db: &Database,
379        username: &str,
380        preferred: Option<(String, String)>,
381    ) -> Result<String> {
382        let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE;
383        let used_discriminators: HashSet<String> = db
384            .fetch_discriminators_in_use(username)
385            .await?
386            .into_iter()
387            .collect();
388
389        let available_discriminators: Vec<&String> =
390            search_space.difference(&used_discriminators).collect();
391
392        if available_discriminators.is_empty() {
393            return Err(create_error!(UsernameTaken));
394        }
395
396        if let Some((preferred, target_id)) = preferred {
397            if available_discriminators.contains(&&preferred) {
398                return Ok(preferred);
399            } else {
400                if db
401                    .has_ratelimited(
402                        &target_id,
403                        crate::RatelimitEventType::DiscriminatorChange,
404                        Duration::from_secs(60 * 60 * 24),
405                        1,
406                    )
407                    .await?
408                {
409                    return Err(create_error!(DiscriminatorChangeRatelimited));
410                }
411
412                RatelimitEvent::create(
413                    db,
414                    target_id,
415                    crate::RatelimitEventType::DiscriminatorChange,
416                )
417                .await?;
418            }
419        }
420
421        let mut rng = rand::thread_rng();
422        Ok(available_discriminators
423            .choose(&mut rng)
424            .expect("we can assert this has an element")
425            .to_string())
426    }
427
428    /// Update a user's username
429    pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
430        let new_username = User::sanitise_username(&username).await?;
431        User::validate_username(&new_username)?;
432
433        if self.username.to_lowercase() == new_username.to_lowercase() {
434            self.update(
435                db,
436                PartialUser {
437                    username: Some(new_username),
438                    ..Default::default()
439                },
440                vec![],
441            )
442            .await
443        } else {
444            self.update(
445                db,
446                PartialUser {
447                    discriminator: Some(
448                        User::find_discriminator(
449                            db,
450                            &new_username,
451                            Some((self.discriminator.to_string(), self.id.clone())),
452                        )
453                        .await?,
454                    ),
455                    username: Some(new_username),
456                    ..Default::default()
457                },
458                vec![],
459            )
460            .await
461        }
462    }
463
464    /// Set a relationship to another user
465    pub async fn set_relationship(
466        &mut self,
467        db: &Database,
468        user_b: &User,
469        status: RelationshipStatus,
470    ) -> Result<()> {
471        db.set_relationship(&self.id, &user_b.id, &status).await?;
472
473        if let RelationshipStatus::None | RelationshipStatus::User = status {
474            if let Some(relations) = &mut self.relations {
475                relations.retain(|relation| relation.id != user_b.id);
476            }
477        } else {
478            let relation = Relationship {
479                id: user_b.id.to_string(),
480                status,
481            };
482
483            if let Some(relations) = &mut self.relations {
484                relations.retain(|relation| relation.id != user_b.id);
485                relations.push(relation);
486            } else {
487                self.relations = Some(vec![relation]);
488            }
489        }
490
491        Ok(())
492    }
493
494    /// Apply a certain relationship between two users
495    pub async fn apply_relationship(
496        &mut self,
497        db: &Database,
498        target: &mut User,
499        local: RelationshipStatus,
500        remote: RelationshipStatus,
501    ) -> Result<()> {
502        target.set_relationship(db, self, remote).await?;
503        self.set_relationship(db, target, local).await?;
504
505        EventV1::UserRelationship {
506            id: target.id.clone(),
507            user: self.clone().into(db, Some(&*target)).await,
508        }
509        .private(target.id.clone())
510        .await;
511
512        EventV1::UserRelationship {
513            id: self.id.clone(),
514            user: target.clone().into(db, Some(&*self)).await,
515        }
516        .private(self.id.clone())
517        .await;
518
519        Ok(())
520    }
521
522    /// Add another user as a friend
523    pub async fn add_friend(
524        &mut self,
525        db: &Database,
526        amqp: &AMQP,
527        target: &mut User,
528    ) -> Result<()> {
529        match self.relationship_with(&target.id) {
530            RelationshipStatus::User => Err(create_error!(NoEffect)),
531            RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
532            RelationshipStatus::Outgoing => Err(create_error!(AlreadySentRequest)),
533            RelationshipStatus::Blocked => Err(create_error!(Blocked)),
534            RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
535            RelationshipStatus::Incoming => {
536                // Accept incoming friend request
537                _ = amqp.friend_request_accepted(self, target).await;
538
539                self.apply_relationship(
540                    db,
541                    target,
542                    RelationshipStatus::Friend,
543                    RelationshipStatus::Friend,
544                )
545                .await
546            }
547            RelationshipStatus::None => {
548                // Get this user's current count of outgoing friend requests
549                let count = self
550                    .relations
551                    .as_ref()
552                    .map(|relations| {
553                        relations
554                            .iter()
555                            .filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
556                            .count()
557                    })
558                    .unwrap_or_default();
559
560                // If we're over the limit, don't allow creating more requests
561                if count >= self.limits().await.outgoing_friend_requests {
562                    return Err(create_error!(TooManyPendingFriendRequests {
563                        max: self.limits().await.outgoing_friend_requests
564                    }));
565                }
566
567                _ = amqp.friend_request_received(target, self).await;
568
569                // Send the friend request
570                self.apply_relationship(
571                    db,
572                    target,
573                    RelationshipStatus::Outgoing,
574                    RelationshipStatus::Incoming,
575                )
576                .await
577            }
578        }
579    }
580
581    /// Remove another user as a friend
582    pub async fn remove_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
583        match self.relationship_with(&target.id) {
584            RelationshipStatus::Friend
585            | RelationshipStatus::Outgoing
586            | RelationshipStatus::Incoming => {
587                self.apply_relationship(
588                    db,
589                    target,
590                    RelationshipStatus::None,
591                    RelationshipStatus::None,
592                )
593                .await
594            }
595            _ => Err(create_error!(NoEffect)),
596        }
597    }
598
599    /// Block another user
600    pub async fn block_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
601        match self.relationship_with(&target.id) {
602            RelationshipStatus::User | RelationshipStatus::Blocked => Err(create_error!(NoEffect)),
603            RelationshipStatus::BlockedOther => {
604                self.apply_relationship(
605                    db,
606                    target,
607                    RelationshipStatus::Blocked,
608                    RelationshipStatus::Blocked,
609                )
610                .await
611            }
612            RelationshipStatus::None
613            | RelationshipStatus::Friend
614            | RelationshipStatus::Incoming
615            | RelationshipStatus::Outgoing => {
616                self.apply_relationship(
617                    db,
618                    target,
619                    RelationshipStatus::Blocked,
620                    RelationshipStatus::BlockedOther,
621                )
622                .await
623            }
624        }
625    }
626
627    /// Unblock another user
628    pub async fn unblock_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
629        match self.relationship_with(&target.id) {
630            RelationshipStatus::Blocked => match target.relationship_with(&self.id) {
631                RelationshipStatus::Blocked => {
632                    self.apply_relationship(
633                        db,
634                        target,
635                        RelationshipStatus::BlockedOther,
636                        RelationshipStatus::Blocked,
637                    )
638                    .await
639                }
640                RelationshipStatus::BlockedOther => {
641                    self.apply_relationship(
642                        db,
643                        target,
644                        RelationshipStatus::None,
645                        RelationshipStatus::None,
646                    )
647                    .await
648                }
649                _ => Err(create_error!(InternalError)),
650            },
651            _ => Err(create_error!(NoEffect)),
652        }
653    }
654
655    /// Update user data
656    pub async fn update(
657        &mut self,
658        db: &Database,
659        partial: PartialUser,
660        remove: Vec<FieldsUser>,
661    ) -> Result<()> {
662        for field in &remove {
663            self.remove_field(field);
664        }
665
666        self.apply_options(partial.clone());
667        db.update_user(&self.id, &partial, remove.clone()).await?;
668
669        EventV1::UserUpdate {
670            id: self.id.clone(),
671            data: partial.into(),
672            clear: remove.into_iter().map(|v| v.into()).collect(),
673            event_id: Some(Ulid::new().to_string()),
674        }
675        .p_user(self.id.clone(), db)
676        .await;
677
678        Ok(())
679    }
680
681    /// Remove a field from User object
682    pub fn remove_field(&mut self, field: &FieldsUser) {
683        match field {
684            FieldsUser::Avatar => self.avatar = None,
685            FieldsUser::StatusText => {
686                if let Some(x) = self.status.as_mut() {
687                    x.text = None;
688                }
689            }
690            FieldsUser::StatusPresence => {
691                if let Some(x) = self.status.as_mut() {
692                    x.presence = None;
693                }
694            }
695            FieldsUser::ProfileContent => {
696                if let Some(x) = self.profile.as_mut() {
697                    x.content = None;
698                }
699            }
700            FieldsUser::ProfileBackground => {
701                if let Some(x) = self.profile.as_mut() {
702                    x.background = None;
703                }
704            }
705            FieldsUser::DisplayName => self.display_name = None,
706            FieldsUser::Suspension => self.suspended_until = None,
707            FieldsUser::None => {}
708        }
709    }
710
711    /// Suspend the user
712    ///
713    /// - If a duration is specified, the user will be automatically unsuspended after the given time.
714    /// - If a reason is specified, an email will be sent.
715    pub async fn suspend(
716        &mut self,
717        db: &Database,
718        duration_days: Option<usize>,
719        reason: Option<Vec<String>>,
720    ) -> Result<()> {
721        let authifier = db.clone().to_authifier().await;
722        let mut account = authifier
723            .database
724            .find_account(&self.id)
725            .await
726            .map_err(|_| create_error!(InternalError))?;
727
728        account
729            .disable(&authifier)
730            .await
731            .map_err(|_| create_error!(InternalError))?;
732
733        account
734            .delete_all_sessions(&authifier, None)
735            .await
736            .map_err(|_| create_error!(InternalError))?;
737
738        self.update(
739            db,
740            PartialUser {
741                flags: Some(UserFlags::SuspendedUntil as i32),
742                suspended_until: duration_days.and_then(|dur| {
743                    Timestamp::now_utc().checked_add(iso8601_timestamp::Duration::days(dur as i64))
744                }),
745                ..Default::default()
746            },
747            vec![],
748        )
749        .await?;
750
751        if let Some(reason) = reason {
752            if let EmailVerificationConfig::Enabled { smtp, .. } =
753                authifier.config.email_verification
754            {
755                smtp.send_email(
756                    account.email.clone(),
757                    // maybe move this to common area?
758                    &Template {
759                        title: "Account Suspension".to_string(),
760                        html: Some(include_str!("../../../templates/suspension.html").to_owned()),
761                        text: include_str!("../../../templates/suspension.txt").to_owned(),
762                        url: Default::default(),
763                    },
764                    json!({
765                        "email": account.email,
766                        "list": reason.join(", "),
767                        "duration": duration_days,
768                        "duration_display": if duration_days.is_some() {
769                            "block"
770                        } else {
771                            "none"
772                        }
773                    }),
774                )
775                .map_err(|_| create_error!(InternalError))?;
776            }
777        }
778
779        Ok(())
780    }
781
782    /// Unsuspend the user
783    pub async fn unsuspend(&mut self, db: &Database) -> Result<()> {
784        self.update(
785            db,
786            PartialUser {
787                flags: Some(0),
788                suspended_until: None,
789                ..Default::default()
790            },
791            vec![],
792        )
793        .await?;
794
795        unimplemented!()
796    }
797
798    /// Permanently ban the user
799    ///
800    /// - If a reason is specified, an email will be sent.
801    pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
802        // Send ban email (if reason provided)
803        unimplemented!()
804    }
805
806    /// Mark as deleted
807    pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
808        self.update(
809            db,
810            PartialUser {
811                username: Some(format!("Deleted User {}", self.id)),
812                flags: Some(2),
813                ..Default::default()
814            },
815            vec![
816                FieldsUser::Avatar,
817                FieldsUser::StatusText,
818                FieldsUser::StatusPresence,
819                FieldsUser::ProfileContent,
820                FieldsUser::ProfileBackground,
821                FieldsUser::Suspension,
822            ],
823        )
824        .await
825    }
826
827    /// Gets the user's badges along with calculating any dynamic badges
828    pub async fn get_badges(&self) -> u32 {
829        let config = config().await;
830        let badges = self.badges.unwrap_or_default() as u32;
831
832        if let Some(cutoff) = config.api.users.early_adopter_cutoff {
833            if Ulid::from_string(&self.id).unwrap().timestamp_ms() < cutoff {
834                return badges + UserBadges::EarlyAdopter as u32;
835            };
836        };
837
838        badges
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use crate::User;
845
846    #[test]
847    fn username_validation_blocked_names() {
848        let username_admin = "Admin";
849        let username_revolt = "Revolt";
850        let username_stoat = "Stoat";
851        let username_allowed = "Allowed";
852
853        assert!(User::validate_username(username_admin).is_err());
854        assert!(User::validate_username(username_revolt).is_err());
855        assert!(User::validate_username(username_stoat).is_err());
856        assert!(User::validate_username(username_allowed).is_ok());
857    }
858
859    #[test]
860    fn username_validation_blocked_patterns() {
861        let username_grave = "```_test";
862        let username_discord = "discord.gg_test";
863        let username_rvlt = "rvlt.gg_test";
864        let username_guilded = "guilded.gg_test";
865        let username_stt = "stt.gg_test";
866        let username_revolt = "revolt.chat_test";
867        let username_stoat = "stoat.chat_test";
868        let username_http = "http://_test";
869        let username_https = "https://_test";
870
871        assert!(User::validate_username(username_grave).is_err());
872        assert!(User::validate_username(username_discord).is_err());
873        assert!(User::validate_username(username_rvlt).is_err());
874        assert!(User::validate_username(username_guilded).is_err());
875        assert!(User::validate_username(username_stt).is_err());
876        assert!(User::validate_username(username_revolt).is_err());
877        assert!(User::validate_username(username_stoat).is_err());
878        assert!(User::validate_username(username_http).is_err());
879        assert!(User::validate_username(username_https).is_err());
880    }
881
882    #[async_std::test]
883    async fn username_sanitisation_clean() {
884        let username_clean = "Test";
885
886        let username_clean_sanitised = User::sanitise_username(username_clean).await;
887
888        assert!(username_clean_sanitised.is_ok());
889        assert_eq!(username_clean, username_clean_sanitised.unwrap());
890    }
891
892    #[async_std::test]
893    async fn username_sanitisation_homoglyphs() {
894        let username_homoglyphs = "𝔽𝕌Ňℕy";
895
896        let username_homoglyphs_sanitised =
897            User::sanitise_username(username_homoglyphs).await.unwrap();
898
899        assert_ne!(username_homoglyphs, username_homoglyphs_sanitised);
900        assert_eq!("funny", username_homoglyphs_sanitised);
901    }
902
903    #[async_std::test]
904    async fn username_sanitisation_padding() {
905        let username_padding = "a";
906
907        let username = User::sanitise_username(username_padding).await.unwrap();
908
909        assert_eq!("a_", username);
910    }
911
912    #[async_std::test]
913    async fn create_user() {
914        use revolt_result::Result;
915
916        database_test!(|db| async move {
917            let mut created_clean = User::create(&db, "Test".to_string(), None, None)
918                .await
919                .unwrap();
920
921            assert_eq!("Test", created_clean.username);
922
923            created_clean
924                .update_username(&db, "Test2".to_string())
925                .await
926                .unwrap();
927
928            assert_eq!("Test2", created_clean.username);
929
930            let created_invalid_result: Result<_> =
931                User::create(&db, "stoat.chat".to_string(), None, None).await;
932
933            assert!(created_invalid_result.is_err());
934
935            let mut updated_invalid = User::create(&db, "Test".to_string(), None, None)
936                .await
937                .unwrap();
938
939            let updated_invalid_update_result = updated_invalid
940                .update_username(&db, "http://test".to_string())
941                .await;
942
943            assert!(updated_invalid_update_result.is_err());
944        });
945    }
946}