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