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