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::{
24 source::{comment::*, comment_report::*, moderator::*},
28 comment_report_view::{CommentReportQueryBuilder, CommentReportView},
29 comment_view::{CommentQueryBuilder, CommentView},
30 local_user_view::LocalUserView,
33 utils::{remove_slurs, scrape_text_for_mentions},
38 use lemmy_websocket::{
39 messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
43 use std::str::FromStr;
45 #[async_trait::async_trait(?Send)]
46 impl Perform for CreateComment {
47 type Response = CommentResponse;
51 context: &Data<LemmyContext>,
52 websocket_id: Option<ConnectionId>,
53 ) -> Result<CommentResponse, LemmyError> {
54 let data: &CreateComment = &self;
55 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
57 let content_slurs_removed = remove_slurs(&data.content.to_owned());
59 // Check for a community ban
60 let post_id = data.post_id;
61 let post = get_post(post_id, context.pool()).await?;
63 check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
65 // Check if post is locked, no new comments
67 return Err(ApiError::err("locked").into());
70 // If there's a parent_id, check to make sure that comment is in that post
71 if let Some(parent_id) = data.parent_id {
72 // Make sure the parent comment exists
74 match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
75 Ok(comment) => comment,
76 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
78 if parent.post_id != post_id {
79 return Err(ApiError::err("couldnt_create_comment").into());
83 let comment_form = CommentForm {
84 content: content_slurs_removed,
85 parent_id: data.parent_id.to_owned(),
86 post_id: data.post_id,
87 creator_id: local_user_view.person.id,
98 let comment_form2 = comment_form.clone();
99 let inserted_comment = match blocking(context.pool(), move |conn| {
100 Comment::create(&conn, &comment_form2)
104 Ok(comment) => comment,
105 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
108 // Necessary to update the ap_id
109 let inserted_comment_id = inserted_comment.id;
110 let updated_comment: Comment =
111 match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
113 generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
114 Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
118 Ok(comment) => comment,
119 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
123 .send_create(&local_user_view.person, context)
126 // Scan the comment for user mentions, add those rows
127 let post_id = post.id;
128 let mentions = scrape_text_for_mentions(&comment_form.content);
129 let recipient_ids = send_local_notifs(
131 updated_comment.clone(),
132 local_user_view.person.clone(),
139 // You like your own comment by default
140 let like_form = CommentLikeForm {
141 comment_id: inserted_comment.id,
143 person_id: local_user_view.person.id,
147 let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
148 if blocking(context.pool(), like).await?.is_err() {
149 return Err(ApiError::err("couldnt_like_comment").into());
153 .send_like(&local_user_view.person, context)
156 let person_id = local_user_view.person.id;
157 let mut comment_view = blocking(context.pool(), move |conn| {
158 CommentView::read(&conn, inserted_comment.id, Some(person_id))
162 // If its a comment to yourself, mark it as read
163 let comment_id = comment_view.comment.id;
164 if local_user_view.person.id == comment_view.get_recipient_id() {
165 match blocking(context.pool(), move |conn| {
166 Comment::update_read(conn, comment_id, true)
170 Ok(comment) => comment,
171 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
173 comment_view.comment.read = true;
176 let mut res = CommentResponse {
179 form_id: data.form_id.to_owned(),
182 context.chat_server().do_send(SendComment {
183 op: UserOperation::CreateComment,
184 comment: res.clone(),
188 res.recipient_ids = Vec::new(); // Necessary to avoid doubles
194 #[async_trait::async_trait(?Send)]
195 impl Perform for EditComment {
196 type Response = CommentResponse;
200 context: &Data<LemmyContext>,
201 websocket_id: Option<ConnectionId>,
202 ) -> Result<CommentResponse, LemmyError> {
203 let data: &EditComment = &self;
204 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
206 let comment_id = data.comment_id;
207 let orig_comment = blocking(context.pool(), move |conn| {
208 CommentView::read(&conn, comment_id, None)
213 local_user_view.person.id,
214 orig_comment.community.id,
219 // Verify that only the creator can edit
220 if local_user_view.person.id != orig_comment.creator.id {
221 return Err(ApiError::err("no_comment_edit_allowed").into());
225 let content_slurs_removed = remove_slurs(&data.content.to_owned());
226 let comment_id = data.comment_id;
227 let updated_comment = match blocking(context.pool(), move |conn| {
228 Comment::update_content(conn, comment_id, &content_slurs_removed)
232 Ok(comment) => comment,
233 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
236 // Send the apub update
238 .send_update(&local_user_view.person, context)
241 // Do the mentions / recipients
242 let updated_comment_content = updated_comment.content.to_owned();
243 let mentions = scrape_text_for_mentions(&updated_comment_content);
244 let recipient_ids = send_local_notifs(
247 local_user_view.person.clone(),
254 let comment_id = data.comment_id;
255 let person_id = local_user_view.person.id;
256 let comment_view = blocking(context.pool(), move |conn| {
257 CommentView::read(conn, comment_id, Some(person_id))
261 let res = CommentResponse {
264 form_id: data.form_id.to_owned(),
267 context.chat_server().do_send(SendComment {
268 op: UserOperation::EditComment,
269 comment: res.clone(),
277 #[async_trait::async_trait(?Send)]
278 impl Perform for DeleteComment {
279 type Response = CommentResponse;
283 context: &Data<LemmyContext>,
284 websocket_id: Option<ConnectionId>,
285 ) -> Result<CommentResponse, LemmyError> {
286 let data: &DeleteComment = &self;
287 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
289 let comment_id = data.comment_id;
290 let orig_comment = blocking(context.pool(), move |conn| {
291 CommentView::read(&conn, comment_id, None)
296 local_user_view.person.id,
297 orig_comment.community.id,
302 // Verify that only the creator can delete
303 if local_user_view.person.id != orig_comment.creator.id {
304 return Err(ApiError::err("no_comment_edit_allowed").into());
308 let deleted = data.deleted;
309 let updated_comment = match blocking(context.pool(), move |conn| {
310 Comment::update_deleted(conn, comment_id, deleted)
314 Ok(comment) => comment,
315 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
318 // Send the apub message
321 .send_delete(&local_user_view.person, context)
325 .send_undo_delete(&local_user_view.person, context)
330 let comment_id = data.comment_id;
331 let person_id = local_user_view.person.id;
332 let comment_view = blocking(context.pool(), move |conn| {
333 CommentView::read(conn, comment_id, Some(person_id))
337 // Build the recipients
338 let comment_view_2 = comment_view.clone();
339 let mentions = vec![];
340 let recipient_ids = send_local_notifs(
343 local_user_view.person.clone(),
350 let res = CommentResponse {
353 form_id: None, // TODO a comment delete might clear forms?
356 context.chat_server().do_send(SendComment {
357 op: UserOperation::DeleteComment,
358 comment: res.clone(),
366 #[async_trait::async_trait(?Send)]
367 impl Perform for RemoveComment {
368 type Response = CommentResponse;
372 context: &Data<LemmyContext>,
373 websocket_id: Option<ConnectionId>,
374 ) -> Result<CommentResponse, LemmyError> {
375 let data: &RemoveComment = &self;
376 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
378 let comment_id = data.comment_id;
379 let orig_comment = blocking(context.pool(), move |conn| {
380 CommentView::read(&conn, comment_id, None)
385 local_user_view.person.id,
386 orig_comment.community.id,
391 // Verify that only a mod or admin can remove
394 local_user_view.person.id,
395 orig_comment.community.id,
400 let removed = data.removed;
401 let updated_comment = match blocking(context.pool(), move |conn| {
402 Comment::update_removed(conn, comment_id, removed)
406 Ok(comment) => comment,
407 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
411 let form = ModRemoveCommentForm {
412 mod_person_id: local_user_view.person.id,
413 comment_id: data.comment_id,
414 removed: Some(removed),
415 reason: data.reason.to_owned(),
417 blocking(context.pool(), move |conn| {
418 ModRemoveComment::create(conn, &form)
422 // Send the apub message
425 .send_remove(&local_user_view.person, context)
429 .send_undo_remove(&local_user_view.person, context)
434 let comment_id = data.comment_id;
435 let person_id = local_user_view.person.id;
436 let comment_view = blocking(context.pool(), move |conn| {
437 CommentView::read(conn, comment_id, Some(person_id))
441 // Build the recipients
442 let comment_view_2 = comment_view.clone();
444 let mentions = vec![];
445 let recipient_ids = send_local_notifs(
448 local_user_view.person.clone(),
455 let res = CommentResponse {
458 form_id: None, // TODO maybe this might clear other forms
461 context.chat_server().do_send(SendComment {
462 op: UserOperation::RemoveComment,
463 comment: res.clone(),
471 #[async_trait::async_trait(?Send)]
472 impl Perform for MarkCommentAsRead {
473 type Response = CommentResponse;
477 context: &Data<LemmyContext>,
478 _websocket_id: Option<ConnectionId>,
479 ) -> Result<CommentResponse, LemmyError> {
480 let data: &MarkCommentAsRead = &self;
481 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
483 let comment_id = data.comment_id;
484 let orig_comment = blocking(context.pool(), move |conn| {
485 CommentView::read(&conn, comment_id, None)
490 local_user_view.person.id,
491 orig_comment.community.id,
496 // Verify that only the recipient can mark as read
497 if local_user_view.person.id != orig_comment.get_recipient_id() {
498 return Err(ApiError::err("no_comment_edit_allowed").into());
501 // Do the mark as read
502 let read = data.read;
503 match blocking(context.pool(), move |conn| {
504 Comment::update_read(conn, comment_id, read)
508 Ok(comment) => comment,
509 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
513 let comment_id = data.comment_id;
514 let person_id = local_user_view.person.id;
515 let comment_view = blocking(context.pool(), move |conn| {
516 CommentView::read(conn, comment_id, Some(person_id))
520 let res = CommentResponse {
522 recipient_ids: Vec::new(),
530 #[async_trait::async_trait(?Send)]
531 impl Perform for SaveComment {
532 type Response = CommentResponse;
536 context: &Data<LemmyContext>,
537 _websocket_id: Option<ConnectionId>,
538 ) -> Result<CommentResponse, LemmyError> {
539 let data: &SaveComment = &self;
540 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
542 let comment_saved_form = CommentSavedForm {
543 comment_id: data.comment_id,
544 person_id: local_user_view.person.id,
548 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
549 if blocking(context.pool(), save_comment).await?.is_err() {
550 return Err(ApiError::err("couldnt_save_comment").into());
553 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
554 if blocking(context.pool(), unsave_comment).await?.is_err() {
555 return Err(ApiError::err("couldnt_save_comment").into());
559 let comment_id = data.comment_id;
560 let person_id = local_user_view.person.id;
561 let comment_view = blocking(context.pool(), move |conn| {
562 CommentView::read(conn, comment_id, Some(person_id))
568 recipient_ids: Vec::new(),
574 #[async_trait::async_trait(?Send)]
575 impl Perform for CreateCommentLike {
576 type Response = CommentResponse;
580 context: &Data<LemmyContext>,
581 websocket_id: Option<ConnectionId>,
582 ) -> Result<CommentResponse, LemmyError> {
583 let data: &CreateCommentLike = &self;
584 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
586 let mut recipient_ids = Vec::<LocalUserId>::new();
588 // Don't do a downvote if site has downvotes disabled
589 check_downvotes_enabled(data.score, context.pool()).await?;
591 let comment_id = data.comment_id;
592 let orig_comment = blocking(context.pool(), move |conn| {
593 CommentView::read(&conn, comment_id, None)
598 local_user_view.person.id,
599 orig_comment.community.id,
604 // Add parent user to recipients
605 let recipient_id = orig_comment.get_recipient_id();
606 if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
607 LocalUserView::read_person(conn, recipient_id)
611 recipient_ids.push(local_recipient.local_user.id);
614 let like_form = CommentLikeForm {
615 comment_id: data.comment_id,
616 post_id: orig_comment.post.id,
617 person_id: local_user_view.person.id,
621 // Remove any likes first
622 let person_id = local_user_view.person.id;
623 blocking(context.pool(), move |conn| {
624 CommentLike::remove(conn, person_id, comment_id)
628 // Only add the like if the score isnt 0
629 let comment = orig_comment.comment;
630 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
632 let like_form2 = like_form.clone();
633 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
634 if blocking(context.pool(), like).await?.is_err() {
635 return Err(ApiError::err("couldnt_like_comment").into());
638 if like_form.score == 1 {
639 comment.send_like(&local_user_view.person, context).await?;
640 } else if like_form.score == -1 {
642 .send_dislike(&local_user_view.person, context)
647 .send_undo_like(&local_user_view.person, context)
651 // Have to refetch the comment to get the current state
652 let comment_id = data.comment_id;
653 let person_id = local_user_view.person.id;
654 let liked_comment = blocking(context.pool(), move |conn| {
655 CommentView::read(conn, comment_id, Some(person_id))
659 let res = CommentResponse {
660 comment_view: liked_comment,
665 context.chat_server().do_send(SendComment {
666 op: UserOperation::CreateCommentLike,
667 comment: res.clone(),
675 #[async_trait::async_trait(?Send)]
676 impl Perform for GetComments {
677 type Response = GetCommentsResponse;
681 context: &Data<LemmyContext>,
682 _websocket_id: Option<ConnectionId>,
683 ) -> Result<GetCommentsResponse, LemmyError> {
684 let data: &GetComments = &self;
685 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
686 let person_id = local_user_view.map(|u| u.person.id);
688 let type_ = ListingType::from_str(&data.type_)?;
689 let sort = SortType::from_str(&data.sort)?;
691 let community_id = data.community_id;
692 let community_name = data.community_name.to_owned();
693 let page = data.page;
694 let limit = data.limit;
695 let comments = blocking(context.pool(), move |conn| {
696 CommentQueryBuilder::create(conn)
699 .community_id(community_id)
700 .community_name(community_name)
701 .my_person_id(person_id)
707 let comments = match comments {
708 Ok(comments) => comments,
709 Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
712 Ok(GetCommentsResponse { comments })
716 /// Creates a comment report and notifies the moderators of the community
717 #[async_trait::async_trait(?Send)]
718 impl Perform for CreateCommentReport {
719 type Response = CreateCommentReportResponse;
723 context: &Data<LemmyContext>,
724 websocket_id: Option<ConnectionId>,
725 ) -> Result<CreateCommentReportResponse, LemmyError> {
726 let data: &CreateCommentReport = &self;
727 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
729 // check size of report and check for whitespace
730 let reason = data.reason.trim();
731 if reason.is_empty() {
732 return Err(ApiError::err("report_reason_required").into());
734 if reason.chars().count() > 1000 {
735 return Err(ApiError::err("report_too_long").into());
738 let person_id = local_user_view.person.id;
739 let comment_id = data.comment_id;
740 let comment_view = blocking(context.pool(), move |conn| {
741 CommentView::read(&conn, comment_id, None)
745 check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
747 let report_form = CommentReportForm {
748 creator_id: person_id,
750 original_comment_text: comment_view.comment.content,
751 reason: data.reason.to_owned(),
754 let report = match blocking(context.pool(), move |conn| {
755 CommentReport::report(conn, &report_form)
759 Ok(report) => report,
760 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
763 let res = CreateCommentReportResponse { success: true };
765 context.chat_server().do_send(SendUserRoomMessage {
766 op: UserOperation::CreateCommentReport,
767 response: res.clone(),
768 local_recipient_id: local_user_view.local_user.id,
772 context.chat_server().do_send(SendModRoomMessage {
773 op: UserOperation::CreateCommentReport,
775 community_id: comment_view.community.id,
783 /// Resolves or unresolves a comment report and notifies the moderators of the community
784 #[async_trait::async_trait(?Send)]
785 impl Perform for ResolveCommentReport {
786 type Response = ResolveCommentReportResponse;
790 context: &Data<LemmyContext>,
791 websocket_id: Option<ConnectionId>,
792 ) -> Result<ResolveCommentReportResponse, LemmyError> {
793 let data: &ResolveCommentReport = &self;
794 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
796 let report_id = data.report_id;
797 let report = blocking(context.pool(), move |conn| {
798 CommentReportView::read(&conn, report_id)
802 let person_id = local_user_view.person.id;
803 is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
805 let resolved = data.resolved;
806 let resolve_fun = move |conn: &'_ _| {
808 CommentReport::resolve(conn, report_id, person_id)
810 CommentReport::unresolve(conn, report_id, person_id)
814 if blocking(context.pool(), resolve_fun).await?.is_err() {
815 return Err(ApiError::err("couldnt_resolve_report").into());
818 let report_id = data.report_id;
819 let res = ResolveCommentReportResponse {
824 context.chat_server().do_send(SendModRoomMessage {
825 op: UserOperation::ResolveCommentReport,
826 response: res.clone(),
827 community_id: report.community.id,
835 /// Lists comment reports for a community if an id is supplied
836 /// or returns all comment reports for communities a user moderates
837 #[async_trait::async_trait(?Send)]
838 impl Perform for ListCommentReports {
839 type Response = ListCommentReportsResponse;
843 context: &Data<LemmyContext>,
844 websocket_id: Option<ConnectionId>,
845 ) -> Result<ListCommentReportsResponse, LemmyError> {
846 let data: &ListCommentReports = &self;
847 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
849 let person_id = local_user_view.person.id;
850 let community_id = data.community;
852 collect_moderated_communities(person_id, community_id, context.pool()).await?;
854 let page = data.page;
855 let limit = data.limit;
856 let comments = blocking(context.pool(), move |conn| {
857 CommentReportQueryBuilder::create(conn)
858 .community_ids(community_ids)
865 let res = ListCommentReportsResponse { comments };
867 context.chat_server().do_send(SendUserRoomMessage {
868 op: UserOperation::ListCommentReports,
869 response: res.clone(),
870 local_recipient_id: local_user_view.local_user.id,