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