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