3 collect_moderated_communities,
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
15 comment_report::{CommentReport, CommentReportForm},
21 comment_report_view::{CommentReportQueryBuilder, CommentReportView},
22 comment_view::{CommentQueryBuilder, CommentView},
32 use lemmy_structs::{blocking, comment::*, send_local_notifs};
34 apub::{make_apub_endpoint, EndpointType},
35 utils::{remove_slurs, scrape_text_for_mentions},
40 use lemmy_websocket::{
41 messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
45 use std::str::FromStr;
47 #[async_trait::async_trait(?Send)]
48 impl Perform for CreateComment {
49 type Response = CommentResponse;
53 context: &Data<LemmyContext>,
54 websocket_id: Option<ConnectionId>,
55 ) -> Result<CommentResponse, LemmyError> {
56 let data: &CreateComment = &self;
57 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
59 let content_slurs_removed = remove_slurs(&data.content.to_owned());
61 let comment_form = CommentForm {
62 content: content_slurs_removed,
63 parent_id: data.parent_id.to_owned(),
64 post_id: data.post_id,
75 // Check for a community ban
76 let post_id = data.post_id;
77 let post = get_post(post_id, context.pool()).await?;
79 check_community_ban(user.id, post.community_id, context.pool()).await?;
81 // Check if post is locked, no new comments
83 return Err(APIError::err("locked").into());
87 let comment_form2 = comment_form.clone();
88 let inserted_comment = match blocking(context.pool(), move |conn| {
89 Comment::create(&conn, &comment_form2)
93 Ok(comment) => comment,
94 Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
97 // Necessary to update the ap_id
98 let inserted_comment_id = inserted_comment.id;
99 let updated_comment: Comment = match blocking(context.pool(), move |conn| {
101 make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
102 Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
106 Ok(comment) => comment,
107 Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
110 updated_comment.send_create(&user, context).await?;
112 // Scan the comment for user mentions, add those rows
113 let mentions = scrape_text_for_mentions(&comment_form.content);
114 let recipient_ids = send_local_notifs(
116 updated_comment.clone(),
124 // You like your own comment by default
125 let like_form = CommentLikeForm {
126 comment_id: inserted_comment.id,
127 post_id: data.post_id,
132 let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
133 if blocking(context.pool(), like).await?.is_err() {
134 return Err(APIError::err("couldnt_like_comment").into());
137 updated_comment.send_like(&user, context).await?;
139 let user_id = user.id;
140 let comment_view = blocking(context.pool(), move |conn| {
141 CommentView::read(&conn, inserted_comment.id, Some(user_id))
145 let mut res = CommentResponse {
148 form_id: data.form_id.to_owned(),
151 context.chat_server().do_send(SendComment {
152 op: UserOperation::CreateComment,
153 comment: res.clone(),
157 // strip out the recipient_ids, so that
158 // users don't get double notifs
159 res.recipient_ids = Vec::new();
165 #[async_trait::async_trait(?Send)]
166 impl Perform for EditComment {
167 type Response = CommentResponse;
171 context: &Data<LemmyContext>,
172 websocket_id: Option<ConnectionId>,
173 ) -> Result<CommentResponse, LemmyError> {
174 let data: &EditComment = &self;
175 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
177 let edit_id = data.edit_id;
178 let orig_comment = blocking(context.pool(), move |conn| {
179 CommentView::read(&conn, edit_id, None)
183 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
185 // Verify that only the creator can edit
186 if user.id != orig_comment.creator.id {
187 return Err(APIError::err("no_comment_edit_allowed").into());
191 let content_slurs_removed = remove_slurs(&data.content.to_owned());
192 let edit_id = data.edit_id;
193 let updated_comment = match blocking(context.pool(), move |conn| {
194 Comment::update_content(conn, edit_id, &content_slurs_removed)
198 Ok(comment) => comment,
199 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
202 // Send the apub update
203 updated_comment.send_update(&user, context).await?;
205 // Do the mentions / recipients
206 let post_id = orig_comment.post.id;
207 let post = get_post(post_id, context.pool()).await?;
209 let updated_comment_content = updated_comment.content.to_owned();
210 let mentions = scrape_text_for_mentions(&updated_comment_content);
211 let recipient_ids = send_local_notifs(
221 let edit_id = data.edit_id;
222 let user_id = user.id;
223 let comment_view = blocking(context.pool(), move |conn| {
224 CommentView::read(conn, edit_id, Some(user_id))
228 let mut res = CommentResponse {
231 form_id: data.form_id.to_owned(),
234 context.chat_server().do_send(SendComment {
235 op: UserOperation::EditComment,
236 comment: res.clone(),
240 // strip out the recipient_ids, so that
241 // users don't get double notifs
242 res.recipient_ids = Vec::new();
248 #[async_trait::async_trait(?Send)]
249 impl Perform for DeleteComment {
250 type Response = CommentResponse;
254 context: &Data<LemmyContext>,
255 websocket_id: Option<ConnectionId>,
256 ) -> Result<CommentResponse, LemmyError> {
257 let data: &DeleteComment = &self;
258 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
260 let edit_id = data.edit_id;
261 let orig_comment = blocking(context.pool(), move |conn| {
262 CommentView::read(&conn, edit_id, None)
266 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
268 // Verify that only the creator can delete
269 if user.id != orig_comment.creator.id {
270 return Err(APIError::err("no_comment_edit_allowed").into());
274 let deleted = data.deleted;
275 let updated_comment = match blocking(context.pool(), move |conn| {
276 Comment::update_deleted(conn, edit_id, deleted)
280 Ok(comment) => comment,
281 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
284 // Send the apub message
286 updated_comment.send_delete(&user, context).await?;
288 updated_comment.send_undo_delete(&user, context).await?;
292 let edit_id = data.edit_id;
293 let user_id = user.id;
294 let comment_view = blocking(context.pool(), move |conn| {
295 CommentView::read(conn, edit_id, Some(user_id))
299 // Build the recipients
300 let post_id = comment_view.post.id;
301 let post = get_post(post_id, context.pool()).await?;
302 let mentions = vec![];
303 let recipient_ids = send_local_notifs(
313 let mut res = CommentResponse {
319 context.chat_server().do_send(SendComment {
320 op: UserOperation::DeleteComment,
321 comment: res.clone(),
325 // strip out the recipient_ids, so that
326 // users don't get double notifs
327 res.recipient_ids = Vec::new();
333 #[async_trait::async_trait(?Send)]
334 impl Perform for RemoveComment {
335 type Response = CommentResponse;
339 context: &Data<LemmyContext>,
340 websocket_id: Option<ConnectionId>,
341 ) -> Result<CommentResponse, LemmyError> {
342 let data: &RemoveComment = &self;
343 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
345 let edit_id = data.edit_id;
346 let orig_comment = blocking(context.pool(), move |conn| {
347 CommentView::read(&conn, edit_id, None)
351 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
353 // Verify that only a mod or admin can remove
354 is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
357 let removed = data.removed;
358 let updated_comment = match blocking(context.pool(), move |conn| {
359 Comment::update_removed(conn, edit_id, removed)
363 Ok(comment) => comment,
364 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
368 let form = ModRemoveCommentForm {
369 mod_user_id: user.id,
370 comment_id: data.edit_id,
371 removed: Some(removed),
372 reason: data.reason.to_owned(),
374 blocking(context.pool(), move |conn| {
375 ModRemoveComment::create(conn, &form)
379 // Send the apub message
381 updated_comment.send_remove(&user, context).await?;
383 updated_comment.send_undo_remove(&user, context).await?;
387 let edit_id = data.edit_id;
388 let user_id = user.id;
389 let comment_view = blocking(context.pool(), move |conn| {
390 CommentView::read(conn, edit_id, Some(user_id))
394 // Build the recipients
395 let post_id = comment_view.post.id;
396 let post = get_post(post_id, context.pool()).await?;
397 let mentions = vec![];
398 let recipient_ids = send_local_notifs(
408 let mut res = CommentResponse {
414 context.chat_server().do_send(SendComment {
415 op: UserOperation::RemoveComment,
416 comment: res.clone(),
420 // strip out the recipient_ids, so that
421 // users don't get double notifs
422 res.recipient_ids = Vec::new();
428 #[async_trait::async_trait(?Send)]
429 impl Perform for MarkCommentAsRead {
430 type Response = CommentResponse;
434 context: &Data<LemmyContext>,
435 _websocket_id: Option<ConnectionId>,
436 ) -> Result<CommentResponse, LemmyError> {
437 let data: &MarkCommentAsRead = &self;
438 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
440 let edit_id = data.edit_id;
441 let orig_comment = blocking(context.pool(), move |conn| {
442 CommentView::read(&conn, edit_id, None)
446 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
448 // Verify that only the recipient can mark as read
449 // Needs to fetch the parent comment / post to get the recipient
450 let parent_id = orig_comment.comment.parent_id;
453 let parent_comment = blocking(context.pool(), move |conn| {
454 CommentView::read(&conn, pid, None)
457 if user.id != parent_comment.creator.id {
458 return Err(APIError::err("no_comment_edit_allowed").into());
462 let parent_post_id = orig_comment.post.id;
464 blocking(context.pool(), move |conn| Post::read(conn, parent_post_id)).await??;
465 if user.id != parent_post.creator_id {
466 return Err(APIError::err("no_comment_edit_allowed").into());
471 // Do the mark as read
472 let read = data.read;
473 match blocking(context.pool(), move |conn| {
474 Comment::update_read(conn, edit_id, read)
478 Ok(comment) => comment,
479 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
483 let edit_id = data.edit_id;
484 let user_id = user.id;
485 let comment_view = blocking(context.pool(), move |conn| {
486 CommentView::read(conn, edit_id, Some(user_id))
490 let res = CommentResponse {
492 recipient_ids: Vec::new(),
500 #[async_trait::async_trait(?Send)]
501 impl Perform for SaveComment {
502 type Response = CommentResponse;
506 context: &Data<LemmyContext>,
507 _websocket_id: Option<ConnectionId>,
508 ) -> Result<CommentResponse, LemmyError> {
509 let data: &SaveComment = &self;
510 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
512 let comment_saved_form = CommentSavedForm {
513 comment_id: data.comment_id,
518 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
519 if blocking(context.pool(), save_comment).await?.is_err() {
520 return Err(APIError::err("couldnt_save_comment").into());
523 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
524 if blocking(context.pool(), unsave_comment).await?.is_err() {
525 return Err(APIError::err("couldnt_save_comment").into());
529 let comment_id = data.comment_id;
530 let user_id = user.id;
531 let comment_view = blocking(context.pool(), move |conn| {
532 CommentView::read(conn, comment_id, Some(user_id))
538 recipient_ids: Vec::new(),
544 #[async_trait::async_trait(?Send)]
545 impl Perform for CreateCommentLike {
546 type Response = CommentResponse;
550 context: &Data<LemmyContext>,
551 websocket_id: Option<ConnectionId>,
552 ) -> Result<CommentResponse, LemmyError> {
553 let data: &CreateCommentLike = &self;
554 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
556 let mut recipient_ids = Vec::new();
558 // Don't do a downvote if site has downvotes disabled
559 if data.score == -1 {
560 let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
561 if !site_view.site.enable_downvotes {
562 return Err(APIError::err("downvotes_disabled").into());
566 let comment_id = data.comment_id;
567 let orig_comment = blocking(context.pool(), move |conn| {
568 CommentView::read(&conn, comment_id, None)
572 let post_id = orig_comment.post.id;
573 let post = get_post(post_id, context.pool()).await?;
574 check_community_ban(user.id, post.community_id, context.pool()).await?;
576 let comment_id = data.comment_id;
577 let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
579 // Add to recipient ids
580 match comment.parent_id {
583 blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await??;
584 if parent_comment.creator_id != user.id {
585 let parent_user = blocking(context.pool(), move |conn| {
586 User_::read(conn, parent_comment.creator_id)
589 recipient_ids.push(parent_user.id);
593 recipient_ids.push(post.creator_id);
597 let like_form = CommentLikeForm {
598 comment_id: data.comment_id,
604 // Remove any likes first
605 let user_id = user.id;
606 blocking(context.pool(), move |conn| {
607 CommentLike::remove(conn, user_id, comment_id)
611 // Only add the like if the score isnt 0
612 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
614 let like_form2 = like_form.clone();
615 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
616 if blocking(context.pool(), like).await?.is_err() {
617 return Err(APIError::err("couldnt_like_comment").into());
620 if like_form.score == 1 {
621 comment.send_like(&user, context).await?;
622 } else if like_form.score == -1 {
623 comment.send_dislike(&user, context).await?;
626 comment.send_undo_like(&user, context).await?;
629 // Have to refetch the comment to get the current state
630 let comment_id = data.comment_id;
631 let user_id = user.id;
632 let liked_comment = blocking(context.pool(), move |conn| {
633 CommentView::read(conn, comment_id, Some(user_id))
637 let mut res = CommentResponse {
638 comment_view: liked_comment,
643 context.chat_server().do_send(SendComment {
644 op: UserOperation::CreateCommentLike,
645 comment: res.clone(),
649 // strip out the recipient_ids, so that
650 // users don't get double notifs
651 res.recipient_ids = Vec::new();
657 #[async_trait::async_trait(?Send)]
658 impl Perform for GetComments {
659 type Response = GetCommentsResponse;
663 context: &Data<LemmyContext>,
664 _websocket_id: Option<ConnectionId>,
665 ) -> Result<GetCommentsResponse, LemmyError> {
666 let data: &GetComments = &self;
667 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
668 let user_id = user.map(|u| u.id);
670 let type_ = ListingType::from_str(&data.type_)?;
671 let sort = SortType::from_str(&data.sort)?;
673 let community_id = data.community_id;
674 let community_name = data.community_name.to_owned();
675 let page = data.page;
676 let limit = data.limit;
677 let comments = blocking(context.pool(), move |conn| {
678 CommentQueryBuilder::create(conn)
681 .community_id(community_id)
682 .community_name(community_name)
689 let comments = match comments {
690 Ok(comments) => comments,
691 Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
694 Ok(GetCommentsResponse { comments })
698 /// Creates a comment report and notifies the moderators of the community
699 #[async_trait::async_trait(?Send)]
700 impl Perform for CreateCommentReport {
701 type Response = CreateCommentReportResponse;
705 context: &Data<LemmyContext>,
706 websocket_id: Option<ConnectionId>,
707 ) -> Result<CreateCommentReportResponse, LemmyError> {
708 let data: &CreateCommentReport = &self;
709 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
711 // check size of report and check for whitespace
712 let reason = data.reason.trim();
713 if reason.is_empty() {
714 return Err(APIError::err("report_reason_required").into());
716 if reason.len() > 1000 {
717 return Err(APIError::err("report_too_long").into());
720 let user_id = user.id;
721 let comment_id = data.comment_id;
722 let comment_view = blocking(context.pool(), move |conn| {
723 CommentView::read(&conn, comment_id, None)
727 check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
729 let report_form = CommentReportForm {
732 original_comment_text: comment_view.comment.content,
733 reason: data.reason.to_owned(),
736 let report = match blocking(context.pool(), move |conn| {
737 CommentReport::report(conn, &report_form)
741 Ok(report) => report,
742 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
745 let res = CreateCommentReportResponse { success: true };
747 context.chat_server().do_send(SendUserRoomMessage {
748 op: UserOperation::CreateCommentReport,
749 response: res.clone(),
750 recipient_id: user.id,
754 context.chat_server().do_send(SendModRoomMessage {
755 op: UserOperation::CreateCommentReport,
757 community_id: comment_view.community.id,
765 /// Resolves or unresolves a comment report and notifies the moderators of the community
766 #[async_trait::async_trait(?Send)]
767 impl Perform for ResolveCommentReport {
768 type Response = ResolveCommentReportResponse;
772 context: &Data<LemmyContext>,
773 websocket_id: Option<ConnectionId>,
774 ) -> Result<ResolveCommentReportResponse, LemmyError> {
775 let data: &ResolveCommentReport = &self;
776 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
778 let report_id = data.report_id;
779 let report = blocking(context.pool(), move |conn| {
780 CommentReportView::read(&conn, report_id)
784 let user_id = user.id;
785 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
787 let resolved = data.resolved;
788 let resolve_fun = move |conn: &'_ _| {
790 CommentReport::resolve(conn, report_id, user_id)
792 CommentReport::unresolve(conn, report_id, user_id)
796 if blocking(context.pool(), resolve_fun).await?.is_err() {
797 return Err(APIError::err("couldnt_resolve_report").into());
800 let report_id = data.report_id;
801 let res = ResolveCommentReportResponse {
806 context.chat_server().do_send(SendModRoomMessage {
807 op: UserOperation::ResolveCommentReport,
808 response: res.clone(),
809 community_id: report.community.id,
817 /// Lists comment reports for a community if an id is supplied
818 /// or returns all comment reports for communities a user moderates
819 #[async_trait::async_trait(?Send)]
820 impl Perform for ListCommentReports {
821 type Response = ListCommentReportsResponse;
825 context: &Data<LemmyContext>,
826 websocket_id: Option<ConnectionId>,
827 ) -> Result<ListCommentReportsResponse, LemmyError> {
828 let data: &ListCommentReports = &self;
829 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
831 let user_id = user.id;
832 let community_id = data.community;
834 collect_moderated_communities(user_id, community_id, context.pool()).await?;
836 let page = data.page;
837 let limit = data.limit;
838 let comments = blocking(context.pool(), move |conn| {
839 CommentReportQueryBuilder::create(conn)
840 .community_ids(community_ids)
847 let res = ListCommentReportsResponse { comments };
849 context.chat_server().do_send(SendUserRoomMessage {
850 op: UserOperation::ListCommentReports,
851 response: res.clone(),
852 recipient_id: user.id,