3 check_downvotes_enabled,
4 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{ApubLikeableType, ApubObjectType};
13 use lemmy_db_queries::{
14 source::comment::Comment_,
22 use lemmy_db_schema::source::{comment::*, comment_report::*, moderator::*};
24 comment_report_view::{CommentReportQueryBuilder, CommentReportView},
25 comment_view::{CommentQueryBuilder, CommentView},
27 use lemmy_structs::{blocking, comment::*, send_local_notifs};
29 apub::{make_apub_endpoint, EndpointType},
30 utils::{remove_slurs, scrape_text_for_mentions},
35 use lemmy_websocket::{
36 messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
40 use std::str::FromStr;
42 #[async_trait::async_trait(?Send)]
43 impl Perform for CreateComment {
44 type Response = CommentResponse;
48 context: &Data<LemmyContext>,
49 websocket_id: Option<ConnectionId>,
50 ) -> Result<CommentResponse, LemmyError> {
51 let data: &CreateComment = &self;
52 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
54 let content_slurs_removed = remove_slurs(&data.content.to_owned());
56 // Check for a community ban
57 let post_id = data.post_id;
58 let post = get_post(post_id, context.pool()).await?;
60 check_community_ban(user.id, post.community_id, context.pool()).await?;
62 // Check if post is locked, no new comments
64 return Err(APIError::err("locked").into());
67 let comment_form = CommentForm {
68 content: content_slurs_removed,
69 parent_id: data.parent_id.to_owned(),
70 post_id: data.post_id,
82 let comment_form2 = comment_form.clone();
83 let inserted_comment = match blocking(context.pool(), move |conn| {
84 Comment::create(&conn, &comment_form2)
88 Ok(comment) => comment,
89 Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
92 // Necessary to update the ap_id
93 let inserted_comment_id = inserted_comment.id;
94 let updated_comment: Comment = match blocking(context.pool(), move |conn| {
96 make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
97 Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
101 Ok(comment) => comment,
102 Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
105 updated_comment.send_create(&user, context).await?;
107 // Scan the comment for user mentions, add those rows
108 let post_id = post.id;
109 let mentions = scrape_text_for_mentions(&comment_form.content);
110 let recipient_ids = send_local_notifs(
112 updated_comment.clone(),
120 // You like your own comment by default
121 let like_form = CommentLikeForm {
122 comment_id: inserted_comment.id,
128 let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
129 if blocking(context.pool(), like).await?.is_err() {
130 return Err(APIError::err("couldnt_like_comment").into());
133 updated_comment.send_like(&user, context).await?;
135 let user_id = user.id;
136 let mut comment_view = blocking(context.pool(), move |conn| {
137 CommentView::read(&conn, inserted_comment.id, Some(user_id))
141 // If its a comment to yourself, mark it as read
142 let comment_id = comment_view.comment.id;
143 if user.id == comment_view.get_recipient_id() {
144 match blocking(context.pool(), move |conn| {
145 Comment::update_read(conn, comment_id, true)
149 Ok(comment) => comment,
150 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
152 comment_view.comment.read = true;
155 let mut res = CommentResponse {
158 form_id: data.form_id.to_owned(),
161 context.chat_server().do_send(SendComment {
162 op: UserOperation::CreateComment,
163 comment: res.clone(),
167 res.recipient_ids = Vec::new(); // Necessary to avoid doubles
173 #[async_trait::async_trait(?Send)]
174 impl Perform for EditComment {
175 type Response = CommentResponse;
179 context: &Data<LemmyContext>,
180 websocket_id: Option<ConnectionId>,
181 ) -> Result<CommentResponse, LemmyError> {
182 let data: &EditComment = &self;
183 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
185 let comment_id = data.comment_id;
186 let orig_comment = blocking(context.pool(), move |conn| {
187 CommentView::read(&conn, comment_id, None)
191 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
193 // Verify that only the creator can edit
194 if user.id != orig_comment.creator.id {
195 return Err(APIError::err("no_comment_edit_allowed").into());
199 let content_slurs_removed = remove_slurs(&data.content.to_owned());
200 let comment_id = data.comment_id;
201 let updated_comment = match blocking(context.pool(), move |conn| {
202 Comment::update_content(conn, comment_id, &content_slurs_removed)
206 Ok(comment) => comment,
207 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
210 // Send the apub update
211 updated_comment.send_update(&user, context).await?;
213 // Do the mentions / recipients
214 let updated_comment_content = updated_comment.content.to_owned();
215 let mentions = scrape_text_for_mentions(&updated_comment_content);
216 let recipient_ids = send_local_notifs(
226 let comment_id = data.comment_id;
227 let user_id = user.id;
228 let comment_view = blocking(context.pool(), move |conn| {
229 CommentView::read(conn, comment_id, Some(user_id))
233 let res = CommentResponse {
236 form_id: data.form_id.to_owned(),
239 context.chat_server().do_send(SendComment {
240 op: UserOperation::EditComment,
241 comment: res.clone(),
249 #[async_trait::async_trait(?Send)]
250 impl Perform for DeleteComment {
251 type Response = CommentResponse;
255 context: &Data<LemmyContext>,
256 websocket_id: Option<ConnectionId>,
257 ) -> Result<CommentResponse, LemmyError> {
258 let data: &DeleteComment = &self;
259 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
261 let comment_id = data.comment_id;
262 let orig_comment = blocking(context.pool(), move |conn| {
263 CommentView::read(&conn, comment_id, None)
267 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
269 // Verify that only the creator can delete
270 if user.id != orig_comment.creator.id {
271 return Err(APIError::err("no_comment_edit_allowed").into());
275 let deleted = data.deleted;
276 let updated_comment = match blocking(context.pool(), move |conn| {
277 Comment::update_deleted(conn, comment_id, deleted)
281 Ok(comment) => comment,
282 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
285 // Send the apub message
287 updated_comment.send_delete(&user, context).await?;
289 updated_comment.send_undo_delete(&user, context).await?;
293 let comment_id = data.comment_id;
294 let user_id = user.id;
295 let comment_view = blocking(context.pool(), move |conn| {
296 CommentView::read(conn, comment_id, Some(user_id))
300 // Build the recipients
301 let comment_view_2 = comment_view.clone();
302 let mentions = vec![];
303 let recipient_ids = send_local_notifs(
313 let res = CommentResponse {
316 form_id: None, // TODO a comment delete might clear forms?
319 context.chat_server().do_send(SendComment {
320 op: UserOperation::DeleteComment,
321 comment: res.clone(),
329 #[async_trait::async_trait(?Send)]
330 impl Perform for RemoveComment {
331 type Response = CommentResponse;
335 context: &Data<LemmyContext>,
336 websocket_id: Option<ConnectionId>,
337 ) -> Result<CommentResponse, LemmyError> {
338 let data: &RemoveComment = &self;
339 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
341 let comment_id = data.comment_id;
342 let orig_comment = blocking(context.pool(), move |conn| {
343 CommentView::read(&conn, comment_id, None)
347 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
349 // Verify that only a mod or admin can remove
350 is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
353 let removed = data.removed;
354 let updated_comment = match blocking(context.pool(), move |conn| {
355 Comment::update_removed(conn, comment_id, removed)
359 Ok(comment) => comment,
360 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
364 let form = ModRemoveCommentForm {
365 mod_user_id: user.id,
366 comment_id: data.comment_id,
367 removed: Some(removed),
368 reason: data.reason.to_owned(),
370 blocking(context.pool(), move |conn| {
371 ModRemoveComment::create(conn, &form)
375 // Send the apub message
377 updated_comment.send_remove(&user, context).await?;
379 updated_comment.send_undo_remove(&user, context).await?;
383 let comment_id = data.comment_id;
384 let user_id = user.id;
385 let comment_view = blocking(context.pool(), move |conn| {
386 CommentView::read(conn, comment_id, Some(user_id))
390 // Build the recipients
391 let comment_view_2 = comment_view.clone();
393 let mentions = vec![];
394 let recipient_ids = send_local_notifs(
404 let res = CommentResponse {
407 form_id: None, // TODO maybe this might clear other forms
410 context.chat_server().do_send(SendComment {
411 op: UserOperation::RemoveComment,
412 comment: res.clone(),
420 #[async_trait::async_trait(?Send)]
421 impl Perform for MarkCommentAsRead {
422 type Response = CommentResponse;
426 context: &Data<LemmyContext>,
427 _websocket_id: Option<ConnectionId>,
428 ) -> Result<CommentResponse, LemmyError> {
429 let data: &MarkCommentAsRead = &self;
430 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
432 let comment_id = data.comment_id;
433 let orig_comment = blocking(context.pool(), move |conn| {
434 CommentView::read(&conn, comment_id, None)
438 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
440 // Verify that only the recipient can mark as read
441 if user.id != orig_comment.get_recipient_id() {
442 return Err(APIError::err("no_comment_edit_allowed").into());
445 // Do the mark as read
446 let read = data.read;
447 match blocking(context.pool(), move |conn| {
448 Comment::update_read(conn, comment_id, read)
452 Ok(comment) => comment,
453 Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
457 let comment_id = data.comment_id;
458 let user_id = user.id;
459 let comment_view = blocking(context.pool(), move |conn| {
460 CommentView::read(conn, comment_id, Some(user_id))
464 let res = CommentResponse {
466 recipient_ids: Vec::new(),
474 #[async_trait::async_trait(?Send)]
475 impl Perform for SaveComment {
476 type Response = CommentResponse;
480 context: &Data<LemmyContext>,
481 _websocket_id: Option<ConnectionId>,
482 ) -> Result<CommentResponse, LemmyError> {
483 let data: &SaveComment = &self;
484 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
486 let comment_saved_form = CommentSavedForm {
487 comment_id: data.comment_id,
492 let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
493 if blocking(context.pool(), save_comment).await?.is_err() {
494 return Err(APIError::err("couldnt_save_comment").into());
497 let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
498 if blocking(context.pool(), unsave_comment).await?.is_err() {
499 return Err(APIError::err("couldnt_save_comment").into());
503 let comment_id = data.comment_id;
504 let user_id = user.id;
505 let comment_view = blocking(context.pool(), move |conn| {
506 CommentView::read(conn, comment_id, Some(user_id))
512 recipient_ids: Vec::new(),
518 #[async_trait::async_trait(?Send)]
519 impl Perform for CreateCommentLike {
520 type Response = CommentResponse;
524 context: &Data<LemmyContext>,
525 websocket_id: Option<ConnectionId>,
526 ) -> Result<CommentResponse, LemmyError> {
527 let data: &CreateCommentLike = &self;
528 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
530 let mut recipient_ids = Vec::new();
532 // Don't do a downvote if site has downvotes disabled
533 check_downvotes_enabled(data.score, context.pool()).await?;
535 let comment_id = data.comment_id;
536 let orig_comment = blocking(context.pool(), move |conn| {
537 CommentView::read(&conn, comment_id, None)
541 check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
543 // Add parent user to recipients
544 recipient_ids.push(orig_comment.get_recipient_id());
546 let like_form = CommentLikeForm {
547 comment_id: data.comment_id,
548 post_id: orig_comment.post.id,
553 // Remove any likes first
554 let user_id = user.id;
555 blocking(context.pool(), move |conn| {
556 CommentLike::remove(conn, user_id, comment_id)
560 // Only add the like if the score isnt 0
561 let comment = orig_comment.comment;
562 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
564 let like_form2 = like_form.clone();
565 let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
566 if blocking(context.pool(), like).await?.is_err() {
567 return Err(APIError::err("couldnt_like_comment").into());
570 if like_form.score == 1 {
571 comment.send_like(&user, context).await?;
572 } else if like_form.score == -1 {
573 comment.send_dislike(&user, context).await?;
576 comment.send_undo_like(&user, context).await?;
579 // Have to refetch the comment to get the current state
580 let comment_id = data.comment_id;
581 let user_id = user.id;
582 let liked_comment = blocking(context.pool(), move |conn| {
583 CommentView::read(conn, comment_id, Some(user_id))
587 let res = CommentResponse {
588 comment_view: liked_comment,
593 context.chat_server().do_send(SendComment {
594 op: UserOperation::CreateCommentLike,
595 comment: res.clone(),
603 #[async_trait::async_trait(?Send)]
604 impl Perform for GetComments {
605 type Response = GetCommentsResponse;
609 context: &Data<LemmyContext>,
610 _websocket_id: Option<ConnectionId>,
611 ) -> Result<GetCommentsResponse, LemmyError> {
612 let data: &GetComments = &self;
613 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
614 let user_id = user.map(|u| u.id);
616 let type_ = ListingType::from_str(&data.type_)?;
617 let sort = SortType::from_str(&data.sort)?;
619 let community_id = data.community_id;
620 let community_name = data.community_name.to_owned();
621 let page = data.page;
622 let limit = data.limit;
623 let comments = blocking(context.pool(), move |conn| {
624 CommentQueryBuilder::create(conn)
627 .community_id(community_id)
628 .community_name(community_name)
635 let comments = match comments {
636 Ok(comments) => comments,
637 Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
640 Ok(GetCommentsResponse { comments })
644 /// Creates a comment report and notifies the moderators of the community
645 #[async_trait::async_trait(?Send)]
646 impl Perform for CreateCommentReport {
647 type Response = CreateCommentReportResponse;
651 context: &Data<LemmyContext>,
652 websocket_id: Option<ConnectionId>,
653 ) -> Result<CreateCommentReportResponse, LemmyError> {
654 let data: &CreateCommentReport = &self;
655 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
657 // check size of report and check for whitespace
658 let reason = data.reason.trim();
659 if reason.is_empty() {
660 return Err(APIError::err("report_reason_required").into());
662 if reason.len() > 1000 {
663 return Err(APIError::err("report_too_long").into());
666 let user_id = user.id;
667 let comment_id = data.comment_id;
668 let comment_view = blocking(context.pool(), move |conn| {
669 CommentView::read(&conn, comment_id, None)
673 check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
675 let report_form = CommentReportForm {
678 original_comment_text: comment_view.comment.content,
679 reason: data.reason.to_owned(),
682 let report = match blocking(context.pool(), move |conn| {
683 CommentReport::report(conn, &report_form)
687 Ok(report) => report,
688 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
691 let res = CreateCommentReportResponse { success: true };
693 context.chat_server().do_send(SendUserRoomMessage {
694 op: UserOperation::CreateCommentReport,
695 response: res.clone(),
696 recipient_id: user.id,
700 context.chat_server().do_send(SendModRoomMessage {
701 op: UserOperation::CreateCommentReport,
703 community_id: comment_view.community.id,
711 /// Resolves or unresolves a comment report and notifies the moderators of the community
712 #[async_trait::async_trait(?Send)]
713 impl Perform for ResolveCommentReport {
714 type Response = ResolveCommentReportResponse;
718 context: &Data<LemmyContext>,
719 websocket_id: Option<ConnectionId>,
720 ) -> Result<ResolveCommentReportResponse, LemmyError> {
721 let data: &ResolveCommentReport = &self;
722 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
724 let report_id = data.report_id;
725 let report = blocking(context.pool(), move |conn| {
726 CommentReportView::read(&conn, report_id)
730 let user_id = user.id;
731 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
733 let resolved = data.resolved;
734 let resolve_fun = move |conn: &'_ _| {
736 CommentReport::resolve(conn, report_id, user_id)
738 CommentReport::unresolve(conn, report_id, user_id)
742 if blocking(context.pool(), resolve_fun).await?.is_err() {
743 return Err(APIError::err("couldnt_resolve_report").into());
746 let report_id = data.report_id;
747 let res = ResolveCommentReportResponse {
752 context.chat_server().do_send(SendModRoomMessage {
753 op: UserOperation::ResolveCommentReport,
754 response: res.clone(),
755 community_id: report.community.id,
763 /// Lists comment reports for a community if an id is supplied
764 /// or returns all comment reports for communities a user moderates
765 #[async_trait::async_trait(?Send)]
766 impl Perform for ListCommentReports {
767 type Response = ListCommentReportsResponse;
771 context: &Data<LemmyContext>,
772 websocket_id: Option<ConnectionId>,
773 ) -> Result<ListCommentReportsResponse, LemmyError> {
774 let data: &ListCommentReports = &self;
775 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
777 let user_id = user.id;
778 let community_id = data.community;
780 collect_moderated_communities(user_id, community_id, context.pool()).await?;
782 let page = data.page;
783 let limit = data.limit;
784 let comments = blocking(context.pool(), move |conn| {
785 CommentReportQueryBuilder::create(conn)
786 .community_ids(community_ids)
793 let res = ListCommentReportsResponse { comments };
795 context.chat_server().do_send(SendUserRoomMessage {
796 op: UserOperation::ListCommentReports,
797 response: res.clone(),
798 recipient_id: user.id,