use std::collections::HashMap; use super::permissions::FinePermission; use serde::{Deserialize, Serialize}; use totp_rs::TOTP; use tetratto_shared::{ hash::{hash_salted, salt}, snow::Snowflake, unix_epoch_timestamp, }; /// `(ip, token, creation timestamp)` pub type Token = (String, String, usize); #[derive(Clone, Debug, Serialize, Deserialize)] pub struct User { pub id: usize, pub created: usize, pub username: String, pub password: String, pub salt: String, pub settings: UserSettings, pub tokens: Vec, pub permissions: FinePermission, pub is_verified: bool, pub notification_count: usize, pub follower_count: usize, pub following_count: usize, pub last_seen: usize, /// The TOTP secret for this profile. An empty value means the user has TOTP disabled. #[serde(default)] pub totp: String, /// The TOTP recovery codes for this profile. #[serde(default)] pub recovery_codes: Vec, #[serde(default)] pub post_count: usize, #[serde(default)] pub request_count: usize, /// External service connection details. #[serde(default)] pub connections: UserConnections, /// The user's Stripe customer ID. #[serde(default)] pub stripe_id: String, } pub type UserConnections = HashMap; #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ThemePreference { Auto, Dark, Light, } impl Default for ThemePreference { fn default() -> Self { Self::Auto } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DefaultTimelineChoice { MyCommunities, MyCommunitiesQuestions, PopularPosts, PopularQuestions, FollowingPosts, FollowingQuestions, AllPosts, AllQuestions, Stack(String), } impl Default for DefaultTimelineChoice { fn default() -> Self { Self::MyCommunities } } impl DefaultTimelineChoice { /// Get the relative URL that the timeline should bring you to. pub fn relative_url(&self) -> String { match &self { Self::MyCommunities => "/".to_string(), Self::MyCommunitiesQuestions => "/questions".to_string(), Self::PopularPosts => "/popular".to_string(), Self::PopularQuestions => "/popular/questions".to_string(), Self::FollowingPosts => "/following".to_string(), Self::FollowingQuestions => "/following/questions".to_string(), Self::AllPosts => "/all".to_string(), Self::AllQuestions => "/all/questions".to_string(), Self::Stack(id) => format!("/stacks/{id}"), } } } #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct UserSettings { #[serde(default)] pub policy_consent: bool, #[serde(default)] pub display_name: String, #[serde(default)] pub biography: String, #[serde(default)] pub warning: String, #[serde(default)] pub private_profile: bool, #[serde(default)] pub private_communities: bool, /// The theme shown to the user. #[serde(default)] pub theme_preference: ThemePreference, /// The theme used on the user's profile. Setting this to `Auto` will use /// the viewing user's `theme_preference` setting. #[serde(default)] pub profile_theme: ThemePreference, #[serde(default)] pub private_last_seen: bool, #[serde(default)] pub theme_hue: String, #[serde(default)] pub theme_sat: String, #[serde(default)] pub theme_lit: String, /// Page background. #[serde(default)] pub theme_color_surface: String, /// Text on elements with the surface backgrounds. #[serde(default)] pub theme_color_text: String, /// Links on all elements. #[serde(default)] pub theme_color_text_link: String, /// Some cards, buttons, or anything else with a darker background color than the surface. #[serde(default)] pub theme_color_lowered: String, /// Text on elements with the lowered backgrounds. #[serde(default)] pub theme_color_text_lowered: String, /// Borders. #[serde(default)] pub theme_color_super_lowered: String, /// Some cards, buttons, or anything else with a lighter background color than the surface. #[serde(default)] pub theme_color_raised: String, /// Text on elements with the raised backgrounds. #[serde(default)] pub theme_color_text_raised: String, /// Some borders. #[serde(default)] pub theme_color_super_raised: String, /// Primary color; navigation bar, some buttons, etc. #[serde(default)] pub theme_color_primary: String, /// Text on elements with the primary backgrounds. #[serde(default)] pub theme_color_text_primary: String, /// Hover state for primary buttons. #[serde(default)] pub theme_color_primary_lowered: String, /// Secondary color. #[serde(default)] pub theme_color_secondary: String, /// Text on elements with the secondary backgrounds. #[serde(default)] pub theme_color_text_secondary: String, /// Hover state for secondary buttons. #[serde(default)] pub theme_color_secondary_lowered: String, /// Custom CSS input. #[serde(default)] pub theme_custom_css: String, #[serde(default)] pub disable_other_themes: bool, #[serde(default)] pub disable_other_theme_css: bool, #[serde(default)] pub enable_questions: bool, /// A header shown in the place of "Ask question" if `enable_questions` is true. #[serde(default)] pub motivational_header: String, /// If questions from anonymous users are allowed. Requires `enable_questions`. #[serde(default)] pub allow_anonymous_questions: bool, /// The username used for anonymous users. #[serde(default)] pub anonymous_username: String, /// The URL of the avatar used for anonymous users. #[serde(default)] pub anonymous_avatar_url: String, /// If dislikes are hidden for the user. #[serde(default)] pub hide_dislikes: bool, /// The timeline that the "Home" button takes you to. #[serde(default)] pub default_timeline: DefaultTimelineChoice, /// If other users that you aren't following can add you to chats. #[serde(default)] pub private_chats: bool, /// The user's status. Shows over connection info. #[serde(default)] pub status: String, /// The mime type of the user's avatar. #[serde(default = "mime_avif")] pub avatar_mime: String, /// The mime type of the user's banner. #[serde(default = "mime_avif")] pub banner_mime: String, } fn mime_avif() -> String { "image/avif".to_string() } impl Default for User { fn default() -> Self { Self::new("".to_string(), String::new()) } } impl User { /// Create a new [`User`]. pub fn new(username: String, password: String) -> Self { let salt = salt(); let password = hash_salted(password, salt.clone()); Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, username, password, salt, settings: UserSettings::default(), tokens: Vec::new(), permissions: FinePermission::DEFAULT, is_verified: false, notification_count: 0, follower_count: 0, following_count: 0, last_seen: unix_epoch_timestamp() as usize, totp: String::new(), recovery_codes: Vec::new(), post_count: 0, request_count: 0, connections: HashMap::new(), stripe_id: String::new(), } } /// Deleted user profile. pub fn deleted() -> Self { Self { username: "".to_string(), id: 0, ..Default::default() } } /// Banned user profile. pub fn banned() -> Self { Self { username: "".to_string(), id: 0, ..Default::default() } } /// Anonymous user profile. pub fn anonymous() -> Self { Self { username: "anonymous".to_string(), id: 0, ..Default::default() } } /// Create a new token /// /// # Returns /// `(unhashed id, token)` pub fn create_token(ip: &str) -> (String, Token) { let unhashed = tetratto_shared::hash::uuid(); ( unhashed.clone(), ( ip.to_string(), tetratto_shared::hash::hash(unhashed), unix_epoch_timestamp() as usize, ), ) } /// Check if the given password is correct for the user. pub fn check_password(&self, against: String) -> bool { self.password == hash_salted(against, self.salt.clone()) } /// Parse user mentions in a given `input`. pub fn parse_mentions(input: &str) -> Vec { // state let mut escape: bool = false; let mut at: bool = false; let mut buffer: String = String::new(); let mut out = Vec::new(); // parse for char in input.chars() { if (char == '\\') && !escape { escape = true; continue; } if (char == '@') && !escape { at = true; continue; // don't push @ } if at { if char == ' ' { // reached space, end @ at = false; if !out.contains(&buffer) { out.push(buffer); } buffer = String::new(); continue; } // push mention text buffer.push(char); } escape = false; } if !buffer.is_empty() { out.push(buffer); } // return out } /// Get a [`TOTP`] from the profile's `totp` secret value. pub fn totp(&self, issuer: Option) -> Option { if self.totp.is_empty() { return None; } TOTP::new( totp_rs::Algorithm::SHA1, 6, 1, 30, self.totp.as_bytes().to_owned(), Some(issuer.unwrap_or("tetratto!".to_string())), self.username.clone(), ) .ok() } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ConnectionService { /// A connection to a Spotify account. Spotify, /// A connection to a last.fm account. LastFm, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum ConnectionType { /// A connection through a token which never expires. Token, /// PKCE, /// A connection with no stored authentication. None, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ExternalConnectionInfo { pub con_type: ConnectionType, pub data: HashMap, pub show_on_profile: bool, } #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct ExternalConnectionData { pub external_urls: HashMap, pub data: HashMap, } #[derive(Debug, Serialize, Deserialize)] pub struct Notification { pub id: usize, pub created: usize, pub title: String, pub content: String, pub owner: usize, pub read: bool, pub tag: String, } impl Notification { /// Returns a new [`Notification`]. pub fn new(title: String, content: String, owner: usize) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, title, content, owner, read: false, tag: String::new(), } } } #[derive(Debug, Serialize, Deserialize)] pub struct UserFollow { pub id: usize, pub created: usize, pub initiator: usize, pub receiver: usize, } impl UserFollow { /// Create a new [`UserFollow`]. pub fn new(initiator: usize, receiver: usize) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, initiator, receiver, } } } #[derive(Serialize, Deserialize, PartialEq, Eq)] pub enum FollowResult { /// Request sent to follow other user. Requested, /// Successfully followed other user. Followed, } #[derive(Serialize, Deserialize)] pub struct UserBlock { pub id: usize, pub created: usize, pub initiator: usize, pub receiver: usize, } impl UserBlock { /// Create a new [`UserBlock`]. pub fn new(initiator: usize, receiver: usize) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, initiator, receiver, } } } #[derive(Serialize, Deserialize)] pub struct IpBlock { pub id: usize, pub created: usize, pub initiator: usize, pub receiver: String, } impl IpBlock { /// Create a new [`IpBlock`]. pub fn new(initiator: usize, receiver: String) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, initiator, receiver, } } } #[derive(Serialize, Deserialize)] pub struct IpBan { pub ip: String, pub created: usize, pub reason: String, pub moderator: usize, } impl IpBan { /// Create a new [`IpBan`]. pub fn new(ip: String, moderator: usize, reason: String) -> Self { Self { ip, created: unix_epoch_timestamp() as usize, reason, moderator, } } } #[derive(Serialize, Deserialize)] pub struct UserWarning { pub id: usize, pub created: usize, pub receiver: usize, pub moderator: usize, pub content: String, } impl UserWarning { /// Create a new [`UserWarning`]. pub fn new(user: usize, moderator: usize, content: String) -> Self { Self { id: Snowflake::new().to_string().parse::().unwrap(), created: unix_epoch_timestamp() as usize, receiver: user, moderator, content, } } }