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 pub struct User {
20 #[serde(rename = "_id")]
22 pub id: String,
23 pub username: String,
25 pub discriminator: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub display_name: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub avatar: Option<File>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub relations: Option<Vec<Relationship>>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub badges: Option<i32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub status: Option<UserStatus>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub profile: Option<UserProfile>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub flags: Option<i32>,
50 #[serde(skip_serializing_if = "crate::if_false", default)]
52 pub privileged: bool,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub bot: Option<BotInformation>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub suspended_until: Option<Timestamp>,
60 pub last_acknowledged_policy_change: Timestamp,
62 },
63 "PartialUser"
64);
65
66auto_derived!(
67 pub enum FieldsUser {
69 Avatar,
70 StatusText,
71 StatusPresence,
72 ProfileContent,
73 ProfileBackground,
74 DisplayName,
75
76 Suspension,
78 None,
79 }
80
81 pub enum RelationshipStatus {
83 None,
84 User,
85 Friend,
86 Outgoing,
87 Incoming,
88 Blocked,
89 BlockedOther,
90 }
91
92 pub struct Relationship {
94 #[serde(rename = "_id")]
95 pub id: String,
96 pub status: RelationshipStatus,
97 }
98
99 pub enum Presence {
101 Online,
103 Idle,
105 Focus,
107 Busy,
109 Invisible,
111 }
112
113 #[derive(Default)]
115 pub struct UserStatus {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub text: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub presence: Option<Presence>,
122 }
123
124 #[derive(Default)]
126 pub struct UserProfile {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub content: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub background: Option<File>,
133 }
134
135 pub struct BotInformation {
137 pub owner: String,
139 }
140
141 pub enum UserHint {
143 Any,
145 Bot,
147 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 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 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 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 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 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 pub fn validate_username(username: String) -> Result<String> {
283 let username_lowercase = username.to_lowercase();
285
286 if decancer::cure(&username_lowercase).into_str() != username_lowercase {
288 return Err(create_error!(InvalidUsername));
289 }
290
291 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 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 #[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 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 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 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 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 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 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 _ = 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 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 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 self.apply_relationship(
558 db,
559 target,
560 RelationshipStatus::Outgoing,
561 RelationshipStatus::Incoming,
562 )
563 .await
564 }
565 }
566 }
567
568 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 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 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 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 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 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 &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 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 pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
789 unimplemented!()
791 }
792
793 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 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}