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