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