]> 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 page = data.page;
688     let limit = data.limit;
689     let comments = blocking(context.pool(), move |conn| {
690       CommentQueryBuilder::create(conn)
691         .listing_type(type_)
692         .sort(&sort)
693         .community_id(community_id)
694         .community_name(community_name)
695         .my_person_id(person_id)
696         .page(page)
697         .limit(limit)
698         .list()
699     })
700     .await?;
701     let comments = match comments {
702       Ok(comments) => comments,
703       Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
704     };
705
706     Ok(GetCommentsResponse { comments })
707   }
708 }
709
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;
714
715   async fn perform(
716     &self,
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?;
722
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());
727     }
728     if reason.chars().count() > 1000 {
729       return Err(ApiError::err("report_too_long").into());
730     }
731
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)
736     })
737     .await??;
738
739     check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
740
741     let report_form = CommentReportForm {
742       creator_id: person_id,
743       comment_id,
744       original_comment_text: comment_view.comment.content,
745       reason: data.reason.to_owned(),
746     };
747
748     let report = match blocking(context.pool(), move |conn| {
749       CommentReport::report(conn, &report_form)
750     })
751     .await?
752     {
753       Ok(report) => report,
754       Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
755     };
756
757     let res = CreateCommentReportResponse { success: true };
758
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,
763       websocket_id,
764     });
765
766     context.chat_server().do_send(SendModRoomMessage {
767       op: UserOperation::CreateCommentReport,
768       response: report,
769       community_id: comment_view.community.id,
770       websocket_id,
771     });
772
773     Ok(res)
774   }
775 }
776
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;
781
782   async fn perform(
783     &self,
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?;
789
790     let report_id = data.report_id;
791     let report = blocking(context.pool(), move |conn| {
792       CommentReportView::read(&conn, report_id)
793     })
794     .await??;
795
796     let person_id = local_user_view.person.id;
797     is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
798
799     let resolved = data.resolved;
800     let resolve_fun = move |conn: &'_ _| {
801       if resolved {
802         CommentReport::resolve(conn, report_id, person_id)
803       } else {
804         CommentReport::unresolve(conn, report_id, person_id)
805       }
806     };
807
808     if blocking(context.pool(), resolve_fun).await?.is_err() {
809       return Err(ApiError::err("couldnt_resolve_report").into());
810     };
811
812     let report_id = data.report_id;
813     let res = ResolveCommentReportResponse {
814       report_id,
815       resolved,
816     };
817
818     context.chat_server().do_send(SendModRoomMessage {
819       op: UserOperation::ResolveCommentReport,
820       response: res.clone(),
821       community_id: report.community.id,
822       websocket_id,
823     });
824
825     Ok(res)
826   }
827 }
828
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;
834
835   async fn perform(
836     &self,
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?;
842
843     let person_id = local_user_view.person.id;
844     let community_id = data.community;
845     let community_ids =
846       collect_moderated_communities(person_id, community_id, context.pool()).await?;
847
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)
853         .page(page)
854         .limit(limit)
855         .list()
856     })
857     .await??;
858
859     let res = ListCommentReportsResponse { comments };
860
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,
865       websocket_id,
866     });
867
868     Ok(res)
869   }
870 }