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