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,
88 ..CommentForm::default()
92 let comment_form2 = comment_form.clone();
93 let inserted_comment = match blocking(context.pool(), move |conn| {
94 Comment::create(&conn, &comment_form2)
98 Ok(comment) => comment,
99 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
102 // Necessary to update the ap_id
103 let inserted_comment_id = inserted_comment.id;
104 let updated_comment: Comment =
105 match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
107 generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
108 Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
112 Ok(comment) => comment,
113 Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
117 .send_create(&local_user_view.person, context)
120 // Scan the comment for user mentions, add those rows
121 let post_id = post.id;
122 let mentions = scrape_text_for_mentions(&comment_form.content);
123 let recipient_ids = send_local_notifs(
125 updated_comment.clone(),
126 local_user_view.person.clone(),
133 // You like your own comment by default
134 let like_form = CommentLikeForm {
135 comment_id: inserted_comment.id,
137 person_id: local_user_view.person.id,
141 let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
142 if blocking(context.pool(), like).await?.is_err() {
143 return Err(ApiError::err("couldnt_like_comment").into());
147 .send_like(&local_user_view.person, context)
150 let person_id = local_user_view.person.id;
151 let mut comment_view = blocking(context.pool(), move |conn| {
152 CommentView::read(&conn, inserted_comment.id, Some(person_id))
156 // If its a comment to yourself, mark it as read
157 let comment_id = comment_view.comment.id;
158 if local_user_view.person.id == comment_view.get_recipient_id() {
159 match blocking(context.pool(), move |conn| {
160 Comment::update_read(conn, comment_id, true)
164 Ok(comment) => comment,
165 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
167 comment_view.comment.read = true;
170 let mut res = CommentResponse {
173 form_id: data.form_id.to_owned(),
176 context.chat_server().do_send(SendComment {
177 op: UserOperation::CreateComment,
178 comment: res.clone(),
182 res.recipient_ids = Vec::new(); // Necessary to avoid doubles
188 #[async_trait::async_trait(?Send)]
189 impl Perform for EditComment {
190 type Response = CommentResponse;
194 context: &Data<LemmyContext>,
195 websocket_id: Option<ConnectionId>,
196 ) -> Result<CommentResponse, LemmyError> {
197 let data: &EditComment = &self;
198 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
200 let comment_id = data.comment_id;
201 let orig_comment = blocking(context.pool(), move |conn| {
202 CommentView::read(&conn, comment_id, None)
207 local_user_view.person.id,
208 orig_comment.community.id,
213 // Verify that only the creator can edit
214 if local_user_view.person.id != orig_comment.creator.id {
215 return Err(ApiError::err("no_comment_edit_allowed").into());
219 let content_slurs_removed = remove_slurs(&data.content.to_owned());
220 let comment_id = data.comment_id;
221 let updated_comment = match blocking(context.pool(), move |conn| {
222 Comment::update_content(conn, comment_id, &content_slurs_removed)
226 Ok(comment) => comment,
227 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
230 // Send the apub update
232 .send_update(&local_user_view.person, context)
235 // Do the mentions / recipients
236 let updated_comment_content = updated_comment.content.to_owned();
237 let mentions = scrape_text_for_mentions(&updated_comment_content);
238 let recipient_ids = send_local_notifs(
241 local_user_view.person.clone(),
248 let comment_id = data.comment_id;
249 let person_id = local_user_view.person.id;
250 let comment_view = blocking(context.pool(), move |conn| {
251 CommentView::read(conn, comment_id, Some(person_id))
255 let res = CommentResponse {
258 form_id: data.form_id.to_owned(),
261 context.chat_server().do_send(SendComment {
262 op: UserOperation::EditComment,
263 comment: res.clone(),
271 #[async_trait::async_trait(?Send)]
272 impl Perform for DeleteComment {
273 type Response = CommentResponse;
277 context: &Data<LemmyContext>,
278 websocket_id: Option<ConnectionId>,
279 ) -> Result<CommentResponse, LemmyError> {
280 let data: &DeleteComment = &self;
281 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
283 let comment_id = data.comment_id;
284 let orig_comment = blocking(context.pool(), move |conn| {
285 CommentView::read(&conn, comment_id, None)
290 local_user_view.person.id,
291 orig_comment.community.id,
296 // Verify that only the creator can delete
297 if local_user_view.person.id != orig_comment.creator.id {
298 return Err(ApiError::err("no_comment_edit_allowed").into());
302 let deleted = data.deleted;
303 let updated_comment = match blocking(context.pool(), move |conn| {
304 Comment::update_deleted(conn, comment_id, deleted)
308 Ok(comment) => comment,
309 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
312 // Send the apub message
315 .send_delete(&local_user_view.person, context)
319 .send_undo_delete(&local_user_view.person, context)
324 let comment_id = data.comment_id;
325 let person_id = local_user_view.person.id;
326 let comment_view = blocking(context.pool(), move |conn| {
327 CommentView::read(conn, comment_id, Some(person_id))
331 // Build the recipients
332 let comment_view_2 = comment_view.clone();
333 let mentions = vec![];
334 let recipient_ids = send_local_notifs(
337 local_user_view.person.clone(),
344 let res = CommentResponse {
347 form_id: None, // TODO a comment delete might clear forms?
350 context.chat_server().do_send(SendComment {
351 op: UserOperation::DeleteComment,
352 comment: res.clone(),
360 #[async_trait::async_trait(?Send)]
361 impl Perform for RemoveComment {
362 type Response = CommentResponse;
366 context: &Data<LemmyContext>,
367 websocket_id: Option<ConnectionId>,
368 ) -> Result<CommentResponse, LemmyError> {
369 let data: &RemoveComment = &self;
370 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
372 let comment_id = data.comment_id;
373 let orig_comment = blocking(context.pool(), move |conn| {
374 CommentView::read(&conn, comment_id, None)
379 local_user_view.person.id,
380 orig_comment.community.id,
385 // Verify that only a mod or admin can remove
388 local_user_view.person.id,
389 orig_comment.community.id,
394 let removed = data.removed;
395 let updated_comment = match blocking(context.pool(), move |conn| {
396 Comment::update_removed(conn, comment_id, removed)
400 Ok(comment) => comment,
401 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
405 let form = ModRemoveCommentForm {
406 mod_person_id: local_user_view.person.id,
407 comment_id: data.comment_id,
408 removed: Some(removed),
409 reason: data.reason.to_owned(),
411 blocking(context.pool(), move |conn| {
412 ModRemoveComment::create(conn, &form)
416 // Send the apub message
419 .send_remove(&local_user_view.person, context)
423 .send_undo_remove(&local_user_view.person, context)
428 let comment_id = data.comment_id;
429 let person_id = local_user_view.person.id;
430 let comment_view = blocking(context.pool(), move |conn| {
431 CommentView::read(conn, comment_id, Some(person_id))
435 // Build the recipients
436 let comment_view_2 = comment_view.clone();
438 let mentions = vec![];
439 let recipient_ids = send_local_notifs(
442 local_user_view.person.clone(),
449 let res = CommentResponse {
452 form_id: None, // TODO maybe this might clear other forms
455 context.chat_server().do_send(SendComment {
456 op: UserOperation::RemoveComment,
457 comment: res.clone(),
465 #[async_trait::async_trait(?Send)]
466 impl Perform for MarkCommentAsRead {
467 type Response = CommentResponse;
471 context: &Data<LemmyContext>,
472 _websocket_id: Option<ConnectionId>,
473 ) -> Result<CommentResponse, LemmyError> {
474 let data: &MarkCommentAsRead = &self;
475 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
477 let comment_id = data.comment_id;
478 let orig_comment = blocking(context.pool(), move |conn| {
479 CommentView::read(&conn, comment_id, None)
484 local_user_view.person.id,
485 orig_comment.community.id,
490 // Verify that only the recipient can mark as read
491 if local_user_view.person.id != orig_comment.get_recipient_id() {
492 return Err(ApiError::err("no_comment_edit_allowed").into());
495 // Do the mark as read
496 let read = data.read;
497 match blocking(context.pool(), move |conn| {
498 Comment::update_read(conn, comment_id, read)
502 Ok(comment) => comment,
503 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
507 let comment_id = data.comment_id;
508 let person_id = local_user_view.person.id;
509 let comment_view = blocking(context.pool(), move |conn| {
510 CommentView::read(conn, comment_id, Some(person_id))
514 let res = CommentResponse {
516 recipient_ids: Vec::new(),
524 #[async_trait::async_trait(?Send)]
525 impl Perform for SaveComment {
526 type Response = CommentResponse;
530 context: &Data<LemmyContext>,
531 _websocket_id: Option<ConnectionId>,
532 ) -> Result<CommentResponse, LemmyError> {
533 let data: &SaveComment = &self;
534 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
536 let comment_saved_form = CommentSavedForm {
537 comment_id: data.comment_id,
538 person_id: local_user_view.person.id,
542 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
543 if blocking(context.pool(), save_comment).await?.is_err() {
544 return Err(ApiError::err("couldnt_save_comment").into());
547 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
548 if blocking(context.pool(), unsave_comment).await?.is_err() {
549 return Err(ApiError::err("couldnt_save_comment").into());
553 let comment_id = data.comment_id;
554 let person_id = local_user_view.person.id;
555 let comment_view = blocking(context.pool(), move |conn| {
556 CommentView::read(conn, comment_id, Some(person_id))
562 recipient_ids: Vec::new(),
568 #[async_trait::async_trait(?Send)]
569 impl Perform for CreateCommentLike {
570 type Response = CommentResponse;
574 context: &Data<LemmyContext>,
575 websocket_id: Option<ConnectionId>,
576 ) -> Result<CommentResponse, LemmyError> {
577 let data: &CreateCommentLike = &self;
578 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
580 let mut recipient_ids = Vec::<LocalUserId>::new();
582 // Don't do a downvote if site has downvotes disabled
583 check_downvotes_enabled(data.score, context.pool()).await?;
585 let comment_id = data.comment_id;
586 let orig_comment = blocking(context.pool(), move |conn| {
587 CommentView::read(&conn, comment_id, None)
592 local_user_view.person.id,
593 orig_comment.community.id,
598 // Add parent user to recipients
599 let recipient_id = orig_comment.get_recipient_id();
600 if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
601 LocalUserView::read_person(conn, recipient_id)
605 recipient_ids.push(local_recipient.local_user.id);
608 let like_form = CommentLikeForm {
609 comment_id: data.comment_id,
610 post_id: orig_comment.post.id,
611 person_id: local_user_view.person.id,
615 // Remove any likes first
616 let person_id = local_user_view.person.id;
617 blocking(context.pool(), move |conn| {
618 CommentLike::remove(conn, person_id, comment_id)
622 // Only add the like if the score isnt 0
623 let comment = orig_comment.comment;
624 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
626 let like_form2 = like_form.clone();
627 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
628 if blocking(context.pool(), like).await?.is_err() {
629 return Err(ApiError::err("couldnt_like_comment").into());
632 if like_form.score == 1 {
633 comment.send_like(&local_user_view.person, context).await?;
634 } else if like_form.score == -1 {
636 .send_dislike(&local_user_view.person, context)
641 .send_undo_like(&local_user_view.person, context)
645 // Have to refetch the comment to get the current state
646 let comment_id = data.comment_id;
647 let person_id = local_user_view.person.id;
648 let liked_comment = blocking(context.pool(), move |conn| {
649 CommentView::read(conn, comment_id, Some(person_id))
653 let res = CommentResponse {
654 comment_view: liked_comment,
659 context.chat_server().do_send(SendComment {
660 op: UserOperation::CreateCommentLike,
661 comment: res.clone(),
669 #[async_trait::async_trait(?Send)]
670 impl Perform for GetComments {
671 type Response = GetCommentsResponse;
675 context: &Data<LemmyContext>,
676 _websocket_id: Option<ConnectionId>,
677 ) -> Result<GetCommentsResponse, LemmyError> {
678 let data: &GetComments = &self;
679 let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
680 let person_id = local_user_view.map(|u| u.person.id);
682 let type_ = ListingType::from_str(&data.type_)?;
683 let sort = SortType::from_str(&data.sort)?;
685 let community_id = data.community_id;
686 let community_name = data.community_name.to_owned();
687 let page = data.page;
688 let limit = data.limit;
689 let comments = blocking(context.pool(), move |conn| {
690 CommentQueryBuilder::create(conn)
693 .community_id(community_id)
694 .community_name(community_name)
695 .my_person_id(person_id)
701 let comments = match comments {
702 Ok(comments) => comments,
703 Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
706 Ok(GetCommentsResponse { comments })
710 /// Creates a comment report and notifies the moderators of the community
711 #[async_trait::async_trait(?Send)]
712 impl Perform for CreateCommentReport {
713 type Response = CreateCommentReportResponse;
717 context: &Data<LemmyContext>,
718 websocket_id: Option<ConnectionId>,
719 ) -> Result<CreateCommentReportResponse, LemmyError> {
720 let data: &CreateCommentReport = &self;
721 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
723 // check size of report and check for whitespace
724 let reason = data.reason.trim();
725 if reason.is_empty() {
726 return Err(ApiError::err("report_reason_required").into());
728 if reason.chars().count() > 1000 {
729 return Err(ApiError::err("report_too_long").into());
732 let person_id = local_user_view.person.id;
733 let comment_id = data.comment_id;
734 let comment_view = blocking(context.pool(), move |conn| {
735 CommentView::read(&conn, comment_id, None)
739 check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
741 let report_form = CommentReportForm {
742 creator_id: person_id,
744 original_comment_text: comment_view.comment.content,
745 reason: data.reason.to_owned(),
748 let report = match blocking(context.pool(), move |conn| {
749 CommentReport::report(conn, &report_form)
753 Ok(report) => report,
754 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
757 let res = CreateCommentReportResponse { success: true };
759 context.chat_server().do_send(SendUserRoomMessage {
760 op: UserOperation::CreateCommentReport,
761 response: res.clone(),
762 local_recipient_id: local_user_view.local_user.id,
766 context.chat_server().do_send(SendModRoomMessage {
767 op: UserOperation::CreateCommentReport,
769 community_id: comment_view.community.id,
777 /// Resolves or unresolves a comment report and notifies the moderators of the community
778 #[async_trait::async_trait(?Send)]
779 impl Perform for ResolveCommentReport {
780 type Response = ResolveCommentReportResponse;
784 context: &Data<LemmyContext>,
785 websocket_id: Option<ConnectionId>,
786 ) -> Result<ResolveCommentReportResponse, LemmyError> {
787 let data: &ResolveCommentReport = &self;
788 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
790 let report_id = data.report_id;
791 let report = blocking(context.pool(), move |conn| {
792 CommentReportView::read(&conn, report_id)
796 let person_id = local_user_view.person.id;
797 is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
799 let resolved = data.resolved;
800 let resolve_fun = move |conn: &'_ _| {
802 CommentReport::resolve(conn, report_id, person_id)
804 CommentReport::unresolve(conn, report_id, person_id)
808 if blocking(context.pool(), resolve_fun).await?.is_err() {
809 return Err(ApiError::err("couldnt_resolve_report").into());
812 let report_id = data.report_id;
813 let res = ResolveCommentReportResponse {
818 context.chat_server().do_send(SendModRoomMessage {
819 op: UserOperation::ResolveCommentReport,
820 response: res.clone(),
821 community_id: report.community.id,
829 /// Lists comment reports for a community if an id is supplied
830 /// or returns all comment reports for communities a user moderates
831 #[async_trait::async_trait(?Send)]
832 impl Perform for ListCommentReports {
833 type Response = ListCommentReportsResponse;
837 context: &Data<LemmyContext>,
838 websocket_id: Option<ConnectionId>,
839 ) -> Result<ListCommentReportsResponse, LemmyError> {
840 let data: &ListCommentReports = &self;
841 let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
843 let person_id = local_user_view.person.id;
844 let community_id = data.community;
846 collect_moderated_communities(person_id, community_id, context.pool()).await?;
848 let page = data.page;
849 let limit = data.limit;
850 let comments = blocking(context.pool(), move |conn| {
851 CommentReportQueryBuilder::create(conn)
852 .community_ids(community_ids)
859 let res = ListCommentReportsResponse { comments };
861 context.chat_server().do_send(SendUserRoomMessage {
862 op: UserOperation::ListCommentReports,
863 response: res.clone(),
864 local_recipient_id: local_user_view.local_user.id,