3 use crate::settings::Settings;
4 use diesel::PgConnection;
8 #[derive(Serialize, Deserialize)]
9 pub struct CreateComment {
11 parent_id: Option<i32>,
12 edit_id: Option<i32>, // TODO this isn't used
17 #[derive(Serialize, Deserialize)]
18 pub struct EditComment {
20 parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
24 removed: Option<bool>,
25 deleted: Option<bool>,
26 reason: Option<String>,
31 #[derive(Serialize, Deserialize)]
32 pub struct SaveComment {
38 #[derive(Serialize, Deserialize, Clone)]
39 pub struct CommentResponse {
40 pub comment: CommentView,
41 pub recipient_ids: Vec<i32>,
44 #[derive(Serialize, Deserialize)]
45 pub struct CreateCommentLike {
52 #[derive(Serialize, Deserialize)]
53 pub struct GetComments {
58 pub community_id: Option<i32>,
62 #[derive(Serialize, Deserialize)]
63 pub struct GetCommentsResponse {
64 comments: Vec<CommentView>,
67 impl Perform<CommentResponse> for Oper<CreateComment> {
68 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
69 let data: &CreateComment = &self.data;
71 let claims = match Claims::decode(&data.auth) {
72 Ok(claims) => claims.claims,
73 Err(_e) => return Err(APIError::err("not_logged_in").into()),
76 let user_id = claims.id;
78 let hostname = &format!("https://{}", Settings::get().hostname);
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());
86 // Check for a site ban
87 if UserView::read(&conn, user_id)?.banned {
88 return Err(APIError::err("site_ban").into());
91 let content_slurs_removed = remove_slurs(&data.content.to_owned());
93 let comment_form = CommentForm {
94 content: content_slurs_removed,
95 parent_id: data.parent_id.to_owned(),
96 post_id: data.post_id,
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()),
109 let mut recipient_ids = Vec::new();
111 // Scan the comment for user mentions, add those rows
112 let extracted_usernames = extract_usernames(&comment_form.content);
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);
122 let user_mention_form = UserMentionForm {
123 recipient_id: mention_user.id,
124 comment_id: inserted_comment.id,
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) {
132 Err(_e) => error!("{}", &_e),
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,
144 "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
145 claims.username, comment_form.content, hostname
147 match send_email(subject, &mention_email, &mention_user.name, html) {
149 Err(e) => error!("{}", e),
157 // Send notifs to the parent commenter / poster
158 match data.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);
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,
173 "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
174 claims.username, comment_form.content, hostname
176 match send_email(subject, &comment_reply_email, &parent_user.name, html) {
178 Err(e) => error!("{}", e),
186 if post.creator_id != user_id {
187 let parent_user = User_::read(&conn, post.creator_id)?;
188 recipient_ids.push(parent_user.id);
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,
198 "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
199 claims.username, comment_form.content, hostname
201 match send_email(subject, &post_reply_email, &parent_user.name, html) {
203 Err(e) => error!("{}", e),
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,
219 let _inserted_like = match CommentLike::like(&conn, &like_form) {
221 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
224 let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
227 comment: comment_view,
233 impl Perform<CommentResponse> for Oper<EditComment> {
234 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
235 let data: &EditComment = &self.data;
237 let claims = match Claims::decode(&data.auth) {
238 Ok(claims) => claims.claims,
239 Err(_e) => return Err(APIError::err("not_logged_in").into()),
242 let user_id = claims.id;
244 let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
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];
251 &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)?
256 editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
258 if !editors.contains(&user_id) {
259 return Err(APIError::err("no_comment_edit_allowed").into());
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());
267 // Check for a site ban
268 if UserView::read(&conn, user_id)?.banned {
269 return Err(APIError::err("site_ban").into());
273 let content_slurs_removed = remove_slurs(&data.content.to_owned());
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() {
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()),
295 let mut recipient_ids = Vec::new();
297 // Scan the comment for user mentions, add those rows
298 let extracted_usernames = extract_usernames(&comment_form.content);
300 for username_mention in &extracted_usernames {
301 let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
303 if mention_user.is_ok() {
304 let mention_user_id = mention_user?.id;
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);
312 let user_mention_form = UserMentionForm {
313 recipient_id: mention_user_id,
314 comment_id: data.edit_id,
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) {
322 Err(_e) => error!("{}", &_e),
328 // Add to recipient ids
329 match data.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);
338 let post = Post::read(&conn, data.post_id)?;
339 recipient_ids.push(post.creator_id);
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(),
351 ModRemoveComment::create(&conn, &form)?;
354 let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
357 comment: comment_view,
363 impl Perform<CommentResponse> for Oper<SaveComment> {
364 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
365 let data: &SaveComment = &self.data;
367 let claims = match Claims::decode(&data.auth) {
368 Ok(claims) => claims.claims,
369 Err(_e) => return Err(APIError::err("not_logged_in").into()),
372 let user_id = claims.id;
374 let comment_saved_form = CommentSavedForm {
375 comment_id: data.comment_id,
380 match CommentSaved::save(&conn, &comment_saved_form) {
381 Ok(comment) => comment,
382 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
385 match CommentSaved::unsave(&conn, &comment_saved_form) {
386 Ok(comment) => comment,
387 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
391 let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
394 comment: comment_view,
395 recipient_ids: Vec::new(),
400 impl Perform<CommentResponse> for Oper<CreateCommentLike> {
401 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
402 let data: &CreateCommentLike = &self.data;
404 let claims = match Claims::decode(&data.auth) {
405 Ok(claims) => claims.claims,
406 Err(_e) => return Err(APIError::err("not_logged_in").into()),
409 let user_id = claims.id;
411 let mut recipient_ids = Vec::new();
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());
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());
427 // Check for a site ban
428 if UserView::read(&conn, user_id)?.banned {
429 return Err(APIError::err("site_ban").into());
432 let comment = Comment::read(&conn, data.comment_id)?;
434 // Add to recipient ids
435 match comment.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);
444 recipient_ids.push(post.creator_id);
448 let like_form = CommentLikeForm {
449 comment_id: data.comment_id,
450 post_id: data.post_id,
455 // Remove any likes first
456 CommentLike::remove(&conn, &like_form)?;
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);
461 let _inserted_like = match CommentLike::like(&conn, &like_form) {
463 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
467 // Have to refetch the comment to get the current state
468 let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
471 comment: liked_comment,
477 impl Perform<GetCommentsResponse> for Oper<GetComments> {
478 fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
479 let data: &GetComments = &self.data;
481 let user_claims: Option<Claims> = match &data.auth {
482 Some(auth) => match Claims::decode(&auth) {
483 Ok(claims) => Some(claims.claims),
489 let user_id = match &user_claims {
490 Some(claims) => Some(claims.id),
494 let type_ = ListingType::from_str(&data.type_)?;
495 let sort = SortType::from_str(&data.sort)?;
497 let comments = match CommentQueryBuilder::create(&conn)
500 .for_community_id(data.community_id)
506 Ok(comments) => comments,
507 Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
510 Ok(GetCommentsResponse { comments })