]> Untitled Git - lemmy.git/blob - lemmy_api/src/comment.rs
Fix nginx config for local federation setup (#104)
[lemmy.git] / lemmy_api / src / comment.rs
1 use crate::{
2   check_community_ban,
3   get_post,
4   get_user_from_jwt,
5   get_user_from_jwt_opt,
6   is_mod_or_admin,
7   Perform,
8 };
9 use actix_web::web::Data;
10 use lemmy_apub::{ApubLikeableType, ApubObjectType};
11 use lemmy_db::{
12   comment::*,
13   comment_view::*,
14   moderator::*,
15   post::*,
16   site_view::*,
17   user::*,
18   Crud,
19   Likeable,
20   ListingType,
21   Saveable,
22   SortType,
23 };
24 use lemmy_structs::{blocking, comment::*, send_local_notifs};
25 use lemmy_utils::{
26   apub::{make_apub_endpoint, EndpointType},
27   utils::{remove_slurs, scrape_text_for_mentions},
28   APIError,
29   ConnectionId,
30   LemmyError,
31 };
32 use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
33 use std::str::FromStr;
34
35 #[async_trait::async_trait(?Send)]
36 impl Perform for CreateComment {
37   type Response = CommentResponse;
38
39   async fn perform(
40     &self,
41     context: &Data<LemmyContext>,
42     websocket_id: Option<ConnectionId>,
43   ) -> Result<CommentResponse, LemmyError> {
44     let data: &CreateComment = &self;
45     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
46
47     let content_slurs_removed = remove_slurs(&data.content.to_owned());
48
49     let comment_form = CommentForm {
50       content: content_slurs_removed,
51       parent_id: data.parent_id.to_owned(),
52       post_id: data.post_id,
53       creator_id: user.id,
54       removed: None,
55       deleted: None,
56       read: None,
57       published: None,
58       updated: None,
59       ap_id: None,
60       local: true,
61     };
62
63     // Check for a community ban
64     let post_id = data.post_id;
65     let post = get_post(post_id, context.pool()).await?;
66
67     check_community_ban(user.id, post.community_id, context.pool()).await?;
68
69     // Check if post is locked, no new comments
70     if post.locked {
71       return Err(APIError::err("locked").into());
72     }
73
74     // Create the comment
75     let comment_form2 = comment_form.clone();
76     let inserted_comment = match blocking(context.pool(), move |conn| {
77       Comment::create(&conn, &comment_form2)
78     })
79     .await?
80     {
81       Ok(comment) => comment,
82       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
83     };
84
85     // Necessary to update the ap_id
86     let inserted_comment_id = inserted_comment.id;
87     let updated_comment: Comment = match blocking(context.pool(), move |conn| {
88       let apub_id =
89         make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
90       Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
91     })
92     .await?
93     {
94       Ok(comment) => comment,
95       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
96     };
97
98     updated_comment.send_create(&user, context).await?;
99
100     // Scan the comment for user mentions, add those rows
101     let mentions = scrape_text_for_mentions(&comment_form.content);
102     let recipient_ids = send_local_notifs(
103       mentions,
104       updated_comment.clone(),
105       &user,
106       post,
107       context.pool(),
108       true,
109     )
110     .await?;
111
112     // You like your own comment by default
113     let like_form = CommentLikeForm {
114       comment_id: inserted_comment.id,
115       post_id: data.post_id,
116       user_id: user.id,
117       score: 1,
118     };
119
120     let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
121     if blocking(context.pool(), like).await?.is_err() {
122       return Err(APIError::err("couldnt_like_comment").into());
123     }
124
125     updated_comment.send_like(&user, context).await?;
126
127     let user_id = user.id;
128     let comment_view = blocking(context.pool(), move |conn| {
129       CommentView::read(&conn, inserted_comment.id, Some(user_id))
130     })
131     .await??;
132
133     let mut res = CommentResponse {
134       comment: comment_view,
135       recipient_ids,
136       form_id: data.form_id.to_owned(),
137     };
138
139     context.chat_server().do_send(SendComment {
140       op: UserOperation::CreateComment,
141       comment: res.clone(),
142       websocket_id,
143     });
144
145     // strip out the recipient_ids, so that
146     // users don't get double notifs
147     res.recipient_ids = Vec::new();
148
149     Ok(res)
150   }
151 }
152
153 #[async_trait::async_trait(?Send)]
154 impl Perform for EditComment {
155   type Response = CommentResponse;
156
157   async fn perform(
158     &self,
159     context: &Data<LemmyContext>,
160     websocket_id: Option<ConnectionId>,
161   ) -> Result<CommentResponse, LemmyError> {
162     let data: &EditComment = &self;
163     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
164
165     let edit_id = data.edit_id;
166     let orig_comment = blocking(context.pool(), move |conn| {
167       CommentView::read(&conn, edit_id, None)
168     })
169     .await??;
170
171     check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
172
173     // Verify that only the creator can edit
174     if user.id != orig_comment.creator_id {
175       return Err(APIError::err("no_comment_edit_allowed").into());
176     }
177
178     // Do the update
179     let content_slurs_removed = remove_slurs(&data.content.to_owned());
180     let edit_id = data.edit_id;
181     let updated_comment = match blocking(context.pool(), move |conn| {
182       Comment::update_content(conn, edit_id, &content_slurs_removed)
183     })
184     .await?
185     {
186       Ok(comment) => comment,
187       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
188     };
189
190     // Send the apub update
191     updated_comment.send_update(&user, context).await?;
192
193     // Do the mentions / recipients
194     let post_id = orig_comment.post_id;
195     let post = get_post(post_id, context.pool()).await?;
196
197     let updated_comment_content = updated_comment.content.to_owned();
198     let mentions = scrape_text_for_mentions(&updated_comment_content);
199     let recipient_ids = send_local_notifs(
200       mentions,
201       updated_comment,
202       &user,
203       post,
204       context.pool(),
205       false,
206     )
207     .await?;
208
209     let edit_id = data.edit_id;
210     let user_id = user.id;
211     let comment_view = blocking(context.pool(), move |conn| {
212       CommentView::read(conn, edit_id, Some(user_id))
213     })
214     .await??;
215
216     let mut res = CommentResponse {
217       comment: comment_view,
218       recipient_ids,
219       form_id: data.form_id.to_owned(),
220     };
221
222     context.chat_server().do_send(SendComment {
223       op: UserOperation::EditComment,
224       comment: res.clone(),
225       websocket_id,
226     });
227
228     // strip out the recipient_ids, so that
229     // users don't get double notifs
230     res.recipient_ids = Vec::new();
231
232     Ok(res)
233   }
234 }
235
236 #[async_trait::async_trait(?Send)]
237 impl Perform for DeleteComment {
238   type Response = CommentResponse;
239
240   async fn perform(
241     &self,
242     context: &Data<LemmyContext>,
243     websocket_id: Option<ConnectionId>,
244   ) -> Result<CommentResponse, LemmyError> {
245     let data: &DeleteComment = &self;
246     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
247
248     let edit_id = data.edit_id;
249     let orig_comment = blocking(context.pool(), move |conn| {
250       CommentView::read(&conn, edit_id, None)
251     })
252     .await??;
253
254     check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
255
256     // Verify that only the creator can delete
257     if user.id != orig_comment.creator_id {
258       return Err(APIError::err("no_comment_edit_allowed").into());
259     }
260
261     // Do the delete
262     let deleted = data.deleted;
263     let updated_comment = match blocking(context.pool(), move |conn| {
264       Comment::update_deleted(conn, edit_id, deleted)
265     })
266     .await?
267     {
268       Ok(comment) => comment,
269       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
270     };
271
272     // Send the apub message
273     if deleted {
274       updated_comment.send_delete(&user, context).await?;
275     } else {
276       updated_comment.send_undo_delete(&user, context).await?;
277     }
278
279     // Refetch it
280     let edit_id = data.edit_id;
281     let user_id = user.id;
282     let comment_view = blocking(context.pool(), move |conn| {
283       CommentView::read(conn, edit_id, Some(user_id))
284     })
285     .await??;
286
287     // Build the recipients
288     let post_id = comment_view.post_id;
289     let post = get_post(post_id, context.pool()).await?;
290     let mentions = vec![];
291     let recipient_ids = send_local_notifs(
292       mentions,
293       updated_comment,
294       &user,
295       post,
296       context.pool(),
297       false,
298     )
299     .await?;
300
301     let mut res = CommentResponse {
302       comment: comment_view,
303       recipient_ids,
304       form_id: None,
305     };
306
307     context.chat_server().do_send(SendComment {
308       op: UserOperation::DeleteComment,
309       comment: res.clone(),
310       websocket_id,
311     });
312
313     // strip out the recipient_ids, so that
314     // users don't get double notifs
315     res.recipient_ids = Vec::new();
316
317     Ok(res)
318   }
319 }
320
321 #[async_trait::async_trait(?Send)]
322 impl Perform for RemoveComment {
323   type Response = CommentResponse;
324
325   async fn perform(
326     &self,
327     context: &Data<LemmyContext>,
328     websocket_id: Option<ConnectionId>,
329   ) -> Result<CommentResponse, LemmyError> {
330     let data: &RemoveComment = &self;
331     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
332
333     let edit_id = data.edit_id;
334     let orig_comment = blocking(context.pool(), move |conn| {
335       CommentView::read(&conn, edit_id, None)
336     })
337     .await??;
338
339     check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
340
341     // Verify that only a mod or admin can remove
342     is_mod_or_admin(context.pool(), user.id, orig_comment.community_id).await?;
343
344     // Do the remove
345     let removed = data.removed;
346     let updated_comment = match blocking(context.pool(), move |conn| {
347       Comment::update_removed(conn, edit_id, removed)
348     })
349     .await?
350     {
351       Ok(comment) => comment,
352       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
353     };
354
355     // Mod tables
356     let form = ModRemoveCommentForm {
357       mod_user_id: user.id,
358       comment_id: data.edit_id,
359       removed: Some(removed),
360       reason: data.reason.to_owned(),
361     };
362     blocking(context.pool(), move |conn| {
363       ModRemoveComment::create(conn, &form)
364     })
365     .await??;
366
367     // Send the apub message
368     if removed {
369       updated_comment.send_remove(&user, context).await?;
370     } else {
371       updated_comment.send_undo_remove(&user, context).await?;
372     }
373
374     // Refetch it
375     let edit_id = data.edit_id;
376     let user_id = user.id;
377     let comment_view = blocking(context.pool(), move |conn| {
378       CommentView::read(conn, edit_id, Some(user_id))
379     })
380     .await??;
381
382     // Build the recipients
383     let post_id = comment_view.post_id;
384     let post = get_post(post_id, context.pool()).await?;
385     let mentions = vec![];
386     let recipient_ids = send_local_notifs(
387       mentions,
388       updated_comment,
389       &user,
390       post,
391       context.pool(),
392       false,
393     )
394     .await?;
395
396     let mut res = CommentResponse {
397       comment: comment_view,
398       recipient_ids,
399       form_id: None,
400     };
401
402     context.chat_server().do_send(SendComment {
403       op: UserOperation::RemoveComment,
404       comment: res.clone(),
405       websocket_id,
406     });
407
408     // strip out the recipient_ids, so that
409     // users don't get double notifs
410     res.recipient_ids = Vec::new();
411
412     Ok(res)
413   }
414 }
415
416 #[async_trait::async_trait(?Send)]
417 impl Perform for MarkCommentAsRead {
418   type Response = CommentResponse;
419
420   async fn perform(
421     &self,
422     context: &Data<LemmyContext>,
423     _websocket_id: Option<ConnectionId>,
424   ) -> Result<CommentResponse, LemmyError> {
425     let data: &MarkCommentAsRead = &self;
426     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
427
428     let edit_id = data.edit_id;
429     let orig_comment = blocking(context.pool(), move |conn| {
430       CommentView::read(&conn, edit_id, None)
431     })
432     .await??;
433
434     check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
435
436     // Verify that only the recipient can mark as read
437     // Needs to fetch the parent comment / post to get the recipient
438     let parent_id = orig_comment.parent_id;
439     match parent_id {
440       Some(pid) => {
441         let parent_comment = blocking(context.pool(), move |conn| {
442           CommentView::read(&conn, pid, None)
443         })
444         .await??;
445         if user.id != parent_comment.creator_id {
446           return Err(APIError::err("no_comment_edit_allowed").into());
447         }
448       }
449       None => {
450         let parent_post_id = orig_comment.post_id;
451         let parent_post =
452           blocking(context.pool(), move |conn| Post::read(conn, parent_post_id)).await??;
453         if user.id != parent_post.creator_id {
454           return Err(APIError::err("no_comment_edit_allowed").into());
455         }
456       }
457     }
458
459     // Do the mark as read
460     let read = data.read;
461     match blocking(context.pool(), move |conn| {
462       Comment::update_read(conn, edit_id, read)
463     })
464     .await?
465     {
466       Ok(comment) => comment,
467       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
468     };
469
470     // Refetch it
471     let edit_id = data.edit_id;
472     let user_id = user.id;
473     let comment_view = blocking(context.pool(), move |conn| {
474       CommentView::read(conn, edit_id, Some(user_id))
475     })
476     .await??;
477
478     let res = CommentResponse {
479       comment: comment_view,
480       recipient_ids: Vec::new(),
481       form_id: None,
482     };
483
484     Ok(res)
485   }
486 }
487
488 #[async_trait::async_trait(?Send)]
489 impl Perform for SaveComment {
490   type Response = CommentResponse;
491
492   async fn perform(
493     &self,
494     context: &Data<LemmyContext>,
495     _websocket_id: Option<ConnectionId>,
496   ) -> Result<CommentResponse, LemmyError> {
497     let data: &SaveComment = &self;
498     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
499
500     let comment_saved_form = CommentSavedForm {
501       comment_id: data.comment_id,
502       user_id: user.id,
503     };
504
505     if data.save {
506       let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
507       if blocking(context.pool(), save_comment).await?.is_err() {
508         return Err(APIError::err("couldnt_save_comment").into());
509       }
510     } else {
511       let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
512       if blocking(context.pool(), unsave_comment).await?.is_err() {
513         return Err(APIError::err("couldnt_save_comment").into());
514       }
515     }
516
517     let comment_id = data.comment_id;
518     let user_id = user.id;
519     let comment_view = blocking(context.pool(), move |conn| {
520       CommentView::read(conn, comment_id, Some(user_id))
521     })
522     .await??;
523
524     Ok(CommentResponse {
525       comment: comment_view,
526       recipient_ids: Vec::new(),
527       form_id: None,
528     })
529   }
530 }
531
532 #[async_trait::async_trait(?Send)]
533 impl Perform for CreateCommentLike {
534   type Response = CommentResponse;
535
536   async fn perform(
537     &self,
538     context: &Data<LemmyContext>,
539     websocket_id: Option<ConnectionId>,
540   ) -> Result<CommentResponse, LemmyError> {
541     let data: &CreateCommentLike = &self;
542     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
543
544     let mut recipient_ids = Vec::new();
545
546     // Don't do a downvote if site has downvotes disabled
547     if data.score == -1 {
548       let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
549       if !site.enable_downvotes {
550         return Err(APIError::err("downvotes_disabled").into());
551       }
552     }
553
554     let comment_id = data.comment_id;
555     let orig_comment = blocking(context.pool(), move |conn| {
556       CommentView::read(&conn, comment_id, None)
557     })
558     .await??;
559
560     let post_id = orig_comment.post_id;
561     let post = get_post(post_id, context.pool()).await?;
562     check_community_ban(user.id, post.community_id, context.pool()).await?;
563
564     let comment_id = data.comment_id;
565     let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
566
567     // Add to recipient ids
568     match comment.parent_id {
569       Some(parent_id) => {
570         let parent_comment =
571           blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await??;
572         if parent_comment.creator_id != user.id {
573           let parent_user = blocking(context.pool(), move |conn| {
574             User_::read(conn, parent_comment.creator_id)
575           })
576           .await??;
577           recipient_ids.push(parent_user.id);
578         }
579       }
580       None => {
581         recipient_ids.push(post.creator_id);
582       }
583     }
584
585     let like_form = CommentLikeForm {
586       comment_id: data.comment_id,
587       post_id,
588       user_id: user.id,
589       score: data.score,
590     };
591
592     // Remove any likes first
593     let user_id = user.id;
594     blocking(context.pool(), move |conn| {
595       CommentLike::remove(conn, user_id, comment_id)
596     })
597     .await??;
598
599     // Only add the like if the score isnt 0
600     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
601     if do_add {
602       let like_form2 = like_form.clone();
603       let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
604       if blocking(context.pool(), like).await?.is_err() {
605         return Err(APIError::err("couldnt_like_comment").into());
606       }
607
608       if like_form.score == 1 {
609         comment.send_like(&user, context).await?;
610       } else if like_form.score == -1 {
611         comment.send_dislike(&user, context).await?;
612       }
613     } else {
614       comment.send_undo_like(&user, context).await?;
615     }
616
617     // Have to refetch the comment to get the current state
618     let comment_id = data.comment_id;
619     let user_id = user.id;
620     let liked_comment = blocking(context.pool(), move |conn| {
621       CommentView::read(conn, comment_id, Some(user_id))
622     })
623     .await??;
624
625     let mut res = CommentResponse {
626       comment: liked_comment,
627       recipient_ids,
628       form_id: None,
629     };
630
631     context.chat_server().do_send(SendComment {
632       op: UserOperation::CreateCommentLike,
633       comment: res.clone(),
634       websocket_id,
635     });
636
637     // strip out the recipient_ids, so that
638     // users don't get double notifs
639     res.recipient_ids = Vec::new();
640
641     Ok(res)
642   }
643 }
644
645 #[async_trait::async_trait(?Send)]
646 impl Perform for GetComments {
647   type Response = GetCommentsResponse;
648
649   async fn perform(
650     &self,
651     context: &Data<LemmyContext>,
652     _websocket_id: Option<ConnectionId>,
653   ) -> Result<GetCommentsResponse, LemmyError> {
654     let data: &GetComments = &self;
655     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
656     let user_id = user.map(|u| u.id);
657
658     let type_ = ListingType::from_str(&data.type_)?;
659     let sort = SortType::from_str(&data.sort)?;
660
661     let community_id = data.community_id;
662     let community_name = data.community_name.to_owned();
663     let page = data.page;
664     let limit = data.limit;
665     let comments = blocking(context.pool(), move |conn| {
666       CommentQueryBuilder::create(conn)
667         .listing_type(type_)
668         .sort(&sort)
669         .for_community_id(community_id)
670         .for_community_name(community_name)
671         .my_user_id(user_id)
672         .page(page)
673         .limit(limit)
674         .list()
675     })
676     .await?;
677     let comments = match comments {
678       Ok(comments) => comments,
679       Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
680     };
681
682     Ok(GetCommentsResponse { comments })
683   }
684 }