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 pub struct User {
24 #[serde(rename = "_id")]
26 pub id: String,
27 pub username: String,
29 pub discriminator: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub display_name: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none", default)]
36 pub pronouns: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub avatar: Option<File>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub relations: Option<Vec<Relationship>>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub badges: Option<i32>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub status: Option<UserStatus>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub profile: Option<UserProfile>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub flags: Option<i32>,
57 #[serde(skip_serializing_if = "crate::if_false", default)]
59 pub privileged: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub bot: Option<BotInformation>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub suspended_until: Option<Timestamp>,
67 pub last_acknowledged_policy_change: Timestamp,
69 },
70 "PartialUser"
71);
72
73auto_derived!(
74 pub enum FieldsUser {
76 Avatar,
77 StatusText,
78 StatusPresence,
79 ProfileContent,
80 ProfileBackground,
81 DisplayName,
82 Pronouns,
83
84 Suspension,
86 None,
87 }
88
89 pub enum RelationshipStatus {
91 None,
92 User,
93 Friend,
94 Outgoing,
95 Incoming,
96 Blocked,
97 BlockedOther,
98 }
99
100 pub struct Relationship {
102 #[serde(rename = "_id")]
103 pub id: String,
104 pub status: RelationshipStatus,
105 }
106
107 pub enum Presence {
109 Online,
111 Idle,
113 Focus,
115 Busy,
117 Invisible,
119 }
120
121 #[derive(Default)]
123 pub struct UserStatus {
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub text: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub presence: Option<Presence>,
130 }
131
132 #[derive(Default)]
134 pub struct UserProfile {
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub content: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub background: Option<File>,
141 }
142
143 pub struct BotInformation {
145 pub owner: String,
147 }
148
149 pub enum UserHint {
151 Any,
153 Bot,
155 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 _ = 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 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 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 self.apply_relationship(
579 db,
580 target,
581 RelationshipStatus::Outgoing,
582 RelationshipStatus::Incoming,
583 )
584 .await
585 }
586 }
587 }
588
589 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 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 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 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 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 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 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 pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
797 unimplemented!()
799 }
800
801 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 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 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 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 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}