3 check_downvotes_enabled,
4 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
13 use lemmy_db_queries::{
14 source::comment::Comment_,
22 use lemmy_db_schema::source::{comment::*, comment_report::*, moderator::*};
24 comment_report_view::{CommentReportQueryBuilder, CommentReportView},
25 comment_view::{CommentQueryBuilder, CommentView},
27 use lemmy_structs::{blocking, comment::*, send_local_notifs};
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 user = get_user_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(user.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,
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()),
118 updated_comment.send_create(&user, context).await?;
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(),
133 // You like your own comment by default
134 let like_form = CommentLikeForm {
135 comment_id: inserted_comment.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());
146 updated_comment.send_like(&user, context).await?;
148 let user_id = user.id;
149 let mut comment_view = blocking(context.pool(), move |conn| {
150 CommentView::read(&conn, inserted_comment.id, Some(user_id))
154 // If its a comment to yourself, mark it as read
155 let comment_id = comment_view.comment.id;
156 if user.id == comment_view.get_recipient_id() {
157 match blocking(context.pool(), move |conn| {
158 Comment::update_read(conn, comment_id, true)
162 Ok(comment) => comment,
163 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
165 comment_view.comment.read = true;
168 let mut res = CommentResponse {
171 form_id: data.form_id.to_owned(),
174 context.chat_server().do_send(SendComment {
175 op: UserOperation::CreateComment,
176 comment: res.clone(),
180 res.recipient_ids = Vec::new(); // Necessary to avoid doubles
186 #[async_trait::async_trait(?Send)]
187 impl Perform for EditComment {
188 type Response = CommentResponse;
192 context: &Data<LemmyContext>,
193 websocket_id: Option<ConnectionId>,
194 ) -> Result<CommentResponse, LemmyError> {
195 let data: &EditComment = &self;
196 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
198 let comment_id = data.comment_id;
199 let orig_comment = blocking(context.pool(), move |conn| {
200 CommentView::read(&conn, comment_id, None)
204 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
206 // Verify that only the creator can edit
207 if user.id != orig_comment.creator.id {
208 return Err(ApiError::err("no_comment_edit_allowed").into());
212 let content_slurs_removed = remove_slurs(&data.content.to_owned());
213 let comment_id = data.comment_id;
214 let updated_comment = match blocking(context.pool(), move |conn| {
215 Comment::update_content(conn, comment_id, &content_slurs_removed)
219 Ok(comment) => comment,
220 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
223 // Send the apub update
224 updated_comment.send_update(&user, context).await?;
226 // Do the mentions / recipients
227 let updated_comment_content = updated_comment.content.to_owned();
228 let mentions = scrape_text_for_mentions(&updated_comment_content);
229 let recipient_ids = send_local_notifs(
239 let comment_id = data.comment_id;
240 let user_id = user.id;
241 let comment_view = blocking(context.pool(), move |conn| {
242 CommentView::read(conn, comment_id, Some(user_id))
246 let res = CommentResponse {
249 form_id: data.form_id.to_owned(),
252 context.chat_server().do_send(SendComment {
253 op: UserOperation::EditComment,
254 comment: res.clone(),
262 #[async_trait::async_trait(?Send)]
263 impl Perform for DeleteComment {
264 type Response = CommentResponse;
268 context: &Data<LemmyContext>,
269 websocket_id: Option<ConnectionId>,
270 ) -> Result<CommentResponse, LemmyError> {
271 let data: &DeleteComment = &self;
272 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
274 let comment_id = data.comment_id;
275 let orig_comment = blocking(context.pool(), move |conn| {
276 CommentView::read(&conn, comment_id, None)
280 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
282 // Verify that only the creator can delete
283 if user.id != orig_comment.creator.id {
284 return Err(ApiError::err("no_comment_edit_allowed").into());
288 let deleted = data.deleted;
289 let updated_comment = match blocking(context.pool(), move |conn| {
290 Comment::update_deleted(conn, comment_id, deleted)
294 Ok(comment) => comment,
295 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
298 // Send the apub message
300 updated_comment.send_delete(&user, context).await?;
302 updated_comment.send_undo_delete(&user, context).await?;
306 let comment_id = data.comment_id;
307 let user_id = user.id;
308 let comment_view = blocking(context.pool(), move |conn| {
309 CommentView::read(conn, comment_id, Some(user_id))
313 // Build the recipients
314 let comment_view_2 = comment_view.clone();
315 let mentions = vec![];
316 let recipient_ids = send_local_notifs(
326 let res = CommentResponse {
329 form_id: None, // TODO a comment delete might clear forms?
332 context.chat_server().do_send(SendComment {
333 op: UserOperation::DeleteComment,
334 comment: res.clone(),
342 #[async_trait::async_trait(?Send)]
343 impl Perform for RemoveComment {
344 type Response = CommentResponse;
348 context: &Data<LemmyContext>,
349 websocket_id: Option<ConnectionId>,
350 ) -> Result<CommentResponse, LemmyError> {
351 let data: &RemoveComment = &self;
352 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
354 let comment_id = data.comment_id;
355 let orig_comment = blocking(context.pool(), move |conn| {
356 CommentView::read(&conn, comment_id, None)
360 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
362 // Verify that only a mod or admin can remove
363 is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
366 let removed = data.removed;
367 let updated_comment = match blocking(context.pool(), move |conn| {
368 Comment::update_removed(conn, comment_id, removed)
372 Ok(comment) => comment,
373 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
377 let form = ModRemoveCommentForm {
378 mod_person_id: user.id,
379 comment_id: data.comment_id,
380 removed: Some(removed),
381 reason: data.reason.to_owned(),
383 blocking(context.pool(), move |conn| {
384 ModRemoveComment::create(conn, &form)
388 // Send the apub message
390 updated_comment.send_remove(&user, context).await?;
392 updated_comment.send_undo_remove(&user, context).await?;
396 let comment_id = data.comment_id;
397 let user_id = user.id;
398 let comment_view = blocking(context.pool(), move |conn| {
399 CommentView::read(conn, comment_id, Some(user_id))
403 // Build the recipients
404 let comment_view_2 = comment_view.clone();
406 let mentions = vec![];
407 let recipient_ids = send_local_notifs(
417 let res = CommentResponse {
420 form_id: None, // TODO maybe this might clear other forms
423 context.chat_server().do_send(SendComment {
424 op: UserOperation::RemoveComment,
425 comment: res.clone(),
433 #[async_trait::async_trait(?Send)]
434 impl Perform for MarkCommentAsRead {
435 type Response = CommentResponse;
439 context: &Data<LemmyContext>,
440 _websocket_id: Option<ConnectionId>,
441 ) -> Result<CommentResponse, LemmyError> {
442 let data: &MarkCommentAsRead = &self;
443 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
445 let comment_id = data.comment_id;
446 let orig_comment = blocking(context.pool(), move |conn| {
447 CommentView::read(&conn, comment_id, None)
451 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
453 // Verify that only the recipient can mark as read
454 if user.id != orig_comment.get_recipient_id() {
455 return Err(ApiError::err("no_comment_edit_allowed").into());
458 // Do the mark as read
459 let read = data.read;
460 match blocking(context.pool(), move |conn| {
461 Comment::update_read(conn, comment_id, read)
465 Ok(comment) => comment,
466 Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
470 let comment_id = data.comment_id;
471 let user_id = user.id;
472 let comment_view = blocking(context.pool(), move |conn| {
473 CommentView::read(conn, comment_id, Some(user_id))
477 let res = CommentResponse {
479 recipient_ids: Vec::new(),
487 #[async_trait::async_trait(?Send)]
488 impl Perform for SaveComment {
489 type Response = CommentResponse;
493 context: &Data<LemmyContext>,
494 _websocket_id: Option<ConnectionId>,
495 ) -> Result<CommentResponse, LemmyError> {
496 let data: &SaveComment = &self;
497 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
499 let comment_saved_form = CommentSavedForm {
500 comment_id: data.comment_id,
505 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
506 if blocking(context.pool(), save_comment).await?.is_err() {
507 return Err(ApiError::err("couldnt_save_comment").into());
510 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
511 if blocking(context.pool(), unsave_comment).await?.is_err() {
512 return Err(ApiError::err("couldnt_save_comment").into());
516 let comment_id = data.comment_id;
517 let user_id = user.id;
518 let comment_view = blocking(context.pool(), move |conn| {
519 CommentView::read(conn, comment_id, Some(user_id))
525 recipient_ids: Vec::new(),
531 #[async_trait::async_trait(?Send)]
532 impl Perform for CreateCommentLike {
533 type Response = CommentResponse;
537 context: &Data<LemmyContext>,
538 websocket_id: Option<ConnectionId>,
539 ) -> Result<CommentResponse, LemmyError> {
540 let data: &CreateCommentLike = &self;
541 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
543 let mut recipient_ids = Vec::new();
545 // Don't do a downvote if site has downvotes disabled
546 check_downvotes_enabled(data.score, context.pool()).await?;
548 let comment_id = data.comment_id;
549 let orig_comment = blocking(context.pool(), move |conn| {
550 CommentView::read(&conn, comment_id, None)
554 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
556 // Add parent user to recipients
557 recipient_ids.push(orig_comment.get_recipient_id());
559 let like_form = CommentLikeForm {
560 comment_id: data.comment_id,
561 post_id: orig_comment.post.id,
566 // Remove any likes first
567 let user_id = user.id;
568 blocking(context.pool(), move |conn| {
569 CommentLike::remove(conn, user_id, comment_id)
573 // Only add the like if the score isnt 0
574 let comment = orig_comment.comment;
575 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
577 let like_form2 = like_form.clone();
578 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
579 if blocking(context.pool(), like).await?.is_err() {
580 return Err(ApiError::err("couldnt_like_comment").into());
583 if like_form.score == 1 {
584 comment.send_like(&user, context).await?;
585 } else if like_form.score == -1 {
586 comment.send_dislike(&user, context).await?;
589 comment.send_undo_like(&user, context).await?;
592 // Have to refetch the comment to get the current state
593 let comment_id = data.comment_id;
594 let user_id = user.id;
595 let liked_comment = blocking(context.pool(), move |conn| {
596 CommentView::read(conn, comment_id, Some(user_id))
600 let res = CommentResponse {
601 comment_view: liked_comment,
606 context.chat_server().do_send(SendComment {
607 op: UserOperation::CreateCommentLike,
608 comment: res.clone(),
616 #[async_trait::async_trait(?Send)]
617 impl Perform for GetComments {
618 type Response = GetCommentsResponse;
622 context: &Data<LemmyContext>,
623 _websocket_id: Option<ConnectionId>,
624 ) -> Result<GetCommentsResponse, LemmyError> {
625 let data: &GetComments = &self;
626 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
627 let user_id = user.map(|u| u.id);
629 let type_ = ListingType::from_str(&data.type_)?;
630 let sort = SortType::from_str(&data.sort)?;
632 let community_id = data.community_id;
633 let community_name = data.community_name.to_owned();
634 let page = data.page;
635 let limit = data.limit;
636 let comments = blocking(context.pool(), move |conn| {
637 CommentQueryBuilder::create(conn)
640 .community_id(community_id)
641 .community_name(community_name)
648 let comments = match comments {
649 Ok(comments) => comments,
650 Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
653 Ok(GetCommentsResponse { comments })
657 /// Creates a comment report and notifies the moderators of the community
658 #[async_trait::async_trait(?Send)]
659 impl Perform for CreateCommentReport {
660 type Response = CreateCommentReportResponse;
664 context: &Data<LemmyContext>,
665 websocket_id: Option<ConnectionId>,
666 ) -> Result<CreateCommentReportResponse, LemmyError> {
667 let data: &CreateCommentReport = &self;
668 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
670 // check size of report and check for whitespace
671 let reason = data.reason.trim();
672 if reason.is_empty() {
673 return Err(ApiError::err("report_reason_required").into());
675 if reason.chars().count() > 1000 {
676 return Err(ApiError::err("report_too_long").into());
679 let user_id = user.id;
680 let comment_id = data.comment_id;
681 let comment_view = blocking(context.pool(), move |conn| {
682 CommentView::read(&conn, comment_id, None)
686 check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
688 let report_form = CommentReportForm {
691 original_comment_text: comment_view.comment.content,
692 reason: data.reason.to_owned(),
695 let report = match blocking(context.pool(), move |conn| {
696 CommentReport::report(conn, &report_form)
700 Ok(report) => report,
701 Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
704 let res = CreateCommentReportResponse { success: true };
706 context.chat_server().do_send(SendUserRoomMessage {
707 op: UserOperation::CreateCommentReport,
708 response: res.clone(),
709 recipient_id: user.id,
713 context.chat_server().do_send(SendModRoomMessage {
714 op: UserOperation::CreateCommentReport,
716 community_id: comment_view.community.id,
724 /// Resolves or unresolves a comment report and notifies the moderators of the community
725 #[async_trait::async_trait(?Send)]
726 impl Perform for ResolveCommentReport {
727 type Response = ResolveCommentReportResponse;
731 context: &Data<LemmyContext>,
732 websocket_id: Option<ConnectionId>,
733 ) -> Result<ResolveCommentReportResponse, LemmyError> {
734 let data: &ResolveCommentReport = &self;
735 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
737 let report_id = data.report_id;
738 let report = blocking(context.pool(), move |conn| {
739 CommentReportView::read(&conn, report_id)
743 let user_id = user.id;
744 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
746 let resolved = data.resolved;
747 let resolve_fun = move |conn: &'_ _| {
749 CommentReport::resolve(conn, report_id, user_id)
751 CommentReport::unresolve(conn, report_id, user_id)
755 if blocking(context.pool(), resolve_fun).await?.is_err() {
756 return Err(ApiError::err("couldnt_resolve_report").into());
759 let report_id = data.report_id;
760 let res = ResolveCommentReportResponse {
765 context.chat_server().do_send(SendModRoomMessage {
766 op: UserOperation::ResolveCommentReport,
767 response: res.clone(),
768 community_id: report.community.id,
776 /// Lists comment reports for a community if an id is supplied
777 /// or returns all comment reports for communities a user moderates
778 #[async_trait::async_trait(?Send)]
779 impl Perform for ListCommentReports {
780 type Response = ListCommentReportsResponse;
784 context: &Data<LemmyContext>,
785 websocket_id: Option<ConnectionId>,
786 ) -> Result<ListCommentReportsResponse, LemmyError> {
787 let data: &ListCommentReports = &self;
788 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
790 let user_id = user.id;
791 let community_id = data.community;
793 collect_moderated_communities(user_id, community_id, context.pool()).await?;
795 let page = data.page;
796 let limit = data.limit;
797 let comments = blocking(context.pool(), move |conn| {
798 CommentReportQueryBuilder::create(conn)
799 .community_ids(community_ids)
806 let res = ListCommentReportsResponse { comments };
808 context.chat_server().do_send(SendUserRoomMessage {
809 op: UserOperation::ListCommentReports,
810 response: res.clone(),
811 recipient_id: user.id,