Skip to main content

tetratto_core/model/
auth.rs

1use std::collections::HashMap;
2use crate::model::{Error, Result};
3
4use super::{
5    oauth::AuthGrant,
6    permissions::{FinePermission, SecondaryPermission},
7};
8use serde::{Deserialize, Serialize};
9use totp_rs::TOTP;
10use tetratto_shared::{
11    hash::{hash_salted, salt},
12    snow::Snowflake,
13    unix_epoch_timestamp,
14};
15use serde_valid::Validate;
16
17/// `(ip, token, creation timestamp)`
18pub type Token = (String, String, usize);
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct User {
22    pub id: usize,
23    pub created: usize,
24    pub username: String,
25    pub password: String,
26    pub salt: String,
27    pub settings: UserSettings,
28    pub tokens: Vec<Token>,
29    pub permissions: FinePermission,
30    pub is_verified: bool,
31    pub notification_count: usize,
32    pub follower_count: usize,
33    pub following_count: usize,
34    pub last_seen: usize,
35    /// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
36    #[serde(default)]
37    pub totp: String,
38    /// The TOTP recovery codes for this profile.
39    #[serde(default)]
40    pub recovery_codes: Vec<String>,
41    #[serde(default)]
42    pub post_count: usize,
43    #[serde(default)]
44    pub request_count: usize,
45    /// External service connection details.
46    #[serde(default)]
47    pub connections: UserConnections,
48    /// The user's Stripe customer ID.
49    #[serde(default)]
50    pub stripe_id: String,
51    /// The grants associated with the user's account.
52    #[serde(default)]
53    pub grants: Vec<AuthGrant>,
54    /// A list of the IDs of all accounts the user has signed into through the UI.
55    #[serde(default)]
56    pub associated: Vec<usize>,
57    /// The ID of the [`InviteCode`] this user provided during registration.
58    #[serde(default)]
59    pub invite_code: usize,
60    /// Secondary permissions because the regular permissions struct ran out of possible bits.
61    #[serde(default)]
62    pub secondary_permissions: SecondaryPermission,
63    /// Users collect achievements through little actions across the site.
64    #[serde(default)]
65    pub achievements: Vec<Achievement>,
66    /// If the account was registered as a "bought" account, the user should not
67    /// be allowed to actually use the account if they haven't paid for supporter yet.
68    #[serde(default)]
69    pub awaiting_purchase: bool,
70    /// This value cannot be changed after account creation. This value is used to
71    /// lock the user's account again if the subscription is cancelled and they haven't
72    /// used an invite code.
73    #[serde(default)]
74    pub was_purchased: bool,
75    /// The reason the user was banned.
76    #[serde(default)]
77    pub ban_reason: String,
78    /// If the user is deactivated. Deactivated users act almost like deleted
79    /// users, but their data is not wiped.
80    #[serde(default)]
81    pub is_deactivated: bool,
82    /// The time at which the user's ban will automatically expire.
83    #[serde(default)]
84    pub ban_expire: usize,
85    /// The IDs of Stripe checkout sessions that this user has successfully completed.
86    ///
87    /// This should be checked BEFORE applying purchases to ensure that the user hasn't
88    /// already applied this purchase.
89    #[serde(default)]
90    pub checkouts: Vec<String>,
91    /// The time in which the user last consented to the site's policies.
92    #[serde(default)]
93    pub last_policy_consent: usize,
94    /// The ID of the user's close friends stack.
95    ///
96    /// The user's close friends stack is a circle stack which only allows the owner
97    /// (the user) to post to it.
98    #[serde(default)]
99    pub close_friends_stack: usize,
100    /// The number of messages this user has missed.
101    #[serde(default)]
102    pub missed_messages_count: usize,
103    /// The number of unique authenticated users who have viewed this user's profile.
104    #[serde(default)]
105    pub views: usize,
106    /// The ID of the Shrimpcamp account the user is linked to.
107    #[serde(default)]
108    pub shrimpcamp_link: usize,
109}
110
111pub type UserConnections =
112    HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;
113
114#[derive(Clone, Debug, Serialize, Deserialize, Default)]
115pub enum ThemePreference {
116    #[default]
117    Auto,
118    Dark,
119    Light,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize, Default)]
123pub enum DefaultTimelineChoice {
124    #[default]
125    MyCommunities,
126    MyCommunitiesQuestions,
127    PopularPosts,
128    PopularQuestions,
129    FollowingPosts,
130    FollowingQuestions,
131    AllPosts,
132    AllQuestions,
133    Stack(String),
134}
135
136impl DefaultTimelineChoice {
137    /// Get the relative URL that the timeline should bring you to.
138    pub fn relative_url(&self) -> String {
139        match &self {
140            Self::MyCommunities => "/".to_string(),
141            Self::MyCommunitiesQuestions => "/questions".to_string(),
142            Self::PopularPosts => "/popular".to_string(),
143            Self::PopularQuestions => "/popular/questions".to_string(),
144            Self::FollowingPosts => "/following".to_string(),
145            Self::FollowingQuestions => "/following/questions".to_string(),
146            Self::AllPosts => "/all".to_string(),
147            Self::AllQuestions => "/all/questions".to_string(),
148            Self::Stack(id) => format!("/stacks/{id}"),
149        }
150    }
151}
152
153#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
154pub enum DefaultProfileTabChoice {
155    /// General posts (in any community) from the user.
156    #[default]
157    Posts,
158    /// Responses to questions.
159    Responses,
160    /// The profile's guestbook.
161    Guestbook,
162}
163
164#[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
165pub struct UserSettings {
166    #[serde(default)]
167    #[validate(max_length = 32)]
168    pub display_name: String,
169    #[serde(default)]
170    #[validate(max_length = 4096)]
171    pub biography: String,
172    #[serde(default)]
173    #[validate(max_length = 2048)]
174    pub warning: String,
175    #[serde(default)]
176    pub private_profile: bool,
177    #[serde(default)]
178    pub private_communities: bool,
179    /// The theme shown to the user.
180    #[serde(default)]
181    pub theme_preference: ThemePreference,
182    /// The theme used on the user's profile. Setting this to `Auto` will use
183    /// the viewing user's `theme_preference` setting.
184    #[serde(default)]
185    pub profile_theme: ThemePreference,
186    #[serde(default)]
187    pub private_last_seen: bool,
188    #[serde(default)]
189    pub theme_hue: String,
190    #[serde(default)]
191    pub theme_sat: String,
192    #[serde(default)]
193    pub theme_lit: String,
194    /// Page background.
195    #[serde(default)]
196    pub theme_color_surface: String,
197    /// Text on elements with the surface backgrounds.
198    #[serde(default)]
199    pub theme_color_text: String,
200    /// Links on all elements.
201    #[serde(default)]
202    pub theme_color_text_link: String,
203    /// Box shadow color.
204    #[serde(default)]
205    pub theme_color_shadow: String,
206    /// Some cards, buttons, or anything else with a darker background color than the surface.
207    #[serde(default)]
208    pub theme_color_lowered: String,
209    /// Text on elements with the lowered backgrounds.
210    #[serde(default)]
211    pub theme_color_text_lowered: String,
212    /// Borders.
213    #[serde(default)]
214    pub theme_color_super_lowered: String,
215    /// Some cards, buttons, or anything else with a lighter background color than the surface.
216    #[serde(default)]
217    pub theme_color_raised: String,
218    /// Text on elements with the raised backgrounds.
219    #[serde(default)]
220    pub theme_color_text_raised: String,
221    /// Some borders.
222    #[serde(default)]
223    pub theme_color_super_raised: String,
224    /// Primary color; navigation bar, some buttons, etc.
225    #[serde(default)]
226    pub theme_color_primary: String,
227    /// Text on elements with the primary backgrounds.
228    #[serde(default)]
229    pub theme_color_text_primary: String,
230    /// Hover state for primary buttons.
231    #[serde(default)]
232    pub theme_color_primary_lowered: String,
233    /// Secondary color.
234    #[serde(default)]
235    pub theme_color_secondary: String,
236    /// Text on elements with the secondary backgrounds.
237    #[serde(default)]
238    pub theme_color_text_secondary: String,
239    /// Hover state for secondary buttons.
240    #[serde(default)]
241    pub theme_color_secondary_lowered: String,
242    /// Custom CSS input.
243    #[serde(default)]
244    pub theme_custom_css: String,
245    /// The color of an online online indicator.
246    #[serde(default)]
247    pub theme_color_online: String,
248    /// The color of an idle online indicator.
249    #[serde(default)]
250    pub theme_color_idle: String,
251    /// The color of an offline online indicator.
252    #[serde(default)]
253    pub theme_color_offline: String,
254    #[serde(default)]
255    pub disable_other_themes: bool,
256    #[serde(default)]
257    pub disable_other_theme_css: bool,
258    #[serde(default)]
259    pub enable_questions: bool,
260    /// A header shown in the place of "Ask question" if `enable_questions` is true.
261    #[serde(default)]
262    pub motivational_header: String,
263    /// If questions from anonymous users are allowed. Requires `enable_questions`.
264    #[serde(default)]
265    pub allow_anonymous_questions: bool,
266    /// The username used for anonymous users.
267    #[serde(default)]
268    pub anonymous_username: String,
269    /// The URL of the avatar used for anonymous users.
270    #[serde(default)]
271    pub anonymous_avatar_url: String,
272    /// If dislikes are hidden for the user.
273    #[serde(default)]
274    pub hide_dislikes: bool,
275    /// The timeline that the "Home" button takes you to.
276    #[serde(default)]
277    pub default_timeline: DefaultTimelineChoice,
278    /// If other users that you aren't following can add you to chats.
279    #[serde(default)]
280    pub private_chats: bool,
281    /// If other users that you aren't following can send you letters.
282    #[serde(default)]
283    pub private_mails: bool,
284    /// The user's status. Shows over connection info.
285    #[serde(default)]
286    #[validate(max_length = 256)]
287    pub status: String,
288    /// The mime type of the user's banner.
289    #[serde(default = "mime_avif")]
290    pub banner_mime: String,
291    /// Require an account to view the user's profile.
292    #[serde(default)]
293    pub require_account: bool,
294    /// If NSFW content should be shown.
295    #[serde(default)]
296    pub show_nsfw: bool,
297    /// If extra post tabs are hidden (replies, media).
298    #[serde(default)]
299    pub hide_extra_post_tabs: bool,
300    /// A list of strings the user has muted.
301    #[serde(default)]
302    pub muted: Vec<String>,
303    /// If timelines are paged instead of infinitely scrolled.
304    #[serde(default)]
305    pub paged_timelines: bool,
306    /// If drawings are enabled for questions sent to the user.
307    #[serde(default)]
308    pub enable_drawings: bool,
309    /// Automatically unlist posts from timelines.
310    #[serde(default)]
311    pub auto_unlist: bool,
312    /// Hide posts that are answering a question on the "All" timeline.
313    #[serde(default)]
314    pub all_timeline_hide_answers: bool,
315    /// Automatically clear all notifications when notifications are viewed.
316    #[serde(default)]
317    pub auto_clear_notifs: bool,
318    /// Increase the text size of buttons and paragraphs.
319    #[serde(default)]
320    pub large_text: bool,
321    /// Disable achievements.
322    #[serde(default)]
323    pub disable_achievements: bool,
324    /// Automatically hide users that you've blocked on your other accounts from your timelines.
325    #[serde(default)]
326    pub hide_associated_blocked_users: bool,
327    /// Which tab is shown by default on the user's profile.
328    #[serde(default)]
329    pub default_profile_tab: DefaultProfileTabChoice,
330    /// If the user is hidden from followers/following tabs.
331    ///
332    /// The user will still impact the followers/following numbers, but will not
333    /// be shown in the UI (or API).
334    #[serde(default)]
335    pub hide_from_social_lists: bool,
336    /// Automatically hide your posts from all timelines except your profile
337    /// and the following timeline.
338    #[serde(default)]
339    pub auto_full_unlist: bool,
340    /// Biography shown on `profile/private.lisp` page.
341    #[serde(default)]
342    pub private_biography: String,
343    /// If the followers/following links are hidden from the user's profile.
344    /// Will also revoke access to their respective pages.
345    #[serde(default)]
346    pub hide_social_follows: bool,
347    /// The signature automatically attached to new mail letters.
348    #[serde(default)]
349    #[validate(max_length = 2048)]
350    pub mail_signature: String,
351    /// The signature automatically attached to new forum posts.
352    #[serde(default)]
353    #[validate(max_length = 2048)]
354    pub forum_signature: String,
355    /// Hide all badges from your username (everywhere but on profile).
356    #[serde(default)]
357    pub hide_username_badges: bool,
358    /// If the user's system font is always used over Lexend.
359    #[serde(default)]
360    pub use_system_font: bool,
361    /// The user's location. This isn't actually verified or anything, so it can really
362    /// be whatever the user wants.
363    #[serde(default)]
364    #[validate(max_length = 128)]
365    pub location: String,
366    /// External links for the user's other profiles on other websites.
367    #[serde(default)]
368    #[validate(max_items = 15)]
369    #[validate(unique_items)]
370    pub links: Vec<(String, String)>,
371    /// Unround corners in the UI (12px->6px).
372    #[serde(default)]
373    pub unround_corners: bool,
374    /// Allows anonymous users to post in the user's "guestbook".
375    #[serde(default)]
376    pub enable_guestbook: bool,
377    /// Require anonymous posts in the guestbook to be sent to the user's requests
378    /// before they're published to the user's profile.
379    #[serde(default)]
380    pub guestbook_require_review: bool,
381    /// A header shown in the place of "Leave a message" in the guestbook.
382    #[serde(default)]
383    pub guestbook_motivational_header: String,
384    /// The simplified profile removes all profile information.
385    /// The Tetratto profile is just used to showcase their
386    /// posts, answers, guestbook, etc.
387    #[serde(default)]
388    pub simplified_profile: bool,
389    /// The maximum percentage of removed posts that is acceptable in timelines.
390    #[serde(default = "default_maximum_timeline_removed_percentage")]
391    pub maximum_timeline_removed_percentage: f64,
392    /// Removes post views (from all posts and from your posts for others).
393    #[serde(default)]
394    pub disable_views: bool,
395}
396
397pub fn default_maximum_timeline_removed_percentage() -> f64 {
398    87.0
399}
400
401impl UserSettings {
402    pub fn verify_values(&self) -> Result<()> {
403        if let Err(e) = self.validate() {
404            return Err(Error::MiscError(e.to_string()));
405        }
406
407        Ok(())
408    }
409}
410
411fn mime_avif() -> String {
412    "image/avif".to_string()
413}
414
415impl Default for User {
416    fn default() -> Self {
417        Self::new("<unknown>".to_string(), String::new())
418    }
419}
420
421impl User {
422    /// Create a new [`User`].
423    pub fn new(username: String, password: String) -> Self {
424        let salt = salt();
425        let password = hash_salted(password, salt.clone());
426        let created = unix_epoch_timestamp();
427
428        Self {
429            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
430            created,
431            username,
432            password,
433            salt,
434            settings: {
435                let mut tmpl = UserSettings::default();
436                tmpl.maximum_timeline_removed_percentage =
437                    default_maximum_timeline_removed_percentage();
438                tmpl
439            },
440            tokens: Vec::new(),
441            permissions: FinePermission::DEFAULT,
442            is_verified: false,
443            notification_count: 0,
444            follower_count: 0,
445            following_count: 0,
446            last_seen: created,
447            totp: String::new(),
448            recovery_codes: Vec::new(),
449            post_count: 0,
450            request_count: 0,
451            connections: HashMap::new(),
452            stripe_id: String::new(),
453            grants: Vec::new(),
454            associated: Vec::new(),
455            invite_code: 0,
456            secondary_permissions: SecondaryPermission::DEFAULT,
457            achievements: Vec::new(),
458            awaiting_purchase: false,
459            was_purchased: false,
460            ban_reason: String::new(),
461            is_deactivated: false,
462            ban_expire: 0,
463            checkouts: Vec::new(),
464            last_policy_consent: created,
465            close_friends_stack: 0,
466            missed_messages_count: 0,
467            views: 0,
468            shrimpcamp_link: 0,
469        }
470    }
471
472    /// Deleted user profile.
473    pub fn deleted() -> Self {
474        Self {
475            username: "<deleted>".to_string(),
476            id: 0,
477            ..Default::default()
478        }
479    }
480
481    /// Banned user profile.
482    pub fn banned() -> Self {
483        Self {
484            username: "<banned>".to_string(),
485            id: 0,
486            ..Default::default()
487        }
488    }
489
490    /// Anonymous user profile.
491    pub fn anonymous() -> Self {
492        Self {
493            username: "anonymous".to_string(),
494            id: 0,
495            ..Default::default()
496        }
497    }
498
499    /// Create a new token
500    ///
501    /// # Returns
502    /// `(unhashed id, token)`
503    pub fn create_token(ip: &str) -> (String, Token) {
504        let unhashed = tetratto_shared::hash::uuid();
505        (
506            unhashed.clone(),
507            (
508                ip.to_string(),
509                tetratto_shared::hash::hash(unhashed),
510                unix_epoch_timestamp(),
511            ),
512        )
513    }
514
515    /// Check if the given password is correct for the user.
516    pub fn check_password(&self, against: String) -> bool {
517        self.password == hash_salted(against, self.salt.clone())
518    }
519
520    /// Parse user mentions in a given `input`.
521    pub fn parse_mentions(input: &str) -> Vec<String> {
522        // state
523        let mut escape: bool = false;
524        let mut at: bool = false;
525        let mut buffer: String = String::new();
526        let mut out = Vec::new();
527
528        // parse
529        for char in input.chars() {
530            if ((char == '\\') | (char == '/')) && !escape {
531                escape = true;
532                continue;
533            }
534
535            if (char == '@') && !escape {
536                at = true;
537                continue; // don't push @
538            }
539
540            if at {
541                if char == ' ' {
542                    // reached space, end @
543                    at = false;
544
545                    if !out.contains(&buffer) {
546                        out.push(buffer);
547                    }
548
549                    buffer = String::new();
550                    continue;
551                }
552
553                // push mention text
554                buffer.push(char);
555            }
556
557            escape = false;
558        }
559
560        if !buffer.is_empty() {
561            out.push(buffer);
562        }
563
564        if out.len() > 5 {
565            // if we're trying to mention more than 5 people, mention nobody (we're a spammer)
566            return Vec::new();
567        }
568
569        // return
570        out
571    }
572
573    /// Get a [`TOTP`] from the profile's `totp` secret value.
574    pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
575        if self.totp.is_empty() {
576            return None;
577        }
578
579        TOTP::new(
580            totp_rs::Algorithm::SHA1,
581            6,
582            1,
583            30,
584            self.totp.as_bytes().to_owned(),
585            Some(issuer.unwrap_or("tetratto!".to_string())),
586            self.username.clone(),
587        )
588        .ok()
589    }
590
591    /// Clean the struct for public viewing.
592    pub fn clean(&mut self) {
593        self.password = String::new();
594        self.salt = String::new();
595
596        self.tokens = Vec::new();
597        self.grants = Vec::new();
598
599        self.recovery_codes = Vec::new();
600        self.totp = String::new();
601
602        self.settings = UserSettings::default();
603        self.stripe_id = String::new();
604        self.connections = HashMap::new();
605    }
606
607    /// Get a grant from the user given the grant's `app` ID.
608    ///
609    /// Should be used **before** adding another grant (to ensure the app doesn't
610    /// already have a grant for this user).
611    pub fn get_grant_by_app_id(&self, id: usize) -> Option<&AuthGrant> {
612        self.grants.iter().find(|x| x.app == id)
613    }
614}
615
616#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
617pub enum ConnectionService {
618    /// A connection to a Spotify account.
619    Spotify,
620    /// A connection to a last.fm account.
621    LastFm,
622}
623
624#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
625pub enum ConnectionType {
626    /// A connection through a token which never expires.
627    Token,
628    /// <https://www.rfc-editor.org/rfc/rfc7636>
629    PKCE,
630    /// A connection with no stored authentication.
631    None,
632}
633
634#[derive(Clone, Debug, Serialize, Deserialize)]
635pub struct ExternalConnectionInfo {
636    pub con_type: ConnectionType,
637    pub data: HashMap<String, String>,
638    pub show_on_profile: bool,
639}
640
641#[derive(Clone, Debug, Serialize, Deserialize, Default)]
642pub struct ExternalConnectionData {
643    pub external_urls: HashMap<String, String>,
644    pub data: HashMap<String, String>,
645}
646
647/// The total number of achievements needed to 100% Tetratto!
648pub const ACHIEVEMENTS: usize = 34;
649/// "self-serve" achievements can be granted by the user through the API.
650pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
651    AchievementName::OpenReference,
652    AchievementName::OpenTos,
653    AchievementName::OpenPrivacyPolicy,
654    AchievementName::AcceptProfileWarning,
655    AchievementName::OpenSessionSettings,
656];
657
658#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
659pub enum AchievementName {
660    CreatePost,
661    FollowUser,
662    Create50Posts,
663    Create100Posts,
664    Create1000Posts,
665    CreateQuestion,
666    EditSettings,
667    FollowedByStaff,
668    CreateDrawing,
669    OpenAchievements,
670    Get1Like,
671    Get10Likes,
672    Get50Likes,
673    Get100Likes,
674    Get25Dislikes,
675    Get1Follower,
676    Get10Followers,
677    Get50Followers,
678    Get100Followers,
679    Follow10Users,
680    JoinCommunity,
681    CreateDraft,
682    EditPost,
683    Enable2fa,
684    EditNote,
685    CreatePostWithTitle,
686    CreateRepost,
687    OpenTos,
688    OpenPrivacyPolicy,
689    OpenReference,
690    GetAllOtherAchievements,
691    AcceptProfileWarning,
692    OpenSessionSettings,
693    #[serde(other)]
694    Removed,
695}
696
697#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
698pub enum AchievementRarity {
699    Common,
700    Uncommon,
701    Rare,
702}
703
704impl AchievementName {
705    pub fn title(&self) -> &str {
706        match self {
707            Self::CreatePost => "Dear friends,",
708            Self::FollowUser => "Virtual connections...",
709            Self::Create50Posts => "Hello, world!",
710            Self::Create100Posts => "It's my world",
711            Self::Create1000Posts => "Timeline domination",
712            Self::CreateQuestion => "Big questions...",
713            Self::EditSettings => "Just how I like it!",
714            Self::FollowedByStaff => "Big Shrimpin'",
715            Self::CreateDrawing => "Modern art",
716            Self::OpenAchievements => "Welcome!",
717            Self::Get1Like => "Baby steps!",
718            Self::Get10Likes => "WOW! 10 LIKES!",
719            Self::Get50Likes => "banger post follow for more",
720            Self::Get100Likes => "everyone liked that",
721            Self::Get25Dislikes => "Sorry...",
722            Self::Get1Follower => "Friends?",
723            Self::Get10Followers => "Friends!",
724            Self::Get50Followers => "50 WHOLE FOLLOWERS??",
725            Self::Get100Followers => "Everyone is my friend!",
726            Self::Follow10Users => "Big fan",
727            Self::JoinCommunity => "A sense of community...",
728            Self::CreateDraft => "Maybe later!",
729            Self::EditPost => "Grammar police?",
730            Self::Enable2fa => "Locked in",
731            Self::EditNote => "I take it back!",
732            Self::CreatePostWithTitle => "Must declutter",
733            Self::CreateRepost => "More than a like or comment...",
734            Self::OpenTos => "Well informed!",
735            Self::OpenPrivacyPolicy => "Privacy conscious",
736            Self::OpenReference => "What does this do?",
737            Self::GetAllOtherAchievements => "The final performance",
738            Self::AcceptProfileWarning => "I accept the risks!",
739            Self::OpenSessionSettings => "Am I alone in here?",
740            Self::Removed => "Removed achievement",
741        }
742    }
743
744    pub fn description(&self) -> &str {
745        match self {
746            Self::CreatePost => "Create your first post!",
747            Self::FollowUser => "Follow somebody!",
748            Self::Create50Posts => "Create your 50th post.",
749            Self::Create100Posts => "Create your 100th post.",
750            Self::Create1000Posts => "Create your 1000th post.",
751            Self::CreateQuestion => "Ask your first question!",
752            Self::EditSettings => "Edit your settings.",
753            Self::FollowedByStaff => "Get followed by a staff member!",
754            Self::CreateDrawing => "Include a drawing in a question.",
755            Self::OpenAchievements => "Open the achievements page.",
756            Self::Get1Like => "Get 1 like on a post! Good job!",
757            Self::Get10Likes => "Get 10 likes on one post.",
758            Self::Get50Likes => "Get 50 likes on one post.",
759            Self::Get100Likes => "Get 100 likes on one post.",
760            Self::Get25Dislikes => "Get 25 dislikes on one post... :(",
761            Self::Get1Follower => "Get 1 follower. Cool!",
762            Self::Get10Followers => "Get 10 followers. You're getting popular!",
763            Self::Get50Followers => "Get 50 followers. Okay, you're fairly popular!",
764            Self::Get100Followers => "Get 100 followers. You might be famous..?",
765            Self::Follow10Users => "Follow 10 other users. I'm sure people appreciate it!",
766            Self::JoinCommunity => "Join a community. Welcome!",
767            Self::CreateDraft => "Save a post as a draft.",
768            Self::EditPost => "Edit a post.",
769            Self::Enable2fa => "Enable TOTP 2FA.",
770            Self::EditNote => "Edit a note.",
771            Self::CreatePostWithTitle => "Create a post with a title.",
772            Self::CreateRepost => "Create a repost or quote.",
773            Self::OpenTos => "Open the terms of service.",
774            Self::OpenPrivacyPolicy => "Open the privacy policy.",
775            Self::OpenReference => "Open the source code reference documentation.",
776            Self::GetAllOtherAchievements => "Get every other achievement.",
777            Self::AcceptProfileWarning => "Accept a profile warning.",
778            Self::OpenSessionSettings => "Open your session settings.",
779            Self::Removed => "An achievement that was removed.",
780        }
781    }
782
783    pub fn rarity(&self) -> AchievementRarity {
784        // i don't want to write that long ass type name everywhere
785        use AchievementRarity::*;
786        match self {
787            Self::CreatePost => Common,
788            Self::FollowUser => Common,
789            Self::Create50Posts => Uncommon,
790            Self::Create100Posts => Uncommon,
791            Self::Create1000Posts => Rare,
792            Self::CreateQuestion => Common,
793            Self::EditSettings => Common,
794            Self::FollowedByStaff => Rare,
795            Self::CreateDrawing => Common,
796            Self::OpenAchievements => Common,
797            Self::Get1Like => Common,
798            Self::Get10Likes => Common,
799            Self::Get50Likes => Uncommon,
800            Self::Get100Likes => Rare,
801            Self::Get25Dislikes => Uncommon,
802            Self::Get1Follower => Common,
803            Self::Get10Followers => Common,
804            Self::Get50Followers => Uncommon,
805            Self::Get100Followers => Rare,
806            Self::Follow10Users => Common,
807            Self::JoinCommunity => Common,
808            Self::CreateDraft => Common,
809            Self::EditPost => Common,
810            Self::Enable2fa => Rare,
811            Self::EditNote => Uncommon,
812            Self::CreatePostWithTitle => Common,
813            Self::CreateRepost => Common,
814            Self::OpenTos => Uncommon,
815            Self::OpenPrivacyPolicy => Uncommon,
816            Self::OpenReference => Uncommon,
817            Self::GetAllOtherAchievements => Rare,
818            Self::AcceptProfileWarning => Common,
819            Self::OpenSessionSettings => Common,
820            Self::Removed => Rare,
821        }
822    }
823}
824
825impl From<AchievementName> for Achievement {
826    fn from(val: AchievementName) -> Self {
827        Achievement {
828            name: val,
829            unlocked: unix_epoch_timestamp(),
830        }
831    }
832}
833
834#[derive(Clone, Debug, Serialize, Deserialize)]
835pub struct Achievement {
836    pub name: AchievementName,
837    pub unlocked: usize,
838}
839
840#[derive(Debug, Serialize, Deserialize)]
841pub struct Notification {
842    pub id: usize,
843    pub created: usize,
844    pub title: String,
845    pub content: String,
846    pub owner: usize,
847    pub read: bool,
848    pub tag: String,
849}
850
851impl Notification {
852    /// Returns a new [`Notification`].
853    pub fn new(title: String, content: String, owner: usize) -> Self {
854        Self {
855            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
856            created: unix_epoch_timestamp(),
857            title,
858            content,
859            owner,
860            read: false,
861            tag: String::new(),
862        }
863    }
864}
865
866#[derive(Clone, Debug, Serialize, Deserialize)]
867pub struct UserFollow {
868    pub id: usize,
869    pub created: usize,
870    pub initiator: usize,
871    pub receiver: usize,
872}
873
874impl UserFollow {
875    /// Create a new [`UserFollow`].
876    pub fn new(initiator: usize, receiver: usize) -> Self {
877        Self {
878            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
879            created: unix_epoch_timestamp(),
880            initiator,
881            receiver,
882        }
883    }
884}
885
886#[derive(Serialize, Deserialize, PartialEq, Eq)]
887pub enum FollowResult {
888    /// Request sent to follow other user.
889    Requested,
890    /// Successfully followed other user.
891    Followed,
892}
893
894#[derive(Serialize, Deserialize)]
895pub struct UserBlock {
896    pub id: usize,
897    pub created: usize,
898    pub initiator: usize,
899    pub receiver: usize,
900}
901
902impl UserBlock {
903    /// Create a new [`UserBlock`].
904    pub fn new(initiator: usize, receiver: usize) -> Self {
905        Self {
906            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
907            created: unix_epoch_timestamp(),
908            initiator,
909            receiver,
910        }
911    }
912}
913
914#[derive(Serialize, Deserialize)]
915pub struct IpBlock {
916    pub id: usize,
917    pub created: usize,
918    pub initiator: usize,
919    pub receiver: String,
920}
921
922impl IpBlock {
923    /// Create a new [`IpBlock`].
924    pub fn new(initiator: usize, receiver: String) -> Self {
925        Self {
926            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
927            created: unix_epoch_timestamp(),
928            initiator,
929            receiver,
930        }
931    }
932}
933
934#[derive(Serialize, Deserialize)]
935pub struct IpBan {
936    pub ip: String,
937    pub created: usize,
938    pub reason: String,
939    pub moderator: usize,
940}
941
942impl IpBan {
943    /// Create a new [`IpBan`].
944    pub fn new(ip: String, moderator: usize, reason: String) -> Self {
945        Self {
946            ip,
947            created: unix_epoch_timestamp(),
948            reason,
949            moderator,
950        }
951    }
952}
953
954#[derive(Serialize, Deserialize)]
955pub struct UserWarning {
956    pub id: usize,
957    pub created: usize,
958    pub receiver: usize,
959    pub moderator: usize,
960    pub content: String,
961}
962
963impl UserWarning {
964    /// Create a new [`UserWarning`].
965    pub fn new(user: usize, moderator: usize, content: String) -> Self {
966        Self {
967            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
968            created: unix_epoch_timestamp(),
969            receiver: user,
970            moderator,
971            content,
972        }
973    }
974}
975
976#[derive(Clone, Debug, Serialize, Deserialize)]
977pub struct InviteCode {
978    pub id: usize,
979    pub created: usize,
980    pub owner: usize,
981    pub code: String,
982    pub is_used: bool,
983}
984
985impl InviteCode {
986    /// Create a new [`InviteCode`].
987    pub fn new(owner: usize) -> Self {
988        Self {
989            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
990            created: unix_epoch_timestamp(),
991            owner,
992            code: salt(),
993            is_used: false,
994        }
995    }
996}
997
998#[derive(Clone, Debug, Serialize, Deserialize)]
999pub struct ProfileView {
1000    pub id: usize,
1001    pub created: usize,
1002    pub owner: usize,
1003    pub profile: usize,
1004}
1005
1006impl ProfileView {
1007    /// Create a new [`ProfileView`]
1008    pub fn new(owner: usize, profile: usize) -> Self {
1009        Self {
1010            id: Snowflake::new().to_string().parse::<usize>().unwrap(),
1011            created: unix_epoch_timestamp(),
1012            owner,
1013            profile,
1014        }
1015    }
1016}