tetratto_core/model/
auth.rs

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