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