lemmy/server/src/api/comment.rs
nutomic d4dccd17ae implement ActivitySender actor (#89)
Merge pull request 'Adding unique ap_ids. Fixes #1100' (#90) from unique_ap_ids into activity-sender

Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/90

Adding back in on_conflict.

Trying to add back in the on_conflict_do_nothing.

Trying to reduce delay time.

Removing createFakes.

Removing some unit tests.

Adding comment jest timeout.

Fixing tests again.

Fixing tests again.

Merge branch 'activity-sender' into unique_ap_ids_2

Replace actix client with reqwest to speed up federation tests

Trying to fix tests again.

Fixing unit tests.

Fixing some broken unit tests, not done yet.

Adding uniques.

Adding unique ap_ids. Fixes #1100

use proper sql functionality for upsert

added logging

in fetcher, replace post/comment::create with upsert

no need to do an actual update in post/comment::upsert

Merge branch 'main' into activity-sender

implement upsert for user/community

reuse http client

got it working

attempt to use background-jobs crate

rewrite with proper error handling and less boilerplate

remove do_send, dont return errors from activity_sender

WIP: implement ActivitySender actor

Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/89
2020-08-31 13:48:02 +00:00

906 lines
24 KiB
Rust

use crate::{
api::{
check_community_ban,
get_post,
get_user_from_jwt,
get_user_from_jwt_opt,
is_mod_or_admin,
APIError,
Perform,
},
apub::{ApubLikeableType, ApubObjectType},
blocking,
websocket::{
server::{JoinCommunityRoom, SendComment},
UserOperation,
},
ConnectionId,
DbPool,
LemmyContext,
LemmyError,
};
use actix_web::web::Data;
use lemmy_db::{
comment::*,
comment_view::*,
moderator::*,
post::*,
site_view::*,
user::*,
user_mention::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
};
use lemmy_utils::{
make_apub_endpoint,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
EndpointType,
MentionData,
};
use log::error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
pub post_id: i32,
form_id: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct EditComment {
content: String,
edit_id: i32,
form_id: Option<String>,
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>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkCommentAsRead {
edit_id: i32,
read: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct SaveComment {
comment_id: i32,
save: bool,
auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CommentResponse {
pub comment: CommentView,
pub recipient_ids: Vec<i32>,
pub form_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentLike {
comment_id: i32,
score: i16,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetComments {
type_: String,
sort: String,
page: Option<i64>,
limit: Option<i64>,
pub community_id: Option<i32>,
auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct GetCommentsResponse {
comments: Vec<CommentView>,
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let comment_form = CommentForm {
content: content_slurs_removed,
parent_id: data.parent_id.to_owned(),
post_id: data.post_id,
creator_id: user.id,
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, context.pool()).await?;
check_community_ban(user.id, post.community_id, context.pool()).await?;
// Check if post is locked, no new comments
if post.locked {
return Err(APIError::err("locked").into());
}
// Create the comment
let comment_form2 = comment_form.clone();
let inserted_comment = match blocking(context.pool(), move |conn| {
Comment::create(&conn, &comment_form2)
})
.await?
{
Ok(comment) => comment,
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(context.pool(), move |conn| {
let apub_id =
make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
updated_comment.send_create(&user, context).await?;
// 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,
post,
context.pool(),
true,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: data.post_id,
user_id: user.id,
score: 1,
};
let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
}
updated_comment.send_like(&user, context).await?;
let user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(&conn, inserted_comment.id, Some(user_id))
})
.await??;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateComment,
comment: res.clone(),
websocket_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 EditComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
// 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 updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Send the apub update
updated_comment.send_update(&user, context).await?;
// Do the mentions / recipients
let post_id = orig_comment.post_id;
let post = get_post(post_id, context.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,
context.pool(),
false,
)
.await?;
let edit_id = data.edit_id;
let user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res.clone(),
websocket_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 DeleteComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
// 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(context.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, context).await?;
} else {
updated_comment.send_undo_delete(&user, context).await?;
}
// Refetch it
let edit_id = data.edit_id;
let user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = get_post(post_id, context.pool()).await?;
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
&user,
post,
context.pool(),
false,
)
.await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::DeleteComment,
comment: res.clone(),
websocket_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 RemoveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
// Verify that only a mod or admin can remove
is_mod_or_admin(context.pool(), user.id, orig_comment.community_id).await?;
// Do the remove
let removed = data.removed;
let updated_comment = match blocking(context.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(context.pool(), move |conn| {
ModRemoveComment::create(conn, &form)
})
.await??;
// Send the apub message
if removed {
updated_comment.send_remove(&user, context).await?;
} else {
updated_comment.send_undo_remove(&user, context).await?;
}
// Refetch it
let edit_id = data.edit_id;
let user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = get_post(post_id, context.pool()).await?;
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
&user,
post,
context.pool(),
false,
)
.await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::RemoveComment,
comment: res.clone(),
websocket_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 MarkCommentAsRead {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
// 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(context.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(context.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(context.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 user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommentResponse {
comment: comment_view,
recipient_ids: Vec::new(),
form_id: None,
};
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SaveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
user_id: user.id,
};
if data.save {
let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
if blocking(context.pool(), save_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
}
} else {
let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
if blocking(context.pool(), unsave_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
}
}
let comment_id = data.comment_id;
let user_id = user.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
Ok(CommentResponse {
comment: comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommentLike {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let mut recipient_ids = Vec::new();
// Don't do a downvote if site has downvotes disabled
if data.score == -1 {
let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into());
}
}
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
let post_id = orig_comment.post_id;
let post = get_post(post_id, context.pool()).await?;
check_community_ban(user.id, post.community_id, context.pool()).await?;
let comment_id = data.comment_id;
let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
// Add to recipient ids
match comment.parent_id {
Some(parent_id) => {
let parent_comment =
blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await??;
if parent_comment.creator_id != user.id {
let parent_user = blocking(context.pool(), move |conn| {
User_::read(conn, parent_comment.creator_id)
})
.await??;
recipient_ids.push(parent_user.id);
}
}
None => {
recipient_ids.push(post.creator_id);
}
}
let like_form = CommentLikeForm {
comment_id: data.comment_id,
post_id,
user_id: user.id,
score: data.score,
};
// Remove any likes first
let user_id = user.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)
})
.await??;
// Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
}
if like_form.score == 1 {
comment.send_like(&user, context).await?;
} else if like_form.score == -1 {
comment.send_dislike(&user, context).await?;
}
} else {
comment.send_undo_like(&user, context).await?;
}
// Have to refetch the comment to get the current state
let comment_id = data.comment_id;
let user_id = user.id;
let liked_comment = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
let mut res = CommentResponse {
comment: liked_comment,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res.clone(),
websocket_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 GetComments {
type Response = GetCommentsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = user.map(|u| u.id);
let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.listing_type(type_)
.sort(&sort)
.for_community_id(community_id)
.my_user_id(user_id)
.page(page)
.limit(limit)
.list()
})
.await?;
let comments = match comments {
Ok(comments) => comments,
Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
};
if let Some(id) = websocket_id {
// You don't need to join the specific community room, bc this is already handled by
// GetCommunity
if data.community_id.is_none() {
// 0 is the "all" community
context.chat_server().do_send(JoinCommunityRoom {
community_id: 0,
id,
});
}
}
Ok(GetCommentsResponse { comments })
}
}
pub async fn send_local_notifs(
mentions: Vec<MentionData>,
comment: Comment,
user: &User_,
post: Post,
pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<i32>, LemmyError> {
let user2 = user.clone();
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &user2, &post, do_send_email)
})
.await?;
Ok(ids)
}
fn do_send_local_notifs(
conn: &diesel::PgConnection,
mentions: &[MentionData],
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);
// Send the local mentions
for mention in mentions
.iter()
.filter(|m| m.is_local() && m.name.ne(&user.name))
.collect::<Vec<&MentionData>>()
{
if let Ok(mention_user) = User_::read_from_name(&conn, &mention.name) {
// TODO
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
recipient_ids.push(mention_user.id);
let user_mention_form = UserMentionForm {
recipient_id: mention_user.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
match UserMention::create(&conn, &user_mention_form) {
Ok(_mention) => (),
Err(_e) => error!("{}", &_e),
};
// Send an email to those users that have notifications on
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!(
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &mention_email, &mention_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
// Send notifs to the parent commenter / poster
match comment.parent_id {
Some(parent_id) => {
if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
if parent_comment.creator_id != user.id {
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
recipient_ids.push(parent_user.id);
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!(
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
}
}
// Its a post
None => {
if post.creator_id != user.id {
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
recipient_ids.push(parent_user.id);
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!(
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &post_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
}
};
recipient_ids
}