diff --git a/crates/api/api/src/comment/distinguish.rs b/crates/api/api/src/comment/distinguish.rs index 35bcd66d1..186fa47e9 100644 --- a/crates/api/api/src/comment/distinguish.rs +++ b/crates/api/api/src/comment/distinguish.rs @@ -69,5 +69,8 @@ pub async fn distinguish_comment( ) .await?; - Ok(Json(CommentResponse { comment_view })) + Ok(Json(CommentResponse { + comment_view, + recipient_ids: Vec::new(), + })) } diff --git a/crates/api/api/src/comment/like.rs b/crates/api/api/src/comment/like.rs index acc9c01a0..404066abf 100644 --- a/crates/api/api/src/comment/like.rs +++ b/crates/api/api/src/comment/like.rs @@ -8,9 +8,10 @@ use lemmy_api_utils::{ utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, }; use lemmy_db_schema::{ - newtypes::PostOrCommentId, + newtypes::{LocalUserId, PostOrCommentId}, source::{ comment::{CommentActions, CommentLikeForm}, + comment_reply::CommentReply, person::PersonActions, }, traits::Likeable, @@ -34,6 +35,8 @@ pub async fn like_comment( let comment_id = data.comment_id; let my_person_id = local_user_view.person.id; + let mut recipient_ids = Vec::::new(); + check_local_vote_mode( data.score, PostOrCommentId::Comment(comment_id), @@ -60,6 +63,16 @@ pub async fn like_comment( ) .await?; + // Add parent poster or commenter to recipients + let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; + if let Ok(Some(reply)) = comment_reply { + let recipient_id = reply.recipient_id; + if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await + { + recipient_ids.push(local_recipient.local_user.id); + } + } + let mut like_form = CommentLikeForm::new(my_person_id, data.comment_id, data.score); // Remove any likes first @@ -106,6 +119,7 @@ pub async fn like_comment( context.deref(), comment_id, Some(local_user_view), + recipient_ids, local_instance_id, ) .await?, diff --git a/crates/api/api/src/comment/save.rs b/crates/api/api/src/comment/save.rs index 386961754..41782f85d 100644 --- a/crates/api/api/src/comment/save.rs +++ b/crates/api/api/src/comment/save.rs @@ -34,5 +34,8 @@ pub async fn save_comment( ) .await?; - Ok(Json(CommentResponse { comment_view })) + Ok(Json(CommentResponse { + comment_view, + recipient_ids: Vec::new(), + })) } diff --git a/crates/api/api/src/post/mod.rs b/crates/api/api/src/post/mod.rs index 93f7feae7..97410f097 100644 --- a/crates/api/api/src/post/mod.rs +++ b/crates/api/api/src/post/mod.rs @@ -7,4 +7,3 @@ pub mod lock; pub mod mark_many_read; pub mod mark_read; pub mod save; -pub mod update_notifications; diff --git a/crates/api/api/src/post/update_notifications.rs b/crates/api/api/src/post/update_notifications.rs deleted file mode 100644 index a4eca80d2..000000000 --- a/crates/api/api/src/post/update_notifications.rs +++ /dev/null @@ -1,23 +0,0 @@ -use activitypub_federation::config::Data; -use actix_web::web::Json; -use lemmy_api_utils::context::LemmyContext; -use lemmy_db_schema::source::post::PostActions; -use lemmy_db_views_local_user::LocalUserView; -use lemmy_db_views_post::api::UpdatePostNotifications; -use lemmy_db_views_site::api::SuccessResponse; -use lemmy_utils::error::LemmyResult; - -pub async fn update_post_notifications( - data: Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - PostActions::update_notification_state( - data.post_id, - local_user_view.person.id, - data.new_state, - &mut context.pool(), - ) - .await?; - Ok(Json(SuccessResponse::default())) -} diff --git a/crates/api/api_crud/src/comment/create.rs b/crates/api/api_crud/src/comment/create.rs index cc17ee1c3..270243ec2 100644 --- a/crates/api/api_crud/src/comment/create.rs +++ b/crates/api/api_crud/src/comment/create.rs @@ -19,6 +19,7 @@ use lemmy_api_utils::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentActions, CommentInsertForm, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, @@ -32,7 +33,7 @@ use lemmy_db_views_post::PostView; use lemmy_db_views_site::SiteView; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, - utils::validation::is_valid_body_field, + utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, }; pub async fn create_comment( @@ -112,14 +113,19 @@ pub async fn create_comment( Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref()).await?; plugin_hook_after("after_create_local_comment", &inserted_comment)?; + let inserted_comment_id = inserted_comment.id; + + // Scan the comment for user mentions, add those rows + let mentions = scrape_text_for_mentions(&content); let do_send_email = !local_site.disable_email_notifications; - send_local_notifs( - &post, - Some(&inserted_comment), + let recipient_ids = send_local_notifs( + mentions, + PostOrCommentId::Comment(inserted_comment_id), &local_user_view.person, - &post_view.community, do_send_email, &context, + Some(&local_user_view), + local_instance_id, ) .await?; @@ -179,6 +185,7 @@ pub async fn create_comment( &context, inserted_comment.id, Some(local_user_view), + recipient_ids, local_instance_id, ) .await?, diff --git a/crates/api/api_crud/src/comment/delete.rs b/crates/api/api_crud/src/comment/delete.rs index 28b3f0a6b..9362ce768 100644 --- a/crates/api/api_crud/src/comment/delete.rs +++ b/crates/api/api_crud/src/comment/delete.rs @@ -1,12 +1,13 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_utils::{ - build_response::build_comment_response, + build_response::{build_comment_response, send_local_notifs}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, utils::check_community_user_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::comment::{Comment, CommentUpdateForm}, traits::Crud, }; @@ -61,6 +62,16 @@ pub async fn delete_comment( ) .await?; + let recipient_ids = send_local_notifs( + vec![], + PostOrCommentId::Comment(comment_id), + &local_user_view.person, + false, + &context, + Some(&local_user_view), + local_instance_id, + ) + .await?; let updated_comment_id = updated_comment.id; ActivityChannel::submit_activity( @@ -77,6 +88,7 @@ pub async fn delete_comment( &context, updated_comment_id, Some(local_user_view), + recipient_ids, local_instance_id, ) .await?, diff --git a/crates/api/api_crud/src/comment/read.rs b/crates/api/api_crud/src/comment/read.rs index 706e89318..d4da80665 100644 --- a/crates/api/api_crud/src/comment/read.rs +++ b/crates/api/api_crud/src/comment/read.rs @@ -21,6 +21,13 @@ pub async fn get_comment( check_private_instance(&local_user_view, &local_site)?; Ok(Json( - build_comment_response(&context, data.id, local_user_view, local_instance_id).await?, + build_comment_response( + &context, + data.id, + local_user_view, + vec![], + local_instance_id, + ) + .await?, )) } diff --git a/crates/api/api_crud/src/comment/remove.rs b/crates/api/api_crud/src/comment/remove.rs index 2b840b072..5fa627074 100644 --- a/crates/api/api_crud/src/comment/remove.rs +++ b/crates/api/api_crud/src/comment/remove.rs @@ -1,12 +1,13 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_utils::{ - build_response::build_comment_response, + build_response::{build_comment_response, send_local_notifs}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, utils::check_community_mod_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, @@ -83,6 +84,16 @@ pub async fn remove_comment( }; ModRemoveComment::create(&mut context.pool(), &form).await?; + let recipient_ids = send_local_notifs( + vec![], + PostOrCommentId::Comment(comment_id), + &local_user_view.person, + false, + &context, + Some(&local_user_view), + local_instance_id, + ) + .await?; let updated_comment_id = updated_comment.id; ActivityChannel::submit_activity( @@ -100,6 +111,7 @@ pub async fn remove_comment( &context, updated_comment_id, Some(local_user_view), + recipient_ids, local_instance_id, ) .await?, diff --git a/crates/api/api_crud/src/comment/update.rs b/crates/api/api_crud/src/comment/update.rs index 0aca0d52a..ef7efef1f 100644 --- a/crates/api/api_crud/src/comment/update.rs +++ b/crates/api/api_crud/src/comment/update.rs @@ -10,6 +10,7 @@ use lemmy_api_utils::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::comment::{Comment, CommentUpdateForm}, traits::Crud, }; @@ -20,7 +21,7 @@ use lemmy_db_views_comment::{ use lemmy_db_views_local_user::LocalUserView; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, - utils::validation::is_valid_body_field, + utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, }; pub async fn update_comment( @@ -78,13 +79,16 @@ pub async fn update_comment( plugin_hook_after("after_update_local_comment", &updated_comment)?; // Do the mentions / recipients - send_local_notifs( - &orig_comment.post, - Some(&updated_comment), + let updated_comment_content = updated_comment.content.clone(); + let mentions = scrape_text_for_mentions(&updated_comment_content); + let recipient_ids = send_local_notifs( + mentions, + PostOrCommentId::Comment(comment_id), &local_user_view.person, - &orig_comment.community, false, &context, + Some(&local_user_view), + local_instance_id, ) .await?; @@ -98,6 +102,7 @@ pub async fn update_comment( &context, updated_comment.id, Some(local_user_view), + recipient_ids, local_instance_id, ) .await?, diff --git a/crates/api/api_crud/src/post/create.rs b/crates/api/api_crud/src/post/create.rs index 7306f8b14..0bad98ba3 100644 --- a/crates/api/api_crud/src/post/create.rs +++ b/crates/api/api_crud/src/post/create.rs @@ -21,6 +21,7 @@ use lemmy_api_utils::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm}, traits::{Crud, Likeable, Readable}, utils::diesel_url_create, @@ -33,6 +34,7 @@ use lemmy_db_views_site::SiteView; use lemmy_utils::{ error::LemmyResult, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -167,18 +169,22 @@ pub async fn create_post( // They like their own post by default let person_id = local_user_view.person.id; let post_id = inserted_post.id; + let local_instance_id = local_user_view.person.instance_id; let like_form = PostLikeForm::new(post_id, person_id, 1); PostActions::like(&mut context.pool(), &like_form).await?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&inserted_post.body.clone().unwrap_or_default()); let do_send_email = !local_site.disable_email_notifications; send_local_notifs( - &inserted_post, - None, + mentions, + PostOrCommentId::Post(inserted_post.id), &local_user_view.person, - community, do_send_email, &context, + Some(&local_user_view), + local_instance_id, ) .await?; diff --git a/crates/api/api_crud/src/post/update.rs b/crates/api/api_crud/src/post/update.rs index 9b57d325d..ea5aa7a04 100644 --- a/crates/api/api_crud/src/post/update.rs +++ b/crates/api/api_crud/src/post/update.rs @@ -20,6 +20,7 @@ use lemmy_api_utils::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ community::Community, post::{Post, PostUpdateForm}, @@ -37,6 +38,7 @@ use lemmy_db_views_site::SiteView; use lemmy_utils::{ error::{LemmyErrorType, LemmyResult}, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -161,13 +163,16 @@ pub async fn update_post( let updated_post = Post::update(&mut context.pool(), post_id, &post_form).await?; plugin_hook_after("after_update_local_post", &post_form)?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default()); send_local_notifs( - &updated_post, - None, + mentions, + PostOrCommentId::Post(updated_post.id), &local_user_view.person, - &orig_post.community, false, &context, + Some(&local_user_view), + local_instance_id, ) .await?; diff --git a/crates/api/api_utils/src/build_response.rs b/crates/api/api_utils/src/build_response.rs index 062fae1fa..67ca60534 100644 --- a/crates/api/api_utils/src/build_response.rs +++ b/crates/api/api_utils/src/build_response.rs @@ -1,36 +1,38 @@ -use crate::{context::LemmyContext, utils::is_mod_or_admin}; +use crate::{ + context::LemmyContext, + utils::{check_person_instance_community_block, is_mod_or_admin}, +}; use actix_web::web::Json; use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, InstanceId, PersonId, PostId}, + newtypes::{CommentId, CommunityId, InstanceId, LocalUserId, PostId, PostOrCommentId}, source::{ actor_language::CommunityLanguage, comment::Comment, comment_reply::{CommentReply, CommentReplyInsertForm}, - community::{Community, CommunityActions}, - instance::InstanceActions, - person::{Person, PersonActions}, + community::Community, + person::Person, person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, person_post_mention::{PersonPostMention, PersonPostMentionInsertForm}, - post::{Post, PostActions}, + post::Post, }, - traits::{Blockable, Crud}, + traits::Crud, }; -use lemmy_db_schema_file::enums::PostNotifications; use lemmy_db_views_comment::{api::CommentResponse, CommentView}; use lemmy_db_views_community::{api::CommunityResponse, CommunityView}; use lemmy_db_views_local_user::LocalUserView; use lemmy_db_views_post::{api::PostResponse, PostView}; -use lemmy_email::notifications::{send_mention_email, send_reply_email}; -use lemmy_utils::{ - error::{LemmyErrorType, LemmyResult}, - utils::mention::scrape_text_for_mentions, +use lemmy_email::notifications::{ + send_comment_reply_email, + send_mention_email, + send_post_reply_email, }; -use url::Url; +use lemmy_utils::{error::LemmyResult, utils::mention::MentionData}; pub async fn build_comment_response( context: &LemmyContext, comment_id: CommentId, local_user_view: Option, + recipient_ids: Vec, local_instance_id: InstanceId, ) -> LemmyResult { let local_user = local_user_view.map(|l| l.local_user); @@ -41,7 +43,10 @@ pub async fn build_comment_response( local_instance_id, ) .await?; - Ok(CommentResponse { comment_view }) + Ok(CommentResponse { + comment_view, + recipient_ids, + }) } pub async fn build_community_response( @@ -89,223 +94,220 @@ pub async fn build_post_response( Ok(Json(PostResponse { post_view })) } -/// Scans the post/comment content for mentions, then sends notifications via db and email -/// to mentioned users and parent creator. +// TODO: this function is a mess and should be split up to handle email separately pub async fn send_local_notifs( - post: &Post, - comment_opt: Option<&Comment>, + mentions: Vec, + post_or_comment_id: PostOrCommentId, person: &Person, - community: &Community, do_send_email: bool, context: &LemmyContext, -) -> LemmyResult<()> { - let parent_creator = - notify_parent_creator(person, post, comment_opt, community, do_send_email, context).await?; + local_user_view: Option<&LocalUserView>, + local_instance_id: InstanceId, +) -> LemmyResult> { + let mut recipient_ids = Vec::new(); - send_local_mentions( - post, - comment_opt, - person, - parent_creator, - community, - do_send_email, - context, - ) - .await?; - - Ok(()) -} - -async fn notify_parent_creator( - person: &Person, - post: &Post, - comment_opt: Option<&Comment>, - community: &Community, - do_send_email: bool, - context: &LemmyContext, -) -> LemmyResult> { - let Some(comment) = comment_opt else { - return Ok(None); + let (comment_opt, post, community) = match post_or_comment_id { + PostOrCommentId::Post(post_id) => { + let post_view = PostView::read( + &mut context.pool(), + post_id, + local_user_view.map(|view| &view.local_user), + local_instance_id, + false, + ) + .await?; + (None, post_view.post, post_view.community) + } + PostOrCommentId::Comment(comment_id) => { + // When called from api code, we have local user view and can read with CommentView + // to reduce db queries. But when receiving a federated comment the user view is None, + // which means that comments inside private communities cant be read. As a workaround + // we need to read the items manually to bypass this check. + if let Some(local_user_view) = local_user_view { + // Read the comment view to get extra info + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + local_instance_id, + ) + .await?; + ( + Some(comment_view.comment), + comment_view.post, + comment_view.community, + ) + } else { + let comment = Comment::read(&mut context.pool(), comment_id).await?; + let post = Post::read(&mut context.pool(), comment.post_id).await?; + let community = Community::read(&mut context.pool(), post.community_id).await?; + (Some(comment), post, community) + } + } }; - // Get the parent data - let (parent_creator_id, parent_comment) = + // Send the local mentions + for mention in mentions + .iter() + .filter(|m| m.is_local(&context.settings().hostname) && m.name.ne(&person.name)) + { + let mention_name = mention.name.clone(); + let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; + if let Ok(mention_user_view) = user_view { + // TODO + // At some point, make it so you can't tag the parent creator either + // Potential duplication of notifications, one for reply and the other for mention, is handled + // below by checking recipient ids + recipient_ids.push(mention_user_view.local_user.id); + + // Make the correct reply form depending on whether its a post or comment mention + let (link, comment_content_or_post_body) = if let Some(comment) = &comment_opt { + let person_comment_mention_form = PersonCommentMentionInsertForm { + recipient_id: mention_user_view.person.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 + PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) + .await + .ok(); + ( + comment.local_url(context.settings())?, + comment.content.clone(), + ) + } else { + let person_post_mention_form = PersonPostMentionInsertForm { + recipient_id: mention_user_view.person.id, + post_id: post.id, + read: None, + }; + + // Allow this to fail softly, since edits might re-update or replace it + PersonPostMention::create(&mut context.pool(), &person_post_mention_form) + .await + .ok(); + ( + post.local_url(context.settings())?, + post.body.clone().unwrap_or_default(), + ) + }; + + // Send an email to those local users that have notifications on + if do_send_email { + send_mention_email( + &mention_user_view, + &comment_content_or_post_body, + person, + link.into(), + context.settings(), + ) + .await; + } + } + } + + // Send comment_reply to the parent commenter / poster + if let Some(comment) = &comment_opt { if let Some(parent_comment_id) = comment.parent_comment_id() { let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; - (parent_comment.creator_id, Some(parent_comment)) - } else { - (post.creator_id, None) - }; - // Dont send notification to yourself - if parent_creator_id == person.id { - return Ok(None); - } + // Get the parent commenter local_user + let parent_creator_id = parent_comment.creator_id; - let is_blocked = check_notifications_allowed( - parent_creator_id, - // Only block from the community's instance_id - community.instance_id, - post, - context, - ) - .await - .is_err(); - if is_blocked { - return Ok(None); - } - - let Ok(user_view) = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await - else { - return Ok(None); - }; - - let comment_reply_form = CommentReplyInsertForm { - recipient_id: user_view.person.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 - CommentReply::create(&mut context.pool(), &comment_reply_form) - .await - .ok(); - - if do_send_email { - send_reply_email( - &user_view, - comment, - person, - &parent_comment, - post, - context.settings(), - ) - .await?; - } - Ok(Some(user_view.person.id)) -} - -async fn send_local_mentions( - post: &Post, - comment_opt: Option<&Comment>, - person: &Person, - parent_creator_id: Option, - community: &Community, - do_send_email: bool, - context: &LemmyContext, -) -> LemmyResult<()> { - let content = if let Some(comment) = comment_opt { - &comment.content - } else { - &post.body.clone().unwrap_or_default() - }; - let mentions = scrape_text_for_mentions(content) - .into_iter() - .filter(|m| m.is_local(&context.settings().hostname) && m.name.ne(&person.name)); - for mention in mentions { - // Ignore error if user is remote - let Ok(user_view) = LocalUserView::read_from_name(&mut context.pool(), &mention.name).await - else { - continue; - }; - - // Dont send any mention notification to parent creator nor to yourself - if Some(user_view.person.id) == parent_creator_id || user_view.person.id == person.id { - continue; - } - - let is_blocked = check_notifications_allowed( - user_view.person.id, - // Only block from the community's instance_id - community.instance_id, - post, - context, - ) - .await - .is_err(); - if is_blocked { - continue; - }; - - let (link, comment_content_or_post_body) = - insert_post_or_comment_mention(&user_view, post, comment_opt, context).await?; - - // Send an email to those local users that have notifications on - if do_send_email { - send_mention_email( - &user_view, - &comment_content_or_post_body, - person, - link.into(), - context.settings(), + let check_blocks = check_person_instance_community_block( + person.id, + parent_creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), ) - .await; + .await + .is_err(); + + // Don't send a notif to yourself + if parent_comment.creator_id != person.id && !check_blocks { + let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; + if let Ok(parent_user_view) = user_view { + // Don't duplicate notif if already mentioned by checking recipient ids + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); + + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.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 + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); + + if do_send_email { + send_comment_reply_email( + &parent_user_view, + comment, + person, + &parent_comment, + &post, + context.settings(), + ) + .await?; + } + } + } + } + } else { + // Use the post creator to check blocks + let check_blocks = check_person_instance_community_block( + person.id, + post.creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), + ) + .await + .is_err(); + + if post.creator_id != person.id && !check_blocks { + let creator_id = post.creator_id; + let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; + if let Ok(parent_user_view) = parent_user { + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); + + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.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 + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); + + if do_send_email { + send_post_reply_email( + &parent_user_view, + comment, + person, + &post, + context.settings(), + ) + .await?; + } + } + } + } } } - Ok(()) -} - -/// Make the correct reply form depending on whether its a post or comment mention -async fn insert_post_or_comment_mention( - mention_user_view: &LocalUserView, - post: &Post, - comment_opt: Option<&Comment>, - context: &LemmyContext, -) -> LemmyResult<(Url, String)> { - if let Some(comment) = &comment_opt { - let person_comment_mention_form = PersonCommentMentionInsertForm { - recipient_id: mention_user_view.person.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 - PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) - .await - .ok(); - Ok(( - comment.local_url(context.settings())?, - comment.content.clone(), - )) - } else { - let person_post_mention_form = PersonPostMentionInsertForm { - recipient_id: mention_user_view.person.id, - post_id: post.id, - read: None, - }; - - // Allow this to fail softly, since edits might re-update or replace it - PersonPostMention::create(&mut context.pool(), &person_post_mention_form) - .await - .ok(); - Ok(( - post.local_url(context.settings())?, - post.body.clone().unwrap_or_default(), - )) - } -} - -pub async fn check_notifications_allowed( - potential_blocker_id: PersonId, - community_instance_id: InstanceId, - post: &Post, - context: &LemmyContext, -) -> LemmyResult<()> { - let pool = &mut context.pool(); - PersonActions::read_block(pool, potential_blocker_id, post.creator_id).await?; - InstanceActions::read_block(pool, potential_blocker_id, community_instance_id).await?; - CommunityActions::read_block(pool, potential_blocker_id, post.community_id).await?; - let post_notifications = PostActions::read(pool, post.id, potential_blocker_id) - .await - .ok() - .and_then(|a| a.notifications) - .unwrap_or_default(); - if post_notifications == PostNotifications::Mute { - // The specific error type is irrelevant - return Err(LemmyErrorType::NotFound.into()); - } - - Ok(()) + + Ok(recipient_ids) } diff --git a/crates/api/api_utils/src/utils.rs b/crates/api/api_utils/src/utils.rs index 9bc4ba54d..679d6f5d1 100644 --- a/crates/api/api_utils/src/utils.rs +++ b/crates/api/api_utils/src/utils.rs @@ -24,13 +24,13 @@ use lemmy_db_schema::{ ModRemovePostForm, }, oauth_account::OAuthAccount, - person::{Person, PersonUpdateForm}, + person::{Person, PersonActions, PersonUpdateForm}, post::{Post, PostActions, PostReadCommentsForm}, private_message::PrivateMessage, registration_application::RegistrationApplication, site::Site, }, - traits::{Crud, Likeable, ReadComments}, + traits::{Blockable, Crud, Likeable, ReadComments}, utils::DbPool, }; use lemmy_db_schema_file::enums::{FederationMode, RegistrationMode}; @@ -321,6 +321,19 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> { } } +pub async fn check_person_instance_community_block( + my_id: PersonId, + potential_blocker_id: PersonId, + community_instance_id: InstanceId, + community_id: CommunityId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + PersonActions::read_block(pool, potential_blocker_id, my_id).await?; + InstanceActions::read_block(pool, potential_blocker_id, community_instance_id).await?; + CommunityActions::read_block(pool, potential_blocker_id, community_id).await?; + Ok(()) +} + pub async fn check_local_vote_mode( score: i16, post_or_comment_id: PostOrCommentId, diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index d61341761..afb98e722 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -27,7 +27,7 @@ use lemmy_apub_objects::{ }, }; use lemmy_db_schema::{ - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, comment::{Comment, CommentActions, CommentLikeForm}, @@ -38,7 +38,10 @@ use lemmy_db_schema::{ traits::{Crud, Likeable}, }; use lemmy_db_views_site::SiteView; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + utils::mention::scrape_text_for_mentions, +}; use serde_json::{from_value, to_value}; use url::Url; @@ -134,12 +137,12 @@ impl ActivityHandler for CreateOrUpdateNote { // Need to do this check here instead of Note::from_json because we need the person who // send the activity, not the comment author. let existing_comment = self.object.id.dereference_local(context).await.ok(); - let (post, _) = self.object.get_parents(context).await?; if let (Some(distinguished), Some(existing_comment)) = (self.object.distinguished, existing_comment) { if distinguished != existing_comment.distinguished { let creator = self.actor.dereference(context).await?; + let (post, _) = self.object.get_parents(context).await?; check_is_mod_or_admin( &mut context.pool(), creator.id, @@ -169,14 +172,17 @@ impl ActivityHandler for CreateOrUpdateNote { // anyway. // TODO: for compatibility with other projects, it would be much better to read this from cc or // tags - let community = Community::read(&mut context.pool(), post.community_id).await?; + let mentions = scrape_text_for_mentions(&comment.content); + // TODO: this fails in local community comment as CommentView::read() returns nothing + // without passing LocalUser send_local_notifs( - &post.0, - Some(&comment.0), + mentions, + PostOrCommentId::Comment(comment.id), &actor, - &community, do_send_email, context, + None, + local_instance_id, ) .await?; Ok(()) diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index 27b22e3c0..b30c11d6f 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -21,7 +21,7 @@ use lemmy_apub_objects::{ }, }; use lemmy_db_schema::{ - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, community::Community, @@ -31,7 +31,10 @@ use lemmy_db_schema::{ traits::{Crud, Likeable}, }; use lemmy_db_views_site::SiteView; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + utils::mention::scrape_text_for_mentions, +}; use url::Url; impl CreateOrUpdatePage { @@ -107,6 +110,7 @@ impl ActivityHandler for CreateOrUpdatePage { async fn receive(self, context: &Data) -> LemmyResult<()> { let site_view = SiteView::read_local(&mut context.pool()).await?; + let local_instance_id = site_view.site.instance_id; let post = ApubPost::from_json(self.object, context).await?; @@ -121,8 +125,18 @@ impl ActivityHandler for CreateOrUpdatePage { self.kind == CreateOrUpdateType::Create && !site_view.local_site.disable_email_notifications; let actor = self.actor.dereference(context).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; - send_local_notifs(&post.0, None, &actor, &community, do_send_email, context).await?; + // Send the post body mentions + let mentions = scrape_text_for_mentions(&post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(post.id), + &actor, + do_send_email, + context, + None, + local_instance_id, + ) + .await?; Ok(()) } diff --git a/crates/db_schema/src/impls/post.rs b/crates/db_schema/src/impls/post.rs index 3a87673f6..d67ba0f5d 100644 --- a/crates/db_schema/src/impls/post.rs +++ b/crates/db_schema/src/impls/post.rs @@ -25,6 +25,7 @@ use crate::{ SITEMAP_LIMIT, }, }; +use ::url::Url; use chrono::{DateTime, Utc}; use diesel::{ dsl::{count, insert_into, not, update}, @@ -38,15 +39,11 @@ use diesel::{ QueryDsl, }; use diesel_async::RunQueryDsl; -use lemmy_db_schema_file::{ - enums::PostNotifications, - schema::{community, person, post, post_actions}, -}; +use lemmy_db_schema_file::schema::{community, person, post, post_actions}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, settings::structs::Settings, }; -use url::Url; impl Crud for Post { type InsertForm = PostInsertForm; @@ -542,7 +539,9 @@ impl PostActions { .map(|post_id| (PostReadForm::new(*post_id, person_id))) .collect::>() } +} +impl PostActions { pub async fn read( pool: &mut DbPool<'_>, post_id: PostId, @@ -568,29 +567,6 @@ impl PostActions { .ok_or(LemmyErrorType::CouldntParsePaginationToken)?; Self::read(pool, PostId(*post_id), PersonId(*person_id)).await } - - pub async fn update_notification_state( - post_id: PostId, - person_id: PersonId, - new_state: PostNotifications, - pool: &mut DbPool<'_>, - ) -> LemmyResult { - let conn = &mut get_conn(pool).await?; - let form = ( - post_actions::person_id.eq(person_id), - post_actions::post_id.eq(post_id), - post_actions::notifications.eq(new_state), - ); - - insert_into(post_actions::table) - .values(form.clone()) - .on_conflict((post_actions::person_id, post_actions::post_id)) - .do_update() - .set(form) - .get_result::(conn) - .await - .with_lemmy_type(LemmyErrorType::NotFound) - } } #[cfg(test)] diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index b16aaf668..d4bceb119 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -1,6 +1,5 @@ use crate::newtypes::{CommunityId, DbUrl, LanguageId, PersonId, PostId}; use chrono::{DateTime, Utc}; -use lemmy_db_schema_file::enums::PostNotifications; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -202,7 +201,6 @@ pub struct PostActions { pub like_score: Option, /// When the post was hidden. pub hidden_at: Option>, - pub notifications: Option, } #[derive(Clone, derive_new::new)] diff --git a/crates/db_schema_file/src/enums.rs b/crates/db_schema_file/src/enums.rs index 9c0a91917..af6e7b28c 100644 --- a/crates/db_schema_file/src/enums.rs +++ b/crates/db_schema_file/src/enums.rs @@ -226,22 +226,3 @@ pub enum VoteShow { ShowForOthers, Hide, } - -#[derive( - EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash, -)] -#[cfg_attr(feature = "full", derive(DbEnum))] -#[cfg_attr( - feature = "full", - ExistingTypePath = "crate::schema::sql_types::PostNotificationsModeEnum" -)] -#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] -#[cfg_attr(feature = "ts-rs", ts(export))] -/// Lets you show votes for others only, show all votes, or hide all votes. -pub enum PostNotifications { - #[default] - RepliesAndMentions, - AllComments, - Mute, -} diff --git a/crates/db_schema_file/src/schema.rs b/crates/db_schema_file/src/schema.rs index ff5ddc15b..bcae07f07 100644 --- a/crates/db_schema_file/src/schema.rs +++ b/crates/db_schema_file/src/schema.rs @@ -33,10 +33,6 @@ pub mod sql_types { #[diesel(postgres_type(name = "post_listing_mode_enum"))] pub struct PostListingModeEnum; - #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "post_notifications_mode_enum"))] - pub struct PostNotificationsModeEnum; - #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "post_sort_type_enum"))] pub struct PostSortTypeEnum; @@ -938,9 +934,6 @@ diesel::table! { } diesel::table! { - use diesel::sql_types::*; - use super::sql_types::PostNotificationsModeEnum; - post_actions (person_id, post_id) { post_id -> Int4, person_id -> Int4, @@ -951,7 +944,6 @@ diesel::table! { liked_at -> Nullable, like_score -> Nullable, hidden_at -> Nullable, - notifications -> Nullable, } } diff --git a/crates/db_views/comment/src/api.rs b/crates/db_views/comment/src/api.rs index 5ea1be8ba..3700df468 100644 --- a/crates/db_views/comment/src/api.rs +++ b/crates/db_views/comment/src/api.rs @@ -1,5 +1,12 @@ use crate::{CommentSlimView, CommentView}; -use lemmy_db_schema::newtypes::{CommentId, CommunityId, LanguageId, PaginationCursor, PostId}; +use lemmy_db_schema::newtypes::{ + CommentId, + CommunityId, + LanguageId, + LocalUserId, + PaginationCursor, + PostId, +}; use lemmy_db_schema_file::enums::{CommentSortType, ListingType}; use lemmy_db_views_vote::VoteView; use serde::{Deserialize, Serialize}; @@ -12,6 +19,7 @@ use serde_with::skip_serializing_none; /// A comment response. pub struct CommentResponse { pub comment_view: CommentView, + pub recipient_ids: Vec, } #[skip_serializing_none] diff --git a/crates/db_views/post/src/api.rs b/crates/db_views/post/src/api.rs index 9b21d75f6..d3b4bc1d8 100644 --- a/crates/db_views/post/src/api.rs +++ b/crates/db_views/post/src/api.rs @@ -12,7 +12,7 @@ use lemmy_db_schema::{ }, PostFeatureType, }; -use lemmy_db_schema_file::enums::{ListingType, PostNotifications, PostSortType}; +use lemmy_db_schema_file::enums::{ListingType, PostSortType}; use lemmy_db_views_community::CommunityView; use lemmy_db_views_vote::VoteView; use serde::{Deserialize, Serialize}; @@ -93,15 +93,6 @@ pub struct FeaturePost { pub feature_type: PostFeatureType, } -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] -#[cfg_attr(feature = "ts-rs", ts(optional_fields, export))] -/// Disable reply notifications for a post and all comments inside it -pub struct UpdatePostNotifications { - pub post_id: PostId, - pub new_state: PostNotifications, -} - #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] diff --git a/crates/email/src/notifications.rs b/crates/email/src/notifications.rs index 4d28aef9b..50bb30b0f 100644 --- a/crates/email/src/notifications.rs +++ b/crates/email/src/notifications.rs @@ -30,42 +30,57 @@ pub async fn send_mention_email( .await } -pub async fn send_reply_email( +pub async fn send_comment_reply_email( parent_user_view: &LocalUserView, comment: &Comment, person: &Person, - parent_comment: &Option, + parent_comment: &Comment, post: &Post, settings: &Settings, ) -> LemmyResult<()> { let inbox_link = inbox_link(settings); let lang = user_language(parent_user_view); let content = markdown_to_html(&comment.content); - let (subject, body) = if let Some(parent_comment) = parent_comment { - ( - lang.notification_comment_reply_subject(&person.name), - lang.notification_comment_reply_body( - comment.local_url(settings)?, - &content, - &inbox_link, - &parent_comment.content, - &post.name, - &person.name, - ), - ) - } else { - ( - lang.notification_post_reply_subject(&person.name), - lang.notification_post_reply_body( - comment.local_url(settings)?, - &content, - &inbox_link, - &post.name, - &person.name, - ), - ) - }; - send_email_to_user(parent_user_view, &subject, &body, settings).await; + send_email_to_user( + parent_user_view, + &lang.notification_comment_reply_subject(&person.name), + &lang.notification_comment_reply_body( + comment.local_url(settings)?, + &content, + &inbox_link, + &parent_comment.content, + &post.name, + &person.name, + ), + settings, + ) + .await; + Ok(()) +} + +pub async fn send_post_reply_email( + parent_user_view: &LocalUserView, + comment: &Comment, + person: &Person, + post: &Post, + settings: &Settings, +) -> LemmyResult<()> { + let inbox_link = inbox_link(settings); + let lang = user_language(parent_user_view); + let content = markdown_to_html(&comment.content); + send_email_to_user( + parent_user_view, + &lang.notification_post_reply_subject(&person.name), + &lang.notification_post_reply_body( + comment.local_url(settings)?, + &content, + &inbox_link, + &post.name, + &person.name, + ), + settings, + ) + .await; Ok(()) } diff --git a/migrations/2025-06-24-120137_disable-post-notifications/down.sql b/migrations/2025-06-24-120137_disable-post-notifications/down.sql deleted file mode 100644 index 190ad3419..000000000 --- a/migrations/2025-06-24-120137_disable-post-notifications/down.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE post_actions - DROP COLUMN notifications; - -DROP TYPE post_notifications_mode_enum; - diff --git a/migrations/2025-06-24-120137_disable-post-notifications/up.sql b/migrations/2025-06-24-120137_disable-post-notifications/up.sql deleted file mode 100644 index dc9d1978d..000000000 --- a/migrations/2025-06-24-120137_disable-post-notifications/up.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TYPE post_notifications_mode_enum AS enum ( - 'RepliesAndMentions', - 'AllComments', - 'Mute' -); - -ALTER TABLE post_actions - ADD COLUMN notifications post_notifications_mode_enum; - diff --git a/src/api_routes.rs b/src/api_routes.rs index 368796a74..a842b6e16 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -67,7 +67,6 @@ use lemmy_api::{ mark_many_read::mark_posts_as_read, mark_read::mark_post_as_read, save::save_post, - update_notifications::update_post_notifications, }, private_message::mark_read::mark_pm_as_read, reports::{ @@ -295,11 +294,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimit) { .route("/like/list", get().to(list_post_likes)) .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) - .route("/report/resolve", put().to(resolve_post_report)) - .route( - "/disable_notifications", - post().to(update_post_notifications), - ), + .route("/report/resolve", put().to(resolve_post_report)), ) // Comment .service(