]> Untitled Git - lemmy.git/blob - crates/api/src/comment.rs
Merge branch 'main' into move_matrix_and_admin_to_person
[lemmy.git] / crates / api / src / comment.rs
1 use crate::{
2   check_community_ban,
3   check_downvotes_enabled,
4   collect_moderated_communities,
5   get_local_user_view_from_jwt,
6   get_local_user_view_from_jwt_opt,
7   get_post,
8   is_mod_or_admin,
9   Perform,
10 };
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_,
16   Crud,
17   Likeable,
18   ListingType,
19   Reportable,
20   Saveable,
21   SortType,
22 };
23 use lemmy_db_schema::{
24   source::{comment::*, comment_report::*, moderator::*},
25   LocalUserId,
26 };
27 use lemmy_db_views::{
28   comment_report_view::{CommentReportQueryBuilder, CommentReportView},
29   comment_view::{CommentQueryBuilder, CommentView},
30   local_user_view::LocalUserView,
31 };
32 use lemmy_utils::{
33   utils::{remove_slurs, scrape_text_for_mentions},
34   ApiError,
35   ConnectionId,
36   LemmyError,
37 };
38 use lemmy_websocket::{
39   messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
40   LemmyContext,
41   UserOperation,
42 };
43 use std::str::FromStr;
44
45 #[async_trait::async_trait(?Send)]
46 impl Perform for CreateComment {
47   type Response = CommentResponse;
48
49   async fn perform(
50     &self,
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?;
56
57     let content_slurs_removed = remove_slurs(&data.content.to_owned());
58
59     // Check for a community ban
60     let post_id = data.post_id;
61     let post = get_post(post_id, context.pool()).await?;
62
63     check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
64
65     // Check if post is locked, no new comments
66     if post.locked {
67       return Err(ApiError::err("locked").into());
68     }
69
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
73       let parent =
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()),
77         };
78       if parent.post_id != post_id {
79         return Err(ApiError::err("couldnt_create_comment").into());
80       }
81     }
82
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()
89     };
90
91     // Create the comment
92     let comment_form2 = comment_form.clone();
93     let inserted_comment = match blocking(context.pool(), move |conn| {
94       Comment::create(&conn, &comment_form2)
95     })
96     .await?
97     {
98       Ok(comment) => comment,
99       Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
100     };
101
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> {
106         let apub_id =
107           generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
108         Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
109       })
110       .await?
111       {
112         Ok(comment) => comment,
113         Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
114       };
115
116     updated_comment
117       .send_create(&local_user_view.person, context)
118       .await?;
119
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(
124       mentions,
125       updated_comment.clone(),
126       local_user_view.person.clone(),
127       post,
128       context.pool(),
129       true,
130     )
131     .await?;
132
133     // You like your own comment by default
134     let like_form = CommentLikeForm {
135       comment_id: inserted_comment.id,
136       post_id,
137       person_id: local_user_view.person.id,
138       score: 1,
139     };
140
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());
144     }
145
146     updated_comment
147       .send_like(&local_user_view.person, context)
148       .await?;
149
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))
153     })
154     .await??;
155
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)
161       })
162       .await?
163       {
164         Ok(comment) => comment,
165         Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
166       };
167       comment_view.comment.read = true;
168     }
169
170     let mut res = CommentResponse {
171       comment_view,
172       recipient_ids,
173       form_id: data.form_id.to_owned(),
174     };
175
176     context.chat_server().do_send(SendComment {
177       op: UserOperation::CreateComment,
178       comment: res.clone(),
179       websocket_id,
180     });
181
182     res.recipient_ids = Vec::new(); // Necessary to avoid doubles
183
184     Ok(res)
185   }
186 }
187
188 #[async_trait::async_trait(?Send)]
189 impl Perform for EditComment {
190   type Response = CommentResponse;
191
192   async fn perform(
193     &self,
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?;
199
200     let comment_id = data.comment_id;
201     let orig_comment = blocking(context.pool(), move |conn| {
202       CommentView::read(&conn, comment_id, None)
203     })
204     .await??;
205
206     check_community_ban(
207       local_user_view.person.id,
208       orig_comment.community.id,
209       context.pool(),
210     )
211     .await?;
212
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());
216     }
217
218     // Do the update
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)
223     })
224     .await?
225     {
226       Ok(comment) => comment,
227       Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
228     };
229
230     // Send the apub update
231     updated_comment
232       .send_update(&local_user_view.person, context)
233       .await?;
234
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(
239       mentions,
240       updated_comment,
241       local_user_view.person.clone(),
242       orig_comment.post,
243       context.pool(),
244       false,
245     )
246     .await?;
247
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))
252     })
253     .await??;
254
255     let res = CommentResponse {
256       comment_view,
257       recipient_ids,
258       form_id: data.form_id.to_owned(),
259     };
260
261     context.chat_server().do_send(SendComment {
262       op: UserOperation::EditComment,
263       comment: res.clone(),
264       websocket_id,
265     });
266
267     Ok(res)
268   }
269 }
270
271 #[async_trait::async_trait(?Send)]
272 impl Perform for DeleteComment {
273   type Response = CommentResponse;
274
275   async fn perform(
276     &self,
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?;
282
283     let comment_id = data.comment_id;
284     let orig_comment = blocking(context.pool(), move |conn| {
285       CommentView::read(&conn, comment_id, None)
286     })
287     .await??;
288
289     check_community_ban(
290       local_user_view.person.id,
291       orig_comment.community.id,
292       context.pool(),
293     )
294     .await?;
295
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());
299     }
300
301     // Do the delete
302     let deleted = data.deleted;
303     let updated_comment = match blocking(context.pool(), move |conn| {
304       Comment::update_deleted(conn, comment_id, deleted)
305     })
306     .await?
307     {
308       Ok(comment) => comment,
309       Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
310     };
311
312     // Send the apub message
313     if deleted {
314       updated_comment
315         .send_delete(&local_user_view.person, context)
316         .await?;
317     } else {
318       updated_comment
319         .send_undo_delete(&local_user_view.person, context)
320         .await?;
321     }
322
323     // Refetch it
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))
328     })
329     .await??;
330
331     // Build the recipients
332     let comment_view_2 = comment_view.clone();
333     let mentions = vec![];
334     let recipient_ids = send_local_notifs(
335       mentions,
336       updated_comment,
337       local_user_view.person.clone(),
338       comment_view_2.post,
339       context.pool(),
340       false,
341     )
342     .await?;
343
344     let res = CommentResponse {
345       comment_view,
346       recipient_ids,
347       form_id: None, // TODO a comment delete might clear forms?
348     };
349
350     context.chat_server().do_send(SendComment {
351       op: UserOperation::DeleteComment,
352       comment: res.clone(),
353       websocket_id,
354     });
355
356     Ok(res)
357   }
358 }
359
360 #[async_trait::async_trait(?Send)]
361 impl Perform for RemoveComment {
362   type Response = CommentResponse;
363
364   async fn perform(
365     &self,
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?;
371
372     let comment_id = data.comment_id;
373     let orig_comment = blocking(context.pool(), move |conn| {
374       CommentView::read(&conn, comment_id, None)
375     })
376     .await??;
377
378     check_community_ban(
379       local_user_view.person.id,
380       orig_comment.community.id,
381       context.pool(),
382     )
383     .await?;
384
385     // Verify that only a mod or admin can remove
386     is_mod_or_admin(
387       context.pool(),
388       local_user_view.person.id,
389       orig_comment.community.id,
390     )
391     .await?;
392
393     // Do the remove
394     let removed = data.removed;
395     let updated_comment = match blocking(context.pool(), move |conn| {
396       Comment::update_removed(conn, comment_id, removed)
397     })
398     .await?
399     {
400       Ok(comment) => comment,
401       Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
402     };
403
404     // Mod tables
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(),
410     };
411     blocking(context.pool(), move |conn| {
412       ModRemoveComment::create(conn, &form)
413     })
414     .await??;
415
416     // Send the apub message
417     if removed {
418       updated_comment
419         .send_remove(&local_user_view.person, context)
420         .await?;
421     } else {
422       updated_comment
423         .send_undo_remove(&local_user_view.person, context)
424         .await?;
425     }
426
427     // Refetch it
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))
432     })
433     .await??;
434
435     // Build the recipients
436     let comment_view_2 = comment_view.clone();
437
438     let mentions = vec![];
439     let recipient_ids = send_local_notifs(
440       mentions,
441       updated_comment,
442       local_user_view.person.clone(),
443       comment_view_2.post,
444       context.pool(),
445       false,
446     )
447     .await?;
448
449     let res = CommentResponse {
450       comment_view,
451       recipient_ids,
452       form_id: None, // TODO maybe this might clear other forms
453     };
454
455     context.chat_server().do_send(SendComment {
456       op: UserOperation::RemoveComment,
457       comment: res.clone(),
458       websocket_id,
459     });
460
461     Ok(res)
462   }
463 }
464
465 #[async_trait::async_trait(?Send)]
466 impl Perform for MarkCommentAsRead {
467   type Response = CommentResponse;
468
469   async fn perform(
470     &self,
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?;
476
477     let comment_id = data.comment_id;
478     let orig_comment = blocking(context.pool(), move |conn| {
479       CommentView::read(&conn, comment_id, None)
480     })
481     .await??;
482
483     check_community_ban(
484       local_user_view.person.id,
485       orig_comment.community.id,
486       context.pool(),
487     )
488     .await?;
489
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());
493     }
494
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)
499     })
500     .await?
501     {
502       Ok(comment) => comment,
503       Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
504     };
505
506     // Refetch it
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))
511     })
512     .await??;
513
514     let res = CommentResponse {
515       comment_view,
516       recipient_ids: Vec::new(),
517       form_id: None,
518     };
519
520     Ok(res)
521   }
522 }
523
524 #[async_trait::async_trait(?Send)]
525 impl Perform for SaveComment {
526   type Response = CommentResponse;
527
528   async fn perform(
529     &self,
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?;
535
536     let comment_saved_form = CommentSavedForm {
537       comment_id: data.comment_id,
538       person_id: local_user_view.person.id,
539     };
540
541     if data.save {
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());
545       }
546     } else {
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());
550       }
551     }
552
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))
557     })
558     .await??;
559
560     Ok(CommentResponse {
561       comment_view,
562       recipient_ids: Vec::new(),
563       form_id: None,
564     })
565   }
566 }
567
568 #[async_trait::async_trait(?Send)]
569 impl Perform for CreateCommentLike {
570   type Response = CommentResponse;
571
572   async fn perform(
573     &self,
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?;
579
580     let mut recipient_ids = Vec::<LocalUserId>::new();
581
582     // Don't do a downvote if site has downvotes disabled
583     check_downvotes_enabled(data.score, context.pool()).await?;
584
585     let comment_id = data.comment_id;
586     let orig_comment = blocking(context.pool(), move |conn| {
587       CommentView::read(&conn, comment_id, None)
588     })
589     .await??;
590
591     check_community_ban(
592       local_user_view.person.id,
593       orig_comment.community.id,
594       context.pool(),
595     )
596     .await?;
597
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)
602     })
603     .await?
604     {
605       recipient_ids.push(local_recipient.local_user.id);
606     }
607
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,
612       score: data.score,
613     };
614
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)
619     })
620     .await??;
621
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);
625     if do_add {
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());
630       }
631
632       if like_form.score == 1 {
633         comment.send_like(&local_user_view.person, context).await?;
634       } else if like_form.score == -1 {
635         comment
636           .send_dislike(&local_user_view.person, context)
637           .await?;
638       }
639     } else {
640       comment
641         .send_undo_like(&local_user_view.person, context)
642         .await?;
643     }
644
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))
650     })
651     .await??;
652
653     let res = CommentResponse {
654       comment_view: liked_comment,
655       recipient_ids,
656       form_id: None,
657     };
658
659     context.chat_server().do_send(SendComment {
660       op: UserOperation::CreateCommentLike,
661       comment: res.clone(),
662       websocket_id,
663     });
664
665     Ok(res)
666   }
667 }
668
669 #[async_trait::async_trait(?Send)]
670 impl Perform for GetComments {
671   type Response = GetCommentsResponse;
672
673   async fn perform(
674     &self,
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);
681
682     let type_ = ListingType::from_str(&data.type_)?;
683     let sort = SortType::from_str(&data.sort)?;
684
685     let community_id = data.community_id;
686     let community_name = data.community_name.to_owned();
687     let saved_only = data.saved_only;
688     let page = data.page;
689     let limit = data.limit;
690     let comments = blocking(context.pool(), move |conn| {
691       CommentQueryBuilder::create(conn)
692         .listing_type(type_)
693         .sort(&sort)
694         .saved_only(saved_only)
695         .community_id(community_id)
696         .community_name(community_name)
697         .my_person_id(person_id)
698         .page(page)
699         .limit(limit)
700         .list()
701     })
702     .await?;
703     let comments = match comments {
704       Ok(comments) => comments,
705       Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
706     };
707
708     Ok(GetCommentsResponse { comments })
709   }
710 }
711
712 /// Creates a comment report and notifies the moderators of the community
713 #[async_trait::async_trait(?Send)]
714 impl Perform for CreateCommentReport {
715   type Response = CreateCommentReportResponse;
716
717   async fn perform(
718     &self,
719     context: &Data<LemmyContext>,
720     websocket_id: Option<ConnectionId>,
721   ) -> Result<CreateCommentReportResponse, LemmyError> {
722     let data: &CreateCommentReport = &self;
723     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
724
725     // check size of report and check for whitespace
726     let reason = data.reason.trim();
727     if reason.is_empty() {
728       return Err(ApiError::err("report_reason_required").into());
729     }
730     if reason.chars().count() > 1000 {
731       return Err(ApiError::err("report_too_long").into());
732     }
733
734     let person_id = local_user_view.person.id;
735     let comment_id = data.comment_id;
736     let comment_view = blocking(context.pool(), move |conn| {
737       CommentView::read(&conn, comment_id, None)
738     })
739     .await??;
740
741     check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
742
743     let report_form = CommentReportForm {
744       creator_id: person_id,
745       comment_id,
746       original_comment_text: comment_view.comment.content,
747       reason: data.reason.to_owned(),
748     };
749
750     let report = match blocking(context.pool(), move |conn| {
751       CommentReport::report(conn, &report_form)
752     })
753     .await?
754     {
755       Ok(report) => report,
756       Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
757     };
758
759     let res = CreateCommentReportResponse { success: true };
760
761     context.chat_server().do_send(SendUserRoomMessage {
762       op: UserOperation::CreateCommentReport,
763       response: res.clone(),
764       local_recipient_id: local_user_view.local_user.id,
765       websocket_id,
766     });
767
768     context.chat_server().do_send(SendModRoomMessage {
769       op: UserOperation::CreateCommentReport,
770       response: report,
771       community_id: comment_view.community.id,
772       websocket_id,
773     });
774
775     Ok(res)
776   }
777 }
778
779 /// Resolves or unresolves a comment report and notifies the moderators of the community
780 #[async_trait::async_trait(?Send)]
781 impl Perform for ResolveCommentReport {
782   type Response = ResolveCommentReportResponse;
783
784   async fn perform(
785     &self,
786     context: &Data<LemmyContext>,
787     websocket_id: Option<ConnectionId>,
788   ) -> Result<ResolveCommentReportResponse, LemmyError> {
789     let data: &ResolveCommentReport = &self;
790     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
791
792     let report_id = data.report_id;
793     let report = blocking(context.pool(), move |conn| {
794       CommentReportView::read(&conn, report_id)
795     })
796     .await??;
797
798     let person_id = local_user_view.person.id;
799     is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
800
801     let resolved = data.resolved;
802     let resolve_fun = move |conn: &'_ _| {
803       if resolved {
804         CommentReport::resolve(conn, report_id, person_id)
805       } else {
806         CommentReport::unresolve(conn, report_id, person_id)
807       }
808     };
809
810     if blocking(context.pool(), resolve_fun).await?.is_err() {
811       return Err(ApiError::err("couldnt_resolve_report").into());
812     };
813
814     let report_id = data.report_id;
815     let res = ResolveCommentReportResponse {
816       report_id,
817       resolved,
818     };
819
820     context.chat_server().do_send(SendModRoomMessage {
821       op: UserOperation::ResolveCommentReport,
822       response: res.clone(),
823       community_id: report.community.id,
824       websocket_id,
825     });
826
827     Ok(res)
828   }
829 }
830
831 /// Lists comment reports for a community if an id is supplied
832 /// or returns all comment reports for communities a user moderates
833 #[async_trait::async_trait(?Send)]
834 impl Perform for ListCommentReports {
835   type Response = ListCommentReportsResponse;
836
837   async fn perform(
838     &self,
839     context: &Data<LemmyContext>,
840     websocket_id: Option<ConnectionId>,
841   ) -> Result<ListCommentReportsResponse, LemmyError> {
842     let data: &ListCommentReports = &self;
843     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
844
845     let person_id = local_user_view.person.id;
846     let community_id = data.community;
847     let community_ids =
848       collect_moderated_communities(person_id, community_id, context.pool()).await?;
849
850     let page = data.page;
851     let limit = data.limit;
852     let comments = blocking(context.pool(), move |conn| {
853       CommentReportQueryBuilder::create(conn)
854         .community_ids(community_ids)
855         .page(page)
856         .limit(limit)
857         .list()
858     })
859     .await??;
860
861     let res = ListCommentReportsResponse { comments };
862
863     context.chat_server().do_send(SendUserRoomMessage {
864       op: UserOperation::ListCommentReports,
865       response: res.clone(),
866       local_recipient_id: local_user_view.local_user.id,
867       websocket_id,
868     });
869
870     Ok(res)
871   }
872 }