534 lines
14 KiB
Rust
534 lines
14 KiB
Rust
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<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,
|
|
}
|
|
|
|
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, 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("<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() 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: "<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() 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<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 == '\\') && !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()
|
|
}
|
|
}
|
|
|
|
#[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>,
|
|
}
|
|
|
|
#[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() 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::<usize>().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::<usize>().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::<usize>().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::<usize>().unwrap(),
|
|
created: unix_epoch_timestamp() as usize,
|
|
receiver: user,
|
|
moderator,
|
|
content,
|
|
}
|
|
}
|
|
}
|