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