Skip to main content

revolt_database/models/users/
model.rs

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