903 lines
28 KiB
Rust
903 lines
28 KiB
Rust
use std::collections::HashMap;
|
|
|
|
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,
|
|
};
|
|
|
|
/// `(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<Token>,
|
|
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<String>,
|
|
#[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,
|
|
/// The grants associated with the user's account.
|
|
#[serde(default)]
|
|
pub grants: Vec<AuthGrant>,
|
|
/// A list of the IDs of all accounts the user has signed into through the UI.
|
|
#[serde(default)]
|
|
pub associated: Vec<usize>,
|
|
/// The ID of the [`InviteCode`] this user provided during registration.
|
|
#[serde(default)]
|
|
pub invite_code: usize,
|
|
/// Secondary permissions because the regular permissions struct ran out of possible bits.
|
|
#[serde(default)]
|
|
pub secondary_permissions: SecondaryPermission,
|
|
/// Users collect achievements through little actions across the site.
|
|
#[serde(default)]
|
|
pub achievements: Vec<Achievement>,
|
|
/// If the account was registered as a "bought" account, the user should not
|
|
/// be allowed to actually use the account if they haven't paid for supporter yet.
|
|
#[serde(default)]
|
|
pub awaiting_purchase: bool,
|
|
/// This value cannot be changed after account creation. This value is used to
|
|
/// lock the user's account again if the subscription is cancelled and they haven't
|
|
/// used an invite code.
|
|
#[serde(default)]
|
|
pub was_purchased: bool,
|
|
/// This value is updated for every **new** littleweb browser session.
|
|
///
|
|
/// This means the user can only have one of these sessions open at once
|
|
/// (unless this token is stored somewhere with a way to say we already have one,
|
|
/// but this does not happen yet).
|
|
///
|
|
/// Without this token, the user can still use the browser, they just cannot
|
|
/// view pages which require authentication (all `$` routes).
|
|
#[serde(default)]
|
|
pub browser_session: String,
|
|
}
|
|
|
|
pub type UserConnections =
|
|
HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;
|
|
|
|
#[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, PartialEq, Eq)]
|
|
pub enum DefaultProfileTabChoice {
|
|
/// General posts (in any community) from the user.
|
|
Posts,
|
|
/// Responses to questions.
|
|
Responses,
|
|
}
|
|
|
|
impl Default for DefaultProfileTabChoice {
|
|
fn default() -> Self {
|
|
Self::Posts
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
/// The color of an online online indicator.
|
|
#[serde(default)]
|
|
pub theme_color_online: String,
|
|
/// The color of an idle online indicator.
|
|
#[serde(default)]
|
|
pub theme_color_idle: String,
|
|
/// The color of an offline online indicator.
|
|
#[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,
|
|
/// 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,
|
|
/// Require an account to view the user's profile.
|
|
#[serde(default)]
|
|
pub require_account: bool,
|
|
/// If NSFW content should be shown.
|
|
#[serde(default)]
|
|
pub show_nsfw: bool,
|
|
/// If extra post tabs are hidden (replies, media).
|
|
#[serde(default)]
|
|
pub hide_extra_post_tabs: bool,
|
|
/// If the GPA experiment is disabled.
|
|
#[serde(default)]
|
|
pub disable_gpa_fun: bool,
|
|
/// A list of strings the user has muted.
|
|
#[serde(default)]
|
|
pub muted: Vec<String>,
|
|
/// If timelines are paged instead of infinitely scrolled.
|
|
#[serde(default)]
|
|
pub paged_timelines: bool,
|
|
/// If drawings are enabled for questions sent to the user.
|
|
#[serde(default)]
|
|
pub enable_drawings: bool,
|
|
/// Automatically unlist posts from timelines.
|
|
#[serde(default)]
|
|
pub auto_unlist: bool,
|
|
/// Hide posts that are answering a question on the "All" timeline.
|
|
#[serde(default)]
|
|
pub all_timeline_hide_answers: bool,
|
|
/// Automatically clear all notifications when notifications are viewed.
|
|
#[serde(default)]
|
|
pub auto_clear_notifs: bool,
|
|
/// Increase the text size of buttons and paragraphs.
|
|
#[serde(default)]
|
|
pub large_text: bool,
|
|
/// Disable achievements.
|
|
#[serde(default)]
|
|
pub disable_achievements: bool,
|
|
/// Automatically hide users that you've blocked on your other accounts from your timelines.
|
|
#[serde(default)]
|
|
pub hide_associated_blocked_users: bool,
|
|
/// Which tab is shown by default on the user's profile.
|
|
#[serde(default)]
|
|
pub default_profile_tab: DefaultProfileTabChoice,
|
|
/// If the user is hidden from followers/following tabs.
|
|
///
|
|
/// The user will still impact the followers/following numbers, but will not
|
|
/// be shown in the UI (or API).
|
|
#[serde(default)]
|
|
pub hide_from_social_lists: bool,
|
|
/// Automatically hide your posts from all timelines except your profile
|
|
/// and the following timeline.
|
|
#[serde(default)]
|
|
pub auto_full_unlist: bool,
|
|
/// Biography shown on `profile/private.lisp` page.
|
|
#[serde(default)]
|
|
pub private_biography: String,
|
|
}
|
|
|
|
fn mime_avif() -> String {
|
|
"image/avif".to_string()
|
|
}
|
|
|
|
impl Default for User {
|
|
fn default() -> Self {
|
|
Self::new("<unknown>".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::<usize>().unwrap(),
|
|
created: unix_epoch_timestamp(),
|
|
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(),
|
|
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(),
|
|
}
|
|
}
|
|
|
|
/// Deleted user profile.
|
|
pub fn deleted() -> Self {
|
|
Self {
|
|
username: "<deleted>".to_string(),
|
|
id: 0,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Banned user profile.
|
|
pub fn banned() -> Self {
|
|
Self {
|
|
username: "<banned>".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(),
|
|
),
|
|
)
|
|
}
|
|
|
|
/// 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<String> {
|
|
// 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 == '\\') | (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<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()
|
|
}
|
|
|
|
/// Clean the struct for public viewing.
|
|
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();
|
|
}
|
|
|
|
/// Get a grant from the user given the grant's `app` ID.
|
|
///
|
|
/// Should be used **before** adding another grant (to ensure the app doesn't
|
|
/// already have a grant for this user).
|
|
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 {
|
|
/// 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,
|
|
/// <https://www.rfc-editor.org/rfc/rfc7636>
|
|
PKCE,
|
|
/// A connection with no stored authentication.
|
|
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>,
|
|
}
|
|
|
|
/// The total number of achievements needed to 100% Tetratto!
|
|
pub const ACHIEVEMENTS: usize = 36;
|
|
/// "self-serve" achievements can be granted by the user through the API.
|
|
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 {
|
|
// i don't want to write that long ass type name everywhere
|
|
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 {
|
|
/// Returns a new [`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 {
|
|
/// Create a new [`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 {
|
|
/// 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::<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 {
|
|
/// Create a new [`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 {
|
|
/// Create a new [`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 {
|
|
/// Create a new [`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 {
|
|
/// Create a new [`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,
|
|
}
|
|
}
|
|
}
|