3 check_downvotes_enabled,
5 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{ApubLikeableType, ApubObjectType};
16 comment_view::CommentQueryBuilder,
17 community::community_moderator_view::CommunityModeratorView,
18 post_report_view::{PostReportQueryBuilder, PostReportView},
19 post_view::{PostQueryBuilder, PostView},
28 use lemmy_db_schema::{
33 post_report::{PostReport, PostReportForm},
36 use lemmy_structs::{blocking, post::*};
38 apub::{make_apub_endpoint, EndpointType},
39 request::fetch_iframely_and_pictrs_data,
40 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
45 use lemmy_websocket::{
46 messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage},
50 use std::str::FromStr;
52 #[async_trait::async_trait(?Send)]
53 impl Perform for CreatePost {
54 type Response = PostResponse;
58 context: &Data<LemmyContext>,
59 websocket_id: Option<ConnectionId>,
60 ) -> Result<PostResponse, LemmyError> {
61 let data: &CreatePost = &self;
62 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
64 check_slurs(&data.name)?;
65 check_slurs_opt(&data.body)?;
67 if !is_valid_post_title(&data.name) {
68 return Err(APIError::err("invalid_post_title").into());
71 check_community_ban(user.id, data.community_id, context.pool()).await?;
73 check_optional_url(&Some(data.url.to_owned()))?;
75 // Fetch Iframely and pictrs cached image
76 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
77 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
79 let post_form = PostForm {
80 name: data.name.trim().to_owned(),
81 url: data.url.to_owned(),
82 body: data.body.to_owned(),
83 community_id: data.community_id,
91 embed_title: iframely_title,
92 embed_description: iframely_description,
93 embed_html: iframely_html,
94 thumbnail_url: pictrs_thumbnail,
101 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
104 let err_type = if e.to_string() == "value too long for type character varying(200)" {
105 "post_title_too_long"
107 "couldnt_create_post"
110 return Err(APIError::err(err_type).into());
114 let inserted_post_id = inserted_post.id;
115 let updated_post = match blocking(context.pool(), move |conn| {
117 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
118 Post::update_ap_id(conn, inserted_post_id, apub_id)
123 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
126 updated_post.send_create(&user, context).await?;
128 // They like their own post by default
129 let like_form = PostLikeForm {
130 post_id: inserted_post.id,
135 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
136 if blocking(context.pool(), like).await?.is_err() {
137 return Err(APIError::err("couldnt_like_post").into());
140 updated_post.send_like(&user, context).await?;
143 let inserted_post_id = inserted_post.id;
144 let post_view = match blocking(context.pool(), move |conn| {
145 PostView::read(conn, inserted_post_id, Some(user.id))
150 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
153 let res = PostResponse { post_view };
155 context.chat_server().do_send(SendPost {
156 op: UserOperation::CreatePost,
165 #[async_trait::async_trait(?Send)]
166 impl Perform for GetPost {
167 type Response = GetPostResponse;
171 context: &Data<LemmyContext>,
172 _websocket_id: Option<ConnectionId>,
173 ) -> Result<GetPostResponse, LemmyError> {
174 let data: &GetPost = &self;
175 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
176 let user_id = user.map(|u| u.id);
179 let post_view = match blocking(context.pool(), move |conn| {
180 PostView::read(conn, id, user_id)
185 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
189 let comments = blocking(context.pool(), move |conn| {
190 CommentQueryBuilder::create(conn)
198 let community_id = post_view.community.id;
199 let moderators = blocking(context.pool(), move |conn| {
200 CommunityModeratorView::for_community(conn, community_id)
206 .send(GetPostUsersOnline { post_id: data.id })
220 #[async_trait::async_trait(?Send)]
221 impl Perform for GetPosts {
222 type Response = GetPostsResponse;
226 context: &Data<LemmyContext>,
227 _websocket_id: Option<ConnectionId>,
228 ) -> Result<GetPostsResponse, LemmyError> {
229 let data: &GetPosts = &self;
230 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
232 let user_id = match &user {
233 Some(user) => Some(user.id),
237 let show_nsfw = match &user {
238 Some(user) => user.show_nsfw,
242 let type_ = ListingType::from_str(&data.type_)?;
243 let sort = SortType::from_str(&data.sort)?;
245 let page = data.page;
246 let limit = data.limit;
247 let community_id = data.community_id;
248 let community_name = data.community_name.to_owned();
249 let posts = match blocking(context.pool(), move |conn| {
250 PostQueryBuilder::create(conn)
251 .listing_type(&type_)
253 .show_nsfw(show_nsfw)
254 .community_id(community_id)
255 .community_name(community_name)
264 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
267 Ok(GetPostsResponse { posts })
271 #[async_trait::async_trait(?Send)]
272 impl Perform for CreatePostLike {
273 type Response = PostResponse;
277 context: &Data<LemmyContext>,
278 websocket_id: Option<ConnectionId>,
279 ) -> Result<PostResponse, LemmyError> {
280 let data: &CreatePostLike = &self;
281 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
283 // Don't do a downvote if site has downvotes disabled
284 check_downvotes_enabled(data.score, context.pool()).await?;
286 // Check for a community ban
287 let post_id = data.post_id;
288 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
290 check_community_ban(user.id, post.community_id, context.pool()).await?;
292 let like_form = PostLikeForm {
293 post_id: data.post_id,
298 // Remove any likes first
299 let user_id = user.id;
300 blocking(context.pool(), move |conn| {
301 PostLike::remove(conn, user_id, post_id)
305 // Only add the like if the score isnt 0
306 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
308 let like_form2 = like_form.clone();
309 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
310 if blocking(context.pool(), like).await?.is_err() {
311 return Err(APIError::err("couldnt_like_post").into());
314 if like_form.score == 1 {
315 post.send_like(&user, context).await?;
316 } else if like_form.score == -1 {
317 post.send_dislike(&user, context).await?;
320 post.send_undo_like(&user, context).await?;
323 let post_id = data.post_id;
324 let user_id = user.id;
325 let post_view = match blocking(context.pool(), move |conn| {
326 PostView::read(conn, post_id, Some(user_id))
331 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
334 let res = PostResponse { post_view };
336 context.chat_server().do_send(SendPost {
337 op: UserOperation::CreatePostLike,
346 #[async_trait::async_trait(?Send)]
347 impl Perform for EditPost {
348 type Response = PostResponse;
352 context: &Data<LemmyContext>,
353 websocket_id: Option<ConnectionId>,
354 ) -> Result<PostResponse, LemmyError> {
355 let data: &EditPost = &self;
356 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
358 check_slurs(&data.name)?;
359 check_slurs_opt(&data.body)?;
361 if !is_valid_post_title(&data.name) {
362 return Err(APIError::err("invalid_post_title").into());
365 let edit_id = data.edit_id;
366 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
368 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
370 // Verify that only the creator can edit
371 if !Post::is_post_creator(user.id, orig_post.creator_id) {
372 return Err(APIError::err("no_post_edit_allowed").into());
375 // Fetch Iframely and Pictrs cached image
376 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
377 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
379 let post_form = PostForm {
380 name: data.name.trim().to_owned(),
381 url: data.url.to_owned(),
382 body: data.body.to_owned(),
384 creator_id: orig_post.creator_id.to_owned(),
385 community_id: orig_post.community_id,
386 removed: Some(orig_post.removed),
387 deleted: Some(orig_post.deleted),
388 locked: Some(orig_post.locked),
389 stickied: Some(orig_post.stickied),
390 updated: Some(naive_now()),
391 embed_title: iframely_title,
392 embed_description: iframely_description,
393 embed_html: iframely_html,
394 thumbnail_url: pictrs_thumbnail,
395 ap_id: Some(orig_post.ap_id),
396 local: orig_post.local,
400 let edit_id = data.edit_id;
401 let res = blocking(context.pool(), move |conn| {
402 Post::update(conn, edit_id, &post_form)
405 let updated_post: Post = match res {
408 let err_type = if e.to_string() == "value too long for type character varying(200)" {
409 "post_title_too_long"
411 "couldnt_update_post"
414 return Err(APIError::err(err_type).into());
419 updated_post.send_update(&user, context).await?;
421 let edit_id = data.edit_id;
422 let post_view = blocking(context.pool(), move |conn| {
423 PostView::read(conn, edit_id, Some(user.id))
427 let res = PostResponse { post_view };
429 context.chat_server().do_send(SendPost {
430 op: UserOperation::EditPost,
439 #[async_trait::async_trait(?Send)]
440 impl Perform for DeletePost {
441 type Response = PostResponse;
445 context: &Data<LemmyContext>,
446 websocket_id: Option<ConnectionId>,
447 ) -> Result<PostResponse, LemmyError> {
448 let data: &DeletePost = &self;
449 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
451 let edit_id = data.edit_id;
452 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
454 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
456 // Verify that only the creator can delete
457 if !Post::is_post_creator(user.id, orig_post.creator_id) {
458 return Err(APIError::err("no_post_edit_allowed").into());
462 let edit_id = data.edit_id;
463 let deleted = data.deleted;
464 let updated_post = blocking(context.pool(), move |conn| {
465 Post::update_deleted(conn, edit_id, deleted)
471 updated_post.send_delete(&user, context).await?;
473 updated_post.send_undo_delete(&user, context).await?;
477 let edit_id = data.edit_id;
478 let post_view = blocking(context.pool(), move |conn| {
479 PostView::read(conn, edit_id, Some(user.id))
483 let res = PostResponse { post_view };
485 context.chat_server().do_send(SendPost {
486 op: UserOperation::DeletePost,
495 #[async_trait::async_trait(?Send)]
496 impl Perform for RemovePost {
497 type Response = PostResponse;
501 context: &Data<LemmyContext>,
502 websocket_id: Option<ConnectionId>,
503 ) -> Result<PostResponse, LemmyError> {
504 let data: &RemovePost = &self;
505 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
507 let edit_id = data.edit_id;
508 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
510 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
512 // Verify that only the mods can remove
513 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
516 let edit_id = data.edit_id;
517 let removed = data.removed;
518 let updated_post = blocking(context.pool(), move |conn| {
519 Post::update_removed(conn, edit_id, removed)
524 let form = ModRemovePostForm {
525 mod_user_id: user.id,
526 post_id: data.edit_id,
527 removed: Some(removed),
528 reason: data.reason.to_owned(),
530 blocking(context.pool(), move |conn| {
531 ModRemovePost::create(conn, &form)
537 updated_post.send_remove(&user, context).await?;
539 updated_post.send_undo_remove(&user, context).await?;
543 let edit_id = data.edit_id;
544 let user_id = user.id;
545 let post_view = blocking(context.pool(), move |conn| {
546 PostView::read(conn, edit_id, Some(user_id))
550 let res = PostResponse { post_view };
552 context.chat_server().do_send(SendPost {
553 op: UserOperation::RemovePost,
562 #[async_trait::async_trait(?Send)]
563 impl Perform for LockPost {
564 type Response = PostResponse;
568 context: &Data<LemmyContext>,
569 websocket_id: Option<ConnectionId>,
570 ) -> Result<PostResponse, LemmyError> {
571 let data: &LockPost = &self;
572 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
574 let edit_id = data.edit_id;
575 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
577 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
579 // Verify that only the mods can lock
580 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
583 let edit_id = data.edit_id;
584 let locked = data.locked;
585 let updated_post = blocking(context.pool(), move |conn| {
586 Post::update_locked(conn, edit_id, locked)
591 let form = ModLockPostForm {
592 mod_user_id: user.id,
593 post_id: data.edit_id,
594 locked: Some(locked),
596 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
599 updated_post.send_update(&user, context).await?;
602 let edit_id = data.edit_id;
603 let post_view = blocking(context.pool(), move |conn| {
604 PostView::read(conn, edit_id, Some(user.id))
608 let res = PostResponse { post_view };
610 context.chat_server().do_send(SendPost {
611 op: UserOperation::LockPost,
620 #[async_trait::async_trait(?Send)]
621 impl Perform for StickyPost {
622 type Response = PostResponse;
626 context: &Data<LemmyContext>,
627 websocket_id: Option<ConnectionId>,
628 ) -> Result<PostResponse, LemmyError> {
629 let data: &StickyPost = &self;
630 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
632 let edit_id = data.edit_id;
633 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
635 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
637 // Verify that only the mods can sticky
638 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
641 let edit_id = data.edit_id;
642 let stickied = data.stickied;
643 let updated_post = blocking(context.pool(), move |conn| {
644 Post::update_stickied(conn, edit_id, stickied)
649 let form = ModStickyPostForm {
650 mod_user_id: user.id,
651 post_id: data.edit_id,
652 stickied: Some(stickied),
654 blocking(context.pool(), move |conn| {
655 ModStickyPost::create(conn, &form)
660 // TODO stickied should pry work like locked for ease of use
661 updated_post.send_update(&user, context).await?;
664 let edit_id = data.edit_id;
665 let post_view = blocking(context.pool(), move |conn| {
666 PostView::read(conn, edit_id, Some(user.id))
670 let res = PostResponse { post_view };
672 context.chat_server().do_send(SendPost {
673 op: UserOperation::StickyPost,
682 #[async_trait::async_trait(?Send)]
683 impl Perform for SavePost {
684 type Response = PostResponse;
688 context: &Data<LemmyContext>,
689 _websocket_id: Option<ConnectionId>,
690 ) -> Result<PostResponse, LemmyError> {
691 let data: &SavePost = &self;
692 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
694 let post_saved_form = PostSavedForm {
695 post_id: data.post_id,
700 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
701 if blocking(context.pool(), save).await?.is_err() {
702 return Err(APIError::err("couldnt_save_post").into());
705 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
706 if blocking(context.pool(), unsave).await?.is_err() {
707 return Err(APIError::err("couldnt_save_post").into());
711 let post_id = data.post_id;
712 let user_id = user.id;
713 let post_view = blocking(context.pool(), move |conn| {
714 PostView::read(conn, post_id, Some(user_id))
718 Ok(PostResponse { post_view })
722 #[async_trait::async_trait(?Send)]
723 impl Perform for PostJoin {
724 type Response = PostJoinResponse;
728 context: &Data<LemmyContext>,
729 websocket_id: Option<ConnectionId>,
730 ) -> Result<PostJoinResponse, LemmyError> {
731 let data: &PostJoin = &self;
733 if let Some(ws_id) = websocket_id {
734 context.chat_server().do_send(JoinPostRoom {
735 post_id: data.post_id,
740 Ok(PostJoinResponse { joined: true })
744 /// Creates a post report and notifies the moderators of the community
745 #[async_trait::async_trait(?Send)]
746 impl Perform for CreatePostReport {
747 type Response = CreatePostReportResponse;
751 context: &Data<LemmyContext>,
752 websocket_id: Option<ConnectionId>,
753 ) -> Result<CreatePostReportResponse, LemmyError> {
754 let data: &CreatePostReport = &self;
755 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
757 // check size of report and check for whitespace
758 let reason = data.reason.trim();
759 if reason.is_empty() {
760 return Err(APIError::err("report_reason_required").into());
762 if reason.len() > 1000 {
763 return Err(APIError::err("report_too_long").into());
766 let user_id = user.id;
767 let post_id = data.post_id;
768 let post_view = blocking(context.pool(), move |conn| {
769 PostView::read(&conn, post_id, None)
773 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
775 let report_form = PostReportForm {
778 original_post_name: post_view.post.name,
779 original_post_url: post_view.post.url,
780 original_post_body: post_view.post.body,
781 reason: data.reason.to_owned(),
784 let report = match blocking(context.pool(), move |conn| {
785 PostReport::report(conn, &report_form)
789 Ok(report) => report,
790 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
793 let res = CreatePostReportResponse { success: true };
795 context.chat_server().do_send(SendUserRoomMessage {
796 op: UserOperation::CreatePostReport,
797 response: res.clone(),
798 recipient_id: user.id,
802 context.chat_server().do_send(SendModRoomMessage {
803 op: UserOperation::CreatePostReport,
805 community_id: post_view.community.id,
813 /// Resolves or unresolves a post report and notifies the moderators of the community
814 #[async_trait::async_trait(?Send)]
815 impl Perform for ResolvePostReport {
816 type Response = ResolvePostReportResponse;
820 context: &Data<LemmyContext>,
821 websocket_id: Option<ConnectionId>,
822 ) -> Result<ResolvePostReportResponse, LemmyError> {
823 let data: &ResolvePostReport = &self;
824 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
826 let report_id = data.report_id;
827 let report = blocking(context.pool(), move |conn| {
828 PostReportView::read(&conn, report_id)
832 let user_id = user.id;
833 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
835 let resolved = data.resolved;
836 let resolve_fun = move |conn: &'_ _| {
838 PostReport::resolve(conn, report_id, user_id)
840 PostReport::unresolve(conn, report_id, user_id)
844 let res = ResolvePostReportResponse {
849 if blocking(context.pool(), resolve_fun).await?.is_err() {
850 return Err(APIError::err("couldnt_resolve_report").into());
853 context.chat_server().do_send(SendModRoomMessage {
854 op: UserOperation::ResolvePostReport,
855 response: res.clone(),
856 community_id: report.community.id,
864 /// Lists post reports for a community if an id is supplied
865 /// or returns all post reports for communities a user moderates
866 #[async_trait::async_trait(?Send)]
867 impl Perform for ListPostReports {
868 type Response = ListPostReportsResponse;
872 context: &Data<LemmyContext>,
873 websocket_id: Option<ConnectionId>,
874 ) -> Result<ListPostReportsResponse, LemmyError> {
875 let data: &ListPostReports = &self;
876 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
878 let user_id = user.id;
879 let community_id = data.community;
881 collect_moderated_communities(user_id, community_id, context.pool()).await?;
883 let page = data.page;
884 let limit = data.limit;
885 let posts = blocking(context.pool(), move |conn| {
886 PostReportQueryBuilder::create(conn)
887 .community_ids(community_ids)
894 let res = ListPostReportsResponse { posts };
896 context.chat_server().do_send(SendUserRoomMessage {
897 op: UserOperation::ListPostReports,
898 response: res.clone(),
899 recipient_id: user.id,