]> Untitled Git - lemmy.git/blob - server/src/api/comment.rs
Merge branch 'admin_settings' into dev
[lemmy.git] / server / src / api / comment.rs
1 use super::*;
2 use crate::send_email;
3 use crate::settings::Settings;
4 use diesel::PgConnection;
5 use log::error;
6 use std::str::FromStr;
7
8 #[derive(Serialize, Deserialize)]
9 pub struct CreateComment {
10   content: String,
11   parent_id: Option<i32>,
12   edit_id: Option<i32>, // TODO this isn't used
13   pub post_id: i32,
14   auth: String,
15 }
16
17 #[derive(Serialize, Deserialize)]
18 pub struct EditComment {
19   content: String,
20   parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
21   edit_id: i32,
22   creator_id: i32,
23   pub post_id: i32,
24   removed: Option<bool>,
25   deleted: Option<bool>,
26   reason: Option<String>,
27   read: Option<bool>,
28   auth: String,
29 }
30
31 #[derive(Serialize, Deserialize)]
32 pub struct SaveComment {
33   comment_id: i32,
34   save: bool,
35   auth: String,
36 }
37
38 #[derive(Serialize, Deserialize, Clone)]
39 pub struct CommentResponse {
40   pub comment: CommentView,
41   pub recipient_ids: Vec<i32>,
42 }
43
44 #[derive(Serialize, Deserialize)]
45 pub struct CreateCommentLike {
46   comment_id: i32,
47   pub post_id: i32,
48   score: i16,
49   auth: String,
50 }
51
52 #[derive(Serialize, Deserialize)]
53 pub struct GetComments {
54   type_: String,
55   sort: String,
56   page: Option<i64>,
57   limit: Option<i64>,
58   pub community_id: Option<i32>,
59   auth: Option<String>,
60 }
61
62 #[derive(Serialize, Deserialize)]
63 pub struct GetCommentsResponse {
64   comments: Vec<CommentView>,
65 }
66
67 impl Perform<CommentResponse> for Oper<CreateComment> {
68   fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
69     let data: &CreateComment = &self.data;
70
71     let claims = match Claims::decode(&data.auth) {
72       Ok(claims) => claims.claims,
73       Err(_e) => return Err(APIError::err("not_logged_in").into()),
74     };
75
76     let user_id = claims.id;
77
78     let hostname = &format!("https://{}", Settings::get().hostname);
79
80     // Check for a community ban
81     let post = Post::read(&conn, data.post_id)?;
82     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
83       return Err(APIError::err("community_ban").into());
84     }
85
86     // Check for a site ban
87     if UserView::read(&conn, user_id)?.banned {
88       return Err(APIError::err("site_ban").into());
89     }
90
91     let content_slurs_removed = remove_slurs(&data.content.to_owned());
92
93     let comment_form = CommentForm {
94       content: content_slurs_removed,
95       parent_id: data.parent_id.to_owned(),
96       post_id: data.post_id,
97       creator_id: user_id,
98       removed: None,
99       deleted: None,
100       read: None,
101       updated: None,
102     };
103
104     let inserted_comment = match Comment::create(&conn, &comment_form) {
105       Ok(comment) => comment,
106       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
107     };
108
109     let mut recipient_ids = Vec::new();
110
111     // Scan the comment for user mentions, add those rows
112     let extracted_usernames = extract_usernames(&comment_form.content);
113
114     for username_mention in &extracted_usernames {
115       if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
116         // You can't mention yourself
117         // At some point, make it so you can't tag the parent creator either
118         // This can cause two notifications, one for reply and the other for mention
119         if mention_user.id != user_id {
120           recipient_ids.push(mention_user.id);
121
122           let user_mention_form = UserMentionForm {
123             recipient_id: mention_user.id,
124             comment_id: inserted_comment.id,
125             read: None,
126           };
127
128           // Allow this to fail softly, since comment edits might re-update or replace it
129           // Let the uniqueness handle this fail
130           match UserMention::create(&conn, &user_mention_form) {
131             Ok(_mention) => (),
132             Err(_e) => error!("{}", &_e),
133           };
134
135           // Send an email to those users that have notifications on
136           if mention_user.send_notifications_to_email {
137             if let Some(mention_email) = mention_user.email {
138               let subject = &format!(
139                 "{} - Mentioned by {}",
140                 Settings::get().hostname,
141                 claims.username
142               );
143               let html = &format!(
144                 "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
145                 claims.username, comment_form.content, hostname
146               );
147               match send_email(subject, &mention_email, &mention_user.name, html) {
148                 Ok(_o) => _o,
149                 Err(e) => error!("{}", e),
150               };
151             }
152           }
153         }
154       }
155     }
156
157     // Send notifs to the parent commenter / poster
158     match data.parent_id {
159       Some(parent_id) => {
160         let parent_comment = Comment::read(&conn, parent_id)?;
161         if parent_comment.creator_id != user_id {
162           let parent_user = User_::read(&conn, parent_comment.creator_id)?;
163           recipient_ids.push(parent_user.id);
164
165           if parent_user.send_notifications_to_email {
166             if let Some(comment_reply_email) = parent_user.email {
167               let subject = &format!(
168                 "{} - Reply from {}",
169                 Settings::get().hostname,
170                 claims.username
171               );
172               let html = &format!(
173                 "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
174                 claims.username, comment_form.content, hostname
175               );
176               match send_email(subject, &comment_reply_email, &parent_user.name, html) {
177                 Ok(_o) => _o,
178                 Err(e) => error!("{}", e),
179               };
180             }
181           }
182         }
183       }
184       // Its a post
185       None => {
186         if post.creator_id != user_id {
187           let parent_user = User_::read(&conn, post.creator_id)?;
188           recipient_ids.push(parent_user.id);
189
190           if parent_user.send_notifications_to_email {
191             if let Some(post_reply_email) = parent_user.email {
192               let subject = &format!(
193                 "{} - Reply from {}",
194                 Settings::get().hostname,
195                 claims.username
196               );
197               let html = &format!(
198                 "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
199                 claims.username, comment_form.content, hostname
200               );
201               match send_email(subject, &post_reply_email, &parent_user.name, html) {
202                 Ok(_o) => _o,
203                 Err(e) => error!("{}", e),
204               };
205             }
206           }
207         }
208       }
209     };
210
211     // You like your own comment by default
212     let like_form = CommentLikeForm {
213       comment_id: inserted_comment.id,
214       post_id: data.post_id,
215       user_id,
216       score: 1,
217     };
218
219     let _inserted_like = match CommentLike::like(&conn, &like_form) {
220       Ok(like) => like,
221       Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
222     };
223
224     let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
225
226     Ok(CommentResponse {
227       comment: comment_view,
228       recipient_ids,
229     })
230   }
231 }
232
233 impl Perform<CommentResponse> for Oper<EditComment> {
234   fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
235     let data: &EditComment = &self.data;
236
237     let claims = match Claims::decode(&data.auth) {
238       Ok(claims) => claims.claims,
239       Err(_e) => return Err(APIError::err("not_logged_in").into()),
240     };
241
242     let user_id = claims.id;
243
244     let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
245
246     // You are allowed to mark the comment as read even if you're banned.
247     if data.read.is_none() {
248       // Verify its the creator or a mod, or an admin
249       let mut editors: Vec<i32> = vec![data.creator_id];
250       editors.append(
251         &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)?
252           .into_iter()
253           .map(|m| m.user_id)
254           .collect(),
255       );
256       editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
257
258       if !editors.contains(&user_id) {
259         return Err(APIError::err("no_comment_edit_allowed").into());
260       }
261
262       // Check for a community ban
263       if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
264         return Err(APIError::err("community_ban").into());
265       }
266
267       // Check for a site ban
268       if UserView::read(&conn, user_id)?.banned {
269         return Err(APIError::err("site_ban").into());
270       }
271     }
272
273     let content_slurs_removed = remove_slurs(&data.content.to_owned());
274
275     let comment_form = CommentForm {
276       content: content_slurs_removed,
277       parent_id: data.parent_id,
278       post_id: data.post_id,
279       creator_id: data.creator_id,
280       removed: data.removed.to_owned(),
281       deleted: data.deleted.to_owned(),
282       read: data.read.to_owned(),
283       updated: if data.read.is_some() {
284         orig_comment.updated
285       } else {
286         Some(naive_now())
287       },
288     };
289
290     let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
291       Ok(comment) => comment,
292       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
293     };
294
295     let mut recipient_ids = Vec::new();
296
297     // Scan the comment for user mentions, add those rows
298     let extracted_usernames = extract_usernames(&comment_form.content);
299
300     for username_mention in &extracted_usernames {
301       let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
302
303       if mention_user.is_ok() {
304         let mention_user_id = mention_user?.id;
305
306         // You can't mention yourself
307         // At some point, make it so you can't tag the parent creator either
308         // This can cause two notifications, one for reply and the other for mention
309         if mention_user_id != user_id {
310           recipient_ids.push(mention_user_id);
311
312           let user_mention_form = UserMentionForm {
313             recipient_id: mention_user_id,
314             comment_id: data.edit_id,
315             read: None,
316           };
317
318           // Allow this to fail softly, since comment edits might re-update or replace it
319           // Let the uniqueness handle this fail
320           match UserMention::create(&conn, &user_mention_form) {
321             Ok(_mention) => (),
322             Err(_e) => error!("{}", &_e),
323           }
324         }
325       }
326     }
327
328     // Add to recipient ids
329     match data.parent_id {
330       Some(parent_id) => {
331         let parent_comment = Comment::read(&conn, parent_id)?;
332         if parent_comment.creator_id != user_id {
333           let parent_user = User_::read(&conn, parent_comment.creator_id)?;
334           recipient_ids.push(parent_user.id);
335         }
336       }
337       None => {
338         let post = Post::read(&conn, data.post_id)?;
339         recipient_ids.push(post.creator_id);
340       }
341     }
342
343     // Mod tables
344     if let Some(removed) = data.removed.to_owned() {
345       let form = ModRemoveCommentForm {
346         mod_user_id: user_id,
347         comment_id: data.edit_id,
348         removed: Some(removed),
349         reason: data.reason.to_owned(),
350       };
351       ModRemoveComment::create(&conn, &form)?;
352     }
353
354     let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
355
356     Ok(CommentResponse {
357       comment: comment_view,
358       recipient_ids,
359     })
360   }
361 }
362
363 impl Perform<CommentResponse> for Oper<SaveComment> {
364   fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
365     let data: &SaveComment = &self.data;
366
367     let claims = match Claims::decode(&data.auth) {
368       Ok(claims) => claims.claims,
369       Err(_e) => return Err(APIError::err("not_logged_in").into()),
370     };
371
372     let user_id = claims.id;
373
374     let comment_saved_form = CommentSavedForm {
375       comment_id: data.comment_id,
376       user_id,
377     };
378
379     if data.save {
380       match CommentSaved::save(&conn, &comment_saved_form) {
381         Ok(comment) => comment,
382         Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
383       };
384     } else {
385       match CommentSaved::unsave(&conn, &comment_saved_form) {
386         Ok(comment) => comment,
387         Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
388       };
389     }
390
391     let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
392
393     Ok(CommentResponse {
394       comment: comment_view,
395       recipient_ids: Vec::new(),
396     })
397   }
398 }
399
400 impl Perform<CommentResponse> for Oper<CreateCommentLike> {
401   fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
402     let data: &CreateCommentLike = &self.data;
403
404     let claims = match Claims::decode(&data.auth) {
405       Ok(claims) => claims.claims,
406       Err(_e) => return Err(APIError::err("not_logged_in").into()),
407     };
408
409     let user_id = claims.id;
410
411     let mut recipient_ids = Vec::new();
412
413     // Don't do a downvote if site has downvotes disabled
414     if data.score == -1 {
415       let site = SiteView::read(&conn)?;
416       if !site.enable_downvotes {
417         return Err(APIError::err("downvotes_disabled").into());
418       }
419     }
420
421     // Check for a community ban
422     let post = Post::read(&conn, data.post_id)?;
423     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
424       return Err(APIError::err("community_ban").into());
425     }
426
427     // Check for a site ban
428     if UserView::read(&conn, user_id)?.banned {
429       return Err(APIError::err("site_ban").into());
430     }
431
432     let comment = Comment::read(&conn, data.comment_id)?;
433
434     // Add to recipient ids
435     match comment.parent_id {
436       Some(parent_id) => {
437         let parent_comment = Comment::read(&conn, parent_id)?;
438         if parent_comment.creator_id != user_id {
439           let parent_user = User_::read(&conn, parent_comment.creator_id)?;
440           recipient_ids.push(parent_user.id);
441         }
442       }
443       None => {
444         recipient_ids.push(post.creator_id);
445       }
446     }
447
448     let like_form = CommentLikeForm {
449       comment_id: data.comment_id,
450       post_id: data.post_id,
451       user_id,
452       score: data.score,
453     };
454
455     // Remove any likes first
456     CommentLike::remove(&conn, &like_form)?;
457
458     // Only add the like if the score isnt 0
459     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
460     if do_add {
461       let _inserted_like = match CommentLike::like(&conn, &like_form) {
462         Ok(like) => like,
463         Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
464       };
465     }
466
467     // Have to refetch the comment to get the current state
468     let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
469
470     Ok(CommentResponse {
471       comment: liked_comment,
472       recipient_ids,
473     })
474   }
475 }
476
477 impl Perform<GetCommentsResponse> for Oper<GetComments> {
478   fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
479     let data: &GetComments = &self.data;
480
481     let user_claims: Option<Claims> = match &data.auth {
482       Some(auth) => match Claims::decode(&auth) {
483         Ok(claims) => Some(claims.claims),
484         Err(_e) => None,
485       },
486       None => None,
487     };
488
489     let user_id = match &user_claims {
490       Some(claims) => Some(claims.id),
491       None => None,
492     };
493
494     let type_ = ListingType::from_str(&data.type_)?;
495     let sort = SortType::from_str(&data.sort)?;
496
497     let comments = match CommentQueryBuilder::create(&conn)
498       .listing_type(type_)
499       .sort(&sort)
500       .for_community_id(data.community_id)
501       .my_user_id(user_id)
502       .page(data.page)
503       .limit(data.limit)
504       .list()
505     {
506       Ok(comments) => comments,
507       Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
508     };
509
510     Ok(GetCommentsResponse { comments })
511   }
512 }