Skip to main content

tetratto_core2/model/
auth.rs

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
11/// `(ip, token, creation timestamp)`
12pub 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    /// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
30    #[serde(default)]
31    pub totp: String,
32    /// The TOTP recovery codes for this profile.
33    #[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    /// External service connection details.
40    #[serde(default)]
41    pub connections: UserConnections,
42    /// The user's Stripe customer ID.
43    #[serde(default)]
44    pub stripe_id: String,
45    /// The grants associated with the user's account.
46    #[serde(default)]
47    pub grants: Vec<AuthGrant>,
48    /// A list of the IDs of all accounts the user has signed into through the UI.
49    #[serde(default)]
50    pub associated: Vec<Id>,
51    /// Users collect achievements through little actions across the site.
52    #[serde(default)]
53    pub achievements: Vec<Achievement>,
54    /// The reason the user was banned.
55    #[serde(default)]
56    pub ban_reason: String,
57    /// If the user is deactivated. Deactivated users act almost like deleted
58    /// users, but their data is not wiped.
59    #[serde(default)]
60    pub is_deactivated: bool,
61    /// The time at which the user's ban will automatically expire.
62    #[serde(default)]
63    pub ban_expire: u128,
64    /// The IDs of Stripe checkout sessions that this user has successfully completed.
65    ///
66    /// This should be checked BEFORE applying purchases to ensure that the user hasn't
67    /// already applied this purchase.
68    #[serde(default)]
69    pub checkouts: Vec<String>,
70    /// The time in which the user last consented to the site's policies.
71    #[serde(default)]
72    pub last_policy_consent: u128,
73    /// The number of messages this user has missed.
74    #[serde(default)]
75    pub missed_messages_count: usize,
76    /// The number of unique authenticated users who have viewed this user's profile.
77    #[serde(default)]
78    pub views: usize,
79    /// The ID of the Shrimpcamp account the user is linked to.
80    #[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    /// The theme shown to the user.
110    #[serde(default)]
111    pub theme_preference: ThemePreference,
112    /// The theme used on the user's profile. Setting this to `Auto` will use
113    /// the viewing user's `theme_preference` setting.
114    #[serde(default)]
115    pub profile_theme: ThemePreference,
116    #[serde(default)]
117    pub private_last_seen: bool,
118    /// Page background.
119    #[serde(default)]
120    pub theme_color_surface: String,
121    /// Text on elements with the surface backgrounds.
122    #[serde(default)]
123    pub theme_color_text: String,
124    /// Links on all elements.
125    #[serde(default)]
126    pub theme_color_text_link: String,
127    /// Box shadow color.
128    #[serde(default)]
129    pub theme_color_shadow: String,
130    /// Some cards, buttons, or anything else with a darker background color than the surface.
131    #[serde(default)]
132    pub theme_color_lowered: String,
133    /// Text on elements with the lowered backgrounds.
134    #[serde(default)]
135    pub theme_color_text_lowered: String,
136    /// Borders.
137    #[serde(default)]
138    pub theme_color_super_lowered: String,
139    /// Some cards, buttons, or anything else with a lighter background color than the surface.
140    #[serde(default)]
141    pub theme_color_raised: String,
142    /// Text on elements with the raised backgrounds.
143    #[serde(default)]
144    pub theme_color_text_raised: String,
145    /// Some borders.
146    #[serde(default)]
147    pub theme_color_super_raised: String,
148    /// Primary color; navigation bar, some buttons, etc.
149    #[serde(default)]
150    pub theme_color_primary: String,
151    /// Text on elements with the primary backgrounds.
152    #[serde(default)]
153    pub theme_color_text_primary: String,
154    /// Hover state for primary buttons.
155    #[serde(default)]
156    pub theme_color_primary_lowered: String,
157    /// Secondary color.
158    #[serde(default)]
159    pub theme_color_secondary: String,
160    /// Text on elements with the secondary backgrounds.
161    #[serde(default)]
162    pub theme_color_text_secondary: String,
163    /// Hover state for secondary buttons.
164    #[serde(default)]
165    pub theme_color_secondary_lowered: String,
166    /// Custom CSS input.
167    #[serde(default)]
168    pub theme_custom_css: String,
169    /// The color of an online online indicator.
170    #[serde(default)]
171    pub theme_color_online: String,
172    /// The color of an idle online indicator.
173    #[serde(default)]
174    pub theme_color_idle: String,
175    /// The color of an offline online indicator.
176    #[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    /// A header shown in the place of "Ask question" if `enable_questions` is true.
183    #[serde(default)]
184    pub motivational_header: String,
185    /// If questions from anonymous users are allowed. Requires `enable_questions`.
186    #[serde(default)]
187    pub allow_anonymous_questions: bool,
188    /// The username used for anonymous users.
189    #[serde(default)]
190    pub anonymous_username: String,
191    /// The URL of the avatar used for anonymous users.
192    #[serde(default)]
193    pub anonymous_avatar_url: String,
194    /// If dislikes are hidden for the user.
195    #[serde(default)]
196    pub hide_dislikes: bool,
197    /// Require an account to view the user's profile.
198    #[serde(default)]
199    pub require_account: bool,
200    /// If NSFW content should be shown.
201    #[serde(default)]
202    pub show_nsfw: bool,
203    /// Make your inbox your homepage.
204    #[serde(default)]
205    pub inbox_homepage: bool,
206    /// A list of strings the user has muted.
207    #[serde(default)]
208    pub muted: Vec<String>,
209    /// If drawings are enabled for questions sent to the user.
210    #[serde(default)]
211    pub enable_drawings: bool,
212    /// Hide posts that are answering a question on the "All" timeline.
213    #[serde(default)]
214    pub all_timeline_hide_answers: bool,
215    /// Increase the text size of buttons and paragraphs.
216    #[serde(default)]
217    pub large_text: bool,
218    /// Disable achievements.
219    #[serde(default)]
220    pub disable_achievements: bool,
221    /// If the user is hidden from followers/following tabs.
222    ///
223    /// The user will still impact the followers/following numbers, but will not
224    /// be shown in the UI (or API).
225    #[serde(default)]
226    pub hide_from_social_lists: bool,
227    /// Biography shown on `profile/private.lisp` page.
228    #[serde(default)]
229    pub private_biography: String,
230    /// If the followers/following links are hidden from the user's profile.
231    /// Will also revoke access to their respective pages.
232    #[serde(default)]
233    pub hide_social_follows: bool,
234    /// The user's location. This isn't actually verified or anything, so it can really
235    /// be whatever the user wants.
236    #[serde(default)]
237    #[validate(max_length = 128)]
238    pub location: String,
239    /// External links for the user's other profiles on other websites.
240    #[serde(default)]
241    #[validate(max_items = 15)]
242    #[validate(unique_items)]
243    pub links: Vec<(String, String)>,
244    /// Automatically mark new posts as NSFW.
245    #[serde(default)]
246    pub auto_nsfw: bool,
247    /// Automatically limit the reach of new posts (don't show them on public timelines).
248    #[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    /// Create a new [`User`].
270    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    /// Deleted user profile.
311    pub fn deleted() -> Self {
312        Self {
313            username: "<deleted>".to_string(),
314            id: Id::Legacy(0),
315            ..Default::default()
316        }
317    }
318
319    /// Banned user profile.
320    pub fn banned() -> Self {
321        Self {
322            username: "<banned>".to_string(),
323            id: Id::Legacy(0),
324            ..Default::default()
325        }
326    }
327
328    /// Anonymous user profile.
329    pub fn anonymous() -> Self {
330        Self {
331            username: "anonymous".to_string(),
332            id: Id::Legacy(0),
333            ..Default::default()
334        }
335    }
336
337    /// Create a new token
338    ///
339    /// # Returns
340    /// `(unhashed id, token)`
341    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    /// Check if the given password is correct for the user.
350    pub fn check_password(&self, against: String) -> bool {
351        self.password == hash_salted(against, self.salt.clone())
352    }
353
354    /// Parse user mentions in a given `input`.
355    pub fn parse_mentions(input: &str) -> Vec<String> {
356        // state
357        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        // parse
363        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; // don't push @
372            }
373
374            if at {
375                if char == ' ' {
376                    // reached space, end @
377                    at = false;
378
379                    if !out.contains(&buffer) {
380                        out.push(buffer);
381                    }
382
383                    buffer = String::new();
384                    continue;
385                }
386
387                // push mention text
388                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            // if we're trying to mention more than 5 people, mention nobody (we're a spammer)
400            return Vec::new();
401        }
402
403        // return
404        out
405    }
406
407    /// Get a [`TOTP`] from the profile's `totp` secret value.
408    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    /// Clean the struct for public viewing.
426    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    /// Get a grant from the user given the grant's `app` ID.
442    ///
443    /// Should be used **before** adding another grant (to ensure the app doesn't
444    /// already have a grant for this user).
445    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    /// A connection to a last.fm account.
453    LastFm,
454    #[serde(other)]
455    Unknown,
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
459pub enum ConnectionType {
460    /// A connection through a token which never expires.
461    Token,
462    /// <https://www.rfc-editor.org/rfc/rfc7636>
463    PKCE,
464    /// A connection with no stored authentication.
465    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
481/// The total number of achievements needed to 100% Tetratto!
482pub const ACHIEVEMENTS: usize = 29;
483/// "self-serve" achievements can be granted by the user through the API.
484pub 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        // i don't want to write that long ass type name everywhere
598        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    /// Returns a new [`Notification`].
660    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    /// Create a new [`UserFollow`].
683    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    /// Create a new [`UserBlock`].
703    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    /// Create a new [`IpBlock`].
723    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    /// Create a new [`IpBan`].
743    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    /// Create a new [`UserWarning`].
764    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    /// Create a new [`InviteCode`].
786    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    /// Create a new [`ProfileView`]
807    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}