Added comment delete, remove, read.

This commit is contained in:
Dessalines 2020-07-20 21:37:44 -04:00
parent ca7d2feedb
commit fd96dfdb5e
20 changed files with 626 additions and 279 deletions

View file

@ -1448,7 +1448,6 @@ Mods and admins can remove and lock a post, creators can delete it.
data: {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>,
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<i32>,
edit_id: i32,
creator_id: i32,
post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
read: Option<bool>,
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<String>,
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
}

View file

@ -97,14 +97,6 @@ impl Comment {
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(true))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
@ -116,6 +108,46 @@ impl Comment {
))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
comment_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
comment_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
comment_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]

View file

@ -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<Self, Error> {
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::<Self>(conn)
}
pub fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, Error> {
use crate::{community_view::CommunityModeratorView, user_view::UserView};
let mut mods_and_admins: Vec<i32> = 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)]

View file

@ -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<Self, Error> {
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::<Self>(conn)
}

View file

@ -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<i32>,
edit_id: Option<i32>, // 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<i32>, // 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<bool>,
deleted: Option<bool>,
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<String>,
read: Option<bool>,
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<CreateComment> {
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<CreateComment> {
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<CreateComment> {
// 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<EditComment> {
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<i32> = vec![orig_comment.creator_id];
let mut moderators: Vec<i32> = 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<EditComment> {
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<EditComment> {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
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<RemoveComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
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<MarkCommentAsRead> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
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<SaveComment> {
type Response = CommentResponse;
@ -512,8 +698,12 @@ impl Perform for Oper<CreateCommentLike> {
}
}
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<CreateCommentLike> {
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<Vec<i32>, 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<i32> {
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!(

View file

@ -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<TransferCommunity> {
})
}
}
pub fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, LemmyError> {
let mut editors: Vec<i32> = 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)
}

View file

@ -936,9 +936,11 @@ impl Perform for Oper<MarkAllAsRead> {
.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());
}

View file

@ -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 =

View file

@ -83,6 +83,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("", web::post().to(route_post::<CreateComment>))
.route("", web::put().to(route_post::<EditComment>))
.route("/delete", web::post().to(route_post::<DeleteComment>))
.route("/remove", web::post().to(route_post::<RemoveComment>))
.route(
"/mark_as_read",
web::post().to(route_post::<MarkCommentAsRead>),
)
.route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)),
)

View file

@ -28,6 +28,9 @@ pub enum UserOperation {
GetCommunity,
CreateComment,
EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment,
CreateCommentLike,
GetPosts,

View file

@ -506,6 +506,9 @@ impl ChatServer {
// Comment ops
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
UserOperation::DeleteComment => do_user_operation::<DeleteComment>(args).await,
UserOperation::RemoveComment => do_user_operation::<RemoveComment>(args).await,
UserOperation::MarkCommentAsRead => do_user_operation::<MarkCommentAsRead>(args).await,
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,

View file

@ -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',
},

View file

@ -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<CommentNodeProps, CommentNodeState> {
}
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<CommentNodeProps, CommentNodeState> {
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<CommentNodeProps, CommentNodeState> {
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<CommentNodeProps, CommentNodeState> {
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<CommentNodeProps, CommentNodeState> {
};
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;

View file

@ -409,7 +409,11 @@ export class Community extends Component<any, State> {
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);

View file

@ -484,9 +484,16 @@ export class Inbox extends Component<any, InboxState> {
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) {

View file

@ -701,7 +701,11 @@ export class Main extends Component<any, MainState> {
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);

View file

@ -8,7 +8,7 @@ import {
GetPostResponse,
PostResponse,
Comment,
CommentForm as CommentFormI,
MarkCommentAsReadForm,
CommentResponse,
CommentSortType,
CommentViewType,
@ -167,16 +167,12 @@ export class Post extends Component<any, PostState> {
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<any, PostState> {
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);

View file

@ -257,7 +257,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
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({

30
ui/src/interfaces.ts vendored
View file

@ -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

View file

@ -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) {