3 check_downvotes_enabled,
4 collect_moderated_communities,
5 get_local_user_view_from_jwt,
6 get_local_user_view_from_jwt_opt,
11 use actix_web::web::Data;
12 use lemmy_api_structs::{blocking, comment::*, send_local_notifs};
13 use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
14 use lemmy_db_queries::{
15 source::comment::Comment_,
23 use lemmy_db_schema::source::{comment::*, comment_report::*, moderator::*};
25 comment_report_view::{CommentReportQueryBuilder, CommentReportView},
26 comment_view::{CommentQueryBuilder, CommentView},
29 utils::{remove_slurs, scrape_text_for_mentions},
34 use lemmy_websocket::{
35 messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
39 use std::str::FromStr;
41 #[async_trait::async_trait(?Send)]
42 impl Perform for CreateComment {
43 type Response = CommentResponse;
47 context: &Data<LemmyContext>,
48 websocket_id: Option<ConnectionId>,
49 ) -> Result<CommentResponse, LemmyError> {
50 let data: &CreateComment = &self;
51 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
53 let content_slurs_removed = remove_slurs(&data.content.to_owned());
55 // Check for a community ban
56 let post_id = data.post_id;
57 let post = get_post(post_id, context.pool()).await?;
59 check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
61 // Check if post is locked, no new comments
63 return Err(ApiError::err("locked").into());
66 // If there's a parent_id, check to make sure that comment is in that post
67 if let Some(parent_id) = data.parent_id {
68 // Make sure the parent comment exists
70 match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
71 Ok(comment) => comment,
72 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
74 if parent.post_id != post_id {
75 return Err(ApiError::err("couldnt_create_comment").into());
79 let comment_form = CommentForm {
80 content: content_slurs_removed,
81 parent_id: data.parent_id.to_owned(),
82 post_id: data.post_id,
83 creator_id: local_user_view.person.id,
94 let comment_form2 = comment_form.clone();
95 let inserted_comment = match blocking(context.pool(), move |conn| {
96 Comment::create(&conn, &comment_form2)
100 Ok(comment) => comment,
101 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
104 // Necessary to update the ap_id
105 let inserted_comment_id = inserted_comment.id;
106 let updated_comment: Comment =
107 match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
109 generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
110 Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
114 Ok(comment) => comment,
115 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
119 .send_create(&local_user_view.person, context)
122 // Scan the comment for user mentions, add those rows
123 let post_id = post.id;
124 let mentions = scrape_text_for_mentions(&comment_form.content);
125 let recipient_ids = send_local_notifs(
127 updated_comment.clone(),
128 local_user_view.person.clone(),
135 // You like your own comment by default
136 let like_form = CommentLikeForm {
137 comment_id: inserted_comment.id,
139 person_id: local_user_view.person.id,
143 let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
144 if blocking(context.pool(), like).await?.is_err() {
145 return Err(ApiError::err("couldnt_like_comment").into());
149 .send_like(&local_user_view.person, context)
152 let person_id = local_user_view.person.id;
153 let mut comment_view = blocking(context.pool(), move |conn| {
154 CommentView::read(&conn, inserted_comment.id, Some(person_id))
158 // If its a comment to yourself, mark it as read
159 let comment_id = comment_view.comment.id;
160 if local_user_view.person.id == comment_view.get_recipient_id() {
161 match blocking(context.pool(), move |conn| {
162 Comment::update_read(conn, comment_id, true)
166 Ok(comment) => comment,
167 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
169 comment_view.comment.read = true;
172 let mut res = CommentResponse {
175 form_id: data.form_id.to_owned(),
178 context.chat_server().do_send(SendComment {
179 op: UserOperation::CreateComment,
180 comment: res.clone(),
184 res.recipient_ids = Vec::new(); // Necessary to avoid doubles
190 #[async_trait::async_trait(?Send)]
191 impl Perform for EditComment {
192 type Response = CommentResponse;
196 context: &Data<LemmyContext>,
197 websocket_id: Option<ConnectionId>,
198 ) -> Result<CommentResponse, LemmyError> {
199 let data: &EditComment = &self;
200 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
202 let comment_id = data.comment_id;
203 let orig_comment = blocking(context.pool(), move |conn| {
204 CommentView::read(&conn, comment_id, None)
209 local_user_view.person.id,
210 orig_comment.community.id,
215 // Verify that only the creator can edit
216 if local_user_view.person.id != orig_comment.creator.id {
217 return Err(ApiError::err("no_comment_edit_allowed").into());
221 let content_slurs_removed = remove_slurs(&data.content.to_owned());
222 let comment_id = data.comment_id;
223 let updated_comment = match blocking(context.pool(), move |conn| {
224 Comment::update_content(conn, comment_id, &content_slurs_removed)
228 Ok(comment) => comment,
229 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
232 // Send the apub update
234 .send_update(&local_user_view.person, context)
237 // Do the mentions / recipients
238 let updated_comment_content = updated_comment.content.to_owned();
239 let mentions = scrape_text_for_mentions(&updated_comment_content);
240 let recipient_ids = send_local_notifs(
243 local_user_view.person.clone(),
250 let comment_id = data.comment_id;
251 let person_id = local_user_view.person.id;
252 let comment_view = blocking(context.pool(), move |conn| {
253 CommentView::read(conn, comment_id, Some(person_id))
257 let res = CommentResponse {
260 form_id: data.form_id.to_owned(),
263 context.chat_server().do_send(SendComment {
264 op: UserOperation::EditComment,
265 comment: res.clone(),
273 #[async_trait::async_trait(?Send)]
274 impl Perform for DeleteComment {
275 type Response = CommentResponse;
279 context: &Data<LemmyContext>,
280 websocket_id: Option<ConnectionId>,
281 ) -> Result<CommentResponse, LemmyError> {
282 let data: &DeleteComment = &self;
283 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
285 let comment_id = data.comment_id;
286 let orig_comment = blocking(context.pool(), move |conn| {
287 CommentView::read(&conn, comment_id, None)
292 local_user_view.person.id,
293 orig_comment.community.id,
298 // Verify that only the creator can delete
299 if local_user_view.person.id != orig_comment.creator.id {
300 return Err(ApiError::err("no_comment_edit_allowed").into());
304 let deleted = data.deleted;
305 let updated_comment = match blocking(context.pool(), move |conn| {
306 Comment::update_deleted(conn, comment_id, deleted)
310 Ok(comment) => comment,
311 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
314 // Send the apub message
317 .send_delete(&local_user_view.person, context)
321 .send_undo_delete(&local_user_view.person, context)
326 let comment_id = data.comment_id;
327 let person_id = local_user_view.person.id;
328 let comment_view = blocking(context.pool(), move |conn| {
329 CommentView::read(conn, comment_id, Some(person_id))
333 // Build the recipients
334 let comment_view_2 = comment_view.clone();
335 let mentions = vec![];
336 let recipient_ids = send_local_notifs(
339 local_user_view.person.clone(),
346 let res = CommentResponse {
349 form_id: None, // TODO a comment delete might clear forms?
352 context.chat_server().do_send(SendComment {
353 op: UserOperation::DeleteComment,
354 comment: res.clone(),
362 #[async_trait::async_trait(?Send)]
363 impl Perform for RemoveComment {
364 type Response = CommentResponse;
368 context: &Data<LemmyContext>,
369 websocket_id: Option<ConnectionId>,
370 ) -> Result<CommentResponse, LemmyError> {
371 let data: &RemoveComment = &self;
372 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
374 let comment_id = data.comment_id;
375 let orig_comment = blocking(context.pool(), move |conn| {
376 CommentView::read(&conn, comment_id, None)
381 local_user_view.person.id,
382 orig_comment.community.id,
387 // Verify that only a mod or admin can remove
390 local_user_view.person.id,
391 orig_comment.community.id,
396 let removed = data.removed;
397 let updated_comment = match blocking(context.pool(), move |conn| {
398 Comment::update_removed(conn, comment_id, removed)
402 Ok(comment) => comment,
403 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
407 let form = ModRemoveCommentForm {
408 mod_person_id: local_user_view.person.id,
409 comment_id: data.comment_id,
410 removed: Some(removed),
411 reason: data.reason.to_owned(),
413 blocking(context.pool(), move |conn| {
414 ModRemoveComment::create(conn, &form)
418 // Send the apub message
421 .send_remove(&local_user_view.person, context)
425 .send_undo_remove(&local_user_view.person, context)
430 let comment_id = data.comment_id;
431 let person_id = local_user_view.person.id;
432 let comment_view = blocking(context.pool(), move |conn| {
433 CommentView::read(conn, comment_id, Some(person_id))
437 // Build the recipients
438 let comment_view_2 = comment_view.clone();
440 let mentions = vec![];
441 let recipient_ids = send_local_notifs(
444 local_user_view.person.clone(),
451 let res = CommentResponse {
454 form_id: None, // TODO maybe this might clear other forms
457 context.chat_server().do_send(SendComment {
458 op: UserOperation::RemoveComment,
459 comment: res.clone(),
467 #[async_trait::async_trait(?Send)]
468 impl Perform for MarkCommentAsRead {
469 type Response = CommentResponse;
473 context: &Data<LemmyContext>,
474 _websocket_id: Option<ConnectionId>,
475 ) -> Result<CommentResponse, LemmyError> {
476 let data: &MarkCommentAsRead = &self;
477 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
479 let comment_id = data.comment_id;
480 let orig_comment = blocking(context.pool(), move |conn| {
481 CommentView::read(&conn, comment_id, None)
486 local_user_view.person.id,
487 orig_comment.community.id,
492 // Verify that only the recipient can mark as read
493 if local_user_view.person.id != orig_comment.get_recipient_id() {
494 return Err(ApiError::err("no_comment_edit_allowed").into());
497 // Do the mark as read
498 let read = data.read;
499 match blocking(context.pool(), move |conn| {
500 Comment::update_read(conn, comment_id, read)
504 Ok(comment) => comment,
505 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
509 let comment_id = data.comment_id;
510 let person_id = local_user_view.person.id;
511 let comment_view = blocking(context.pool(), move |conn| {
512 CommentView::read(conn, comment_id, Some(person_id))
516 let res = CommentResponse {
518 recipient_ids: Vec::new(),
526 #[async_trait::async_trait(?Send)]
527 impl Perform for SaveComment {
528 type Response = CommentResponse;
532 context: &Data<LemmyContext>,
533 _websocket_id: Option<ConnectionId>,
534 ) -> Result<CommentResponse, LemmyError> {
535 let data: &SaveComment = &self;
536 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
538 let comment_saved_form = CommentSavedForm {
539 comment_id: data.comment_id,
540 person_id: local_user_view.person.id,
544 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
545 if blocking(context.pool(), save_comment).await?.is_err() {
546 return Err(ApiError::err("couldnt_save_comment").into());
549 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
550 if blocking(context.pool(), unsave_comment).await?.is_err() {
551 return Err(ApiError::err("couldnt_save_comment").into());
555 let comment_id = data.comment_id;
556 let person_id = local_user_view.person.id;
557 let comment_view = blocking(context.pool(), move |conn| {
558 CommentView::read(conn, comment_id, Some(person_id))
564 recipient_ids: Vec::new(),
570 #[async_trait::async_trait(?Send)]
571 impl Perform for CreateCommentLike {
572 type Response = CommentResponse;
576 context: &Data<LemmyContext>,
577 websocket_id: Option<ConnectionId>,
578 ) -> Result<CommentResponse, LemmyError> {
579 let data: &CreateCommentLike = &self;
580 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
582 let mut recipient_ids = Vec::new();
584 // Don't do a downvote if site has downvotes disabled
585 check_downvotes_enabled(data.score, context.pool()).await?;
587 let comment_id = data.comment_id;
588 let orig_comment = blocking(context.pool(), move |conn| {
589 CommentView::read(&conn, comment_id, None)
594 local_user_view.person.id,
595 orig_comment.community.id,
600 // Add parent user to recipients
601 recipient_ids.push(orig_comment.get_recipient_id());
603 let like_form = CommentLikeForm {
604 comment_id: data.comment_id,
605 post_id: orig_comment.post.id,
606 person_id: local_user_view.person.id,
610 // Remove any likes first
611 let person_id = local_user_view.person.id;
612 blocking(context.pool(), move |conn| {
613 CommentLike::remove(conn, person_id, comment_id)
617 // Only add the like if the score isnt 0
618 let comment = orig_comment.comment;
619 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
621 let like_form2 = like_form.clone();
622 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
623 if blocking(context.pool(), like).await?.is_err() {
624 return Err(ApiError::err("couldnt_like_comment").into());
627 if like_form.score == 1 {
628 comment.send_like(&local_user_view.person, context).await?;
629 } else if like_form.score == -1 {
631 .send_dislike(&local_user_view.person, context)
636 .send_undo_like(&local_user_view.person, context)
640 // Have to refetch the comment to get the current state
641 let comment_id = data.comment_id;
642 let person_id = local_user_view.person.id;
643 let liked_comment = blocking(context.pool(), move |conn| {
644 CommentView::read(conn, comment_id, Some(person_id))
648 let res = CommentResponse {
649 comment_view: liked_comment,
654 context.chat_server().do_send(SendComment {
655 op: UserOperation::CreateCommentLike,
656 comment: res.clone(),
664 #[async_trait::async_trait(?Send)]
665 impl Perform for GetComments {
666 type Response = GetCommentsResponse;
670 context: &Data<LemmyContext>,
671 _websocket_id: Option<ConnectionId>,
672 ) -> Result<GetCommentsResponse, LemmyError> {
673 let data: &GetComments = &self;
674 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
675 let person_id = local_user_view.map(|u| u.person.id);
677 let type_ = ListingType::from_str(&data.type_)?;
678 let sort = SortType::from_str(&data.sort)?;
680 let community_id = data.community_id;
681 let community_name = data.community_name.to_owned();
682 let page = data.page;
683 let limit = data.limit;
684 let comments = blocking(context.pool(), move |conn| {
685 CommentQueryBuilder::create(conn)
688 .community_id(community_id)
689 .community_name(community_name)
690 .my_person_id(person_id)
696 let comments = match comments {
697 Ok(comments) => comments,
698 Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
701 Ok(GetCommentsResponse { comments })
705 /// Creates a comment report and notifies the moderators of the community
706 #[async_trait::async_trait(?Send)]
707 impl Perform for CreateCommentReport {
708 type Response = CreateCommentReportResponse;
712 context: &Data<LemmyContext>,
713 websocket_id: Option<ConnectionId>,
714 ) -> Result<CreateCommentReportResponse, LemmyError> {
715 let data: &CreateCommentReport = &self;
716 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
718 // check size of report and check for whitespace
719 let reason = data.reason.trim();
720 if reason.is_empty() {
721 return Err(ApiError::err("report_reason_required").into());
723 if reason.chars().count() > 1000 {
724 return Err(ApiError::err("report_too_long").into());
727 let person_id = local_user_view.person.id;
728 let comment_id = data.comment_id;
729 let comment_view = blocking(context.pool(), move |conn| {
730 CommentView::read(&conn, comment_id, None)
734 check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
736 let report_form = CommentReportForm {
737 creator_id: person_id,
739 original_comment_text: comment_view.comment.content,
740 reason: data.reason.to_owned(),
743 let report = match blocking(context.pool(), move |conn| {
744 CommentReport::report(conn, &report_form)
748 Ok(report) => report,
749 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
752 let res = CreateCommentReportResponse { success: true };
754 context.chat_server().do_send(SendUserRoomMessage {
755 op: UserOperation::CreateCommentReport,
756 response: res.clone(),
757 recipient_id: local_user_view.person.id,
761 context.chat_server().do_send(SendModRoomMessage {
762 op: UserOperation::CreateCommentReport,
764 community_id: comment_view.community.id,
772 /// Resolves or unresolves a comment report and notifies the moderators of the community
773 #[async_trait::async_trait(?Send)]
774 impl Perform for ResolveCommentReport {
775 type Response = ResolveCommentReportResponse;
779 context: &Data<LemmyContext>,
780 websocket_id: Option<ConnectionId>,
781 ) -> Result<ResolveCommentReportResponse, LemmyError> {
782 let data: &ResolveCommentReport = &self;
783 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
785 let report_id = data.report_id;
786 let report = blocking(context.pool(), move |conn| {
787 CommentReportView::read(&conn, report_id)
791 let person_id = local_user_view.person.id;
792 is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
794 let resolved = data.resolved;
795 let resolve_fun = move |conn: &'_ _| {
797 CommentReport::resolve(conn, report_id, person_id)
799 CommentReport::unresolve(conn, report_id, person_id)
803 if blocking(context.pool(), resolve_fun).await?.is_err() {
804 return Err(ApiError::err("couldnt_resolve_report").into());
807 let report_id = data.report_id;
808 let res = ResolveCommentReportResponse {
813 context.chat_server().do_send(SendModRoomMessage {
814 op: UserOperation::ResolveCommentReport,
815 response: res.clone(),
816 community_id: report.community.id,
824 /// Lists comment reports for a community if an id is supplied
825 /// or returns all comment reports for communities a user moderates
826 #[async_trait::async_trait(?Send)]
827 impl Perform for ListCommentReports {
828 type Response = ListCommentReportsResponse;
832 context: &Data<LemmyContext>,
833 websocket_id: Option<ConnectionId>,
834 ) -> Result<ListCommentReportsResponse, LemmyError> {
835 let data: &ListCommentReports = &self;
836 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
838 let person_id = local_user_view.person.id;
839 let community_id = data.community;
841 collect_moderated_communities(person_id, community_id, context.pool()).await?;
843 let page = data.page;
844 let limit = data.limit;
845 let comments = blocking(context.pool(), move |conn| {
846 CommentReportQueryBuilder::create(conn)
847 .community_ids(community_ids)
854 let res = ListCommentReportsResponse { comments };
856 context.chat_server().do_send(SendUserRoomMessage {
857 op: UserOperation::ListCommentReports,
858 response: res.clone(),
859 recipient_id: local_user_view.person.id,