3 use crate::settings::Settings;
4 use diesel::PgConnection;
7 #[derive(Serialize, Deserialize)]
8 pub struct CreateComment {
10 parent_id: Option<i32>,
11 edit_id: Option<i32>, // TODO this isn't used
16 #[derive(Serialize, Deserialize)]
17 pub struct EditComment {
19 parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
23 removed: Option<bool>,
24 deleted: Option<bool>,
25 reason: Option<String>,
30 #[derive(Serialize, Deserialize)]
31 pub struct SaveComment {
37 #[derive(Serialize, Deserialize, Clone)]
38 pub struct CommentResponse {
39 pub comment: CommentView,
40 pub recipient_ids: Vec<i32>,
43 #[derive(Serialize, Deserialize)]
44 pub struct CreateCommentLike {
51 #[derive(Serialize, Deserialize)]
52 pub struct GetComments {
57 pub community_id: Option<i32>,
61 #[derive(Serialize, Deserialize)]
62 pub struct GetCommentsResponse {
63 comments: Vec<CommentView>,
66 impl Perform<CommentResponse> for Oper<CreateComment> {
67 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
68 let data: &CreateComment = &self.data;
70 let claims = match Claims::decode(&data.auth) {
71 Ok(claims) => claims.claims,
72 Err(_e) => return Err(APIError::err("not_logged_in").into()),
75 let user_id = claims.id;
77 let hostname = &format!("https://{}", Settings::get().hostname);
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());
85 // Check for a site ban
86 if UserView::read(&conn, user_id)?.banned {
87 return Err(APIError::err("site_ban").into());
90 let content_slurs_removed = remove_slurs(&data.content.to_owned());
92 let comment_form = CommentForm {
93 content: content_slurs_removed,
94 parent_id: data.parent_id.to_owned(),
95 post_id: data.post_id,
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()),
108 let mut recipient_ids = Vec::new();
110 // Scan the comment for user mentions, add those rows
111 let extracted_usernames = extract_usernames(&comment_form.content);
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);
121 let user_mention_form = UserMentionForm {
122 recipient_id: mention_user.id,
123 comment_id: inserted_comment.id,
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) {
131 Err(_e) => eprintln!("{}", &_e),
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,
143 "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
144 claims.username, comment_form.content, hostname
146 match send_email(subject, &mention_email, &mention_user.name, html) {
148 Err(e) => eprintln!("{}", e),
156 // Send notifs to the parent commenter / poster
157 match data.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);
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,
172 "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
173 claims.username, comment_form.content, hostname
175 match send_email(subject, &comment_reply_email, &parent_user.name, html) {
177 Err(e) => eprintln!("{}", e),
185 if post.creator_id != user_id {
186 let parent_user = User_::read(&conn, post.creator_id)?;
187 recipient_ids.push(parent_user.id);
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,
197 "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
198 claims.username, comment_form.content, hostname
200 match send_email(subject, &post_reply_email, &parent_user.name, html) {
202 Err(e) => eprintln!("{}", e),
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,
218 let _inserted_like = match CommentLike::like(&conn, &like_form) {
220 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
223 let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
226 comment: comment_view,
232 impl Perform<CommentResponse> for Oper<EditComment> {
233 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
234 let data: &EditComment = &self.data;
236 let claims = match Claims::decode(&data.auth) {
237 Ok(claims) => claims.claims,
238 Err(_e) => return Err(APIError::err("not_logged_in").into()),
241 let user_id = claims.id;
243 let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
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];
250 &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)?
255 editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
257 if !editors.contains(&user_id) {
258 return Err(APIError::err("no_comment_edit_allowed").into());
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());
266 // Check for a site ban
267 if UserView::read(&conn, user_id)?.banned {
268 return Err(APIError::err("site_ban").into());
272 let content_slurs_removed = remove_slurs(&data.content.to_owned());
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() {
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()),
294 let mut recipient_ids = Vec::new();
296 // Scan the comment for user mentions, add those rows
297 let extracted_usernames = extract_usernames(&comment_form.content);
299 for username_mention in &extracted_usernames {
300 let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
302 if mention_user.is_ok() {
303 let mention_user_id = mention_user?.id;
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);
311 let user_mention_form = UserMentionForm {
312 recipient_id: mention_user_id,
313 comment_id: data.edit_id,
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) {
321 Err(_e) => eprintln!("{}", &_e),
327 // Add to recipient ids
328 match data.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);
337 let post = Post::read(&conn, data.post_id)?;
338 recipient_ids.push(post.creator_id);
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(),
350 ModRemoveComment::create(&conn, &form)?;
353 let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
356 comment: comment_view,
362 impl Perform<CommentResponse> for Oper<SaveComment> {
363 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
364 let data: &SaveComment = &self.data;
366 let claims = match Claims::decode(&data.auth) {
367 Ok(claims) => claims.claims,
368 Err(_e) => return Err(APIError::err("not_logged_in").into()),
371 let user_id = claims.id;
373 let comment_saved_form = CommentSavedForm {
374 comment_id: data.comment_id,
379 match CommentSaved::save(&conn, &comment_saved_form) {
380 Ok(comment) => comment,
381 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
384 match CommentSaved::unsave(&conn, &comment_saved_form) {
385 Ok(comment) => comment,
386 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
390 let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
393 comment: comment_view,
394 recipient_ids: Vec::new(),
399 impl Perform<CommentResponse> for Oper<CreateCommentLike> {
400 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
401 let data: &CreateCommentLike = &self.data;
403 let claims = match Claims::decode(&data.auth) {
404 Ok(claims) => claims.claims,
405 Err(_e) => return Err(APIError::err("not_logged_in").into()),
408 let user_id = claims.id;
410 let mut recipient_ids = Vec::new();
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());
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());
426 // Check for a site ban
427 if UserView::read(&conn, user_id)?.banned {
428 return Err(APIError::err("site_ban").into());
431 let comment = Comment::read(&conn, data.comment_id)?;
433 // Add to recipient ids
434 match comment.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);
443 recipient_ids.push(post.creator_id);
447 let like_form = CommentLikeForm {
448 comment_id: data.comment_id,
449 post_id: data.post_id,
454 // Remove any likes first
455 CommentLike::remove(&conn, &like_form)?;
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);
460 let _inserted_like = match CommentLike::like(&conn, &like_form) {
462 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
466 // Have to refetch the comment to get the current state
467 let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
470 comment: liked_comment,
476 impl Perform<GetCommentsResponse> for Oper<GetComments> {
477 fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
478 let data: &GetComments = &self.data;
480 let user_claims: Option<Claims> = match &data.auth {
481 Some(auth) => match Claims::decode(&auth) {
482 Ok(claims) => Some(claims.claims),
488 let user_id = match &user_claims {
489 Some(claims) => Some(claims.id),
493 let type_ = ListingType::from_str(&data.type_)?;
494 let sort = SortType::from_str(&data.sort)?;
496 let comments = match CommentQueryBuilder::create(&conn)
499 .for_community_id(data.community_id)
505 Ok(comments) => comments,
506 Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
509 Ok(GetCommentsResponse { comments })