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