diff --git a/Cargo.lock b/Cargo.lock index eacc870..c67c3d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3155,8 +3155,9 @@ dependencies = [ [[package]] name = "tetratto" -version = "1.0.1" +version = "1.0.2" dependencies = [ + "ammonia", "axum", "axum-extra", "cf-turnstile", @@ -3179,7 +3180,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "1.0.1" +version = "1.0.2" dependencies = [ "async-recursion", "bb8-postgres", @@ -3198,7 +3199,7 @@ dependencies = [ [[package]] name = "tetratto-l10n" -version = "1.0.1" +version = "1.0.2" dependencies = [ "pathbufd", "serde", @@ -3207,7 +3208,7 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "1.0.1" +version = "1.0.2" dependencies = [ "ammonia", "chrono", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 153f670..7c582ee 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto" -version = "1.0.1" +version = "1.0.2" edition = "2024" [features] @@ -19,6 +19,7 @@ tower-http = { version = "0.6.2", features = ["trace", "fs"] } axum = { version = "0.8.3", features = ["macros"] } tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] } axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] } +ammonia = "4.0.0" tetratto-shared = { path = "../shared" } tetratto-core = { path = "../core", features = [ "redis", diff --git a/crates/app/src/avif.rs b/crates/app/src/avif.rs index 4e82223..f92ea62 100644 --- a/crates/app/src/avif.rs +++ b/crates/app/src/avif.rs @@ -71,7 +71,10 @@ pub fn save_avif_buffer(path: &str, bytes: Vec) -> std::io::Result<()> { let file = File::create(path)?; let mut writer = BufWriter::new(file); - if let Err(_) = pre_img_buffer.write_to(&mut writer, image::ImageFormat::Avif) { + if pre_img_buffer + .write_to(&mut writer, image::ImageFormat::Avif) + .is_err() + { return Err(std::io::Error::new( std::io::ErrorKind::Other, "Image conversion failed", diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index a7115ac..839bf95 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -18,6 +18,8 @@ version = "1.0.0" "general:action.back" = "Back" "general:action.report" = "Report" "general:action.manage" = "Manage" +"general:label.safety" = "Safety" +"general:label.share" = "Share" "general:action.add_account" = "Add account" "general:action.switch_account" = "Switch account" "general:label.mod" = "Mod" @@ -75,6 +77,9 @@ version = "1.0.0" "communities:label.new_title" = "New title" "communities:label.pinned" = "Pinned" "communities:label.edit_content" = "Edit content" +"communities:label.repost" = "Repost" +"communities:label.quote_post" = "Quote post" +"communities:label.expand_original" = "Expand original" "notifs:action.mark_as_read" = "Mark as read" "notifs:action.mark_as_unread" = "Mark as unread" diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 02b4f19..fc976c2 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -2,6 +2,7 @@ mod assets; mod avif; mod macros; mod routes; +mod sanitize; use assets::{init_dirs, write_assets}; pub use tetratto_core::*; diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index b26dd19..2d4531b 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -524,6 +524,7 @@ select { resize: vertical; width: 100%; font-family: inherit; + font-size: 16px; /* personality */ background: transparent; color: inherit; diff --git a/crates/app/src/public/html/communities/feed.html b/crates/app/src/public/html/communities/feed.html index 1d61c85..7fc3778 100644 --- a/crates/app/src/public/html/communities/feed.html +++ b/crates/app/src/public/html/communities/feed.html @@ -42,7 +42,11 @@
{% for post in pinned %} - {{ components::post(post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% if post[0].context.repost and post[0].context.repost.reposting %} + {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% else %} + {{ components::post(post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% endif %} {% endfor %}
@@ -57,7 +61,11 @@
{% for post in feed %} - {{ components::post(post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% if post[0].context.repost and post[0].context.repost.reposting %} + {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% else %} + {{ components::post(post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} + {% endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }} diff --git a/crates/app/src/public/html/communities/post.html b/crates/app/src/public/html/communities/post.html index 21a85d6..db536cf 100644 --- a/crates/app/src/public/html/communities/post.html +++ b/crates/app/src/public/html/communities/post.html @@ -7,9 +7,18 @@ {{ icon "arrow-up" }} {{ text "communities:action.continue_thread" }} - {% endif %} {{ components::post(post=post, owner=owner, community=community, - show_community=true, can_manage_post=can_manage_posts) }} {% if user and - post.context.comments_enabled %} + {% endif %} + + +
+ {% if post.context.repost and post.context.repost.reposting %} + {{ components::repost(repost=reposting, post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} + {% else %} + {{ components::post(post=post, owner=owner, community=community, show_community=true, can_manage_post=can_manage_posts) }} + {% endif %} +
+ + {% if user and post.context.comments_enabled %}
{{ text "communities:label.create_reply" }} @@ -116,6 +125,11 @@ "{{ post.context.comments_enabled }}", "checkbox", ], + [ + ["reposts_enabled", "Allow people to repost your post"], + "{{ post.context.reposts_enabled }}", + "checkbox", + ], [ ["is_nsfw", "Mark as NSFW"], "{{ community.context.is_nsfw }}", diff --git a/crates/app/src/public/html/communities/settings.html b/crates/app/src/public/html/communities/settings.html index 22d5904..137257f 100644 --- a/crates/app/src/public/html/communities/settings.html +++ b/crates/app/src/public/html/communities/settings.html @@ -396,10 +396,15 @@ }, 250); + + + +
{%- endmacro %} {% macro post(post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%} {% if community and show_community and post.community != config.town_square %} @@ -130,6 +169,7 @@ show_community and post.community != config.town_square %} {% endif %}
{% else %} {{ post.created }} - {% endif %} {% if show_community %} - - - {% if not community %} - {{ components::community_avatar(id=post.community) }} - {% endif %} - {% endif %} {% if post.context.is_nsfw %} {{ icon "square-asterisk" }} + {% endif %} {% if post.context.repost and + post.context.repost.reposting %} + + {{ icon "repeat-2" }} + {% endif %}
@@ -175,7 +217,16 @@ show_community and post.community != config.town_square %}
- {% if user %} + {% if user %} {% if post.context.repost and + post.context.repost.reposting %} + + {{ icon "expand" }} + {{ text "communities:label.expand_original" }} + + {% else %}
- {% else %} + {% endif %} {% else %}
{% endif %} @@ -213,7 +264,26 @@ show_community and post.community != config.town_square %}
- {% if user.id != post.owner %} + {% if config.town_square and + post.context.reposts_enabled %} + {{ text "general:label.share" }} + + + + {% endif %} {% if user.id != post.owner %} + {{ text "general:label.safety" }} + +
+ + +{% endif %} {%- endmacro %} diff --git a/crates/app/src/public/html/profile/base.html b/crates/app/src/public/html/profile/base.html index d8b4c37..3bc21cd 100644 --- a/crates/app/src/public/html/profile/base.html +++ b/crates/app/src/public/html/profile/base.html @@ -67,7 +67,10 @@
-
+
+ + {% if is_following_you %} + + {{ icon "heart" }} + Follows you + + {% endif %}
diff --git a/crates/app/src/public/html/profile/posts.html b/crates/app/src/public/html/profile/posts.html index 88bef12..7cd69e3 100644 --- a/crates/app/src/public/html/profile/posts.html +++ b/crates/app/src/public/html/profile/posts.html @@ -9,7 +9,11 @@
{% for post in pinned %} - {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} + {% if post[0].context.repost and post[0].context.repost.reposting %} + {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} + {% else %} + {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], can_manage_post=is_self) }} + {% endif %} {% endfor %}
@@ -24,7 +28,11 @@
{% for post in posts %} - {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} + {% if post[0].context.repost and post[0].context.repost.reposting %} + {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true, can_manage_post=is_self) }} + {% else %} + {{ components::post(post=post[0], owner=post[1], secondary=true, community=post[2], can_manage_post=is_self) }} + {% endif %} {% endfor %} {{ components::pagination(page=page, items=posts|length) }} diff --git a/crates/app/src/public/html/profile/settings.html b/crates/app/src/public/html/profile/settings.html index b368884..35c39c0 100644 --- a/crates/app/src/public/html/profile/settings.html +++ b/crates/app/src/public/html/profile/settings.html @@ -326,10 +326,15 @@ {% endfor %}
+ + + ", " String { + input + .replace("<", "<") + .replace(">", ">") + .replace("url(\"", "url(\"/api/v0/util/ext/image?img=") + .replace("url(https://", "url(/api/v0/util/ext/image?img=https://") + .replace("", "") +} + +/// Clean user settings +pub fn clean_settings(settings: &UserSettings) -> String { + remove_tags(&serde_json::to_string(&clean_settings_raw(settings)).unwrap()) + .replace("\u{200d}", "") + // how do you end up with these in your settings? + .replace("\u{0010}", "") + .replace("\u{0011}", "") + .replace("\u{0012}", "") + .replace("\u{0013}", "") + .replace("\u{0014}", "") +} + +/// Clean user settings row +pub fn clean_settings_raw(settings: &UserSettings) -> UserSettings { + let mut settings = settings.to_owned(); + + settings.biography = clean_single(&settings.biography); + settings.theme_hue = clean_single(&settings.theme_hue); + settings.theme_sat = clean_single(&settings.theme_sat); + settings.theme_lit = clean_single(&settings.theme_lit); + + settings +} + +/// Clean community context +pub fn clean_context(context: &CommunityContext) -> String { + remove_tags(&serde_json::to_string(&clean_context_raw(context)).unwrap()) + .replace("\u{200d}", "") + // how do you end up with these in your settings? + .replace("\u{0010}", "") + .replace("\u{0011}", "") + .replace("\u{0012}", "") + .replace("\u{0013}", "") + .replace("\u{0014}", "") +} + +/// Clean community context row +pub fn clean_context_raw(context: &CommunityContext) -> CommunityContext { + let mut context = context.to_owned(); + context.description = clean_single(&context.description); + context +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index cbc5c50..684ca77 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-core" -version = "1.0.1" +version = "1.0.2" edition = "2024" [features] diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 8f2f3c4..627db30 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -34,11 +34,7 @@ impl DataManager { settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(), tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), permissions: FinePermission::from_bits(get!(x->7(i32)) as u32).unwrap(), - is_verified: if get!(x->8(i32)) as i8 == 1 { - true - } else { - false - }, + is_verified: get!(x->8(i32)) as i8 == 1, notification_count: get!(x->9(i32)) as usize, follower_count: get!(x->10(i32)) as usize, following_count: get!(x->11(i32)) as usize, @@ -80,7 +76,7 @@ impl DataManager { /// # Arguments /// * `data` - a mock [`User`] object to insert pub async fn create_user(&self, mut data: User) -> Result<()> { - if self.0.security.registration_enabled == false { + if !self.0.security.registration_enabled { return Err(Error::RegistrationDisabled); } @@ -124,10 +120,10 @@ impl DataManager { &serde_json::to_string(&data.settings).unwrap(), &serde_json::to_string(&data.tokens).unwrap(), &(FinePermission::DEFAULT.bits() as i32), - &(if data.is_verified { 1 as i32 } else { 0 as i32 }), - &(0 as i32), - &(0 as i32), - &(0 as i32), + &(if data.is_verified { 1_i32 } else { 0_i32 }), + &0_i32, + &0_i32, + &0_i32, &(data.last_seen as i64), &String::new(), &"[]" @@ -260,7 +256,7 @@ impl DataManager { let res = execute!( &conn, "UPDATE users SET verified = $1 WHERE id = $2", - params![&(if x { 1 } else { 0 } as i32), &(id as i64)] + params![&{ if x { 1 } else { 0 } }, &(id as i64)] ); if let Err(e) = res { @@ -327,7 +323,7 @@ impl DataManager { let res = execute!( &conn, - "UPDATE users SET username = $1 WHERE id = $3", + "UPDATE users SET username = $1 WHERE id = $2", params![&to.as_str(), &(id as i64)] ); @@ -414,7 +410,7 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - self.cache_clear_user(&user).await; + self.cache_clear_user(user).await; Ok(()) } diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 7c7bfec..0841bd3 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -174,7 +174,7 @@ macro_rules! auto_method { if !user.permissions.check(FinePermission::$permission) { return Err(Error::NotAllowed); } else { - self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new( + self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new( user.id, format!("invoked `{}` with x value `{id}`", stringify!($name)), )) @@ -204,7 +204,7 @@ macro_rules! auto_method { if !user.permissions.check(FinePermission::$permission) { return Err(Error::NotAllowed); } else { - self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new( + self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new( user.id, format!("invoked `{}` with x value `{id}`", stringify!($name)), )) @@ -236,7 +236,7 @@ macro_rules! auto_method { if !user.permissions.check(FinePermission::$permission) { return Err(Error::NotAllowed); } else { - self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new( + self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new( user.id, format!("invoked `{}` with x value `{id}`", stringify!($name)), )) diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 43038c6..1d11c26 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -224,9 +224,9 @@ impl DataManager { &serde_json::to_string(&data.read_access).unwrap().as_str(), &serde_json::to_string(&data.write_access).unwrap().as_str(), &serde_json::to_string(&data.join_access).unwrap().as_str(), - &(0 as i32), - &(0 as i32), - &(1 as i32) + &0_i32, + &0_i32, + &1_i32 ] ); diff --git a/crates/core/src/database/drivers/sqlite.rs b/crates/core/src/database/drivers/sqlite.rs index 91bfe54..e5f4da0 100644 --- a/crates/core/src/database/drivers/sqlite.rs +++ b/crates/core/src/database/drivers/sqlite.rs @@ -21,7 +21,7 @@ pub struct DataManager( impl DataManager { /// Obtain a connection to the staging database. pub(crate) async fn connect(&self) -> Result { - Ok(Connection::open(&self.0.database.name)?) + Connection::open(&self.0.database.name) } /// Create a new [`DataManager`] (and init database). diff --git a/crates/core/src/database/memberships.rs b/crates/core/src/database/memberships.rs index 3ae84a6..ef30ee7 100644 --- a/crates/core/src/database/memberships.rs +++ b/crates/core/src/database/memberships.rs @@ -50,7 +50,7 @@ impl DataManager { ) -> Result> { let mut users: Vec<(CommunityMembership, User)> = Vec::new(); for membership in list { - let owner = membership.owner.clone(); + let owner = membership.owner; users.push((membership, self.get_user_by_id(owner).await?)); } Ok(users) diff --git a/crates/core/src/database/notifications.rs b/crates/core/src/database/notifications.rs index e7befc4..95f64d4 100644 --- a/crates/core/src/database/notifications.rs +++ b/crates/core/src/database/notifications.rs @@ -21,11 +21,7 @@ impl DataManager { title: get!(x->2(String)), content: get!(x->3(String)), owner: get!(x->4(i64)) as usize, - read: if get!(x->5(i32)) as i8 == 1 { - true - } else { - false - }, + read: get!(x->5(i32)) as i8 == 1, } } @@ -71,7 +67,7 @@ impl DataManager { &data.title, &data.content, &(data.owner as i64), - &(if data.read { 1 } else { 0 } as i32) + &{ if data.read { 1 } else { 0 } } ] ); @@ -89,10 +85,8 @@ impl DataManager { pub async fn delete_notification(&self, id: usize, user: &User) -> Result<()> { let notification = self.get_notification_by_id(id).await?; - if user.id != notification.owner { - if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { - return Err(Error::NotAllowed); - } + if user.id != notification.owner && !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); } let conn = match self.connect().await { @@ -127,10 +121,8 @@ impl DataManager { let notifications = self.get_notifications_by_owner(user.id).await?; for notification in notifications { - if user.id != notification.owner { - if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { - return Err(Error::NotAllowed); - } + if user.id != notification.owner && !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); } self.delete_notification(notification.id, user).await? @@ -147,10 +139,8 @@ impl DataManager { ) -> Result<()> { let y = self.get_notification_by_id(id).await?; - if y.owner != user.id { - if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { - return Err(Error::NotAllowed); - } + if y.owner != user.id && !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); } // ... @@ -162,7 +152,7 @@ impl DataManager { let res = execute!( &conn, "UPDATE notifications SET read = $1 WHERE id = $2", - params![&(if new_read { 1 } else { 0 } as i32), &(id as i64)] + params![&{ if new_read { 1 } else { 0 } }, &(id as i64)] ); if let Err(e) = res { @@ -171,9 +161,9 @@ impl DataManager { self.2.remove(format!("atto.notification:{}", id)).await; - if (y.read == true) && (new_read == false) { + if (y.read) && (!new_read) { self.incr_user_notifications(user.id).await?; - } else if (y.read == false) && (new_read == true) { + } else if (!y.read) && (new_read) { self.decr_user_notifications(user.id).await?; } diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index fabe0ef..e0c2e17 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -33,11 +33,7 @@ impl DataManager { owner: get!(x->3(i64)) as usize, community: get!(x->4(i64)) as usize, context: serde_json::from_str(&get!(x->5(String))).unwrap(), - replying_to: if let Some(id) = get!(x->6(Option)) { - Some(id as usize) - } else { - None - }, + replying_to: get!(x->6(Option)).map(|id| id as usize), // likes likes: get!(x->7(i32)) as isize, dislikes: get!(x->8(i32)) as isize, @@ -79,20 +75,52 @@ impl DataManager { Ok(res.unwrap()) } + /// Get the post the given post is reposting (if some). + pub async fn get_post_reposting(&self, post: &Post) -> Option<(User, Post)> { + if let Some(ref repost) = post.context.repost { + if let Some(reposting) = repost.reposting { + let mut x = match self.get_post_by_id(reposting).await { + Ok(p) => p, + Err(_) => return None, + }; + + x.mark_as_repost(); + Some(( + match self.get_user_by_id(x.owner).await { + Ok(ua) => ua, + Err(_) => return None, + }, + x, + )) + } else { + None + } + } else { + None + } + } + /// Complete a vector of just posts with their owner as well. - pub async fn fill_posts(&self, posts: Vec) -> Result> { - let mut out: Vec<(Post, User)> = Vec::new(); + pub async fn fill_posts( + &self, + posts: Vec, + ) -> Result)>> { + let mut out: Vec<(Post, User, Option<(User, Post)>)> = Vec::new(); let mut users: HashMap = HashMap::new(); for post in posts { - let owner = post.owner.clone(); + let owner = post.owner; if let Some(user) = users.get(&owner) { - out.push((post, user.clone())); + out.push(( + post.clone(), + user.clone(), + self.get_post_reposting(&post).await, + )); } else { let user = self.get_user_by_id(owner).await?; users.insert(owner, user.clone()); - out.push((post, user)); + out.push((post.clone(), user, self.get_post_reposting(&post).await)); } } @@ -103,22 +131,62 @@ impl DataManager { pub async fn fill_posts_with_community( &self, posts: Vec, - ) -> Result> { - let mut out: Vec<(Post, User, Community)> = Vec::new(); + user_id: usize, + ) -> Result)>> { + let mut out: Vec<(Post, User, Community, Option<(User, Post)>)> = Vec::new(); let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); + let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); + for post in posts { - let owner = post.owner.clone(); - let community = post.community.clone(); + let owner = post.owner; + let community = post.community; if let Some((user, community)) = seen_before.get(&(owner, community)) { - out.push((post, user.clone(), community.to_owned())); + out.push(( + post.clone(), + user.clone(), + community.to_owned(), + self.get_post_reposting(&post).await, + )); } else { let user = self.get_user_by_id(owner).await?; - let community = self.get_community_by_id(community).await?; + // check relationship + if user.settings.private_profile { + if user_id == 0 { + continue; + } + + if let Some(is_following) = seen_user_follow_statuses.get(&(user.id, user_id)) { + if !is_following { + // post owner is not following us + continue; + } + } else { + if self + .get_userfollow_by_initiator_receiver(user.id, user_id) + .await + .is_err() + { + // post owner is not following us + seen_user_follow_statuses.insert((user.id, user_id), false); + continue; + } + + seen_user_follow_statuses.insert((user.id, user_id), true); + } + } + + // ... + let community = self.get_community_by_id(community).await?; seen_before.insert((owner, community.id), (user.clone(), community.clone())); - out.push((post, user, community)); + out.push(( + post.clone(), + user, + community, + self.get_post_reposting(&post).await, + )); } } @@ -357,25 +425,13 @@ impl DataManager { /// Check if the given `uid` can post in the given `community`. pub async fn check_can_post(&self, community: &Community, uid: usize) -> bool { match community.write_access { - CommunityWriteAccess::Owner => { - if uid != community.owner { - false - } else { - true - } - } + CommunityWriteAccess::Owner => uid == community.owner, CommunityWriteAccess::Joined => { match self .get_membership_by_owner_community(uid, community.id) .await { - Ok(m) => { - if !m.role.check_member() { - false - } else { - true - } - } + Ok(m) => !(!m.role.check_member()), Err(_) => false, } } @@ -388,11 +444,19 @@ impl DataManager { /// # Arguments /// * `data` - a mock [`JournalEntry`] object to insert pub async fn create_post(&self, mut data: Post) -> Result { - // check values - if data.content.len() < 2 { - return Err(Error::DataTooShort("content".to_string())); - } else if data.content.len() > 4096 { - return Err(Error::DataTooLong("username".to_string())); + // check values (if this isn't reposting something else) + let is_reposting = if let Some(ref repost) = data.context.repost { + repost.reposting.is_some() + } else { + false + }; + + if !is_reposting { + if data.content.len() < 2 { + return Err(Error::DataTooShort("content".to_string())); + } else if data.content.len() > 4096 { + return Err(Error::DataTooLong("content".to_string())); + } } // check permission in community @@ -408,10 +472,37 @@ impl DataManager { // mirror nsfw state data.context.is_nsfw = community.context.is_nsfw; - // check if we're blocked - if let Some(replying_to) = data.replying_to { + // check if we're reposting a post + let reposting = if let Some(ref repost) = data.context.repost { + if let Some(id) = repost.reposting { + Some(self.get_post_by_id(id).await?) + } else { + None + } + } else { + None + }; + + if let Some(ref rt) = reposting { + data.context.reposts_enabled = false; // cannot repost reposts + + // make sure we aren't trying to repost a repost + if if let Some(ref repost) = rt.context.repost { + !(!repost.is_repost) + } else { + false + } { + return Err(Error::MiscError("Cannot repost a repost".to_string())); + } + + // ... + if !rt.context.reposts_enabled { + return Err(Error::MiscError("Post has reposts disabled".to_string())); + } + + // check blocked status if let Ok(_) = self - .get_userblock_by_initiator_receiver(replying_to, data.owner) + .get_userblock_by_initiator_receiver(rt.owner, data.owner) .await { return Err(Error::NotAllowed); @@ -429,6 +520,14 @@ impl DataManager { if !rt.context.comments_enabled { return Err(Error::MiscError("Post has comments disabled".to_string())); } + + // check blocked status + if let Ok(_) = self + .get_userblock_by_initiator_receiver(rt.owner, data.owner) + .await + { + return Err(Error::NotAllowed); + } } // send mention notifications @@ -480,11 +579,11 @@ impl DataManager { &if replying_to_id != "0" { replying_to_id.parse::().unwrap() } else { - 0 as i64 + 0_i64 }, - &(0 as i32), - &(0 as i32), - &(0 as i32) + &0_i32, + &0_i32, + &0_i32 ] ); @@ -509,7 +608,7 @@ impl DataManager { )) .await?; - if rt.context.comments_enabled == false { + if !rt.context.comments_enabled { return Err(Error::NotAllowed); } } @@ -564,8 +663,14 @@ impl DataManager { Ok(()) } - pub async fn update_post_context(&self, id: usize, user: User, x: PostContext) -> Result<()> { + pub async fn update_post_context( + &self, + id: usize, + user: User, + mut x: PostContext, + ) -> Result<()> { let y = self.get_post_by_id(id).await?; + x.repost = y.context.repost; // cannot change repost settings at all let user_membership = self .get_membership_by_owner_community(user.id, y.community) @@ -588,19 +693,19 @@ impl DataManager { } // check if we can manage pins - if x.is_pinned != y.context.is_pinned { - if !user_membership.role.check(CommunityPermission::MANAGE_PINS) { - // lacking this permission is overtaken by having the MANAGE_POSTS - // global permission - if !user.permissions.check(FinePermission::MANAGE_POSTS) { - return Err(Error::NotAllowed); - } else { - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!("invoked `update_post_context(pinned)` with x value `{id}`"), - )) - .await? - } + if x.is_pinned != y.context.is_pinned + && !user_membership.role.check(CommunityPermission::MANAGE_PINS) + { + // lacking this permission is overtaken by having the MANAGE_POSTS + // global permission + if !user.permissions.check(FinePermission::MANAGE_POSTS) { + return Err(Error::NotAllowed); + } else { + self.create_audit_log_entry(AuditLogEntry::new( + user.id, + format!("invoked `update_post_context(pinned)` with x value `{id}`"), + )) + .await? } } diff --git a/crates/core/src/database/reactions.rs b/crates/core/src/database/reactions.rs index 7c82e39..d806a37 100644 --- a/crates/core/src/database/reactions.rs +++ b/crates/core/src/database/reactions.rs @@ -26,11 +26,7 @@ impl DataManager { owner: get!(x->2(i64)) as usize, asset: get!(x->3(i64)) as usize, asset_type: serde_json::from_str(&get!(x->4(String))).unwrap(), - is_like: if get!(x->5(i32)) as i8 == 1 { - true - } else { - false - }, + is_like: get!(x->5(i32)) as i8 == 1, } } @@ -80,7 +76,7 @@ impl DataManager { &(data.owner as i64), &(data.asset as i64), &serde_json::to_string(&data.asset_type).unwrap().as_str(), - &(if data.is_like { 1 } else { 0 } as i32) + &{ if data.is_like { 1 } else { 0 } } ] ); @@ -103,7 +99,7 @@ impl DataManager { let community = self.get_community_by_id_no_void(data.asset).await.unwrap(); if community.owner != user.id { - if let Err(e) = self + self .create_notification(Notification::new( "Your community has received a like!".to_string(), format!( @@ -112,10 +108,7 @@ impl DataManager { ), community.owner, )) - .await - { - return Err(e); - } + .await? } } } @@ -132,19 +125,16 @@ impl DataManager { let post = self.get_post_by_id(data.asset).await.unwrap(); if post.owner != user.id { - if let Err(e) = self + self .create_notification(Notification::new( "Your post has received a like!".to_string(), format!( - "[@{}](/api/v1/auth/user/find/{}) has liked your post!", - user.username, user.id + "[@{}](/api/v1/auth/user/find/{}) has liked your [post](/post/{})!", + user.username, user.id, data.asset ), post.owner, )) - .await - { - return Err(e); - } + .await? } } } @@ -160,10 +150,8 @@ impl DataManager { pub async fn delete_reaction(&self, id: usize, user: &User) -> Result<()> { let reaction = self.get_reaction_by_id(id).await?; - if user.id != reaction.owner { - if !user.permissions.check(FinePermission::MANAGE_REACTIONS) { - return Err(Error::NotAllowed); - } + if user.id != reaction.owner && !user.permissions.check(FinePermission::MANAGE_REACTIONS) { + return Err(Error::NotAllowed); } let conn = match self.connect().await { @@ -186,26 +174,22 @@ impl DataManager { // decr corresponding match reaction.asset_type { AssetType::Community => { - if let Err(e) = { + { if reaction.is_like { self.decr_community_likes(reaction.asset).await } else { self.decr_community_dislikes(reaction.asset).await } - } { - return Err(e); - } + }? } AssetType::Post => { - if let Err(e) = { + { if reaction.is_like { self.decr_post_likes(reaction.asset).await } else { self.decr_post_dislikes(reaction.asset).await } - } { - return Err(e); - } + }? } AssetType::User => { return Err(Error::NotAllowed); diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs index 085be15..a2288d8 100644 --- a/crates/core/src/database/userfollows.rs +++ b/crates/core/src/database/userfollows.rs @@ -193,7 +193,7 @@ impl DataManager { let mut out: Vec<(UserFollow, User)> = Vec::new(); for userfollow in userfollows { - let receiver = userfollow.receiver.clone(); + let receiver = userfollow.receiver; out.push((userfollow, self.get_user_by_id(receiver).await?)); } @@ -208,7 +208,7 @@ impl DataManager { let mut out: Vec<(UserFollow, User)> = Vec::new(); for userfollow in userfollows { - let initiator = userfollow.initiator.clone(); + let initiator = userfollow.initiator; out.push((userfollow, self.get_user_by_id(initiator).await?)); } @@ -254,10 +254,8 @@ impl DataManager { pub async fn delete_userfollow(&self, id: usize, user: &User) -> Result<()> { let follow = self.get_userfollow_by_id(id).await?; - if (user.id != follow.initiator) && (user.id != follow.receiver) { - if !user.permissions.check(FinePermission::MANAGE_FOLLOWS) { - return Err(Error::NotAllowed); - } + if (user.id != follow.initiator) && (user.id != follow.receiver) && !user.permissions.check(FinePermission::MANAGE_FOLLOWS) { + return Err(Error::NotAllowed); } let conn = match self.connect().await { diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 6058986..ef9e8b7 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -47,6 +47,7 @@ impl Default for ThemePreference { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Default)] pub struct UserSettings { #[serde(default)] pub policy_consent: bool, @@ -72,23 +73,6 @@ pub struct UserSettings { pub disable_other_themes: bool, } -impl Default for UserSettings { - fn default() -> Self { - Self { - policy_consent: false, - display_name: String::new(), - biography: String::new(), - private_profile: false, - private_communities: false, - theme_preference: ThemePreference::default(), - private_last_seen: false, - theme_hue: String::new(), - theme_sat: String::new(), - theme_lit: String::new(), - disable_other_themes: false, - } - } -} impl Default for User { fn default() -> Self { @@ -212,7 +196,7 @@ impl User { return None; } - match TOTP::new( + TOTP::new( totp_rs::Algorithm::SHA1, 6, 1, @@ -220,10 +204,7 @@ impl User { self.totp.as_bytes().to_owned(), Some(issuer.unwrap_or("tetratto!".to_string())), self.username.clone(), - ) { - Ok(t) => Some(t), - Err(_) => None, - } + ).ok() } } diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index aacb1ac..d1c537b 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -70,7 +70,7 @@ impl Community { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct CommunityContext { #[serde(default)] pub display_name: String, @@ -80,16 +80,6 @@ pub struct CommunityContext { pub is_nsfw: bool, } -impl Default for CommunityContext { - fn default() -> Self { - Self { - display_name: String::new(), - description: String::new(), - is_nsfw: false, - } - } -} - /// Who can read a [`Community`]. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum CommunityReadAccess { @@ -166,7 +156,7 @@ impl CommunityMembership { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct PostContext { #[serde(default = "default_comments_enabled")] pub comments_enabled: bool, @@ -178,25 +168,47 @@ pub struct PostContext { pub edited: usize, #[serde(default)] pub is_nsfw: bool, + #[serde(default)] + pub repost: Option, + #[serde(default = "default_reposts_enabled")] + pub reposts_enabled: bool, } fn default_comments_enabled() -> bool { true } +fn default_reposts_enabled() -> bool { + true +} + impl Default for PostContext { fn default() -> Self { Self { comments_enabled: default_comments_enabled(), + reposts_enabled: true, is_pinned: false, is_profile_pinned: false, edited: 0, is_nsfw: false, + repost: None, } } } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RepostContext { + /// Should be `false` is `repost_of` is `Some`. + /// + /// Declares the post to be a repost of another post. + pub is_repost: bool, + /// Should be `None` if `is_repost` is true. + /// + /// Sets the ID of the other post to load. + pub reposting: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Post { pub id: usize, pub created: usize, @@ -238,4 +250,24 @@ impl Post { comment_count: 0, } } + + /// Create a new [`Post`] (as a repost of the given `post_id`). + pub fn repost(content: String, community: usize, owner: usize, post_id: usize) -> Self { + let mut post = Self::new(content, community, None, owner); + + post.context.repost = Some(RepostContext { + is_repost: false, + reposting: Some(post_id), + }); + + post + } + + /// Make the given post a reposted post. + pub fn mark_as_repost(&mut self) { + self.context.repost = Some(RepostContext { + is_repost: true, + reposting: None, + }); + } } diff --git a/crates/core/src/model/communities_permissions.rs b/crates/core/src/model/communities_permissions.rs index 656dd96..246ecb0 100644 --- a/crates/core/src/model/communities_permissions.rs +++ b/crates/core/src/model/communities_permissions.rs @@ -32,7 +32,7 @@ impl Serialize for CommunityPermission { } struct CommunityPermissionVisitor; -impl<'de> Visitor<'de> for CommunityPermissionVisitor { +impl Visitor<'_> for CommunityPermissionVisitor { type Value = CommunityPermission; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 1ae1b18..da5181b 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -54,14 +54,14 @@ impl ToString for Error { } } -impl Into> for Error +impl From for ApiReturn where T: Default + Serialize, { - fn into(self) -> ApiReturn { + fn from(val: Error) -> Self { ApiReturn { ok: false, - message: self.to_string(), + message: val.to_string(), payload: T::default(), } } diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 3941314..fba4b88 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -43,7 +43,7 @@ impl Serialize for FinePermission { } struct FinePermissionVisitor; -impl<'de> Visitor<'de> for FinePermissionVisitor { +impl Visitor<'_> for FinePermissionVisitor { type Value = FinePermission; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 6c64919..4926341 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-l10n" -version = "1.0.1" +version = "1.0.2" edition = "2024" authors.workspace = true repository.workspace = true diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index 450ac12..f90bcca 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tetratto-shared" -version = "1.0.1" +version = "1.0.2" edition = "2024" authors.workspace = true repository.workspace = true