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,
102 ap_id: "changeme".into(),
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()),
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()),
116 let mut recipient_ids = Vec::new();
118 // Scan the comment for user mentions, add those rows
119 let extracted_usernames = extract_usernames(&comment_form.content);
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);
129 let user_mention_form = UserMentionForm {
130 recipient_id: mention_user.id,
131 comment_id: inserted_comment.id,
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) {
139 Err(_e) => error!("{}", &_e),
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,
151 "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
152 claims.username, comment_form.content, hostname
154 match send_email(subject, &mention_email, &mention_user.name, html) {
156 Err(e) => error!("{}", e),
164 // Send notifs to the parent commenter / poster
165 match data.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);
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,
180 "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
181 claims.username, comment_form.content, hostname
183 match send_email(subject, &comment_reply_email, &parent_user.name, html) {
185 Err(e) => error!("{}", e),
193 if post.creator_id != user_id {
194 let parent_user = User_::read(&conn, post.creator_id)?;
195 recipient_ids.push(parent_user.id);
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,
205 "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
206 claims.username, comment_form.content, hostname
208 match send_email(subject, &post_reply_email, &parent_user.name, html) {
210 Err(e) => error!("{}", e),
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,
226 let _inserted_like = match CommentLike::like(&conn, &like_form) {
228 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
231 let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
234 comment: comment_view,
240 impl Perform<CommentResponse> for Oper<EditComment> {
241 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
242 let data: &EditComment = &self.data;
244 let claims = match Claims::decode(&data.auth) {
245 Ok(claims) => claims.claims,
246 Err(_e) => return Err(APIError::err("not_logged_in").into()),
249 let user_id = claims.id;
251 let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
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];
258 &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)?
263 editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
265 if !editors.contains(&user_id) {
266 return Err(APIError::err("no_comment_edit_allowed").into());
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());
274 // Check for a site ban
275 if UserView::read(&conn, user_id)?.banned {
276 return Err(APIError::err("site_ban").into());
280 let content_slurs_removed = remove_slurs(&data.content.to_owned());
282 let read_comment = Comment::read(&conn, data.edit_id)?;
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() {
297 ap_id: read_comment.ap_id,
298 local: read_comment.local,
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()),
306 let mut recipient_ids = Vec::new();
308 // Scan the comment for user mentions, add those rows
309 let extracted_usernames = extract_usernames(&comment_form.content);
311 for username_mention in &extracted_usernames {
312 let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
314 if mention_user.is_ok() {
315 let mention_user_id = mention_user?.id;
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);
323 let user_mention_form = UserMentionForm {
324 recipient_id: mention_user_id,
325 comment_id: data.edit_id,
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) {
333 Err(_e) => error!("{}", &_e),
339 // Add to recipient ids
340 match data.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);
349 let post = Post::read(&conn, data.post_id)?;
350 recipient_ids.push(post.creator_id);
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(),
362 ModRemoveComment::create(&conn, &form)?;
365 let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
368 comment: comment_view,
374 impl Perform<CommentResponse> for Oper<SaveComment> {
375 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
376 let data: &SaveComment = &self.data;
378 let claims = match Claims::decode(&data.auth) {
379 Ok(claims) => claims.claims,
380 Err(_e) => return Err(APIError::err("not_logged_in").into()),
383 let user_id = claims.id;
385 let comment_saved_form = CommentSavedForm {
386 comment_id: data.comment_id,
391 match CommentSaved::save(&conn, &comment_saved_form) {
392 Ok(comment) => comment,
393 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
396 match CommentSaved::unsave(&conn, &comment_saved_form) {
397 Ok(comment) => comment,
398 Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
402 let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
405 comment: comment_view,
406 recipient_ids: Vec::new(),
411 impl Perform<CommentResponse> for Oper<CreateCommentLike> {
412 fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
413 let data: &CreateCommentLike = &self.data;
415 let claims = match Claims::decode(&data.auth) {
416 Ok(claims) => claims.claims,
417 Err(_e) => return Err(APIError::err("not_logged_in").into()),
420 let user_id = claims.id;
422 let mut recipient_ids = Vec::new();
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());
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());
438 // Check for a site ban
439 if UserView::read(&conn, user_id)?.banned {
440 return Err(APIError::err("site_ban").into());
443 let comment = Comment::read(&conn, data.comment_id)?;
445 // Add to recipient ids
446 match comment.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);
455 recipient_ids.push(post.creator_id);
459 let like_form = CommentLikeForm {
460 comment_id: data.comment_id,
461 post_id: data.post_id,
466 // Remove any likes first
467 CommentLike::remove(&conn, &like_form)?;
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);
472 let _inserted_like = match CommentLike::like(&conn, &like_form) {
474 Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
478 // Have to refetch the comment to get the current state
479 let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
482 comment: liked_comment,
488 impl Perform<GetCommentsResponse> for Oper<GetComments> {
489 fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
490 let data: &GetComments = &self.data;
492 let user_claims: Option<Claims> = match &data.auth {
493 Some(auth) => match Claims::decode(&auth) {
494 Ok(claims) => Some(claims.claims),
500 let user_id = match &user_claims {
501 Some(claims) => Some(claims.id),
505 let type_ = ListingType::from_str(&data.type_)?;
506 let sort = SortType::from_str(&data.sort)?;
508 let comments = match CommentQueryBuilder::create(&conn)
511 .for_community_id(data.community_id)
517 Ok(comments) => comments,
518 Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
521 Ok(GetCommentsResponse { comments })