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