diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html index f2bf48e..73cba83 100644 --- a/crates/app/src/public/html/components.html +++ b/crates/app/src/public/html/components.html @@ -227,6 +227,14 @@ and show_community and community.id != config.town_square or question %} > {{ icon "user-round" }} + {% endif %} {% if post.is_deleted %} + + {{ icon "trash-2" }} + {% endif %} diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs index cb8c3c1..584cc78 100644 --- a/crates/app/src/routes/api/v1/channels/channels.rs +++ b/crates/app/src/routes/api/v1/channels/channels.rs @@ -138,7 +138,7 @@ pub async fn update_title_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_channel_title(id, user, &req.title).await { + match data.update_channel_title(id, &user, &req.title).await { Ok(_) => Json(ApiReturn { ok: true, message: "Channel updated".to_string(), @@ -160,7 +160,7 @@ pub async fn update_position_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_channel_position(id, user, req.position).await { + match data.update_channel_position(id, &user, req.position).await { Ok(_) => Json(ApiReturn { ok: true, message: "Channel updated".to_string(), diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs index 1601d69..5f8688a 100644 --- a/crates/app/src/routes/api/v1/communities/communities.rs +++ b/crates/app/src/routes/api/v1/communities/communities.rs @@ -117,7 +117,7 @@ pub async fn update_context_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_community_context(id, user, req.context).await { + match data.update_community_context(id, &user, req.context).await { Ok(_) => Json(ApiReturn { ok: true, message: "Community updated".to_string(), @@ -140,7 +140,7 @@ pub async fn update_read_access_request( }; match data - .update_community_read_access(id, user, req.access) + .update_community_read_access(id, &user, req.access) .await { Ok(_) => Json(ApiReturn { @@ -165,7 +165,7 @@ pub async fn update_write_access_request( }; match data - .update_community_write_access(id, user, req.access) + .update_community_write_access(id, &user, req.access) .await { Ok(_) => Json(ApiReturn { @@ -190,7 +190,7 @@ pub async fn update_join_access_request( }; match data - .update_community_join_access(id, user, req.access) + .update_community_join_access(id, &user, req.access) .await { Ok(_) => Json(ApiReturn { diff --git a/crates/app/src/routes/api/v1/communities/emojis.rs b/crates/app/src/routes/api/v1/communities/emojis.rs index 2676d6c..c8d1713 100644 --- a/crates/app/src/routes/api/v1/communities/emojis.rs +++ b/crates/app/src/routes/api/v1/communities/emojis.rs @@ -183,7 +183,7 @@ pub async fn update_name_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_emoji_name(id, user, &req.name).await { + match data.update_emoji_name(id, &user, &req.name).await { Ok(_) => Json(ApiReturn { ok: true, message: "Emoji updated".to_string(), diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 8f565bc..b278adf 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -173,6 +173,31 @@ pub async fn delete_request( None => return Json(Error::NotAllowed.into()), }; + match data.fake_delete_post(id, user, true).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Post deleted".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn real_delete_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_POSTS) { + return Json(Error::NotAllowed.into()); + } + match data.delete_post(id, user).await { Ok(_) => Json(ApiReturn { ok: true, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 1cea099..9d35aa8 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -93,6 +93,10 @@ pub fn routes() -> Router { // posts .route("/posts", post(communities::posts::create_request)) .route("/posts/{id}", delete(communities::posts::delete_request)) + .route( + "/posts/{id}/real_delete", + delete(communities::posts::real_delete_request), + ) .route( "/posts/{id}/repost", post(communities::posts::create_repost_request), diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index a33df1e..85ebd5a 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -43,7 +43,7 @@ pub async fn update_name_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_stack_name(id, user, &req.name).await { + match data.update_stack_name(id, &user, &req.name).await { Ok(_) => Json(ApiReturn { ok: true, message: "Stack updated".to_string(), @@ -65,7 +65,7 @@ pub async fn update_privacy_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_stack_privacy(id, user, req.privacy).await { + match data.update_stack_privacy(id, &user, req.privacy).await { Ok(_) => Json(ApiReturn { ok: true, message: "Stack updated".to_string(), @@ -87,7 +87,7 @@ pub async fn update_mode_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_stack_mode(id, user, req.mode).await { + match data.update_stack_mode(id, &user, req.mode).await { Ok(_) => Json(ApiReturn { ok: true, message: "Stack updated".to_string(), @@ -109,7 +109,7 @@ pub async fn update_sort_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_stack_sort(id, user, req.sort).await { + match data.update_stack_sort(id, &user, req.sort).await { Ok(_) => Json(ApiReturn { ok: true, message: "Stack updated".to_string(), @@ -152,7 +152,7 @@ pub async fn add_user_request( }; stack.users.push(other_user.id); - match data.update_stack_users(id, user, stack.users).await { + match data.update_stack_users(id, &user, stack.users).await { Ok(_) => Json(ApiReturn { ok: true, message: "User added".to_string(), @@ -191,7 +191,7 @@ pub async fn remove_user_request( None => return Json(Error::GeneralNotFound("user".to_string()).into()), }); - match data.update_stack_users(id, user, stack.users).await { + match data.update_stack_users(id, &user, stack.users).await { Ok(_) => Json(ApiReturn { ok: true, message: "User removed".to_string(), diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index e6949ff..8de6c41 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -589,6 +589,33 @@ pub async fn post_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; + if post.is_deleted { + // act like the post doesn't exist (if missing MANAGE_POSTS) + if let Some(ref ua) = user { + if !ua.permissions.check(FinePermission::MANAGE_POSTS) { + return Err(Html( + render_error( + Error::GeneralNotFound("post".to_string()), + &jar, + &data, + &user, + ) + .await, + )); + } + } else { + return Err(Html( + render_error( + Error::GeneralNotFound("post".to_string()), + &jar, + &data, + &user, + ) + .await, + )); + } + } + let community = match data.0.get_community_by_id(post.community).await { Ok(c) => c, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), diff --git a/crates/app/src/routes/pages/stacks.rs b/crates/app/src/routes/pages/stacks.rs index e103636..52cb44d 100644 --- a/crates/app/src/routes/pages/stacks.rs +++ b/crates/app/src/routes/pages/stacks.rs @@ -120,15 +120,29 @@ pub async fn manage_request( )); } + let mut new_users = stack.users.clone(); + let mut changed_stack_users: bool = false; + let mut users: Vec = Vec::new(); for uid in &stack.users { users.push(match data.0.get_user_by_id(uid.to_owned()).await { Ok(ua) => ua, - Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + Err(_) => { + // user deleted profile, remove from list + new_users.remove(stack.users.iter().position(|x| x == uid).unwrap()); + changed_stack_users = true; + continue; + } }); } + if changed_stack_users { + if let Err(e) = data.0.update_stack_users(stack.id, &user, new_users).await { + return Err(Html(render_error(e, &jar, &data, &None).await)); + } + } + let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0, lang, &Some(user)).await; diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index a500067..399005b 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -69,7 +69,9 @@ macro_rules! auto_method { ($name:ident()@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => { pub async fn $name(&self, id: usize) -> Result<$returns_> { if let Some(cached) = self.2.get(format!($cache_key_tmpl, id)).await { - return Ok(serde_json::from_str(&cached).unwrap()); + if let Ok(c) = serde_json::from_str(&cached) { + return Ok(c); + } } let conn = match self.connect().await { @@ -183,7 +185,7 @@ macro_rules! auto_method { }; ($name:ident()@$select_fn:ident:$permission:ident -> $query:literal) => { - pub async fn $name(&self, id: usize, user: User) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -213,7 +215,7 @@ macro_rules! auto_method { }; ($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => { - pub async fn $name(&self, id: usize, user: User) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -245,7 +247,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -276,7 +278,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:literal) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -309,7 +311,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -344,7 +346,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:literal) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -499,7 +501,7 @@ macro_rules! auto_method { }; ($name:ident()@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:ident) => { - pub async fn $name(&self, id: usize, user: User) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -532,7 +534,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --cache-key-tmpl=$cache_key_tmpl:ident) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { @@ -611,7 +613,7 @@ macro_rules! auto_method { }; ($name:ident($x:ty)@$select_fn:ident:$permission:ident -> $query:literal --serde --cache-key-tmpl=$cache_key_tmpl:ident) => { - pub async fn $name(&self, id: usize, user: User, x: $x) -> Result<()> { + pub async fn $name(&self, id: usize, user: &User, x: $x) -> Result<()> { let y = self.$select_fn(id).await?; if user.id != y.owner { diff --git a/crates/core/src/database/drivers/sql/create_posts.sql b/crates/core/src/database/drivers/sql/create_posts.sql index ab0bc6f..76350d7 100644 --- a/crates/core/src/database/drivers/sql/create_posts.sql +++ b/crates/core/src/database/drivers/sql/create_posts.sql @@ -12,5 +12,6 @@ CREATE TABLE IF NOT EXISTS posts ( -- other counts comment_count INT NOT NULL, -- ... - uploads TEXT NOT NULL + uploads TEXT NOT NULL, + is_deleted INT NOT NULL ) diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index dfa279b..99bb4b8 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -42,6 +42,7 @@ impl DataManager { comment_count: get!(x->9(i32)) as usize, // ... uploads: serde_json::from_str(&get!(x->10(String))).unwrap(), + is_deleted: get!(x->11(i32)) as i8 == 1, } } @@ -195,6 +196,8 @@ impl DataManager { // check relationship if ua.settings.private_profile { + // if someone were to look for places to optimize memory usage, + // look no further than here if let Some(ua1) = user { if ua1.id == 0 { continue; @@ -202,7 +205,10 @@ impl DataManager { if let Some(is_following) = seen_user_follow_statuses.get(&(ua.id, ua1.id)) { - if !is_following && (ua.id != ua1.id) { + if !is_following + && (ua.id != ua1.id) + && !ua1.permissions.check(FinePermission::MANAGE_POSTS) + { // post owner is not following us continue; } @@ -211,6 +217,7 @@ impl DataManager { .get_userfollow_by_initiator_receiver(ua.id, ua1.id) .await .is_err() + && !ua1.permissions.check(FinePermission::MANAGE_POSTS) { // post owner is not following us seen_user_follow_statuses.insert((ua.id, ua1.id), false); @@ -1105,7 +1112,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", + "INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", params![ &(data.id as i64), &(data.created as i64), @@ -1121,7 +1128,8 @@ impl DataManager { &0_i32, &0_i32, &0_i32, - &serde_json::to_string(&data.uploads).unwrap() + &serde_json::to_string(&data.uploads).unwrap(), + &{ if data.is_deleted { 1 } else { 0 } } ] ); @@ -1212,7 +1220,7 @@ impl DataManager { let question = self.get_question_by_id(y.context.answering).await?; if question.is_global { - self.incr_question_answer_count(y.context.answering).await?; + self.decr_question_answer_count(y.context.answering).await?; } } @@ -1225,6 +1233,94 @@ impl DataManager { Ok(()) } + pub async fn fake_delete_post(&self, id: usize, user: User, is_deleted: bool) -> Result<()> { + let y = self.get_post_by_id(id).await?; + + let user_membership = self + .get_membership_by_owner_community(user.id, y.community) + .await?; + + if (user.id != y.owner) + && !user_membership + .role + .check(CommunityPermission::MANAGE_POSTS) + { + if !user.permissions.check(FinePermission::MANAGE_POSTS) { + return Err(Error::NotAllowed); + } else { + self.create_audit_log_entry(AuditLogEntry::new( + user.id, + format!("invoked `fake_delete_post` with x value `{id}`"), + )) + .await? + } + } + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "UPDATE posts SET is_deleted = $1 WHERE id = $2", + params![if is_deleted { 1 } else { 0 }, &(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.post:{}", id)).await; + + if is_deleted { + // decr parent comment count + if let Some(replying_to) = y.replying_to { + self.decr_post_comments(replying_to).await.unwrap(); + } + + // decr user post count + let owner = self.get_user_by_id(y.owner).await?; + + if owner.post_count > 0 { + self.decr_user_post_count(y.owner).await?; + } + + // decr question answer count + if y.context.answering != 0 { + let question = self.get_question_by_id(y.context.answering).await?; + + if question.is_global { + self.incr_question_answer_count(y.context.answering).await?; + } + } + } else { + // incr parent comment count + if let Some(replying_to) = y.replying_to { + self.incr_post_comments(replying_to).await.unwrap(); + } + + // incr user post count + let owner = self.get_user_by_id(y.owner).await?; + + if owner.post_count > 0 { + self.incr_user_post_count(y.owner).await?; + } + + // incr question answer count + if y.context.answering != 0 { + let question = self.get_question_by_id(y.context.answering).await?; + + if question.is_global { + self.decr_question_answer_count(y.context.answering).await?; + } + } + } + + // return + Ok(()) + } + pub async fn update_post_context( &self, id: usize, diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index 1e7094e..886dfd0 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -238,6 +238,8 @@ pub struct Post { pub comment_count: usize, /// IDs of all uploads linked to this post. pub uploads: Vec, + /// If the post was deleted. + pub is_deleted: bool, } impl Post { @@ -260,6 +262,7 @@ impl Post { dislikes: 0, comment_count: 0, uploads: Vec::new(), + is_deleted: false, } } diff --git a/sql_changes/posts_is_deleted.sql b/sql_changes/posts_is_deleted.sql new file mode 100644 index 0000000..01207f2 --- /dev/null +++ b/sql_changes/posts_is_deleted.sql @@ -0,0 +1,2 @@ +ALTER TABLE posts +ADD COLUMN is_deleted INT NOT NULL DEFAULT 0;