use std::collections::HashMap;
use crate::model::{Error, Result};
use super::{
oauth::AuthGrant,
permissions::{FinePermission, SecondaryPermission},
};
use serde::{Deserialize, Serialize};
use totp_rs::TOTP;
use tetratto_shared::{
hash::{hash_salted, salt},
snow::Snowflake,
unix_epoch_timestamp,
};
use serde_valid::Validate;
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<Token>,
pub permissions: FinePermission,
pub is_verified: bool,
pub notification_count: usize,
pub follower_count: usize,
pub following_count: usize,
pub last_seen: usize,
#[serde(default)]
pub totp: String,
#[serde(default)]
pub recovery_codes: Vec<String>,
#[serde(default)]
pub post_count: usize,
#[serde(default)]
pub request_count: usize,
#[serde(default)]
pub connections: UserConnections,
#[serde(default)]
pub stripe_id: String,
#[serde(default)]
pub grants: Vec<AuthGrant>,
#[serde(default)]
pub associated: Vec<usize>,
#[serde(default)]
pub invite_code: usize,
#[serde(default)]
pub secondary_permissions: SecondaryPermission,
#[serde(default)]
pub achievements: Vec<Achievement>,
#[serde(default)]
pub awaiting_purchase: bool,
#[serde(default)]
pub was_purchased: bool,
#[serde(default)]
pub browser_session: String,
#[serde(default)]
pub ban_reason: String,
#[serde(default)]
pub channel_mutes: Vec<usize>,
#[serde(default)]
pub is_deactivated: bool,
#[serde(default)]
pub ban_expire: usize,
#[serde(default)]
pub coins: i32,
#[serde(default)]
pub checkouts: Vec<String>,
#[serde(default)]
pub applied_configurations: Vec<(AppliedConfigType, usize)>,
#[serde(default)]
pub last_policy_consent: usize,
#[serde(default)]
pub close_friends_stack: usize,
#[serde(default)]
pub missed_messages_count: usize,
}
pub type UserConnections =
HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AppliedConfigType {
StyleSnippet,
}
#[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 {
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, PartialEq, Eq)]
pub enum DefaultProfileTabChoice {
Posts,
Responses,
}
impl Default for DefaultProfileTabChoice {
fn default() -> Self {
Self::Posts
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, Validate)]
pub struct UserSettings {
#[serde(default)]
#[validate(max_length = 32)]
pub display_name: String,
#[serde(default)]
#[validate(max_length = 4096)]
pub biography: String,
#[serde(default)]
#[validate(max_length = 2048)]
pub warning: String,
#[serde(default)]
pub private_profile: bool,
#[serde(default)]
pub private_communities: bool,
#[serde(default)]
pub theme_preference: ThemePreference,
#[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,
#[serde(default)]
pub theme_color_surface: String,
#[serde(default)]
pub theme_color_text: String,
#[serde(default)]
pub theme_color_text_link: String,
#[serde(default)]
pub theme_color_lowered: String,
#[serde(default)]
pub theme_color_text_lowered: String,
#[serde(default)]
pub theme_color_super_lowered: String,
#[serde(default)]
pub theme_color_raised: String,
#[serde(default)]
pub theme_color_text_raised: String,
#[serde(default)]
pub theme_color_super_raised: String,
#[serde(default)]
pub theme_color_primary: String,
#[serde(default)]
pub theme_color_text_primary: String,
#[serde(default)]
pub theme_color_primary_lowered: String,
#[serde(default)]
pub theme_color_secondary: String,
#[serde(default)]
pub theme_color_text_secondary: String,
#[serde(default)]
pub theme_color_secondary_lowered: String,
#[serde(default)]
pub theme_custom_css: String,
#[serde(default)]
pub theme_color_online: String,
#[serde(default)]
pub theme_color_idle: String,
#[serde(default)]
pub theme_color_offline: String,
#[serde(default)]
pub disable_other_themes: bool,
#[serde(default)]
pub disable_other_theme_css: bool,
#[serde(default)]
pub enable_questions: bool,
#[serde(default)]
pub motivational_header: String,
#[serde(default)]
pub allow_anonymous_questions: bool,
#[serde(default)]
pub anonymous_username: String,
#[serde(default)]
pub anonymous_avatar_url: String,
#[serde(default)]
pub hide_dislikes: bool,
#[serde(default)]
pub default_timeline: DefaultTimelineChoice,
#[serde(default)]
pub private_chats: bool,
#[serde(default)]
pub private_mails: bool,
#[serde(default)]
#[validate(max_length = 256)]
pub status: String,
#[serde(default = "mime_avif")]
pub banner_mime: String,
#[serde(default)]
pub require_account: bool,
#[serde(default)]
pub show_nsfw: bool,
#[serde(default)]
pub hide_extra_post_tabs: bool,
#[serde(default)]
pub muted: Vec<String>,
#[serde(default)]
pub paged_timelines: bool,
#[serde(default)]
pub enable_drawings: bool,
#[serde(default)]
pub auto_unlist: bool,
#[serde(default)]
pub all_timeline_hide_answers: bool,
#[serde(default)]
pub auto_clear_notifs: bool,
#[serde(default)]
pub large_text: bool,
#[serde(default)]
pub disable_achievements: bool,
#[serde(default)]
pub hide_associated_blocked_users: bool,
#[serde(default)]
pub default_profile_tab: DefaultProfileTabChoice,
#[serde(default)]
pub hide_from_social_lists: bool,
#[serde(default)]
pub auto_full_unlist: bool,
#[serde(default)]
pub private_biography: String,
#[serde(default)]
pub hide_social_follows: bool,
#[serde(default)]
#[validate(max_length = 2048)]
pub mail_signature: String,
#[serde(default)]
#[validate(max_length = 2048)]
pub forum_signature: String,
#[serde(default)]
pub no_transfers: bool,
#[serde(default)]
pub enable_shop: bool,
#[serde(default)]
pub hide_username_badges: bool,
#[serde(default)]
pub use_system_font: bool,
#[serde(default)]
#[validate(max_length = 128)]
pub location: String,
#[serde(default)]
#[validate(max_items = 5)]
#[validate(unique_items)]
pub links: Vec<(String, String)>,
}
impl UserSettings {
pub fn verify_values(&self) -> Result<()> {
if let Err(e) = self.validate() {
return Err(Error::MiscError(e.to_string()));
}
Ok(())
}
}
fn mime_avif() -> String {
"image/avif".to_string()
}
impl Default for User {
fn default() -> Self {
Self::new("<unknown>".to_string(), String::new())
}
}
impl User {
pub fn new(username: String, password: String) -> Self {
let salt = salt();
let password = hash_salted(password, salt.clone());
let created = unix_epoch_timestamp();
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created,
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: created,
totp: String::new(),
recovery_codes: Vec::new(),
post_count: 0,
request_count: 0,
connections: HashMap::new(),
stripe_id: String::new(),
grants: Vec::new(),
associated: Vec::new(),
invite_code: 0,
secondary_permissions: SecondaryPermission::DEFAULT,
achievements: Vec::new(),
awaiting_purchase: false,
was_purchased: false,
browser_session: String::new(),
ban_reason: String::new(),
channel_mutes: Vec::new(),
is_deactivated: false,
ban_expire: 0,
coins: 0,
checkouts: Vec::new(),
applied_configurations: Vec::new(),
last_policy_consent: created,
close_friends_stack: 0,
missed_messages_count: 0,
}
}
pub fn deleted() -> Self {
Self {
username: "<deleted>".to_string(),
id: 0,
..Default::default()
}
}
pub fn banned() -> Self {
Self {
username: "<banned>".to_string(),
id: 0,
..Default::default()
}
}
pub fn anonymous() -> Self {
Self {
username: "anonymous".to_string(),
id: 0,
..Default::default()
}
}
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(),
),
)
}
pub fn check_password(&self, against: String) -> bool {
self.password == hash_salted(against, self.salt.clone())
}
pub fn parse_mentions(input: &str) -> Vec<String> {
let mut escape: bool = false;
let mut at: bool = false;
let mut buffer: String = String::new();
let mut out = Vec::new();
for char in input.chars() {
if ((char == '\\') | (char == '/')) && !escape {
escape = true;
continue;
}
if (char == '@') && !escape {
at = true;
continue; }
if at {
if char == ' ' {
at = false;
if !out.contains(&buffer) {
out.push(buffer);
}
buffer = String::new();
continue;
}
buffer.push(char);
}
escape = false;
}
if !buffer.is_empty() {
out.push(buffer);
}
if out.len() > 5 {
return Vec::new();
}
out
}
pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
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()
}
pub fn clean(&mut self) {
self.password = String::new();
self.salt = String::new();
self.tokens = Vec::new();
self.grants = Vec::new();
self.recovery_codes = Vec::new();
self.totp = String::new();
self.settings = UserSettings::default();
self.stripe_id = String::new();
self.connections = HashMap::new();
}
pub fn get_grant_by_app_id(&self, id: usize) -> Option<&AuthGrant> {
self.grants.iter().find(|x| x.app == id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConnectionService {
Spotify,
LastFm,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ConnectionType {
Token,
PKCE,
None,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExternalConnectionInfo {
pub con_type: ConnectionType,
pub data: HashMap<String, String>,
pub show_on_profile: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct ExternalConnectionData {
pub external_urls: HashMap<String, String>,
pub data: HashMap<String, String>,
}
pub const ACHIEVEMENTS: usize = 36;
pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[
AchievementName::OpenReference,
AchievementName::OpenTos,
AchievementName::OpenPrivacyPolicy,
AchievementName::AcceptProfileWarning,
AchievementName::OpenSessionSettings,
];
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementName {
CreatePost,
FollowUser,
Create50Posts,
Create100Posts,
Create1000Posts,
CreateQuestion,
EditSettings,
CreateJournal,
FollowedByStaff,
CreateDrawing,
OpenAchievements,
Get1Like,
Get10Likes,
Get50Likes,
Get100Likes,
Get25Dislikes,
Get1Follower,
Get10Followers,
Get50Followers,
Get100Followers,
Follow10Users,
JoinCommunity,
CreateDraft,
EditPost,
Enable2fa,
EditNote,
CreatePostWithTitle,
CreateRepost,
OpenTos,
OpenPrivacyPolicy,
OpenReference,
GetAllOtherAchievements,
AcceptProfileWarning,
OpenSessionSettings,
CreateSite,
CreateDomain,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementRarity {
Common,
Uncommon,
Rare,
}
impl AchievementName {
pub fn title(&self) -> &str {
match self {
Self::CreatePost => "Dear friends,",
Self::FollowUser => "Virtual connections...",
Self::Create50Posts => "Hello, world!",
Self::Create100Posts => "It's my world",
Self::Create1000Posts => "Timeline domination",
Self::CreateQuestion => "Big questions...",
Self::EditSettings => "Just how I like it!",
Self::CreateJournal => "Dear diary...",
Self::FollowedByStaff => "Big Shrimpin'",
Self::CreateDrawing => "Modern art",
Self::OpenAchievements => "Welcome!",
Self::Get1Like => "Baby steps!",
Self::Get10Likes => "WOW! 10 LIKES!",
Self::Get50Likes => "banger post follow for more",
Self::Get100Likes => "everyone liked that",
Self::Get25Dislikes => "Sorry...",
Self::Get1Follower => "Friends?",
Self::Get10Followers => "Friends!",
Self::Get50Followers => "50 WHOLE FOLLOWERS??",
Self::Get100Followers => "Everyone is my friend!",
Self::Follow10Users => "Big fan",
Self::JoinCommunity => "A sense of community...",
Self::CreateDraft => "Maybe later!",
Self::EditPost => "Grammar police?",
Self::Enable2fa => "Locked in",
Self::EditNote => "I take it back!",
Self::CreatePostWithTitle => "Must declutter",
Self::CreateRepost => "More than a like or comment...",
Self::OpenTos => "Well informed!",
Self::OpenPrivacyPolicy => "Privacy conscious",
Self::OpenReference => "What does this do?",
Self::GetAllOtherAchievements => "The final performance",
Self::AcceptProfileWarning => "I accept the risks!",
Self::OpenSessionSettings => "Am I alone in here?",
Self::CreateSite => "Littlewebmaster",
Self::CreateDomain => "LittleDNS",
}
}
pub fn description(&self) -> &str {
match self {
Self::CreatePost => "Create your first post!",
Self::FollowUser => "Follow somebody!",
Self::Create50Posts => "Create your 50th post.",
Self::Create100Posts => "Create your 100th post.",
Self::Create1000Posts => "Create your 1000th post.",
Self::CreateQuestion => "Ask your first question!",
Self::EditSettings => "Edit your settings.",
Self::CreateJournal => "Create your first journal.",
Self::FollowedByStaff => "Get followed by a staff member!",
Self::CreateDrawing => "Include a drawing in a question.",
Self::OpenAchievements => "Open the achievements page.",
Self::Get1Like => "Get 1 like on a post! Good job!",
Self::Get10Likes => "Get 10 likes on one post.",
Self::Get50Likes => "Get 50 likes on one post.",
Self::Get100Likes => "Get 100 likes on one post.",
Self::Get25Dislikes => "Get 25 dislikes on one post... :(",
Self::Get1Follower => "Get 1 follower. Cool!",
Self::Get10Followers => "Get 10 followers. You're getting popular!",
Self::Get50Followers => "Get 50 followers. Okay, you're fairly popular!",
Self::Get100Followers => "Get 100 followers. You might be famous..?",
Self::Follow10Users => "Follow 10 other users. I'm sure people appreciate it!",
Self::JoinCommunity => "Join a community. Welcome!",
Self::CreateDraft => "Save a post as a draft.",
Self::EditPost => "Edit a post.",
Self::Enable2fa => "Enable TOTP 2FA.",
Self::EditNote => "Edit a note.",
Self::CreatePostWithTitle => "Create a post with a title.",
Self::CreateRepost => "Create a repost or quote.",
Self::OpenTos => "Open the terms of service.",
Self::OpenPrivacyPolicy => "Open the privacy policy.",
Self::OpenReference => "Open the source code reference documentation.",
Self::GetAllOtherAchievements => "Get every other achievement.",
Self::AcceptProfileWarning => "Accept a profile warning.",
Self::OpenSessionSettings => "Open your session settings.",
Self::CreateSite => "Create a site.",
Self::CreateDomain => "Create a domain.",
}
}
pub fn rarity(&self) -> AchievementRarity {
use AchievementRarity::*;
match self {
Self::CreatePost => Common,
Self::FollowUser => Common,
Self::Create50Posts => Uncommon,
Self::Create100Posts => Uncommon,
Self::Create1000Posts => Rare,
Self::CreateQuestion => Common,
Self::EditSettings => Common,
Self::CreateJournal => Uncommon,
Self::FollowedByStaff => Rare,
Self::CreateDrawing => Common,
Self::OpenAchievements => Common,
Self::Get1Like => Common,
Self::Get10Likes => Common,
Self::Get50Likes => Uncommon,
Self::Get100Likes => Rare,
Self::Get25Dislikes => Uncommon,
Self::Get1Follower => Common,
Self::Get10Followers => Common,
Self::Get50Followers => Uncommon,
Self::Get100Followers => Rare,
Self::Follow10Users => Common,
Self::JoinCommunity => Common,
Self::CreateDraft => Common,
Self::EditPost => Common,
Self::Enable2fa => Rare,
Self::EditNote => Uncommon,
Self::CreatePostWithTitle => Common,
Self::CreateRepost => Common,
Self::OpenTos => Uncommon,
Self::OpenPrivacyPolicy => Uncommon,
Self::OpenReference => Uncommon,
Self::GetAllOtherAchievements => Rare,
Self::AcceptProfileWarning => Common,
Self::OpenSessionSettings => Common,
Self::CreateSite => Common,
Self::CreateDomain => Common,
}
}
}
impl Into<Achievement> for AchievementName {
fn into(self) -> Achievement {
Achievement {
name: self,
unlocked: unix_epoch_timestamp(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Achievement {
pub name: AchievementName,
pub unlocked: usize,
}
#[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 {
pub fn new(title: String, content: String, owner: usize) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
title,
content,
owner,
read: false,
tag: String::new(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserFollow {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: usize,
}
impl UserFollow {
pub fn new(initiator: usize, receiver: usize) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
initiator,
receiver,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum FollowResult {
Requested,
Followed,
}
#[derive(Serialize, Deserialize)]
pub struct UserBlock {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: usize,
}
impl UserBlock {
pub fn new(initiator: usize, receiver: usize) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
initiator,
receiver,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct IpBlock {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: String,
}
impl IpBlock {
pub fn new(initiator: usize, receiver: String) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
initiator,
receiver,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct IpBan {
pub ip: String,
pub created: usize,
pub reason: String,
pub moderator: usize,
}
impl IpBan {
pub fn new(ip: String, moderator: usize, reason: String) -> Self {
Self {
ip,
created: unix_epoch_timestamp(),
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 {
pub fn new(user: usize, moderator: usize, content: String) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
receiver: user,
moderator,
content,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InviteCode {
pub id: usize,
pub created: usize,
pub owner: usize,
pub code: String,
pub is_used: bool,
}
impl InviteCode {
pub fn new(owner: usize) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
owner,
code: salt(),
is_used: false,
}
}
}