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