]> Untitled Git - lemmy.git/blob - lemmy_api/src/comment.rs
689fe4b8abb0da3e47c801344046db7e23c80dad
[lemmy.git] / lemmy_api / src / comment.rs
1 use crate::{
2   check_community_ban,
3   collect_moderated_communities,
4   get_post,
5   get_user_from_jwt,
6   get_user_from_jwt_opt,
7   is_mod_or_admin,
8   Perform,
9 };
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
12 use lemmy_db::{
13   source::{
14     comment::*,
15     comment_report::{CommentReport, CommentReportForm},
16     moderator::*,
17     post::*,
18     user::*,
19   },
20   views::{
21     comment_report_view::{CommentReportQueryBuilder, CommentReportView},
22     comment_view::{CommentQueryBuilder, CommentView},
23     site_view::SiteView,
24   },
25   Crud,
26   Likeable,
27   ListingType,
28   Reportable,
29   Saveable,
30   SortType,
31 };
32 use lemmy_structs::{blocking, comment::*, send_local_notifs};
33 use lemmy_utils::{
34   apub::{make_apub_endpoint, EndpointType},
35   utils::{remove_slurs, scrape_text_for_mentions},
36   APIError,
37   ConnectionId,
38   LemmyError,
39 };
40 use lemmy_websocket::{
41   messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
42   LemmyContext,
43   UserOperation,
44 };
45 use std::str::FromStr;
46
47 #[async_trait::async_trait(?Send)]
48 impl Perform for CreateComment {
49   type Response = CommentResponse;
50
51   async fn perform(
52     &self,
53     context: &Data<LemmyContext>,
54     websocket_id: Option<ConnectionId>,
55   ) -> Result<CommentResponse, LemmyError> {
56     let data: &CreateComment = &self;
57     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
58
59     let content_slurs_removed = remove_slurs(&data.content.to_owned());
60
61     let comment_form = CommentForm {
62       content: content_slurs_removed,
63       parent_id: data.parent_id.to_owned(),
64       post_id: data.post_id,
65       creator_id: user.id,
66       removed: None,
67       deleted: None,
68       read: None,
69       published: None,
70       updated: None,
71       ap_id: None,
72       local: true,
73     };
74
75     // Check for a community ban
76     let post_id = data.post_id;
77     let post = get_post(post_id, context.pool()).await?;
78
79     check_community_ban(user.id, post.community_id, context.pool()).await?;
80
81     // Check if post is locked, no new comments
82     if post.locked {
83       return Err(APIError::err("locked").into());
84     }
85
86     // Create the comment
87     let comment_form2 = comment_form.clone();
88     let inserted_comment = match blocking(context.pool(), move |conn| {
89       Comment::create(&conn, &comment_form2)
90     })
91     .await?
92     {
93       Ok(comment) => comment,
94       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
95     };
96
97     // Necessary to update the ap_id
98     let inserted_comment_id = inserted_comment.id;
99     let updated_comment: Comment = match blocking(context.pool(), move |conn| {
100       let apub_id =
101         make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
102       Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
103     })
104     .await?
105     {
106       Ok(comment) => comment,
107       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
108     };
109
110     updated_comment.send_create(&user, context).await?;
111
112     // Scan the comment for user mentions, add those rows
113     let mentions = scrape_text_for_mentions(&comment_form.content);
114     let recipient_ids = send_local_notifs(
115       mentions,
116       updated_comment.clone(),
117       &user,
118       post,
119       context.pool(),
120       true,
121     )
122     .await?;
123
124     // You like your own comment by default
125     let like_form = CommentLikeForm {
126       comment_id: inserted_comment.id,
127       post_id: data.post_id,
128       user_id: user.id,
129       score: 1,
130     };
131
132     let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
133     if blocking(context.pool(), like).await?.is_err() {
134       return Err(APIError::err("couldnt_like_comment").into());
135     }
136
137     updated_comment.send_like(&user, context).await?;
138
139     let user_id = user.id;
140     let comment_view = blocking(context.pool(), move |conn| {
141       CommentView::read(&conn, inserted_comment.id, Some(user_id))
142     })
143     .await??;
144
145     let mut res = CommentResponse {
146       comment_view,
147       recipient_ids,
148       form_id: data.form_id.to_owned(),
149     };
150
151     context.chat_server().do_send(SendComment {
152       op: UserOperation::CreateComment,
153       comment: res.clone(),
154       websocket_id,
155     });
156
157     // strip out the recipient_ids, so that
158     // users don't get double notifs
159     res.recipient_ids = Vec::new();
160
161     Ok(res)
162   }
163 }
164
165 #[async_trait::async_trait(?Send)]
166 impl Perform for EditComment {
167   type Response = CommentResponse;
168
169   async fn perform(
170     &self,
171     context: &Data<LemmyContext>,
172     websocket_id: Option<ConnectionId>,
173   ) -> Result<CommentResponse, LemmyError> {
174     let data: &EditComment = &self;
175     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
176
177     let edit_id = data.edit_id;
178     let orig_comment = blocking(context.pool(), move |conn| {
179       CommentView::read(&conn, edit_id, None)
180     })
181     .await??;
182
183     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
184
185     // Verify that only the creator can edit
186     if user.id != orig_comment.creator.id {
187       return Err(APIError::err("no_comment_edit_allowed").into());
188     }
189
190     // Do the update
191     let content_slurs_removed = remove_slurs(&data.content.to_owned());
192     let edit_id = data.edit_id;
193     let updated_comment = match blocking(context.pool(), move |conn| {
194       Comment::update_content(conn, edit_id, &content_slurs_removed)
195     })
196     .await?
197     {
198       Ok(comment) => comment,
199       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
200     };
201
202     // Send the apub update
203     updated_comment.send_update(&user, context).await?;
204
205     // Do the mentions / recipients
206     let post_id = orig_comment.post.id;
207     let post = get_post(post_id, context.pool()).await?;
208
209     let updated_comment_content = updated_comment.content.to_owned();
210     let mentions = scrape_text_for_mentions(&updated_comment_content);
211     let recipient_ids = send_local_notifs(
212       mentions,
213       updated_comment,
214       &user,
215       post,
216       context.pool(),
217       false,
218     )
219     .await?;
220
221     let edit_id = data.edit_id;
222     let user_id = user.id;
223     let comment_view = blocking(context.pool(), move |conn| {
224       CommentView::read(conn, edit_id, Some(user_id))
225     })
226     .await??;
227
228     let mut res = CommentResponse {
229       comment_view,
230       recipient_ids,
231       form_id: data.form_id.to_owned(),
232     };
233
234     context.chat_server().do_send(SendComment {
235       op: UserOperation::EditComment,
236       comment: res.clone(),
237       websocket_id,
238     });
239
240     // strip out the recipient_ids, so that
241     // users don't get double notifs
242     res.recipient_ids = Vec::new();
243
244     Ok(res)
245   }
246 }
247
248 #[async_trait::async_trait(?Send)]
249 impl Perform for DeleteComment {
250   type Response = CommentResponse;
251
252   async fn perform(
253     &self,
254     context: &Data<LemmyContext>,
255     websocket_id: Option<ConnectionId>,
256   ) -> Result<CommentResponse, LemmyError> {
257     let data: &DeleteComment = &self;
258     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
259
260     let edit_id = data.edit_id;
261     let orig_comment = blocking(context.pool(), move |conn| {
262       CommentView::read(&conn, edit_id, None)
263     })
264     .await??;
265
266     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
267
268     // Verify that only the creator can delete
269     if user.id != orig_comment.creator.id {
270       return Err(APIError::err("no_comment_edit_allowed").into());
271     }
272
273     // Do the delete
274     let deleted = data.deleted;
275     let updated_comment = match blocking(context.pool(), move |conn| {
276       Comment::update_deleted(conn, edit_id, deleted)
277     })
278     .await?
279     {
280       Ok(comment) => comment,
281       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
282     };
283
284     // Send the apub message
285     if deleted {
286       updated_comment.send_delete(&user, context).await?;
287     } else {
288       updated_comment.send_undo_delete(&user, context).await?;
289     }
290
291     // Refetch it
292     let edit_id = data.edit_id;
293     let user_id = user.id;
294     let comment_view = blocking(context.pool(), move |conn| {
295       CommentView::read(conn, edit_id, Some(user_id))
296     })
297     .await??;
298
299     // Build the recipients
300     let post_id = comment_view.post.id;
301     let post = get_post(post_id, context.pool()).await?;
302     let mentions = vec![];
303     let recipient_ids = send_local_notifs(
304       mentions,
305       updated_comment,
306       &user,
307       post,
308       context.pool(),
309       false,
310     )
311     .await?;
312
313     let mut res = CommentResponse {
314       comment_view,
315       recipient_ids,
316       form_id: None,
317     };
318
319     context.chat_server().do_send(SendComment {
320       op: UserOperation::DeleteComment,
321       comment: res.clone(),
322       websocket_id,
323     });
324
325     // strip out the recipient_ids, so that
326     // users don't get double notifs
327     res.recipient_ids = Vec::new();
328
329     Ok(res)
330   }
331 }
332
333 #[async_trait::async_trait(?Send)]
334 impl Perform for RemoveComment {
335   type Response = CommentResponse;
336
337   async fn perform(
338     &self,
339     context: &Data<LemmyContext>,
340     websocket_id: Option<ConnectionId>,
341   ) -> Result<CommentResponse, LemmyError> {
342     let data: &RemoveComment = &self;
343     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
344
345     let edit_id = data.edit_id;
346     let orig_comment = blocking(context.pool(), move |conn| {
347       CommentView::read(&conn, edit_id, None)
348     })
349     .await??;
350
351     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
352
353     // Verify that only a mod or admin can remove
354     is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
355
356     // Do the remove
357     let removed = data.removed;
358     let updated_comment = match blocking(context.pool(), move |conn| {
359       Comment::update_removed(conn, edit_id, removed)
360     })
361     .await?
362     {
363       Ok(comment) => comment,
364       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
365     };
366
367     // Mod tables
368     let form = ModRemoveCommentForm {
369       mod_user_id: user.id,
370       comment_id: data.edit_id,
371       removed: Some(removed),
372       reason: data.reason.to_owned(),
373     };
374     blocking(context.pool(), move |conn| {
375       ModRemoveComment::create(conn, &form)
376     })
377     .await??;
378
379     // Send the apub message
380     if removed {
381       updated_comment.send_remove(&user, context).await?;
382     } else {
383       updated_comment.send_undo_remove(&user, context).await?;
384     }
385
386     // Refetch it
387     let edit_id = data.edit_id;
388     let user_id = user.id;
389     let comment_view = blocking(context.pool(), move |conn| {
390       CommentView::read(conn, edit_id, Some(user_id))
391     })
392     .await??;
393
394     // Build the recipients
395     let post_id = comment_view.post.id;
396     let post = get_post(post_id, context.pool()).await?;
397     let mentions = vec![];
398     let recipient_ids = send_local_notifs(
399       mentions,
400       updated_comment,
401       &user,
402       post,
403       context.pool(),
404       false,
405     )
406     .await?;
407
408     let mut res = CommentResponse {
409       comment_view,
410       recipient_ids,
411       form_id: None,
412     };
413
414     context.chat_server().do_send(SendComment {
415       op: UserOperation::RemoveComment,
416       comment: res.clone(),
417       websocket_id,
418     });
419
420     // strip out the recipient_ids, so that
421     // users don't get double notifs
422     res.recipient_ids = Vec::new();
423
424     Ok(res)
425   }
426 }
427
428 #[async_trait::async_trait(?Send)]
429 impl Perform for MarkCommentAsRead {
430   type Response = CommentResponse;
431
432   async fn perform(
433     &self,
434     context: &Data<LemmyContext>,
435     _websocket_id: Option<ConnectionId>,
436   ) -> Result<CommentResponse, LemmyError> {
437     let data: &MarkCommentAsRead = &self;
438     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
439
440     let edit_id = data.edit_id;
441     let orig_comment = blocking(context.pool(), move |conn| {
442       CommentView::read(&conn, edit_id, None)
443     })
444     .await??;
445
446     check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
447
448     // Verify that only the recipient can mark as read
449     // Needs to fetch the parent comment / post to get the recipient
450     let parent_id = orig_comment.comment.parent_id;
451     match parent_id {
452       Some(pid) => {
453         let parent_comment = blocking(context.pool(), move |conn| {
454           CommentView::read(&conn, pid, None)
455         })
456         .await??;
457         if user.id != parent_comment.creator.id {
458           return Err(APIError::err("no_comment_edit_allowed").into());
459         }
460       }
461       None => {
462         let parent_post_id = orig_comment.post.id;
463         let parent_post =
464           blocking(context.pool(), move |conn| Post::read(conn, parent_post_id)).await??;
465         if user.id != parent_post.creator_id {
466           return Err(APIError::err("no_comment_edit_allowed").into());
467         }
468       }
469     }
470
471     // Do the mark as read
472     let read = data.read;
473     match blocking(context.pool(), move |conn| {
474       Comment::update_read(conn, edit_id, read)
475     })
476     .await?
477     {
478       Ok(comment) => comment,
479       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
480     };
481
482     // Refetch it
483     let edit_id = data.edit_id;
484     let user_id = user.id;
485     let comment_view = blocking(context.pool(), move |conn| {
486       CommentView::read(conn, edit_id, Some(user_id))
487     })
488     .await??;
489
490     let res = CommentResponse {
491       comment_view,
492       recipient_ids: Vec::new(),
493       form_id: None,
494     };
495
496     Ok(res)
497   }
498 }
499
500 #[async_trait::async_trait(?Send)]
501 impl Perform for SaveComment {
502   type Response = CommentResponse;
503
504   async fn perform(
505     &self,
506     context: &Data<LemmyContext>,
507     _websocket_id: Option<ConnectionId>,
508   ) -> Result<CommentResponse, LemmyError> {
509     let data: &SaveComment = &self;
510     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
511
512     let comment_saved_form = CommentSavedForm {
513       comment_id: data.comment_id,
514       user_id: user.id,
515     };
516
517     if data.save {
518       let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
519       if blocking(context.pool(), save_comment).await?.is_err() {
520         return Err(APIError::err("couldnt_save_comment").into());
521       }
522     } else {
523       let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
524       if blocking(context.pool(), unsave_comment).await?.is_err() {
525         return Err(APIError::err("couldnt_save_comment").into());
526       }
527     }
528
529     let comment_id = data.comment_id;
530     let user_id = user.id;
531     let comment_view = blocking(context.pool(), move |conn| {
532       CommentView::read(conn, comment_id, Some(user_id))
533     })
534     .await??;
535
536     Ok(CommentResponse {
537       comment_view,
538       recipient_ids: Vec::new(),
539       form_id: None,
540     })
541   }
542 }
543
544 #[async_trait::async_trait(?Send)]
545 impl Perform for CreateCommentLike {
546   type Response = CommentResponse;
547
548   async fn perform(
549     &self,
550     context: &Data<LemmyContext>,
551     websocket_id: Option<ConnectionId>,
552   ) -> Result<CommentResponse, LemmyError> {
553     let data: &CreateCommentLike = &self;
554     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
555
556     let mut recipient_ids = Vec::new();
557
558     // Don't do a downvote if site has downvotes disabled
559     if data.score == -1 {
560       let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
561       if !site_view.site.enable_downvotes {
562         return Err(APIError::err("downvotes_disabled").into());
563       }
564     }
565
566     let comment_id = data.comment_id;
567     let orig_comment = blocking(context.pool(), move |conn| {
568       CommentView::read(&conn, comment_id, None)
569     })
570     .await??;
571
572     let post_id = orig_comment.post.id;
573     let post = get_post(post_id, context.pool()).await?;
574     check_community_ban(user.id, post.community_id, context.pool()).await?;
575
576     let comment_id = data.comment_id;
577     let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
578
579     // Add to recipient ids
580     match comment.parent_id {
581       Some(parent_id) => {
582         let parent_comment =
583           blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await??;
584         if parent_comment.creator_id != user.id {
585           let parent_user = blocking(context.pool(), move |conn| {
586             User_::read(conn, parent_comment.creator_id)
587           })
588           .await??;
589           recipient_ids.push(parent_user.id);
590         }
591       }
592       None => {
593         recipient_ids.push(post.creator_id);
594       }
595     }
596
597     let like_form = CommentLikeForm {
598       comment_id: data.comment_id,
599       post_id,
600       user_id: user.id,
601       score: data.score,
602     };
603
604     // Remove any likes first
605     let user_id = user.id;
606     blocking(context.pool(), move |conn| {
607       CommentLike::remove(conn, user_id, comment_id)
608     })
609     .await??;
610
611     // Only add the like if the score isnt 0
612     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
613     if do_add {
614       let like_form2 = like_form.clone();
615       let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
616       if blocking(context.pool(), like).await?.is_err() {
617         return Err(APIError::err("couldnt_like_comment").into());
618       }
619
620       if like_form.score == 1 {
621         comment.send_like(&user, context).await?;
622       } else if like_form.score == -1 {
623         comment.send_dislike(&user, context).await?;
624       }
625     } else {
626       comment.send_undo_like(&user, context).await?;
627     }
628
629     // Have to refetch the comment to get the current state
630     let comment_id = data.comment_id;
631     let user_id = user.id;
632     let liked_comment = blocking(context.pool(), move |conn| {
633       CommentView::read(conn, comment_id, Some(user_id))
634     })
635     .await??;
636
637     let mut res = CommentResponse {
638       comment_view: liked_comment,
639       recipient_ids,
640       form_id: None,
641     };
642
643     context.chat_server().do_send(SendComment {
644       op: UserOperation::CreateCommentLike,
645       comment: res.clone(),
646       websocket_id,
647     });
648
649     // strip out the recipient_ids, so that
650     // users don't get double notifs
651     res.recipient_ids = Vec::new();
652
653     Ok(res)
654   }
655 }
656
657 #[async_trait::async_trait(?Send)]
658 impl Perform for GetComments {
659   type Response = GetCommentsResponse;
660
661   async fn perform(
662     &self,
663     context: &Data<LemmyContext>,
664     _websocket_id: Option<ConnectionId>,
665   ) -> Result<GetCommentsResponse, LemmyError> {
666     let data: &GetComments = &self;
667     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
668     let user_id = user.map(|u| u.id);
669
670     let type_ = ListingType::from_str(&data.type_)?;
671     let sort = SortType::from_str(&data.sort)?;
672
673     let community_id = data.community_id;
674     let community_name = data.community_name.to_owned();
675     let page = data.page;
676     let limit = data.limit;
677     let comments = blocking(context.pool(), move |conn| {
678       CommentQueryBuilder::create(conn)
679         .listing_type(type_)
680         .sort(&sort)
681         .community_id(community_id)
682         .community_name(community_name)
683         .my_user_id(user_id)
684         .page(page)
685         .limit(limit)
686         .list()
687     })
688     .await?;
689     let comments = match comments {
690       Ok(comments) => comments,
691       Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
692     };
693
694     Ok(GetCommentsResponse { comments })
695   }
696 }
697
698 /// Creates a comment report and notifies the moderators of the community
699 #[async_trait::async_trait(?Send)]
700 impl Perform for CreateCommentReport {
701   type Response = CreateCommentReportResponse;
702
703   async fn perform(
704     &self,
705     context: &Data<LemmyContext>,
706     websocket_id: Option<ConnectionId>,
707   ) -> Result<CreateCommentReportResponse, LemmyError> {
708     let data: &CreateCommentReport = &self;
709     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
710
711     // check size of report and check for whitespace
712     let reason = data.reason.trim();
713     if reason.is_empty() {
714       return Err(APIError::err("report_reason_required").into());
715     }
716     if reason.len() > 1000 {
717       return Err(APIError::err("report_too_long").into());
718     }
719
720     let user_id = user.id;
721     let comment_id = data.comment_id;
722     let comment_view = blocking(context.pool(), move |conn| {
723       CommentView::read(&conn, comment_id, None)
724     })
725     .await??;
726
727     check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
728
729     let report_form = CommentReportForm {
730       creator_id: user_id,
731       comment_id,
732       original_comment_text: comment_view.comment.content,
733       reason: data.reason.to_owned(),
734     };
735
736     let report = match blocking(context.pool(), move |conn| {
737       CommentReport::report(conn, &report_form)
738     })
739     .await?
740     {
741       Ok(report) => report,
742       Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
743     };
744
745     let res = CreateCommentReportResponse { success: true };
746
747     context.chat_server().do_send(SendUserRoomMessage {
748       op: UserOperation::CreateCommentReport,
749       response: res.clone(),
750       recipient_id: user.id,
751       websocket_id,
752     });
753
754     context.chat_server().do_send(SendModRoomMessage {
755       op: UserOperation::CreateCommentReport,
756       response: report,
757       community_id: comment_view.community.id,
758       websocket_id,
759     });
760
761     Ok(res)
762   }
763 }
764
765 /// Resolves or unresolves a comment report and notifies the moderators of the community
766 #[async_trait::async_trait(?Send)]
767 impl Perform for ResolveCommentReport {
768   type Response = ResolveCommentReportResponse;
769
770   async fn perform(
771     &self,
772     context: &Data<LemmyContext>,
773     websocket_id: Option<ConnectionId>,
774   ) -> Result<ResolveCommentReportResponse, LemmyError> {
775     let data: &ResolveCommentReport = &self;
776     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
777
778     let report_id = data.report_id;
779     let report = blocking(context.pool(), move |conn| {
780       CommentReportView::read(&conn, report_id)
781     })
782     .await??;
783
784     let user_id = user.id;
785     is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
786
787     let resolved = data.resolved;
788     let resolve_fun = move |conn: &'_ _| {
789       if resolved {
790         CommentReport::resolve(conn, report_id, user_id)
791       } else {
792         CommentReport::unresolve(conn, report_id, user_id)
793       }
794     };
795
796     if blocking(context.pool(), resolve_fun).await?.is_err() {
797       return Err(APIError::err("couldnt_resolve_report").into());
798     };
799
800     let report_id = data.report_id;
801     let res = ResolveCommentReportResponse {
802       report_id,
803       resolved,
804     };
805
806     context.chat_server().do_send(SendModRoomMessage {
807       op: UserOperation::ResolveCommentReport,
808       response: res.clone(),
809       community_id: report.community.id,
810       websocket_id,
811     });
812
813     Ok(res)
814   }
815 }
816
817 /// Lists comment reports for a community if an id is supplied
818 /// or returns all comment reports for communities a user moderates
819 #[async_trait::async_trait(?Send)]
820 impl Perform for ListCommentReports {
821   type Response = ListCommentReportsResponse;
822
823   async fn perform(
824     &self,
825     context: &Data<LemmyContext>,
826     websocket_id: Option<ConnectionId>,
827   ) -> Result<ListCommentReportsResponse, LemmyError> {
828     let data: &ListCommentReports = &self;
829     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
830
831     let user_id = user.id;
832     let community_id = data.community;
833     let community_ids =
834       collect_moderated_communities(user_id, community_id, context.pool()).await?;
835
836     let page = data.page;
837     let limit = data.limit;
838     let comments = blocking(context.pool(), move |conn| {
839       CommentReportQueryBuilder::create(conn)
840         .community_ids(community_ids)
841         .page(page)
842         .limit(limit)
843         .list()
844     })
845     .await??;
846
847     let res = ListCommentReportsResponse { comments };
848
849     context.chat_server().do_send(SendUserRoomMessage {
850       op: UserOperation::ListCommentReports,
851       response: res.clone(),
852       recipient_id: user.id,
853       websocket_id,
854     });
855
856     Ok(res)
857   }
858 }