diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 4931ed583..390fa9887 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1448,7 +1448,6 @@ Mods and admins can remove and lock a post, creators can delete it. data: { content: String, parent_id: Option, - edit_id: Option, post_id: i32, auth: String } @@ -1470,7 +1469,7 @@ Mods and admins can remove and lock a post, creators can delete it. #### Edit Comment -Mods and admins can remove a comment, creators can delete it. +Only the creator can edit the comment. ##### Request ```rust @@ -1478,15 +1477,8 @@ Mods and admins can remove a comment, creators can delete it. op: "EditComment", data: { content: String, - parent_id: Option, edit_id: i32, - creator_id: i32, - post_id: i32, - removed: Option, - deleted: Option, - reason: Option, - read: Option, - auth: String + auth: String, } } ``` @@ -1503,6 +1495,89 @@ Mods and admins can remove a comment, creators can delete it. `PUT /comment` +#### Delete Comment + +Only the creator can delete the comment. + +##### Request +```rust +{ + op: "DeleteComment", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeleteComment", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/delete` + + +#### Remove Comment + +Only a mod or admin can remove the comment. + +##### Request +```rust +{ + op: "RemoveComment", + data: { + edit_id: i32, + removed: bool, + reason: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "RemoveComment", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/remove` + +#### Mark Comment as Read +##### Request +```rust +{ + op: "MarkCommentAsRead", + data: { + edit_id: i32, + read: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "MarkCommentAsRead", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/mark_as_read` + #### Save Comment ##### Request ```rust @@ -1538,7 +1613,6 @@ Mods and admins can remove a comment, creators can delete it. op: "CreateCommentLike", data: { comment_id: i32, - post_id: i32, score: i16, auth: String } diff --git a/server/lemmy_db/src/comment.rs b/server/lemmy_db/src/comment.rs index dc369c8bc..de6904133 100644 --- a/server/lemmy_db/src/comment.rs +++ b/server/lemmy_db/src/comment.rs @@ -97,14 +97,6 @@ impl Comment { comment.filter(ap_id.eq(object_id)).first::(conn) } - pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result { - use crate::schema::comment::dsl::*; - - diesel::update(comment.find(comment_id)) - .set(read.eq(true)) - .get_result::(conn) - } - pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result { use crate::schema::comment::dsl::*; @@ -116,6 +108,46 @@ impl Comment { )) .get_result::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + comment_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + comment_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn update_content( + conn: &PgConnection, + comment_id: i32, + new_content: &str, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set((content.eq(new_content), updated.eq(naive_now()))) + .get_result::(conn) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index 4fe507f72..03c47e463 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -1,4 +1,5 @@ use crate::{ + naive_now, schema::{community, community_follower, community_moderator, community_user_ban}, Bannable, Crud, @@ -29,7 +30,6 @@ pub struct Community { pub last_refreshed_at: chrono::NaiveDateTime, } -// TODO add better delete, remove, lock actions here. #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)] #[table_name = "community"] pub struct CommunityForm { @@ -129,9 +129,24 @@ impl Community { ) -> Result { use crate::schema::community::dsl::*; diesel::update(community.find(community_id)) - .set(creator_id.eq(new_creator_id)) + .set((creator_id.eq(new_creator_id), updated.eq(naive_now()))) .get_result::(conn) } + + pub fn community_mods_and_admins( + conn: &PgConnection, + community_id: i32, + ) -> Result, Error> { + use crate::{community_view::CommunityModeratorView, user_view::UserView}; + let mut mods_and_admins: Vec = Vec::new(); + mods_and_admins.append( + &mut CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect())?, + ); + mods_and_admins + .append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); + Ok(mods_and_admins) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/lemmy_db/src/private_message.rs b/server/lemmy_db/src/private_message.rs index 30f40e6b8..3486cf545 100644 --- a/server/lemmy_db/src/private_message.rs +++ b/server/lemmy_db/src/private_message.rs @@ -1,4 +1,4 @@ -use crate::{schema::private_message, Crud}; +use crate::{naive_now, schema::private_message, Crud}; use diesel::{dsl::*, result::Error, *}; use serde::{Deserialize, Serialize}; @@ -88,7 +88,7 @@ impl PrivateMessage { ) -> Result { use crate::schema::private_message::dsl::*; diesel::update(private_message.find(private_message_id)) - .set(content.eq(new_content)) + .set((content.eq(new_content), updated.eq(naive_now()))) .get_result::(conn) } diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index f8bdf5d5b..1a06032b2 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -13,14 +13,13 @@ use crate::{ use lemmy_db::{ comment::*, comment_view::*, + community::Community, community_view::*, moderator::*, - naive_now, post::*, site_view::*, user::*, user_mention::*, - user_view::*, Crud, Likeable, ListingType, @@ -44,7 +43,6 @@ use std::str::FromStr; pub struct CreateComment { content: String, parent_id: Option, - edit_id: Option, // TODO this isn't used pub post_id: i32, auth: String, } @@ -52,14 +50,29 @@ pub struct CreateComment { #[derive(Serialize, Deserialize)] pub struct EditComment { content: String, - parent_id: Option, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change edit_id: i32, - creator_id: i32, - pub post_id: i32, - removed: Option, - deleted: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeleteComment { + edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RemoveComment { + edit_id: i32, + removed: bool, reason: Option, - read: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MarkCommentAsRead { + edit_id: i32, + read: bool, auth: String, } @@ -79,7 +92,6 @@ pub struct CommentResponse { #[derive(Serialize, Deserialize)] pub struct CreateCommentLike { comment_id: i32, - pub post_id: i32, score: i16, auth: String, } @@ -150,6 +162,7 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + // Create the comment let comment_form2 = comment_form.clone(); let inserted_comment = match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? { @@ -157,6 +170,7 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), }; + // Necessary to update the ap_id let inserted_comment_id = inserted_comment.id; let updated_comment: Comment = match blocking(pool, move |conn| { let apub_id = @@ -175,8 +189,15 @@ impl Perform for Oper { // Scan the comment for user mentions, add those rows let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = - send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?; + let recipient_ids = send_local_notifs( + mentions, + updated_comment.clone(), + user.clone(), + post, + pool, + true, + ) + .await?; // You like your own comment by default let like_form = CommentLikeForm { @@ -237,122 +258,34 @@ impl Perform for Oper { let user_id = claims.id; - let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??; - let edit_id = data.edit_id; let orig_comment = blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; - let mut editors: Vec = vec![orig_comment.creator_id]; - let mut moderators: Vec = vec![]; - - let community_id = orig_comment.community_id; - moderators.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(&conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - moderators.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - - editors.extend(&moderators); - // You are allowed to mark the comment as read even if you're banned. - if data.read.is_none() { - // Verify its the creator or a mod, or an admin - - if !editors.contains(&user_id) { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - - // Check for a community ban - let community_id = orig_comment.community_id; - let is_banned = - move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); - if blocking(pool, is_banned).await? { - return Err(APIError::err("community_ban").into()); - } - - // Check for a site ban - if user.banned { - return Err(APIError::err("site_ban").into()); - } - } else { - // check that user can mark as read - let parent_id = orig_comment.parent_id; - match parent_id { - Some(pid) => { - let parent_comment = - blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; - if user_id != parent_comment.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - None => { - let parent_post_id = orig_comment.post_id; - let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; - if user_id != parent_post.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - } + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); } + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the creator can edit + if user_id != orig_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + + // Do the update let content_slurs_removed = remove_slurs(&data.content.to_owned()); - let edit_id = data.edit_id; - let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??; - - let comment_form = { - if data.read.is_none() { - // the ban etc checks should been made and have passed - // the comment can be properly edited - let post_removed = if moderators.contains(&user_id) { - data.removed - } else { - Some(read_comment.removed) - }; - - CommentForm { - content: content_slurs_removed, - parent_id: read_comment.parent_id, - post_id: read_comment.post_id, - creator_id: read_comment.creator_id, - removed: post_removed.to_owned(), - deleted: data.deleted.to_owned(), - read: Some(read_comment.read), - published: None, - updated: Some(naive_now()), - ap_id: read_comment.ap_id, - local: read_comment.local, - } - } else { - // the only field that can be updated it the read field - CommentForm { - content: read_comment.content, - parent_id: read_comment.parent_id, - post_id: read_comment.post_id, - creator_id: read_comment.creator_id, - removed: Some(read_comment.removed).to_owned(), - deleted: Some(read_comment.deleted).to_owned(), - read: data.read.to_owned(), - published: None, - updated: orig_comment.updated, - ap_id: read_comment.ap_id, - local: read_comment.local, - } - } - }; - - let edit_id = data.edit_id; - let comment_form2 = comment_form.clone(); let updated_comment = match blocking(pool, move |conn| { - Comment::update(conn, edit_id, &comment_form2) + Comment::update_content(conn, edit_id, &content_slurs_removed) }) .await? { @@ -360,54 +293,19 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; - if data.read.is_none() { - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_comment - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_comment - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else if let Some(removed) = data.removed.to_owned() { - if moderators.contains(&user_id) { - if removed { - updated_comment - .send_remove(&user, &self.client, pool) - .await?; - } else { - updated_comment - .send_undo_remove(&user, &self.client, pool) - .await?; - } - } - } else { - updated_comment - .send_update(&user, &self.client, pool) - .await?; - } + // Send the apub update + updated_comment + .send_update(&user, &self.client, pool) + .await?; - // Mod tables - if moderators.contains(&user_id) { - if let Some(removed) = data.removed.to_owned() { - let form = ModRemoveCommentForm { - mod_user_id: user_id, - comment_id: data.edit_id, - removed: Some(removed), - reason: data.reason.to_owned(), - }; - blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??; - } - } - } - - let post_id = data.post_id; + // Do the mentions / recipients + let post_id = orig_comment.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; - let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; + let updated_comment_content = updated_comment.content.to_owned(); + let mentions = scrape_text_for_mentions(&updated_comment_content); + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; let edit_id = data.edit_id; let comment_view = blocking(pool, move |conn| { @@ -436,6 +334,294 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeleteComment = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the creator can delete + if user_id != orig_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + + // Do the delete + let deleted = data.deleted; + let updated_comment = match blocking(pool, move |conn| { + Comment::update_deleted(conn, edit_id, deleted) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Send the apub message + if deleted { + updated_comment + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_comment + .send_undo_delete(&user, &self.client, pool) + .await?; + } + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + // Build the recipients + let post_id = comment_view.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let mentions = vec![]; + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; + + let mut res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendComment { + op: UserOperation::DeleteComment, + comment: res.clone(), + my_id: ws.id, + }); + + // strip out the recipient_ids, so that + // users don't get double notifs + res.recipient_ids = Vec::new(); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &RemoveComment = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only a mod or admin can remove + let mods_and_admins = blocking(pool, move |conn| { + Community::community_mods_and_admins(conn, community_id) + }) + .await??; + if !mods_and_admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Do the remove + let removed = data.removed; + let updated_comment = match blocking(pool, move |conn| { + Comment::update_removed(conn, edit_id, removed) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Mod tables + let form = ModRemoveCommentForm { + mod_user_id: user_id, + comment_id: data.edit_id, + removed: Some(removed), + reason: data.reason.to_owned(), + }; + blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??; + + // Send the apub message + if removed { + updated_comment + .send_remove(&user, &self.client, pool) + .await?; + } else { + updated_comment + .send_undo_remove(&user, &self.client, pool) + .await?; + } + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + // Build the recipients + let post_id = comment_view.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let mentions = vec![]; + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; + + let mut res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendComment { + op: UserOperation::RemoveComment, + comment: res.clone(), + my_id: ws.id, + }); + + // strip out the recipient_ids, so that + // users don't get double notifs + res.recipient_ids = Vec::new(); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + _websocket_info: Option, + ) -> Result { + let data: &MarkCommentAsRead = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the recipient can mark as read + // Needs to fetch the parent comment / post to get the recipient + let parent_id = orig_comment.parent_id; + match parent_id { + Some(pid) => { + let parent_comment = + blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; + if user_id != parent_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + None => { + let parent_post_id = orig_comment.post_id; + let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; + if user_id != parent_post.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + } + + // Do the mark as read + let read = data.read; + match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = CommentResponse { + comment: comment_view, + recipient_ids: Vec::new(), + }; + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; @@ -512,8 +698,12 @@ impl Perform for Oper { } } + let comment_id = data.comment_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??; + // Check for a community ban - let post_id = data.post_id; + let post_id = orig_comment.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let community_id = post.community_id; let is_banned = @@ -550,7 +740,7 @@ impl Perform for Oper { let like_form = CommentLikeForm { comment_id: data.comment_id, - post_id: data.post_id, + post_id, user_id, score: data.score, }; @@ -675,9 +865,10 @@ pub async fn send_local_notifs( user: User_, post: Post, pool: &DbPool, + do_send_email: bool, ) -> Result, LemmyError> { let ids = blocking(pool, move |conn| { - do_send_local_notifs(conn, &mentions, &comment, &user, &post) + do_send_local_notifs(conn, &mentions, &comment, &user, &post, do_send_email) }) .await?; @@ -690,6 +881,7 @@ fn do_send_local_notifs( comment: &Comment, user: &User_, post: &Post, + do_send_email: bool, ) -> Vec { let mut recipient_ids = Vec::new(); let hostname = &format!("https://{}", Settings::get().hostname); @@ -720,7 +912,7 @@ fn do_send_local_notifs( }; // Send an email to those users that have notifications on - if mention_user.send_notifications_to_email { + if do_send_email && mention_user.send_notifications_to_email { if let Some(mention_email) = mention_user.email { let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,); let html = &format!( @@ -744,7 +936,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) { recipient_ids.push(parent_user.id); - if parent_user.send_notifications_to_email { + if do_send_email && parent_user.send_notifications_to_email { if let Some(comment_reply_email) = parent_user.email { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( @@ -767,7 +959,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, post.creator_id) { recipient_ids.push(parent_user.id); - if parent_user.send_notifications_to_email { + if do_send_email && parent_user.send_notifications_to_email { if let Some(post_reply_email) = parent_user.email { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 1f46c596e..5e84bc6c6 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -10,7 +10,6 @@ use crate::{ }, DbPool, }; -use diesel::PgConnection; use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType}; use lemmy_utils::{ generate_actor_keypair, @@ -1078,16 +1077,3 @@ impl Perform for Oper { }) } } - -pub fn community_mods_and_admins( - conn: &PgConnection, - community_id: i32, -) -> Result, LemmyError> { - let mut editors: Vec = Vec::new(); - editors.append( - &mut CommunityModeratorView::for_community(conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect())?, - ); - editors.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); - Ok(editors) -} diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 71fecea78..ec61c658e 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -936,9 +936,11 @@ impl Perform for Oper { .await??; // TODO: this should probably be a bulk operation + // Not easy to do as a bulk operation, + // because recipient_id isn't in the comment table for reply in &replies { let reply_id = reply.id; - let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id); + let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true); if blocking(pool, mark_as_read).await?.is_err() { return Err(APIError::err("couldnt_update_comment").into()); } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 8d6b255d3..41d1a80e8 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -393,7 +393,7 @@ async fn receive_create_comment( // anyway. let mentions = scrape_text_for_mentions(&inserted_comment.content); let recipient_ids = - send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?; + send_local_notifs(mentions, inserted_comment.clone(), user, post, pool, true).await?; // Refetch the view let comment_view = blocking(pool, move |conn| { @@ -558,7 +558,7 @@ async fn receive_update_comment( let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let mentions = scrape_text_for_mentions(&updated_comment.content); - let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; + let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; // Refetch the view let comment_view = diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 4722fb81f..9fc84f4c3 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -83,6 +83,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("", web::post().to(route_post::)) .route("", web::put().to(route_post::)) + .route("/delete", web::post().to(route_post::)) + .route("/remove", web::post().to(route_post::)) + .route( + "/mark_as_read", + web::post().to(route_post::), + ) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)), ) diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index c4c021463..ed8ee272a 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -28,6 +28,9 @@ pub enum UserOperation { GetCommunity, CreateComment, EditComment, + DeleteComment, + RemoveComment, + MarkCommentAsRead, SaveComment, CreateCommentLike, GetPosts, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 0344e1b99..6f0516ffd 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -506,6 +506,9 @@ impl ChatServer { // Comment ops UserOperation::CreateComment => do_user_operation::(args).await, UserOperation::EditComment => do_user_operation::(args).await, + UserOperation::DeleteComment => do_user_operation::(args).await, + UserOperation::RemoveComment => do_user_operation::(args).await, + UserOperation::MarkCommentAsRead => do_user_operation::(args).await, UserOperation::SaveComment => do_user_operation::(args).await, UserOperation::GetComments => do_user_operation::(args).await, UserOperation::CreateCommentLike => do_user_operation::(args).await, diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 891654b28..f3cc86736 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -11,6 +11,8 @@ import { GetFollowedCommunitiesResponse, GetPostResponse, CommentForm, + DeleteCommentForm, + RemoveCommentForm, CommentResponse, CommunityForm, DeleteCommunityForm, @@ -383,7 +385,6 @@ describe('main', () => { let unlikeCommentForm: CommentLikeForm = { comment_id: createResponse.comment.id, score: 0, - post_id: 2, auth: lemmyAlphaAuth, }; @@ -621,19 +622,16 @@ describe('main', () => { expect(createCommentRes.comment.content).toBe(commentContent); // lemmy_beta deletes the comment - let deleteCommentForm: CommentForm = { - content: commentContent, + let deleteCommentForm: DeleteCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, deleted: true, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let deleteCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -650,19 +648,16 @@ describe('main', () => { expect(getPostRes.comments[0].deleted).toBe(true); // lemmy_beta undeletes the comment - let undeleteCommentForm: CommentForm = { - content: commentContent, + let undeleteCommentForm: DeleteCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, deleted: false, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let undeleteCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -889,19 +884,16 @@ describe('main', () => { expect(createCommentRes.comment.content).toBe(commentContent); // lemmy_beta removes the comment - let removeCommentForm: CommentForm = { - content: commentContent, + let removeCommentForm: RemoveCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, removed: true, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let removeCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -918,19 +910,16 @@ describe('main', () => { expect(getPostRes.comments[0].removed).toBe(true); // lemmy_beta undeletes the comment - let unremoveCommentForm: CommentForm = { - content: commentContent, + let unremoveCommentForm: RemoveCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, removed: false, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let unremoveCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index dfe52ec1c..527cad895 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -3,7 +3,9 @@ import { Link } from 'inferno-router'; import { CommentNode as CommentNodeI, CommentLikeForm, - CommentForm as CommentFormI, + DeleteCommentForm, + RemoveCommentForm, + MarkCommentAsReadForm, MarkUserMentionAsReadForm, SaveCommentForm, BanFromCommunityForm, @@ -848,16 +850,12 @@ export class CommentNode extends Component { } handleDeleteClick(i: CommentNode) { - let deleteForm: CommentFormI = { - content: i.props.node.comment.content, + let deleteForm: DeleteCommentForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, deleted: !i.props.node.comment.deleted, auth: null, }; - WebSocketService.Instance.editComment(deleteForm); + WebSocketService.Instance.deleteComment(deleteForm); } handleSaveCommentClick(i: CommentNode) { @@ -901,7 +899,6 @@ export class CommentNode extends Component { let form: CommentLikeForm = { comment_id: i.comment.id, - post_id: i.comment.post_id, score: this.state.my_vote, }; @@ -929,7 +926,6 @@ export class CommentNode extends Component { let form: CommentLikeForm = { comment_id: i.comment.id, - post_id: i.comment.post_id, score: this.state.my_vote, }; @@ -950,17 +946,13 @@ export class CommentNode extends Component { handleModRemoveSubmit(i: CommentNode) { event.preventDefault(); - let form: CommentFormI = { - content: i.props.node.comment.content, + let form: RemoveCommentForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, removed: !i.props.node.comment.removed, reason: i.state.removeReason, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.removeComment(form); i.state.showRemoveDialog = false; i.setState(i.state); @@ -975,16 +967,12 @@ export class CommentNode extends Component { }; WebSocketService.Instance.markUserMentionAsRead(form); } else { - let form: CommentFormI = { - content: i.props.node.comment.content, + let form: MarkCommentAsReadForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, read: !i.props.node.comment.read, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.markCommentAsRead(form); } i.state.readLoading = true; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 2899c2cb6..66eaf96e6 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -409,7 +409,11 @@ export class Community extends Component { this.state.comments = data.comments; this.state.loading = false; this.setState(this.state); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 5609879cc..66a3d6767 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -484,9 +484,16 @@ export class Inbox extends Component { this.setState(this.state); } else if (res.op == UserOperation.MarkAllAsRead) { // Moved to be instant - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.replies); + this.setState(this.state); + } else if (res.op == UserOperation.MarkCommentAsRead) { + let data = res.data as CommentResponse; // If youre in the unread view, just remove it from the list if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) { diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 0392090a5..d203cd081 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -701,7 +701,11 @@ export class Main extends Component { this.state.comments = data.comments; this.state.loading = false; this.setState(this.state); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 97f80b6e6..91ffeb19f 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -8,7 +8,7 @@ import { GetPostResponse, PostResponse, Comment, - CommentForm as CommentFormI, + MarkCommentAsReadForm, CommentResponse, CommentSortType, CommentViewType, @@ -167,16 +167,12 @@ export class Post extends Component { UserService.Instance.user && UserService.Instance.user.id == parent_user_id ) { - let form: CommentFormI = { - content: found.content, + let form: MarkCommentAsReadForm = { edit_id: found.id, - creator_id: found.creator_id, - post_id: found.post_id, - parent_id: found.parent_id, read: true, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.markCommentAsRead(form); UserService.Instance.user.unreadCount--; UserService.Instance.sub.next({ user: UserService.Instance.user, @@ -435,7 +431,11 @@ export class Post extends Component { this.state.comments.unshift(data.comment); this.setState(this.state); } - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/user-details.tsx b/ui/src/components/user-details.tsx index 5f2346a24..339dc5e4a 100644 --- a/ui/src/components/user-details.tsx +++ b/ui/src/components/user-details.tsx @@ -257,7 +257,11 @@ export class UserDetails extends Component { this.setState({ comments: this.state.comments, }); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { const data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState({ diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 7f650f1b1..6006f2ee4 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -9,6 +9,9 @@ export enum UserOperation { GetCommunity, CreateComment, EditComment, + DeleteComment, + RemoveComment, + MarkCommentAsRead, SaveComment, CreateCommentLike, GetPosts, @@ -679,14 +682,29 @@ export interface PostResponse { export interface CommentForm { content: string; - post_id: number; + post_id?: number; parent_id?: number; edit_id?: number; creator_id?: number; - removed?: boolean; - deleted?: boolean; + auth: string; +} + +export interface DeleteCommentForm { + edit_id: number; + deleted: boolean; + auth: string; +} + +export interface RemoveCommentForm { + edit_id: number; + removed: boolean; reason?: string; - read?: boolean; + auth: string; +} + +export interface MarkCommentAsReadForm { + edit_id: number; + read: boolean; auth: string; } @@ -703,7 +721,6 @@ export interface CommentResponse { export interface CommentLikeForm { comment_id: number; - post_id: number; score: number; auth?: string; } @@ -901,6 +918,9 @@ export type MessageType = | GetPostsForm | GetCommunityForm | CommentForm + | DeleteCommentForm + | RemoveCommentForm + | MarkCommentAsReadForm | CommentLikeForm | SaveCommentForm | CreatePostLikeForm diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 26e58135d..2c85425d8 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -9,6 +9,9 @@ import { PostForm, SavePostForm, CommentForm, + DeleteCommentForm, + RemoveCommentForm, + MarkCommentAsReadForm, SaveCommentForm, CommentLikeForm, GetPostForm, @@ -165,14 +168,29 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form)); } - public createComment(commentForm: CommentForm) { - this.setAuth(commentForm); - this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm)); + public createComment(form: CommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form)); } - public editComment(commentForm: CommentForm) { - this.setAuth(commentForm); - this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm)); + public editComment(form: CommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.EditComment, form)); + } + + public deleteComment(form: DeleteCommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeleteComment, form)); + } + + public removeComment(form: RemoveCommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.RemoveComment, form)); + } + + public markCommentAsRead(form: MarkCommentAsReadForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.MarkCommentAsRead, form)); } public likeComment(form: CommentLikeForm) {