1use std::collections::HashMap;
2use super::{oauth::AuthGrant, permissions::PermissionsContainer, Error, Result, id::Id};
3use serde::{Deserialize, Serialize};
4use totp_rs::TOTP;
5use tritools::{
6 encoding::{hash, hash_salted, salt},
7 time::unix_epoch_timestamp,
8};
9use serde_valid::Validate;
10
11pub type Token = (String, String, u128);
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct User {
16 pub id: Id,
17 pub created: u128,
18 pub username: String,
19 pub password: String,
20 pub salt: String,
21 pub settings: UserSettings,
22 pub tokens: Vec<Token>,
23 pub legacy_permissions: i32,
24 pub is_verified: bool,
25 pub notification_count: usize,
26 pub follower_count: usize,
27 pub following_count: usize,
28 pub last_seen: u128,
29 #[serde(default)]
31 pub totp: String,
32 #[serde(default)]
34 pub recovery_codes: Vec<String>,
35 #[serde(default)]
36 pub post_count: usize,
37 #[serde(default)]
38 pub request_count: usize,
39 #[serde(default)]
41 pub connections: UserConnections,
42 #[serde(default)]
44 pub stripe_id: String,
45 #[serde(default)]
47 pub grants: Vec<AuthGrant>,
48 #[serde(default)]
50 pub associated: Vec<Id>,
51 #[serde(default)]
53 pub achievements: Vec<Achievement>,
54 #[serde(default)]
56 pub ban_reason: String,
57 #[serde(default)]
60 pub is_deactivated: bool,
61 #[serde(default)]
63 pub ban_expire: u128,
64 #[serde(default)]
69 pub checkouts: Vec<String>,
70 #[serde(default)]
72 pub last_policy_consent: u128,
73 #[serde(default)]
75 pub missed_messages_count: usize,
76 #[serde(default)]
78 pub views: usize,
79 #[serde(default)]
81 pub shrimpcamp_link: usize,
82 pub permissions: PermissionsContainer,
83}
84
85pub type UserConnections =
86 HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;
87
88#[derive(Clone, Debug, Serialize, Deserialize, Default)]
89pub enum ThemePreference {
90 #[default]
91 Auto,
92 Dark,
93 Light,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
97pub struct UserSettings {
98 #[serde(default)]
99 #[validate(max_length = 32)]
100 pub display_name: String,
101 #[serde(default)]
102 #[validate(max_length = 4096)]
103 pub biography: String,
104 #[serde(default)]
105 #[validate(max_length = 2048)]
106 pub warning: String,
107 #[serde(default)]
108 pub private_profile: bool,
109 #[serde(default)]
111 pub theme_preference: ThemePreference,
112 #[serde(default)]
115 pub profile_theme: ThemePreference,
116 #[serde(default)]
117 pub private_last_seen: bool,
118 #[serde(default)]
120 pub theme_color_surface: String,
121 #[serde(default)]
123 pub theme_color_text: String,
124 #[serde(default)]
126 pub theme_color_text_link: String,
127 #[serde(default)]
129 pub theme_color_shadow: String,
130 #[serde(default)]
132 pub theme_color_lowered: String,
133 #[serde(default)]
135 pub theme_color_text_lowered: String,
136 #[serde(default)]
138 pub theme_color_super_lowered: String,
139 #[serde(default)]
141 pub theme_color_raised: String,
142 #[serde(default)]
144 pub theme_color_text_raised: String,
145 #[serde(default)]
147 pub theme_color_super_raised: String,
148 #[serde(default)]
150 pub theme_color_primary: String,
151 #[serde(default)]
153 pub theme_color_text_primary: String,
154 #[serde(default)]
156 pub theme_color_primary_lowered: String,
157 #[serde(default)]
159 pub theme_color_secondary: String,
160 #[serde(default)]
162 pub theme_color_text_secondary: String,
163 #[serde(default)]
165 pub theme_color_secondary_lowered: String,
166 #[serde(default)]
168 pub theme_custom_css: String,
169 #[serde(default)]
171 pub theme_color_online: String,
172 #[serde(default)]
174 pub theme_color_idle: String,
175 #[serde(default)]
177 pub theme_color_offline: String,
178 #[serde(default)]
179 pub disable_other_themes: bool,
180 #[serde(default)]
181 pub enable_questions: bool,
182 #[serde(default)]
184 pub motivational_header: String,
185 #[serde(default)]
187 pub allow_anonymous_questions: bool,
188 #[serde(default)]
190 pub anonymous_username: String,
191 #[serde(default)]
193 pub anonymous_avatar_url: String,
194 #[serde(default)]
196 pub hide_dislikes: bool,
197 #[serde(default)]
199 pub require_account: bool,
200 #[serde(default)]
202 pub show_nsfw: bool,
203 #[serde(default)]
205 pub inbox_homepage: bool,
206 #[serde(default)]
208 pub muted: Vec<String>,
209 #[serde(default)]
211 pub enable_drawings: bool,
212 #[serde(default)]
214 pub all_timeline_hide_answers: bool,
215 #[serde(default)]
217 pub large_text: bool,
218 #[serde(default)]
220 pub disable_achievements: bool,
221 #[serde(default)]
226 pub hide_from_social_lists: bool,
227 #[serde(default)]
229 pub private_biography: String,
230 #[serde(default)]
233 pub hide_social_follows: bool,
234 #[serde(default)]
237 #[validate(max_length = 128)]
238 pub location: String,
239 #[serde(default)]
241 #[validate(max_items = 15)]
242 #[validate(unique_items)]
243 pub links: Vec<(String, String)>,
244 #[serde(default)]
246 pub auto_nsfw: bool,
247 #[serde(default)]
249 pub auto_limit_posts: bool,
250}
251
252impl UserSettings {
253 pub fn verify_values(&self) -> Result<()> {
254 if let Err(e) = self.validate() {
255 return Err(Error::MiscError(e.to_string()));
256 }
257
258 Ok(())
259 }
260}
261
262impl Default for User {
263 fn default() -> Self {
264 Self::new("<unknown>".to_string(), String::new())
265 }
266}
267
268impl User {
269 pub fn new(username: String, password: String) -> Self {
271 let salt = salt();
272 let password = hash_salted(password, salt.clone());
273 let created = unix_epoch_timestamp();
274
275 Self {
276 id: Id::new(),
277 created,
278 username,
279 password,
280 salt,
281 settings: UserSettings::default(),
282 tokens: Vec::new(),
283 legacy_permissions: 0,
284 is_verified: false,
285 notification_count: 0,
286 follower_count: 0,
287 following_count: 0,
288 last_seen: created,
289 totp: String::new(),
290 recovery_codes: Vec::new(),
291 post_count: 0,
292 request_count: 0,
293 connections: HashMap::new(),
294 stripe_id: String::new(),
295 grants: Vec::new(),
296 associated: Vec::new(),
297 achievements: Vec::new(),
298 ban_reason: String::new(),
299 is_deactivated: false,
300 ban_expire: 0,
301 checkouts: Vec::new(),
302 last_policy_consent: created,
303 missed_messages_count: 0,
304 views: 0,
305 shrimpcamp_link: 0,
306 permissions: PermissionsContainer::new(),
307 }
308 }
309
310 pub fn deleted() -> Self {
312 Self {
313 username: "<deleted>".to_string(),
314 id: Id::Legacy(0),
315 ..Default::default()
316 }
317 }
318
319 pub fn banned() -> Self {
321 Self {
322 username: "<banned>".to_string(),
323 id: Id::Legacy(0),
324 ..Default::default()
325 }
326 }
327
328 pub fn anonymous() -> Self {
330 Self {
331 username: "anonymous".to_string(),
332 id: Id::Legacy(0),
333 ..Default::default()
334 }
335 }
336
337 pub fn create_token(ip: &str) -> (String, Token) {
342 let unhashed = Id::new().printable();
343 (
344 unhashed.clone(),
345 (ip.to_string(), hash(unhashed), unix_epoch_timestamp()),
346 )
347 }
348
349 pub fn check_password(&self, against: String) -> bool {
351 self.password == hash_salted(against, self.salt.clone())
352 }
353
354 pub fn parse_mentions(input: &str) -> Vec<String> {
356 let mut escape: bool = false;
358 let mut at: bool = false;
359 let mut buffer: String = String::new();
360 let mut out = Vec::new();
361
362 for char in input.chars() {
364 if ((char == '\\') | (char == '/')) && !escape {
365 escape = true;
366 continue;
367 }
368
369 if (char == '@') && !escape {
370 at = true;
371 continue; }
373
374 if at {
375 if char == ' ' {
376 at = false;
378
379 if !out.contains(&buffer) {
380 out.push(buffer);
381 }
382
383 buffer = String::new();
384 continue;
385 }
386
387 buffer.push(char);
389 }
390
391 escape = false;
392 }
393
394 if !buffer.is_empty() {
395 out.push(buffer);
396 }
397
398 if out.len() > 5 {
399 return Vec::new();
401 }
402
403 out
405 }
406
407 pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
409 if self.totp.is_empty() {
410 return None;
411 }
412
413 TOTP::new(
414 totp_rs::Algorithm::SHA1,
415 6,
416 1,
417 30,
418 self.totp.as_bytes().to_owned(),
419 Some(issuer.unwrap_or("tetratto!".to_string())),
420 self.username.clone(),
421 )
422 .ok()
423 }
424
425 pub fn clean(&mut self) {
427 self.password = String::new();
428 self.salt = String::new();
429
430 self.tokens = Vec::new();
431 self.grants = Vec::new();
432
433 self.recovery_codes = Vec::new();
434 self.totp = String::new();
435
436 self.settings = UserSettings::default();
437 self.stripe_id = String::new();
438 self.connections = HashMap::new();
439 }
440
441 pub fn get_grant_by_app_id(&self, id: &crate::model::id::Id) -> Option<&AuthGrant> {
446 self.grants.iter().find(|x| x.app == *id)
447 }
448}
449
450#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
451pub enum ConnectionService {
452 LastFm,
454 #[serde(other)]
455 Unknown,
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
459pub enum ConnectionType {
460 Token,
462 PKCE,
464 None,
466}
467
468#[derive(Clone, Debug, Serialize, Deserialize)]
469pub struct ExternalConnectionInfo {
470 pub con_type: ConnectionType,
471 pub data: HashMap<String, String>,
472 pub show_on_profile: bool,
473}
474
475#[derive(Clone, Debug, Serialize, Deserialize, Default)]
476pub struct ExternalConnectionData {
477 pub external_urls: HashMap<String, String>,
478 pub data: HashMap<String, String>,
479}
480
481pub const ACHIEVEMENTS: usize = 29;
483pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
485 AchievementName::AcceptProfileWarning,
486 AchievementName::OpenSessionSettings,
487];
488
489#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
490pub enum AchievementName {
491 CreatePost,
492 FollowUser,
493 Create50Posts,
494 Create100Posts,
495 Create1000Posts,
496 CreateQuestion,
497 EditSettings,
498 FollowedByStaff,
499 CreateDrawing,
500 OpenAchievements,
501 Get1Like,
502 Get10Likes,
503 Get50Likes,
504 Get100Likes,
505 Get25Dislikes,
506 Get1Follower,
507 Get10Followers,
508 Get50Followers,
509 Get100Followers,
510 Follow10Users,
511 CreateDraft,
512 EditPost,
513 Enable2fa,
514 CreateRepost,
515 GetAllOtherAchievements,
516 AcceptProfileWarning,
517 OpenSessionSettings,
518 #[serde(other)]
519 Removed,
520}
521
522#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
523pub enum AchievementRarity {
524 Common,
525 Uncommon,
526 Rare,
527}
528
529impl AchievementName {
530 pub fn title(&self) -> &str {
531 match self {
532 Self::CreatePost => "Dear friends,",
533 Self::FollowUser => "Virtual connections...",
534 Self::Create50Posts => "Hello, world!",
535 Self::Create100Posts => "It's my world",
536 Self::Create1000Posts => "Timeline domination",
537 Self::CreateQuestion => "Big questions...",
538 Self::EditSettings => "Just how I like it!",
539 Self::FollowedByStaff => "Big Shrimpin'",
540 Self::CreateDrawing => "Modern art",
541 Self::OpenAchievements => "Welcome!",
542 Self::Get1Like => "Baby steps!",
543 Self::Get10Likes => "WOW! 10 LIKES!",
544 Self::Get50Likes => "banger post follow for more",
545 Self::Get100Likes => "everyone liked that",
546 Self::Get25Dislikes => "Sorry...",
547 Self::Get1Follower => "Friends?",
548 Self::Get10Followers => "Friends!",
549 Self::Get50Followers => "50 WHOLE FOLLOWERS??",
550 Self::Get100Followers => "Everyone is my friend!",
551 Self::Follow10Users => "Big fan",
552 Self::CreateDraft => "Maybe later!",
553 Self::EditPost => "Grammar police?",
554 Self::Enable2fa => "Locked in",
555 Self::CreateRepost => "More than a like or comment...",
556 Self::GetAllOtherAchievements => "The final performance",
557 Self::AcceptProfileWarning => "I accept the risks!",
558 Self::OpenSessionSettings => "Am I alone in here?",
559 Self::Removed => "Removed achievement",
560 }
561 }
562
563 pub fn description(&self) -> &str {
564 match self {
565 Self::CreatePost => "Create your first post!",
566 Self::FollowUser => "Follow somebody!",
567 Self::Create50Posts => "Create your 50th post.",
568 Self::Create100Posts => "Create your 100th post.",
569 Self::Create1000Posts => "Create your 1000th post.",
570 Self::CreateQuestion => "Ask your first question!",
571 Self::EditSettings => "Edit your settings.",
572 Self::FollowedByStaff => "Get followed by a staff member!",
573 Self::CreateDrawing => "Include a drawing in a question.",
574 Self::OpenAchievements => "Open the achievements page.",
575 Self::Get1Like => "Get 1 like on a post! Good job!",
576 Self::Get10Likes => "Get 10 likes on one post.",
577 Self::Get50Likes => "Get 50 likes on one post.",
578 Self::Get100Likes => "Get 100 likes on one post.",
579 Self::Get25Dislikes => "Get 25 dislikes on one post... :(",
580 Self::Get1Follower => "Get 1 follower. Cool!",
581 Self::Get10Followers => "Get 10 followers. You're getting popular!",
582 Self::Get50Followers => "Get 50 followers. Okay, you're fairly popular!",
583 Self::Get100Followers => "Get 100 followers. You might be famous..?",
584 Self::Follow10Users => "Follow 10 other users. I'm sure people appreciate it!",
585 Self::CreateDraft => "Save a post as a draft.",
586 Self::EditPost => "Edit a post.",
587 Self::Enable2fa => "Enable TOTP 2FA.",
588 Self::CreateRepost => "Create a repost or quote.",
589 Self::GetAllOtherAchievements => "Get every other achievement.",
590 Self::AcceptProfileWarning => "Accept a profile warning.",
591 Self::OpenSessionSettings => "Open your session settings.",
592 Self::Removed => "An achievement that was removed.",
593 }
594 }
595
596 pub fn rarity(&self) -> AchievementRarity {
597 use AchievementRarity::*;
599 match self {
600 Self::CreatePost => Common,
601 Self::FollowUser => Common,
602 Self::Create50Posts => Uncommon,
603 Self::Create100Posts => Uncommon,
604 Self::Create1000Posts => Rare,
605 Self::CreateQuestion => Common,
606 Self::EditSettings => Common,
607 Self::FollowedByStaff => Rare,
608 Self::CreateDrawing => Common,
609 Self::OpenAchievements => Common,
610 Self::Get1Like => Common,
611 Self::Get10Likes => Common,
612 Self::Get50Likes => Uncommon,
613 Self::Get100Likes => Rare,
614 Self::Get25Dislikes => Uncommon,
615 Self::Get1Follower => Common,
616 Self::Get10Followers => Common,
617 Self::Get50Followers => Uncommon,
618 Self::Get100Followers => Rare,
619 Self::Follow10Users => Common,
620 Self::CreateDraft => Common,
621 Self::EditPost => Common,
622 Self::Enable2fa => Rare,
623 Self::CreateRepost => Common,
624 Self::GetAllOtherAchievements => Rare,
625 Self::AcceptProfileWarning => Common,
626 Self::OpenSessionSettings => Common,
627 Self::Removed => Rare,
628 }
629 }
630}
631
632impl From<AchievementName> for Achievement {
633 fn from(val: AchievementName) -> Self {
634 Achievement {
635 name: val,
636 unlocked: unix_epoch_timestamp(),
637 }
638 }
639}
640
641#[derive(Clone, Debug, Serialize, Deserialize)]
642pub struct Achievement {
643 pub name: AchievementName,
644 pub unlocked: u128,
645}
646
647#[derive(Debug, Serialize, Deserialize)]
648pub struct Notification {
649 pub id: Id,
650 pub created: u128,
651 pub title: String,
652 pub content: String,
653 pub owner: Id,
654 pub read: bool,
655 pub tag: String,
656}
657
658impl Notification {
659 pub fn new(title: String, content: String, owner: Id) -> Self {
661 Self {
662 id: Id::new(),
663 created: unix_epoch_timestamp(),
664 title,
665 content,
666 owner,
667 read: false,
668 tag: String::new(),
669 }
670 }
671}
672
673#[derive(Clone, Debug, Serialize, Deserialize)]
674pub struct UserFollow {
675 pub id: Id,
676 pub created: u128,
677 pub initiator: Id,
678 pub receiver: Id,
679}
680
681impl UserFollow {
682 pub fn new(initiator: Id, receiver: Id) -> Self {
684 Self {
685 id: Id::new(),
686 created: unix_epoch_timestamp(),
687 initiator,
688 receiver,
689 }
690 }
691}
692
693#[derive(Serialize, Deserialize)]
694pub struct UserBlock {
695 pub id: Id,
696 pub created: u128,
697 pub initiator: Id,
698 pub receiver: Id,
699}
700
701impl UserBlock {
702 pub fn new(initiator: Id, receiver: Id) -> Self {
704 Self {
705 id: Id::new(),
706 created: unix_epoch_timestamp(),
707 initiator,
708 receiver,
709 }
710 }
711}
712
713#[derive(Serialize, Deserialize)]
714pub struct IpBlock {
715 pub id: Id,
716 pub created: u128,
717 pub initiator: Id,
718 pub receiver: String,
719}
720
721impl IpBlock {
722 pub fn new(initiator: Id, receiver: String) -> Self {
724 Self {
725 id: Id::new(),
726 created: unix_epoch_timestamp(),
727 initiator,
728 receiver,
729 }
730 }
731}
732
733#[derive(Serialize, Deserialize)]
734pub struct IpBan {
735 pub ip: String,
736 pub created: u128,
737 pub reason: String,
738 pub moderator: Id,
739}
740
741impl IpBan {
742 pub fn new(ip: String, moderator: Id, reason: String) -> Self {
744 Self {
745 ip,
746 created: unix_epoch_timestamp(),
747 reason,
748 moderator,
749 }
750 }
751}
752
753#[derive(Serialize, Deserialize)]
754pub struct UserWarning {
755 pub id: Id,
756 pub created: u128,
757 pub receiver: Id,
758 pub moderator: Id,
759 pub content: String,
760}
761
762impl UserWarning {
763 pub fn new(receiver: Id, moderator: Id, content: String) -> Self {
765 Self {
766 id: Id::new(),
767 created: unix_epoch_timestamp(),
768 receiver,
769 moderator,
770 content,
771 }
772 }
773}
774
775#[derive(Clone, Debug, Serialize, Deserialize)]
776pub struct InviteCode {
777 pub id: Id,
778 pub created: u128,
779 pub owner: crate::model::id::Id,
780 pub code: String,
781 pub is_used: bool,
782}
783
784impl InviteCode {
785 pub fn new(owner: crate::model::id::Id) -> Self {
787 Self {
788 id: Id::new(),
789 created: unix_epoch_timestamp(),
790 owner,
791 code: salt(),
792 is_used: false,
793 }
794 }
795}
796
797#[derive(Clone, Debug, Serialize, Deserialize)]
798pub struct ProfileView {
799 pub id: Id,
800 pub created: u128,
801 pub owner: crate::model::id::Id,
802 pub profile: usize,
803}
804
805impl ProfileView {
806 pub fn new(owner: crate::model::id::Id, profile: usize) -> Self {
808 Self {
809 id: Id::new(),
810 created: unix_epoch_timestamp(),
811 owner,
812 profile,
813 }
814 }
815}